diff --git a/.codex b/.codex deleted file mode 100644 index e69de29bb..000000000 diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index cb406d2b4..3cd695354 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -14,9 +14,9 @@ body: label: Affected area description: Choose the module this issue mainly affects. options: - - Desktop UI (agent-gui / React) + - Desktop UI (frontend / React) - Desktop core (Tauri / Rust) - - Gateway (agent-gateway / Go) + - Backend (backend / Rust) - Agent sessions / streaming - Tool execution / MCP / Skills - Model providers diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 4245f67bc..f453e5478 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -10,9 +10,9 @@ body: label: Affected area description: Choose the module this proposal mainly involves. options: - - Desktop UI (agent-gui / React) + - Desktop UI (frontend / React) - Desktop core (Tauri / Rust) - - Gateway (agent-gateway / Go) + - Backend (backend / Rust) - Agent sessions / streaming - Tool execution / MCP / Skills - Model providers @@ -42,8 +42,8 @@ body: label: Estimated change scope description: If you plan to implement this yourself, list the modules / directories / files you expect to touch, to help assess complexity. placeholder: | - crates/agent-gui/src/... - crates/agent-gateway/internal/... + crates/frontend/src/... + crates/backend/src/... validations: required: false - type: textarea diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 3a8cd4d0f..b615467a8 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -20,7 +20,7 @@ Closes # -- Modules: +- Modules: - Key paths: ## Screenshots / preview @@ -34,7 +34,7 @@ Closes # ## Verification diff --git a/.github/workflows/backend-docker.yml b/.github/workflows/backend-docker.yml new file mode 100644 index 000000000..7791e1dd3 --- /dev/null +++ b/.github/workflows/backend-docker.yml @@ -0,0 +1,104 @@ +name: Backend Docker + +on: + push: + tags: + - "v*" + workflow_dispatch: + inputs: + tag: + description: Existing release tag to publish, for example v0.1.0 + required: true + +permissions: + contents: read + packages: write + +env: + # 新镜像名。旧的 ghcr.io//liveagent-gateway 历史 tag 保持不动、仍可拉取 + # (决策 15):我们只是不再发布新的 gateway 镜像,不删除任何已发布的 tag。 + IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/liveagent-backend + +jobs: + build-and-push: + name: Build and Push Backend Image + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ github.event.inputs.tag || github.ref }} + + - uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE_NAME }} + flavor: | + latest=false + tags: | + type=raw,value=${{ github.event.inputs.tag || github.ref_name }} + type=raw,value=latest + + # 只发 linux/amd64。Go 版 gateway 能轻松出 arm64(GOARCH 交叉编译白送), + # Rust 后端不行:依赖链里有 aws-lc-sys,交叉编译要 cmake + aarch64 工具链和 + # sysroot,Dockerfile 现在也没接 --target。宁可少发一个架构,也不发一个 + # 架构标错的镜像(见下面的验证步骤,那正是 v0.1.0–v1.1.8 踩过的坑)。 + - name: Build and push + id: push + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + platforms: linux/amd64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + provenance: false + + # 防"标着一个架构、装着另一个架构的二进制"。v0.1.0 到 v1.1.8 的镜像宣称 + # arm64 却装着 x86-64 二进制,就是这么漏出去的。加架构时这步必须跟着扩。 + - name: Verify binary architecture + run: | + set -euo pipefail + repo="$(echo "$IMAGE_NAME" | tr '[:upper:]' '[:lower:]')" + ref="$repo@${{ steps.push.outputs.digest }}" + docker pull "$ref" >/dev/null + docker rm -f verify-amd64 >/dev/null 2>&1 || true + docker create --name verify-amd64 "$ref" >/dev/null + docker cp verify-amd64:/usr/local/bin/backend /tmp/backend-amd64 + docker rm -f verify-amd64 >/dev/null + info="$(file -b /tmp/backend-amd64)" + echo "linux/amd64: $info" + echo "$info" | grep -q "x86-64" || { + echo "::error::linux/amd64 镜像里的 backend 不是 x86-64 二进制" + exit 1 + } + + # 引擎 bundle 漏进镜像的话,后端会退化成"纯 API 模式"静默跑起来 —— + # /healthz 照样返回 ok,问题要到用户发第一条消息才暴露。这里提前拦。 + - name: Verify Node engine bundle is present + run: | + set -euo pipefail + repo="$(echo "$IMAGE_NAME" | tr '[:upper:]' '[:lower:]')" + ref="$repo@${{ steps.push.outputs.digest }}" + docker rm -f verify-engine >/dev/null 2>&1 || true + docker create --name verify-engine "$ref" >/dev/null + docker cp verify-engine:/opt/liveagent/engine/index.js /tmp/engine-index.js + docker rm -f verify-engine >/dev/null + test -s /tmp/engine-index.js || { + echo "::error::镜像里的 Node 引擎 bundle 是空的" + exit 1 + } + echo "engine bundle: $(wc -c < /tmp/engine-index.js) bytes" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b20856341..0e2976104 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,65 +13,65 @@ env: NODE_VERSION: 22.19.0 jobs: - gateway: - name: Gateway + backend: + name: Backend Rust runs-on: ubuntu-latest + timeout-minutes: 20 steps: - uses: actions/checkout@v6 - with: - # buf breaking 需要完整历史以对比 origin/main 的 proto 契约。 - fetch-depth: 0 - - - uses: actions/setup-go@v5 - with: - go-version-file: crates/agent-gateway/go.mod - cache-dependency-path: crates/agent-gateway/go.sum - uses: actions/setup-node@v6 with: node-version: ${{ env.NODE_VERSION }} - - name: Install pnpm - run: npm install -g pnpm@10.32.1 + - uses: dtolnay/rust-toolchain@stable - - name: Install buf - uses: bufbuild/buf-action@v1 - with: - setup_only: true - version: 1.71.0 + - uses: Swatinem/rust-cache@v2 - - name: Install protobuf Go plugins - run: | - go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.11 - echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" + # routes_gen.rs 是脚本生成的:wrapper 变了没重新生成 → 这里失败。 + - name: Check generated routes are in sync + run: make check-routes - - name: Build embedded Gateway WebUI - working-directory: crates/agent-gateway/web - run: | - pnpm install --frozen-lockfile - pnpm build + # 事件契约真相源在 core,前端那份是逐字镜像;改一边忘改另一边 → 这里失败。 + - name: Check wire event contract mirror is in sync + run: make check-wire-events - - name: Proto lint and breaking-change check - run: make proto-check BUF_BREAKING_AGAINST='../../.git#branch=origin/main,subdir=crates/agent-gateway' + # settings 契约(任务 #9 止血带):共享逻辑两侧必须同步,只许登记过的差异。 + - name: Check settings contract has not forked + run: make check-settings-drift - - name: Check generated protobuf + # 编译期防线的 CI 版:backend 是跨壳内核,一旦它依赖 tauri, + # headless 后端就再也编不出来了。依赖链上出现 tauri 直接失败。 + - name: Ensure backend stays Tauri-free run: | - make proto - git diff --exit-code -- crates/agent-gateway/internal/proto - - - name: Lint gateway - uses: golangci/golangci-lint-action@v8 - with: - version: v2.12.2 - working-directory: crates/agent-gateway + set -euo pipefail + # 先落到变量:直接 `cargo tree | grep -q` 的话,cargo tree 自己失败会让 + # grep 收到空输入从而"通过",门禁就成了摆设。 + tree="$(cargo tree -p backend)" + if printf '%s\n' "$tree" | grep -q tauri; then + echo "::error::backend 依赖树里出现了 tauri,跨壳内核的防线破了" + printf '%s\n' "$tree" | grep tauri + exit 1 + fi + echo "backend 依赖树干净,无 tauri" + + - name: Test backend and backend + env: + CARGO_TERM_COLOR: always + run: cargo test -p backend - - name: Test gateway - working-directory: crates/agent-gateway - run: go test ./... + # 只 lint 后端这两个 crate:src-tauri 也在同一 workspace 里,用 + # --workspace 会把 GTK/WebKit 那套系统依赖拖进这个 job。src-tauri + # 由 tauri-rust job 单独覆盖。 + - name: Clippy backend and backend + env: + CARGO_TERM_COLOR: always + run: cargo clippy -p backend --all-targets -- -D warnings - gateway-docker: - name: Gateway Docker Smoke + backend-docker: + name: Backend Docker Smoke runs-on: ubuntu-latest + timeout-minutes: 30 steps: - uses: actions/checkout@v6 @@ -83,59 +83,34 @@ jobs: context: . file: Dockerfile load: true - tags: liveagent-gateway:ci + tags: liveagent-backend:ci cache-from: type=gha cache-to: type=gha,mode=max - name: Smoke test container run: | set -euo pipefail - docker rm -f liveagent-gateway-smoke >/dev/null 2>&1 || true + docker rm -f liveagent-backend-smoke >/dev/null 2>&1 || true + # 不传 --tls-cert,容器内是明文 HTTP:TLS 由外层(Railway / 反代)终结。 + # 也不传 --password,后端会自己生成一个并打到 stderr —— /healthz 不需要鉴权。 docker run -d \ - --name liveagent-gateway-smoke \ - -p 18080:8080 \ - -e LIVEAGENT_GATEWAY_TOKEN=ci-token \ - liveagent-gateway:ci - trap 'docker rm -f liveagent-gateway-smoke >/dev/null 2>&1 || true' EXIT - for _ in $(seq 1 30); do - if curl -fsS http://127.0.0.1:18080/healthz | grep -q '"ok":true'; then + --name liveagent-backend-smoke \ + -p 18443:8443 \ + liveagent-backend:ci + trap 'docker rm -f liveagent-backend-smoke >/dev/null 2>&1 || true' EXIT + # /healthz 返回纯文本 ok,且刻意在鉴权之外。Node 引擎就绪探测最多 30s, + # 这里给 60s 余量。 + for _ in $(seq 1 60); do + if curl -fsS http://127.0.0.1:18443/healthz | grep -q 'ok'; then + echo "Backend Docker smoke test passed" exit 0 fi sleep 1 done - echo "Gateway Docker smoke test failed; container logs:" - docker logs liveagent-gateway-smoke || true + echo "Backend Docker smoke test failed; container logs:" + docker logs liveagent-backend-smoke || true exit 1 - webui: - name: Gateway WebUI - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - uses: actions/setup-node@v6 - with: - node-version: ${{ env.NODE_VERSION }} - - - name: Install pnpm - run: npm install -g pnpm@10.32.1 - - - name: Install dependencies - working-directory: crates/agent-gateway/web - run: pnpm install --frozen-lockfile - - - name: Build WebUI - working-directory: crates/agent-gateway/web - run: pnpm build - - - name: Lint WebUI - working-directory: crates/agent-gateway/web - run: pnpm lint - - - name: Test WebUI modules - working-directory: crates/agent-gateway/web - run: pnpm test - gui: name: GUI runs-on: ubuntu-latest @@ -150,23 +125,29 @@ jobs: run: npm install -g pnpm@10.32.1 - name: Install dependencies - working-directory: crates/agent-gui + working-directory: crates/frontend + run: pnpm install --frozen-lockfile + + # 前端模块测试会加载 core 源码(test/ 里 localLoader 直接 import core/src), + # core 的依赖(fetch-socks 等)必须就位,否则 Cannot find module。 + - name: Install core dependencies for module tests + working-directory: crates/core run: pnpm install --frozen-lockfile - name: Typecheck and build GUI frontend - working-directory: crates/agent-gui + working-directory: crates/frontend run: pnpm build - name: Lint GUI frontend - working-directory: crates/agent-gui + working-directory: crates/frontend run: pnpm lint - name: Test frontend modules - working-directory: crates/agent-gui + working-directory: crates/frontend run: pnpm test:frontend - name: Test release scripts - working-directory: crates/agent-gui + working-directory: crates/frontend run: pnpm test:release tauri-rust: @@ -180,55 +161,56 @@ jobs: run: | sudo apt-get update sudo apt-get install -y \ - protobuf-compiler \ libwebkit2gtk-4.1-dev \ libgtk-3-dev \ libayatana-appindicator3-dev \ librsvg2-dev + - uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Install pnpm + run: npm install -g pnpm@10.32.1 + + # tauri.conf.json 把 core/dist/index.js 列进 resources,build script 会要求该文件存在。 + # 先 build core,否则 cargo check 在 liveagent build script 处失败。 + - name: Build core engine bundle + working-directory: crates/core + run: | + pnpm install --frozen-lockfile + pnpm build + - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 with: - workspaces: crates/agent-gui/src-tauri + workspaces: crates/frontend/src-tauri - name: Check Tauri backend tests env: CARGO_TERM_COLOR: always - run: cargo check --manifest-path crates/agent-gui/src-tauri/Cargo.toml --tests + run: cargo check --manifest-path crates/frontend/src-tauri/Cargo.toml --tests - name: Test Tauri history migrations env: CARGO_TERM_COLOR: always - run: cargo test --manifest-path crates/agent-gui/src-tauri/Cargo.toml chat_history --lib + run: cargo test --manifest-path crates/frontend/src-tauri/Cargo.toml chat_history --lib - name: Test SSH local forwarding env: CARGO_TERM_COLOR: always - run: cargo test --manifest-path crates/agent-gui/src-tauri/Cargo.toml ssh_local_forward --lib + run: cargo test --manifest-path crates/frontend/src-tauri/Cargo.toml ssh_local_forward --lib - name: Test Tauri shell-runner cancellation env: CARGO_TERM_COLOR: always - run: cargo test --manifest-path crates/agent-gui/src-tauri/Cargo.toml shell_runner --lib + run: cargo test --manifest-path crates/frontend/src-tauri/Cargo.toml shell_runner --lib - name: Test Tauri MCP integration env: CARGO_TERM_COLOR: always - run: cargo test --manifest-path crates/agent-gui/src-tauri/Cargo.toml integration_commands::mcp --lib - - mirror: - name: GUI/WebUI Mirror Check - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - uses: actions/setup-node@v6 - with: - node-version: ${{ env.NODE_VERSION }} - - - name: Check mirrored files are byte-identical - run: node scripts/check-mirror.mjs + run: cargo test --manifest-path crates/frontend/src-tauri/Cargo.toml integration_commands::mcp --lib whitespace: name: Diff Hygiene diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index 04b6a5c05..0c05ffd54 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -67,7 +67,7 @@ jobs: node-version: ${{ env.NODE_VERSION }} - name: Prepare release version config - working-directory: crates/agent-gui + working-directory: crates/frontend run: node ../../scripts/release/prepare-app-version-from-tag.mjs "$LIVEAGENT_RELEASE_TAG" --github-env "$GITHUB_ENV" --tauri-config src-tauri/tauri.version.generated.conf.json - name: Install pnpm @@ -77,15 +77,12 @@ jobs: with: targets: ${{ matrix.target }} - - name: Install protobuf compiler - run: brew install protobuf - - uses: Swatinem/rust-cache@v2 with: - workspaces: crates/agent-gui/src-tauri + workspaces: crates/frontend/src-tauri - name: Install frontend dependencies - working-directory: crates/agent-gui + working-directory: crates/frontend run: pnpm install --frozen-lockfile - name: Import Apple signing certificate @@ -173,7 +170,7 @@ jobs: - name: Prepare release version config shell: bash - working-directory: crates/agent-gui + working-directory: crates/frontend run: node ../../scripts/release/prepare-app-version-from-tag.mjs "$LIVEAGENT_RELEASE_TAG" --github-env "$GITHUB_ENV" --tauri-config src-tauri/tauri.version.generated.conf.json - name: Install pnpm @@ -181,21 +178,16 @@ jobs: - uses: dtolnay/rust-toolchain@stable - - name: Install protobuf compiler - run: | - choco install protoc -y --no-progress - "$env:ChocolateyInstall\lib\protoc\tools\bin" | Out-File -Append $env:GITHUB_PATH - - uses: Swatinem/rust-cache@v2 with: - workspaces: crates/agent-gui/src-tauri + workspaces: crates/frontend/src-tauri - name: Install frontend dependencies - working-directory: crates/agent-gui + working-directory: crates/frontend run: pnpm install --frozen-lockfile - name: Build Windows bundles - working-directory: crates/agent-gui + working-directory: crates/frontend shell: bash env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} @@ -209,9 +201,9 @@ jobs: run: | set -euo pipefail mkdir -p dist - msi_path="$(find target crates/agent-gui/src-tauri/target -type f -name '*.msi' -path '*bundle*' -print -quit 2>/dev/null || true)" - exe_path="$(find target crates/agent-gui/src-tauri/target -type f -name '*.exe' -path '*bundle*' -print -quit 2>/dev/null || true)" - portable_exe_path="$(find target crates/agent-gui/src-tauri/target -type f -name 'liveagent.exe' -path '*/release/*' ! -path '*bundle*' -print -quit 2>/dev/null || true)" + msi_path="$(find target crates/frontend/src-tauri/target -type f -name '*.msi' -path '*bundle*' -print -quit 2>/dev/null || true)" + exe_path="$(find target crates/frontend/src-tauri/target -type f -name '*.exe' -path '*bundle*' -print -quit 2>/dev/null || true)" + portable_exe_path="$(find target crates/frontend/src-tauri/target -type f -name 'liveagent.exe' -path '*/release/*' ! -path '*bundle*' -print -quit 2>/dev/null || true)" test -n "$msi_path" test -n "$exe_path" test -n "$portable_exe_path" @@ -252,7 +244,7 @@ jobs: node-version: ${{ env.NODE_VERSION }} - name: Prepare release version config - working-directory: crates/agent-gui + working-directory: crates/frontend run: node ../../scripts/release/prepare-app-version-from-tag.mjs "$LIVEAGENT_RELEASE_TAG" --github-env "$GITHUB_ENV" --tauri-config src-tauri/tauri.version.generated.conf.json - name: Install pnpm @@ -273,20 +265,19 @@ jobs: libwebkit2gtk-4.1-dev \ libxdo-dev \ patchelf \ - protobuf-compiler \ rpm \ wget - uses: Swatinem/rust-cache@v2 with: - workspaces: crates/agent-gui/src-tauri + workspaces: crates/frontend/src-tauri - name: Install frontend dependencies - working-directory: crates/agent-gui + working-directory: crates/frontend run: pnpm install --frozen-lockfile - name: Build Linux bundles - working-directory: crates/agent-gui + working-directory: crates/frontend env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} @@ -300,21 +291,21 @@ jobs: TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} run: | set -euo pipefail - appimage_path="$(find crates/agent-gui/src-tauri/target target -type f -name '*.AppImage' -path '*bundle*' -print -quit 2>/dev/null || true)" + appimage_path="$(find crates/frontend/src-tauri/target target -type f -name '*.AppImage' -path '*bundle*' -print -quit 2>/dev/null || true)" test -n "$appimage_path" appimage_path="$(realpath "$appimage_path")" scripts/release/postprocess-linux-appimage.sh "$appimage_path" test ! -e "$appimage_path.sig" - pnpm --dir crates/agent-gui tauri signer sign "$appimage_path" + pnpm --dir crates/frontend tauri signer sign "$appimage_path" test -s "$appimage_path.sig" - name: Stage Linux artifacts run: | set -euo pipefail mkdir -p dist - appimage_path="$(find crates/agent-gui/src-tauri/target target -type f -name '*.AppImage' -path '*bundle*' -print -quit 2>/dev/null || true)" - deb_path="$(find crates/agent-gui/src-tauri/target target -type f -name '*.deb' -path '*bundle*' -print -quit 2>/dev/null || true)" - rpm_path="$(find crates/agent-gui/src-tauri/target target -type f -name '*.rpm' -path '*bundle*' -print -quit 2>/dev/null || true)" + appimage_path="$(find crates/frontend/src-tauri/target target -type f -name '*.AppImage' -path '*bundle*' -print -quit 2>/dev/null || true)" + deb_path="$(find crates/frontend/src-tauri/target target -type f -name '*.deb' -path '*bundle*' -print -quit 2>/dev/null || true)" + rpm_path="$(find crates/frontend/src-tauri/target target -type f -name '*.rpm' -path '*bundle*' -print -quit 2>/dev/null || true)" test -n "$appimage_path" test -n "$deb_path" test -n "$rpm_path" diff --git a/.github/workflows/gateway-docker.yml b/.github/workflows/gateway-docker.yml deleted file mode 100644 index 370008433..000000000 --- a/.github/workflows/gateway-docker.yml +++ /dev/null @@ -1,92 +0,0 @@ -name: Gateway Docker - -on: - push: - tags: - - "v*" - workflow_dispatch: - inputs: - tag: - description: Existing release tag to publish, for example v0.1.0 - required: true - -permissions: - contents: read - packages: write - -env: - IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/liveagent-gateway - -jobs: - build-and-push: - name: Build and Push Gateway Image - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - ref: ${{ github.event.inputs.tag || github.ref }} - - - uses: docker/setup-qemu-action@v3 - - - uses: docker/setup-buildx-action@v3 - - - name: Log in to GHCR - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Docker metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.IMAGE_NAME }} - flavor: | - latest=false - tags: | - type=raw,value=${{ github.event.inputs.tag || github.ref_name }} - type=raw,value=latest - - - name: Build and push - id: push - uses: docker/build-push-action@v6 - with: - context: . - file: Dockerfile - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - provenance: false - - # Guard against mislabeled variants: v0.1.0 through v1.1.8 advertised - # arm64 while shipping an x86-64 binary (TARGETARCH defaults in the - # Dockerfile shadowed the values buildx injects). - - name: Verify per-arch binaries - run: | - set -euo pipefail - repo="$(echo "$IMAGE_NAME" | tr '[:upper:]' '[:lower:]')" - index="$repo@${{ steps.push.outputs.digest }}" - for arch in amd64 arm64; do - case "$arch" in - amd64) want="x86-64" ;; - arm64) want="aarch64" ;; - esac - # Pull each variant by its own manifest digest. Pulling the shared - # index digest with --platform binds repo@digest to one platform, - # so the second arch fails with "cannot overwrite digest". - digest="$(docker buildx imagetools inspect --raw "$index" \ - | jq -r --arg arch "$arch" '.manifests[] | select(.platform.os == "linux" and .platform.architecture == $arch) | .digest')" - test -n "$digest" || { echo "::error::pushed index has no linux/$arch manifest"; exit 1; } - docker pull "$repo@$digest" >/dev/null - docker rm -f "verify-$arch" >/dev/null 2>&1 || true - docker create --name "verify-$arch" "$repo@$digest" >/dev/null - docker cp "verify-$arch:/usr/local/bin/liveagent-gateway" "/tmp/gateway-$arch" - docker rm -f "verify-$arch" >/dev/null - info="$(file -b "/tmp/gateway-$arch")" - echo "linux/$arch: $info" - echo "$info" | grep -q "$want" || { echo "::error::linux/$arch image contains a non-$arch gateway binary"; exit 1; } - done diff --git a/.github/workflows/pr-governance.yml b/.github/workflows/pr-governance.yml index 111873fbe..1a8745c82 100644 --- a/.github/workflows/pr-governance.yml +++ b/.github/workflows/pr-governance.yml @@ -62,8 +62,7 @@ jobs: // 2) UI 改动必须附截图/预览:改动文件命中前端路径,而正文没有图片即视为缺失。 const UI_PATHS = [ - 'crates/agent-gui/src/', - 'crates/agent-gateway/web/src/', + 'crates/frontend/src/', ]; const files = await github.paginate(github.rest.pulls.listFiles, { owner, diff --git a/.gitignore b/.gitignore index 9501f6c75..f12689e9f 100644 --- a/.gitignore +++ b/.gitignore @@ -13,10 +13,10 @@ lerna-debug.log* .codex-artifacts .liveagent workspace -!crates/agent-gui/src-tauri/src/commands/workspace/ -!crates/agent-gui/src-tauri/src/commands/workspace/** -!crates/agent-gui/src/pages/chat/workspace/ -!crates/agent-gui/src/pages/chat/workspace/** +!crates/frontend/src-tauri/src/commands/workspace/ +!crates/frontend/src-tauri/src/commands/workspace/** +!crates/frontend/src/pages/chat/workspace/ +!crates/frontend/src/pages/chat/workspace/** cert/ node_modules dist @@ -25,7 +25,6 @@ target *.local .vscode target -bin .serena chatroom @@ -41,3 +40,5 @@ chatroom *.sw? .pnpm-store workspace.zip +.codegraph/ +.playwright-mcp/ diff --git a/Cargo.lock b/Cargo.lock index 3bb8b3ead..764db741a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -320,6 +320,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core", + "base64 0.22.1", "bytes", "form_urlencoded", "futures-util", @@ -332,14 +333,17 @@ dependencies = [ "matchit", "memchr", "mime", + "multer", "percent-encoding", "pin-project-lite", "serde_core", "serde_json", "serde_path_to_error", "serde_urlencoded", + "sha1 0.10.6", "sync_wrapper", "tokio", + "tokio-tungstenite 0.29.0", "tower", "tower-layer", "tower-service", @@ -365,6 +369,51 @@ dependencies = [ "tracing", ] +[[package]] +name = "backend" +version = "0.1.0" +dependencies = [ + "axum", + "base64 0.22.1", + "chardetng", + "chrono", + "dirs", + "encoding_rs", + "futures-util", + "globset", + "http-body-util", + "ignore", + "leveldb-core", + "lopdf", + "notify", + "percent-encoding", + "portable-pty", + "quick-xml 0.41.0", + "regex", + "reqwest", + "rquickjs", + "rusqlite", + "russh", + "russh-sftp", + "serde", + "serde_json", + "sha2 0.11.0", + "subtle", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tokio-cron-scheduler", + "tokio-tungstenite 0.29.0", + "toml 0.9.12+spec-1.1.0", + "tower", + "tower-http", + "uuid", + "wait-timeout", + "walkdir", + "windows-sys 0.61.2", + "zip 8.6.0", +] + [[package]] name = "base16ct" version = "1.0.0" @@ -409,7 +458,7 @@ dependencies = [ "bitflags 2.13.0", "cexpr", "clang-sys", - "itertools 0.12.1", + "itertools", "lazy_static", "lazycell", "log", @@ -2947,15 +2996,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.18" @@ -3321,6 +3361,7 @@ version = "1.3.0-dev.0" dependencies = [ "arboard", "axum", + "backend", "base64 0.22.1", "chardetng", "chrono", @@ -3335,8 +3376,6 @@ dependencies = [ "objc2-app-kit 0.3.2", "percent-encoding", "portable-pty", - "prost", - "prost-build", "quick-xml 0.41.0", "regex", "reqwest", @@ -3569,10 +3608,21 @@ dependencies = [ ] [[package]] -name = "multimap" -version = "0.10.1" +name = "multer" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] [[package]] name = "ndk" @@ -4771,57 +4821,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "prost" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-build" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" -dependencies = [ - "heck 0.5.0", - "itertools 0.14.0", - "log", - "multimap", - "petgraph", - "prettyplease", - "prost", - "prost-types", - "regex", - "syn 2.0.118", - "tempfile", -] - -[[package]] -name = "prost-derive" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" -dependencies = [ - "anyhow", - "itertools 0.14.0", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "prost-types" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" -dependencies = [ - "prost", -] - [[package]] name = "pxfm" version = "0.1.30" @@ -5419,7 +5418,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6110,6 +6109,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" + [[package]] name = "spki" version = "0.8.0" @@ -7134,6 +7139,7 @@ dependencies = [ "futures-util", "http", "http-body", + "http-body-util", "pin-project-lite", "tower", "tower-layer", diff --git a/Cargo.toml b/Cargo.toml index 2b077d9d2..95cf2f428 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "2" -members = ["crates/agent-gui/src-tauri"] +members = ["crates/backend", "crates/frontend/src-tauri"] diff --git a/Dockerfile b/Dockerfile index a3406a632..b95a6ca0b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,51 +1,88 @@ # syntax=docker/dockerfile:1.7 -FROM --platform=$BUILDPLATFORM node:22.17.1-bookworm-slim AS webui +# 阶段1:构建 Node 引擎 bundle +# core 经 esbuild 打包成单文件 dist/index.js +FROM --platform=$BUILDPLATFORM node:22.19.0-bookworm-slim AS engine-builder -WORKDIR /src/crates/agent-gateway/web +WORKDIR /src/crates/core + +# 安装 pnpm RUN npm install -g pnpm@10.32.1 -COPY crates/agent-gateway/web/package.json crates/agent-gateway/web/pnpm-lock.yaml ./ +# 复制依赖声明 +COPY crates/core/package.json crates/core/pnpm-lock.yaml ./ + +# 安装依赖 RUN pnpm install --frozen-lockfile -COPY crates/agent-gateway/web ./ +# 复制源码 +COPY crates/core ./ + +# 构建:tsc 类型检查 + esbuild 打包 RUN pnpm build -FROM --platform=$BUILDPLATFORM golang:1.25-bookworm AS gateway-builder +# 阶段2:构建 Rust 后端二进制 +FROM --platform=$BUILDPLATFORM rust:1-bookworm AS backend-builder -# Keep these ARGs bare: a default value shadows the per-platform values buildx injects. +# 保持这些 ARG 裸露:不赋默认值,让 buildx 按每平台注入 ARG TARGETOS ARG TARGETARCH -WORKDIR /src/crates/agent-gateway +WORKDIR /src -COPY crates/agent-gateway/go.mod crates/agent-gateway/go.sum ./ -RUN go mod download +# rquickjs-sys 的 bindgen 构建脚本要 libclang 才能编译,缺失会在 build.rs 里直接 panic。 +RUN apt-get update && apt-get install -y --no-install-recommends libclang-dev \ + && rm -rf /var/lib/apt/lists/* -COPY crates/agent-gateway ./ -COPY --from=webui /src/crates/agent-gateway/web/dist ./web/dist +# 复制 Cargo workspace 根 +COPY Cargo.toml Cargo.lock ./ -RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ - go build -trimpath -ldflags="-s -w" -o /out/liveagent-gateway ./cmd/gateway +# 复制两个 crate +COPY crates/backend ./crates/backend +COPY crates/frontend ./crates/frontend -FROM debian:bookworm-slim AS runtime +# 缓存依赖下载 +RUN cargo fetch --manifest-path crates/backend/Cargo.toml -RUN apt-get update \ - && apt-get install -y --no-install-recommends ca-certificates \ +# 构建 backend release 二进制 +RUN cargo build -p backend --release \ + --target-dir /out/target + +# 阶段3:运行时镜像 +# node:22-bookworm-slim 自带 Node 和必要的系统库,chat 引擎(core bundle)跑在它上面。 +FROM node:22.19.0-bookworm-slim AS runtime + +# 非 root 用户:权限隔离。数据目录 /var/lib/liveagent 建好并归属该用户, +# 供 VOLUME 挂载持久化。 +# ca-certificates:reqwest 0.13 启动时从系统加载根证书,bookworm-slim 不带, +# 缺了 Client::new() 直接 panic(backend 一启动就建 HTTP client)。 +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \ + && useradd --system --uid 10001 --user-group --home-dir /var/lib/liveagent --shell /usr/sbin/nologin liveagent \ + && install -d -o liveagent -g liveagent -m 0700 /opt/liveagent/engine /var/lib/liveagent \ && rm -rf /var/lib/apt/lists/* -RUN useradd --system --uid 10001 --user-group --home-dir /nonexistent --shell /usr/sbin/nologin liveagent \ - && install -d -o liveagent -g liveagent -m 0700 /var/lib/liveagent +# 从 backend-builder 阶段复制 Rust 二进制 +COPY --from=backend-builder /out/target/release/backend /usr/local/bin/backend + +# 从 engine-builder 阶段复制 Node 引擎 bundle +COPY --from=engine-builder /src/crates/core/dist/index.js /opt/liveagent/engine/index.js -COPY --from=gateway-builder /out/liveagent-gateway /usr/local/bin/liveagent-gateway +# 调整所有权为 liveagent 用户 +RUN chown -R liveagent:liveagent /opt/liveagent USER liveagent -ENV PORT=8080 \ - LIVEAGENT_GATEWAY_DATA_DIR=/var/lib/liveagent +# 后端直接认环境变量(PORT、LIVEAGENT_BACKEND_PASSWORD 也可覆盖), +# 不再需要 entrypoint 脚本翻译。 +# +# HOME 显式指到数据卷:Node 侧的库会读写 `~` 下的配置。原先这个用户的 +# home 是 /nonexistent,Node 程序往那儿写就是 EACCES。 +ENV LIVEAGENT_DATA_DIR=/var/lib/liveagent \ + LIVEAGENT_ENGINE_BUNDLE=/opt/liveagent/engine \ + HOME=/var/lib/liveagent VOLUME ["/var/lib/liveagent"] -EXPOSE 8080 +EXPOSE 8443 -ENTRYPOINT ["/usr/local/bin/liveagent-gateway"] +ENTRYPOINT ["/usr/local/bin/backend"] diff --git a/Makefile b/Makefile index 3b5786dc0..78d6e9dae 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ .DEFAULT_GOAL := dev -AGENT_GUI_DIR := crates/agent-gui -AGENT_GATEWAY_DIR := crates/agent-gateway -AGENT_GATEWAY_WEB_DIR := $(AGENT_GATEWAY_DIR)/web +AGENT_GUI_DIR := crates/frontend +AGENT_BACKEND_DIR := crates/backend +AGENT_CORE_JS_DIR := crates/core HOST_ARCH := $(shell uname -m) @@ -23,24 +23,25 @@ DESKTOP_WINDOWS_TAURI_CONFIG ?= src-tauri/tauri.windows.conf.json DESKTOP_RELEASE_TAURI_CONFIG ?= src-tauri/tauri.macos.release.conf.json DESKTOP_RELEASE_TAURI_CONFIG_FLAGS ?= --config $(DESKTOP_RELEASE_TAURI_CONFIG) $(if $(LIVEAGENT_TAURI_VERSION_CONFIG),--config $(LIVEAGENT_TAURI_VERSION_CONFIG)) -DEV_GATEWAY_TOKEN ?= dev-token -DEV_GATEWAY_HTTP_ADDR ?= :50052 -DEV_WEBUI_PROXY_API ?= http://localhost:50052 -GATEWAY_DOCKER_IMAGE ?= liveagent-gateway:local +MODEL_CATALOG_GENERATED_FILES := $(AGENT_CORE_JS_DIR)/src/models/catalog.generated.ts $(AGENT_GUI_DIR)/src/lib/models/catalog.generated.ts + +BACKEND_DOCKER_IMAGE ?= liveagent-backend:local RELEASE_TAG ?= -MODEL_CATALOG_GENERATED_FILES := $(AGENT_GUI_DIR)/src/lib/models/catalog.generated.ts $(AGENT_GATEWAY_WEB_DIR)/src/lib/models/catalog.generated.ts -.PHONY: all dev build desktop-build-macos desktop-build-macos-release desktop-build-macos-intel desktop-build-macos-m desktop-build-windows desktop-build-linux github-release-main check-github-release-tag help -.PHONY: dev-gateway dev-webui ensure-webui-embed-stub -.PHONY: proto proto-check webui gateway-build gateway-docker-build gateway-docker-run gateway-docker-smoke build-linux build-linux-amd build-linux-arm +.PHONY: all dev core-build build desktop-build-macos desktop-build-macos-release desktop-build-macos-intel desktop-build-macos-m desktop-build-windows desktop-build-linux github-release-main check-github-release-tag help +.PHONY: backend-docker-build backend-docker-run backend-docker-smoke .PHONY: clean update-model-catalog check-rust-target-% check-macos-signing-identity check-macos-notary-profile desktop-store-macos-notary-profile desktop-wait-macos-notary desktop-staple-macos desktop-verify-macos +.PHONY: update-routes check-routes check-wire-events check-settings-drift -all: build gateway-build +all: build ## Desktop app -dev: +dev: core-build pnpm --dir $(AGENT_GUI_DIR) tauri dev +core-build: + pnpm --dir $(AGENT_CORE_JS_DIR) build + build: pnpm --dir $(AGENT_GUI_DIR) tauri build @@ -117,91 +118,54 @@ check-github-release-tag: @if [ -z "$(RELEASE_TAG)" ]; then echo "RELEASE_TAG is required. Example: make github-release-main RELEASE_TAG=v0.1.10"; exit 1; fi @node scripts/release/prepare-app-version-from-tag.mjs "$(RELEASE_TAG)" --json >/dev/null -## Gateway development -# go:embed requires web/dist at compile time. Dev serves the SPA from Vite, so -# a tiny stub is enough to let `go run` start without a full WebUI build. -dev-gateway: ensure-webui-embed-stub - go -C $(AGENT_GATEWAY_DIR) run ./cmd/gateway --token=$(DEV_GATEWAY_TOKEN) --http-addr=$(DEV_GATEWAY_HTTP_ADDR) - -dev-webui: - npm_config_proxy_api=$(DEV_WEBUI_PROXY_API) pnpm --dir $(AGENT_GATEWAY_WEB_DIR) dev - -ensure-webui-embed-stub: - @if [ ! -f "$(AGENT_GATEWAY_WEB_DIR)/dist/index.html" ]; then \ - mkdir -p "$(AGENT_GATEWAY_WEB_DIR)/dist"; \ - printf '%s\n' \ - '' \ - '' \ - 'LiveAgent Gateway' \ - '

WebUI embed stub. Run make dev-webui for the real SPA.

' \ - '' \ - > "$(AGENT_GATEWAY_WEB_DIR)/dist/index.html"; \ - echo "created $(AGENT_GATEWAY_WEB_DIR)/dist stub for go:embed"; \ - fi - -## Gateway build and generated assets -proto: - @command -v buf >/dev/null || (echo "buf is required. Run: mise install" && exit 1) - cd $(AGENT_GATEWAY_DIR) && buf generate - -# buf breaking 的对比基线(本地默认与当前 HEAD 对比;CI 覆写为 origin/main)。 -BUF_BREAKING_AGAINST ?= ../../.git\#subdir=$(AGENT_GATEWAY_DIR) - -proto-check: - @command -v buf >/dev/null || (echo "buf is required. Run: mise install" && exit 1) - cd $(AGENT_GATEWAY_DIR) && buf lint - cd $(AGENT_GATEWAY_DIR) && buf breaking --against '$(BUF_BREAKING_AGAINST)' - -webui: - pnpm --dir $(AGENT_GATEWAY_WEB_DIR) install --offline - pnpm --dir $(AGENT_GATEWAY_WEB_DIR) build - -gateway-build: proto webui - CGO_ENABLED=0 go -C $(AGENT_GATEWAY_DIR) build -o bin/liveagent-gateway ./cmd/gateway - -gateway-docker-build: - docker build -t $(GATEWAY_DOCKER_IMAGE) . +## Backend build and Docker +backend-docker-build: + docker build -t $(BACKEND_DOCKER_IMAGE) . -gateway-docker-run: - docker run --rm -p 8080:8080 -e LIVEAGENT_GATEWAY_TOKEN=$(DEV_GATEWAY_TOKEN) $(GATEWAY_DOCKER_IMAGE) +backend-docker-run: + docker run --rm -p 8443:8443 $(BACKEND_DOCKER_IMAGE) -gateway-docker-smoke: gateway-docker-build +backend-docker-smoke: backend-docker-build @set -e; \ - name="liveagent-gateway-smoke"; \ + name="liveagent-backend-smoke"; \ docker rm -f "$$name" >/dev/null 2>&1 || true; \ - docker run -d --name "$$name" -p 18080:8080 -e LIVEAGENT_GATEWAY_TOKEN=$(DEV_GATEWAY_TOKEN) $(GATEWAY_DOCKER_IMAGE) >/dev/null; \ + docker run -d --name "$$name" -p 18443:8443 $(BACKEND_DOCKER_IMAGE) >/dev/null; \ trap 'docker rm -f "$$name" >/dev/null 2>&1 || true' EXIT; \ - for _ in $$(seq 1 30); do \ - if curl -fsS http://127.0.0.1:18080/healthz | grep -q '"ok":true'; then \ - echo "Gateway Docker smoke test passed: http://127.0.0.1:18080/healthz"; \ + for _ in $$(seq 1 60); do \ + if curl -fsS http://127.0.0.1:18443/healthz 2>/dev/null | grep -q 'ok'; then \ + echo "Backend Docker smoke test passed: http://127.0.0.1:18443/healthz"; \ exit 0; \ fi; \ sleep 1; \ done; \ - echo "Gateway Docker smoke test failed; container logs:"; \ + echo "Backend Docker smoke test failed; container logs:"; \ docker logs "$$name" || true; \ exit 1 -build-linux: proto webui - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go -C $(AGENT_GATEWAY_DIR) build -o bin/liveagent-gateway-linux-amd64 ./cmd/gateway - -build-linux-amd: build-linux - -build-linux-arm: proto webui - CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go -C $(AGENT_GATEWAY_DIR) build -o bin/liveagent-gateway-linux-arm64 ./cmd/gateway - -build-windows: proto webui - CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go -C $(AGENT_GATEWAY_DIR) build -o bin/liveagent-gateway-windows-amd64.exe ./cmd/gateway - -gateway-build-windows: build-windows - ## Maintenance clean: - rm -rf $(AGENT_GATEWAY_DIR)/bin/ $(AGENT_GATEWAY_WEB_DIR)/dist/ + cargo clean + rm -rf $(AGENT_GUI_DIR)/dist $(AGENT_CORE_JS_DIR)/dist update-model-catalog: node scripts/generate-model-catalog.mjs +# 从 src-tauri/src/tauri_commands/*.rs 重新生成 backend 的路由层(routes_gen.rs)。 +update-routes: + node scripts/generate-routes.mjs + +# 校验 routes_gen.rs 与 wrapper 层一致(CI 用);漂移即失败。 +check-routes: + node scripts/generate-routes.mjs --check + +# 校验前端 wireEvents.ts 是 core 那份的逐字镜像(仅允许本地 ui_stopping 差异,CI 用)。 +check-wire-events: + node scripts/check-wire-events.mjs + +# 校验 settings 契约未分叉:core 版是前端版的有序子序列,mcpOps/normalize 逐字一致(CI 用)。 +check-settings-drift: + node scripts/check-settings-drift.mjs + check-rust-target-%: @rustup target list --installed | grep -qx "$*" || (echo "Rust target $* is not installed. Run: rustup target add $*" && exit 1) @@ -256,21 +220,16 @@ help: @printf " %-34s %s\n" "make desktop-build-windows" "构建 Windows Tauri 应用" @printf " %-34s %s\n" "make desktop-build-linux" "构建 Linux AppImage/deb/rpm" @printf " %-34s %s\n" "make github-release-main RELEASE_TAG=vX.Y.Z" "从 main 打 tag 并触发 GitHub Release(自动刷新模型目录并提交)" - @printf "\n%s\n" "Gateway development" - @printf " %-34s %s\n" "make dev-gateway" "启动 agent-gateway Go 服务" - @printf " %-34s %s\n" "make dev-webui" "启动 agent-gateway Web UI 开发服务" - @printf "\n%s\n" "Gateway build" - @printf " %-34s %s\n" "make proto" "生成 agent-gateway protobuf 代码" - @printf " %-34s %s\n" "make webui" "构建 agent-gateway Web UI" - @printf " %-34s %s\n" "make gateway-build" "构建 agent-gateway 本地二进制" - @printf " %-34s %s\n" "make gateway-docker-build" "构建 agent-gateway Docker 镜像" - @printf " %-34s %s\n" "make gateway-docker-run" "本地运行 agent-gateway Docker 镜像" - @printf " %-34s %s\n" "make gateway-docker-smoke" "构建并健康检查 agent-gateway Docker 镜像" - @printf " %-34s %s\n" "make build-linux" "构建 agent-gateway Linux amd64 二进制" - @printf " %-34s %s\n" "make build-linux-arm" "构建 agent-gateway Linux arm64 二进制" - @printf " %-34s %s\n" "make build-windows" "构建 agent-gateway Windows amd64 二进制" + @printf "\n%s\n" "Backend build" + @printf " %-34s %s\n" "make backend-docker-build" "构建 backend Docker 镜像" + @printf " %-34s %s\n" "make backend-docker-run" "本地运行 backend Docker 镜像" + @printf " %-34s %s\n" "make backend-docker-smoke" "构建并健康检查 backend Docker 镜像" @printf "\n%s\n" "Maintenance" - @printf " %-34s %s\n" "make all" "同时构建 GUI 和 agent-gateway" - @printf " %-34s %s\n" "make clean" "清理 agent-gateway 构建产物" + @printf " %-34s %s\n" "make all" "构建 GUI" + @printf " %-34s %s\n" "make clean" "清理构建产物" @printf " %-34s %s\n" "make update-model-catalog" "刷新 models.dev 模型目录快照" + @printf " %-34s %s\n" "make update-routes" "从 tauri_commands 重新生成 backend 路由层" + @printf " %-34s %s\n" "make check-routes" "校验路由层与 wrapper 一致(CI 门禁)" + @printf " %-34s %s\n" "make check-wire-events" "校验前端 wireEvents 是 core 的逐字镜像(CI 门禁)" + @printf " %-34s %s\n" "make check-settings-drift" "校验 settings 契约两侧未分叉(CI 门禁)" @printf " %-34s %s\n" "make help" "查看可用命令" diff --git a/README.md b/README.md index 8f560fc21..5a0bdc2cc 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@

LiveAgent

- Your Local-First AI Agent Desktop
- Multi-model access · Local tool execution · MCP & Skills ecosystem · Remote Gateway + Your Self-Hosted AI Agent Workspace
+ Multi-model access · Real tool execution · MCP & Skills ecosystem · Browser or desktop, same backend

@@ -18,7 +18,7 @@ Tauri React Rust - Go + Node License

@@ -78,11 +78,11 @@ ## Why LiveAgent? -LiveAgent is a **local-first** AI agent desktop client. It deeply integrates large language model reasoning with local system tools, so the AI can genuinely operate your file system, run commands, and manage scheduled tasks — while the Gateway enables remote access and collaboration. +LiveAgent is a **self-hosted** AI agent you run yourself. It deeply integrates large language model reasoning with real system tools, so the AI can genuinely operate your file system, run commands, and manage scheduled tasks — and you reach it from a desktop app or a browser, both talking to the same backend. - **An agent that actually gets things done** — beyond chat: read and write files, make precise edits, run Bash, and supervise long-running processes - **A fully open ecosystem** — bridge any external tool via the MCP protocol, and load Skills packages on demand -- **Both local and remote** — the desktop app works fully standalone; deploy the Gateway and control it from any browser +- **Your keys stay on your backend** — the backend is the only thing that holds credentials, and you decide where it runs: your laptop, your home server, or your own VPS --- @@ -106,7 +106,7 @@ LiveAgent is a **local-first** AI agent desktop client. It deeply integrates lar ### 🧩 MCP & Skills Ecosystem -- **MCP protocol bridging** — the Tauri side natively bridges any stdio / http MCP server for unlimited tool extension +- **MCP protocol bridging** — the backend natively bridges any stdio / http MCP server for unlimited tool extension - **Skills packages** — progressive disclosure and on-demand loading, with install / create / package support and the ClawHub ecosystem ### 💾 Memory & Automation @@ -114,10 +114,10 @@ LiveAgent is a **local-first** AI agent desktop client. It deeply integrates lar - **Persistent memory** — Markdown + SQLite FTS full-text search for cross-session knowledge management - **Scheduled tasks** — bash / http / prompt cron job types, executed automatically in the background -### 🌐 Remote Gateway +### 🌐 Browser and Desktop, One Backend -- **Access from any browser** — Go gateway (WebSocket + Protobuf) with a WebUI for remotely controlling the local agent -- **Disconnect recovery** — a bounded seq window replays short outages, with desktop-side persistence as the safety net +- **Same code, two shells** — the desktop app and the browser run the identical frontend build; the only difference is which backend URL it points at +- **Disconnect recovery** — the event WebSocket reconnects and replays, with backend-side persistence as the safety net --- @@ -162,9 +162,14 @@ Choose by distribution from [Releases](https://github.com/Stack-Cairn/LiveAgent/ | DEB | Debian / Ubuntu family | `sudo dpkg -i LiveAgent--Linux-x86_64.deb` | | RPM | Fedora / openSUSE family | `sudo rpm -i LiveAgent--Linux-x86_64.rpm` | -### Need Remote Access? Deploy the Gateway +### Need Remote Access? (Legacy Gateway) -The desktop app works out of the box and depends on no server. Deploy the Gateway only if you want to **control your local agent from a browser**. +> **⚠️ 计划在 v2.0 停用 / Deprecated in v2.0** —— 下面这套 Gateway 部署方式在 +> v2.0 会断。已经部署的用户请先读 [v2.0 迁移指南](#v20-迁移指南--v20-migration-guide)。 + +In v2.0 there is no Gateway: you deploy the **backend** where you want it, and the desktop app or a browser points straight at it. If the backend sits behind NAT and you are not, that is a networking problem with networking answers — [Tailscale](https://tailscale.com/), [frp](https://github.com/fatedier/frp), or [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) — not something the app punches through for you. + +The instructions below are the v1 Gateway path, kept for users who already run it. **Note: when deployed behind an Nginx reverse proxy, set the Gateway address on the Settings → Remote page to the HTTPS URL and use port 443.** @@ -243,47 +248,52 @@ location / { Expand the Development Guide below for the full set of Make commands. -![](docs/images/architecture.webp) -
Architecture Overview — diagram & tech stack ``` ┌──────────────────────────────────────────────────────────────┐ -│ Browser WebUI │ -│ React + Vite + WebSocket + Gateway API │ +│ Frontend (one codebase) │ +│ React 19 · Vite · runs in a browser tab, or inside │ +│ the Tauri 2 desktop shell — same build output │ └────────────────────────────┬─────────────────────────────────┘ - │ WebSocket / HTTP + │ HTTP POST /api/ (JSON) + │ WebSocket /api/events (JSON) ┌────────────────────────────▼─────────────────────────────────┐ -│ Agent Gateway │ -│ Go · WebSocket · HTTP · Session Manager · Event Store │ -│ (Railway / Docker / self-hosted) │ +│ Backend · backend │ +│ Rust · axum · SQLite · password auth · static assets │ +│ The only public listener. Holds the API keys. │ +│ (laptop / home server / Docker / VPS) │ └────────────────────────────┬─────────────────────────────────┘ - │ WebSocket v2 (bidirectional stream) + │ loopback HTTP (never exposed) ┌────────────────────────────▼─────────────────────────────────┐ -│ Agent GUI │ -│ Tauri 2 · React 19 · Rust │ +│ Engine · core │ +│ Node 22 · TypeScript · pi-agent-core │ ├──────────┬────────────┬───────────┬────────────┬─────────────┤ │ Models │ Runtime │ Tools │ Skills │ Memory/Cron │ │ pi-ai │ multi-turn │ FS/Bash/ │ progressive│ SQLite+MD │ -│ + Codex │ + SubAgent │ MCP bridge│ + Hub │ FTS index │ +│ │ + SubAgent │ MCP bridge│ + Hub │ FTS index │ └──────────┴────────────┴───────────┴────────────┴─────────────┘ ``` +The desktop shell is a shell: it renders the frontend and adds native niceties +(tray, notifications, file dialogs). It is not a second implementation — point +the same frontend at a remote backend URL and everything works identically. + **Tech Stack** | Component | Technology | |---|---| -| **Agent GUI** · Framework | Tauri 2 + React 19 + TypeScript 6 | -| **Agent GUI** · Build | Vite 8 + pnpm | -| **Agent GUI** · Styling | Tailwind CSS 4 + Radix UI | -| **Agent GUI** · Rendering | streamdown + KaTeX + Mermaid + Monaco Editor | -| **Agent GUI** · Backend | Rust + Tokio + SQLite (rusqlite) + WebSocket (tokio-tungstenite) | -| **Agent GUI** · LLM | @earendil-works/pi-ai · @earendil-works/pi-agent-core | -| **Gateway** · Language | Go 1.25 | -| **Gateway** · Protocols | WebSocket + Protobuf + HTTP | -| **Gateway** · Web UI | React + Vite + Tailwind CSS (embedded) | -| **Gateway** · Deployment | Docker multi-stage · Railway CI/CD | +| **Frontend** · Framework | React 19 + TypeScript 6 | +| **Frontend** · Build | Vite 8 + pnpm | +| **Frontend** · Styling | Tailwind CSS 4 + Base UI | +| **Frontend** · Rendering | streamdown + KaTeX + Mermaid + Monaco Editor | +| **Desktop shell** | Tauri 2 (optional — the browser is a first-class target) | +| **Backend** · `backend` | Rust + Tokio + axum + SQLite (rusqlite) | +| **Backend** · Protocol | JSON over HTTP + WebSocket | +| **Engine** · `core` | Node 22 + TypeScript | +| **Engine** · LLM | @earendil-works/pi-ai · @earendil-works/pi-agent-core | +| **Deployment** | Docker (backend + Node runtime in one image) · Railway CI/CD |
@@ -294,15 +304,12 @@ Expand the Development Guide below for the full set of Make commands. |---|---| | `make dev` | Start the Tauri development environment | | `make build` | Build the desktop app | -| `make dev-gateway` | Start the Gateway dev server | -| `make dev-webui` | Start the WebUI dev server | -| `make gateway-build` | Build the Gateway binary | -| `make gateway-docker-build` | Build the Docker image | -| `make gateway-docker-smoke` | Build + health check | +| `make backend-docker-build` | Build the backend Docker image | +| `make backend-docker-run` | Run the backend image (HTTPS on 8443) | +| `make backend-docker-smoke` | Build + `/healthz` check | | `make desktop-build-macos-release` | macOS signed release build | -| `make build-linux` | Linux amd64 gateway | -| `make build-linux-arm` | Linux arm64 gateway | -| `make proto` | Regenerate Protobuf code | +| `make update-routes` | Regenerate the backend route layer from the command wrappers | +| `make check-routes` | Fail if the generated routes have drifted (CI gate) | | `make clean` | Clean build artifacts | @@ -313,20 +320,23 @@ Expand the Development Guide below for the full set of Make commands. ``` LiveAgent/ ├── crates/ -│ ├── agent-gui/ # Desktop client -│ │ ├── src/ # React frontend +│ ├── frontend/ # Frontend + desktop shell +│ │ ├── src/ # React frontend (browser and desktop share it) │ │ │ ├── components/ # UI components │ │ │ ├── lib/ # Core logic (chat, tools, skills, memory) │ │ │ ├── pages/ # Pages (Chat, Settings) │ │ │ ├── i18n/ # Internationalization │ │ │ └── prompt/ # System prompt templates -│ │ └── src-tauri/ # Rust backend (Tauri) +│ │ └── src-tauri/ # Tauri 2 desktop shell (Rust) │ │ -│ └── agent-gateway/ # Go gateway service -│ ├── cmd/gateway/ # Entry point -│ ├── internal/ # Core implementation -│ ├── proto/v2/ # Protobuf definitions -│ └── web/ # Embedded WebUI +│ ├── backend/ # Rust backend — the only public listener, +│ │ │ # plus the shared core (tools, runtime, storage) +│ │ ├── src/server/ # HTTP command routes and the event WebSocket +│ │ ├── src/engine_process.rs # Spawns and supervises the Node engine +│ │ └── src/engine_proxy.rs # Chat reverse proxy and event backflow +│ │ +│ └── core/ # Node engine — model calls and the agent loop +│ └── src/ # TypeScript, bundled with esbuild │ ├── docs/ # Project docs │ ├── architecture/ # Architecture design @@ -334,8 +344,8 @@ LiveAgent/ │ └── operations/ # Operations & deployment │ ├── scripts/release/ # Release automation -├── .github/workflows/ # CI/CD (CI + Desktop Release + Gateway Docker) -├── Dockerfile # Gateway container image +├── .github/workflows/ # CI/CD +├── Dockerfile # Backend container image (Rust + Node runtime) ├── Makefile # Build commands └── Cargo.toml # Rust workspace ``` @@ -344,19 +354,90 @@ LiveAgent/ --- +## v2.0 迁移指南 / v2.0 Migration Guide + +**v2.0 会改掉远程访问的整个架构。已经部署 `ghcr.io/stack-cairn/liveagent-gateway` +的用户一定会断 —— 这是无法避免的破坏性变更,不是 bug。** + +### 为什么会断 + +旧模型:桌面端**主动拨出**连到 Gateway,浏览器再连 Gateway,Gateway 在中间转发。 +新模型:后端(Rust + Node)自己就是服务端,**前端直接连后端**。 + +两边都在等对方来连,技术上对不上,没有兼容层可写。 + +### 你的选择 + +| 情况 | 怎么办 | +|---|---| +| 现在跑得好好的,不想动 | **什么都不用做。** 旧镜像 tag 冻结保留、可以继续拉;旧桌面端配旧网关继续可用 | +| 想升到 v2.0 | 按下面的步骤迁移 | + +旧镜像会一直留在 registry 里,但**不再收到更新**(包括安全修复)。 + +### 迁移步骤 + +1. **把旧版本钉死,不要用 `:latest`。** 升级前先确认旧部署用的是具体 tag: + + ```bash + docker pull ghcr.io/stack-cairn/liveagent-gateway:v1 # 冻结的旧 tag + ``` + +2. **备份 Gateway 数据卷。** 里面有 Agent token 和数据库: + + ```bash + docker run --rm -v liveagent-gateway-data:/data -v "$PWD":/backup \ + alpine tar czf /backup/liveagent-gateway-backup.tar.gz -C /data . + ``` + +3. **在要远程访问的那台机器上部署 v2.0 后端**(替代 Gateway 容器)。 + 它同时提供 HTTP API、WebSocket 和前端静态资源,一个端口。 + +4. **前端只需要两样东西:base URL + 密码。** + 「连到哪个 Gateway」「Agent ID」「自动重连」「心跳间隔」这些设置项在 v2.0 + 不存在了 —— 本地和远程的唯一差别就是那个 base URL: + + ```ts + const backend = createBackendClient({ baseUrl, password }); + ``` + + - 桌面版:壳自动注入密码,跳过登录页,双击即用 + - 浏览器:访问后端地址,走登录页输密码 + +5. **旧 Agent token 不迁移。** v2.0 用密码直接当 Bearer token,旧的 Agent token + 体系没有对应物。部署后端时重新设一个密码即可。 + +6. **确认无误后再删旧容器:** + + ```bash + docker rm -f liveagent-gateway + ``` + +### 新桌面端连旧网关会怎样 + +不会静默失败,但目前的提示是**调用时**给的,不是配置时给的:前端仍残留的 +`gateway_*` 调用点会被本地拦下并抛出 + +> v2 不再需要 Gateway:桌面端不再外拨连接它,改为前端直连后端(本机或远程)。 +> 迁移步骤见 README 的 v2 迁移指南。 + +「在设置页检测到旧网关地址就直接提示」还没做。 + +--- + ## FAQ
-Does my API key ever leave my machine? +Where do my API keys live? -No. Keys are stored locally on the desktop side only. The Gateway is a pure protocol relay — it never accesses the file system and never stores any credentials. +Only on the backend — and you decide where the backend runs. The frontend never sees a key: it sends commands, the backend calls the model. If you run the backend on your own laptop the keys never leave the machine; if you deploy it to your own server, they live there and nowhere else. There is no service of ours in the path.
-Do I have to deploy the Gateway? +Do I have to deploy anything? -No. The desktop client works standalone with all local capabilities; deploy the Gateway only when you need browser-based remote access to your local agent. +Not for local use — the desktop app ships the backend inside it and works out of the box. Deploy the backend separately when you want to reach the same agent from a browser, from another machine, or keep it running while your laptop is closed.
@@ -370,7 +451,7 @@ Claude (Anthropic), Codex (OpenAI), and Gemini protocols are built in, plus cust
Will long conversations / disconnects lose context? -No. The desktop app persists the full history with Segment + Summary Checkpoints; the Gateway replays short disconnects through a bounded seq window and converges automatically after reconnecting. +No. The backend persists the full history with Segment + Summary Checkpoints, and it keeps running while you are disconnected — the frontend reconnects to the event stream and catches up.
@@ -382,22 +463,20 @@ Issues and pull requests are welcome! See the [Development Guide](docs/operation Before submitting a PR, make sure all of the following checks pass (they match the CI gates): -**Desktop client · `crates/agent-gui`** +**Frontend · `crates/frontend`** 1. Type check & build pass: `pnpm build` 2. Lint passes: `pnpm lint` 3. Frontend unit tests pass: `pnpm test:frontend` (also run `pnpm test:release` when touching release scripts) -4. Rust backend check passes: `cargo check --manifest-path crates/agent-gui/src-tauri/Cargo.toml --tests` (run from the repo root) +4. Desktop shell check passes: `cargo check --manifest-path crates/frontend/src-tauri/Cargo.toml --tests` (run from the repo root) -**Gateway · `crates/agent-gateway` (if changed)** +**Backend · `crates/backend` (if changed)** -1. Go unit tests pass: `go test ./...` -2. WebUI build / lint / tests pass: `pnpm build && pnpm lint && pnpm test` (run in `web/`) -3. Regenerate and commit artifacts after proto changes: `make proto` +1. Generated routes are in sync: `make check-routes` (adding a command without a route must fail here) +2. Backend tests pass: `cargo test -p backend` -**Cross-frontend consistency** +**Diff hygiene** -- Mirrored files between GUI and WebUI must be byte-identical: `node scripts/check-mirror.mjs` - Keep the diff clean (no trailing whitespace): `git diff --check` --- diff --git a/README.zh-CN.md b/README.zh-CN.md index 4cf483cc5..92b125111 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -5,8 +5,8 @@

LiveAgent

- Your Local-First AI Agent Desktop
- 多模型接入 · 本地工具执行 · MCP & Skills 生态 · 远程 Gateway + Your Self-Hosted AI Agent Workspace
+ 多模型接入 · 真实工具执行 · MCP & Skills 生态 · 浏览器与桌面端共用一个后端

@@ -18,7 +18,7 @@ Tauri React Rust - Go + Node License

@@ -79,11 +79,11 @@ ## 为什么是 LiveAgent? -LiveAgent 是一个 **本地优先** 的 AI Agent 桌面客户端。它将大语言模型的推理能力与本地系统工具深度整合,让 AI 能够真正操作你的文件系统、执行命令、管理定时任务,同时通过 Gateway 实现远程访问与协作。 +LiveAgent 是一个 **自部署** 的 AI Agent。它将大语言模型的推理能力与真实系统工具深度整合,让 AI 能够真正操作你的文件系统、执行命令、管理定时任务;桌面端和浏览器连的是同一个后端。 - **真正动手的 Agent** — 不止于对话:读写文件、精确编辑、执行 Bash、托管长驻进程 - **生态完全开放** — MCP 协议桥接任意外部工具,Skills 技能包按需加载 -- **本地与远程兼得** — 桌面端独立可用,部署 Gateway 后浏览器随处操控 +- **密钥只在后端** — 只有后端持有凭据,而后端跑在哪里由你决定:自己的笔记本、家里的服务器,或者你自己的 VPS --- @@ -107,7 +107,7 @@ LiveAgent 是一个 **本地优先** 的 AI Agent 桌面客户端。它将大语 ### 🧩 MCP 与 Skills 生态 -- **MCP 协议桥接** — Tauri 端原生桥接任意 stdio / http MCP Server,无限扩展工具能力 +- **MCP 协议桥接** — 后端原生桥接任意 stdio / http MCP Server,无限扩展工具能力 - **Skills 技能包** — 渐进式披露、按需加载,支持安装 / 创建 / 打包与 ClawHub 生态 ### 💾 记忆与自动化 @@ -115,10 +115,10 @@ LiveAgent 是一个 **本地优先** 的 AI Agent 桌面客户端。它将大语 - **持久化记忆** — Markdown + SQLite FTS 全文检索,跨会话知识管理 - **定时任务** — bash / http / prompt 三种 Cron 任务类型,后台自动执行 -### 🌐 远程 Gateway +### 🌐 浏览器与桌面端,同一个后端 -- **浏览器随处访问** — Go 网关(WebSocket + Protobuf),WebUI 远程操控本地 Agent -- **断线可恢复** — 有界 seq window 补齐短时断线,桌面端持久化兜底 +- **一份代码,两个壳** — 桌面端和浏览器跑的是同一份前端构建产物,唯一区别是指向哪个后端地址 +- **断线可恢复** — 事件 WebSocket 断线重连并补齐,后端持久化兜底 --- @@ -163,9 +163,18 @@ LiveAgent 是一个 **本地优先** 的 AI Agent 桌面客户端。它将大语 | DEB | Debian / Ubuntu 系 | `sudo dpkg -i LiveAgent-<版本>-Linux-x86_64.deb` | | RPM | Fedora / openSUSE 系 | `sudo rpm -i LiveAgent-<版本>-Linux-x86_64.rpm` | -### 需要远程访问? 部署 Gateway +### 需要远程访问?(旧版 Gateway) -桌面端开箱即用,不依赖任何服务端。只有想 **在浏览器里远程操控本地 Agent** 时,才需要部署 Gateway。 +> **⚠️ 计划在 v2.0 停用** —— 下面这套 Gateway 部署方式在 v2.0 会断。 +> 已经部署的用户请先读 [v2.0 迁移指南](README.md#v20-迁移指南--v20-migration-guide)。 + +v2.0 没有 Gateway 这一层:你把 **后端** 部署在想要的位置,桌面端或浏览器直连它。 +如果后端在 NAT 后面而你在外面,那是 **网络层的问题,用网络层的办法解决** —— +[Tailscale](https://tailscale.com/)、[frp](https://github.com/fatedier/frp) 或 +[Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/), +应用本身不再内置任何会合注册与打洞逻辑。 + +下面是 v1 的 Gateway 部署方式,保留给已经在用的人。 **注意:在部署并使用Nginx反向代理后,设置中Remote页面Gateway地址填写Https地址,端口号填写443。** @@ -243,47 +252,51 @@ location / { 展开下方「开发指南」查看完整 Make 命令。 -![](docs/images/architecture.webp) -
架构总览 — 架构图与技术栈 ``` ┌──────────────────────────────────────────────────────────────┐ -│ Browser WebUI │ -│ React + Vite + WebSocket + Gateway API │ +│ 前端(同一份代码) │ +│ React 19 · Vite · 跑在浏览器标签页里,或跑在 Tauri 2 │ +│ 桌面壳里 —— 构建产物完全相同 │ └────────────────────────────┬─────────────────────────────────┘ - │ WebSocket / HTTP + │ HTTP POST /api/ (JSON) + │ WebSocket /api/events (JSON) ┌────────────────────────────▼─────────────────────────────────┐ -│ Agent Gateway │ -│ Go · WebSocket · HTTP · Session Manager · Event Store │ -│ (Railway / Docker / 自部署) │ +│ 后端 · backend │ +│ Rust · axum · SQLite · 密码鉴权 · 前端静态资源托管 │ +│ 唯一对外监听者。密钥只存在这一层。 │ +│ (笔记本 / 家庭服务器 / Docker / VPS) │ └────────────────────────────┬─────────────────────────────────┘ - │ WebSocket v2 (双向流) + │ loopback HTTP(不对外暴露) ┌────────────────────────────▼─────────────────────────────────┐ -│ Agent GUI │ -│ Tauri 2 · React 19 · Rust │ +│ 引擎 · core │ +│ Node 22 · TypeScript · pi-agent-core │ ├──────────┬───────────┬───────────┬───────────┬───────────────┤ │ 模型协议 │ Agent运行时 │ 工具执行 │ Skills │ Memory/Cron │ │ pi-ai │ 多轮循环 │ FS/Bash/ │ 渐进披露 │ SQLite+MD │ -│ + Codex │ + SubAgent │ MCP桥接 │ + Hub │ FTS索引 │ +│ │ + SubAgent │ MCP桥接 │ + Hub │ FTS索引 │ └──────────┴───────────┴───────────┴───────────┴───────────────┘ ``` +桌面壳就是个壳:它渲染前端,外加托盘、通知、文件对话框这些原生能力。 +它不是第二套实现 —— 把同一份前端指向远程后端地址,行为完全一致。 + **技术栈** | 组件 | 技术 | |---|---| -| **Agent GUI** · 框架 | Tauri 2 + React 19 + TypeScript 6 | -| **Agent GUI** · 构建 | Vite 8 + pnpm | -| **Agent GUI** · 样式 | Tailwind CSS 4 + Radix UI | -| **Agent GUI** · 渲染 | streamdown + KaTeX + Mermaid + Monaco Editor | -| **Agent GUI** · 后端 | Rust + Tokio + SQLite (rusqlite) + WebSocket (tokio-tungstenite) | -| **Agent GUI** · LLM | @earendil-works/pi-ai · @earendil-works/pi-agent-core | -| **Gateway** · 语言 | Go 1.25 | -| **Gateway** · 协议 | WebSocket + Protobuf + HTTP | -| **Gateway** · Web UI | React + Vite + Tailwind CSS(嵌入式) | -| **Gateway** · 部署 | Docker multi-stage · Railway CI/CD | +| **前端** · 框架 | React 19 + TypeScript 6 | +| **前端** · 构建 | Vite 8 + pnpm | +| **前端** · 样式 | Tailwind CSS 4 + Base UI | +| **前端** · 渲染 | streamdown + KaTeX + Mermaid + Monaco Editor | +| **桌面壳** | Tauri 2(可选 —— 浏览器是一等公民) | +| **后端** · `backend` | Rust + Tokio + axum + SQLite (rusqlite) | +| **后端** · 协议 | JSON over HTTP + WebSocket | +| **引擎** · `core` | Node 22 + TypeScript | +| **引擎** · LLM | @earendil-works/pi-ai · @earendil-works/pi-agent-core | +| **部署** | Docker(后端与 Node runtime 同一镜像)· Railway CI/CD |
@@ -294,15 +307,12 @@ location / { |---|---| | `make dev` | 启动 Tauri 开发环境 | | `make build` | 构建桌面应用 | -| `make dev-gateway` | 启动 Gateway 开发服务 | -| `make dev-webui` | 启动 WebUI 开发服务 | -| `make gateway-build` | 构建 Gateway 二进制 | -| `make gateway-docker-build` | 构建 Docker 镜像 | -| `make gateway-docker-smoke` | 构建 + 健康检查 | +| `make backend-docker-build` | 构建后端 Docker 镜像 | +| `make backend-docker-run` | 运行后端镜像(HTTPS 8443) | +| `make backend-docker-smoke` | 构建 + `/healthz` 健康检查 | | `make desktop-build-macos-release` | macOS 签名发布构建 | -| `make build-linux` | Linux amd64 网关 | -| `make build-linux-arm` | Linux arm64 网关 | -| `make proto` | 重新生成 Protobuf 代码 | +| `make update-routes` | 从 command wrapper 重新生成后端路由层 | +| `make check-routes` | 校验生成的路由是否漂移(CI 门禁) | | `make clean` | 清理构建产物 | @@ -313,20 +323,23 @@ location / { ``` LiveAgent/ ├── crates/ -│ ├── agent-gui/ # 桌面客户端 -│ │ ├── src/ # React 前端 +│ ├── frontend/ # 前端 + 桌面壳 +│ │ ├── src/ # React 前端(浏览器与桌面端共用) │ │ │ ├── components/ # UI 组件 │ │ │ ├── lib/ # 核心逻辑 (chat, tools, skills, memory) │ │ │ ├── pages/ # 页面 (Chat, Settings) │ │ │ ├── i18n/ # 国际化 │ │ │ └── prompt/ # System Prompt 模板 -│ │ └── src-tauri/ # Rust 后端 (Tauri) +│ │ └── src-tauri/ # Tauri 2 桌面壳 (Rust) +│ │ +│ ├── backend/ # Rust 后端 —— 唯一对外监听者, +│ │ │ # 同时是共享核心(工具、运行时、存储) +│ │ ├── src/server/ # HTTP 命令路由与事件 WebSocket +│ │ ├── src/engine_process.rs # 拉起并守护 Node 引擎进程 +│ │ └── src/engine_proxy.rs # chat 请求反向代理与事件回流 │ │ -│ └── agent-gateway/ # Go 网关服务 -│ ├── cmd/gateway/ # 入口 -│ ├── internal/ # 核心实现 -│ ├── proto/v2/ # Protobuf 定义 -│ └── web/ # 嵌入式 WebUI +│ └── core/ # Node 引擎 —— 模型调用与 Agent 循环 +│ └── src/ # TypeScript,esbuild 打包 │ ├── docs/ # 项目文档 │ ├── architecture/ # 架构设计 @@ -334,8 +347,8 @@ LiveAgent/ │ └── operations/ # 运维部署 │ ├── scripts/release/ # 发布自动化 -├── .github/workflows/ # CI/CD (CI + Desktop Release + Gateway Docker) -├── Dockerfile # Gateway 容器镜像 +├── .github/workflows/ # CI/CD +├── Dockerfile # 后端容器镜像(Rust + Node runtime) ├── Makefile # 构建命令集 └── Cargo.toml # Rust workspace ``` @@ -347,16 +360,16 @@ LiveAgent/ ## FAQ
-API Key 会离开本机吗? +我的 API Key 存在哪? -不会。秘钥仅保存在桌面端本地,Gateway 只做协议中继 — 不访问文件系统、不存储任何凭据。 +只在后端 —— 而后端跑在哪里由你决定。前端从头到尾看不到密钥:它只发命令,由后端去调模型。后端跑在自己笔记本上,密钥就不出这台机器;部署到自己的服务器上,密钥就只在那台服务器上。链路里没有任何我们的服务。
-必须部署 Gateway 吗? +必须部署什么吗? -不需要。桌面客户端可独立使用全部本地能力;只有需要从浏览器远程访问本地 Agent 时,才部署 Gateway。 +本地用不需要 —— 桌面端自带后端,开箱即用。只有当你想从浏览器、从另一台机器访问同一个 Agent,或者想在合上笔记本后让它继续跑,才需要单独部署后端。
@@ -370,7 +383,7 @@ LiveAgent/
长对话 / 断线后上下文会丢吗? -不会。桌面端以 Segment + Summary Checkpoint 持久化完整历史;Gateway 通过有界 seq window 补齐短时断线,重连后自动收敛。 +不会。后端以 Segment + Summary Checkpoint 持久化完整历史,并且在你断线期间继续跑 —— 前端重连事件流后自动补齐。
@@ -382,22 +395,20 @@ LiveAgent/ 提交 PR 前,请确保以下检查全部通过(与 CI 门禁一致): -**桌面客户端 · `crates/agent-gui`** +**前端 · `crates/frontend`** 1. 类型检查与构建通过:`pnpm build` 2. 代码规范检查通过:`pnpm lint` 3. 前端单元测试通过:`pnpm test:frontend`(改动发布脚本时另跑 `pnpm test:release`) -4. Rust 后端检查通过:`cargo check --manifest-path crates/agent-gui/src-tauri/Cargo.toml --tests`(仓库根目录执行) +4. 桌面壳检查通过:`cargo check --manifest-path crates/frontend/src-tauri/Cargo.toml --tests`(仓库根目录执行) -**Gateway · `crates/agent-gateway`(如有改动)** +**后端 · `crates/backend`(如有改动)** -1. Go 单元测试通过:`go test ./...` -2. WebUI 构建 / Lint / 测试通过:`pnpm build && pnpm lint && pnpm test`(在 `web/` 目录执行) -3. Proto 变更后重新生成并提交产物:`make proto` +1. 生成的路由无漂移:`make check-routes`(新增 command 未加路由必须在这里失败) +2. 后端测试通过:`cargo test -p backend` -**跨端一致性** +**Diff 卫生** -- GUI 与 WebUI 的镜像文件必须逐字节一致:`node scripts/check-mirror.mjs` - 保持 diff 干净 (无行尾空白):`git diff --check` --- diff --git a/crates/agent-gateway/.gitignore b/crates/agent-gateway/.gitignore deleted file mode 100644 index 1aa7f87b7..000000000 --- a/crates/agent-gateway/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -bin/ -/gateway -web/node_modules/ -web/dist/ -web/*.tsbuildinfo -web/vite.config.js -web/vite.config.d.ts diff --git a/crates/agent-gateway/.golangci.yml b/crates/agent-gateway/.golangci.yml deleted file mode 100644 index b73acf17d..000000000 --- a/crates/agent-gateway/.golangci.yml +++ /dev/null @@ -1,24 +0,0 @@ -# golangci-lint v2 —— 网关 Go 代码检查基线(此前无 linter,选务实检查集):govet/staticcheck/unused -# 管正确性与死代码,errcheck/ineffassign/misspell 做基础卫生。 -# 生成代码(internal/proto/)不参与检查。 -version: "2" - -linters: - default: none - enable: - - govet - - staticcheck - - unused - - errcheck - - ineffassign - - misspell - exclusions: - generated: lax - rules: - # 测试代码允许忽略清理类调用的返回值。 - - path: _test\.go - linters: [errcheck] - -issues: - max-issues-per-linter: 0 - max-same-issues: 0 diff --git a/crates/agent-gateway/buf.gen.yaml b/crates/agent-gateway/buf.gen.yaml deleted file mode 100644 index 5ff20981f..000000000 --- a/crates/agent-gateway/buf.gen.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# buf 代码生成配置(v2 schema)。Go 插件用 mise 固定版本、`module=` 与历史 protoc 一致, -# 输出 internal/proto/;TS 插件 protoc-gen-es 由 web/node_modules 锁定,输出 web/src/lib/proto/gen/。 -version: v2 -plugins: - - local: protoc-gen-go - out: . - opt: module=github.com/liveagent/agent-gateway - - local: web/node_modules/.bin/protoc-gen-es - out: web/src/lib/proto/gen - opt: target=ts -inputs: - - directory: . diff --git a/crates/agent-gateway/buf.yaml b/crates/agent-gateway/buf.yaml deleted file mode 100644 index 046b2637d..000000000 --- a/crates/agent-gateway/buf.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# buf 模块配置(v2 schema)。模块根设在 crates/agent-gateway/,供 v2 帧壳导入共享业务消息。 -version: v2 -modules: - - path: . - excludes: - # 排除 WebUI node_modules 里的第三方 .proto。 - - web -lint: - use: - - STANDARD - except: - # 历史布局 proto/<版本>/ 早于 buf;迁目录会改 import 路径、破坏 Rust build.rs 与生成物,不翻新。 - - PACKAGE_DIRECTORY_MATCH - # gateway.proto 中已发布的枚举命名保持不变,仅保留必要的 lint 豁免。 - ignore_only: - ENUM_VALUE_PREFIX: - - proto/v2/gateway.proto - ENUM_ZERO_VALUE_SUFFIX: - - proto/v2/gateway.proto -breaking: - use: - # 同时保护 Protobuf 二进制线格式与 proto JSON 表示的兼容性。 - - WIRE_JSON diff --git a/crates/agent-gateway/cmd/gateway/main.go b/crates/agent-gateway/cmd/gateway/main.go deleted file mode 100644 index a76686236..000000000 --- a/crates/agent-gateway/cmd/gateway/main.go +++ /dev/null @@ -1,88 +0,0 @@ -package main - -import ( - "context" - "errors" - "log/slog" - "net/http" - "os" - "os/signal" - "syscall" - "time" - - "github.com/liveagent/agent-gateway/internal/auth/agenttoken" - "github.com/liveagent/agent-gateway/internal/config" - "github.com/liveagent/agent-gateway/internal/db" - "github.com/liveagent/agent-gateway/internal/observability" - "github.com/liveagent/agent-gateway/internal/server" - "github.com/liveagent/agent-gateway/internal/session" -) - -// fatal 记录错误并以非零码退出(slog 没有 Fatal 级别,集中在此处理)。 -func fatal(msg string, args ...any) { - slog.Error(msg, args...) - os.Exit(1) -} - -func main() { - observability.SetupLogging() - cfg := config.Load() - sm := session.NewManager() - - // 统一连接池:库由 internal/db 打开并集中管理,各持久化子系统在共享池上 - // 初始化自己的表;main 持有生命周期(退出时统一关闭)。 - database, err := db.Open(cfg.AgentDB) - if err != nil { - fatal("open gateway db failed", "path", cfg.AgentDB, "err", err) - } - defer func() { _ = database.Close() }() - - tokens, err := agenttoken.NewStore(database) - if err != nil { - fatal("init agent token store failed", "err", err) - } - slog.Info("agent registry db ready", "path", cfg.AgentDB) - slog.Info("agent authentication accepts gateway token or per-agent token") - - httpServer := &http.Server{ - Addr: cfg.HTTPAddr, - Handler: server.NewHTTPServer(cfg, sm, tokens), - ReadHeaderTimeout: 10 * time.Second, - // 空闲 keep-alive 连接必须回收,否则 REST/静态资源访问方挂住连接会把 fd - // 慢性耗尽到 ulimit。刻意不设全局 Read/WriteTimeout:流式上传与隧道长响应 - // 需要;WS 连接已被 hijack、自管理超时,不受影响。 - IdleTimeout: 120 * time.Second, - } - - errCh := make(chan error, 1) - - go func() { - slog.Info("HTTP listening", "addr", cfg.HTTPAddr) - var serveErr error - if cfg.TLSCert != "" || cfg.TLSKey != "" { - serveErr = httpServer.ListenAndServeTLS(cfg.TLSCert, cfg.TLSKey) - } else { - serveErr = httpServer.ListenAndServe() - } - if serveErr != nil && !errors.Is(serveErr, http.ErrServerClosed) { - errCh <- serveErr - } - }() - - signalCh := make(chan os.Signal, 1) - signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM) - - select { - case sig := <-signalCh: - slog.Info("received signal, shutting down", "signal", sig.String()) - case err := <-errCh: - fatal("server error", "err", err) - } - - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - if err := httpServer.Shutdown(ctx); err != nil { - slog.Warn("http shutdown error", "err", err) - } -} diff --git a/crates/agent-gateway/embed.go b/crates/agent-gateway/embed.go deleted file mode 100644 index fe1d98ef4..000000000 --- a/crates/agent-gateway/embed.go +++ /dev/null @@ -1,12 +0,0 @@ -package gateway - -import "embed" - -// WebUIAssets contains the embedded WebUI build output served by the HTTP server. -// -// The all: prefix is required because Vite may emit chunks whose names begin -// with "_" (for example, lodash's _baseFor chunk). Plain directory embeds -// silently exclude files and directories beginning with "." or "_". -// -//go:embed all:web/dist -var WebUIAssets embed.FS diff --git a/crates/agent-gateway/embed_test.go b/crates/agent-gateway/embed_test.go deleted file mode 100644 index 1596c6788..000000000 --- a/crates/agent-gateway/embed_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package gateway - -import ( - "io/fs" - "os" - "sort" - "testing" -) - -func TestWebUIAssetsIncludeEntireDistTree(t *testing.T) { - diskFiles := regularFileSizes(t, os.DirFS("."), "web/dist") - embeddedFiles := regularFileSizes(t, WebUIAssets, "web/dist") - - var missing []string - for file, size := range diskFiles { - embeddedSize, ok := embeddedFiles[file] - if !ok { - missing = append(missing, file) - continue - } - if embeddedSize != size { - t.Fatalf("embedded WebUI asset %q size = %d, want %d", file, embeddedSize, size) - } - } - - if len(missing) > 0 { - sort.Strings(missing) - t.Fatalf("embedded WebUI assets are missing files from web/dist: %v", missing) - } -} - -func regularFileSizes(t *testing.T, fileSystem fs.FS, root string) map[string]int64 { - t.Helper() - - files := make(map[string]int64) - err := fs.WalkDir(fileSystem, root, func(path string, entry fs.DirEntry, walkErr error) error { - if walkErr != nil { - return walkErr - } - if entry.IsDir() { - return nil - } - info, err := entry.Info() - if err != nil { - return err - } - if info.Mode().IsRegular() { - files[path] = info.Size() - } - return nil - }) - if err != nil { - t.Fatalf("walk %q: %v", root, err) - } - return files -} diff --git a/crates/agent-gateway/go.mod b/crates/agent-gateway/go.mod deleted file mode 100644 index 5993762ca..000000000 --- a/crates/agent-gateway/go.mod +++ /dev/null @@ -1,27 +0,0 @@ -module github.com/liveagent/agent-gateway - -go 1.25.12 - -require ( - github.com/doyensec/safeurl v0.2.5 - github.com/gabriel-vasile/mimetype v1.4.13 - github.com/google/uuid v1.6.0 - github.com/gorilla/websocket v1.5.3 - github.com/klauspost/compress v1.18.0 - github.com/tdewolff/parse/v2 v2.8.13 - golang.org/x/net v0.57.0 - google.golang.org/protobuf v1.36.11 - modernc.org/sqlite v1.54.0 -) - -require ( - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/ncruces/go-strftime v1.0.0 // indirect - github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - golang.org/x/sync v0.22.0 // indirect - golang.org/x/sys v0.47.0 // indirect - modernc.org/libc v1.74.1 // indirect - modernc.org/mathutil v1.7.1 // indirect - modernc.org/memory v1.11.0 // indirect -) diff --git a/crates/agent-gateway/go.sum b/crates/agent-gateway/go.sum deleted file mode 100644 index 39f5aa74b..000000000 --- a/crates/agent-gateway/go.sum +++ /dev/null @@ -1,69 +0,0 @@ -github.com/doyensec/safeurl v0.2.5 h1:kKu0JNQy0tJ8jkDyB5h6Aml9vWWniq+mpoa12EGLcOQ= -github.com/doyensec/safeurl v0.2.5/go.mod h1:3H0cgRpPYPSpgxRRn5yGD35Ns/LgGX/BVWSBbzUqXtY= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= -github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= -github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= -github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= -github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/tdewolff/parse/v2 v2.8.13 h1:si/8rLw5BZZTWCCiMm9A3f6x+RmqYfrkEeXCgpX5ick= -github.com/tdewolff/parse/v2 v2.8.13/go.mod h1:XdsoSFThlVIRIajAuqz1evNY7bagZS8LBOPA3aVopwQ= -github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk= -github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= -golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= -golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= -golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= -golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= -golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -modernc.org/cc/v4 v4.29.0 h1:CXgwL8cvxmyzBQZzbSl/6xFtMCryb6u8IOqDci39cgc= -modernc.org/cc/v4 v4.29.0/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= -modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU= -modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk= -modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= -modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= -modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= -modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= -modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI= -modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= -modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= -modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.74.1 h1:bdR4VTKFMC4966QSNZ05XLGI/VwzVa2kTUX51Dm0riQ= -modernc.org/libc v1.74.1/go.mod h1:uH4t5bOx3G3g9Xcmj10YKlTcVISlRDwv8VoQJG9n8Os= -modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= -modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= -modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= -modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= -modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= -modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= -modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= -modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.54.0 h1:JCxR4qwkJvOaqAoYcgDoO25Nc+ROg6EJ2LfBVzdrgog= -modernc.org/sqlite v1.54.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw= -modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= -modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= -modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= -modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/crates/agent-gateway/internal/auth/agenttoken/store.go b/crates/agent-gateway/internal/auth/agenttoken/store.go deleted file mode 100644 index 876459035..000000000 --- a/crates/agent-gateway/internal/auth/agenttoken/store.go +++ /dev/null @@ -1,519 +0,0 @@ -// Package agenttoken 维护持久化 Agent 目录及每 Agent 独立、可轮换和删除的接入凭证。 -// 凭证明文只在签发响应中出现一次,落库仅存 SHA-256。 -package agenttoken - -import ( - "crypto/rand" - "crypto/sha256" - "crypto/subtle" - "database/sql" - "encoding/base64" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "strings" - "sync" - "time" - "unicode/utf8" - - "github.com/google/uuid" - "github.com/liveagent/agent-gateway/internal/db" -) - -// tokenPrefix 便于在日志/配置里一眼识别凭证类型(不参与校验)。 -const tokenPrefix = "agt_" - -// tokenEntropyBytes 是凭证随机部分的字节数(32 字节 ≈ 43 个 base64url 字符)。 -const tokenEntropyBytes = 32 - -const maxAgentNameLength = 64 - -var ErrAgentIDRequired = errors.New("agent id is required") - -var ErrInvalidAgentID = errors.New("agent id must be a canonical agent UUID v4") - -var ErrAgentNameTooLong = errors.New("agent name must not exceed 64 characters") - -var ErrAgentNotFound = errors.New("agent not found") - -var ErrInvalidStatusFilter = errors.New("invalid agent status filter") - -var ErrUnauthorized = errors.New("unauthorized agent credential") - -// dummyHashHex 是未知 agent_id 时参与比较的占位哈希(长度与真实哈希一致、 -// 恒不匹配),保证未知 id 与错误凭证的校验路径耗时一致、不暴露 id 是否存在。 -var dummyHashHex = strings.Repeat("0", sha256.Size*2) - -// DirectoryEntry 是持久化 Agent 目录条目。 -type DirectoryEntry struct { - AgentID string - Name string - RegisteredAt time.Time - HasToken bool - TokenCreatedAt time.Time -} - -// Store 是 Agent 目录和凭证子系统句柄;由 Gateway 启动时创建并始终可用。 -type Store struct { - pool *sql.DB - knownAgents sync.Map - credentialMu sync.RWMutex - credentialEpochs map[string]uint64 -} - -// NewStore 在 Gateway 共享库上初始化首版 Agent 单表结构。 -func NewStore(database *db.DB) (*Store, error) { - if database == nil || !database.Enabled() { - return nil, errors.New("gateway database is required") - } - pool := database.Pool() - if _, err := pool.Exec(` - CREATE TABLE IF NOT EXISTS agents ( - agent_id TEXT PRIMARY KEY, - name TEXT NOT NULL DEFAULT '' CHECK (length(name) <= 64), - token_sha256 TEXT, - created_at INTEGER NOT NULL, - token_issued_at INTEGER, - CHECK ((token_sha256 IS NULL) = (token_issued_at IS NULL)) - ); - CREATE INDEX IF NOT EXISTS idx_agents_created_at_agent_id - ON agents(created_at, agent_id); - `); err != nil { - return nil, fmt.Errorf("init agent schema: %w", err) - } - store := &Store{pool: pool, credentialEpochs: make(map[string]uint64)} - if err := store.loadKnownAgents(); err != nil { - return nil, err - } - return store, nil -} - -// loadKnownAgents 在 Gateway 开始监听前一次性恢复已登记 ID 缓存。只读取主键, -// 不缓存凭证;独立 Token 的轮换和撤销仍由每次连接时的数据库校验保证。 -func (s *Store) loadKnownAgents() error { - rows, err := s.pool.Query(`SELECT agent_id FROM agents`) - if err != nil { - return fmt.Errorf("load known agents: %w", err) - } - for rows.Next() { - var agentID string - if err := rows.Scan(&agentID); err != nil { - _ = rows.Close() - return fmt.Errorf("load known agents: %w", err) - } - s.knownAgents.Store(agentID, struct{}{}) - } - if err := rows.Err(); err != nil { - _ = rows.Close() - return fmt.Errorf("load known agents: %w", err) - } - if err := rows.Close(); err != nil { - return fmt.Errorf("load known agents: %w", err) - } - return nil -} - -// NormalizeAgentID 接受桌面端自动生成的规范 agent-UUIDv4 标识。 -func NormalizeAgentID(raw string) (string, error) { - agentID := strings.TrimSpace(raw) - if agentID == "" { - return "", ErrAgentIDRequired - } - const prefix = "agent-" - if !strings.HasPrefix(agentID, prefix) { - return "", ErrInvalidAgentID - } - parsed, err := uuid.Parse(strings.TrimPrefix(agentID, prefix)) - if err != nil || parsed.Version() != 4 || prefix+parsed.String() != agentID { - return "", ErrInvalidAgentID - } - return agentID, nil -} - -func normalizeName(raw string) (string, error) { - name := strings.TrimSpace(raw) - if utf8.RuneCountInString(name) > maxAgentNameLength { - return "", ErrAgentNameTooLong - } - return name, nil -} - -// 分页默认与上限(沿用项目 history 列表的钳制风格)。 -const ( - defaultPageSize = 50 - maxPageSize = 200 -) - -// StatusFilter 是 Agent 目录的实时在线状态筛选条件。 -type StatusFilter string - -const ( - StatusAll StatusFilter = "all" - StatusOnline StatusFilter = "online" - StatusOffline StatusFilter = "offline" -) - -// ParseStatusFilter 解析管理 API 的状态筛选;空值等同于 all。 -func ParseStatusFilter(raw string) (StatusFilter, error) { - filter := StatusFilter(strings.ToLower(strings.TrimSpace(raw))) - if filter == "" { - return StatusAll, nil - } - switch filter { - case StatusAll, StatusOnline, StatusOffline: - return filter, nil - default: - return "", ErrInvalidStatusFilter - } -} - -// PageParams 分页入参;状态筛选和分页都由 SQLite 执行。 -type PageParams struct { - Page int - PageSize int - Status StatusFilter - OnlineAgentIDs []string -} - -func (p PageParams) normalized() (page, pageSize, offset int) { - page = p.Page - if page < 1 { - page = 1 - } - pageSize = p.PageSize - switch { - case pageSize <= 0: - pageSize = defaultPageSize - case pageSize > maxPageSize: - pageSize = maxPageSize - } - return page, pageSize, (page - 1) * pageSize -} - -// Page 是一页 Agent 目录及分页元信息。 -type Page struct { - Entries []DirectoryEntry - Total int - Page int - PageSize int - HasMore bool -} - -func (s *Store) validateLocked(agentID, token string) (bool, error) { - agentID = strings.TrimSpace(agentID) - token = strings.TrimSpace(token) - if agentID == "" || token == "" { - return false, nil - } - - stored := dummyHashHex - known := false - var fetched sql.NullString - err := s.pool.QueryRow(`SELECT token_sha256 FROM agents WHERE agent_id = ?`, agentID).Scan(&fetched) - if err == nil && fetched.Valid && fetched.String != "" { - stored = fetched.String - known = true - } - - presented := sha256.Sum256([]byte(token)) - presentedHex := hex.EncodeToString(presented[:]) - valid := subtle.ConstantTimeCompare([]byte(presentedHex), []byte(stored)) == 1 && known - if err != nil && !errors.Is(err, sql.ErrNoRows) { - return false, fmt.Errorf("query agent credential: %w", err) - } - return valid, nil -} - -// Register 持久化首次成功接入的 Agent。进程内已知 ID 不访问数据库;缓存未命中 -// 时用 INSERT OR IGNORE 原子补登记,避免重复连接产生 WAL 更新。 -func (s *Store) Register(agentID string) error { - if s == nil { - return errors.New("agent registry is not enabled") - } - s.credentialMu.RLock() - defer s.credentialMu.RUnlock() - return s.registerLocked(agentID) -} - -func (s *Store) registerLocked(agentID string) error { - agentID = strings.TrimSpace(agentID) - if agentID == "" { - return ErrAgentIDRequired - } - if _, known := s.knownAgents.Load(agentID); known { - return nil - } - if _, err := s.pool.Exec( - `INSERT OR IGNORE INTO agents (agent_id, name, created_at) VALUES (?, '', ?)`, - agentID, time.Now().Unix(), - ); err != nil { - return fmt.Errorf("register agent: %w", err) - } - s.knownAgents.Store(agentID, struct{}{}) - return nil -} - -// AuthenticateAndRegister 在凭证变更读锁内完成独立凭证校验与目录登记,并返回 -// 当前凭证纪元。sharedAuthenticated 只能由网关共享 Token 的常量时间校验结果提供。 -// Issue/Delete 必须等待本方法退出,避免“旧凭证已校验、轮换后才登记”的窗口。 -func (s *Store) AuthenticateAndRegister( - agentID, token string, - sharedAuthenticated bool, -) (uint64, error) { - if s == nil { - return 0, errors.New("agent token store is not enabled") - } - s.credentialMu.RLock() - defer s.credentialMu.RUnlock() - if !sharedAuthenticated { - valid, err := s.validateLocked(agentID, token) - if err != nil { - return 0, err - } - if !valid { - return 0, ErrUnauthorized - } - } - if err := s.registerLocked(agentID); err != nil { - return 0, err - } - return s.credentialEpochs[strings.TrimSpace(agentID)], nil -} - -// AuthenticationCurrent 供会话在登记锁内确认:从鉴权到登记之间没有发生任何 -// 同 Agent 的凭证轮换或删除;不同 Agent 的并发管理操作互不影响。 -func (s *Store) AuthenticationCurrent(agentID string, epoch uint64) bool { - if s == nil { - return false - } - s.credentialMu.RLock() - defer s.credentialMu.RUnlock() - return s.credentialEpochs[strings.TrimSpace(agentID)] == epoch -} - -// Issue 为 agent_id 生成新凭证并落库,返回明文(仅此一次);已有凭证被轮换顶替。 -func (s *Store) Issue(agentID, name string) (string, error) { - if s == nil { - return "", errors.New("agent token store is not enabled") - } - agentID = strings.TrimSpace(agentID) - if agentID == "" { - return "", ErrAgentIDRequired - } - var err error - name, err = normalizeName(name) - if err != nil { - return "", err - } - - buf := make([]byte, tokenEntropyBytes) - if _, err := rand.Read(buf); err != nil { - return "", fmt.Errorf("generate agent token: %w", err) - } - token := tokenPrefix + base64.RawURLEncoding.EncodeToString(buf) - digest := sha256.Sum256([]byte(token)) - now := time.Now().Unix() - - s.credentialMu.Lock() - defer s.credentialMu.Unlock() - if _, err := s.pool.Exec( - `INSERT INTO agents (agent_id, name, token_sha256, created_at, token_issued_at) - VALUES (?, ?, ?, ?, ?) - ON CONFLICT(agent_id) DO UPDATE SET - name = excluded.name, - token_sha256 = excluded.token_sha256, - token_issued_at = excluded.token_issued_at`, - agentID, name, hex.EncodeToString(digest[:]), now, now, - ); err != nil { - return "", fmt.Errorf("persist agent credential: %w", err) - } - s.knownAgents.Store(agentID, struct{}{}) - s.credentialEpochs[agentID] += 1 - return token, nil -} - -// UpdateName 修改已登记 Agent 的可选名称。 -func (s *Store) UpdateName(agentID, name string) error { - if s == nil { - return ErrAgentNotFound - } - agentID = strings.TrimSpace(agentID) - if agentID == "" { - return ErrAgentIDRequired - } - name, err := normalizeName(name) - if err != nil { - return err - } - s.credentialMu.RLock() - defer s.credentialMu.RUnlock() - result, err := s.pool.Exec(`UPDATE agents SET name = ? WHERE agent_id = ?`, name, agentID) - if err != nil { - return fmt.Errorf("update agent name: %w", err) - } - affected, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("update agent name: %w", err) - } - if affected == 0 { - return ErrAgentNotFound - } - return nil -} - -// Delete 删除 Agent 目录行及其独立凭证。 -func (s *Store) Delete(agentID string) (bool, error) { - if s == nil { - return false, nil - } - agentID = strings.TrimSpace(agentID) - if agentID == "" { - return false, ErrAgentIDRequired - } - s.credentialMu.Lock() - defer s.credentialMu.Unlock() - result, err := s.pool.Exec(`DELETE FROM agents WHERE agent_id = ?`, agentID) - if err != nil { - return false, fmt.Errorf("delete agent: %w", err) - } - affected, err := result.RowsAffected() - if err != nil { - return false, fmt.Errorf("delete agent: %w", err) - } - if affected > 0 { - s.knownAgents.Delete(agentID) - s.credentialEpochs[agentID] += 1 - } - return affected > 0, nil -} - -// Count 返回持久化 Agent 目录总数。 -func (s *Store) Count() (int, error) { - if s == nil { - return 0, nil - } - var total int - if err := s.pool.QueryRow(`SELECT COUNT(*) FROM agents`).Scan(&total); err != nil { - return 0, fmt.Errorf("count agents: %w", err) - } - return total, nil -} - -// List 按状态筛选并分页返回 Agent 目录。在线 ID 作为单个 JSON 参数交给 SQLite -// json_each 展开,避免动态 IN 参数上限;Go 层不会读取全量凭证后再分页。 -func (s *Store) List(params PageParams) (Page, error) { - page, pageSize, offset := params.normalized() - if s == nil { - return Page{Page: page, PageSize: pageSize}, nil - } - - filter, err := ParseStatusFilter(string(params.Status)) - if err != nil { - return Page{}, err - } - whereClause := "" - var filterArg any - switch filter { - case StatusOnline, StatusOffline: - onlineIDs := params.OnlineAgentIDs - if onlineIDs == nil { - onlineIDs = []string{} - } - encoded, marshalErr := json.Marshal(onlineIDs) - if marshalErr != nil { - return Page{}, fmt.Errorf("encode online agent ids: %w", marshalErr) - } - filterArg = string(encoded) - if filter == StatusOnline { - whereClause = ` WHERE a.agent_id IN ( - SELECT CAST(value AS TEXT) FROM json_each(?) - )` - } else { - whereClause = ` WHERE a.agent_id NOT IN ( - SELECT CAST(value AS TEXT) FROM json_each(?) - )` - } - } - - countQuery := `SELECT COUNT(*) FROM agents AS a` + whereClause - var total int - if filter == StatusAll { - err = s.pool.QueryRow(countQuery).Scan(&total) - } else { - err = s.pool.QueryRow(countQuery, filterArg).Scan(&total) - } - if err != nil { - return Page{}, fmt.Errorf("count filtered agents: %w", err) - } - - listQuery := `SELECT a.agent_id, a.name, a.created_at, a.token_issued_at - FROM agents AS a` + whereClause + - ` ORDER BY a.created_at, a.agent_id LIMIT ? OFFSET ?` - - var rows *sql.Rows - if filter == StatusAll { - rows, err = s.pool.Query(listQuery, pageSize, offset) - } else { - rows, err = s.pool.Query(listQuery, filterArg, pageSize, offset) - } - if err != nil { - return Page{}, fmt.Errorf("list filtered agent registry: %w", err) - } - defer func() { _ = rows.Close() }() - - entries := make([]DirectoryEntry, 0, pageSize) - for rows.Next() { - var entry DirectoryEntry - var registeredAt int64 - var tokenCreatedAt sql.NullInt64 - if err := rows.Scan(&entry.AgentID, &entry.Name, ®isteredAt, &tokenCreatedAt); err != nil { - return Page{}, fmt.Errorf("scan agent directory: %w", err) - } - entry.RegisteredAt = time.Unix(registeredAt, 0).UTC() - entry.HasToken = tokenCreatedAt.Valid - if tokenCreatedAt.Valid { - entry.TokenCreatedAt = time.Unix(tokenCreatedAt.Int64, 0).UTC() - } - entries = append(entries, entry) - } - if err := rows.Err(); err != nil { - return Page{}, fmt.Errorf("iterate agent directory: %w", err) - } - - return Page{ - Entries: entries, - Total: total, - Page: page, - PageSize: pageSize, - HasMore: offset+len(entries) < total, - }, nil -} - -// Registered 返回全部持久化 Agent(agent_id 序),供 WS Agent 选择器补全离线项; -// 管理页面的大目录展示必须走数据库分页的 List。 -func (s *Store) Registered() ([]DirectoryEntry, error) { - if s == nil { - return nil, errors.New("agent registry is not enabled") - } - rows, err := s.pool.Query(`SELECT agent_id, name, created_at FROM agents ORDER BY agent_id`) - if err != nil { - return nil, fmt.Errorf("query agent directory: %w", err) - } - defer func() { _ = rows.Close() }() - - var out []DirectoryEntry - for rows.Next() { - var entry DirectoryEntry - var createdAt int64 - if err := rows.Scan(&entry.AgentID, &entry.Name, &createdAt); err != nil { - return nil, fmt.Errorf("scan agent directory: %w", err) - } - entry.RegisteredAt = time.Unix(createdAt, 0).UTC() - out = append(out, entry) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("iterate agent directory: %w", err) - } - return out, nil -} diff --git a/crates/agent-gateway/internal/auth/agenttoken/store_benchmark_test.go b/crates/agent-gateway/internal/auth/agenttoken/store_benchmark_test.go deleted file mode 100644 index aed39677c..000000000 --- a/crates/agent-gateway/internal/auth/agenttoken/store_benchmark_test.go +++ /dev/null @@ -1,302 +0,0 @@ -package agenttoken - -import ( - "fmt" - "path/filepath" - "sync" - "sync/atomic" - "testing" - - "github.com/liveagent/agent-gateway/internal/db" -) - -const benchmarkAgentCount = 1000 - -func openBenchmarkStore(b *testing.B) *Store { - b.Helper() - database, err := db.Open(filepath.Join(b.TempDir(), "gateway.db")) - if err != nil { - b.Fatalf("open benchmark database: %v", err) - } - b.Cleanup(func() { _ = database.Close() }) - store, err := NewStore(database) - if err != nil { - b.Fatalf("open benchmark store: %v", err) - } - return store -} - -func benchmarkAgentID(index int) string { - return fmt.Sprintf("agent-00000000-0000-4000-8000-%012x", index) -} - -func seedBenchmarkAgents(b *testing.B, store *Store, issueTokens bool) ([]string, []string) { - b.Helper() - ids := make([]string, benchmarkAgentCount) - tokens := make([]string, benchmarkAgentCount) - for index := range ids { - ids[index] = benchmarkAgentID(index) - if issueTokens { - token, err := store.Issue(ids[index], "") - if err != nil { - b.Fatalf("issue benchmark token %d: %v", index, err) - } - tokens[index] = token - continue - } - if err := store.Register(ids[index]); err != nil { - b.Fatalf("register benchmark agent %d: %v", index, err) - } - } - return ids, tokens -} - -func BenchmarkOpenStorePreloads1000Agents(b *testing.B) { - path := filepath.Join(b.TempDir(), "gateway.db") - database, err := db.Open(path) - if err != nil { - b.Fatalf("open seed database: %v", err) - } - store, err := NewStore(database) - if err != nil { - _ = database.Close() - b.Fatalf("open seed store: %v", err) - } - ids, _ := seedBenchmarkAgents(b, store, true) - if err := database.Close(); err != nil { - b.Fatalf("close seed database: %v", err) - } - - b.ReportAllocs() - b.ResetTimer() - for range b.N { - database, err := db.Open(path) - if err != nil { - b.Fatalf("reopen benchmark database: %v", err) - } - store, err := NewStore(database) - if err != nil { - _ = database.Close() - b.Fatalf("reopen benchmark store: %v", err) - } - if _, ok := store.knownAgents.Load(ids[0]); !ok { - _ = database.Close() - b.Fatal("first agent was not preloaded") - } - if _, ok := store.knownAgents.Load(ids[len(ids)-1]); !ok { - _ = database.Close() - b.Fatal("last agent was not preloaded") - } - if err := database.Close(); err != nil { - b.Fatalf("close benchmark database: %v", err) - } - } -} - -func BenchmarkAuthenticateAndRegisterSharedTokenCached(b *testing.B) { - store := openBenchmarkStore(b) - ids, _ := seedBenchmarkAgents(b, store, false) - - b.ReportAllocs() - b.ResetTimer() - for index := 0; index < b.N; index++ { - if _, err := store.AuthenticateAndRegister(ids[index%len(ids)], "", true); err != nil { - b.Fatalf("authenticate shared token agent: %v", err) - } - } -} - -func BenchmarkAuthenticateAndRegisterAgentToken(b *testing.B) { - store := openBenchmarkStore(b) - ids, tokens := seedBenchmarkAgents(b, store, true) - - b.ReportAllocs() - b.ResetTimer() - for index := 0; index < b.N; index++ { - agentIndex := index % len(ids) - if _, err := store.AuthenticateAndRegister(ids[agentIndex], tokens[agentIndex], false); err != nil { - b.Fatalf("authenticate independent agent token: %v", err) - } - } -} - -func BenchmarkAuthenticateAndRegisterAgentTokenParallel(b *testing.B) { - store := openBenchmarkStore(b) - ids, tokens := seedBenchmarkAgents(b, store, true) - var sequence atomic.Uint64 - - b.ReportAllocs() - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - agentIndex := int(sequence.Add(1)-1) % len(ids) - if _, err := store.AuthenticateAndRegister(ids[agentIndex], tokens[agentIndex], false); err != nil { - b.Errorf("authenticate independent agent token: %v", err) - return - } - } - }) -} - -func benchmarkAuthenticate1000AgentsConcurrent(b *testing.B, sharedAuthenticated bool) { - store := openBenchmarkStore(b) - ids, tokens := seedBenchmarkAgents(b, store, !sharedAuthenticated) - - b.ReportAllocs() - b.ResetTimer() - for range b.N { - b.StopTimer() - start := make(chan struct{}) - errs := make([]error, len(ids)) - var ready sync.WaitGroup - var done sync.WaitGroup - ready.Add(len(ids)) - done.Add(len(ids)) - for index := range ids { - go func() { - defer done.Done() - ready.Done() - <-start - _, errs[index] = store.AuthenticateAndRegister( - ids[index], tokens[index], sharedAuthenticated, - ) - }() - } - ready.Wait() - b.StartTimer() - close(start) - done.Wait() - b.StopTimer() - - for index, err := range errs { - if err != nil { - b.Fatalf("authenticate concurrent agent %d: %v", index, err) - } - } - } - elapsed := b.Elapsed() - b.ReportMetric( - float64(b.N*len(ids))/elapsed.Seconds(), - "agents/s", - ) - b.ReportMetric( - float64(elapsed.Nanoseconds())/float64(b.N*len(ids)), - "ns/agent", - ) -} - -func BenchmarkAuthenticateAndRegister1000SharedTokenAgentsConcurrent(b *testing.B) { - benchmarkAuthenticate1000AgentsConcurrent(b, true) -} - -func BenchmarkAuthenticateAndRegister1000AgentTokensConcurrent(b *testing.B) { - benchmarkAuthenticate1000AgentsConcurrent(b, false) -} - -func BenchmarkRegister1000SharedTokenAgentsConcurrent(b *testing.B) { - ids := make([]string, benchmarkAgentCount) - for index := range ids { - ids[index] = benchmarkAgentID(index) - } - tempDir := b.TempDir() - - b.ReportAllocs() - b.ResetTimer() - for iteration := range b.N { - b.StopTimer() - database, err := db.Open(filepath.Join(tempDir, fmt.Sprintf("gateway-%d.db", iteration))) - if err != nil { - b.Fatalf("open first-registration benchmark database: %v", err) - } - store, err := NewStore(database) - if err != nil { - _ = database.Close() - b.Fatalf("open first-registration benchmark store: %v", err) - } - - start := make(chan struct{}) - errs := make([]error, len(ids)) - var ready sync.WaitGroup - var done sync.WaitGroup - ready.Add(len(ids)) - done.Add(len(ids)) - for index := range ids { - go func() { - defer done.Done() - ready.Done() - <-start - _, errs[index] = store.AuthenticateAndRegister(ids[index], "", true) - }() - } - ready.Wait() - b.StartTimer() - close(start) - done.Wait() - b.StopTimer() - - for index, authErr := range errs { - if authErr != nil { - _ = database.Close() - b.Fatalf("register concurrent shared-token agent %d: %v", index, authErr) - } - } - if err := database.Close(); err != nil { - b.Fatalf("close first-registration benchmark database: %v", err) - } - } - elapsed := b.Elapsed() - b.ReportMetric(float64(b.N*len(ids))/elapsed.Seconds(), "agents/s") - b.ReportMetric( - float64(elapsed.Nanoseconds())/float64(b.N*len(ids)), - "ns/agent", - ) -} - -func BenchmarkList1000Agents(b *testing.B) { - store := openBenchmarkStore(b) - ids, _ := seedBenchmarkAgents(b, store, true) - onlineIDs := make([]string, 0, benchmarkAgentCount/2) - for index := 0; index < len(ids); index += 2 { - onlineIDs = append(onlineIDs, ids[index]) - } - - benchmarks := []struct { - name string - params PageParams - }{ - {name: "all_page_50", params: PageParams{Page: 1, PageSize: 50}}, - { - name: "online_page_25", - params: PageParams{ - Page: 1, - PageSize: 25, - Status: StatusOnline, - OnlineAgentIDs: onlineIDs, - }, - }, - { - name: "offline_page_25", - params: PageParams{ - Page: 1, - PageSize: 25, - Status: StatusOffline, - OnlineAgentIDs: onlineIDs, - }, - }, - } - - for _, benchmark := range benchmarks { - b.Run(benchmark.name, func(b *testing.B) { - b.ReportAllocs() - for range b.N { - page, err := store.List(benchmark.params) - if err != nil { - b.Fatalf("list benchmark agents: %v", err) - } - if len(page.Entries) != benchmark.params.PageSize { - b.Fatalf("listed %d entries, want %d", len(page.Entries), benchmark.params.PageSize) - } - } - }) - } -} diff --git a/crates/agent-gateway/internal/auth/agenttoken/store_test.go b/crates/agent-gateway/internal/auth/agenttoken/store_test.go deleted file mode 100644 index 3cd99f685..000000000 --- a/crates/agent-gateway/internal/auth/agenttoken/store_test.go +++ /dev/null @@ -1,623 +0,0 @@ -package agenttoken - -import ( - "errors" - "fmt" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/liveagent/agent-gateway/internal/db" -) - -// openTestDB 打开共享池并在其上初始化凭证表(生命周期由测试清理关闭)。 -func openTestDB(t *testing.T, path string) (*db.DB, *Store) { - t.Helper() - database, err := db.Open(path) - if err != nil { - t.Fatalf("open db: %v", err) - } - t.Cleanup(func() { _ = database.Close() }) - store, err := NewStore(database) - if err != nil { - t.Fatalf("init store: %v", err) - } - return database, store -} - -func openTestStore(t *testing.T) (*Store, string) { - t.Helper() - path := filepath.Join(t.TempDir(), "agents.db") - _, store := openTestDB(t, path) - return store, path -} - -func tokenAuthenticates(t *testing.T, store *Store, agentID, token string) bool { - t.Helper() - _, err := store.AuthenticateAndRegister(agentID, token, false) - if err == nil { - return true - } - if errors.Is(err, ErrUnauthorized) { - return false - } - t.Fatalf("authenticate agent token: %v", err) - return false -} - -func TestOpenRequiresDatabasePath(t *testing.T) { - t.Parallel() - - if _, err := db.Open(""); err == nil { - t.Fatal("empty database path must be rejected") - } -} - -func TestOpenCreatesDatabaseParentDirectory(t *testing.T) { - t.Parallel() - - path := filepath.Join(t.TempDir(), "nested", "gateway.db") - database, err := db.Open(path) - if err != nil { - t.Fatalf("open nested gateway db: %v", err) - } - t.Cleanup(func() { _ = database.Close() }) - if _, err := os.Stat(path); err != nil { - t.Fatalf("automatic gateway db was not created: %v", err) - } -} - -func TestNewStoreCreatesOnlyAgentsTable(t *testing.T) { - t.Parallel() - - store, _ := openTestStore(t) - rows, err := store.pool.Query(` - SELECT name - FROM sqlite_schema - WHERE type = 'table' AND name NOT LIKE 'sqlite_%' - ORDER BY name - `) - if err != nil { - t.Fatalf("list gateway tables: %v", err) - } - defer func() { _ = rows.Close() }() - var tables []string - for rows.Next() { - var name string - if err := rows.Scan(&name); err != nil { - t.Fatalf("scan gateway table: %v", err) - } - tables = append(tables, name) - } - if err := rows.Err(); err != nil { - t.Fatalf("iterate gateway tables: %v", err) - } - if len(tables) != 1 || tables[0] != "agents" { - t.Fatalf("gateway tables = %v, want [agents]", tables) - } -} - -func TestIssueValidateDeleteLifecycle(t *testing.T) { - t.Parallel() - - store, _ := openTestStore(t) - token, err := store.Issue("agent-a", "备注") - if err != nil { - t.Fatalf("issue: %v", err) - } - if !strings.HasPrefix(token, "agt_") { - t.Fatalf("token prefix = %q", token) - } - - if !tokenAuthenticates(t, store, "agent-a", token) { - t.Fatal("issued token must validate") - } - if tokenAuthenticates(t, store, "agent-a", "wrong") || tokenAuthenticates(t, store, "agent-b", token) { - t.Fatal("wrong token / wrong agent must be rejected") - } - - entries, err := store.Registered() - if err != nil { - t.Fatalf("registered: %v", err) - } - if len(entries) != 1 || entries[0].AgentID != "agent-a" || entries[0].Name != "备注" { - t.Fatalf("registered = %#v", entries) - } - - if deleted, err := store.Delete("agent-a"); err != nil || !deleted { - t.Fatalf("delete = %v, %v", deleted, err) - } - if tokenAuthenticates(t, store, "agent-a", token) { - t.Fatal("deleted token must be invalid") - } - if deleted, _ := store.Delete("agent-a"); deleted { - t.Fatal("second delete must report nothing deleted") - } - page, err := store.List(PageParams{}) - if err != nil || len(page.Entries) != 0 { - t.Fatalf("deleted agent must leave the directory: page=%#v err=%v", page, err) - } -} - -func TestRotationInvalidatesOldToken(t *testing.T) { - t.Parallel() - - store, _ := openTestStore(t) - oldToken, err := store.Issue("agent-a", "") - if err != nil { - t.Fatalf("issue: %v", err) - } - newToken, err := store.Issue("agent-a", "") - if err != nil { - t.Fatalf("rotate: %v", err) - } - if tokenAuthenticates(t, store, "agent-a", oldToken) { - t.Fatal("rotated-out token must be invalid") - } - if !tokenAuthenticates(t, store, "agent-a", newToken) { - t.Fatal("rotated-in token must be valid") - } - if entries, err := store.Registered(); err != nil || len(entries) != 1 { - t.Fatalf("rotation must not duplicate entries: entries=%d err=%v", len(entries), err) - } -} - -func TestAuthenticationEpochInvalidatedByRotationAndDelete(t *testing.T) { - t.Parallel() - - store, _ := openTestStore(t) - oldToken, err := store.Issue("agent-a", "") - if err != nil { - t.Fatalf("issue old token: %v", err) - } - rotationEpoch, err := store.AuthenticateAndRegister("agent-a", oldToken, false) - if err != nil { - t.Fatalf("authenticate old token: %v", err) - } - if !store.AuthenticationCurrent("agent-a", rotationEpoch) { - t.Fatal("fresh authentication epoch should be current") - } - newToken, err := store.Issue("agent-a", "") - if err != nil { - t.Fatalf("rotate token: %v", err) - } - if store.AuthenticationCurrent("agent-a", rotationEpoch) { - t.Fatal("rotation must invalidate an in-flight authentication epoch") - } - if _, err := store.AuthenticateAndRegister("agent-a", oldToken, false); !errors.Is(err, ErrUnauthorized) { - t.Fatalf("old token authentication error = %v, want ErrUnauthorized", err) - } - - deleteEpoch, err := store.AuthenticateAndRegister("agent-a", newToken, false) - if err != nil { - t.Fatalf("authenticate new token: %v", err) - } - if deleted, err := store.Delete("agent-a"); err != nil || !deleted { - t.Fatalf("delete agent = %v, %v", deleted, err) - } - if store.AuthenticationCurrent("agent-a", deleteEpoch) { - t.Fatal("delete must invalidate an in-flight authentication epoch") - } -} - -func TestAuthenticationEpochIsScopedPerAgent(t *testing.T) { - t.Parallel() - - store, _ := openTestStore(t) - tokenA, err := store.Issue("agent-a", "") - if err != nil { - t.Fatalf("issue agent-a token: %v", err) - } - tokenB, err := store.Issue("agent-b", "") - if err != nil { - t.Fatalf("issue agent-b token: %v", err) - } - epochA, err := store.AuthenticateAndRegister("agent-a", tokenA, false) - if err != nil { - t.Fatalf("authenticate agent-a: %v", err) - } - epochB, err := store.AuthenticateAndRegister("agent-b", tokenB, false) - if err != nil { - t.Fatalf("authenticate agent-b: %v", err) - } - - if _, err := store.Issue("agent-a", ""); err != nil { - t.Fatalf("rotate agent-a token: %v", err) - } - if store.AuthenticationCurrent("agent-a", epochA) { - t.Fatal("agent-a rotation must invalidate agent-a authentication") - } - if !store.AuthenticationCurrent("agent-b", epochB) { - t.Fatal("agent-a rotation must not invalidate agent-b authentication") - } -} - -func TestRegisteredReturnsDatabaseError(t *testing.T) { - t.Parallel() - - database, store := openTestDB(t, filepath.Join(t.TempDir(), "agents.db")) - if err := database.Close(); err != nil { - t.Fatalf("close database: %v", err) - } - if _, err := store.Registered(); err == nil { - t.Fatal("registered must return the database error instead of an empty directory") - } -} - -func TestAuthenticationReturnsDatabaseError(t *testing.T) { - t.Parallel() - - database, store := openTestDB(t, filepath.Join(t.TempDir(), "agents.db")) - if err := database.Close(); err != nil { - t.Fatalf("close database: %v", err) - } - if _, err := store.AuthenticateAndRegister("agent-a", "token", false); err == nil || errors.Is(err, ErrUnauthorized) { - t.Fatalf("authentication error = %v, want database error", err) - } -} - -func TestRegisterAddsGatewayTokenAgentWithoutCredential(t *testing.T) { - t.Parallel() - - store, _ := openTestStore(t) - if err := store.Register("shared-token-agent"); err != nil { - t.Fatalf("register: %v", err) - } - if err := store.Register("shared-token-agent"); err != nil { - t.Fatalf("repeat register: %v", err) - } - page, err := store.List(PageParams{}) - if err != nil { - t.Fatalf("list: %v", err) - } - if page.Total != 1 || len(page.Entries) != 1 || page.Entries[0].AgentID != "shared-token-agent" || page.Entries[0].HasToken { - t.Fatalf("registered gateway-token agent = %#v", page) - } - if page.Entries[0].Name != "" { - t.Fatalf("automatic registration name = %q, want empty", page.Entries[0].Name) - } - - if err := store.UpdateName("shared-token-agent", " Office desktop "); err != nil { - t.Fatalf("update name: %v", err) - } - page, err = store.List(PageParams{}) - if err != nil || page.Entries[0].Name != "Office desktop" { - t.Fatalf("updated name page = %#v err=%v", page, err) - } - if err := store.UpdateName("shared-token-agent", ""); err != nil { - t.Fatalf("clear name: %v", err) - } - if err := store.UpdateName("shared-token-agent", strings.Repeat("名", 65)); !errors.Is(err, ErrAgentNameTooLong) { - t.Fatalf("long name error = %v", err) - } - - if deleted, err := store.Delete("shared-token-agent"); err != nil || !deleted { - t.Fatalf("delete shared-token agent = %v, %v", deleted, err) - } - if err := store.Register("shared-token-agent"); err != nil { - t.Fatalf("register after delete: %v", err) - } - page, err = store.List(PageParams{}) - if err != nil || page.Total != 1 || page.Entries[0].Name != "" { - t.Fatalf("re-registered gateway-token agent = %#v err=%v", page, err) - } -} - -func TestNormalizeAgentIDRequiresCanonicalUUIDv4(t *testing.T) { - t.Parallel() - - const valid = "agent-550e8400-e29b-41d4-a716-446655440000" - if got, err := NormalizeAgentID(" " + valid + " "); err != nil || got != valid { - t.Fatalf("normalize valid id = %q, %v", got, err) - } - for _, invalid := range []string{"", "hhhh", "agent-550e8400-e29b-11d4-a716-446655440000", "agent-550E8400-E29B-41D4-A716-446655440000"} { - if _, err := NormalizeAgentID(invalid); err == nil { - t.Fatalf("invalid id %q was accepted", invalid) - } - } -} - -func TestTokensSurviveReopen(t *testing.T) { - t.Parallel() - - path := filepath.Join(t.TempDir(), "agents.db") - database, store := openTestDB(t, path) - token, err := store.Issue("agent-a", "prod") - if err != nil { - t.Fatalf("issue: %v", err) - } - if err := database.Close(); err != nil { - t.Fatalf("close: %v", err) - } - - // 模拟网关重启:重开库后凭证仍有效,删除也持久化。 - _, reopened := openTestDB(t, path) - if !tokenAuthenticates(t, reopened, "agent-a", token) { - t.Fatal("token must survive reopen") - } - if deleted, err := reopened.Delete("agent-a"); err != nil || !deleted { - t.Fatalf("delete after reopen = %v, %v", deleted, err) - } - - _, third := openTestDB(t, path) - if tokenAuthenticates(t, third, "agent-a", token) { - t.Fatal("deletion must survive reopen") - } -} - -func TestNewStorePreloadsKnownAgentsAfterReopen(t *testing.T) { - t.Parallel() - - path := filepath.Join(t.TempDir(), "agents.db") - database, store := openTestDB(t, path) - if err := store.Register("shared-token-agent"); err != nil { - t.Fatalf("register shared-token agent: %v", err) - } - agentToken, err := store.Issue("independent-token-agent", "") - if err != nil { - t.Fatalf("issue independent agent token: %v", err) - } - if err := database.Close(); err != nil { - t.Fatalf("close database: %v", err) - } - - // 模拟 Gateway 重启。把重开的单连接切为只读,确保已有 Agent 重连完全依赖 - // 启动预加载缓存;如果仍执行 INSERT OR IGNORE,此处会因只读而失败。 - reopenedDB, reopened := openTestDB(t, path) - reopenedDB.Pool().SetMaxOpenConns(1) - reopenedDB.Pool().SetMaxIdleConns(1) - if _, err := reopenedDB.Pool().Exec(`PRAGMA query_only = ON`); err != nil { - t.Fatalf("enable query-only mode: %v", err) - } - if _, ok := reopened.knownAgents.Load("shared-token-agent"); !ok { - t.Fatal("shared-token agent was not preloaded") - } - if _, ok := reopened.knownAgents.Load("independent-token-agent"); !ok { - t.Fatal("independent-token agent was not preloaded") - } - if _, err := reopened.AuthenticateAndRegister("shared-token-agent", "", true); err != nil { - t.Fatalf("reconnect preloaded shared-token agent: %v", err) - } - if _, err := reopened.AuthenticateAndRegister( - "independent-token-agent", agentToken, false, - ); err != nil { - t.Fatalf("reconnect preloaded independent-token agent: %v", err) - } - if _, err := reopened.AuthenticateAndRegister("new-agent", "", true); err == nil { - t.Fatal("new shared-token agent unexpectedly registered in query-only mode") - } -} - -func TestDBFilePermissionsAndNoPlaintext(t *testing.T) { - t.Parallel() - - path := filepath.Join(t.TempDir(), "agents.db") - database, store := openTestDB(t, path) - token, err := store.Issue("agent-a", "") - if err != nil { - t.Fatalf("issue: %v", err) - } - if err := database.Close(); err != nil { - t.Fatalf("close: %v", err) - } - - info, err := os.Stat(path) - if err != nil { - t.Fatalf("stat db: %v", err) - } - if perm := info.Mode().Perm(); perm != 0o600 { - t.Fatalf("db file perm = %o, want 0600", perm) - } - // 明文绝不落库:直接扫库文件字节。 - raw, err := os.ReadFile(path) - if err != nil { - t.Fatalf("read db: %v", err) - } - if strings.Contains(string(raw), token) { - t.Fatal("plaintext token must never be stored") - } -} - -func TestListPaginates(t *testing.T) { - t.Parallel() - - store, _ := openTestStore(t) - // 混合登记 120 个 Agent:一半有独立凭证,一半仅在目录中。 - for i := 0; i < 120; i++ { - agentID := fmt.Sprintf("agent-%03d", i) - if i%2 == 0 { - if _, err := store.Issue(agentID, ""); err != nil { - t.Fatalf("issue %d: %v", i, err) - } - } else if err := store.Register(agentID); err != nil { - t.Fatalf("register %d: %v", i, err) - } - } - - first, err := store.List(PageParams{Page: 1, PageSize: 50}) - if err != nil { - t.Fatalf("list page 1: %v", err) - } - if first.Total != 120 || len(first.Entries) != 50 || !first.HasMore { - t.Fatalf("page 1 = total %d, len %d, hasMore %v", first.Total, len(first.Entries), first.HasMore) - } - if !first.Entries[0].HasToken || first.Entries[1].HasToken { - t.Fatalf("mixed credential flags = %#v, %#v", first.Entries[0], first.Entries[1]) - } - - last, err := store.List(PageParams{Page: 3, PageSize: 50}) - if err != nil { - t.Fatalf("list page 3: %v", err) - } - if len(last.Entries) != 20 || last.HasMore { - t.Fatalf("page 3 = len %d, hasMore %v, want 20 entries no-more", len(last.Entries), last.HasMore) - } - - // 超末页返回空、total 仍准确。 - beyond, err := store.List(PageParams{Page: 99, PageSize: 50}) - if err != nil { - t.Fatalf("list beyond: %v", err) - } - if len(beyond.Entries) != 0 || beyond.Total != 120 || beyond.HasMore { - t.Fatalf("beyond = len %d total %d hasMore %v", len(beyond.Entries), beyond.Total, beyond.HasMore) - } -} - -func TestListFiltersInDatabaseBeforePaging(t *testing.T) { - t.Parallel() - - store, _ := openTestStore(t) - for i := 0; i < 120; i++ { - if _, err := store.Issue(fmt.Sprintf("agent-%03d", i), ""); err != nil { - t.Fatalf("issue %d: %v", i, err) - } - } - onlineIDs := make([]string, 0, 60) - for i := 0; i < 120; i += 2 { - onlineIDs = append(onlineIDs, fmt.Sprintf("agent-%03d", i)) - } - - page, err := store.List(PageParams{ - Page: 2, - PageSize: 10, - Status: StatusOnline, - OnlineAgentIDs: onlineIDs, - }) - if err != nil { - t.Fatalf("online list: %v", err) - } - if page.Total != 60 || len(page.Entries) != 10 || !page.HasMore || page.Entries[0].AgentID != "agent-020" { - t.Fatalf("online page = total %d len %d hasMore %v first %q", page.Total, len(page.Entries), page.HasMore, page.Entries[0].AgentID) - } - - offline, err := store.List(PageParams{ - Page: 6, - PageSize: 10, - Status: StatusOffline, - OnlineAgentIDs: onlineIDs, - }) - if err != nil { - t.Fatalf("offline list: %v", err) - } - if offline.Total != 60 || len(offline.Entries) != 10 || offline.HasMore || offline.Entries[0].AgentID != "agent-101" { - t.Fatalf("offline last page = total %d len %d hasMore %v first %q", offline.Total, len(offline.Entries), offline.HasMore, offline.Entries[0].AgentID) - } -} - -func TestListRejectsInvalidStatusFilter(t *testing.T) { - t.Parallel() - - store, _ := openTestStore(t) - if _, err := store.List(PageParams{Status: StatusFilter("unknown")}); err == nil { - t.Fatal("invalid status filter must be rejected") - } -} - -func TestListParamsClamped(t *testing.T) { - t.Parallel() - - store, _ := openTestStore(t) - for i := 0; i < 5; i++ { - if _, err := store.Issue(fmt.Sprintf("a-%d", i), ""); err != nil { - t.Fatalf("issue: %v", err) - } - } - - // page<1 归一到 1,page_size<=0 用默认,超上限钳制到 maxPageSize。 - zero, _ := store.List(PageParams{Page: 0, PageSize: 0}) - if zero.Page != 1 || zero.PageSize != defaultPageSize { - t.Fatalf("clamp low = page %d size %d", zero.Page, zero.PageSize) - } - big, _ := store.List(PageParams{Page: 1, PageSize: 9999}) - if big.PageSize != maxPageSize { - t.Fatalf("clamp high = size %d, want %d", big.PageSize, maxPageSize) - } -} - -func TestListOrderUsesIndexNotFullScan(t *testing.T) { - t.Parallel() - - store, _ := openTestStore(t) - for i := 0; i < 3; i++ { - if _, err := store.Issue(fmt.Sprintf("a-%d", i), ""); err != nil { - t.Fatalf("issue: %v", err) - } - } - // 分页排序不得触发临时 B-Tree 排序(全表 sort),应走 created_at 索引。 - var plan strings.Builder - rows, err := store.pool.Query( - `EXPLAIN QUERY PLAN - SELECT a.agent_id, a.name, a.created_at, a.token_issued_at - FROM agents AS a - ORDER BY a.created_at, a.agent_id LIMIT 50 OFFSET 0`, - ) - if err != nil { - t.Fatalf("explain: %v", err) - } - defer func() { _ = rows.Close() }() - for rows.Next() { - var id, parent, notused int - var detail string - if err := rows.Scan(&id, &parent, ¬used, &detail); err != nil { - t.Fatalf("scan plan: %v", err) - } - plan.WriteString(detail) - plan.WriteString("\n") - } - if strings.Contains(plan.String(), "USE TEMP B-TREE FOR ORDER BY") { - t.Fatalf("ORDER BY fell back to temp b-tree sort:\n%s", plan.String()) - } - if !strings.Contains(plan.String(), "idx_agents_created_at_agent_id") { - t.Fatalf("ORDER BY did not use the paging index:\n%s", plan.String()) - } - - plan.Reset() - rows, err = store.pool.Query( - `EXPLAIN QUERY PLAN - SELECT a.agent_id, a.name, a.created_at, a.token_issued_at - FROM agents AS a - WHERE a.agent_id NOT IN (SELECT CAST(value AS TEXT) FROM json_each(?)) - ORDER BY a.created_at, a.agent_id LIMIT 50 OFFSET 0`, - `["a-0"]`, - ) - if err != nil { - t.Fatalf("explain offline: %v", err) - } - defer func() { _ = rows.Close() }() - for rows.Next() { - var id, parent, notused int - var detail string - if err := rows.Scan(&id, &parent, ¬used, &detail); err != nil { - t.Fatalf("scan offline plan: %v", err) - } - plan.WriteString(detail) - plan.WriteString("\n") - } - if strings.Contains(plan.String(), "USE TEMP B-TREE FOR ORDER BY") || - !strings.Contains(plan.String(), "idx_agents_created_at_agent_id") { - t.Fatalf("offline paging did not use the ordering index:\n%s", plan.String()) - } - - plan.Reset() - rows, err = store.pool.Query( - `EXPLAIN QUERY PLAN SELECT token_sha256 FROM agents WHERE agent_id = ?`, - "a-0", - ) - if err != nil { - t.Fatalf("explain lookup: %v", err) - } - defer func() { _ = rows.Close() }() - for rows.Next() { - var id, parent, notused int - var detail string - if err := rows.Scan(&id, &parent, ¬used, &detail); err != nil { - t.Fatalf("scan lookup plan: %v", err) - } - plan.WriteString(detail) - plan.WriteString("\n") - } - if !strings.Contains(plan.String(), "agent_id=?") { - t.Fatalf("credential lookup did not use the agent_id primary key:\n%s", plan.String()) - } -} diff --git a/crates/agent-gateway/internal/auth/http_middleware.go b/crates/agent-gateway/internal/auth/http_middleware.go deleted file mode 100644 index 81ca4adb0..000000000 --- a/crates/agent-gateway/internal/auth/http_middleware.go +++ /dev/null @@ -1,53 +0,0 @@ -package auth - -import ( - "crypto/sha256" - "crypto/subtle" - "encoding/json" - "net/http" - "strings" -) - -func HTTPMiddleware(expectedToken string, next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if !ValidateBearerHeader(r.Header.Get("Authorization"), expectedToken) { - writeJSONError(w, http.StatusUnauthorized, "unauthorized") - return - } - next.ServeHTTP(w, r) - }) -} - -func ValidateBearerHeader(headerValue, expectedToken string) bool { - headerValue = strings.TrimSpace(headerValue) - if headerValue == "" { - return false - } - parts := strings.SplitN(headerValue, " ", 2) - if len(parts) != 2 { - return false - } - if !strings.EqualFold(parts[0], "Bearer") { - return false - } - return ValidateToken(parts[1], expectedToken) -} - -func ValidateToken(value, expectedToken string) bool { - value = strings.TrimSpace(value) - expectedToken = strings.TrimSpace(expectedToken) - if value == "" || expectedToken == "" { - return false - } - valueHash := sha256.Sum256([]byte(value)) - expectedHash := sha256.Sum256([]byte(expectedToken)) - return subtle.ConstantTimeCompare(valueHash[:], expectedHash[:]) == 1 -} - -func writeJSONError(w http.ResponseWriter, status int, message string) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - _ = json.NewEncoder(w).Encode(map[string]any{ - "error": message, - }) -} diff --git a/crates/agent-gateway/internal/chatcmd/chatcmd.go b/crates/agent-gateway/internal/chatcmd/chatcmd.go deleted file mode 100644 index 30c93ce7a..000000000 --- a/crates/agent-gateway/internal/chatcmd/chatcmd.go +++ /dev/null @@ -1,423 +0,0 @@ -// Package chatcmd 承载网关侧 chat 命令编排(请求体归一化、运行时探活、命令投递与启动看门狗、 -// proto 信封构造),供 v2 协议层复用, -// 协议层只做载荷编解码,编排逻辑一律收敛于此。 -package chatcmd - -import ( - "context" - "errors" - "log/slog" - "strings" - "time" - - "github.com/google/uuid" - "github.com/liveagent/agent-gateway/internal/config" - "github.com/liveagent/agent-gateway/internal/handler" - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" - "github.com/liveagent/agent-gateway/internal/session" -) - -// MessageRef 是 chat.edit_resend 引用的既有消息定位。 -type MessageRef struct { - SegmentIndex int `json:"segment_index"` - MessageIndex int `json:"message_index"` - SegmentID string `json:"segment_id"` - MessageID string `json:"message_id"` - Role string `json:"role"` - ContentHash string `json:"content_hash"` -} - -const ( - // runtimeWakeRequestPrefix 是探活请求 id 的约定前缀;桌面端识别到它会先唤醒 Chat WebView 运行时。 - runtimeWakeRequestPrefix = "chat-runtime-wake-" - runtimeProbeReuseWindow = 2 * time.Second -) - -// NewTraceID 生成 chat 命令链路的追踪 id。 -func NewTraceID() string { - return strings.ReplaceAll(uuid.NewString(), "-", "") -} - -// LogCommandSpan 记录 chat 命令生命周期中的一个阶段(结构化日志)。 -func LogCommandSpan( - traceID string, - span string, - runID string, - conversationID string, - clientRequestID string, - commandType string, -) { - slog.Info("chat_command_span", - "span", strings.TrimSpace(span), - "trace_id", strings.TrimSpace(traceID), - "run_id", strings.TrimSpace(runID), - "conversation_id", strings.TrimSpace(conversationID), - "client_request_id", strings.TrimSpace(clientRequestID), - "command_type", strings.TrimSpace(commandType), - ) -} - -// NormalizeRequestBody 归一化并校验 chat 请求体(trim、默认值、必填项)。 -func NormalizeRequestBody(body *handler.ChatRequestBody) error { - body.Message = strings.TrimSpace(body.Message) - body.ConversationID = strings.TrimSpace(body.ConversationID) - body.ClientRequestID = strings.TrimSpace(body.ClientRequestID) - body.ExecutionMode = handler.NormalizeExecutionMode(body.ExecutionMode) - body.Workdir = handler.NormalizeWorkdir(body.Workdir) - body.QueuePolicy = normalizeQueuePolicy(body.QueuePolicy) - body.UploadedFiles = handler.NormalizeChatUploadedFiles(body.UploadedFiles) - body.RuntimeControls = handler.NormalizeChatRuntimeControls(body.RuntimeControls) - selectedModel, err := handler.NormalizeChatSelectedModel(body.SelectedModel) - if err != nil { - return err - } - body.SelectedModel = selectedModel - if body.ClientRequestID == "" { - return errors.New("client_request_id is required") - } - if body.Message == "" && len(body.UploadedFiles) == 0 { - return errors.New("message is required") - } - return nil -} - -func normalizeQueuePolicy(value string) string { - switch strings.TrimSpace(value) { - case "append", "interrupt": - return strings.TrimSpace(value) - default: - return "auto" - } -} - -// DispatchAcceptedCommand 把已接受的命令投递给桌面端并布防启动看门狗; -// cleanupWatch 在命令落定或判失败后关闭调用方的命令更新观察流。 -func DispatchAcceptedCommand( - parent context.Context, - cfg *config.Config, - sm *session.Manager, - agentID string, - cleanupWatch func(), - start session.ChatCommandStart, - body handler.ChatRequestBody, - baseMessageRef *MessageRef, - traceID string, -) { - if cleanupWatch != nil { - defer cleanupWatch() - } - timeout := DeliveryTimeout(cfg) - ctx, cancel := context.WithTimeout(parent, timeout) - defer cancel() - - commandType := "chat.submit" - if baseMessageRef != nil { - commandType = "chat.edit_resend" - } - if err := sm.SendToAgentContext(ctx, agentID, buildCommandEnvelope(start.RunID, commandType, body, baseMessageRef)); err != nil { - message := "chat command failed" - if err != nil && strings.TrimSpace(err.Error()) != "" { - message = strings.TrimSpace(err.Error()) - } - sm.FailChatCommand(agentID, start.RunID, "desktop_runtime_unavailable", message) - return - } - LogCommandSpan(traceID, "command_delivered", start.RunID, start.ConversationID, body.ClientRequestID, commandType) - WatchAcceptedCommandStartup(parent, cfg, sm, agentID, start.RunID) -} - -// ProbeRuntime 验证桌面端连接可完成真实往返;探活请求 id 的特殊前缀同时是唤醒 -// Chat WebView 运行时的信号。 -func ProbeRuntime( - ctx context.Context, - sm *session.Manager, - agentID string, -) error { - if sm == nil { - return session.ErrAgentOffline - } - if !sm.ChatIngressV1Ready(agentID) { - if sm.IsOnline(agentID) { - return session.ErrChatProtocolIncompatible - } - return session.ErrAgentOffline - } - sessionEpoch, online := sm.ChatRuntimeProbeEpoch(agentID) - if !online { - return session.ErrAgentOffline - } - requestID := runtimeWakeRequestPrefix + uuid.NewString() - response, err := sm.AwaitUnaryResponse(ctx, agentID, requestID, &gatewayv2.GatewayEnvelope{ - RequestId: requestID, - Timestamp: time.Now().Unix(), - Payload: &gatewayv2.GatewayEnvelope_Ping{ - Ping: &gatewayv2.PingRequest{Timestamp: time.Now().Unix()}, - }, - }) - if err != nil { - return err - } - if response == nil || response.GetPong() == nil { - return errors.New("desktop agent returned an invalid chat runtime probe response") - } - if !sm.RecordChatRuntimeProbe(agentID, sessionEpoch) { - return session.ErrAgentOffline - } - return nil -} - -// ProbeRuntimeForCommand 在近期已有成功探活时直接复用结果。 -func ProbeRuntimeForCommand(ctx context.Context, sm *session.Manager, agentID string) error { - if sm != nil && sm.ChatRuntimeProbeFresh(agentID, runtimeProbeReuseWindow) { - return nil - } - return ProbeRuntime(ctx, sm, agentID) -} - -// WatchAcceptedCommandStartup 对启动窗口内未落定(开始、结束或进入桌面提示队列)的命令判失败。 -func WatchAcceptedCommandStartup( - parent context.Context, - cfg *config.Config, - sm *session.Manager, - agentID string, - runID string, -) { - agentID = strings.TrimSpace(agentID) - if sm == nil || agentID == "" || strings.TrimSpace(runID) == "" { - return - } - if !waitCommandWatchdog(parent, StartTimeout(cfg)) { - return - } - if sm.ChatCommandSettled(agentID, runID) { - return - } - if !waitCommandWatchdog(parent, RenderStartTimeout(cfg)) { - return - } - if sm.ChatCommandSettled(agentID, runID) { - return - } - sm.FailChatCommand(agentID, runID, "startup_timeout", - "The desktop app did not start the remote chat request. Please retry.") -} - -func waitCommandWatchdog(ctx context.Context, timeout time.Duration) bool { - if timeout <= 0 { - return true - } - timer := time.NewTimer(timeout) - defer timer.Stop() - select { - case <-ctx.Done(): - return false - case <-timer.C: - return true - } -} - -// StartTimeout / RenderStartTimeout / PrepareTimeout / DeliveryTimeout 返回各阶段超时 -// (未配置时取保守默认值)。 -func StartTimeout(cfg *config.Config) time.Duration { - if cfg != nil && cfg.ChatStartTimeout > 0 { - return cfg.ChatStartTimeout - } - return 5 * time.Second -} - -func RenderStartTimeout(cfg *config.Config) time.Duration { - if cfg != nil && cfg.ChatRenderStartTimeout > 0 { - return cfg.ChatRenderStartTimeout - } - return 10 * time.Second -} - -func PrepareTimeout(cfg *config.Config) time.Duration { - if cfg != nil && cfg.ChatPrepareTimeout > 0 { - return cfg.ChatPrepareTimeout - } - return 2 * time.Second -} - -func DeliveryTimeout(cfg *config.Config) time.Duration { - if cfg != nil && cfg.ChatDeliveryTimeout > 0 { - return cfg.ChatDeliveryTimeout - } - return 5 * time.Second -} - -// BuildAcceptedCommandPayloads 构造命令被接受时立即写入会话流的事件载荷 -// (edit_resend 先补一条 rebase 事件)。 -func BuildAcceptedCommandPayloads( - body handler.ChatRequestBody, - baseMessageRef *MessageRef, -) []map[string]any { - payloads := make([]map[string]any, 0, 2) - if baseMessageRef != nil { - payloads = append(payloads, map[string]any{ - "type": session.StreamEventRebased, - "base_message_ref": baseMessageRef, - "reason": "edit_resend", - }) - } - payloads = append(payloads, buildUserMessageAppendedPayload(body, baseMessageRef)) - return payloads -} - -func buildUserMessageAppendedPayload( - body handler.ChatRequestBody, - baseMessageRef *MessageRef, -) map[string]any { - payload := map[string]any{ - "type": "user_message", - "message": body.Message, - "uploaded_files": body.UploadedFiles, - "execution_mode": body.ExecutionMode, - "workdir": body.Workdir, - "runtime_controls": body.RuntimeControls, - "selected_model": body.SelectedModel, - } - if baseMessageRef != nil { - payload["base_message_ref"] = baseMessageRef - payload["reason"] = "edit_resend" - } - return payload -} - -func buildCommandEnvelope( - requestID string, - commandType string, - body handler.ChatRequestBody, - baseMessageRef *MessageRef, -) *gatewayv2.GatewayEnvelope { - return &gatewayv2.GatewayEnvelope{ - RequestId: strings.TrimSpace(requestID), - Timestamp: time.Now().Unix(), - Payload: &gatewayv2.GatewayEnvelope_ChatCommand{ - ChatCommand: &gatewayv2.ChatCommandRequest{ - Type: strings.TrimSpace(commandType), - Request: buildProtoRequest(body), - BaseMessageRef: BuildProtoMessageRef(baseMessageRef), - }, - }, - } -} - -// BuildCancelCommandPayload 构造 chat.cancel 的 GatewayEnvelope 载荷臂。 -func BuildCancelCommandPayload(conversationID string) *gatewayv2.GatewayEnvelope_ChatCommand { - return &gatewayv2.GatewayEnvelope_ChatCommand{ - ChatCommand: &gatewayv2.ChatCommandRequest{ - Type: "chat.cancel", - Cancel: &gatewayv2.CancelChatRequest{ - ConversationId: strings.TrimSpace(conversationID), - }, - }, - } -} - -func buildProtoRequest(body handler.ChatRequestBody) *gatewayv2.ChatRequest { - return &gatewayv2.ChatRequest{ - ConversationId: body.ConversationID, - ClientRequestId: body.ClientRequestID, - Message: body.Message, - SelectedModel: handler.ToProtoChatSelectedModel(body.SelectedModel), - RuntimeControls: handler.ToProtoChatRuntimeControls(body.RuntimeControls), - ExecutionMode: body.ExecutionMode, - Workdir: body.Workdir, - UploadedFiles: handler.ToProtoChatUploadedFiles(body.UploadedFiles), - QueuePolicy: body.QueuePolicy, - } -} - -// BuildProtoMessageRef 把 MessageRef 转为 proto 表示(nil 安全)。 -func BuildProtoMessageRef(ref *MessageRef) *gatewayv2.ChatMessageRef { - if ref == nil { - return nil - } - return &gatewayv2.ChatMessageRef{ - SegmentIndex: int32(ref.SegmentIndex), - MessageIndex: int32(ref.MessageIndex), - SegmentId: strings.TrimSpace(ref.SegmentID), - MessageId: strings.TrimSpace(ref.MessageID), - Role: strings.TrimSpace(ref.Role), - ContentHash: strings.TrimSpace(ref.ContentHash), - } -} - -// RequestBodyFromProto 把 v2 直带的 proto ChatRequest 还原为编排层请求体 -// (buildProtoRequest 的逆向;调用方随后统一走 NormalizeRequestBody)。 -func RequestBodyFromProto(req *gatewayv2.ChatRequest) handler.ChatRequestBody { - if req == nil { - return handler.ChatRequestBody{} - } - body := handler.ChatRequestBody{ - ConversationID: req.GetConversationId(), - ClientRequestID: req.GetClientRequestId(), - Message: req.GetMessage(), - ExecutionMode: req.GetExecutionMode(), - Workdir: req.GetWorkdir(), - QueuePolicy: req.GetQueuePolicy(), - } - if selected := req.GetSelectedModel(); selected != nil { - body.SelectedModel = &handler.ChatSelectedModelBody{ - CustomProviderID: selected.GetCustomProviderId(), - Model: selected.GetModel(), - ProviderType: selected.GetProviderType(), - } - } - if controls := req.GetRuntimeControls(); controls != nil { - thinking := controls.GetThinkingEnabled() - webSearch := controls.GetNativeWebSearchEnabled() - body.RuntimeControls = &handler.ChatRuntimeControlsBody{ - ThinkingEnabled: &thinking, - NativeWebSearchEnabled: &webSearch, - Reasoning: controls.GetReasoning(), - } - } - for _, file := range req.GetUploadedFiles() { - body.UploadedFiles = append(body.UploadedFiles, handler.ChatUploadedFileBody{ - RelativePath: file.GetRelativePath(), - AbsolutePath: file.GetAbsolutePath(), - FileName: file.GetFileName(), - Kind: file.GetKind(), - SizeBytes: file.GetSizeBytes(), - }) - } - return body -} - -// MessageRefFromProto 把 proto 消息引用还原为编排层表示(nil 安全)。 -func MessageRefFromProto(ref *gatewayv2.ChatMessageRef) *MessageRef { - if ref == nil { - return nil - } - return &MessageRef{ - SegmentIndex: int(ref.GetSegmentIndex()), - MessageIndex: int(ref.GetMessageIndex()), - SegmentID: ref.GetSegmentId(), - MessageID: ref.GetMessageId(), - Role: ref.GetRole(), - ContentHash: ref.GetContentHash(), - } -} - -// ValidateMessageRef 校验并归一化消息引用(原地 trim)。 -func ValidateMessageRef(ref *MessageRef) error { - if ref == nil { - return nil - } - if ref.SegmentIndex < 0 || ref.MessageIndex < 0 { - return errors.New("base_message_ref indexes must be non-negative") - } - ref.SegmentID = strings.TrimSpace(ref.SegmentID) - ref.MessageID = strings.TrimSpace(ref.MessageID) - ref.Role = strings.TrimSpace(ref.Role) - ref.ContentHash = strings.TrimSpace(ref.ContentHash) - if ref.SegmentID == "" || ref.MessageID == "" || ref.Role == "" || ref.ContentHash == "" { - return errors.New("base_message_ref requires segment_id, message_id, role, and content_hash") - } - if ref.Role != "user" { - return errors.New("base_message_ref role must be user") - } - return nil -} diff --git a/crates/agent-gateway/internal/chatcmd/chatcmd_test.go b/crates/agent-gateway/internal/chatcmd/chatcmd_test.go deleted file mode 100644 index 827f6a39d..000000000 --- a/crates/agent-gateway/internal/chatcmd/chatcmd_test.go +++ /dev/null @@ -1,111 +0,0 @@ -package chatcmd - -import ( - "context" - "errors" - "testing" - "time" - - "github.com/liveagent/agent-gateway/internal/config" - "github.com/liveagent/agent-gateway/internal/handler" - "github.com/liveagent/agent-gateway/internal/session" -) - -func newCommandTestManager(t *testing.T) (*session.Manager, *session.AgentSession) { - t.Helper() - sm := session.NewManager() - sm.RecordAuthentication("desktop-agent", "test", "session-test") - sess := session.NewAgentSession(sm.LatestAuthSnapshot("desktop-agent")) - sm.SetSession(sess) - t.Cleanup(func() { sm.ClearSession(sess) }) - return sm, sess -} - -func TestProbeRuntimeRejectsDesktopWithoutChatIngressV1(t *testing.T) { - sm, sess := newCommandTestManager(t) - - err := ProbeRuntime(context.Background(), sm, "desktop-agent") - if !errors.Is(err, session.ErrChatProtocolIncompatible) { - t.Fatalf("ProbeRuntime() error = %v, want ErrChatProtocolIncompatible", err) - } - select { - case outbound := <-sess.Outbound(): - t.Fatalf("incompatible desktop received probe envelope: %#v", outbound.GatewayEnvelope) - default: - } -} - -func TestChatTimeoutDefaultsAreShortAndDedicated(t *testing.T) { - t.Parallel() - - if got := PrepareTimeout(nil); got != 2*time.Second { - t.Fatalf("PrepareTimeout(nil) = %s, want 2s", got) - } - if got := DeliveryTimeout(nil); got != 5*time.Second { - t.Fatalf("DeliveryTimeout(nil) = %s, want 5s", got) - } - if got := StartTimeout(nil); got != 5*time.Second { - t.Fatalf("StartTimeout(nil) = %s, want 5s", got) - } - if got := RenderStartTimeout(nil); got != 10*time.Second { - t.Fatalf("RenderStartTimeout(nil) = %s, want 10s", got) - } -} - -func TestDispatchAcceptedCommandUsesDeliveryTimeout(t *testing.T) { - t.Parallel() - - sm, _ := newCommandTestManager(t) - start := sm.StartChatCommand("desktop-agent", "run-delivery-timeout", "conv-1", "", "client-1", nil) - cfg := &config.Config{ChatDeliveryTimeout: 30 * time.Millisecond} - body := handler.ChatRequestBody{ - ConversationID: "conv-1", - ClientRequestID: "client-1", - Message: "hello", - } - - startedAt := time.Now() - DispatchAcceptedCommand( - context.Background(), cfg, sm, "desktop-agent", nil, start, body, nil, "trace-delivery-timeout", - ) - elapsed := time.Since(startedAt) - if elapsed < 20*time.Millisecond || elapsed > 500*time.Millisecond { - t.Fatalf("delivery timeout elapsed = %s, want about 30ms", elapsed) - } - - sub := sm.SubscribeConversationStream("desktop-agent", "conv-1", 0, "") - defer sub.Cleanup() - if len(sub.Events) == 0 { - t.Fatal("delivery timeout did not terminalize the accepted run") - } - last := sub.Events[len(sub.Events)-1] - if last.Type != session.StreamEventRunFinished || - last.Payload["error_code"] != "desktop_runtime_unavailable" { - t.Fatalf("delivery timeout terminal = %s %#v", last.Type, last.Payload) - } -} - -func TestChatStartupWatchdogUsesShortCombinedWindow(t *testing.T) { - t.Parallel() - - sm, _ := newCommandTestManager(t) - sm.StartChatCommand("desktop-agent", "run-start-timeout", "conv-1", "", "client-1", nil) - cfg := &config.Config{ - ChatStartTimeout: 15 * time.Millisecond, - ChatRenderStartTimeout: 20 * time.Millisecond, - } - - startedAt := time.Now() - WatchAcceptedCommandStartup(context.Background(), cfg, sm, "desktop-agent", "run-start-timeout") - elapsed := time.Since(startedAt) - if elapsed < 25*time.Millisecond || elapsed > 500*time.Millisecond { - t.Fatalf("startup watchdog elapsed = %s, want about 35ms", elapsed) - } - - sub := sm.SubscribeConversationStream("desktop-agent", "conv-1", 0, "") - defer sub.Cleanup() - last := sub.Events[len(sub.Events)-1] - if last.Type != session.StreamEventRunFinished || last.Payload["error_code"] != "startup_timeout" { - t.Fatalf("startup watchdog terminal = %s %#v", last.Type, last.Payload) - } -} diff --git a/crates/agent-gateway/internal/chatwire/payloads.go b/crates/agent-gateway/internal/chatwire/payloads.go deleted file mode 100644 index 4ed77dcb5..000000000 --- a/crates/agent-gateway/internal/chatwire/payloads.go +++ /dev/null @@ -1,159 +0,0 @@ -// Package chatwire shapes agent chat protobuf events into the JSON payloads -// sent to webui clients. Shaping (decode, normalize, result trimming) happens -// exactly once at ingress so every subscriber observes identical bytes. -// -// Tool-call arguments pass through untouched: the desktop app is the single -// producer of streaming previews (truncated text + __liveagent_stream_preview -// metadata) and the gateway must never recompute or overwrite them. -package chatwire - -import ( - "encoding/json" - "strings" - "unicode/utf8" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -// EventPayload shapes a ChatEvent into a wire payload, decoding the JSON data -// blob and trimming oversized tool-result content. -func EventPayload(event *gatewayv2.ChatEvent, seq int64, workdirInput ...string) map[string]any { - protoType := EventTypeName(event.GetType()) - payload := map[string]any{ - "type": protoType, - } - if seq > 0 { - payload["seq"] = seq - } - if len(workdirInput) > 0 { - if workdir := strings.TrimSpace(workdirInput[0]); workdir != "" { - payload["workdir"] = workdir - } - } - - raw := strings.TrimSpace(event.GetData()) - if raw == "" { - raw = "{}" - } - - var decoded map[string]any - if err := json.Unmarshal([]byte(raw), &decoded); err == nil { - for key, value := range decoded { - payload[key] = value - } - } - - if conversationID := strings.TrimSpace(event.GetConversationId()); conversationID != "" { - payload["conversation_id"] = conversationID - } - - TrimLargeToolResultContent(payload, protoType) - - return payload -} - -const toolResultMaxBytes = 200 - -// TrimLargeToolResultContent truncates oversized tool-result content in place, -// attaching a __liveagent_stream_preview meta block describing the original -// size. Tool-call arguments are never touched. -func TrimLargeToolResultContent(payload map[string]any, protoType string) { - eventType, _ := payload["type"].(string) - if eventType != "tool_result" && protoType != "tool_result" { - return - } - switch content := payload["content"].(type) { - case string: - if len(content) > toolResultMaxBytes { - payload["content"] = truncateRuneSafe(content, toolResultMaxBytes) - setPreviewMeta(payload, "content", content) - } - case []any: - for _, item := range content { - block, ok := item.(map[string]any) - if !ok { - continue - } - if text, ok := block["text"].(string); ok && len(text) > toolResultMaxBytes { - block["text"] = truncateRuneSafe(text, toolResultMaxBytes) - setPreviewMeta(block, "text", text) - } - } - } -} - -// truncateRuneSafe cuts s to at most maxBytes without splitting a UTF-8 rune. -func truncateRuneSafe(s string, maxBytes int) string { - if len(s) <= maxBytes { - return s - } - cut := maxBytes - for cut > 0 && !utf8.RuneStart(s[cut]) { - cut-- - } - return s[:cut] -} - -func setPreviewMeta(container map[string]any, fieldName string, original string) { - const metaKey = "__liveagent_stream_preview" - meta, _ := container[metaKey].(map[string]any) - if meta == nil { - meta = map[string]any{} - container[metaKey] = meta - } - fields, _ := meta["fields"].(map[string]any) - if fields == nil { - fields = map[string]any{} - meta["fields"] = fields - } - fields[fieldName] = map[string]any{ - "chars": utf8.RuneCountInString(original), - "lines": countLines(original), - "truncated": true, - } -} - -func countLines(s string) int { - if len(s) == 0 { - return 0 - } - n := 1 - for i := 0; i < len(s); i++ { - switch s[i] { - case '\n': - n++ - case '\r': - n++ - if i+1 < len(s) && s[i+1] == '\n' { - i++ - } - } - } - return n -} - -// EventTypeName maps the protobuf ChatEvent type enum to its wire name. -func EventTypeName(eventType gatewayv2.ChatEvent_ChatEventType) string { - switch eventType { - case gatewayv2.ChatEvent_TOKEN: - return "token" - case gatewayv2.ChatEvent_THINKING: - return "thinking" - case gatewayv2.ChatEvent_TOOL_CALL: - return "tool_call" - case gatewayv2.ChatEvent_TOOL_RESULT: - return "tool_result" - case gatewayv2.ChatEvent_DONE: - return "done" - case gatewayv2.ChatEvent_ERROR: - return "error" - case gatewayv2.ChatEvent_TOOL_STATUS: - return "tool_status" - case gatewayv2.ChatEvent_HOSTED_SEARCH: - return "hosted_search" - case gatewayv2.ChatEvent_USER_MESSAGE: - return "user_message" - default: - return "message" - } -} diff --git a/crates/agent-gateway/internal/chatwire/payloads_test.go b/crates/agent-gateway/internal/chatwire/payloads_test.go deleted file mode 100644 index 6342d8133..000000000 --- a/crates/agent-gateway/internal/chatwire/payloads_test.go +++ /dev/null @@ -1,141 +0,0 @@ -package chatwire - -import ( - "strings" - "testing" - "unicode/utf8" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -func TestEventPayloadPreservesHostedSearch(t *testing.T) { - payload := EventPayload(&gatewayv2.ChatEvent{ - Type: gatewayv2.ChatEvent_HOSTED_SEARCH, - ConversationId: "conversation-1", - Data: `{"id":"search-1","provider":"codex","status":"completed","queries":["设计模式定义"],"sources":[{"url":"https://example.com/pattern","title":"设计模式"}],"round":2}`, - }, 7) - - if payload["type"] != "hosted_search" { - t.Fatalf("expected hosted_search type, got %#v", payload["type"]) - } - if payload["conversation_id"] != "conversation-1" { - t.Fatalf("expected conversation id, got %#v", payload["conversation_id"]) - } - if payload["id"] != "search-1" { - t.Fatalf("expected search id, got %#v", payload["id"]) - } - if payload["provider"] != "codex" { - t.Fatalf("expected provider, got %#v", payload["provider"]) - } - if payload["status"] != "completed" { - t.Fatalf("expected status, got %#v", payload["status"]) - } - if payload["seq"] != int64(7) { - t.Fatalf("expected seq 7, got %#v", payload["seq"]) - } -} - -func TestEventPayloadPreservesToolCallDeltaType(t *testing.T) { - payload := EventPayload(&gatewayv2.ChatEvent{ - Type: gatewayv2.ChatEvent_TOOL_CALL, - ConversationId: "conversation-1", - Data: `{"type":"tool_call_delta","id":"call-write","name":"Write","arguments":{"path":"src/app.ts","content":"con"},"round":1}`, - }, 8) - - if payload["type"] != "tool_call_delta" { - t.Fatalf("expected tool_call_delta type, got %#v", payload["type"]) - } - if payload["conversation_id"] != "conversation-1" { - t.Fatalf("expected conversation id, got %#v", payload["conversation_id"]) - } - if payload["id"] != "call-write" { - t.Fatalf("expected tool call id, got %#v", payload["id"]) - } - if payload["name"] != "Write" { - t.Fatalf("expected tool name, got %#v", payload["name"]) - } - if payload["seq"] != int64(8) { - t.Fatalf("expected seq 8, got %#v", payload["seq"]) - } -} - -// Tool-call arguments must pass through untouched: the desktop app already -// truncated them and stamped the preview meta; the gateway rewriting either -// caused the chars regression this suite guards against. -func TestEventPayloadLeavesToolCallArgsUntouched(t *testing.T) { - longContent := strings.Repeat("x", 500) - payload := EventPayload(&gatewayv2.ChatEvent{ - Type: gatewayv2.ChatEvent_TOOL_CALL, - ConversationId: "conversation-1", - Data: `{"type":"tool_call_delta","id":"call-write","name":"Write","arguments":{"path":"src/app.ts","content":"` + - longContent + - `","__liveagent_stream_preview":{"v":2,"progress":6000,"fields":{"content":{"chars":6000,"lines":12,"truncated":true}}}},"round":1}`, - }, 9) - - args, ok := payload["arguments"].(map[string]any) - if !ok { - t.Fatalf("expected arguments map, got %#v", payload["arguments"]) - } - if content := args["content"].(string); content != longContent { - t.Fatalf("content modified: len=%d, want %d", len(content), len(longContent)) - } - meta, ok := args["__liveagent_stream_preview"].(map[string]any) - if !ok { - t.Fatalf("expected producer preview meta preserved, got %#v", args) - } - fields := meta["fields"].(map[string]any) - info := fields["content"].(map[string]any) - if chars, _ := info["chars"].(float64); chars != 6000 { - t.Fatalf("producer chars overwritten: %#v", info) - } - if progress, _ := meta["progress"].(float64); progress != 6000 { - t.Fatalf("producer progress overwritten: %#v", meta) - } -} - -func TestTrimLargeToolResultContentTruncatesToolResult(t *testing.T) { - longText := strings.Repeat("r", 300) - payload := map[string]any{ - "type": "tool_result", - "content": longText, - } - - TrimLargeToolResultContent(payload, "tool_result") - - if content := payload["content"].(string); len(content) != 200 { - t.Fatalf("trimmed result length = %d, want 200", len(content)) - } - meta, ok := payload["__liveagent_stream_preview"].(map[string]any) - if !ok { - t.Fatalf("expected preview meta on tool_result payload") - } - fields := meta["fields"].(map[string]any) - info := fields["content"].(map[string]any) - if info["chars"] != 300 || info["truncated"] != true { - t.Fatalf("preview meta = %#v", info) - } -} - -func TestTrimLargeToolResultContentIsRuneSafe(t *testing.T) { - longText := strings.Repeat("汉", 100) // 300 bytes, 100 runes - payload := map[string]any{ - "type": "tool_result", - "content": longText, - } - - TrimLargeToolResultContent(payload, "tool_result") - - content := payload["content"].(string) - if !utf8.ValidString(content) { - t.Fatalf("truncated content is not valid UTF-8") - } - if len(content) > 200 { - t.Fatalf("trimmed result length = %d, want <= 200", len(content)) - } - meta := payload["__liveagent_stream_preview"].(map[string]any) - fields := meta["fields"].(map[string]any) - info := fields["content"].(map[string]any) - if info["chars"] != 100 { - t.Fatalf("chars should count runes, got %#v", info["chars"]) - } -} diff --git a/crates/agent-gateway/internal/config/config.go b/crates/agent-gateway/internal/config/config.go deleted file mode 100644 index f9e15a212..000000000 --- a/crates/agent-gateway/internal/config/config.go +++ /dev/null @@ -1,246 +0,0 @@ -package config - -import ( - "flag" - "os" - "path/filepath" - "strconv" - "strings" - "time" -) - -const DefaultMaxMessageBytes = 64 * 1024 * 1024 - -// 三条 v2 链路并发连接上限的默认值;浏览器/终端按"每 Agent 数个会话"的 -// 使用形态放大(100 Agent × 若干浏览器页签/终端页)。 -const ( - DefaultMaxAgentConnections = 256 - DefaultMaxBrowserConnections = 128 - DefaultMaxTerminalConnections = 512 -) - -type Config struct { - Token string - // AgentDB 是每 Agent 凭证 SQLite 数据库路径;默认自动创建于用户配置目录。 - AgentDB string - // 三条 v2 链路的并发连接上限(升级前检查,超限 503);0/负值回落默认。 - // 默认值按 100+ 桌面 Agent 的目标规模取整。 - MaxAgentConnections int - MaxBrowserConnections int - MaxTerminalConnections int - HTTPAddr string - TLSCert string - TLSKey string - RequestTimeout time.Duration - ChatPrepareTimeout time.Duration - ChatDeliveryTimeout time.Duration - ChatStartTimeout time.Duration - ChatRenderStartTimeout time.Duration - HeartbeatPeriod time.Duration - WebSocketHeartbeatPeriod time.Duration - WebSocketHeartbeatGrace time.Duration - WebSocketWriteTimeout time.Duration - WebSocketWriteQueueSize int - MaxMessageBytes int - RelayBufferSeconds int -} - -func Load() *Config { - cfg := &Config{} - - flag.StringVar(&cfg.Token, "token", getenv("LIVEAGENT_GATEWAY_TOKEN", ""), "gateway authentication token") - flag.StringVar(&cfg.AgentDB, "agent-db", getenv("LIVEAGENT_GATEWAY_AGENT_DB", defaultAgentDBPath()), "per-agent token SQLite database path (auto-created by default)") - flag.IntVar(&cfg.MaxAgentConnections, "max-agent-connections", getenvInt("LIVEAGENT_GATEWAY_MAX_AGENT_CONNECTIONS", DefaultMaxAgentConnections), "maximum concurrent desktop agent connections") - flag.IntVar(&cfg.MaxBrowserConnections, "max-browser-connections", getenvInt("LIVEAGENT_GATEWAY_MAX_BROWSER_CONNECTIONS", DefaultMaxBrowserConnections), "maximum concurrent browser connections") - flag.IntVar(&cfg.MaxTerminalConnections, "max-terminal-connections", getenvInt("LIVEAGENT_GATEWAY_MAX_TERMINAL_CONNECTIONS", DefaultMaxTerminalConnections), "maximum concurrent terminal data-plane connections") - flag.StringVar(&cfg.HTTPAddr, "http-addr", getenv("LIVEAGENT_GATEWAY_HTTP_ADDR", defaultHTTPAddr()), "HTTP listen address") - flag.StringVar(&cfg.TLSCert, "tls-cert", getenv("LIVEAGENT_GATEWAY_TLS_CERT", ""), "TLS certificate path") - flag.StringVar(&cfg.TLSKey, "tls-key", getenv("LIVEAGENT_GATEWAY_TLS_KEY", ""), "TLS private key path") - flag.DurationVar(&cfg.RequestTimeout, "request-timeout", getenvDuration("LIVEAGENT_GATEWAY_REQUEST_TIMEOUT", 2*time.Minute), "request timeout for non-streaming API calls") - flag.DurationVar(&cfg.ChatPrepareTimeout, "chat-prepare-timeout", getenvDuration("LIVEAGENT_GATEWAY_CHAT_PREPARE_TIMEOUT", 2*time.Second), "timeout for the pre-submit desktop agent liveness probe") - flag.DurationVar(&cfg.ChatDeliveryTimeout, "chat-delivery-timeout", getenvDuration("LIVEAGENT_GATEWAY_CHAT_DELIVERY_TIMEOUT", 5*time.Second), "timeout delivering an accepted chat command to the desktop agent stream") - flag.DurationVar(&cfg.ChatStartTimeout, "chat-start-timeout", getenvDuration("LIVEAGENT_GATEWAY_CHAT_START_TIMEOUT", 5*time.Second), "initial timeout waiting for a delivered remote chat request to start") - flag.DurationVar(&cfg.ChatRenderStartTimeout, "chat-render-start-timeout", getenvDuration("LIVEAGENT_GATEWAY_CHAT_RENDER_START_TIMEOUT", 10*time.Second), "additional timeout waiting for the desktop app to start a delivered remote chat request") - flag.DurationVar(&cfg.HeartbeatPeriod, "heartbeat-period", getenvDuration("LIVEAGENT_GATEWAY_HEARTBEAT_PERIOD", 30*time.Second), "ping interval for agent connection") - flag.DurationVar(&cfg.WebSocketHeartbeatPeriod, "websocket-heartbeat-period", getenvDuration("LIVEAGENT_GATEWAY_WS_HEARTBEAT_PERIOD", 15*time.Second), "ping interval for browser WebSocket connections") - flag.DurationVar(&cfg.WebSocketHeartbeatGrace, "websocket-heartbeat-grace", getenvDuration("LIVEAGENT_GATEWAY_WS_HEARTBEAT_GRACE", 5*time.Second), "extra slack added to the browser WebSocket idle timeout (idle = 3x period + grace)") - flag.DurationVar(&cfg.WebSocketWriteTimeout, "websocket-write-timeout", getenvDuration("LIVEAGENT_GATEWAY_WS_WRITE_TIMEOUT", 10*time.Second), "write timeout for browser WebSocket connections") - flag.IntVar(&cfg.WebSocketWriteQueueSize, "websocket-write-queue-size", getenvInt("LIVEAGENT_GATEWAY_WS_WRITE_QUEUE_SIZE", 512), "write queue buffer size for browser WebSocket connections") - flag.IntVar( - &cfg.MaxMessageBytes, - "max-message-bytes", - getenvInt( - "LIVEAGENT_GATEWAY_MAX_MESSAGE_BYTES", - getenvInt("LIVEAGENT_GATEWAY_GRPC_MAX_MESSAGE_BYTES", DefaultMaxMessageBytes), - ), - "maximum WebSocket protobuf message size in bytes", - ) - flag.IntVar(&cfg.RelayBufferSeconds, "relay-buffer-seconds", getenvInt("LIVEAGENT_GATEWAY_RELAY_BUFFER_SECONDS", 30), "seconds of chat events to buffer for brief reconnections") - os.Args = normalizeLegacyArgs(os.Args) - flag.Parse() - - cfg.Token = strings.TrimSpace(cfg.Token) - cfg.AgentDB = strings.TrimSpace(cfg.AgentDB) - // Agent 凭证数据库是网关始终启用的基础能力;即使启动参数显式传空,也 - // 回退到自动路径,不能通过空值关闭。 - if cfg.AgentDB == "" { - cfg.AgentDB = defaultAgentDBPath() - } - cfg.TLSCert = strings.TrimSpace(cfg.TLSCert) - cfg.TLSKey = strings.TrimSpace(cfg.TLSKey) - - if cfg.Token == "" { - flag.Usage() - panic("gateway token is required") - } - if cfg.MaxMessageBytes <= 0 { - cfg.MaxMessageBytes = DefaultMaxMessageBytes - } - if cfg.MaxAgentConnections <= 0 { - cfg.MaxAgentConnections = DefaultMaxAgentConnections - } - if cfg.MaxBrowserConnections <= 0 { - cfg.MaxBrowserConnections = DefaultMaxBrowserConnections - } - if cfg.MaxTerminalConnections <= 0 { - cfg.MaxTerminalConnections = DefaultMaxTerminalConnections - } - if cfg.ChatPrepareTimeout <= 0 { - cfg.ChatPrepareTimeout = 2 * time.Second - } - if cfg.ChatDeliveryTimeout <= 0 { - cfg.ChatDeliveryTimeout = 5 * time.Second - } - if cfg.ChatStartTimeout <= 0 { - cfg.ChatStartTimeout = 5 * time.Second - } - if cfg.ChatRenderStartTimeout <= 0 { - cfg.ChatRenderStartTimeout = 10 * time.Second - } - if cfg.WebSocketHeartbeatPeriod <= 0 { - cfg.WebSocketHeartbeatPeriod = 15 * time.Second - } - if cfg.WebSocketHeartbeatGrace <= 0 { - cfg.WebSocketHeartbeatGrace = 5 * time.Second - } - if cfg.WebSocketWriteTimeout <= 0 { - cfg.WebSocketWriteTimeout = 10 * time.Second - } - if cfg.WebSocketWriteQueueSize <= 0 { - cfg.WebSocketWriteQueueSize = 512 - } - if cfg.RelayBufferSeconds <= 0 { - cfg.RelayBufferSeconds = 30 - } - - return cfg -} - -// normalizeLegacyArgs 在进入新版 FlagSet 前统一清理已删除参数。旧名称不再注册、 -// 不出现在帮助中,也不会恢复 v1/gRPC 或离线命令队列;真正未知的参数仍由 flag -// 正常拒绝。消息大小参数仍有对应语义,因此转换为新名称;显式的新名称优先。 -func normalizeLegacyArgs(args []string) []string { - if len(args) == 0 { - return args - } - - hasCurrentMessageLimit := false - for _, arg := range args[1:] { - if arg == "-max-message-bytes" || arg == "--max-message-bytes" || - strings.HasPrefix(arg, "-max-message-bytes=") || - strings.HasPrefix(arg, "--max-message-bytes=") { - hasCurrentMessageLimit = true - break - } - } - - normalized := make([]string, 0, len(args)) - normalized = append(normalized, args[0]) - for index := 1; index < len(args); index++ { - arg := args[index] - if arg == "--" { - normalized = append(normalized, args[index:]...) - break - } - switch { - case arg == "-grpc-addr" || arg == "--grpc-addr" || - arg == "-command-queue-timeout" || arg == "--command-queue-timeout": - if index+1 < len(args) { - index++ - } - case strings.HasPrefix(arg, "-grpc-addr=") || - strings.HasPrefix(arg, "--grpc-addr=") || - strings.HasPrefix(arg, "-command-queue-timeout=") || - strings.HasPrefix(arg, "--command-queue-timeout="): - continue - case arg == "-grpc-max-message-bytes" || arg == "--grpc-max-message-bytes": - if index+1 < len(args) { - if !hasCurrentMessageLimit { - normalized = append(normalized, "-max-message-bytes", args[index+1]) - } - index++ - } - case strings.HasPrefix(arg, "-grpc-max-message-bytes=") || - strings.HasPrefix(arg, "--grpc-max-message-bytes="): - if !hasCurrentMessageLimit { - value := strings.SplitN(arg, "=", 2)[1] - normalized = append(normalized, "-max-message-bytes="+value) - } - default: - normalized = append(normalized, arg) - } - } - return normalized -} - -func defaultAgentDBPath() string { - if dataDir := strings.TrimSpace(os.Getenv("LIVEAGENT_GATEWAY_DATA_DIR")); dataDir != "" { - return filepath.Join(dataDir, "gateway.db") - } - if configDir, err := os.UserConfigDir(); err == nil && strings.TrimSpace(configDir) != "" { - return filepath.Join(configDir, "liveagent", "gateway.db") - } - return filepath.Join(".", "liveagent-gateway.db") -} - -func getenv(key, fallback string) string { - if value := os.Getenv(key); value != "" { - return value - } - return fallback -} - -func defaultHTTPAddr() string { - port := strings.TrimSpace(os.Getenv("PORT")) - if port == "" { - return ":443" - } - if strings.HasPrefix(port, ":") { - return port - } - return ":" + port -} - -func getenvDuration(key string, fallback time.Duration) time.Duration { - value := os.Getenv(key) - if value == "" { - return fallback - } - parsed, err := time.ParseDuration(value) - if err != nil { - return fallback - } - return parsed -} - -func getenvInt(key string, fallback int) int { - value := os.Getenv(key) - if value == "" { - return fallback - } - parsed, err := strconv.Atoi(value) - if err != nil || parsed <= 0 { - return fallback - } - return parsed -} diff --git a/crates/agent-gateway/internal/config/config_test.go b/crates/agent-gateway/internal/config/config_test.go deleted file mode 100644 index 2d04b98be..000000000 --- a/crates/agent-gateway/internal/config/config_test.go +++ /dev/null @@ -1,200 +0,0 @@ -package config - -import ( - "flag" - "io" - "os" - "path/filepath" - "testing" - "time" -) - -func TestLoadNormalizesTokenAndTLSPaths(t *testing.T) { - t.Setenv("LIVEAGENT_GATEWAY_TOKEN", " secret-token\r\n") - t.Setenv("LIVEAGENT_GATEWAY_TLS_CERT", " cert.pem ") - t.Setenv("LIVEAGENT_GATEWAY_TLS_KEY", "\tkey.pem\r\n") - resetFlagsForTest(t) - cfg := Load() - if cfg.Token != "secret-token" { - t.Fatalf("Token = %q, want %q", cfg.Token, "secret-token") - } - if cfg.TLSCert != "cert.pem" { - t.Fatalf("TLSCert = %q, want %q", cfg.TLSCert, "cert.pem") - } - if cfg.TLSKey != "key.pem" { - t.Fatalf("TLSKey = %q, want %q", cfg.TLSKey, "key.pem") - } -} - -func TestLoadWebSocketHeartbeatGrace(t *testing.T) { - t.Setenv("LIVEAGENT_GATEWAY_TOKEN", "dev-token") - resetFlagsForTest(t) - cfg := Load() - if cfg.WebSocketHeartbeatGrace != 5*time.Second { - t.Fatalf("WebSocketHeartbeatGrace default = %s, want 5s", cfg.WebSocketHeartbeatGrace) - } - - t.Setenv("LIVEAGENT_GATEWAY_WS_HEARTBEAT_GRACE", "45s") - resetFlagsForTest(t) - cfg = Load() - if cfg.WebSocketHeartbeatGrace != 45*time.Second { - t.Fatalf("WebSocketHeartbeatGrace = %s, want 45s", cfg.WebSocketHeartbeatGrace) - } - - t.Setenv("LIVEAGENT_GATEWAY_WS_HEARTBEAT_GRACE", "-3s") - resetFlagsForTest(t) - cfg = Load() - if cfg.WebSocketHeartbeatGrace != 5*time.Second { - t.Fatalf("WebSocketHeartbeatGrace with negative env = %s, want 5s fallback", cfg.WebSocketHeartbeatGrace) - } -} - -func TestLoadChatTimeouts(t *testing.T) { - t.Setenv("LIVEAGENT_GATEWAY_TOKEN", "dev-token") - resetFlagsForTest(t) - cfg := Load() - if cfg.ChatPrepareTimeout != 2*time.Second { - t.Fatalf("ChatPrepareTimeout default = %s, want 2s", cfg.ChatPrepareTimeout) - } - if cfg.ChatDeliveryTimeout != 5*time.Second { - t.Fatalf("ChatDeliveryTimeout default = %s, want 5s", cfg.ChatDeliveryTimeout) - } - if cfg.ChatStartTimeout != 5*time.Second { - t.Fatalf("ChatStartTimeout default = %s, want 5s", cfg.ChatStartTimeout) - } - if cfg.ChatRenderStartTimeout != 10*time.Second { - t.Fatalf("ChatRenderStartTimeout default = %s, want 10s", cfg.ChatRenderStartTimeout) - } - - t.Setenv("LIVEAGENT_GATEWAY_CHAT_PREPARE_TIMEOUT", "750ms") - t.Setenv("LIVEAGENT_GATEWAY_CHAT_DELIVERY_TIMEOUT", "3s") - t.Setenv("LIVEAGENT_GATEWAY_CHAT_START_TIMEOUT", "4s") - t.Setenv("LIVEAGENT_GATEWAY_CHAT_RENDER_START_TIMEOUT", "8s") - resetFlagsForTest(t) - cfg = Load() - if cfg.ChatPrepareTimeout != 750*time.Millisecond || - cfg.ChatDeliveryTimeout != 3*time.Second || - cfg.ChatStartTimeout != 4*time.Second || - cfg.ChatRenderStartTimeout != 8*time.Second { - t.Fatalf("custom chat timeouts = prepare:%s delivery:%s start:%s render:%s", - cfg.ChatPrepareTimeout, - cfg.ChatDeliveryTimeout, - cfg.ChatStartTimeout, - cfg.ChatRenderStartTimeout, - ) - } - - t.Setenv("LIVEAGENT_GATEWAY_CHAT_PREPARE_TIMEOUT", "-1s") - t.Setenv("LIVEAGENT_GATEWAY_CHAT_DELIVERY_TIMEOUT", "0s") - t.Setenv("LIVEAGENT_GATEWAY_CHAT_START_TIMEOUT", "-1s") - t.Setenv("LIVEAGENT_GATEWAY_CHAT_RENDER_START_TIMEOUT", "-1s") - resetFlagsForTest(t) - cfg = Load() - if cfg.ChatPrepareTimeout != 2*time.Second || - cfg.ChatDeliveryTimeout != 5*time.Second || - cfg.ChatStartTimeout != 5*time.Second || - cfg.ChatRenderStartTimeout != 10*time.Second { - t.Fatalf("normalized chat timeouts = prepare:%s delivery:%s start:%s render:%s", - cfg.ChatPrepareTimeout, - cfg.ChatDeliveryTimeout, - cfg.ChatStartTimeout, - cfg.ChatRenderStartTimeout, - ) - } -} - -func TestLoadUsesRailwayPortForHTTPDefault(t *testing.T) { - t.Setenv("PORT", "8080") - t.Setenv("LIVEAGENT_GATEWAY_TOKEN", "dev-token") - - resetFlagsForTest(t) - cfg := Load() - - if cfg.HTTPAddr != ":8080" { - t.Fatalf("HTTPAddr = %q, want :8080", cfg.HTTPAddr) - } -} - -func TestLoadDefaultsToAutoDB(t *testing.T) { - t.Setenv("LIVEAGENT_GATEWAY_TOKEN", "dev-token") - dataDir := t.TempDir() - t.Setenv("LIVEAGENT_GATEWAY_DATA_DIR", dataDir) - resetFlagsForTest(t) - cfg := Load() - if cfg.AgentDB != filepath.Join(dataDir, "gateway.db") { - t.Fatalf("AgentDB = %q, want automatic data-dir path", cfg.AgentDB) - } -} - -func TestLoadEmptyAgentDBStillUsesAutomaticPath(t *testing.T) { - t.Setenv("LIVEAGENT_GATEWAY_TOKEN", "dev-token") - dataDir := t.TempDir() - t.Setenv("LIVEAGENT_GATEWAY_DATA_DIR", dataDir) - resetFlagsForTest(t) - os.Args = []string{"gateway", "-agent-db", ""} - cfg := Load() - if cfg.AgentDB != filepath.Join(dataDir, "gateway.db") { - t.Fatalf("AgentDB = %q, want automatic path when explicitly empty", cfg.AgentDB) - } -} - -func TestLoadAcceptsLegacyFlagsAndMapsMessageLimit(t *testing.T) { - t.Setenv("LIVEAGENT_GATEWAY_TOKEN", "dev-token") - resetFlagsForTest(t) - os.Args = []string{ - "gateway", - "-grpc-addr", ":50051", - "-grpc-max-message-bytes", "33554432", - "-command-queue-timeout", "45s", - } - - cfg := Load() - if cfg.MaxMessageBytes != 32*1024*1024 { - t.Fatalf("MaxMessageBytes = %d, want legacy flag value", cfg.MaxMessageBytes) - } - for _, name := range []string{"grpc-addr", "grpc-max-message-bytes", "command-queue-timeout"} { - if flag.CommandLine.Lookup(name) != nil { - t.Fatalf("legacy flag %q should not be registered", name) - } - } -} - -func TestLoadCurrentMessageLimitTakesPriorityOverLegacyFlag(t *testing.T) { - t.Setenv("LIVEAGENT_GATEWAY_TOKEN", "dev-token") - resetFlagsForTest(t) - os.Args = []string{ - "gateway", - "-max-message-bytes", "16777216", - "-grpc-max-message-bytes", "33554432", - } - - cfg := Load() - if cfg.MaxMessageBytes != 16*1024*1024 { - t.Fatalf("MaxMessageBytes = %d, want current flag value", cfg.MaxMessageBytes) - } -} - -func TestLoadMapsLegacyMessageLimitEnvironment(t *testing.T) { - t.Setenv("LIVEAGENT_GATEWAY_TOKEN", "dev-token") - t.Setenv("LIVEAGENT_GATEWAY_GRPC_MAX_MESSAGE_BYTES", "33554432") - resetFlagsForTest(t) - - cfg := Load() - if cfg.MaxMessageBytes != 32*1024*1024 { - t.Fatalf("MaxMessageBytes = %d, want legacy environment value", cfg.MaxMessageBytes) - } -} - -func resetFlagsForTest(t *testing.T) { - t.Helper() - oldCommandLine := flag.CommandLine - oldArgs := os.Args - t.Cleanup(func() { - flag.CommandLine = oldCommandLine - os.Args = oldArgs - }) - - flag.CommandLine = flag.NewFlagSet("gateway", flag.ContinueOnError) - flag.CommandLine.SetOutput(io.Discard) - os.Args = []string{"gateway"} -} diff --git a/crates/agent-gateway/internal/db/db.go b/crates/agent-gateway/internal/db/db.go deleted file mode 100644 index b5cb67a89..000000000 --- a/crates/agent-gateway/internal/db/db.go +++ /dev/null @@ -1,47 +0,0 @@ -// Package db 管理网关共享的数据库连接池:各持久化子系统在同一池上建各自的表, -// 避免对同一库开多个池放大锁冲突。当前后端为内嵌 SQLite,后端切换在 Open 分发。 -package db - -import ( - "database/sql" - "errors" - "strings" -) - -// DB 是 Gateway 共享连接池句柄。 -type DB struct { - pool *sql.DB -} - -// Open 打开连接池;DSN 为空直接报错,Gateway 不提供关闭持久化的模式。目前仅 -// 支持 SQLite(DSN 即文件路径),后端扩展(如 PostgreSQL)在此按 DSN 分发。 -func Open(dsn string) (*DB, error) { - dsn = strings.TrimSpace(dsn) - if dsn == "" { - return nil, errors.New("gateway database path is required") - } - pool, err := openSQLite(dsn) - if err != nil { - return nil, err - } - return &DB{pool: pool}, nil -} - -func (d *DB) Enabled() bool { - return d != nil -} - -// Pool 返回底层连接池供子系统建表与查询;生命周期归本包,调用方不得 Close。 -func (d *DB) Pool() *sql.DB { - if d == nil { - return nil - } - return d.pool -} - -func (d *DB) Close() error { - if d == nil { - return nil - } - return d.pool.Close() -} diff --git a/crates/agent-gateway/internal/db/sqlite.go b/crates/agent-gateway/internal/db/sqlite.go deleted file mode 100644 index 9f4979fe8..000000000 --- a/crates/agent-gateway/internal/db/sqlite.go +++ /dev/null @@ -1,37 +0,0 @@ -package db - -import ( - "database/sql" - "fmt" - "os" - "path/filepath" - - _ "modernc.org/sqlite" -) - -// openSQLite 打开内嵌 SQLite 连接池。WAL 让读写不互斥,busy_timeout 让偶发写 -// 冲突等待重试;文件收紧到 0600(默认 0644,库内含凭证哈希等敏感数据)。 -func openSQLite(path string) (*sql.DB, error) { - if dir := filepath.Dir(path); dir != "." && dir != "" { - if err := os.MkdirAll(dir, 0o700); err != nil { - return nil, fmt.Errorf("create sqlite directory: %w", err) - } - } - pool, err := sql.Open("sqlite", "file:"+path+"?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)") - if err != nil { - return nil, fmt.Errorf("open sqlite: %w", err) - } - // SQLite 写天然单写者串行、读走 WAL 并行;4 足够覆盖握手校验 + 管理 API 的并发面。 - pool.SetMaxOpenConns(4) - pool.SetMaxIdleConns(4) - // sql.Open 惰性建连;Ping 强制创建库文件,chmod 才有目标。 - if err := pool.Ping(); err != nil { - _ = pool.Close() - return nil, fmt.Errorf("ping sqlite: %w", err) - } - if err := os.Chmod(path, 0o600); err != nil { - _ = pool.Close() - return nil, fmt.Errorf("chmod sqlite file: %w", err) - } - return pool, nil -} diff --git a/crates/agent-gateway/internal/handler/agents.go b/crates/agent-gateway/internal/handler/agents.go deleted file mode 100644 index 172fe36a0..000000000 --- a/crates/agent-gateway/internal/handler/agents.go +++ /dev/null @@ -1,202 +0,0 @@ -package handler - -import ( - "encoding/json" - "errors" - "io" - "net/http" - "strconv" - "strings" - "time" - - "github.com/liveagent/agent-gateway/internal/auth/agenttoken" - "github.com/liveagent/agent-gateway/internal/session" -) - -// Agent 目录与凭证管理 API(挂在管理 token 中间件下): -// GET /api/agents?page=&page_size=&status=all|online|offline — Agent 筛选分页目录 -// POST /api/agents/{id}/token — 签发/轮换凭证并立即踢下线(明文仅出现在本次响应) -// PATCH /api/agents/{id} — 修改可选名称 -// DELETE /api/agents/{id} — 删除整条记录并断开活跃会话 - -// agentDirectoryEntry 合并持久化登记、独立凭证信息与实时会话状态。 -type agentDirectoryEntry struct { - AgentID string `json:"agent_id"` - Online bool `json:"online"` - HasToken bool `json:"has_token"` - RegisteredAt string `json:"registered_at"` - TokenCreatedAt string `json:"token_created_at,omitempty"` - Name string `json:"name"` - - AgentVersion string `json:"agent_version,omitempty"` - ConnectedSince int64 `json:"connected_since,omitempty"` -} - -// ListAgents 按状态筛选并分页返回持久化 Agent 目录,同时合并当前页实时状态。 -func ListAgents(sm *session.Manager, tokens *agenttoken.Store) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - statusFilter, err := agenttoken.ParseStatusFilter(r.URL.Query().Get("status")) - if err != nil { - writeError(w, http.StatusBadRequest, "invalid status filter") - return - } - - // 同一次状态快照同时用于数据库筛选和当前页状态合并,避免两次读取间的竞态。 - statusesByAgentID, onlineAgentIDs := sm.AgentDirectoryStatusSnapshot() - - page, err := tokens.List(agenttoken.PageParams{ - Page: atoiDefault(r.URL.Query().Get("page"), 0), - PageSize: atoiDefault(r.URL.Query().Get("page_size"), 0), - Status: statusFilter, - OnlineAgentIDs: onlineAgentIDs, - }) - if err != nil { - writeError(w, http.StatusInternalServerError, "list agents failed") - return - } - - agents := make([]agentDirectoryEntry, 0, len(page.Entries)) - for _, entry := range page.Entries { - row := agentDirectoryEntry{ - AgentID: entry.AgentID, - HasToken: entry.HasToken, - RegisteredAt: entry.RegisteredAt.UTC().Format(time.RFC3339), - Name: entry.Name, - } - if entry.HasToken { - row.TokenCreatedAt = entry.TokenCreatedAt.UTC().Format(time.RFC3339) - } - if status, ok := statusesByAgentID[entry.AgentID]; ok { - row.Online = status.Online - row.AgentVersion = status.AgentVersion - row.ConnectedSince = status.ConnectedSince - } - agents = append(agents, row) - } - - writeJSON(w, http.StatusOK, map[string]any{ - "agents": agents, - "page": page.Page, - "page_size": page.PageSize, - "total": page.Total, - "has_more": page.HasMore, - }) - } -} - -// atoiDefault 解析非负整数查询参数,非法/缺省回落到 fallback(钳制交给 Store)。 -func atoiDefault(raw string, fallback int) int { - raw = strings.TrimSpace(raw) - if raw == "" { - return fallback - } - if n, err := strconv.Atoi(raw); err == nil && n >= 0 { - return n - } - return fallback -} - -func IssueAgentToken(sm *session.Manager, tokens *agenttoken.Store) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - agentID, err := agenttoken.NormalizeAgentID(r.PathValue("id")) - if err != nil { - writeAgentStoreError(w, err) - return - } - name, err := decodeAgentName(r) - if err != nil { - writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()}) - return - } - token, err := tokens.Issue(agentID, name) - if err != nil { - writeAgentStoreError(w, err) - return - } - disconnected := sm != nil && sm.DisconnectAgent(agentID) - // 明文只出现在本次响应;轮换后旧凭证立即不可用于下一次连接。 - writeJSON(w, http.StatusOK, map[string]any{ - "agent_id": agentID, - "token": token, - "disconnected": disconnected, - }) - } -} - -func UpdateAgentName(tokens *agenttoken.Store) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - agentID := strings.TrimSpace(r.PathValue("id")) - if agentID == "" { - writeAgentStoreError(w, agenttoken.ErrAgentIDRequired) - return - } - name, err := decodeAgentName(r) - if err != nil { - writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()}) - return - } - if err := tokens.UpdateName(agentID, name); err != nil { - writeAgentStoreError(w, err) - return - } - writeJSON(w, http.StatusOK, map[string]any{"agent_id": agentID, "name": strings.TrimSpace(name)}) - } -} - -func DeleteAgent(sm *session.Manager, tokens *agenttoken.Store) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - agentID := strings.TrimSpace(r.PathValue("id")) - if agentID == "" { - writeAgentStoreError(w, agenttoken.ErrAgentIDRequired) - return - } - deleted, err := tokens.Delete(agentID) - if err != nil { - writeAgentStoreError(w, err) - return - } - if !deleted { - writeAgentStoreError(w, agenttoken.ErrAgentNotFound) - return - } - disconnected := sm.ForgetAgent(agentID) - writeJSON(w, http.StatusOK, map[string]any{ - "agent_id": agentID, - "deleted": true, - "disconnected": disconnected, - }) - } -} - -type agentNameRequest struct { - Name string `json:"name"` -} - -func decodeAgentName(r *http.Request) (string, error) { - if r.Body == nil || r.ContentLength == 0 { - return "", nil - } - decoder := json.NewDecoder(io.LimitReader(r.Body, 4097)) - decoder.DisallowUnknownFields() - var payload agentNameRequest - if err := decoder.Decode(&payload); err != nil { - if errors.Is(err, io.EOF) { - return "", nil - } - return "", errors.New("invalid request body") - } - return payload.Name, nil -} - -func writeAgentStoreError(w http.ResponseWriter, err error) { - status := http.StatusInternalServerError - switch { - case errors.Is(err, agenttoken.ErrAgentIDRequired), - errors.Is(err, agenttoken.ErrInvalidAgentID), - errors.Is(err, agenttoken.ErrAgentNameTooLong): - status = http.StatusBadRequest - case errors.Is(err, agenttoken.ErrAgentNotFound): - status = http.StatusNotFound - } - writeJSON(w, status, map[string]any{"error": err.Error()}) -} diff --git a/crates/agent-gateway/internal/handler/health.go b/crates/agent-gateway/internal/handler/health.go deleted file mode 100644 index 65dc0005b..000000000 --- a/crates/agent-gateway/internal/handler/health.go +++ /dev/null @@ -1,11 +0,0 @@ -package handler - -import "net/http" - -func Health() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - writeJSON(w, http.StatusOK, map[string]any{ - "ok": true, - }) - } -} diff --git a/crates/agent-gateway/internal/handler/helpers.go b/crates/agent-gateway/internal/handler/helpers.go deleted file mode 100644 index 7ca83c300..000000000 --- a/crates/agent-gateway/internal/handler/helpers.go +++ /dev/null @@ -1,75 +0,0 @@ -package handler - -import ( - "context" - "encoding/json" - "errors" - "net/http" - - "github.com/google/uuid" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" - "github.com/liveagent/agent-gateway/internal/session" -) - -func writeJSON(w http.ResponseWriter, status int, payload any) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - _ = json.NewEncoder(w).Encode(payload) -} - -func writeError(w http.ResponseWriter, status int, message string) { - writeJSON(w, status, map[string]any{ - "error": message, - }) -} - -func newRequestID() string { - return uuid.NewString() -} - -func waitForEnvelope( - ctx context.Context, - ch <-chan *gatewayv2.AgentEnvelope, - done <-chan struct{}, -) (*gatewayv2.AgentEnvelope, error) { - select { - case <-ctx.Done(): - return nil, ctx.Err() - case <-done: - return nil, session.ErrAgentOffline - case env, ok := <-ch: - if !ok { - return nil, session.ErrAgentOffline - } - return env, nil - } -} - -func GatewayErrorStatus(errResp *gatewayv2.ErrorResponse) int { - if errResp == nil { - return http.StatusBadGateway - } - switch int(errResp.GetCode()) { - case http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound, http.StatusConflict: - return int(errResp.GetCode()) - default: - return http.StatusBadGateway - } -} - -func errorMessage(err error, fallback string) string { - if err == nil { - return fallback - } - if errors.Is(err, context.DeadlineExceeded) { - return "request timed out" - } - if errors.Is(err, context.Canceled) { - return "request canceled" - } - if errors.Is(err, session.ErrAgentOffline) { - return "agent offline" - } - return err.Error() -} diff --git a/crates/agent-gateway/internal/handler/helpers_test.go b/crates/agent-gateway/internal/handler/helpers_test.go deleted file mode 100644 index 5786ef0a2..000000000 --- a/crates/agent-gateway/internal/handler/helpers_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package handler - -import ( - "net/http" - "testing" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -func TestGatewayErrorStatusPassesExpectedClientErrors(t *testing.T) { - t.Parallel() - - cases := map[int32]int{ - http.StatusBadRequest: http.StatusBadRequest, - http.StatusUnauthorized: http.StatusUnauthorized, - http.StatusForbidden: http.StatusForbidden, - http.StatusNotFound: http.StatusNotFound, - http.StatusConflict: http.StatusConflict, - http.StatusTeapot: http.StatusBadGateway, - 0: http.StatusBadGateway, - } - - for code, want := range cases { - got := GatewayErrorStatus(&gatewayv2.ErrorResponse{Code: code}) - if got != want { - t.Fatalf("GatewayErrorStatus(%d) = %d, want %d", code, got, want) - } - } -} diff --git a/crates/agent-gateway/internal/handler/image_proxy.go b/crates/agent-gateway/internal/handler/image_proxy.go deleted file mode 100644 index f8982b45b..000000000 --- a/crates/agent-gateway/internal/handler/image_proxy.go +++ /dev/null @@ -1,137 +0,0 @@ -package handler - -import ( - "fmt" - "io" - "net/http" - "net/url" - "strings" - "time" - - "github.com/gabriel-vasile/mimetype" -) - -const ( - imageProxyMaxBytes = 25 * 1024 * 1024 - imageProxyAccept = "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8" - imageProxyAcceptLanguage = "en-US,en;q=0.9" - imageProxyUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" -) - -func ImageProxy(timeout time.Duration) http.HandlerFunc { - return imageProxyWithClient(newSafeOutboundHTTPClient(timeout)) -} - -func imageProxyWithClient(client outboundHTTPClient) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - rawURL := strings.TrimSpace(r.URL.Query().Get("url")) - targetURL, err := validateImageProxyURL(rawURL) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - upstreamReq, err := http.NewRequestWithContext(r.Context(), http.MethodGet, targetURL.String(), nil) - if err != nil { - http.Error(w, fmt.Sprintf("failed to create image proxy request: %v", err), http.StatusBadRequest) - return - } - applyImageProxyRequestHeaders(upstreamReq, targetURL) - - resp, err := client.Do(upstreamReq) - if err != nil { - if isSafeOutboundBlockedError(err) { - http.Error(w, "image proxy URL is not allowed", http.StatusBadRequest) - return - } - http.Error(w, fmt.Sprintf("failed to load image through proxy: %v", err), http.StatusBadGateway) - return - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { - http.Error(w, fmt.Sprintf("image proxy upstream returned HTTP status %d", resp.StatusCode), http.StatusBadGateway) - return - } - if resp.ContentLength > imageProxyMaxBytes { - http.Error(w, "image proxy response is too large", http.StatusRequestEntityTooLarge) - return - } - - body, err := io.ReadAll(io.LimitReader(resp.Body, imageProxyMaxBytes+1)) - if err != nil { - http.Error(w, fmt.Sprintf("failed to read image proxy response: %v", err), http.StatusBadGateway) - return - } - if len(body) > imageProxyMaxBytes { - http.Error(w, "image proxy response is too large", http.StatusRequestEntityTooLarge) - return - } - - mimeType, ok := resolveImageProxyMime(resp.Header.Get("Content-Type"), body) - if !ok { - http.Error(w, "image proxy upstream response is not a supported image", http.StatusBadGateway) - return - } - - w.Header().Set("Content-Type", mimeType) - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(body))) - w.Header().Set("Cache-Control", "private, max-age=300") - w.Header().Set("X-Content-Type-Options", "nosniff") - w.Header().Set("Referrer-Policy", "no-referrer") - _, _ = w.Write(body) - } -} - -func applyImageProxyRequestHeaders(req *http.Request, targetURL *url.URL) { - req.Header.Set("Accept", imageProxyAccept) - req.Header.Set("Accept-Language", imageProxyAcceptLanguage) - req.Header.Set("User-Agent", imageProxyUserAgent) - req.Header.Set("Referer", imageProxyReferer(targetURL)) -} - -func imageProxyReferer(targetURL *url.URL) string { - if targetURL == nil || targetURL.Scheme == "" || targetURL.Host == "" { - return "" - } - return (&url.URL{Scheme: targetURL.Scheme, Host: targetURL.Host, Path: "/"}).String() -} - -func validateImageProxyURL(raw string) (*url.URL, error) { - parsed, err := validateOutboundHTTPURL(raw) - if err != nil { - return nil, fmt.Errorf("image URL is not allowed: %v", err) - } - return parsed, nil -} - -func normalizeImageProxyMime(value string) (string, bool) { - mimeType := strings.ToLower(strings.TrimSpace(strings.Split(value, ";")[0])) - switch mimeType { - case "image/png": - return "image/png", true - case "image/jpeg", "image/jpg": - return "image/jpeg", true - case "image/gif": - return "image/gif", true - case "image/webp": - return "image/webp", true - case "image/bmp": - return "image/bmp", true - case "image/svg+xml": - return "image/svg+xml", true - case "image/x-icon", "image/vnd.microsoft.icon": - return "image/x-icon", true - default: - return "", false - } -} - -func resolveImageProxyMime(_ string, body []byte) (string, bool) { - if detected := mimetype.Detect(body); detected != nil { - if mimeType, ok := normalizeImageProxyMime(detected.String()); ok { - return mimeType, true - } - } - return "", false -} diff --git a/crates/agent-gateway/internal/handler/image_proxy_test.go b/crates/agent-gateway/internal/handler/image_proxy_test.go deleted file mode 100644 index ff6b6e7f7..000000000 --- a/crates/agent-gateway/internal/handler/image_proxy_test.go +++ /dev/null @@ -1,156 +0,0 @@ -package handler - -import ( - "io" - "net/http" - "net/http/httptest" - "net/url" - "strings" - "testing" - "time" -) - -func TestImageProxyServesSupportedImage(t *testing.T) { - client := outboundHTTPClientFunc(func(r *http.Request) (*http.Response, error) { - if got := r.Header.Get("Accept"); got != imageProxyAccept { - t.Fatalf("upstream Accept = %q, want %q", got, imageProxyAccept) - } - if got := r.Header.Get("Accept-Language"); got != imageProxyAcceptLanguage { - t.Fatalf("upstream Accept-Language = %q, want %q", got, imageProxyAcceptLanguage) - } - if got := r.Header.Get("User-Agent"); got != imageProxyUserAgent { - t.Fatalf("upstream User-Agent = %q, want %q", got, imageProxyUserAgent) - } - if got, want := r.Header.Get("Referer"), "https://images.example/"; got != want { - t.Fatalf("upstream Referer = %q, want %q", got, want) - } - body := []byte("\x89PNG\r\n\x1a\nliveagent-test") - return &http.Response{ - StatusCode: http.StatusOK, - Header: http.Header{"Content-Type": []string{"image/png"}}, - Body: io.NopCloser(strings.NewReader(string(body))), - ContentLength: int64(len(body)), - Request: r, - }, nil - }) - - req := httptest.NewRequest(http.MethodGet, "/image-proxy?url=https://images.example/photo.png", nil) - rec := httptest.NewRecorder() - imageProxyWithClient(client)(rec, req) - - if rec.Code != http.StatusOK { - t.Fatalf("expected status %d, got %d body=%q", http.StatusOK, rec.Code, rec.Body.String()) - } - if got := rec.Header().Get("Content-Type"); got != "image/png" { - t.Fatalf("content-type = %q, want image/png", got) - } -} - -func TestImageProxyRefererUsesTargetOrigin(t *testing.T) { - targetURL, err := url.Parse("https://example.com:8443/path/photo.png?size=large") - if err != nil { - t.Fatalf("parse target url: %v", err) - } - - if got, want := imageProxyReferer(targetURL), "https://example.com:8443/"; got != want { - t.Fatalf("referer = %q, want %q", got, want) - } -} - -func TestApplyImageProxyRequestHeaders(t *testing.T) { - targetURL, err := url.Parse("https://example.com/path/photo.png") - if err != nil { - t.Fatalf("parse target url: %v", err) - } - req := httptest.NewRequest(http.MethodGet, "/proxy", nil) - - applyImageProxyRequestHeaders(req, targetURL) - - if got := req.Header.Get("Accept"); got != imageProxyAccept { - t.Fatalf("Accept = %q, want %q", got, imageProxyAccept) - } - if got := req.Header.Get("Accept-Language"); got != imageProxyAcceptLanguage { - t.Fatalf("Accept-Language = %q, want %q", got, imageProxyAcceptLanguage) - } - if got := req.Header.Get("User-Agent"); got != imageProxyUserAgent { - t.Fatalf("User-Agent = %q, want %q", got, imageProxyUserAgent) - } - if got, want := req.Header.Get("Referer"), "https://example.com/"; got != want { - t.Fatalf("Referer = %q, want %q", got, want) - } -} - -func TestImageProxyRejectsNonImage(t *testing.T) { - client := outboundHTTPClientFunc(func(r *http.Request) (*http.Response, error) { - return &http.Response{ - StatusCode: http.StatusOK, - Header: http.Header{"Content-Type": []string{"text/html"}}, - Body: io.NopCloser(strings.NewReader("")), - ContentLength: int64(len("")), - Request: r, - }, nil - }) - - req := httptest.NewRequest(http.MethodGet, "/image-proxy?url=https://images.example/page", nil) - rec := httptest.NewRecorder() - imageProxyWithClient(client)(rec, req) - - if rec.Code != http.StatusBadGateway { - t.Fatalf("expected status %d, got %d", http.StatusBadGateway, rec.Code) - } -} - -func TestImageProxyRejectsLoopbackURL(t *testing.T) { - req := httptest.NewRequest(http.MethodGet, "/image-proxy?url=http://127.0.0.1/photo.png", nil) - rec := httptest.NewRecorder() - - ImageProxy(time.Second)(rec, req) - - if rec.Code != http.StatusBadRequest { - t.Fatalf("expected status %d, got %d body=%q", http.StatusBadRequest, rec.Code, rec.Body.String()) - } -} - -func TestImageProxyRejectsIPv4MappedLoopbackURL(t *testing.T) { - req := httptest.NewRequest(http.MethodGet, "/image-proxy?url=http://[::ffff:127.0.0.1]/photo.png", nil) - rec := httptest.NewRecorder() - - ImageProxy(time.Second)(rec, req) - - if rec.Code != http.StatusBadRequest { - t.Fatalf("expected status %d, got %d body=%q", http.StatusBadRequest, rec.Code, rec.Body.String()) - } -} - -func TestImageProxyDoesNotTrustSpoofedImageContentType(t *testing.T) { - client := outboundHTTPClientFunc(func(r *http.Request) (*http.Response, error) { - return &http.Response{ - StatusCode: http.StatusOK, - Header: http.Header{"Content-Type": []string{"image/png"}}, - Body: io.NopCloser(strings.NewReader("")), - ContentLength: int64(len("")), - Request: r, - }, nil - }) - - req := httptest.NewRequest(http.MethodGet, "/image-proxy?url=https://images.example/spoofed", nil) - rec := httptest.NewRecorder() - imageProxyWithClient(client)(rec, req) - - if rec.Code != http.StatusBadGateway { - t.Fatalf("expected status %d, got %d", http.StatusBadGateway, rec.Code) - } -} - -func TestResolveImageProxyMimeDetectsSVGFromBytes(t *testing.T) { - mimeType, ok := resolveImageProxyMime("text/plain", []byte(``)) - if !ok || mimeType != "image/svg+xml" { - t.Fatalf("resolveImageProxyMime() = %q, %v; want image/svg+xml, true", mimeType, ok) - } -} - -type outboundHTTPClientFunc func(*http.Request) (*http.Response, error) - -func (fn outboundHTTPClientFunc) Do(req *http.Request) (*http.Response, error) { - return fn(req) -} diff --git a/crates/agent-gateway/internal/handler/outbound_http.go b/crates/agent-gateway/internal/handler/outbound_http.go deleted file mode 100644 index 4dff957f6..000000000 --- a/crates/agent-gateway/internal/handler/outbound_http.go +++ /dev/null @@ -1,177 +0,0 @@ -package handler - -import ( - "errors" - "fmt" - "net/http" - "net/netip" - "net/url" - "strings" - "time" - - "github.com/doyensec/safeurl" -) - -type outboundHTTPClient interface { - Do(*http.Request) (*http.Response, error) -} - -var errUnsafeOutboundURL = errors.New("unsafe outbound URL") - -type unsafeOutboundURLError struct { - message string -} - -func (e *unsafeOutboundURLError) Error() string { - return e.message -} - -func (e *unsafeOutboundURLError) Unwrap() error { - return errUnsafeOutboundURL -} - -var outboundAllowedPorts = buildOutboundAllowedPorts() - -var outboundBlockedIPPrefixes = []netip.Prefix{ - netip.MustParsePrefix("0.0.0.0/8"), - netip.MustParsePrefix("10.0.0.0/8"), - netip.MustParsePrefix("100.64.0.0/10"), - netip.MustParsePrefix("127.0.0.0/8"), - netip.MustParsePrefix("169.254.0.0/16"), - netip.MustParsePrefix("172.16.0.0/12"), - netip.MustParsePrefix("192.0.0.0/24"), - netip.MustParsePrefix("192.0.2.0/24"), - netip.MustParsePrefix("192.88.99.0/24"), - netip.MustParsePrefix("192.168.0.0/16"), - netip.MustParsePrefix("198.18.0.0/15"), - netip.MustParsePrefix("198.51.100.0/24"), - netip.MustParsePrefix("203.0.113.0/24"), - netip.MustParsePrefix("224.0.0.0/4"), - netip.MustParsePrefix("240.0.0.0/4"), - netip.MustParsePrefix("255.255.255.255/32"), - netip.MustParsePrefix("::/128"), - netip.MustParsePrefix("::1/128"), - netip.MustParsePrefix("64:ff9b::/96"), - netip.MustParsePrefix("64:ff9b:1::/48"), - netip.MustParsePrefix("100::/64"), - netip.MustParsePrefix("2001::/23"), - netip.MustParsePrefix("2001::/32"), - netip.MustParsePrefix("2001:2::/48"), - netip.MustParsePrefix("2001:10::/28"), - netip.MustParsePrefix("2001:20::/28"), - netip.MustParsePrefix("2001:db8::/32"), - netip.MustParsePrefix("2002::/16"), - netip.MustParsePrefix("3fff::/20"), - netip.MustParsePrefix("5f00::/16"), - netip.MustParsePrefix("fc00::/7"), - netip.MustParsePrefix("fe80::/10"), - netip.MustParsePrefix("ff00::/8"), -} - -func buildOutboundAllowedPorts() []int { - ports := make([]int, 65535) - for i := range ports { - ports[i] = i + 1 - } - return ports -} - -func newSafeOutboundHTTPClient(timeout time.Duration) outboundHTTPClient { - config := safeurl.GetConfigBuilder(). - SetTimeout(timeout). - SetAllowedSchemes("http", "https"). - SetAllowedPorts(outboundAllowedPorts...). - SetCheckRedirect(validateSafeOutboundRedirect). - EnableIPv6(true). - AllowSendingCredentials(false). - Build() - return safeurl.Client(config) -} - -func validateSafeOutboundRedirect(req *http.Request, via []*http.Request) error { - if len(via) >= 10 { - return &unsafeOutboundURLError{message: "too many redirects"} - } - if req == nil || req.URL == nil { - return &unsafeOutboundURLError{message: "redirect URL is required"} - } - return validateParsedOutboundHTTPURL(req.URL) -} - -func validateOutboundHTTPURL(raw string) (*url.URL, error) { - raw = strings.TrimSpace(raw) - if raw == "" { - return nil, &unsafeOutboundURLError{message: "url is required"} - } - parsed, err := url.Parse(raw) - if err != nil { - return nil, &unsafeOutboundURLError{message: fmt.Sprintf("URL must be absolute: %v", err)} - } - if err := validateParsedOutboundHTTPURL(parsed); err != nil { - return nil, err - } - return parsed, nil -} - -func validateParsedOutboundHTTPURL(parsed *url.URL) error { - if parsed == nil { - return &unsafeOutboundURLError{message: "URL must be absolute"} - } - if parsed.Scheme != "http" && parsed.Scheme != "https" { - return &unsafeOutboundURLError{message: fmt.Sprintf("only http and https URLs are supported, got %s", parsed.Scheme)} - } - if parsed.Host == "" || parsed.Hostname() == "" { - return &unsafeOutboundURLError{message: "URL must include a valid host"} - } - if parsed.User != nil { - return &unsafeOutboundURLError{message: "URL cannot include embedded credentials"} - } - if hostIP, err := netip.ParseAddr(parsed.Hostname()); err == nil && isBlockedOutboundIP(hostIP) { - return &unsafeOutboundURLError{message: "URL host resolves to a blocked IP range"} - } - return nil -} - -func isBlockedOutboundIP(ip netip.Addr) bool { - if !ip.IsValid() { - return true - } - ip = ip.Unmap() - for _, prefix := range outboundBlockedIPPrefixes { - if prefix.Contains(ip) { - return true - } - } - return false -} - -func isSafeOutboundBlockedError(err error) bool { - if err == nil { - return false - } - if errors.Is(err, errUnsafeOutboundURL) { - return true - } - var allowedIP *safeurl.AllowedIPError - if errors.As(err, &allowedIP) { - return true - } - var allowedPort *safeurl.AllowedPortError - if errors.As(err, &allowedPort) { - return true - } - var allowedScheme *safeurl.AllowedSchemeError - if errors.As(err, &allowedScheme) { - return true - } - var allowedHost *safeurl.AllowedHostError - if errors.As(err, &allowedHost) { - return true - } - var invalidHost *safeurl.InvalidHostError - if errors.As(err, &invalidHost) { - return true - } - var credentials *safeurl.SendingCredentialsBlockedError - return errors.As(err, &credentials) -} diff --git a/crates/agent-gateway/internal/handler/status.go b/crates/agent-gateway/internal/handler/status.go deleted file mode 100644 index c24abf390..000000000 --- a/crates/agent-gateway/internal/handler/status.go +++ /dev/null @@ -1,23 +0,0 @@ -package handler - -import ( - "net/http" - - "github.com/liveagent/agent-gateway/internal/observability" - "github.com/liveagent/agent-gateway/internal/session" -) - -// statusResponse 是全局鉴权检查与 Agent 目录响应,不承担具体 Agent 寻址。 -type statusResponse struct { - Agents []session.Status `json:"agents"` - ProtocolUsage map[string]int64 `json:"protocol_usage"` -} - -func Status(sm *session.Manager) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - writeJSON(w, http.StatusOK, statusResponse{ - Agents: sm.AgentStatuses(), - ProtocolUsage: observability.Usage.Snapshot(), - }) - } -} diff --git a/crates/agent-gateway/internal/handler/types.go b/crates/agent-gateway/internal/handler/types.go deleted file mode 100644 index 38fe12eb1..000000000 --- a/crates/agent-gateway/internal/handler/types.go +++ /dev/null @@ -1,219 +0,0 @@ -package handler - -import ( - "fmt" - "strings" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -type ChatSelectedModelBody struct { - CustomProviderID string `json:"custom_provider_id"` - Model string `json:"model"` - ProviderType string `json:"provider_type"` -} - -type ChatRuntimeControlsBody struct { - ThinkingEnabled *bool `json:"thinking_enabled,omitempty"` - NativeWebSearchEnabled *bool `json:"native_web_search_enabled,omitempty"` - Reasoning string `json:"reasoning"` -} - -type ChatUploadedFileBody struct { - RelativePath string `json:"relative_path"` - AbsolutePath string `json:"absolute_path,omitempty"` - FileName string `json:"file_name"` - Kind string `json:"kind"` - SizeBytes int64 `json:"size_bytes"` -} - -type ChatRequestBody struct { - ConversationID string `json:"conversation_id"` - ClientRequestID string `json:"client_request_id,omitempty"` - Message string `json:"message"` - SelectedModel *ChatSelectedModelBody `json:"selected_model,omitempty"` - RuntimeControls *ChatRuntimeControlsBody `json:"runtime_controls,omitempty"` - ExecutionMode string `json:"execution_mode,omitempty"` - Workdir string `json:"workdir,omitempty"` - UploadedFiles []ChatUploadedFileBody `json:"uploaded_files,omitempty"` - QueuePolicy string `json:"queue_policy,omitempty"` -} - -type CancelChatRequestBody struct { - ConversationID string `json:"conversation_id"` -} - -type UploadedImagePreviewRequestBody struct { - Workdir string `json:"workdir"` - AbsolutePath string `json:"absolute_path"` -} - -type CronManageRequestBody struct { - Action string `json:"action"` - TaskID string `json:"task_id"` - TaskJSON string `json:"task_json"` -} - -type ProviderModelsRequestBody struct { - Type string `json:"type"` - BaseURL string `json:"base_url"` - APIKey string `json:"api_key"` - UseSystemProxy bool `json:"use_system_proxy"` -} - -func boolPtr(value bool) *bool { - return &value -} - -func boolValue(input *bool, fallback bool) bool { - if input == nil { - return fallback - } - return *input -} - -func NormalizeChatSelectedModel( - input *ChatSelectedModelBody, -) (*ChatSelectedModelBody, error) { - if input == nil { - return nil, nil - } - - selectedModel := &ChatSelectedModelBody{ - CustomProviderID: normalizeTrimmedText(input.CustomProviderID), - Model: normalizeTrimmedText(input.Model), - ProviderType: normalizeTrimmedText(input.ProviderType), - } - - if selectedModel.CustomProviderID == "" { - return nil, fmt.Errorf("selected_model.custom_provider_id is required") - } - if selectedModel.Model == "" { - return nil, fmt.Errorf("selected_model.model is required") - } - - switch selectedModel.ProviderType { - case "codex", "claude_code", "gemini", "xai": - return selectedModel, nil - case "": - return nil, fmt.Errorf("selected_model.provider_type is required") - default: - return nil, fmt.Errorf( - "selected_model.provider_type must be codex, claude_code, gemini, or xai", - ) - } -} - -func NormalizeChatRuntimeControls(input *ChatRuntimeControlsBody) *ChatRuntimeControlsBody { - if input == nil { - return nil - } - - return &ChatRuntimeControlsBody{ - ThinkingEnabled: boolPtr(boolValue(input.ThinkingEnabled, true)), - NativeWebSearchEnabled: boolPtr(boolValue(input.NativeWebSearchEnabled, true)), - Reasoning: normalizeChatRuntimeReasoning(input.Reasoning), - } -} - -func normalizeChatRuntimeReasoning(value string) string { - switch normalizeTrimmedText(value) { - case "minimal", "low", "medium", "high", "xhigh", "max": - return normalizeTrimmedText(value) - default: - return "high" - } -} - -func normalizeTrimmedText(value string) string { - return strings.TrimSpace(value) -} - -func NormalizeExecutionMode(value string) string { - normalized := normalizeTrimmedText(value) - switch normalized { - case "tools", "agent-dev": - return normalized - default: - return "text" - } -} - -func NormalizeWorkdir(value string) string { - return normalizeTrimmedText(value) -} - -func NormalizeChatUploadedFiles(input []ChatUploadedFileBody) []ChatUploadedFileBody { - out := make([]ChatUploadedFileBody, 0, len(input)) - seen := make(map[string]struct{}, len(input)) - - for _, item := range input { - relativePath := normalizeTrimmedText(item.RelativePath) - fileName := normalizeTrimmedText(item.FileName) - kind := normalizeTrimmedText(item.Kind) - if relativePath == "" || fileName == "" { - continue - } - switch kind { - case "text", "image", "pdf", "notebook", "word", "spreadsheet", "archive": - default: - continue - } - key := relativePath + "\n" + fileName - if _, ok := seen[key]; ok { - continue - } - seen[key] = struct{}{} - out = append(out, ChatUploadedFileBody{ - RelativePath: relativePath, - AbsolutePath: normalizeTrimmedText(item.AbsolutePath), - FileName: fileName, - Kind: kind, - SizeBytes: item.SizeBytes, - }) - } - - return out -} - -func ToProtoChatSelectedModel(input *ChatSelectedModelBody) *gatewayv2.ChatSelectedModel { - if input == nil { - return nil - } - - return &gatewayv2.ChatSelectedModel{ - CustomProviderId: input.CustomProviderID, - Model: input.Model, - ProviderType: input.ProviderType, - } -} - -func ToProtoChatRuntimeControls(input *ChatRuntimeControlsBody) *gatewayv2.ChatRuntimeControls { - if input == nil { - return nil - } - - return &gatewayv2.ChatRuntimeControls{ - ThinkingEnabled: boolValue(input.ThinkingEnabled, true), - NativeWebSearchEnabled: boolValue(input.NativeWebSearchEnabled, true), - Reasoning: normalizeChatRuntimeReasoning(input.Reasoning), - } -} - -func ToProtoChatUploadedFiles(input []ChatUploadedFileBody) []*gatewayv2.ChatUploadedFile { - if len(input) == 0 { - return nil - } - - out := make([]*gatewayv2.ChatUploadedFile, 0, len(input)) - for _, item := range input { - out = append(out, &gatewayv2.ChatUploadedFile{ - RelativePath: item.RelativePath, - AbsolutePath: item.AbsolutePath, - FileName: item.FileName, - Kind: item.Kind, - SizeBytes: item.SizeBytes, - }) - } - return out -} diff --git a/crates/agent-gateway/internal/handler/types_test.go b/crates/agent-gateway/internal/handler/types_test.go deleted file mode 100644 index 86395ffe2..000000000 --- a/crates/agent-gateway/internal/handler/types_test.go +++ /dev/null @@ -1,195 +0,0 @@ -package handler - -import ( - "reflect" - "testing" -) - -func TestNormalizeExecutionMode(t *testing.T) { - t.Parallel() - - cases := map[string]string{ - "": "text", - " text ": "text", - "tools": "tools", - "agent-dev": "agent-dev", - "unknown": "text", - } - - for input, want := range cases { - if got := NormalizeExecutionMode(input); got != want { - t.Fatalf("NormalizeExecutionMode(%q) = %q, want %q", input, got, want) - } - } -} - -func TestNormalizeChatSelectedModelAcceptsGemini(t *testing.T) { - t.Parallel() - - got, err := NormalizeChatSelectedModel(&ChatSelectedModelBody{ - CustomProviderID: " gemini-provider ", - Model: " gemini-3.5-flash ", - ProviderType: " gemini ", - }) - if err != nil { - t.Fatalf("NormalizeChatSelectedModel() error = %v", err) - } - if got.CustomProviderID != "gemini-provider" || - got.Model != "gemini-3.5-flash" || - got.ProviderType != "gemini" { - t.Fatalf("NormalizeChatSelectedModel() = %#v", got) - } -} - -func TestNormalizeChatSelectedModelAcceptsXai(t *testing.T) { - t.Parallel() - - got, err := NormalizeChatSelectedModel(&ChatSelectedModelBody{ - CustomProviderID: " builtin-xai ", - Model: " grok-4.5 ", - ProviderType: " xai ", - }) - if err != nil { - t.Fatalf("NormalizeChatSelectedModel() error = %v", err) - } - if got.CustomProviderID != "builtin-xai" || - got.Model != "grok-4.5" || - got.ProviderType != "xai" { - t.Fatalf("NormalizeChatSelectedModel() = %#v", got) - } -} - -func TestNormalizeChatSelectedModelRejectsUnknownProviderType(t *testing.T) { - t.Parallel() - - if _, err := NormalizeChatSelectedModel(&ChatSelectedModelBody{ - CustomProviderID: "provider", - Model: "model", - ProviderType: "grok", - }); err == nil { - t.Fatalf("NormalizeChatSelectedModel() expected error for unknown provider type") - } -} - -func TestNormalizeChatRuntimeControlsDefaultsAndTrims(t *testing.T) { - t.Parallel() - - got := NormalizeChatRuntimeControls(&ChatRuntimeControlsBody{ - ThinkingEnabled: boolPtr(false), - Reasoning: " xhigh ", - }) - if got == nil { - t.Fatalf("NormalizeChatRuntimeControls() = nil") - } - if *got.ThinkingEnabled != false { - t.Fatalf("thinking enabled = %v, want false", *got.ThinkingEnabled) - } - if *got.NativeWebSearchEnabled != true { - t.Fatalf("web search enabled = %v, want true default", *got.NativeWebSearchEnabled) - } - if got.Reasoning != "xhigh" { - t.Fatalf("reasoning = %q, want xhigh", got.Reasoning) - } - - max := NormalizeChatRuntimeControls(&ChatRuntimeControlsBody{ - Reasoning: " max ", - }) - if max == nil { - t.Fatalf("NormalizeChatRuntimeControls(max) = nil") - } - if max.Reasoning != "max" { - t.Fatalf("reasoning = %q, want max", max.Reasoning) - } - - invalid := NormalizeChatRuntimeControls(&ChatRuntimeControlsBody{ - NativeWebSearchEnabled: boolPtr(false), - Reasoning: "remote-xhigh", - }) - if invalid == nil { - t.Fatalf("NormalizeChatRuntimeControls(invalid) = nil") - } - if *invalid.ThinkingEnabled != true { - t.Fatalf("invalid thinking enabled = %v, want true default", *invalid.ThinkingEnabled) - } - if *invalid.NativeWebSearchEnabled != false { - t.Fatalf("invalid web search enabled = %v, want false", *invalid.NativeWebSearchEnabled) - } - if invalid.Reasoning != "high" { - t.Fatalf("invalid reasoning = %q, want high", invalid.Reasoning) - } -} - -func TestNormalizeChatUploadedFiles(t *testing.T) { - t.Parallel() - - got := NormalizeChatUploadedFiles([]ChatUploadedFileBody{ - { - RelativePath: " docs/spec.md ", - AbsolutePath: " /tmp/docs/spec.md ", - FileName: " spec.md ", - Kind: "text", - SizeBytes: 128, - }, - { - RelativePath: "docs/spec.md", - FileName: "spec.md", - Kind: "text", - SizeBytes: 128, - }, - { - RelativePath: "bad.bin", - FileName: "bad.bin", - Kind: "binary", - SizeBytes: 64, - }, - { - RelativePath: "uploads/report.docx", - FileName: "report.docx", - Kind: "word", - SizeBytes: 256, - }, - { - RelativePath: "uploads/workbook.xlsx", - FileName: "workbook.xlsx", - Kind: "spreadsheet", - SizeBytes: 512, - }, - { - RelativePath: "uploads/assets.zip", - FileName: "assets.zip", - Kind: "archive", - SizeBytes: 1024, - }, - }) - want := []ChatUploadedFileBody{ - { - RelativePath: "docs/spec.md", - AbsolutePath: "/tmp/docs/spec.md", - FileName: "spec.md", - Kind: "text", - SizeBytes: 128, - }, - { - RelativePath: "uploads/report.docx", - FileName: "report.docx", - Kind: "word", - SizeBytes: 256, - }, - { - RelativePath: "uploads/workbook.xlsx", - FileName: "workbook.xlsx", - Kind: "spreadsheet", - SizeBytes: 512, - }, - { - RelativePath: "uploads/assets.zip", - FileName: "assets.zip", - Kind: "archive", - SizeBytes: 1024, - }, - } - - if !reflect.DeepEqual(got, want) { - t.Fatalf("NormalizeChatUploadedFiles() = %#v, want %#v", got, want) - } -} diff --git a/crates/agent-gateway/internal/handler/upload.go b/crates/agent-gateway/internal/handler/upload.go deleted file mode 100644 index cb7016b2d..000000000 --- a/crates/agent-gateway/internal/handler/upload.go +++ /dev/null @@ -1,136 +0,0 @@ -package handler - -import ( - "context" - "io" - "net/http" - "strings" - "time" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" - "github.com/liveagent/agent-gateway/internal/session" -) - -const maxReadableUploadBytes int64 = 100 << 20 // 100 MiB - -func ImportReadableFiles( - sm *session.Manager, - requestTimeout time.Duration, -) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - agentID := strings.TrimSpace(r.URL.Query().Get("agent_id")) - if agentID == "" { - writeError(w, http.StatusBadRequest, "agent_id is required") - return - } - if !sm.IsOnline(agentID) { - writeError(w, http.StatusServiceUnavailable, "agent offline") - return - } - - r.Body = http.MaxBytesReader(w, r.Body, maxReadableUploadBytes) - if err := r.ParseMultipartForm(32 << 20); err != nil { - status := http.StatusBadRequest - message := "invalid multipart form" - if strings.Contains(err.Error(), "http: request body too large") { - status = http.StatusRequestEntityTooLarge - message = "uploaded files are too large" - } - writeError(w, status, message) - return - } - if r.MultipartForm != nil { - defer func() { _ = r.MultipartForm.RemoveAll() }() - } - - workdir := strings.TrimSpace(r.FormValue("workdir")) - if workdir == "" { - writeError(w, http.StatusBadRequest, "workdir is required") - return - } - - fileHeaders := r.MultipartForm.File["files"] - if len(fileHeaders) == 0 { - writeError(w, http.StatusBadRequest, "files is required") - return - } - - uploads := make([]*gatewayv2.UploadReadableFile, 0, len(fileHeaders)) - for _, header := range fileHeaders { - file, err := header.Open() - if err != nil { - writeError(w, http.StatusBadRequest, "failed to read uploaded files") - return - } - - content, readErr := io.ReadAll(file) - closeErr := file.Close() - if readErr != nil { - writeError(w, http.StatusBadRequest, "failed to read uploaded files") - return - } - if closeErr != nil { - writeError(w, http.StatusBadRequest, "failed to finalize uploaded files") - return - } - - uploads = append(uploads, &gatewayv2.UploadReadableFile{ - FileName: header.Filename, - MimeType: strings.TrimSpace(header.Header.Get("Content-Type")), - Content: content, - }) - } - - ctx, cancel := context.WithTimeout(r.Context(), requestTimeout) - defer cancel() - - requestID := newRequestID() - ch, done, cleanup, err := sm.RegisterStreamAndSendContext(ctx, agentID, requestID, &gatewayv2.GatewayEnvelope{ - RequestId: requestID, - Timestamp: time.Now().Unix(), - Payload: &gatewayv2.GatewayEnvelope_UploadReadableFiles{ - UploadReadableFiles: &gatewayv2.UploadReadableFilesRequest{ - Workdir: workdir, - Files: uploads, - }, - }, - }) - if err != nil { - writeError(w, http.StatusServiceUnavailable, "agent offline") - return - } - defer cleanup() - - env, err := waitForEnvelope(ctx, ch, done) - if err != nil { - writeError(w, http.StatusGatewayTimeout, errorMessage(err, "request failed")) - return - } - if errResp := env.GetError(); errResp != nil { - writeError(w, GatewayErrorStatus(errResp), errResp.GetMessage()) - return - } - - resp := env.GetUploadReadableFilesResp() - if resp == nil { - writeError(w, http.StatusBadGateway, "unexpected agent response") - return - } - - files := make([]map[string]any, 0, len(resp.GetFiles())) - for _, file := range resp.GetFiles() { - files = append(files, map[string]any{ - "relativePath": file.GetRelativePath(), - "absolutePath": file.GetAbsolutePath(), - "fileName": file.GetFileName(), - "kind": file.GetKind(), - "sizeBytes": file.GetSizeBytes(), - }) - } - - writeJSON(w, http.StatusOK, map[string]any{ - "files": files, - "skipped": resp.GetSkipped(), - }) - } -} diff --git a/crates/agent-gateway/internal/observability/logging.go b/crates/agent-gateway/internal/observability/logging.go deleted file mode 100644 index 53fb1eb07..000000000 --- a/crates/agent-gateway/internal/observability/logging.go +++ /dev/null @@ -1,13 +0,0 @@ -// Package observability 汇集网关的可观测性基础设施(slog 初始化与 v2 协议打点)。 -package observability - -import ( - "log/slog" - "os" -) - -// SetupLogging 安装进程级默认 slog logger:单行 key=value 输出到 stderr, -// 对容器/journald 日志采集友好,结构化字段便于检索与告警。 -func SetupLogging() { - slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, nil))) -} diff --git a/crates/agent-gateway/internal/observability/protousage.go b/crates/agent-gateway/internal/observability/protousage.go deleted file mode 100644 index 12a23a649..000000000 --- a/crates/agent-gateway/internal/observability/protousage.go +++ /dev/null @@ -1,47 +0,0 @@ -package observability - -import "sync/atomic" - -// ProtoUsage 统计 v2 协议链路使用量:进程内原子计数,经 /api/status 的 -// protocol_usage 字段暴露。 -type ProtoUsage struct { - V2BrowserConnectionsTotal atomic.Int64 - V2BrowserConnectionsActive atomic.Int64 - V2BrowserRequestsTotal atomic.Int64 - V2AgentConnectsTotal atomic.Int64 - V2AgentActive atomic.Int64 - V2AgentInboundOverflowsTotal atomic.Int64 - V2TerminalConnectsTotal atomic.Int64 - ChatIngressGapsTotal atomic.Int64 - ChatIngressCheckpointRequestsTotal atomic.Int64 - ChatIngressCheckpointsCommittedTotal atomic.Int64 - ChatIngressReplayRequestsTotal atomic.Int64 - ChatIngressTerminalsCommittedTotal atomic.Int64 - ChatIngressFragmentRejectsTotal atomic.Int64 - WebSocketWriterClosesTotal atomic.Int64 - WebSocketQueueByteOverflowsTotal atomic.Int64 -} - -// Usage 是进程级单例;各协议层直接打点。 -var Usage ProtoUsage - -// Snapshot 导出当前计数(键名即对外 JSON 字段名)。 -func (u *ProtoUsage) Snapshot() map[string]int64 { - return map[string]int64{ - "v2_browser_connections_total": u.V2BrowserConnectionsTotal.Load(), - "v2_browser_connections_active": u.V2BrowserConnectionsActive.Load(), - "v2_browser_requests_total": u.V2BrowserRequestsTotal.Load(), - "v2_agent_connects_total": u.V2AgentConnectsTotal.Load(), - "v2_agent_active": u.V2AgentActive.Load(), - "v2_agent_inbound_overflows_total": u.V2AgentInboundOverflowsTotal.Load(), - "v2_terminal_connects_total": u.V2TerminalConnectsTotal.Load(), - "chat_ingress_gaps_total": u.ChatIngressGapsTotal.Load(), - "chat_ingress_checkpoint_requests_total": u.ChatIngressCheckpointRequestsTotal.Load(), - "chat_ingress_checkpoints_committed_total": u.ChatIngressCheckpointsCommittedTotal.Load(), - "chat_ingress_replay_requests_total": u.ChatIngressReplayRequestsTotal.Load(), - "chat_ingress_terminals_committed_total": u.ChatIngressTerminalsCommittedTotal.Load(), - "chat_ingress_fragment_rejects_total": u.ChatIngressFragmentRejectsTotal.Load(), - "websocket_writer_closes_total": u.WebSocketWriterClosesTotal.Load(), - "websocket_queue_byte_overflows_total": u.WebSocketQueueByteOverflowsTotal.Load(), - } -} diff --git a/crates/agent-gateway/internal/observability/protousage_test.go b/crates/agent-gateway/internal/observability/protousage_test.go deleted file mode 100644 index 5af670423..000000000 --- a/crates/agent-gateway/internal/observability/protousage_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package observability - -import "testing" - -func TestProtoUsageSnapshotIncludesReliableIngressAndTransportCounters(t *testing.T) { - var usage ProtoUsage - usage.V2AgentInboundOverflowsTotal.Add(1) - usage.ChatIngressGapsTotal.Add(2) - usage.ChatIngressCheckpointRequestsTotal.Add(3) - usage.ChatIngressCheckpointsCommittedTotal.Add(4) - usage.ChatIngressReplayRequestsTotal.Add(5) - usage.ChatIngressTerminalsCommittedTotal.Add(6) - usage.ChatIngressFragmentRejectsTotal.Add(7) - usage.WebSocketWriterClosesTotal.Add(8) - usage.WebSocketQueueByteOverflowsTotal.Add(9) - - snapshot := usage.Snapshot() - want := map[string]int64{ - "v2_agent_inbound_overflows_total": 1, - "chat_ingress_gaps_total": 2, - "chat_ingress_checkpoint_requests_total": 3, - "chat_ingress_checkpoints_committed_total": 4, - "chat_ingress_replay_requests_total": 5, - "chat_ingress_terminals_committed_total": 6, - "chat_ingress_fragment_rejects_total": 7, - "websocket_writer_closes_total": 8, - "websocket_queue_byte_overflows_total": 9, - } - for key, expected := range want { - if got := snapshot[key]; got != expected { - t.Fatalf("Snapshot[%q] = %d, want %d", key, got, expected) - } - } -} diff --git a/crates/agent-gateway/internal/proto/v2/capabilities.go b/crates/agent-gateway/internal/proto/v2/capabilities.go deleted file mode 100644 index cf721ff74..000000000 --- a/crates/agent-gateway/internal/proto/v2/capabilities.go +++ /dev/null @@ -1,5 +0,0 @@ -package gatewayv2 - -// ChatIngressV1Capability is the capability identifier for reliable desktop -// chat mirroring into the gateway. -const ChatIngressV1Capability = "CHAT_INGRESS_V1" diff --git a/crates/agent-gateway/internal/proto/v2/chat_ingress_test.go b/crates/agent-gateway/internal/proto/v2/chat_ingress_test.go deleted file mode 100644 index 2d8596afb..000000000 --- a/crates/agent-gateway/internal/proto/v2/chat_ingress_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package gatewayv2 - -import ( - "testing" - - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/reflect/protoreflect" -) - -func TestChatIngressWireFieldNumbers(t *testing.T) { - tests := []struct { - message protoreflect.MessageDescriptor - field protoreflect.Name - want protoreflect.FieldNumber - }{ - {(&ClientHello{}).ProtoReflect().Descriptor(), "capabilities", 8}, - {(&ServerHello{}).ProtoReflect().Descriptor(), "capabilities", 7}, - {(&GatewayEnvelope{}).ProtoReflect().Descriptor(), "chat_ingress_ack", 75}, - {(&AgentEnvelope{}).ProtoReflect().Descriptor(), "chat_ingress_batch", 95}, - {(&AgentEnvelope{}).ProtoReflect().Descriptor(), "chat_ingress_resume", 96}, - {(&AgentEnvelope{}).ProtoReflect().Descriptor(), "chat_ingress_fragment", 97}, - } - - for _, test := range tests { - field := test.message.Fields().ByName(test.field) - if field == nil { - t.Fatalf("%s.%s is missing", test.message.Name(), test.field) - } - if got := field.Number(); got != test.want { - t.Fatalf("%s.%s number = %d, want %d", test.message.Name(), test.field, got, test.want) - } - } -} - -func TestChatIngressRecordRoundTrip(t *testing.T) { - want := &AgentEnvelope{ - Payload: &AgentEnvelope_ChatIngressBatch{ - ChatIngressBatch: &ChatIngressBatch{ - RunId: "run-1", - ConversationId: "conversation-1", - FirstSeq: 9, - Records: []*ChatIngressRecord{{ - Payload: &ChatIngressRecord_Terminal{ - Terminal: &ChatIngressTerminal{ - CoversThroughSeq: 8, - Revision: 3, - CompressedProjection: []byte("projection"), - UncompressedBytes: 64, - Sha256: "sha256", - ContentComplete: true, - HistoryRequired: true, - State: "completed", - }, - }, - }}, - }, - }, - } - - encoded, err := proto.Marshal(want) - if err != nil { - t.Fatalf("marshal chat ingress envelope: %v", err) - } - got := &AgentEnvelope{} - if err := proto.Unmarshal(encoded, got); err != nil { - t.Fatalf("unmarshal chat ingress envelope: %v", err) - } - if !proto.Equal(got, want) { - t.Fatalf("round trip mismatch:\n got: %v\nwant: %v", got, want) - } -} diff --git a/crates/agent-gateway/internal/proto/v2/gateway.pb.go b/crates/agent-gateway/internal/proto/v2/gateway.pb.go deleted file mode 100644 index bccca859a..000000000 --- a/crates/agent-gateway/internal/proto/v2/gateway.pb.go +++ /dev/null @@ -1,14112 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc (unknown) -// source: proto/v2/gateway.proto - -package gatewayv2 - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type TunnelFrameKind int32 - -const ( - TunnelFrameKind_TUNNEL_FRAME_KIND_UNSPECIFIED TunnelFrameKind = 0 - TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_REQUEST_START TunnelFrameKind = 1 - TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_REQUEST_BODY TunnelFrameKind = 2 - TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_REQUEST_END TunnelFrameKind = 3 - TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_RESPONSE_START TunnelFrameKind = 4 - TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_RESPONSE_BODY TunnelFrameKind = 5 - TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_RESPONSE_END TunnelFrameKind = 6 - TunnelFrameKind_TUNNEL_FRAME_KIND_WS_DIAL TunnelFrameKind = 7 - TunnelFrameKind_TUNNEL_FRAME_KIND_WS_DIAL_OK TunnelFrameKind = 8 - TunnelFrameKind_TUNNEL_FRAME_KIND_WS_DIAL_ERROR TunnelFrameKind = 9 - TunnelFrameKind_TUNNEL_FRAME_KIND_WS_FRAME TunnelFrameKind = 10 - TunnelFrameKind_TUNNEL_FRAME_KIND_WS_CLOSE TunnelFrameKind = 11 - TunnelFrameKind_TUNNEL_FRAME_KIND_ERROR TunnelFrameKind = 12 - TunnelFrameKind_TUNNEL_FRAME_KIND_CANCEL TunnelFrameKind = 13 - TunnelFrameKind_TUNNEL_FRAME_KIND_PING TunnelFrameKind = 14 - TunnelFrameKind_TUNNEL_FRAME_KIND_PONG TunnelFrameKind = 15 -) - -// Enum value maps for TunnelFrameKind. -var ( - TunnelFrameKind_name = map[int32]string{ - 0: "TUNNEL_FRAME_KIND_UNSPECIFIED", - 1: "TUNNEL_FRAME_KIND_HTTP_REQUEST_START", - 2: "TUNNEL_FRAME_KIND_HTTP_REQUEST_BODY", - 3: "TUNNEL_FRAME_KIND_HTTP_REQUEST_END", - 4: "TUNNEL_FRAME_KIND_HTTP_RESPONSE_START", - 5: "TUNNEL_FRAME_KIND_HTTP_RESPONSE_BODY", - 6: "TUNNEL_FRAME_KIND_HTTP_RESPONSE_END", - 7: "TUNNEL_FRAME_KIND_WS_DIAL", - 8: "TUNNEL_FRAME_KIND_WS_DIAL_OK", - 9: "TUNNEL_FRAME_KIND_WS_DIAL_ERROR", - 10: "TUNNEL_FRAME_KIND_WS_FRAME", - 11: "TUNNEL_FRAME_KIND_WS_CLOSE", - 12: "TUNNEL_FRAME_KIND_ERROR", - 13: "TUNNEL_FRAME_KIND_CANCEL", - 14: "TUNNEL_FRAME_KIND_PING", - 15: "TUNNEL_FRAME_KIND_PONG", - } - TunnelFrameKind_value = map[string]int32{ - "TUNNEL_FRAME_KIND_UNSPECIFIED": 0, - "TUNNEL_FRAME_KIND_HTTP_REQUEST_START": 1, - "TUNNEL_FRAME_KIND_HTTP_REQUEST_BODY": 2, - "TUNNEL_FRAME_KIND_HTTP_REQUEST_END": 3, - "TUNNEL_FRAME_KIND_HTTP_RESPONSE_START": 4, - "TUNNEL_FRAME_KIND_HTTP_RESPONSE_BODY": 5, - "TUNNEL_FRAME_KIND_HTTP_RESPONSE_END": 6, - "TUNNEL_FRAME_KIND_WS_DIAL": 7, - "TUNNEL_FRAME_KIND_WS_DIAL_OK": 8, - "TUNNEL_FRAME_KIND_WS_DIAL_ERROR": 9, - "TUNNEL_FRAME_KIND_WS_FRAME": 10, - "TUNNEL_FRAME_KIND_WS_CLOSE": 11, - "TUNNEL_FRAME_KIND_ERROR": 12, - "TUNNEL_FRAME_KIND_CANCEL": 13, - "TUNNEL_FRAME_KIND_PING": 14, - "TUNNEL_FRAME_KIND_PONG": 15, - } -) - -func (x TunnelFrameKind) Enum() *TunnelFrameKind { - p := new(TunnelFrameKind) - *p = x - return p -} - -func (x TunnelFrameKind) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (TunnelFrameKind) Descriptor() protoreflect.EnumDescriptor { - return file_proto_v2_gateway_proto_enumTypes[0].Descriptor() -} - -func (TunnelFrameKind) Type() protoreflect.EnumType { - return &file_proto_v2_gateway_proto_enumTypes[0] -} - -func (x TunnelFrameKind) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use TunnelFrameKind.Descriptor instead. -func (TunnelFrameKind) EnumDescriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{0} -} - -type TunnelWsMessageType int32 - -const ( - TunnelWsMessageType_TUNNEL_WS_MESSAGE_TYPE_UNSPECIFIED TunnelWsMessageType = 0 - TunnelWsMessageType_TUNNEL_WS_MESSAGE_TYPE_TEXT TunnelWsMessageType = 1 - TunnelWsMessageType_TUNNEL_WS_MESSAGE_TYPE_BINARY TunnelWsMessageType = 2 -) - -// Enum value maps for TunnelWsMessageType. -var ( - TunnelWsMessageType_name = map[int32]string{ - 0: "TUNNEL_WS_MESSAGE_TYPE_UNSPECIFIED", - 1: "TUNNEL_WS_MESSAGE_TYPE_TEXT", - 2: "TUNNEL_WS_MESSAGE_TYPE_BINARY", - } - TunnelWsMessageType_value = map[string]int32{ - "TUNNEL_WS_MESSAGE_TYPE_UNSPECIFIED": 0, - "TUNNEL_WS_MESSAGE_TYPE_TEXT": 1, - "TUNNEL_WS_MESSAGE_TYPE_BINARY": 2, - } -) - -func (x TunnelWsMessageType) Enum() *TunnelWsMessageType { - p := new(TunnelWsMessageType) - *p = x - return p -} - -func (x TunnelWsMessageType) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (TunnelWsMessageType) Descriptor() protoreflect.EnumDescriptor { - return file_proto_v2_gateway_proto_enumTypes[1].Descriptor() -} - -func (TunnelWsMessageType) Type() protoreflect.EnumType { - return &file_proto_v2_gateway_proto_enumTypes[1] -} - -func (x TunnelWsMessageType) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use TunnelWsMessageType.Descriptor instead. -func (TunnelWsMessageType) EnumDescriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{1} -} - -type ChatEvent_ChatEventType int32 - -const ( - ChatEvent_TOKEN ChatEvent_ChatEventType = 0 - ChatEvent_THINKING ChatEvent_ChatEventType = 1 - ChatEvent_TOOL_CALL ChatEvent_ChatEventType = 2 - ChatEvent_TOOL_RESULT ChatEvent_ChatEventType = 3 - ChatEvent_DONE ChatEvent_ChatEventType = 4 - ChatEvent_ERROR ChatEvent_ChatEventType = 5 - ChatEvent_TOOL_STATUS ChatEvent_ChatEventType = 6 - ChatEvent_HOSTED_SEARCH ChatEvent_ChatEventType = 7 - ChatEvent_USER_MESSAGE ChatEvent_ChatEventType = 8 -) - -// Enum value maps for ChatEvent_ChatEventType. -var ( - ChatEvent_ChatEventType_name = map[int32]string{ - 0: "TOKEN", - 1: "THINKING", - 2: "TOOL_CALL", - 3: "TOOL_RESULT", - 4: "DONE", - 5: "ERROR", - 6: "TOOL_STATUS", - 7: "HOSTED_SEARCH", - 8: "USER_MESSAGE", - } - ChatEvent_ChatEventType_value = map[string]int32{ - "TOKEN": 0, - "THINKING": 1, - "TOOL_CALL": 2, - "TOOL_RESULT": 3, - "DONE": 4, - "ERROR": 5, - "TOOL_STATUS": 6, - "HOSTED_SEARCH": 7, - "USER_MESSAGE": 8, - } -) - -func (x ChatEvent_ChatEventType) Enum() *ChatEvent_ChatEventType { - p := new(ChatEvent_ChatEventType) - *p = x - return p -} - -func (x ChatEvent_ChatEventType) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (ChatEvent_ChatEventType) Descriptor() protoreflect.EnumDescriptor { - return file_proto_v2_gateway_proto_enumTypes[2].Descriptor() -} - -func (ChatEvent_ChatEventType) Type() protoreflect.EnumType { - return &file_proto_v2_gateway_proto_enumTypes[2] -} - -func (x ChatEvent_ChatEventType) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use ChatEvent_ChatEventType.Descriptor instead. -func (ChatEvent_ChatEventType) EnumDescriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{56, 0} -} - -type ChatIngressAck_Action int32 - -const ( - ChatIngressAck_ACTION_UNSPECIFIED ChatIngressAck_Action = 0 - ChatIngressAck_CONTINUE ChatIngressAck_Action = 1 - ChatIngressAck_REPLAY_FROM_EXPECTED ChatIngressAck_Action = 2 - ChatIngressAck_SEND_CHECKPOINT ChatIngressAck_Action = 3 - ChatIngressAck_REJECTED ChatIngressAck_Action = 4 -) - -// Enum value maps for ChatIngressAck_Action. -var ( - ChatIngressAck_Action_name = map[int32]string{ - 0: "ACTION_UNSPECIFIED", - 1: "CONTINUE", - 2: "REPLAY_FROM_EXPECTED", - 3: "SEND_CHECKPOINT", - 4: "REJECTED", - } - ChatIngressAck_Action_value = map[string]int32{ - "ACTION_UNSPECIFIED": 0, - "CONTINUE": 1, - "REPLAY_FROM_EXPECTED": 2, - "SEND_CHECKPOINT": 3, - "REJECTED": 4, - } -) - -func (x ChatIngressAck_Action) Enum() *ChatIngressAck_Action { - p := new(ChatIngressAck_Action) - *p = x - return p -} - -func (x ChatIngressAck_Action) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (ChatIngressAck_Action) Descriptor() protoreflect.EnumDescriptor { - return file_proto_v2_gateway_proto_enumTypes[3].Descriptor() -} - -func (ChatIngressAck_Action) Type() protoreflect.EnumType { - return &file_proto_v2_gateway_proto_enumTypes[3] -} - -func (x ChatIngressAck_Action) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use ChatIngressAck_Action.Descriptor instead. -func (ChatIngressAck_Action) EnumDescriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{150, 0} -} - -type GatewayEnvelope struct { - state protoimpl.MessageState `protogen:"open.v1"` - RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` - Timestamp int64 `protobuf:"varint,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"` - // Types that are valid to be assigned to Payload: - // - // *GatewayEnvelope_ChatCommand - // *GatewayEnvelope_CronManage - // *GatewayEnvelope_HistoryList - // *GatewayEnvelope_HistoryGet - // *GatewayEnvelope_HistoryRename - // *GatewayEnvelope_HistoryDelete - // *GatewayEnvelope_HistoryPrefix - // *GatewayEnvelope_HistoryPin - // *GatewayEnvelope_HistoryShareGet - // *GatewayEnvelope_HistoryShareSet - // *GatewayEnvelope_HistoryShareResolve - // *GatewayEnvelope_HistoryWorkdirs - // *GatewayEnvelope_ProviderList - // *GatewayEnvelope_SettingsGet - // *GatewayEnvelope_SettingsUpdate - // *GatewayEnvelope_SkillFilesList - // *GatewayEnvelope_SkillMetadataRead - // *GatewayEnvelope_SkillTextRead - // *GatewayEnvelope_FileMentionList - // *GatewayEnvelope_UploadReadableFiles - // *GatewayEnvelope_FsRoots - // *GatewayEnvelope_FsListDirs - // *GatewayEnvelope_Ping - // *GatewayEnvelope_UploadedImagePreview - // *GatewayEnvelope_MemoryManage - // *GatewayEnvelope_SkillManage - // *GatewayEnvelope_FsCreateProjectFolder - // *GatewayEnvelope_TerminalRequest - // *GatewayEnvelope_FsList - // *GatewayEnvelope_FsWriteText - // *GatewayEnvelope_FsCreateDir - // *GatewayEnvelope_FsRename - // *GatewayEnvelope_FsDelete - // *GatewayEnvelope_GitRequest - // *GatewayEnvelope_FsReadEditableText - // *GatewayEnvelope_FsReadWorkspaceImage - // *GatewayEnvelope_SftpRequest - // *GatewayEnvelope_ProviderModels - // *GatewayEnvelope_SettingsResetSshKnownHost - // *GatewayEnvelope_ChatQueue - // *GatewayEnvelope_ChatIngressAck - // *GatewayEnvelope_TunnelState - // *GatewayEnvelope_TunnelMutation - // *GatewayEnvelope_TunnelFrame - // *GatewayEnvelope_WorkspaceWatch - // *GatewayEnvelope_ManagedProcessRequest - // *GatewayEnvelope_HistoryBranch - // *GatewayEnvelope_ProviderUsage - // *GatewayEnvelope_ChatFileOpen - Payload isGatewayEnvelope_Payload `protobuf_oneof:"payload"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GatewayEnvelope) Reset() { - *x = GatewayEnvelope{} - mi := &file_proto_v2_gateway_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GatewayEnvelope) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GatewayEnvelope) ProtoMessage() {} - -func (x *GatewayEnvelope) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[0] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GatewayEnvelope.ProtoReflect.Descriptor instead. -func (*GatewayEnvelope) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{0} -} - -func (x *GatewayEnvelope) GetRequestId() string { - if x != nil { - return x.RequestId - } - return "" -} - -func (x *GatewayEnvelope) GetTimestamp() int64 { - if x != nil { - return x.Timestamp - } - return 0 -} - -func (x *GatewayEnvelope) GetPayload() isGatewayEnvelope_Payload { - if x != nil { - return x.Payload - } - return nil -} - -func (x *GatewayEnvelope) GetChatCommand() *ChatCommandRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_ChatCommand); ok { - return x.ChatCommand - } - } - return nil -} - -func (x *GatewayEnvelope) GetCronManage() *CronManageRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_CronManage); ok { - return x.CronManage - } - } - return nil -} - -func (x *GatewayEnvelope) GetHistoryList() *HistoryListRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_HistoryList); ok { - return x.HistoryList - } - } - return nil -} - -func (x *GatewayEnvelope) GetHistoryGet() *HistoryGetRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_HistoryGet); ok { - return x.HistoryGet - } - } - return nil -} - -func (x *GatewayEnvelope) GetHistoryRename() *HistoryRenameRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_HistoryRename); ok { - return x.HistoryRename - } - } - return nil -} - -func (x *GatewayEnvelope) GetHistoryDelete() *HistoryDeleteRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_HistoryDelete); ok { - return x.HistoryDelete - } - } - return nil -} - -func (x *GatewayEnvelope) GetHistoryPrefix() *HistoryPrefixRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_HistoryPrefix); ok { - return x.HistoryPrefix - } - } - return nil -} - -func (x *GatewayEnvelope) GetHistoryPin() *HistoryPinRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_HistoryPin); ok { - return x.HistoryPin - } - } - return nil -} - -func (x *GatewayEnvelope) GetHistoryShareGet() *HistoryShareGetRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_HistoryShareGet); ok { - return x.HistoryShareGet - } - } - return nil -} - -func (x *GatewayEnvelope) GetHistoryShareSet() *HistoryShareSetRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_HistoryShareSet); ok { - return x.HistoryShareSet - } - } - return nil -} - -func (x *GatewayEnvelope) GetHistoryShareResolve() *HistoryShareResolveRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_HistoryShareResolve); ok { - return x.HistoryShareResolve - } - } - return nil -} - -func (x *GatewayEnvelope) GetHistoryWorkdirs() *HistoryWorkdirsRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_HistoryWorkdirs); ok { - return x.HistoryWorkdirs - } - } - return nil -} - -func (x *GatewayEnvelope) GetProviderList() *ProviderListRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_ProviderList); ok { - return x.ProviderList - } - } - return nil -} - -func (x *GatewayEnvelope) GetSettingsGet() *SettingsGetRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_SettingsGet); ok { - return x.SettingsGet - } - } - return nil -} - -func (x *GatewayEnvelope) GetSettingsUpdate() *SettingsUpdateRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_SettingsUpdate); ok { - return x.SettingsUpdate - } - } - return nil -} - -func (x *GatewayEnvelope) GetSkillFilesList() *SkillFilesListRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_SkillFilesList); ok { - return x.SkillFilesList - } - } - return nil -} - -func (x *GatewayEnvelope) GetSkillMetadataRead() *SkillMetadataReadRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_SkillMetadataRead); ok { - return x.SkillMetadataRead - } - } - return nil -} - -func (x *GatewayEnvelope) GetSkillTextRead() *SkillTextReadRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_SkillTextRead); ok { - return x.SkillTextRead - } - } - return nil -} - -func (x *GatewayEnvelope) GetFileMentionList() *FileMentionListRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_FileMentionList); ok { - return x.FileMentionList - } - } - return nil -} - -func (x *GatewayEnvelope) GetUploadReadableFiles() *UploadReadableFilesRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_UploadReadableFiles); ok { - return x.UploadReadableFiles - } - } - return nil -} - -func (x *GatewayEnvelope) GetFsRoots() *FsRootsRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_FsRoots); ok { - return x.FsRoots - } - } - return nil -} - -func (x *GatewayEnvelope) GetFsListDirs() *FsListDirsRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_FsListDirs); ok { - return x.FsListDirs - } - } - return nil -} - -func (x *GatewayEnvelope) GetPing() *PingRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_Ping); ok { - return x.Ping - } - } - return nil -} - -func (x *GatewayEnvelope) GetUploadedImagePreview() *UploadedImagePreviewRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_UploadedImagePreview); ok { - return x.UploadedImagePreview - } - } - return nil -} - -func (x *GatewayEnvelope) GetMemoryManage() *MemoryManageRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_MemoryManage); ok { - return x.MemoryManage - } - } - return nil -} - -func (x *GatewayEnvelope) GetSkillManage() *SkillManageRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_SkillManage); ok { - return x.SkillManage - } - } - return nil -} - -func (x *GatewayEnvelope) GetFsCreateProjectFolder() *FsCreateProjectFolderRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_FsCreateProjectFolder); ok { - return x.FsCreateProjectFolder - } - } - return nil -} - -func (x *GatewayEnvelope) GetTerminalRequest() *TerminalRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_TerminalRequest); ok { - return x.TerminalRequest - } - } - return nil -} - -func (x *GatewayEnvelope) GetFsList() *FsListRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_FsList); ok { - return x.FsList - } - } - return nil -} - -func (x *GatewayEnvelope) GetFsWriteText() *FsWriteTextRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_FsWriteText); ok { - return x.FsWriteText - } - } - return nil -} - -func (x *GatewayEnvelope) GetFsCreateDir() *FsCreateDirRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_FsCreateDir); ok { - return x.FsCreateDir - } - } - return nil -} - -func (x *GatewayEnvelope) GetFsRename() *FsRenameRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_FsRename); ok { - return x.FsRename - } - } - return nil -} - -func (x *GatewayEnvelope) GetFsDelete() *FsDeleteRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_FsDelete); ok { - return x.FsDelete - } - } - return nil -} - -func (x *GatewayEnvelope) GetGitRequest() *GitRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_GitRequest); ok { - return x.GitRequest - } - } - return nil -} - -func (x *GatewayEnvelope) GetFsReadEditableText() *FsReadEditableTextRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_FsReadEditableText); ok { - return x.FsReadEditableText - } - } - return nil -} - -func (x *GatewayEnvelope) GetFsReadWorkspaceImage() *FsReadWorkspaceImageRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_FsReadWorkspaceImage); ok { - return x.FsReadWorkspaceImage - } - } - return nil -} - -func (x *GatewayEnvelope) GetSftpRequest() *SftpRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_SftpRequest); ok { - return x.SftpRequest - } - } - return nil -} - -func (x *GatewayEnvelope) GetProviderModels() *ProviderModelsRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_ProviderModels); ok { - return x.ProviderModels - } - } - return nil -} - -func (x *GatewayEnvelope) GetSettingsResetSshKnownHost() *SettingsResetSshKnownHostRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_SettingsResetSshKnownHost); ok { - return x.SettingsResetSshKnownHost - } - } - return nil -} - -func (x *GatewayEnvelope) GetChatQueue() *ChatQueueRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_ChatQueue); ok { - return x.ChatQueue - } - } - return nil -} - -func (x *GatewayEnvelope) GetChatIngressAck() *ChatIngressAck { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_ChatIngressAck); ok { - return x.ChatIngressAck - } - } - return nil -} - -func (x *GatewayEnvelope) GetTunnelState() *TunnelStateSnapshot { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_TunnelState); ok { - return x.TunnelState - } - } - return nil -} - -func (x *GatewayEnvelope) GetTunnelMutation() *TunnelMutation { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_TunnelMutation); ok { - return x.TunnelMutation - } - } - return nil -} - -func (x *GatewayEnvelope) GetTunnelFrame() *TunnelFrame { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_TunnelFrame); ok { - return x.TunnelFrame - } - } - return nil -} - -func (x *GatewayEnvelope) GetWorkspaceWatch() *WorkspaceWatchRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_WorkspaceWatch); ok { - return x.WorkspaceWatch - } - } - return nil -} - -func (x *GatewayEnvelope) GetManagedProcessRequest() *ManagedProcessRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_ManagedProcessRequest); ok { - return x.ManagedProcessRequest - } - } - return nil -} - -func (x *GatewayEnvelope) GetHistoryBranch() *HistoryBranchRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_HistoryBranch); ok { - return x.HistoryBranch - } - } - return nil -} - -func (x *GatewayEnvelope) GetProviderUsage() *ProviderUsageRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_ProviderUsage); ok { - return x.ProviderUsage - } - } - return nil -} - -func (x *GatewayEnvelope) GetChatFileOpen() *ChatFileOpenRequest { - if x != nil { - if x, ok := x.Payload.(*GatewayEnvelope_ChatFileOpen); ok { - return x.ChatFileOpen - } - } - return nil -} - -type isGatewayEnvelope_Payload interface { - isGatewayEnvelope_Payload() -} - -type GatewayEnvelope_ChatCommand struct { - ChatCommand *ChatCommandRequest `protobuf:"bytes,10,opt,name=chat_command,json=chatCommand,proto3,oneof"` -} - -type GatewayEnvelope_CronManage struct { - CronManage *CronManageRequest `protobuf:"bytes,20,opt,name=cron_manage,json=cronManage,proto3,oneof"` -} - -type GatewayEnvelope_HistoryList struct { - HistoryList *HistoryListRequest `protobuf:"bytes,30,opt,name=history_list,json=historyList,proto3,oneof"` -} - -type GatewayEnvelope_HistoryGet struct { - HistoryGet *HistoryGetRequest `protobuf:"bytes,31,opt,name=history_get,json=historyGet,proto3,oneof"` -} - -type GatewayEnvelope_HistoryRename struct { - HistoryRename *HistoryRenameRequest `protobuf:"bytes,32,opt,name=history_rename,json=historyRename,proto3,oneof"` -} - -type GatewayEnvelope_HistoryDelete struct { - HistoryDelete *HistoryDeleteRequest `protobuf:"bytes,33,opt,name=history_delete,json=historyDelete,proto3,oneof"` -} - -type GatewayEnvelope_HistoryPrefix struct { - HistoryPrefix *HistoryPrefixRequest `protobuf:"bytes,34,opt,name=history_prefix,json=historyPrefix,proto3,oneof"` -} - -type GatewayEnvelope_HistoryPin struct { - HistoryPin *HistoryPinRequest `protobuf:"bytes,35,opt,name=history_pin,json=historyPin,proto3,oneof"` -} - -type GatewayEnvelope_HistoryShareGet struct { - HistoryShareGet *HistoryShareGetRequest `protobuf:"bytes,36,opt,name=history_share_get,json=historyShareGet,proto3,oneof"` -} - -type GatewayEnvelope_HistoryShareSet struct { - HistoryShareSet *HistoryShareSetRequest `protobuf:"bytes,37,opt,name=history_share_set,json=historyShareSet,proto3,oneof"` -} - -type GatewayEnvelope_HistoryShareResolve struct { - HistoryShareResolve *HistoryShareResolveRequest `protobuf:"bytes,38,opt,name=history_share_resolve,json=historyShareResolve,proto3,oneof"` -} - -type GatewayEnvelope_HistoryWorkdirs struct { - HistoryWorkdirs *HistoryWorkdirsRequest `protobuf:"bytes,39,opt,name=history_workdirs,json=historyWorkdirs,proto3,oneof"` -} - -type GatewayEnvelope_ProviderList struct { - ProviderList *ProviderListRequest `protobuf:"bytes,40,opt,name=provider_list,json=providerList,proto3,oneof"` -} - -type GatewayEnvelope_SettingsGet struct { - SettingsGet *SettingsGetRequest `protobuf:"bytes,41,opt,name=settings_get,json=settingsGet,proto3,oneof"` -} - -type GatewayEnvelope_SettingsUpdate struct { - SettingsUpdate *SettingsUpdateRequest `protobuf:"bytes,42,opt,name=settings_update,json=settingsUpdate,proto3,oneof"` -} - -type GatewayEnvelope_SkillFilesList struct { - SkillFilesList *SkillFilesListRequest `protobuf:"bytes,43,opt,name=skill_files_list,json=skillFilesList,proto3,oneof"` -} - -type GatewayEnvelope_SkillMetadataRead struct { - SkillMetadataRead *SkillMetadataReadRequest `protobuf:"bytes,44,opt,name=skill_metadata_read,json=skillMetadataRead,proto3,oneof"` -} - -type GatewayEnvelope_SkillTextRead struct { - SkillTextRead *SkillTextReadRequest `protobuf:"bytes,45,opt,name=skill_text_read,json=skillTextRead,proto3,oneof"` -} - -type GatewayEnvelope_FileMentionList struct { - FileMentionList *FileMentionListRequest `protobuf:"bytes,46,opt,name=file_mention_list,json=fileMentionList,proto3,oneof"` -} - -type GatewayEnvelope_UploadReadableFiles struct { - UploadReadableFiles *UploadReadableFilesRequest `protobuf:"bytes,47,opt,name=upload_readable_files,json=uploadReadableFiles,proto3,oneof"` -} - -type GatewayEnvelope_FsRoots struct { - FsRoots *FsRootsRequest `protobuf:"bytes,48,opt,name=fs_roots,json=fsRoots,proto3,oneof"` -} - -type GatewayEnvelope_FsListDirs struct { - FsListDirs *FsListDirsRequest `protobuf:"bytes,49,opt,name=fs_list_dirs,json=fsListDirs,proto3,oneof"` -} - -type GatewayEnvelope_Ping struct { - Ping *PingRequest `protobuf:"bytes,50,opt,name=ping,proto3,oneof"` -} - -type GatewayEnvelope_UploadedImagePreview struct { - UploadedImagePreview *UploadedImagePreviewRequest `protobuf:"bytes,51,opt,name=uploaded_image_preview,json=uploadedImagePreview,proto3,oneof"` -} - -type GatewayEnvelope_MemoryManage struct { - MemoryManage *MemoryManageRequest `protobuf:"bytes,52,opt,name=memory_manage,json=memoryManage,proto3,oneof"` -} - -type GatewayEnvelope_SkillManage struct { - SkillManage *SkillManageRequest `protobuf:"bytes,53,opt,name=skill_manage,json=skillManage,proto3,oneof"` -} - -type GatewayEnvelope_FsCreateProjectFolder struct { - FsCreateProjectFolder *FsCreateProjectFolderRequest `protobuf:"bytes,54,opt,name=fs_create_project_folder,json=fsCreateProjectFolder,proto3,oneof"` -} - -type GatewayEnvelope_TerminalRequest struct { - TerminalRequest *TerminalRequest `protobuf:"bytes,55,opt,name=terminal_request,json=terminalRequest,proto3,oneof"` -} - -type GatewayEnvelope_FsList struct { - FsList *FsListRequest `protobuf:"bytes,56,opt,name=fs_list,json=fsList,proto3,oneof"` -} - -type GatewayEnvelope_FsWriteText struct { - FsWriteText *FsWriteTextRequest `protobuf:"bytes,57,opt,name=fs_write_text,json=fsWriteText,proto3,oneof"` -} - -type GatewayEnvelope_FsCreateDir struct { - FsCreateDir *FsCreateDirRequest `protobuf:"bytes,58,opt,name=fs_create_dir,json=fsCreateDir,proto3,oneof"` -} - -type GatewayEnvelope_FsRename struct { - FsRename *FsRenameRequest `protobuf:"bytes,59,opt,name=fs_rename,json=fsRename,proto3,oneof"` -} - -type GatewayEnvelope_FsDelete struct { - FsDelete *FsDeleteRequest `protobuf:"bytes,60,opt,name=fs_delete,json=fsDelete,proto3,oneof"` -} - -type GatewayEnvelope_GitRequest struct { - GitRequest *GitRequest `protobuf:"bytes,61,opt,name=git_request,json=gitRequest,proto3,oneof"` -} - -type GatewayEnvelope_FsReadEditableText struct { - FsReadEditableText *FsReadEditableTextRequest `protobuf:"bytes,62,opt,name=fs_read_editable_text,json=fsReadEditableText,proto3,oneof"` -} - -type GatewayEnvelope_FsReadWorkspaceImage struct { - FsReadWorkspaceImage *FsReadWorkspaceImageRequest `protobuf:"bytes,63,opt,name=fs_read_workspace_image,json=fsReadWorkspaceImage,proto3,oneof"` -} - -type GatewayEnvelope_SftpRequest struct { - SftpRequest *SftpRequest `protobuf:"bytes,64,opt,name=sftp_request,json=sftpRequest,proto3,oneof"` -} - -type GatewayEnvelope_ProviderModels struct { - ProviderModels *ProviderModelsRequest `protobuf:"bytes,65,opt,name=provider_models,json=providerModels,proto3,oneof"` -} - -type GatewayEnvelope_SettingsResetSshKnownHost struct { - SettingsResetSshKnownHost *SettingsResetSshKnownHostRequest `protobuf:"bytes,72,opt,name=settings_reset_ssh_known_host,json=settingsResetSshKnownHost,proto3,oneof"` -} - -type GatewayEnvelope_ChatQueue struct { - ChatQueue *ChatQueueRequest `protobuf:"bytes,73,opt,name=chat_queue,json=chatQueue,proto3,oneof"` -} - -type GatewayEnvelope_ChatIngressAck struct { - ChatIngressAck *ChatIngressAck `protobuf:"bytes,75,opt,name=chat_ingress_ack,json=chatIngressAck,proto3,oneof"` -} - -type GatewayEnvelope_TunnelState struct { - TunnelState *TunnelStateSnapshot `protobuf:"bytes,80,opt,name=tunnel_state,json=tunnelState,proto3,oneof"` -} - -type GatewayEnvelope_TunnelMutation struct { - TunnelMutation *TunnelMutation `protobuf:"bytes,81,opt,name=tunnel_mutation,json=tunnelMutation,proto3,oneof"` -} - -type GatewayEnvelope_TunnelFrame struct { - TunnelFrame *TunnelFrame `protobuf:"bytes,82,opt,name=tunnel_frame,json=tunnelFrame,proto3,oneof"` -} - -type GatewayEnvelope_WorkspaceWatch struct { - WorkspaceWatch *WorkspaceWatchRequest `protobuf:"bytes,90,opt,name=workspace_watch,json=workspaceWatch,proto3,oneof"` -} - -type GatewayEnvelope_ManagedProcessRequest struct { - ManagedProcessRequest *ManagedProcessRequest `protobuf:"bytes,91,opt,name=managed_process_request,json=managedProcessRequest,proto3,oneof"` -} - -type GatewayEnvelope_HistoryBranch struct { - HistoryBranch *HistoryBranchRequest `protobuf:"bytes,92,opt,name=history_branch,json=historyBranch,proto3,oneof"` -} - -type GatewayEnvelope_ProviderUsage struct { - ProviderUsage *ProviderUsageRequest `protobuf:"bytes,93,opt,name=provider_usage,json=providerUsage,proto3,oneof"` -} - -type GatewayEnvelope_ChatFileOpen struct { - ChatFileOpen *ChatFileOpenRequest `protobuf:"bytes,94,opt,name=chat_file_open,json=chatFileOpen,proto3,oneof"` -} - -func (*GatewayEnvelope_ChatCommand) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_CronManage) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_HistoryList) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_HistoryGet) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_HistoryRename) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_HistoryDelete) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_HistoryPrefix) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_HistoryPin) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_HistoryShareGet) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_HistoryShareSet) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_HistoryShareResolve) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_HistoryWorkdirs) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_ProviderList) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_SettingsGet) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_SettingsUpdate) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_SkillFilesList) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_SkillMetadataRead) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_SkillTextRead) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_FileMentionList) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_UploadReadableFiles) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_FsRoots) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_FsListDirs) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_Ping) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_UploadedImagePreview) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_MemoryManage) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_SkillManage) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_FsCreateProjectFolder) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_TerminalRequest) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_FsList) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_FsWriteText) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_FsCreateDir) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_FsRename) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_FsDelete) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_GitRequest) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_FsReadEditableText) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_FsReadWorkspaceImage) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_SftpRequest) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_ProviderModels) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_SettingsResetSshKnownHost) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_ChatQueue) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_ChatIngressAck) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_TunnelState) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_TunnelMutation) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_TunnelFrame) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_WorkspaceWatch) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_ManagedProcessRequest) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_HistoryBranch) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_ProviderUsage) isGatewayEnvelope_Payload() {} - -func (*GatewayEnvelope_ChatFileOpen) isGatewayEnvelope_Payload() {} - -type AgentEnvelope struct { - state protoimpl.MessageState `protogen:"open.v1"` - RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` - Timestamp int64 `protobuf:"varint,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"` - // Types that are valid to be assigned to Payload: - // - // *AgentEnvelope_ChatEvent - // *AgentEnvelope_CronManageResp - // *AgentEnvelope_HistoryListResp - // *AgentEnvelope_HistoryGetResp - // *AgentEnvelope_HistoryRenameResp - // *AgentEnvelope_HistoryDeleteResp - // *AgentEnvelope_HistorySync - // *AgentEnvelope_HistoryPrefixResp - // *AgentEnvelope_HistoryPinResp - // *AgentEnvelope_HistoryShareGetResp - // *AgentEnvelope_HistoryShareSetResp - // *AgentEnvelope_HistoryShareResolveResp - // *AgentEnvelope_HistoryWorkdirsResp - // *AgentEnvelope_ProviderListResp - // *AgentEnvelope_SettingsGetResp - // *AgentEnvelope_SettingsUpdateResp - // *AgentEnvelope_SettingsSync - // *AgentEnvelope_SkillFilesListResp - // *AgentEnvelope_SkillMetadataReadResp - // *AgentEnvelope_SkillTextReadResp - // *AgentEnvelope_FileMentionListResp - // *AgentEnvelope_UploadReadableFilesResp - // *AgentEnvelope_FsRootsResp - // *AgentEnvelope_Pong - // *AgentEnvelope_FsListDirsResp - // *AgentEnvelope_UploadedImagePreviewResp - // *AgentEnvelope_MemoryManageResp - // *AgentEnvelope_SkillManageResp - // *AgentEnvelope_FsCreateProjectFolderResp - // *AgentEnvelope_TerminalResponse - // *AgentEnvelope_TerminalEvent - // *AgentEnvelope_FsListResp - // *AgentEnvelope_FsWriteTextResp - // *AgentEnvelope_FsCreateDirResp - // *AgentEnvelope_FsRenameResp - // *AgentEnvelope_FsDeleteResp - // *AgentEnvelope_GitResponse - // *AgentEnvelope_FsReadEditableTextResp - // *AgentEnvelope_FsReadWorkspaceImageResp - // *AgentEnvelope_SftpResponse - // *AgentEnvelope_SftpEvent - // *AgentEnvelope_ChatQueueResp - // *AgentEnvelope_ChatQueueEvent - // *AgentEnvelope_ChatControl - // *AgentEnvelope_RuntimeStatus - // *AgentEnvelope_SettingsResetSshKnownHostResp - // *AgentEnvelope_ChatRuntimeSnapshot - // *AgentEnvelope_ProviderModelsResp - // *AgentEnvelope_TunnelDesired - // *AgentEnvelope_TunnelMutationResult - // *AgentEnvelope_TunnelFrame - // *AgentEnvelope_TunnelProbeReport - // *AgentEnvelope_WorkspaceActivity - // *AgentEnvelope_ManagedProcessResponse - // *AgentEnvelope_ManagedProcessSnapshot - // *AgentEnvelope_HistoryBranchResp - // *AgentEnvelope_ProviderUsageResp - // *AgentEnvelope_ChatIngressBatch - // *AgentEnvelope_ChatIngressResume - // *AgentEnvelope_ChatIngressFragment - // *AgentEnvelope_ChatFileOpenResp - // *AgentEnvelope_Error - Payload isAgentEnvelope_Payload `protobuf_oneof:"payload"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *AgentEnvelope) Reset() { - *x = AgentEnvelope{} - mi := &file_proto_v2_gateway_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AgentEnvelope) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AgentEnvelope) ProtoMessage() {} - -func (x *AgentEnvelope) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[1] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use AgentEnvelope.ProtoReflect.Descriptor instead. -func (*AgentEnvelope) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{1} -} - -func (x *AgentEnvelope) GetRequestId() string { - if x != nil { - return x.RequestId - } - return "" -} - -func (x *AgentEnvelope) GetTimestamp() int64 { - if x != nil { - return x.Timestamp - } - return 0 -} - -func (x *AgentEnvelope) GetPayload() isAgentEnvelope_Payload { - if x != nil { - return x.Payload - } - return nil -} - -func (x *AgentEnvelope) GetChatEvent() *ChatEvent { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_ChatEvent); ok { - return x.ChatEvent - } - } - return nil -} - -func (x *AgentEnvelope) GetCronManageResp() *CronManageResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_CronManageResp); ok { - return x.CronManageResp - } - } - return nil -} - -func (x *AgentEnvelope) GetHistoryListResp() *HistoryListResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_HistoryListResp); ok { - return x.HistoryListResp - } - } - return nil -} - -func (x *AgentEnvelope) GetHistoryGetResp() *HistoryGetResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_HistoryGetResp); ok { - return x.HistoryGetResp - } - } - return nil -} - -func (x *AgentEnvelope) GetHistoryRenameResp() *HistoryRenameResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_HistoryRenameResp); ok { - return x.HistoryRenameResp - } - } - return nil -} - -func (x *AgentEnvelope) GetHistoryDeleteResp() *HistoryDeleteResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_HistoryDeleteResp); ok { - return x.HistoryDeleteResp - } - } - return nil -} - -func (x *AgentEnvelope) GetHistorySync() *HistorySyncEvent { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_HistorySync); ok { - return x.HistorySync - } - } - return nil -} - -func (x *AgentEnvelope) GetHistoryPrefixResp() *HistoryPrefixResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_HistoryPrefixResp); ok { - return x.HistoryPrefixResp - } - } - return nil -} - -func (x *AgentEnvelope) GetHistoryPinResp() *HistoryPinResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_HistoryPinResp); ok { - return x.HistoryPinResp - } - } - return nil -} - -func (x *AgentEnvelope) GetHistoryShareGetResp() *HistoryShareGetResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_HistoryShareGetResp); ok { - return x.HistoryShareGetResp - } - } - return nil -} - -func (x *AgentEnvelope) GetHistoryShareSetResp() *HistoryShareSetResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_HistoryShareSetResp); ok { - return x.HistoryShareSetResp - } - } - return nil -} - -func (x *AgentEnvelope) GetHistoryShareResolveResp() *HistoryShareResolveResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_HistoryShareResolveResp); ok { - return x.HistoryShareResolveResp - } - } - return nil -} - -func (x *AgentEnvelope) GetHistoryWorkdirsResp() *HistoryWorkdirsResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_HistoryWorkdirsResp); ok { - return x.HistoryWorkdirsResp - } - } - return nil -} - -func (x *AgentEnvelope) GetProviderListResp() *ProviderListResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_ProviderListResp); ok { - return x.ProviderListResp - } - } - return nil -} - -func (x *AgentEnvelope) GetSettingsGetResp() *SettingsGetResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_SettingsGetResp); ok { - return x.SettingsGetResp - } - } - return nil -} - -func (x *AgentEnvelope) GetSettingsUpdateResp() *SettingsUpdateResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_SettingsUpdateResp); ok { - return x.SettingsUpdateResp - } - } - return nil -} - -func (x *AgentEnvelope) GetSettingsSync() *SettingsSyncEvent { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_SettingsSync); ok { - return x.SettingsSync - } - } - return nil -} - -func (x *AgentEnvelope) GetSkillFilesListResp() *SkillFilesListResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_SkillFilesListResp); ok { - return x.SkillFilesListResp - } - } - return nil -} - -func (x *AgentEnvelope) GetSkillMetadataReadResp() *SkillMetadataReadResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_SkillMetadataReadResp); ok { - return x.SkillMetadataReadResp - } - } - return nil -} - -func (x *AgentEnvelope) GetSkillTextReadResp() *SkillTextReadResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_SkillTextReadResp); ok { - return x.SkillTextReadResp - } - } - return nil -} - -func (x *AgentEnvelope) GetFileMentionListResp() *FileMentionListResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_FileMentionListResp); ok { - return x.FileMentionListResp - } - } - return nil -} - -func (x *AgentEnvelope) GetUploadReadableFilesResp() *UploadReadableFilesResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_UploadReadableFilesResp); ok { - return x.UploadReadableFilesResp - } - } - return nil -} - -func (x *AgentEnvelope) GetFsRootsResp() *FsRootsResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_FsRootsResp); ok { - return x.FsRootsResp - } - } - return nil -} - -func (x *AgentEnvelope) GetPong() *PongResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_Pong); ok { - return x.Pong - } - } - return nil -} - -func (x *AgentEnvelope) GetFsListDirsResp() *FsListDirsResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_FsListDirsResp); ok { - return x.FsListDirsResp - } - } - return nil -} - -func (x *AgentEnvelope) GetUploadedImagePreviewResp() *UploadedImagePreviewResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_UploadedImagePreviewResp); ok { - return x.UploadedImagePreviewResp - } - } - return nil -} - -func (x *AgentEnvelope) GetMemoryManageResp() *MemoryManageResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_MemoryManageResp); ok { - return x.MemoryManageResp - } - } - return nil -} - -func (x *AgentEnvelope) GetSkillManageResp() *SkillManageResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_SkillManageResp); ok { - return x.SkillManageResp - } - } - return nil -} - -func (x *AgentEnvelope) GetFsCreateProjectFolderResp() *FsCreateProjectFolderResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_FsCreateProjectFolderResp); ok { - return x.FsCreateProjectFolderResp - } - } - return nil -} - -func (x *AgentEnvelope) GetTerminalResponse() *TerminalResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_TerminalResponse); ok { - return x.TerminalResponse - } - } - return nil -} - -func (x *AgentEnvelope) GetTerminalEvent() *TerminalEvent { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_TerminalEvent); ok { - return x.TerminalEvent - } - } - return nil -} - -func (x *AgentEnvelope) GetFsListResp() *FsListResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_FsListResp); ok { - return x.FsListResp - } - } - return nil -} - -func (x *AgentEnvelope) GetFsWriteTextResp() *FsWriteTextResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_FsWriteTextResp); ok { - return x.FsWriteTextResp - } - } - return nil -} - -func (x *AgentEnvelope) GetFsCreateDirResp() *FsCreateDirResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_FsCreateDirResp); ok { - return x.FsCreateDirResp - } - } - return nil -} - -func (x *AgentEnvelope) GetFsRenameResp() *FsRenameResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_FsRenameResp); ok { - return x.FsRenameResp - } - } - return nil -} - -func (x *AgentEnvelope) GetFsDeleteResp() *FsDeleteResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_FsDeleteResp); ok { - return x.FsDeleteResp - } - } - return nil -} - -func (x *AgentEnvelope) GetGitResponse() *GitResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_GitResponse); ok { - return x.GitResponse - } - } - return nil -} - -func (x *AgentEnvelope) GetFsReadEditableTextResp() *FsReadEditableTextResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_FsReadEditableTextResp); ok { - return x.FsReadEditableTextResp - } - } - return nil -} - -func (x *AgentEnvelope) GetFsReadWorkspaceImageResp() *FsReadWorkspaceImageResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_FsReadWorkspaceImageResp); ok { - return x.FsReadWorkspaceImageResp - } - } - return nil -} - -func (x *AgentEnvelope) GetSftpResponse() *SftpResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_SftpResponse); ok { - return x.SftpResponse - } - } - return nil -} - -func (x *AgentEnvelope) GetSftpEvent() *SftpEvent { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_SftpEvent); ok { - return x.SftpEvent - } - } - return nil -} - -func (x *AgentEnvelope) GetChatQueueResp() *ChatQueueResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_ChatQueueResp); ok { - return x.ChatQueueResp - } - } - return nil -} - -func (x *AgentEnvelope) GetChatQueueEvent() *ChatQueueEvent { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_ChatQueueEvent); ok { - return x.ChatQueueEvent - } - } - return nil -} - -func (x *AgentEnvelope) GetChatControl() *ChatControlEvent { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_ChatControl); ok { - return x.ChatControl - } - } - return nil -} - -func (x *AgentEnvelope) GetRuntimeStatus() *RuntimeStatusEvent { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_RuntimeStatus); ok { - return x.RuntimeStatus - } - } - return nil -} - -func (x *AgentEnvelope) GetSettingsResetSshKnownHostResp() *SettingsResetSshKnownHostResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_SettingsResetSshKnownHostResp); ok { - return x.SettingsResetSshKnownHostResp - } - } - return nil -} - -func (x *AgentEnvelope) GetChatRuntimeSnapshot() *ChatRuntimeSnapshot { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_ChatRuntimeSnapshot); ok { - return x.ChatRuntimeSnapshot - } - } - return nil -} - -func (x *AgentEnvelope) GetProviderModelsResp() *ProviderModelsResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_ProviderModelsResp); ok { - return x.ProviderModelsResp - } - } - return nil -} - -func (x *AgentEnvelope) GetTunnelDesired() *TunnelDesiredState { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_TunnelDesired); ok { - return x.TunnelDesired - } - } - return nil -} - -func (x *AgentEnvelope) GetTunnelMutationResult() *TunnelMutationResult { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_TunnelMutationResult); ok { - return x.TunnelMutationResult - } - } - return nil -} - -func (x *AgentEnvelope) GetTunnelFrame() *TunnelFrame { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_TunnelFrame); ok { - return x.TunnelFrame - } - } - return nil -} - -func (x *AgentEnvelope) GetTunnelProbeReport() *TunnelProbeReport { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_TunnelProbeReport); ok { - return x.TunnelProbeReport - } - } - return nil -} - -func (x *AgentEnvelope) GetWorkspaceActivity() *WorkspaceActivityEvent { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_WorkspaceActivity); ok { - return x.WorkspaceActivity - } - } - return nil -} - -func (x *AgentEnvelope) GetManagedProcessResponse() *ManagedProcessResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_ManagedProcessResponse); ok { - return x.ManagedProcessResponse - } - } - return nil -} - -func (x *AgentEnvelope) GetManagedProcessSnapshot() *ManagedProcessSnapshot { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_ManagedProcessSnapshot); ok { - return x.ManagedProcessSnapshot - } - } - return nil -} - -func (x *AgentEnvelope) GetHistoryBranchResp() *HistoryBranchResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_HistoryBranchResp); ok { - return x.HistoryBranchResp - } - } - return nil -} - -func (x *AgentEnvelope) GetProviderUsageResp() *ProviderUsageResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_ProviderUsageResp); ok { - return x.ProviderUsageResp - } - } - return nil -} - -func (x *AgentEnvelope) GetChatIngressBatch() *ChatIngressBatch { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_ChatIngressBatch); ok { - return x.ChatIngressBatch - } - } - return nil -} - -func (x *AgentEnvelope) GetChatIngressResume() *ChatIngressResume { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_ChatIngressResume); ok { - return x.ChatIngressResume - } - } - return nil -} - -func (x *AgentEnvelope) GetChatIngressFragment() *ChatIngressFragment { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_ChatIngressFragment); ok { - return x.ChatIngressFragment - } - } - return nil -} - -func (x *AgentEnvelope) GetChatFileOpenResp() *ChatFileOpenResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_ChatFileOpenResp); ok { - return x.ChatFileOpenResp - } - } - return nil -} - -func (x *AgentEnvelope) GetError() *ErrorResponse { - if x != nil { - if x, ok := x.Payload.(*AgentEnvelope_Error); ok { - return x.Error - } - } - return nil -} - -type isAgentEnvelope_Payload interface { - isAgentEnvelope_Payload() -} - -type AgentEnvelope_ChatEvent struct { - ChatEvent *ChatEvent `protobuf:"bytes,10,opt,name=chat_event,json=chatEvent,proto3,oneof"` -} - -type AgentEnvelope_CronManageResp struct { - CronManageResp *CronManageResponse `protobuf:"bytes,20,opt,name=cron_manage_resp,json=cronManageResp,proto3,oneof"` -} - -type AgentEnvelope_HistoryListResp struct { - HistoryListResp *HistoryListResponse `protobuf:"bytes,30,opt,name=history_list_resp,json=historyListResp,proto3,oneof"` -} - -type AgentEnvelope_HistoryGetResp struct { - HistoryGetResp *HistoryGetResponse `protobuf:"bytes,31,opt,name=history_get_resp,json=historyGetResp,proto3,oneof"` -} - -type AgentEnvelope_HistoryRenameResp struct { - HistoryRenameResp *HistoryRenameResponse `protobuf:"bytes,32,opt,name=history_rename_resp,json=historyRenameResp,proto3,oneof"` -} - -type AgentEnvelope_HistoryDeleteResp struct { - HistoryDeleteResp *HistoryDeleteResponse `protobuf:"bytes,33,opt,name=history_delete_resp,json=historyDeleteResp,proto3,oneof"` -} - -type AgentEnvelope_HistorySync struct { - HistorySync *HistorySyncEvent `protobuf:"bytes,34,opt,name=history_sync,json=historySync,proto3,oneof"` -} - -type AgentEnvelope_HistoryPrefixResp struct { - HistoryPrefixResp *HistoryPrefixResponse `protobuf:"bytes,35,opt,name=history_prefix_resp,json=historyPrefixResp,proto3,oneof"` -} - -type AgentEnvelope_HistoryPinResp struct { - HistoryPinResp *HistoryPinResponse `protobuf:"bytes,36,opt,name=history_pin_resp,json=historyPinResp,proto3,oneof"` -} - -type AgentEnvelope_HistoryShareGetResp struct { - HistoryShareGetResp *HistoryShareGetResponse `protobuf:"bytes,37,opt,name=history_share_get_resp,json=historyShareGetResp,proto3,oneof"` -} - -type AgentEnvelope_HistoryShareSetResp struct { - HistoryShareSetResp *HistoryShareSetResponse `protobuf:"bytes,38,opt,name=history_share_set_resp,json=historyShareSetResp,proto3,oneof"` -} - -type AgentEnvelope_HistoryShareResolveResp struct { - HistoryShareResolveResp *HistoryShareResolveResponse `protobuf:"bytes,39,opt,name=history_share_resolve_resp,json=historyShareResolveResp,proto3,oneof"` -} - -type AgentEnvelope_HistoryWorkdirsResp struct { - HistoryWorkdirsResp *HistoryWorkdirsResponse `protobuf:"bytes,56,opt,name=history_workdirs_resp,json=historyWorkdirsResp,proto3,oneof"` -} - -type AgentEnvelope_ProviderListResp struct { - ProviderListResp *ProviderListResponse `protobuf:"bytes,40,opt,name=provider_list_resp,json=providerListResp,proto3,oneof"` -} - -type AgentEnvelope_SettingsGetResp struct { - SettingsGetResp *SettingsGetResponse `protobuf:"bytes,41,opt,name=settings_get_resp,json=settingsGetResp,proto3,oneof"` -} - -type AgentEnvelope_SettingsUpdateResp struct { - SettingsUpdateResp *SettingsUpdateResponse `protobuf:"bytes,42,opt,name=settings_update_resp,json=settingsUpdateResp,proto3,oneof"` -} - -type AgentEnvelope_SettingsSync struct { - SettingsSync *SettingsSyncEvent `protobuf:"bytes,43,opt,name=settings_sync,json=settingsSync,proto3,oneof"` -} - -type AgentEnvelope_SkillFilesListResp struct { - SkillFilesListResp *SkillFilesListResponse `protobuf:"bytes,44,opt,name=skill_files_list_resp,json=skillFilesListResp,proto3,oneof"` -} - -type AgentEnvelope_SkillMetadataReadResp struct { - SkillMetadataReadResp *SkillMetadataReadResponse `protobuf:"bytes,45,opt,name=skill_metadata_read_resp,json=skillMetadataReadResp,proto3,oneof"` -} - -type AgentEnvelope_SkillTextReadResp struct { - SkillTextReadResp *SkillTextReadResponse `protobuf:"bytes,46,opt,name=skill_text_read_resp,json=skillTextReadResp,proto3,oneof"` -} - -type AgentEnvelope_FileMentionListResp struct { - FileMentionListResp *FileMentionListResponse `protobuf:"bytes,47,opt,name=file_mention_list_resp,json=fileMentionListResp,proto3,oneof"` -} - -type AgentEnvelope_UploadReadableFilesResp struct { - UploadReadableFilesResp *UploadReadableFilesResponse `protobuf:"bytes,48,opt,name=upload_readable_files_resp,json=uploadReadableFilesResp,proto3,oneof"` -} - -type AgentEnvelope_FsRootsResp struct { - FsRootsResp *FsRootsResponse `protobuf:"bytes,49,opt,name=fs_roots_resp,json=fsRootsResp,proto3,oneof"` -} - -type AgentEnvelope_Pong struct { - Pong *PongResponse `protobuf:"bytes,50,opt,name=pong,proto3,oneof"` -} - -type AgentEnvelope_FsListDirsResp struct { - FsListDirsResp *FsListDirsResponse `protobuf:"bytes,51,opt,name=fs_list_dirs_resp,json=fsListDirsResp,proto3,oneof"` -} - -type AgentEnvelope_UploadedImagePreviewResp struct { - UploadedImagePreviewResp *UploadedImagePreviewResponse `protobuf:"bytes,52,opt,name=uploaded_image_preview_resp,json=uploadedImagePreviewResp,proto3,oneof"` -} - -type AgentEnvelope_MemoryManageResp struct { - MemoryManageResp *MemoryManageResponse `protobuf:"bytes,53,opt,name=memory_manage_resp,json=memoryManageResp,proto3,oneof"` -} - -type AgentEnvelope_SkillManageResp struct { - SkillManageResp *SkillManageResponse `protobuf:"bytes,54,opt,name=skill_manage_resp,json=skillManageResp,proto3,oneof"` -} - -type AgentEnvelope_FsCreateProjectFolderResp struct { - FsCreateProjectFolderResp *FsCreateProjectFolderResponse `protobuf:"bytes,55,opt,name=fs_create_project_folder_resp,json=fsCreateProjectFolderResp,proto3,oneof"` -} - -type AgentEnvelope_TerminalResponse struct { - TerminalResponse *TerminalResponse `protobuf:"bytes,57,opt,name=terminal_response,json=terminalResponse,proto3,oneof"` -} - -type AgentEnvelope_TerminalEvent struct { - TerminalEvent *TerminalEvent `protobuf:"bytes,58,opt,name=terminal_event,json=terminalEvent,proto3,oneof"` -} - -type AgentEnvelope_FsListResp struct { - FsListResp *FsListResponse `protobuf:"bytes,59,opt,name=fs_list_resp,json=fsListResp,proto3,oneof"` -} - -type AgentEnvelope_FsWriteTextResp struct { - FsWriteTextResp *FsWriteTextResponse `protobuf:"bytes,60,opt,name=fs_write_text_resp,json=fsWriteTextResp,proto3,oneof"` -} - -type AgentEnvelope_FsCreateDirResp struct { - FsCreateDirResp *FsCreateDirResponse `protobuf:"bytes,61,opt,name=fs_create_dir_resp,json=fsCreateDirResp,proto3,oneof"` -} - -type AgentEnvelope_FsRenameResp struct { - FsRenameResp *FsRenameResponse `protobuf:"bytes,62,opt,name=fs_rename_resp,json=fsRenameResp,proto3,oneof"` -} - -type AgentEnvelope_FsDeleteResp struct { - FsDeleteResp *FsDeleteResponse `protobuf:"bytes,63,opt,name=fs_delete_resp,json=fsDeleteResp,proto3,oneof"` -} - -type AgentEnvelope_GitResponse struct { - GitResponse *GitResponse `protobuf:"bytes,64,opt,name=git_response,json=gitResponse,proto3,oneof"` -} - -type AgentEnvelope_FsReadEditableTextResp struct { - FsReadEditableTextResp *FsReadEditableTextResponse `protobuf:"bytes,65,opt,name=fs_read_editable_text_resp,json=fsReadEditableTextResp,proto3,oneof"` -} - -type AgentEnvelope_FsReadWorkspaceImageResp struct { - FsReadWorkspaceImageResp *FsReadWorkspaceImageResponse `protobuf:"bytes,66,opt,name=fs_read_workspace_image_resp,json=fsReadWorkspaceImageResp,proto3,oneof"` -} - -type AgentEnvelope_SftpResponse struct { - SftpResponse *SftpResponse `protobuf:"bytes,73,opt,name=sftp_response,json=sftpResponse,proto3,oneof"` -} - -type AgentEnvelope_SftpEvent struct { - SftpEvent *SftpEvent `protobuf:"bytes,74,opt,name=sftp_event,json=sftpEvent,proto3,oneof"` -} - -type AgentEnvelope_ChatQueueResp struct { - ChatQueueResp *ChatQueueResponse `protobuf:"bytes,75,opt,name=chat_queue_resp,json=chatQueueResp,proto3,oneof"` -} - -type AgentEnvelope_ChatQueueEvent struct { - ChatQueueEvent *ChatQueueEvent `protobuf:"bytes,76,opt,name=chat_queue_event,json=chatQueueEvent,proto3,oneof"` -} - -type AgentEnvelope_ChatControl struct { - ChatControl *ChatControlEvent `protobuf:"bytes,70,opt,name=chat_control,json=chatControl,proto3,oneof"` -} - -type AgentEnvelope_RuntimeStatus struct { - RuntimeStatus *RuntimeStatusEvent `protobuf:"bytes,71,opt,name=runtime_status,json=runtimeStatus,proto3,oneof"` -} - -type AgentEnvelope_SettingsResetSshKnownHostResp struct { - SettingsResetSshKnownHostResp *SettingsResetSshKnownHostResponse `protobuf:"bytes,72,opt,name=settings_reset_ssh_known_host_resp,json=settingsResetSshKnownHostResp,proto3,oneof"` -} - -type AgentEnvelope_ChatRuntimeSnapshot struct { - ChatRuntimeSnapshot *ChatRuntimeSnapshot `protobuf:"bytes,77,opt,name=chat_runtime_snapshot,json=chatRuntimeSnapshot,proto3,oneof"` -} - -type AgentEnvelope_ProviderModelsResp struct { - ProviderModelsResp *ProviderModelsResponse `protobuf:"bytes,79,opt,name=provider_models_resp,json=providerModelsResp,proto3,oneof"` -} - -type AgentEnvelope_TunnelDesired struct { - TunnelDesired *TunnelDesiredState `protobuf:"bytes,80,opt,name=tunnel_desired,json=tunnelDesired,proto3,oneof"` -} - -type AgentEnvelope_TunnelMutationResult struct { - TunnelMutationResult *TunnelMutationResult `protobuf:"bytes,81,opt,name=tunnel_mutation_result,json=tunnelMutationResult,proto3,oneof"` -} - -type AgentEnvelope_TunnelFrame struct { - TunnelFrame *TunnelFrame `protobuf:"bytes,82,opt,name=tunnel_frame,json=tunnelFrame,proto3,oneof"` -} - -type AgentEnvelope_TunnelProbeReport struct { - TunnelProbeReport *TunnelProbeReport `protobuf:"bytes,83,opt,name=tunnel_probe_report,json=tunnelProbeReport,proto3,oneof"` -} - -type AgentEnvelope_WorkspaceActivity struct { - WorkspaceActivity *WorkspaceActivityEvent `protobuf:"bytes,90,opt,name=workspace_activity,json=workspaceActivity,proto3,oneof"` -} - -type AgentEnvelope_ManagedProcessResponse struct { - ManagedProcessResponse *ManagedProcessResponse `protobuf:"bytes,91,opt,name=managed_process_response,json=managedProcessResponse,proto3,oneof"` -} - -type AgentEnvelope_ManagedProcessSnapshot struct { - ManagedProcessSnapshot *ManagedProcessSnapshot `protobuf:"bytes,92,opt,name=managed_process_snapshot,json=managedProcessSnapshot,proto3,oneof"` -} - -type AgentEnvelope_HistoryBranchResp struct { - HistoryBranchResp *HistoryBranchResponse `protobuf:"bytes,93,opt,name=history_branch_resp,json=historyBranchResp,proto3,oneof"` -} - -type AgentEnvelope_ProviderUsageResp struct { - ProviderUsageResp *ProviderUsageResponse `protobuf:"bytes,94,opt,name=provider_usage_resp,json=providerUsageResp,proto3,oneof"` -} - -type AgentEnvelope_ChatIngressBatch struct { - ChatIngressBatch *ChatIngressBatch `protobuf:"bytes,95,opt,name=chat_ingress_batch,json=chatIngressBatch,proto3,oneof"` -} - -type AgentEnvelope_ChatIngressResume struct { - ChatIngressResume *ChatIngressResume `protobuf:"bytes,96,opt,name=chat_ingress_resume,json=chatIngressResume,proto3,oneof"` -} - -type AgentEnvelope_ChatIngressFragment struct { - ChatIngressFragment *ChatIngressFragment `protobuf:"bytes,97,opt,name=chat_ingress_fragment,json=chatIngressFragment,proto3,oneof"` -} - -type AgentEnvelope_ChatFileOpenResp struct { - ChatFileOpenResp *ChatFileOpenResponse `protobuf:"bytes,98,opt,name=chat_file_open_resp,json=chatFileOpenResp,proto3,oneof"` -} - -type AgentEnvelope_Error struct { - Error *ErrorResponse `protobuf:"bytes,99,opt,name=error,proto3,oneof"` -} - -func (*AgentEnvelope_ChatEvent) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_CronManageResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_HistoryListResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_HistoryGetResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_HistoryRenameResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_HistoryDeleteResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_HistorySync) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_HistoryPrefixResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_HistoryPinResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_HistoryShareGetResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_HistoryShareSetResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_HistoryShareResolveResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_HistoryWorkdirsResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_ProviderListResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_SettingsGetResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_SettingsUpdateResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_SettingsSync) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_SkillFilesListResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_SkillMetadataReadResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_SkillTextReadResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_FileMentionListResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_UploadReadableFilesResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_FsRootsResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_Pong) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_FsListDirsResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_UploadedImagePreviewResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_MemoryManageResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_SkillManageResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_FsCreateProjectFolderResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_TerminalResponse) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_TerminalEvent) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_FsListResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_FsWriteTextResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_FsCreateDirResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_FsRenameResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_FsDeleteResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_GitResponse) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_FsReadEditableTextResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_FsReadWorkspaceImageResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_SftpResponse) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_SftpEvent) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_ChatQueueResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_ChatQueueEvent) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_ChatControl) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_RuntimeStatus) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_SettingsResetSshKnownHostResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_ChatRuntimeSnapshot) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_ProviderModelsResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_TunnelDesired) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_TunnelMutationResult) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_TunnelFrame) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_TunnelProbeReport) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_WorkspaceActivity) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_ManagedProcessResponse) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_ManagedProcessSnapshot) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_HistoryBranchResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_ProviderUsageResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_ChatIngressBatch) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_ChatIngressResume) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_ChatIngressFragment) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_ChatFileOpenResp) isAgentEnvelope_Payload() {} - -func (*AgentEnvelope_Error) isAgentEnvelope_Payload() {} - -type ChatSelectedModel struct { - state protoimpl.MessageState `protogen:"open.v1"` - CustomProviderId string `protobuf:"bytes,1,opt,name=custom_provider_id,json=customProviderId,proto3" json:"custom_provider_id,omitempty"` - Model string `protobuf:"bytes,2,opt,name=model,proto3" json:"model,omitempty"` - ProviderType string `protobuf:"bytes,3,opt,name=provider_type,json=providerType,proto3" json:"provider_type,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatSelectedModel) Reset() { - *x = ChatSelectedModel{} - mi := &file_proto_v2_gateway_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatSelectedModel) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatSelectedModel) ProtoMessage() {} - -func (x *ChatSelectedModel) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[2] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatSelectedModel.ProtoReflect.Descriptor instead. -func (*ChatSelectedModel) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{2} -} - -func (x *ChatSelectedModel) GetCustomProviderId() string { - if x != nil { - return x.CustomProviderId - } - return "" -} - -func (x *ChatSelectedModel) GetModel() string { - if x != nil { - return x.Model - } - return "" -} - -func (x *ChatSelectedModel) GetProviderType() string { - if x != nil { - return x.ProviderType - } - return "" -} - -type ChatRuntimeControls struct { - state protoimpl.MessageState `protogen:"open.v1"` - ThinkingEnabled bool `protobuf:"varint,1,opt,name=thinking_enabled,json=thinkingEnabled,proto3" json:"thinking_enabled,omitempty"` - NativeWebSearchEnabled bool `protobuf:"varint,2,opt,name=native_web_search_enabled,json=nativeWebSearchEnabled,proto3" json:"native_web_search_enabled,omitempty"` - Reasoning string `protobuf:"bytes,3,opt,name=reasoning,proto3" json:"reasoning,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatRuntimeControls) Reset() { - *x = ChatRuntimeControls{} - mi := &file_proto_v2_gateway_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatRuntimeControls) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatRuntimeControls) ProtoMessage() {} - -func (x *ChatRuntimeControls) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[3] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatRuntimeControls.ProtoReflect.Descriptor instead. -func (*ChatRuntimeControls) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{3} -} - -func (x *ChatRuntimeControls) GetThinkingEnabled() bool { - if x != nil { - return x.ThinkingEnabled - } - return false -} - -func (x *ChatRuntimeControls) GetNativeWebSearchEnabled() bool { - if x != nil { - return x.NativeWebSearchEnabled - } - return false -} - -func (x *ChatRuntimeControls) GetReasoning() string { - if x != nil { - return x.Reasoning - } - return "" -} - -type ChatUploadedFile struct { - state protoimpl.MessageState `protogen:"open.v1"` - RelativePath string `protobuf:"bytes,1,opt,name=relative_path,json=relativePath,proto3" json:"relative_path,omitempty"` - AbsolutePath string `protobuf:"bytes,2,opt,name=absolute_path,json=absolutePath,proto3" json:"absolute_path,omitempty"` - FileName string `protobuf:"bytes,3,opt,name=file_name,json=fileName,proto3" json:"file_name,omitempty"` - Kind string `protobuf:"bytes,4,opt,name=kind,proto3" json:"kind,omitempty"` - SizeBytes int64 `protobuf:"varint,5,opt,name=size_bytes,json=sizeBytes,proto3" json:"size_bytes,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatUploadedFile) Reset() { - *x = ChatUploadedFile{} - mi := &file_proto_v2_gateway_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatUploadedFile) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatUploadedFile) ProtoMessage() {} - -func (x *ChatUploadedFile) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[4] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatUploadedFile.ProtoReflect.Descriptor instead. -func (*ChatUploadedFile) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{4} -} - -func (x *ChatUploadedFile) GetRelativePath() string { - if x != nil { - return x.RelativePath - } - return "" -} - -func (x *ChatUploadedFile) GetAbsolutePath() string { - if x != nil { - return x.AbsolutePath - } - return "" -} - -func (x *ChatUploadedFile) GetFileName() string { - if x != nil { - return x.FileName - } - return "" -} - -func (x *ChatUploadedFile) GetKind() string { - if x != nil { - return x.Kind - } - return "" -} - -func (x *ChatUploadedFile) GetSizeBytes() int64 { - if x != nil { - return x.SizeBytes - } - return 0 -} - -type UploadReadableFile struct { - state protoimpl.MessageState `protogen:"open.v1"` - FileName string `protobuf:"bytes,1,opt,name=file_name,json=fileName,proto3" json:"file_name,omitempty"` - MimeType string `protobuf:"bytes,2,opt,name=mime_type,json=mimeType,proto3" json:"mime_type,omitempty"` - Content []byte `protobuf:"bytes,3,opt,name=content,proto3" json:"content,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UploadReadableFile) Reset() { - *x = UploadReadableFile{} - mi := &file_proto_v2_gateway_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UploadReadableFile) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UploadReadableFile) ProtoMessage() {} - -func (x *UploadReadableFile) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[5] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use UploadReadableFile.ProtoReflect.Descriptor instead. -func (*UploadReadableFile) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{5} -} - -func (x *UploadReadableFile) GetFileName() string { - if x != nil { - return x.FileName - } - return "" -} - -func (x *UploadReadableFile) GetMimeType() string { - if x != nil { - return x.MimeType - } - return "" -} - -func (x *UploadReadableFile) GetContent() []byte { - if x != nil { - return x.Content - } - return nil -} - -type UploadReadableFilesRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Workdir string `protobuf:"bytes,1,opt,name=workdir,proto3" json:"workdir,omitempty"` - Files []*UploadReadableFile `protobuf:"bytes,2,rep,name=files,proto3" json:"files,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UploadReadableFilesRequest) Reset() { - *x = UploadReadableFilesRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UploadReadableFilesRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UploadReadableFilesRequest) ProtoMessage() {} - -func (x *UploadReadableFilesRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[6] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use UploadReadableFilesRequest.ProtoReflect.Descriptor instead. -func (*UploadReadableFilesRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{6} -} - -func (x *UploadReadableFilesRequest) GetWorkdir() string { - if x != nil { - return x.Workdir - } - return "" -} - -func (x *UploadReadableFilesRequest) GetFiles() []*UploadReadableFile { - if x != nil { - return x.Files - } - return nil -} - -type UploadReadableFilesResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Files []*ChatUploadedFile `protobuf:"bytes,1,rep,name=files,proto3" json:"files,omitempty"` - Skipped []string `protobuf:"bytes,2,rep,name=skipped,proto3" json:"skipped,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UploadReadableFilesResponse) Reset() { - *x = UploadReadableFilesResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UploadReadableFilesResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UploadReadableFilesResponse) ProtoMessage() {} - -func (x *UploadReadableFilesResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[7] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use UploadReadableFilesResponse.ProtoReflect.Descriptor instead. -func (*UploadReadableFilesResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{7} -} - -func (x *UploadReadableFilesResponse) GetFiles() []*ChatUploadedFile { - if x != nil { - return x.Files - } - return nil -} - -func (x *UploadReadableFilesResponse) GetSkipped() []string { - if x != nil { - return x.Skipped - } - return nil -} - -type UploadedImagePreviewRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Workdir string `protobuf:"bytes,1,opt,name=workdir,proto3" json:"workdir,omitempty"` - AbsolutePath string `protobuf:"bytes,2,opt,name=absolute_path,json=absolutePath,proto3" json:"absolute_path,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UploadedImagePreviewRequest) Reset() { - *x = UploadedImagePreviewRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UploadedImagePreviewRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UploadedImagePreviewRequest) ProtoMessage() {} - -func (x *UploadedImagePreviewRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[8] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use UploadedImagePreviewRequest.ProtoReflect.Descriptor instead. -func (*UploadedImagePreviewRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{8} -} - -func (x *UploadedImagePreviewRequest) GetWorkdir() string { - if x != nil { - return x.Workdir - } - return "" -} - -func (x *UploadedImagePreviewRequest) GetAbsolutePath() string { - if x != nil { - return x.AbsolutePath - } - return "" -} - -type UploadedImagePreviewResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - MimeType string `protobuf:"bytes,1,opt,name=mime_type,json=mimeType,proto3" json:"mime_type,omitempty"` - Data string `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UploadedImagePreviewResponse) Reset() { - *x = UploadedImagePreviewResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UploadedImagePreviewResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UploadedImagePreviewResponse) ProtoMessage() {} - -func (x *UploadedImagePreviewResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[9] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use UploadedImagePreviewResponse.ProtoReflect.Descriptor instead. -func (*UploadedImagePreviewResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{9} -} - -func (x *UploadedImagePreviewResponse) GetMimeType() string { - if x != nil { - return x.MimeType - } - return "" -} - -func (x *UploadedImagePreviewResponse) GetData() string { - if x != nil { - return x.Data - } - return "" -} - -type TunnelSpec struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // agent-generated, stable across restarts - SlugHint string `protobuf:"bytes,2,opt,name=slug_hint,json=slugHint,proto3" json:"slug_hint,omitempty"` // last allocated slug; gateway honors when valid and unused - Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` - TargetUrl string `protobuf:"bytes,4,opt,name=target_url,json=targetUrl,proto3" json:"target_url,omitempty"` // http://localhost:PORT[/base] - ExpiresAt int64 `protobuf:"varint,5,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` // unix seconds, 0 = never - ProjectPathKey string `protobuf:"bytes,6,opt,name=project_path_key,json=projectPathKey,proto3" json:"project_path_key,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TunnelSpec) Reset() { - *x = TunnelSpec{} - mi := &file_proto_v2_gateway_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TunnelSpec) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TunnelSpec) ProtoMessage() {} - -func (x *TunnelSpec) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[10] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TunnelSpec.ProtoReflect.Descriptor instead. -func (*TunnelSpec) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{10} -} - -func (x *TunnelSpec) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *TunnelSpec) GetSlugHint() string { - if x != nil { - return x.SlugHint - } - return "" -} - -func (x *TunnelSpec) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *TunnelSpec) GetTargetUrl() string { - if x != nil { - return x.TargetUrl - } - return "" -} - -func (x *TunnelSpec) GetExpiresAt() int64 { - if x != nil { - return x.ExpiresAt - } - return 0 -} - -func (x *TunnelSpec) GetProjectPathKey() string { - if x != nil { - return x.ProjectPathKey - } - return "" -} - -type TunnelDesiredState struct { - state protoimpl.MessageState `protogen:"open.v1"` - Tunnels []*TunnelSpec `protobuf:"bytes,1,rep,name=tunnels,proto3" json:"tunnels,omitempty"` - Revision uint64 `protobuf:"varint,2,opt,name=revision,proto3" json:"revision,omitempty"` // agent-side monotonic - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TunnelDesiredState) Reset() { - *x = TunnelDesiredState{} - mi := &file_proto_v2_gateway_proto_msgTypes[11] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TunnelDesiredState) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TunnelDesiredState) ProtoMessage() {} - -func (x *TunnelDesiredState) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[11] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TunnelDesiredState.ProtoReflect.Descriptor instead. -func (*TunnelDesiredState) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{11} -} - -func (x *TunnelDesiredState) GetTunnels() []*TunnelSpec { - if x != nil { - return x.Tunnels - } - return nil -} - -func (x *TunnelDesiredState) GetRevision() uint64 { - if x != nil { - return x.Revision - } - return 0 -} - -type TunnelHealth struct { - state protoimpl.MessageState `protogen:"open.v1"` - Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` // "ok" | "failed" | "unknown" - HttpStatus uint32 `protobuf:"varint,2,opt,name=http_status,json=httpStatus,proto3" json:"http_status,omitempty"` // local layer only - Error string `protobuf:"bytes,3,opt,name=error,proto3" json:"error,omitempty"` - CheckedAt int64 `protobuf:"varint,4,opt,name=checked_at,json=checkedAt,proto3" json:"checked_at,omitempty"` - RttMs uint32 `protobuf:"varint,5,opt,name=rtt_ms,json=rttMs,proto3" json:"rtt_ms,omitempty"` // relay layer only - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TunnelHealth) Reset() { - *x = TunnelHealth{} - mi := &file_proto_v2_gateway_proto_msgTypes[12] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TunnelHealth) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TunnelHealth) ProtoMessage() {} - -func (x *TunnelHealth) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[12] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TunnelHealth.ProtoReflect.Descriptor instead. -func (*TunnelHealth) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{12} -} - -func (x *TunnelHealth) GetStatus() string { - if x != nil { - return x.Status - } - return "" -} - -func (x *TunnelHealth) GetHttpStatus() uint32 { - if x != nil { - return x.HttpStatus - } - return 0 -} - -func (x *TunnelHealth) GetError() string { - if x != nil { - return x.Error - } - return "" -} - -func (x *TunnelHealth) GetCheckedAt() int64 { - if x != nil { - return x.CheckedAt - } - return 0 -} - -func (x *TunnelHealth) GetRttMs() uint32 { - if x != nil { - return x.RttMs - } - return 0 -} - -type TunnelStatus struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Slug string `protobuf:"bytes,2,opt,name=slug,proto3" json:"slug,omitempty"` - Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` - TargetUrl string `protobuf:"bytes,4,opt,name=target_url,json=targetUrl,proto3" json:"target_url,omitempty"` - PublicPath string `protobuf:"bytes,5,opt,name=public_path,json=publicPath,proto3" json:"public_path,omitempty"` // "/t/{slug}/"; clients compose the full URL - CreatedAt int64 `protobuf:"varint,6,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` - ExpiresAt int64 `protobuf:"varint,7,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` - ActiveConnections uint32 `protobuf:"varint,8,opt,name=active_connections,json=activeConnections,proto3" json:"active_connections,omitempty"` - ProjectPathKey string `protobuf:"bytes,9,opt,name=project_path_key,json=projectPathKey,proto3" json:"project_path_key,omitempty"` - Local *TunnelHealth `protobuf:"bytes,10,opt,name=local,proto3" json:"local,omitempty"` // agent -> local service reachability - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TunnelStatus) Reset() { - *x = TunnelStatus{} - mi := &file_proto_v2_gateway_proto_msgTypes[13] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TunnelStatus) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TunnelStatus) ProtoMessage() {} - -func (x *TunnelStatus) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[13] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TunnelStatus.ProtoReflect.Descriptor instead. -func (*TunnelStatus) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{13} -} - -func (x *TunnelStatus) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *TunnelStatus) GetSlug() string { - if x != nil { - return x.Slug - } - return "" -} - -func (x *TunnelStatus) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *TunnelStatus) GetTargetUrl() string { - if x != nil { - return x.TargetUrl - } - return "" -} - -func (x *TunnelStatus) GetPublicPath() string { - if x != nil { - return x.PublicPath - } - return "" -} - -func (x *TunnelStatus) GetCreatedAt() int64 { - if x != nil { - return x.CreatedAt - } - return 0 -} - -func (x *TunnelStatus) GetExpiresAt() int64 { - if x != nil { - return x.ExpiresAt - } - return 0 -} - -func (x *TunnelStatus) GetActiveConnections() uint32 { - if x != nil { - return x.ActiveConnections - } - return 0 -} - -func (x *TunnelStatus) GetProjectPathKey() string { - if x != nil { - return x.ProjectPathKey - } - return "" -} - -func (x *TunnelStatus) GetLocal() *TunnelHealth { - if x != nil { - return x.Local - } - return nil -} - -type TunnelStateSnapshot struct { - state protoimpl.MessageState `protogen:"open.v1"` - Tunnels []*TunnelStatus `protobuf:"bytes,1,rep,name=tunnels,proto3" json:"tunnels,omitempty"` - Revision uint64 `protobuf:"varint,2,opt,name=revision,proto3" json:"revision,omitempty"` // gateway-side monotonic - AgentOnline bool `protobuf:"varint,3,opt,name=agent_online,json=agentOnline,proto3" json:"agent_online,omitempty"` - Relay *TunnelHealth `protobuf:"bytes,4,opt,name=relay,proto3" json:"relay,omitempty"` // gateway <-> agent frame path - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TunnelStateSnapshot) Reset() { - *x = TunnelStateSnapshot{} - mi := &file_proto_v2_gateway_proto_msgTypes[14] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TunnelStateSnapshot) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TunnelStateSnapshot) ProtoMessage() {} - -func (x *TunnelStateSnapshot) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[14] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TunnelStateSnapshot.ProtoReflect.Descriptor instead. -func (*TunnelStateSnapshot) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{14} -} - -func (x *TunnelStateSnapshot) GetTunnels() []*TunnelStatus { - if x != nil { - return x.Tunnels - } - return nil -} - -func (x *TunnelStateSnapshot) GetRevision() uint64 { - if x != nil { - return x.Revision - } - return 0 -} - -func (x *TunnelStateSnapshot) GetAgentOnline() bool { - if x != nil { - return x.AgentOnline - } - return false -} - -func (x *TunnelStateSnapshot) GetRelay() *TunnelHealth { - if x != nil { - return x.Relay - } - return nil -} - -type TunnelMutation struct { - state protoimpl.MessageState `protogen:"open.v1"` - Action string `protobuf:"bytes,1,opt,name=action,proto3" json:"action,omitempty"` // "create" | "update" | "close" | "check" - TunnelId string `protobuf:"bytes,2,opt,name=tunnel_id,json=tunnelId,proto3" json:"tunnel_id,omitempty"` // update/close/check - TargetUrl string `protobuf:"bytes,3,opt,name=target_url,json=targetUrl,proto3" json:"target_url,omitempty"` - Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"` - TtlSeconds *uint32 `protobuf:"varint,5,opt,name=ttl_seconds,json=ttlSeconds,proto3,oneof" json:"ttl_seconds,omitempty"` // absent on update = keep current expiry - ProjectPathKey string `protobuf:"bytes,6,opt,name=project_path_key,json=projectPathKey,proto3" json:"project_path_key,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TunnelMutation) Reset() { - *x = TunnelMutation{} - mi := &file_proto_v2_gateway_proto_msgTypes[15] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TunnelMutation) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TunnelMutation) ProtoMessage() {} - -func (x *TunnelMutation) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[15] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TunnelMutation.ProtoReflect.Descriptor instead. -func (*TunnelMutation) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{15} -} - -func (x *TunnelMutation) GetAction() string { - if x != nil { - return x.Action - } - return "" -} - -func (x *TunnelMutation) GetTunnelId() string { - if x != nil { - return x.TunnelId - } - return "" -} - -func (x *TunnelMutation) GetTargetUrl() string { - if x != nil { - return x.TargetUrl - } - return "" -} - -func (x *TunnelMutation) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *TunnelMutation) GetTtlSeconds() uint32 { - if x != nil && x.TtlSeconds != nil { - return *x.TtlSeconds - } - return 0 -} - -func (x *TunnelMutation) GetProjectPathKey() string { - if x != nil { - return x.ProjectPathKey - } - return "" -} - -type TunnelMutationResult struct { - state protoimpl.MessageState `protogen:"open.v1"` - TunnelId string `protobuf:"bytes,1,opt,name=tunnel_id,json=tunnelId,proto3" json:"tunnel_id,omitempty"` - ErrorCode string `protobuf:"bytes,2,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"` // "" = ok; invalid_target|limit_exceeded|not_found|invalid_ttl - ErrorMessage string `protobuf:"bytes,3,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TunnelMutationResult) Reset() { - *x = TunnelMutationResult{} - mi := &file_proto_v2_gateway_proto_msgTypes[16] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TunnelMutationResult) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TunnelMutationResult) ProtoMessage() {} - -func (x *TunnelMutationResult) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[16] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TunnelMutationResult.ProtoReflect.Descriptor instead. -func (*TunnelMutationResult) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{16} -} - -func (x *TunnelMutationResult) GetTunnelId() string { - if x != nil { - return x.TunnelId - } - return "" -} - -func (x *TunnelMutationResult) GetErrorCode() string { - if x != nil { - return x.ErrorCode - } - return "" -} - -func (x *TunnelMutationResult) GetErrorMessage() string { - if x != nil { - return x.ErrorMessage - } - return "" -} - -type TunnelProbeResult struct { - state protoimpl.MessageState `protogen:"open.v1"` - TunnelId string `protobuf:"bytes,1,opt,name=tunnel_id,json=tunnelId,proto3" json:"tunnel_id,omitempty"` - Local *TunnelHealth `protobuf:"bytes,2,opt,name=local,proto3" json:"local,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TunnelProbeResult) Reset() { - *x = TunnelProbeResult{} - mi := &file_proto_v2_gateway_proto_msgTypes[17] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TunnelProbeResult) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TunnelProbeResult) ProtoMessage() {} - -func (x *TunnelProbeResult) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[17] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TunnelProbeResult.ProtoReflect.Descriptor instead. -func (*TunnelProbeResult) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{17} -} - -func (x *TunnelProbeResult) GetTunnelId() string { - if x != nil { - return x.TunnelId - } - return "" -} - -func (x *TunnelProbeResult) GetLocal() *TunnelHealth { - if x != nil { - return x.Local - } - return nil -} - -type TunnelProbeReport struct { - state protoimpl.MessageState `protogen:"open.v1"` - Results []*TunnelProbeResult `protobuf:"bytes,1,rep,name=results,proto3" json:"results,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TunnelProbeReport) Reset() { - *x = TunnelProbeReport{} - mi := &file_proto_v2_gateway_proto_msgTypes[18] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TunnelProbeReport) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TunnelProbeReport) ProtoMessage() {} - -func (x *TunnelProbeReport) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[18] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TunnelProbeReport.ProtoReflect.Descriptor instead. -func (*TunnelProbeReport) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{18} -} - -func (x *TunnelProbeReport) GetResults() []*TunnelProbeResult { - if x != nil { - return x.Results - } - return nil -} - -type TunnelHeader struct { - state protoimpl.MessageState `protogen:"open.v1"` - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TunnelHeader) Reset() { - *x = TunnelHeader{} - mi := &file_proto_v2_gateway_proto_msgTypes[19] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TunnelHeader) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TunnelHeader) ProtoMessage() {} - -func (x *TunnelHeader) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[19] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TunnelHeader.ProtoReflect.Descriptor instead. -func (*TunnelHeader) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{19} -} - -func (x *TunnelHeader) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *TunnelHeader) GetValue() string { - if x != nil { - return x.Value - } - return "" -} - -type TunnelFrame struct { - state protoimpl.MessageState `protogen:"open.v1"` - StreamId string `protobuf:"bytes,1,opt,name=stream_id,json=streamId,proto3" json:"stream_id,omitempty"` - Kind TunnelFrameKind `protobuf:"varint,2,opt,name=kind,proto3,enum=liveagent.gateway.v2.TunnelFrameKind" json:"kind,omitempty"` - TargetUrl string `protobuf:"bytes,3,opt,name=target_url,json=targetUrl,proto3" json:"target_url,omitempty"` // set on HTTP_REQUEST_START and WS_DIAL only - Method string `protobuf:"bytes,4,opt,name=method,proto3" json:"method,omitempty"` - Path string `protobuf:"bytes,5,opt,name=path,proto3" json:"path,omitempty"` // path+query relative to the target base - Headers []*TunnelHeader `protobuf:"bytes,6,rep,name=headers,proto3" json:"headers,omitempty"` - Status uint32 `protobuf:"varint,7,opt,name=status,proto3" json:"status,omitempty"` - Body []byte `protobuf:"bytes,8,opt,name=body,proto3" json:"body,omitempty"` - Error string `protobuf:"bytes,9,opt,name=error,proto3" json:"error,omitempty"` - WsMessageType TunnelWsMessageType `protobuf:"varint,10,opt,name=ws_message_type,json=wsMessageType,proto3,enum=liveagent.gateway.v2.TunnelWsMessageType" json:"ws_message_type,omitempty"` - WsSubprotocol string `protobuf:"bytes,11,opt,name=ws_subprotocol,json=wsSubprotocol,proto3" json:"ws_subprotocol,omitempty"` - WsCloseCode uint32 `protobuf:"varint,12,opt,name=ws_close_code,json=wsCloseCode,proto3" json:"ws_close_code,omitempty"` - WsCloseReason string `protobuf:"bytes,13,opt,name=ws_close_reason,json=wsCloseReason,proto3" json:"ws_close_reason,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TunnelFrame) Reset() { - *x = TunnelFrame{} - mi := &file_proto_v2_gateway_proto_msgTypes[20] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TunnelFrame) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TunnelFrame) ProtoMessage() {} - -func (x *TunnelFrame) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[20] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TunnelFrame.ProtoReflect.Descriptor instead. -func (*TunnelFrame) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{20} -} - -func (x *TunnelFrame) GetStreamId() string { - if x != nil { - return x.StreamId - } - return "" -} - -func (x *TunnelFrame) GetKind() TunnelFrameKind { - if x != nil { - return x.Kind - } - return TunnelFrameKind_TUNNEL_FRAME_KIND_UNSPECIFIED -} - -func (x *TunnelFrame) GetTargetUrl() string { - if x != nil { - return x.TargetUrl - } - return "" -} - -func (x *TunnelFrame) GetMethod() string { - if x != nil { - return x.Method - } - return "" -} - -func (x *TunnelFrame) GetPath() string { - if x != nil { - return x.Path - } - return "" -} - -func (x *TunnelFrame) GetHeaders() []*TunnelHeader { - if x != nil { - return x.Headers - } - return nil -} - -func (x *TunnelFrame) GetStatus() uint32 { - if x != nil { - return x.Status - } - return 0 -} - -func (x *TunnelFrame) GetBody() []byte { - if x != nil { - return x.Body - } - return nil -} - -func (x *TunnelFrame) GetError() string { - if x != nil { - return x.Error - } - return "" -} - -func (x *TunnelFrame) GetWsMessageType() TunnelWsMessageType { - if x != nil { - return x.WsMessageType - } - return TunnelWsMessageType_TUNNEL_WS_MESSAGE_TYPE_UNSPECIFIED -} - -func (x *TunnelFrame) GetWsSubprotocol() string { - if x != nil { - return x.WsSubprotocol - } - return "" -} - -func (x *TunnelFrame) GetWsCloseCode() uint32 { - if x != nil { - return x.WsCloseCode - } - return 0 -} - -func (x *TunnelFrame) GetWsCloseReason() string { - if x != nil { - return x.WsCloseReason - } - return "" -} - -type WorkspaceWatchRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Workdirs []string `protobuf:"bytes,1,rep,name=workdirs,proto3" json:"workdirs,omitempty"` // full desired set; replaces the previous one - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *WorkspaceWatchRequest) Reset() { - *x = WorkspaceWatchRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[21] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *WorkspaceWatchRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*WorkspaceWatchRequest) ProtoMessage() {} - -func (x *WorkspaceWatchRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[21] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use WorkspaceWatchRequest.ProtoReflect.Descriptor instead. -func (*WorkspaceWatchRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{21} -} - -func (x *WorkspaceWatchRequest) GetWorkdirs() []string { - if x != nil { - return x.Workdirs - } - return nil -} - -type WorkspaceActivityEvent struct { - state protoimpl.MessageState `protogen:"open.v1"` - Workdir string `protobuf:"bytes,1,opt,name=workdir,proto3" json:"workdir,omitempty"` - Revision uint64 `protobuf:"varint,2,opt,name=revision,proto3" json:"revision,omitempty"` // per-workdir monotonic within one agent process - Fs bool `protobuf:"varint,3,opt,name=fs,proto3" json:"fs,omitempty"` // working-tree content changed - Git bool `protobuf:"varint,4,opt,name=git,proto3" json:"git,omitempty"` // git state (HEAD/refs/index/...) changed - ChangedPaths []string `protobuf:"bytes,5,rep,name=changed_paths,json=changedPaths,proto3" json:"changed_paths,omitempty"` // relative to workdir, deduped, capped - Truncated bool `protobuf:"varint,6,opt,name=truncated,proto3" json:"truncated,omitempty"` // changed_paths hit the cap or is unknown - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *WorkspaceActivityEvent) Reset() { - *x = WorkspaceActivityEvent{} - mi := &file_proto_v2_gateway_proto_msgTypes[22] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *WorkspaceActivityEvent) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*WorkspaceActivityEvent) ProtoMessage() {} - -func (x *WorkspaceActivityEvent) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[22] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use WorkspaceActivityEvent.ProtoReflect.Descriptor instead. -func (*WorkspaceActivityEvent) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{22} -} - -func (x *WorkspaceActivityEvent) GetWorkdir() string { - if x != nil { - return x.Workdir - } - return "" -} - -func (x *WorkspaceActivityEvent) GetRevision() uint64 { - if x != nil { - return x.Revision - } - return 0 -} - -func (x *WorkspaceActivityEvent) GetFs() bool { - if x != nil { - return x.Fs - } - return false -} - -func (x *WorkspaceActivityEvent) GetGit() bool { - if x != nil { - return x.Git - } - return false -} - -func (x *WorkspaceActivityEvent) GetChangedPaths() []string { - if x != nil { - return x.ChangedPaths - } - return nil -} - -func (x *WorkspaceActivityEvent) GetTruncated() bool { - if x != nil { - return x.Truncated - } - return false -} - -type ManagedProcessRecord struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Label string `protobuf:"bytes,2,opt,name=label,proto3" json:"label,omitempty"` - Command string `protobuf:"bytes,3,opt,name=command,proto3" json:"command,omitempty"` - Cwd string `protobuf:"bytes,4,opt,name=cwd,proto3" json:"cwd,omitempty"` - Shell string `protobuf:"bytes,5,opt,name=shell,proto3" json:"shell,omitempty"` - Pid uint32 `protobuf:"varint,6,opt,name=pid,proto3" json:"pid,omitempty"` - LogPath string `protobuf:"bytes,7,opt,name=log_path,json=logPath,proto3" json:"log_path,omitempty"` - StartedAt int64 `protobuf:"varint,8,opt,name=started_at,json=startedAt,proto3" json:"started_at,omitempty"` // unix ms - FinishedAt *int64 `protobuf:"varint,9,opt,name=finished_at,json=finishedAt,proto3,oneof" json:"finished_at,omitempty"` // unix ms - ExitCode *int32 `protobuf:"varint,10,opt,name=exit_code,json=exitCode,proto3,oneof" json:"exit_code,omitempty"` // absent for restored/pid-managed entries - Running bool `protobuf:"varint,11,opt,name=running,proto3" json:"running,omitempty"` - Isolated bool `protobuf:"varint,12,opt,name=isolated,proto3" json:"isolated,omitempty"` // survives LiveAgent exit - Restored bool `protobuf:"varint,13,opt,name=restored,proto3" json:"restored,omitempty"` // recovered after restart; managed by pid - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ManagedProcessRecord) Reset() { - *x = ManagedProcessRecord{} - mi := &file_proto_v2_gateway_proto_msgTypes[23] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ManagedProcessRecord) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ManagedProcessRecord) ProtoMessage() {} - -func (x *ManagedProcessRecord) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[23] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ManagedProcessRecord.ProtoReflect.Descriptor instead. -func (*ManagedProcessRecord) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{23} -} - -func (x *ManagedProcessRecord) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *ManagedProcessRecord) GetLabel() string { - if x != nil { - return x.Label - } - return "" -} - -func (x *ManagedProcessRecord) GetCommand() string { - if x != nil { - return x.Command - } - return "" -} - -func (x *ManagedProcessRecord) GetCwd() string { - if x != nil { - return x.Cwd - } - return "" -} - -func (x *ManagedProcessRecord) GetShell() string { - if x != nil { - return x.Shell - } - return "" -} - -func (x *ManagedProcessRecord) GetPid() uint32 { - if x != nil { - return x.Pid - } - return 0 -} - -func (x *ManagedProcessRecord) GetLogPath() string { - if x != nil { - return x.LogPath - } - return "" -} - -func (x *ManagedProcessRecord) GetStartedAt() int64 { - if x != nil { - return x.StartedAt - } - return 0 -} - -func (x *ManagedProcessRecord) GetFinishedAt() int64 { - if x != nil && x.FinishedAt != nil { - return *x.FinishedAt - } - return 0 -} - -func (x *ManagedProcessRecord) GetExitCode() int32 { - if x != nil && x.ExitCode != nil { - return *x.ExitCode - } - return 0 -} - -func (x *ManagedProcessRecord) GetRunning() bool { - if x != nil { - return x.Running - } - return false -} - -func (x *ManagedProcessRecord) GetIsolated() bool { - if x != nil { - return x.Isolated - } - return false -} - -func (x *ManagedProcessRecord) GetRestored() bool { - if x != nil { - return x.Restored - } - return false -} - -type ManagedProcessSnapshot struct { - state protoimpl.MessageState `protogen:"open.v1"` - Processes []*ManagedProcessRecord `protobuf:"bytes,1,rep,name=processes,proto3" json:"processes,omitempty"` - Revision uint64 `protobuf:"varint,2,opt,name=revision,proto3" json:"revision,omitempty"` // agent-side monotonic, restart-safe - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ManagedProcessSnapshot) Reset() { - *x = ManagedProcessSnapshot{} - mi := &file_proto_v2_gateway_proto_msgTypes[24] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ManagedProcessSnapshot) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ManagedProcessSnapshot) ProtoMessage() {} - -func (x *ManagedProcessSnapshot) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[24] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ManagedProcessSnapshot.ProtoReflect.Descriptor instead. -func (*ManagedProcessSnapshot) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{24} -} - -func (x *ManagedProcessSnapshot) GetProcesses() []*ManagedProcessRecord { - if x != nil { - return x.Processes - } - return nil -} - -func (x *ManagedProcessSnapshot) GetRevision() uint64 { - if x != nil { - return x.Revision - } - return 0 -} - -type ManagedProcessRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Action string `protobuf:"bytes,1,opt,name=action,proto3" json:"action,omitempty"` // "snapshot" | "stop" | "read_log" | "clear" - ProcessId string `protobuf:"bytes,2,opt,name=process_id,json=processId,proto3" json:"process_id,omitempty"` // stop/read_log; optional for clear - MaxBytes uint32 `protobuf:"varint,3,opt,name=max_bytes,json=maxBytes,proto3" json:"max_bytes,omitempty"` // read_log only; 0 = default - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ManagedProcessRequest) Reset() { - *x = ManagedProcessRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[25] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ManagedProcessRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ManagedProcessRequest) ProtoMessage() {} - -func (x *ManagedProcessRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[25] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ManagedProcessRequest.ProtoReflect.Descriptor instead. -func (*ManagedProcessRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{25} -} - -func (x *ManagedProcessRequest) GetAction() string { - if x != nil { - return x.Action - } - return "" -} - -func (x *ManagedProcessRequest) GetProcessId() string { - if x != nil { - return x.ProcessId - } - return "" -} - -func (x *ManagedProcessRequest) GetMaxBytes() uint32 { - if x != nil { - return x.MaxBytes - } - return 0 -} - -type ManagedProcessResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Action string `protobuf:"bytes,1,opt,name=action,proto3" json:"action,omitempty"` - Snapshot *ManagedProcessSnapshot `protobuf:"bytes,2,opt,name=snapshot,proto3" json:"snapshot,omitempty"` // set for snapshot/stop/clear - LogContent string `protobuf:"bytes,3,opt,name=log_content,json=logContent,proto3" json:"log_content,omitempty"` // read_log only - LogPath string `protobuf:"bytes,4,opt,name=log_path,json=logPath,proto3" json:"log_path,omitempty"` // read_log only - LogTruncated bool `protobuf:"varint,5,opt,name=log_truncated,json=logTruncated,proto3" json:"log_truncated,omitempty"` // read_log only - Stopped bool `protobuf:"varint,6,opt,name=stopped,proto3" json:"stopped,omitempty"` // stop only - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ManagedProcessResponse) Reset() { - *x = ManagedProcessResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[26] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ManagedProcessResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ManagedProcessResponse) ProtoMessage() {} - -func (x *ManagedProcessResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[26] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ManagedProcessResponse.ProtoReflect.Descriptor instead. -func (*ManagedProcessResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{26} -} - -func (x *ManagedProcessResponse) GetAction() string { - if x != nil { - return x.Action - } - return "" -} - -func (x *ManagedProcessResponse) GetSnapshot() *ManagedProcessSnapshot { - if x != nil { - return x.Snapshot - } - return nil -} - -func (x *ManagedProcessResponse) GetLogContent() string { - if x != nil { - return x.LogContent - } - return "" -} - -func (x *ManagedProcessResponse) GetLogPath() string { - if x != nil { - return x.LogPath - } - return "" -} - -func (x *ManagedProcessResponse) GetLogTruncated() bool { - if x != nil { - return x.LogTruncated - } - return false -} - -func (x *ManagedProcessResponse) GetStopped() bool { - if x != nil { - return x.Stopped - } - return false -} - -type MemoryManageRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Command string `protobuf:"bytes,1,opt,name=command,proto3" json:"command,omitempty"` - ArgsJson string `protobuf:"bytes,2,opt,name=args_json,json=argsJson,proto3" json:"args_json,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *MemoryManageRequest) Reset() { - *x = MemoryManageRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[27] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *MemoryManageRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*MemoryManageRequest) ProtoMessage() {} - -func (x *MemoryManageRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[27] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use MemoryManageRequest.ProtoReflect.Descriptor instead. -func (*MemoryManageRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{27} -} - -func (x *MemoryManageRequest) GetCommand() string { - if x != nil { - return x.Command - } - return "" -} - -func (x *MemoryManageRequest) GetArgsJson() string { - if x != nil { - return x.ArgsJson - } - return "" -} - -type MemoryManageResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - ResultJson string `protobuf:"bytes,1,opt,name=result_json,json=resultJson,proto3" json:"result_json,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *MemoryManageResponse) Reset() { - *x = MemoryManageResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[28] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *MemoryManageResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*MemoryManageResponse) ProtoMessage() {} - -func (x *MemoryManageResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[28] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use MemoryManageResponse.ProtoReflect.Descriptor instead. -func (*MemoryManageResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{28} -} - -func (x *MemoryManageResponse) GetResultJson() string { - if x != nil { - return x.ResultJson - } - return "" -} - -type TerminalRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Action string `protobuf:"bytes,1,opt,name=action,proto3" json:"action,omitempty"` - SessionId string `protobuf:"bytes,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` - ProjectPathKey string `protobuf:"bytes,3,opt,name=project_path_key,json=projectPathKey,proto3" json:"project_path_key,omitempty"` - Cwd string `protobuf:"bytes,4,opt,name=cwd,proto3" json:"cwd,omitempty"` - Shell string `protobuf:"bytes,5,opt,name=shell,proto3" json:"shell,omitempty"` - Title string `protobuf:"bytes,6,opt,name=title,proto3" json:"title,omitempty"` - Data string `protobuf:"bytes,7,opt,name=data,proto3" json:"data,omitempty"` - Cols uint32 `protobuf:"varint,8,opt,name=cols,proto3" json:"cols,omitempty"` - Rows uint32 `protobuf:"varint,9,opt,name=rows,proto3" json:"rows,omitempty"` - MaxBytes uint32 `protobuf:"varint,10,opt,name=max_bytes,json=maxBytes,proto3" json:"max_bytes,omitempty"` - SshHostId string `protobuf:"bytes,11,opt,name=ssh_host_id,json=sshHostId,proto3" json:"ssh_host_id,omitempty"` - PromptId string `protobuf:"bytes,12,opt,name=prompt_id,json=promptId,proto3" json:"prompt_id,omitempty"` - PromptAnswer string `protobuf:"bytes,13,opt,name=prompt_answer,json=promptAnswer,proto3" json:"prompt_answer,omitempty"` - TrustHostKey bool `protobuf:"varint,14,opt,name=trust_host_key,json=trustHostKey,proto3" json:"trust_host_key,omitempty"` - SftpEnabled bool `protobuf:"varint,15,opt,name=sftp_enabled,json=sftpEnabled,proto3" json:"sftp_enabled,omitempty"` - TabId string `protobuf:"bytes,16,opt,name=tab_id,json=tabId,proto3" json:"tab_id,omitempty"` - TabKind string `protobuf:"bytes,17,opt,name=tab_kind,json=tabKind,proto3" json:"tab_kind,omitempty"` - RemoteHost string `protobuf:"bytes,18,opt,name=remote_host,json=remoteHost,proto3" json:"remote_host,omitempty"` - RemotePort uint32 `protobuf:"varint,19,opt,name=remote_port,json=remotePort,proto3" json:"remote_port,omitempty"` - LocalPort uint32 `protobuf:"varint,20,opt,name=local_port,json=localPort,proto3" json:"local_port,omitempty"` - ForwardId string `protobuf:"bytes,21,opt,name=forward_id,json=forwardId,proto3" json:"forward_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TerminalRequest) Reset() { - *x = TerminalRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[29] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TerminalRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TerminalRequest) ProtoMessage() {} - -func (x *TerminalRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[29] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TerminalRequest.ProtoReflect.Descriptor instead. -func (*TerminalRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{29} -} - -func (x *TerminalRequest) GetAction() string { - if x != nil { - return x.Action - } - return "" -} - -func (x *TerminalRequest) GetSessionId() string { - if x != nil { - return x.SessionId - } - return "" -} - -func (x *TerminalRequest) GetProjectPathKey() string { - if x != nil { - return x.ProjectPathKey - } - return "" -} - -func (x *TerminalRequest) GetCwd() string { - if x != nil { - return x.Cwd - } - return "" -} - -func (x *TerminalRequest) GetShell() string { - if x != nil { - return x.Shell - } - return "" -} - -func (x *TerminalRequest) GetTitle() string { - if x != nil { - return x.Title - } - return "" -} - -func (x *TerminalRequest) GetData() string { - if x != nil { - return x.Data - } - return "" -} - -func (x *TerminalRequest) GetCols() uint32 { - if x != nil { - return x.Cols - } - return 0 -} - -func (x *TerminalRequest) GetRows() uint32 { - if x != nil { - return x.Rows - } - return 0 -} - -func (x *TerminalRequest) GetMaxBytes() uint32 { - if x != nil { - return x.MaxBytes - } - return 0 -} - -func (x *TerminalRequest) GetSshHostId() string { - if x != nil { - return x.SshHostId - } - return "" -} - -func (x *TerminalRequest) GetPromptId() string { - if x != nil { - return x.PromptId - } - return "" -} - -func (x *TerminalRequest) GetPromptAnswer() string { - if x != nil { - return x.PromptAnswer - } - return "" -} - -func (x *TerminalRequest) GetTrustHostKey() bool { - if x != nil { - return x.TrustHostKey - } - return false -} - -func (x *TerminalRequest) GetSftpEnabled() bool { - if x != nil { - return x.SftpEnabled - } - return false -} - -func (x *TerminalRequest) GetTabId() string { - if x != nil { - return x.TabId - } - return "" -} - -func (x *TerminalRequest) GetTabKind() string { - if x != nil { - return x.TabKind - } - return "" -} - -func (x *TerminalRequest) GetRemoteHost() string { - if x != nil { - return x.RemoteHost - } - return "" -} - -func (x *TerminalRequest) GetRemotePort() uint32 { - if x != nil { - return x.RemotePort - } - return 0 -} - -func (x *TerminalRequest) GetLocalPort() uint32 { - if x != nil { - return x.LocalPort - } - return 0 -} - -func (x *TerminalRequest) GetForwardId() string { - if x != nil { - return x.ForwardId - } - return "" -} - -type TerminalSession struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - ProjectPathKey string `protobuf:"bytes,2,opt,name=project_path_key,json=projectPathKey,proto3" json:"project_path_key,omitempty"` - Cwd string `protobuf:"bytes,3,opt,name=cwd,proto3" json:"cwd,omitempty"` - Shell string `protobuf:"bytes,4,opt,name=shell,proto3" json:"shell,omitempty"` - Title string `protobuf:"bytes,5,opt,name=title,proto3" json:"title,omitempty"` - Pid uint32 `protobuf:"varint,6,opt,name=pid,proto3" json:"pid,omitempty"` - Cols uint32 `protobuf:"varint,7,opt,name=cols,proto3" json:"cols,omitempty"` - Rows uint32 `protobuf:"varint,8,opt,name=rows,proto3" json:"rows,omitempty"` - CreatedAt uint64 `protobuf:"varint,9,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` - UpdatedAt uint64 `protobuf:"varint,10,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` - FinishedAt uint64 `protobuf:"varint,11,opt,name=finished_at,json=finishedAt,proto3" json:"finished_at,omitempty"` - ExitCode int32 `protobuf:"varint,12,opt,name=exit_code,json=exitCode,proto3" json:"exit_code,omitempty"` - Running bool `protobuf:"varint,13,opt,name=running,proto3" json:"running,omitempty"` - Kind string `protobuf:"bytes,14,opt,name=kind,proto3" json:"kind,omitempty"` - Ssh *TerminalSshMetadata `protobuf:"bytes,15,opt,name=ssh,proto3" json:"ssh,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TerminalSession) Reset() { - *x = TerminalSession{} - mi := &file_proto_v2_gateway_proto_msgTypes[30] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TerminalSession) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TerminalSession) ProtoMessage() {} - -func (x *TerminalSession) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[30] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TerminalSession.ProtoReflect.Descriptor instead. -func (*TerminalSession) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{30} -} - -func (x *TerminalSession) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *TerminalSession) GetProjectPathKey() string { - if x != nil { - return x.ProjectPathKey - } - return "" -} - -func (x *TerminalSession) GetCwd() string { - if x != nil { - return x.Cwd - } - return "" -} - -func (x *TerminalSession) GetShell() string { - if x != nil { - return x.Shell - } - return "" -} - -func (x *TerminalSession) GetTitle() string { - if x != nil { - return x.Title - } - return "" -} - -func (x *TerminalSession) GetPid() uint32 { - if x != nil { - return x.Pid - } - return 0 -} - -func (x *TerminalSession) GetCols() uint32 { - if x != nil { - return x.Cols - } - return 0 -} - -func (x *TerminalSession) GetRows() uint32 { - if x != nil { - return x.Rows - } - return 0 -} - -func (x *TerminalSession) GetCreatedAt() uint64 { - if x != nil { - return x.CreatedAt - } - return 0 -} - -func (x *TerminalSession) GetUpdatedAt() uint64 { - if x != nil { - return x.UpdatedAt - } - return 0 -} - -func (x *TerminalSession) GetFinishedAt() uint64 { - if x != nil { - return x.FinishedAt - } - return 0 -} - -func (x *TerminalSession) GetExitCode() int32 { - if x != nil { - return x.ExitCode - } - return 0 -} - -func (x *TerminalSession) GetRunning() bool { - if x != nil { - return x.Running - } - return false -} - -func (x *TerminalSession) GetKind() string { - if x != nil { - return x.Kind - } - return "" -} - -func (x *TerminalSession) GetSsh() *TerminalSshMetadata { - if x != nil { - return x.Ssh - } - return nil -} - -type TerminalSshMetadata struct { - state protoimpl.MessageState `protogen:"open.v1"` - HostId string `protobuf:"bytes,1,opt,name=host_id,json=hostId,proto3" json:"host_id,omitempty"` - HostName string `protobuf:"bytes,2,opt,name=host_name,json=hostName,proto3" json:"host_name,omitempty"` - Username string `protobuf:"bytes,3,opt,name=username,proto3" json:"username,omitempty"` - Host string `protobuf:"bytes,4,opt,name=host,proto3" json:"host,omitempty"` - Port uint32 `protobuf:"varint,5,opt,name=port,proto3" json:"port,omitempty"` - AuthType string `protobuf:"bytes,6,opt,name=auth_type,json=authType,proto3" json:"auth_type,omitempty"` - Status string `protobuf:"bytes,7,opt,name=status,proto3" json:"status,omitempty"` - ReconnectAttempt uint32 `protobuf:"varint,8,opt,name=reconnect_attempt,json=reconnectAttempt,proto3" json:"reconnect_attempt,omitempty"` - ReconnectMaxAttempts uint32 `protobuf:"varint,9,opt,name=reconnect_max_attempts,json=reconnectMaxAttempts,proto3" json:"reconnect_max_attempts,omitempty"` - SftpEnabled bool `protobuf:"varint,10,opt,name=sftp_enabled,json=sftpEnabled,proto3" json:"sftp_enabled,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TerminalSshMetadata) Reset() { - *x = TerminalSshMetadata{} - mi := &file_proto_v2_gateway_proto_msgTypes[31] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TerminalSshMetadata) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TerminalSshMetadata) ProtoMessage() {} - -func (x *TerminalSshMetadata) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[31] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TerminalSshMetadata.ProtoReflect.Descriptor instead. -func (*TerminalSshMetadata) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{31} -} - -func (x *TerminalSshMetadata) GetHostId() string { - if x != nil { - return x.HostId - } - return "" -} - -func (x *TerminalSshMetadata) GetHostName() string { - if x != nil { - return x.HostName - } - return "" -} - -func (x *TerminalSshMetadata) GetUsername() string { - if x != nil { - return x.Username - } - return "" -} - -func (x *TerminalSshMetadata) GetHost() string { - if x != nil { - return x.Host - } - return "" -} - -func (x *TerminalSshMetadata) GetPort() uint32 { - if x != nil { - return x.Port - } - return 0 -} - -func (x *TerminalSshMetadata) GetAuthType() string { - if x != nil { - return x.AuthType - } - return "" -} - -func (x *TerminalSshMetadata) GetStatus() string { - if x != nil { - return x.Status - } - return "" -} - -func (x *TerminalSshMetadata) GetReconnectAttempt() uint32 { - if x != nil { - return x.ReconnectAttempt - } - return 0 -} - -func (x *TerminalSshMetadata) GetReconnectMaxAttempts() uint32 { - if x != nil { - return x.ReconnectMaxAttempts - } - return 0 -} - -func (x *TerminalSshMetadata) GetSftpEnabled() bool { - if x != nil { - return x.SftpEnabled - } - return false -} - -type SftpRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Action string `protobuf:"bytes,1,opt,name=action,proto3" json:"action,omitempty"` - SessionId string `protobuf:"bytes,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` - ProjectPathKey string `protobuf:"bytes,3,opt,name=project_path_key,json=projectPathKey,proto3" json:"project_path_key,omitempty"` - Workdir string `protobuf:"bytes,4,opt,name=workdir,proto3" json:"workdir,omitempty"` - LocalPath string `protobuf:"bytes,5,opt,name=local_path,json=localPath,proto3" json:"local_path,omitempty"` - RemotePath string `protobuf:"bytes,6,opt,name=remote_path,json=remotePath,proto3" json:"remote_path,omitempty"` - FromPath string `protobuf:"bytes,7,opt,name=from_path,json=fromPath,proto3" json:"from_path,omitempty"` - ToPath string `protobuf:"bytes,8,opt,name=to_path,json=toPath,proto3" json:"to_path,omitempty"` - Direction string `protobuf:"bytes,9,opt,name=direction,proto3" json:"direction,omitempty"` - TargetPath string `protobuf:"bytes,10,opt,name=target_path,json=targetPath,proto3" json:"target_path,omitempty"` - Recursive bool `protobuf:"varint,11,opt,name=recursive,proto3" json:"recursive,omitempty"` - Overwrite bool `protobuf:"varint,12,opt,name=overwrite,proto3" json:"overwrite,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SftpRequest) Reset() { - *x = SftpRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[32] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SftpRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SftpRequest) ProtoMessage() {} - -func (x *SftpRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[32] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SftpRequest.ProtoReflect.Descriptor instead. -func (*SftpRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{32} -} - -func (x *SftpRequest) GetAction() string { - if x != nil { - return x.Action - } - return "" -} - -func (x *SftpRequest) GetSessionId() string { - if x != nil { - return x.SessionId - } - return "" -} - -func (x *SftpRequest) GetProjectPathKey() string { - if x != nil { - return x.ProjectPathKey - } - return "" -} - -func (x *SftpRequest) GetWorkdir() string { - if x != nil { - return x.Workdir - } - return "" -} - -func (x *SftpRequest) GetLocalPath() string { - if x != nil { - return x.LocalPath - } - return "" -} - -func (x *SftpRequest) GetRemotePath() string { - if x != nil { - return x.RemotePath - } - return "" -} - -func (x *SftpRequest) GetFromPath() string { - if x != nil { - return x.FromPath - } - return "" -} - -func (x *SftpRequest) GetToPath() string { - if x != nil { - return x.ToPath - } - return "" -} - -func (x *SftpRequest) GetDirection() string { - if x != nil { - return x.Direction - } - return "" -} - -func (x *SftpRequest) GetTargetPath() string { - if x != nil { - return x.TargetPath - } - return "" -} - -func (x *SftpRequest) GetRecursive() bool { - if x != nil { - return x.Recursive - } - return false -} - -func (x *SftpRequest) GetOverwrite() bool { - if x != nil { - return x.Overwrite - } - return false -} - -type SftpEntry struct { - state protoimpl.MessageState `protogen:"open.v1"` - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - Kind string `protobuf:"bytes,3,opt,name=kind,proto3" json:"kind,omitempty"` - SizeBytes uint64 `protobuf:"varint,4,opt,name=size_bytes,json=sizeBytes,proto3" json:"size_bytes,omitempty"` - Mtime uint64 `protobuf:"varint,5,opt,name=mtime,proto3" json:"mtime,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SftpEntry) Reset() { - *x = SftpEntry{} - mi := &file_proto_v2_gateway_proto_msgTypes[33] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SftpEntry) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SftpEntry) ProtoMessage() {} - -func (x *SftpEntry) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[33] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SftpEntry.ProtoReflect.Descriptor instead. -func (*SftpEntry) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{33} -} - -func (x *SftpEntry) GetPath() string { - if x != nil { - return x.Path - } - return "" -} - -func (x *SftpEntry) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *SftpEntry) GetKind() string { - if x != nil { - return x.Kind - } - return "" -} - -func (x *SftpEntry) GetSizeBytes() uint64 { - if x != nil { - return x.SizeBytes - } - return 0 -} - -func (x *SftpEntry) GetMtime() uint64 { - if x != nil { - return x.Mtime - } - return 0 -} - -type SftpTransfer struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - SessionId string `protobuf:"bytes,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` - Direction string `protobuf:"bytes,3,opt,name=direction,proto3" json:"direction,omitempty"` - Status string `protobuf:"bytes,4,opt,name=status,proto3" json:"status,omitempty"` - SourcePath string `protobuf:"bytes,5,opt,name=source_path,json=sourcePath,proto3" json:"source_path,omitempty"` - TargetPath string `protobuf:"bytes,6,opt,name=target_path,json=targetPath,proto3" json:"target_path,omitempty"` - CurrentPath string `protobuf:"bytes,7,opt,name=current_path,json=currentPath,proto3" json:"current_path,omitempty"` - BytesDone uint64 `protobuf:"varint,8,opt,name=bytes_done,json=bytesDone,proto3" json:"bytes_done,omitempty"` - BytesTotal uint64 `protobuf:"varint,9,opt,name=bytes_total,json=bytesTotal,proto3" json:"bytes_total,omitempty"` - FilesDone uint32 `protobuf:"varint,10,opt,name=files_done,json=filesDone,proto3" json:"files_done,omitempty"` - FilesTotal uint32 `protobuf:"varint,11,opt,name=files_total,json=filesTotal,proto3" json:"files_total,omitempty"` - Error string `protobuf:"bytes,12,opt,name=error,proto3" json:"error,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SftpTransfer) Reset() { - *x = SftpTransfer{} - mi := &file_proto_v2_gateway_proto_msgTypes[34] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SftpTransfer) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SftpTransfer) ProtoMessage() {} - -func (x *SftpTransfer) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[34] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SftpTransfer.ProtoReflect.Descriptor instead. -func (*SftpTransfer) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{34} -} - -func (x *SftpTransfer) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *SftpTransfer) GetSessionId() string { - if x != nil { - return x.SessionId - } - return "" -} - -func (x *SftpTransfer) GetDirection() string { - if x != nil { - return x.Direction - } - return "" -} - -func (x *SftpTransfer) GetStatus() string { - if x != nil { - return x.Status - } - return "" -} - -func (x *SftpTransfer) GetSourcePath() string { - if x != nil { - return x.SourcePath - } - return "" -} - -func (x *SftpTransfer) GetTargetPath() string { - if x != nil { - return x.TargetPath - } - return "" -} - -func (x *SftpTransfer) GetCurrentPath() string { - if x != nil { - return x.CurrentPath - } - return "" -} - -func (x *SftpTransfer) GetBytesDone() uint64 { - if x != nil { - return x.BytesDone - } - return 0 -} - -func (x *SftpTransfer) GetBytesTotal() uint64 { - if x != nil { - return x.BytesTotal - } - return 0 -} - -func (x *SftpTransfer) GetFilesDone() uint32 { - if x != nil { - return x.FilesDone - } - return 0 -} - -func (x *SftpTransfer) GetFilesTotal() uint32 { - if x != nil { - return x.FilesTotal - } - return 0 -} - -func (x *SftpTransfer) GetError() string { - if x != nil { - return x.Error - } - return "" -} - -type SftpResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Action string `protobuf:"bytes,1,opt,name=action,proto3" json:"action,omitempty"` - Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` - Entries []*SftpEntry `protobuf:"bytes,3,rep,name=entries,proto3" json:"entries,omitempty"` - Entry *SftpEntry `protobuf:"bytes,4,opt,name=entry,proto3" json:"entry,omitempty"` - Exists bool `protobuf:"varint,5,opt,name=exists,proto3" json:"exists,omitempty"` - Transfer *SftpTransfer `protobuf:"bytes,6,opt,name=transfer,proto3" json:"transfer,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SftpResponse) Reset() { - *x = SftpResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[35] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SftpResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SftpResponse) ProtoMessage() {} - -func (x *SftpResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[35] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SftpResponse.ProtoReflect.Descriptor instead. -func (*SftpResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{35} -} - -func (x *SftpResponse) GetAction() string { - if x != nil { - return x.Action - } - return "" -} - -func (x *SftpResponse) GetPath() string { - if x != nil { - return x.Path - } - return "" -} - -func (x *SftpResponse) GetEntries() []*SftpEntry { - if x != nil { - return x.Entries - } - return nil -} - -func (x *SftpResponse) GetEntry() *SftpEntry { - if x != nil { - return x.Entry - } - return nil -} - -func (x *SftpResponse) GetExists() bool { - if x != nil { - return x.Exists - } - return false -} - -func (x *SftpResponse) GetTransfer() *SftpTransfer { - if x != nil { - return x.Transfer - } - return nil -} - -type SftpEvent struct { - state protoimpl.MessageState `protogen:"open.v1"` - Kind string `protobuf:"bytes,1,opt,name=kind,proto3" json:"kind,omitempty"` - Transfer *SftpTransfer `protobuf:"bytes,2,opt,name=transfer,proto3" json:"transfer,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SftpEvent) Reset() { - *x = SftpEvent{} - mi := &file_proto_v2_gateway_proto_msgTypes[36] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SftpEvent) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SftpEvent) ProtoMessage() {} - -func (x *SftpEvent) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[36] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SftpEvent.ProtoReflect.Descriptor instead. -func (*SftpEvent) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{36} -} - -func (x *SftpEvent) GetKind() string { - if x != nil { - return x.Kind - } - return "" -} - -func (x *SftpEvent) GetTransfer() *SftpTransfer { - if x != nil { - return x.Transfer - } - return nil -} - -type TerminalSshPrompt struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Kind string `protobuf:"bytes,2,opt,name=kind,proto3" json:"kind,omitempty"` - HostId string `protobuf:"bytes,3,opt,name=host_id,json=hostId,proto3" json:"host_id,omitempty"` - HostName string `protobuf:"bytes,4,opt,name=host_name,json=hostName,proto3" json:"host_name,omitempty"` - Host string `protobuf:"bytes,5,opt,name=host,proto3" json:"host,omitempty"` - Port uint32 `protobuf:"varint,6,opt,name=port,proto3" json:"port,omitempty"` - Message string `protobuf:"bytes,7,opt,name=message,proto3" json:"message,omitempty"` - FingerprintSha256 string `protobuf:"bytes,8,opt,name=fingerprint_sha256,json=fingerprintSha256,proto3" json:"fingerprint_sha256,omitempty"` - KeyType string `protobuf:"bytes,9,opt,name=key_type,json=keyType,proto3" json:"key_type,omitempty"` - AnswerEcho bool `protobuf:"varint,10,opt,name=answer_echo,json=answerEcho,proto3" json:"answer_echo,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TerminalSshPrompt) Reset() { - *x = TerminalSshPrompt{} - mi := &file_proto_v2_gateway_proto_msgTypes[37] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TerminalSshPrompt) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TerminalSshPrompt) ProtoMessage() {} - -func (x *TerminalSshPrompt) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[37] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TerminalSshPrompt.ProtoReflect.Descriptor instead. -func (*TerminalSshPrompt) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{37} -} - -func (x *TerminalSshPrompt) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *TerminalSshPrompt) GetKind() string { - if x != nil { - return x.Kind - } - return "" -} - -func (x *TerminalSshPrompt) GetHostId() string { - if x != nil { - return x.HostId - } - return "" -} - -func (x *TerminalSshPrompt) GetHostName() string { - if x != nil { - return x.HostName - } - return "" -} - -func (x *TerminalSshPrompt) GetHost() string { - if x != nil { - return x.Host - } - return "" -} - -func (x *TerminalSshPrompt) GetPort() uint32 { - if x != nil { - return x.Port - } - return 0 -} - -func (x *TerminalSshPrompt) GetMessage() string { - if x != nil { - return x.Message - } - return "" -} - -func (x *TerminalSshPrompt) GetFingerprintSha256() string { - if x != nil { - return x.FingerprintSha256 - } - return "" -} - -func (x *TerminalSshPrompt) GetKeyType() string { - if x != nil { - return x.KeyType - } - return "" -} - -func (x *TerminalSshPrompt) GetAnswerEcho() bool { - if x != nil { - return x.AnswerEcho - } - return false -} - -type TerminalShellOption struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Label string `protobuf:"bytes,2,opt,name=label,proto3" json:"label,omitempty"` - Command string `protobuf:"bytes,3,opt,name=command,proto3" json:"command,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TerminalShellOption) Reset() { - *x = TerminalShellOption{} - mi := &file_proto_v2_gateway_proto_msgTypes[38] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TerminalShellOption) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TerminalShellOption) ProtoMessage() {} - -func (x *TerminalShellOption) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[38] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TerminalShellOption.ProtoReflect.Descriptor instead. -func (*TerminalShellOption) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{38} -} - -func (x *TerminalShellOption) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *TerminalShellOption) GetLabel() string { - if x != nil { - return x.Label - } - return "" -} - -func (x *TerminalShellOption) GetCommand() string { - if x != nil { - return x.Command - } - return "" -} - -type TerminalSshTab struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - SessionId string `protobuf:"bytes,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` - ProjectPathKey string `protobuf:"bytes,3,opt,name=project_path_key,json=projectPathKey,proto3" json:"project_path_key,omitempty"` - Kind string `protobuf:"bytes,4,opt,name=kind,proto3" json:"kind,omitempty"` - CreatedAt uint64 `protobuf:"varint,5,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` - UpdatedAt uint64 `protobuf:"varint,6,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TerminalSshTab) Reset() { - *x = TerminalSshTab{} - mi := &file_proto_v2_gateway_proto_msgTypes[39] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TerminalSshTab) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TerminalSshTab) ProtoMessage() {} - -func (x *TerminalSshTab) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[39] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TerminalSshTab.ProtoReflect.Descriptor instead. -func (*TerminalSshTab) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{39} -} - -func (x *TerminalSshTab) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *TerminalSshTab) GetSessionId() string { - if x != nil { - return x.SessionId - } - return "" -} - -func (x *TerminalSshTab) GetProjectPathKey() string { - if x != nil { - return x.ProjectPathKey - } - return "" -} - -func (x *TerminalSshTab) GetKind() string { - if x != nil { - return x.Kind - } - return "" -} - -func (x *TerminalSshTab) GetCreatedAt() uint64 { - if x != nil { - return x.CreatedAt - } - return 0 -} - -func (x *TerminalSshTab) GetUpdatedAt() uint64 { - if x != nil { - return x.UpdatedAt - } - return 0 -} - -type TerminalSshTabsSnapshot struct { - state protoimpl.MessageState `protogen:"open.v1"` - ProjectPathKey string `protobuf:"bytes,1,opt,name=project_path_key,json=projectPathKey,proto3" json:"project_path_key,omitempty"` - Tabs []*TerminalSshTab `protobuf:"bytes,2,rep,name=tabs,proto3" json:"tabs,omitempty"` - Revision uint64 `protobuf:"varint,4,opt,name=revision,proto3" json:"revision,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TerminalSshTabsSnapshot) Reset() { - *x = TerminalSshTabsSnapshot{} - mi := &file_proto_v2_gateway_proto_msgTypes[40] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TerminalSshTabsSnapshot) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TerminalSshTabsSnapshot) ProtoMessage() {} - -func (x *TerminalSshTabsSnapshot) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[40] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TerminalSshTabsSnapshot.ProtoReflect.Descriptor instead. -func (*TerminalSshTabsSnapshot) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{40} -} - -func (x *TerminalSshTabsSnapshot) GetProjectPathKey() string { - if x != nil { - return x.ProjectPathKey - } - return "" -} - -func (x *TerminalSshTabsSnapshot) GetTabs() []*TerminalSshTab { - if x != nil { - return x.Tabs - } - return nil -} - -func (x *TerminalSshTabsSnapshot) GetRevision() uint64 { - if x != nil { - return x.Revision - } - return 0 -} - -type TerminalSshLocalForward struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - SessionId string `protobuf:"bytes,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` - ProjectPathKey string `protobuf:"bytes,3,opt,name=project_path_key,json=projectPathKey,proto3" json:"project_path_key,omitempty"` - LocalHost string `protobuf:"bytes,4,opt,name=local_host,json=localHost,proto3" json:"local_host,omitempty"` - LocalPort uint32 `protobuf:"varint,5,opt,name=local_port,json=localPort,proto3" json:"local_port,omitempty"` - Address string `protobuf:"bytes,6,opt,name=address,proto3" json:"address,omitempty"` - RemoteHost string `protobuf:"bytes,7,opt,name=remote_host,json=remoteHost,proto3" json:"remote_host,omitempty"` - RemotePort uint32 `protobuf:"varint,8,opt,name=remote_port,json=remotePort,proto3" json:"remote_port,omitempty"` - Status string `protobuf:"bytes,9,opt,name=status,proto3" json:"status,omitempty"` - CreatedAt uint64 `protobuf:"varint,10,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` - UpdatedAt uint64 `protobuf:"varint,11,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` - Error string `protobuf:"bytes,12,opt,name=error,proto3" json:"error,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TerminalSshLocalForward) Reset() { - *x = TerminalSshLocalForward{} - mi := &file_proto_v2_gateway_proto_msgTypes[41] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TerminalSshLocalForward) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TerminalSshLocalForward) ProtoMessage() {} - -func (x *TerminalSshLocalForward) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[41] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TerminalSshLocalForward.ProtoReflect.Descriptor instead. -func (*TerminalSshLocalForward) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{41} -} - -func (x *TerminalSshLocalForward) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *TerminalSshLocalForward) GetSessionId() string { - if x != nil { - return x.SessionId - } - return "" -} - -func (x *TerminalSshLocalForward) GetProjectPathKey() string { - if x != nil { - return x.ProjectPathKey - } - return "" -} - -func (x *TerminalSshLocalForward) GetLocalHost() string { - if x != nil { - return x.LocalHost - } - return "" -} - -func (x *TerminalSshLocalForward) GetLocalPort() uint32 { - if x != nil { - return x.LocalPort - } - return 0 -} - -func (x *TerminalSshLocalForward) GetAddress() string { - if x != nil { - return x.Address - } - return "" -} - -func (x *TerminalSshLocalForward) GetRemoteHost() string { - if x != nil { - return x.RemoteHost - } - return "" -} - -func (x *TerminalSshLocalForward) GetRemotePort() uint32 { - if x != nil { - return x.RemotePort - } - return 0 -} - -func (x *TerminalSshLocalForward) GetStatus() string { - if x != nil { - return x.Status - } - return "" -} - -func (x *TerminalSshLocalForward) GetCreatedAt() uint64 { - if x != nil { - return x.CreatedAt - } - return 0 -} - -func (x *TerminalSshLocalForward) GetUpdatedAt() uint64 { - if x != nil { - return x.UpdatedAt - } - return 0 -} - -func (x *TerminalSshLocalForward) GetError() string { - if x != nil { - return x.Error - } - return "" -} - -type TerminalSshLocalForwardsSnapshot struct { - state protoimpl.MessageState `protogen:"open.v1"` - Forwards []*TerminalSshLocalForward `protobuf:"bytes,1,rep,name=forwards,proto3" json:"forwards,omitempty"` - Revision uint64 `protobuf:"varint,2,opt,name=revision,proto3" json:"revision,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TerminalSshLocalForwardsSnapshot) Reset() { - *x = TerminalSshLocalForwardsSnapshot{} - mi := &file_proto_v2_gateway_proto_msgTypes[42] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TerminalSshLocalForwardsSnapshot) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TerminalSshLocalForwardsSnapshot) ProtoMessage() {} - -func (x *TerminalSshLocalForwardsSnapshot) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[42] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TerminalSshLocalForwardsSnapshot.ProtoReflect.Descriptor instead. -func (*TerminalSshLocalForwardsSnapshot) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{42} -} - -func (x *TerminalSshLocalForwardsSnapshot) GetForwards() []*TerminalSshLocalForward { - if x != nil { - return x.Forwards - } - return nil -} - -func (x *TerminalSshLocalForwardsSnapshot) GetRevision() uint64 { - if x != nil { - return x.Revision - } - return 0 -} - -type TerminalSshLocalForwardAction struct { - state protoimpl.MessageState `protogen:"open.v1"` - Forward *TerminalSshLocalForward `protobuf:"bytes,1,opt,name=forward,proto3" json:"forward,omitempty"` - Revision uint64 `protobuf:"varint,2,opt,name=revision,proto3" json:"revision,omitempty"` - // Only set on events: started | stopped | failed. - Kind string `protobuf:"bytes,3,opt,name=kind,proto3" json:"kind,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TerminalSshLocalForwardAction) Reset() { - *x = TerminalSshLocalForwardAction{} - mi := &file_proto_v2_gateway_proto_msgTypes[43] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TerminalSshLocalForwardAction) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TerminalSshLocalForwardAction) ProtoMessage() {} - -func (x *TerminalSshLocalForwardAction) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[43] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TerminalSshLocalForwardAction.ProtoReflect.Descriptor instead. -func (*TerminalSshLocalForwardAction) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{43} -} - -func (x *TerminalSshLocalForwardAction) GetForward() *TerminalSshLocalForward { - if x != nil { - return x.Forward - } - return nil -} - -func (x *TerminalSshLocalForwardAction) GetRevision() uint64 { - if x != nil { - return x.Revision - } - return 0 -} - -func (x *TerminalSshLocalForwardAction) GetKind() string { - if x != nil { - return x.Kind - } - return "" -} - -type TerminalResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Action string `protobuf:"bytes,1,opt,name=action,proto3" json:"action,omitempty"` - Sessions []*TerminalSession `protobuf:"bytes,2,rep,name=sessions,proto3" json:"sessions,omitempty"` - Session *TerminalSession `protobuf:"bytes,3,opt,name=session,proto3" json:"session,omitempty"` - Output []byte `protobuf:"bytes,4,opt,name=output,proto3" json:"output,omitempty"` - Truncated bool `protobuf:"varint,5,opt,name=truncated,proto3" json:"truncated,omitempty"` - ShellOptions []*TerminalShellOption `protobuf:"bytes,6,rep,name=shell_options,json=shellOptions,proto3" json:"shell_options,omitempty"` - DefaultShell string `protobuf:"bytes,7,opt,name=default_shell,json=defaultShell,proto3" json:"default_shell,omitempty"` - OutputStartOffset uint64 `protobuf:"varint,8,opt,name=output_start_offset,json=outputStartOffset,proto3" json:"output_start_offset,omitempty"` - OutputEndOffset uint64 `protobuf:"varint,9,opt,name=output_end_offset,json=outputEndOffset,proto3" json:"output_end_offset,omitempty"` - SshPrompt *TerminalSshPrompt `protobuf:"bytes,10,opt,name=ssh_prompt,json=sshPrompt,proto3" json:"ssh_prompt,omitempty"` - LatencyMs uint32 `protobuf:"varint,11,opt,name=latency_ms,json=latencyMs,proto3" json:"latency_ms,omitempty"` - SshTabs *TerminalSshTabsSnapshot `protobuf:"bytes,12,opt,name=ssh_tabs,json=sshTabs,proto3" json:"ssh_tabs,omitempty"` - SshLocalForwards *TerminalSshLocalForwardsSnapshot `protobuf:"bytes,13,opt,name=ssh_local_forwards,json=sshLocalForwards,proto3" json:"ssh_local_forwards,omitempty"` - SshLocalForward *TerminalSshLocalForwardAction `protobuf:"bytes,14,opt,name=ssh_local_forward,json=sshLocalForward,proto3" json:"ssh_local_forward,omitempty"` - SshLocalForwardPortAvailable bool `protobuf:"varint,15,opt,name=ssh_local_forward_port_available,json=sshLocalForwardPortAvailable,proto3" json:"ssh_local_forward_port_available,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TerminalResponse) Reset() { - *x = TerminalResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[44] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TerminalResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TerminalResponse) ProtoMessage() {} - -func (x *TerminalResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[44] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TerminalResponse.ProtoReflect.Descriptor instead. -func (*TerminalResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{44} -} - -func (x *TerminalResponse) GetAction() string { - if x != nil { - return x.Action - } - return "" -} - -func (x *TerminalResponse) GetSessions() []*TerminalSession { - if x != nil { - return x.Sessions - } - return nil -} - -func (x *TerminalResponse) GetSession() *TerminalSession { - if x != nil { - return x.Session - } - return nil -} - -func (x *TerminalResponse) GetOutput() []byte { - if x != nil { - return x.Output - } - return nil -} - -func (x *TerminalResponse) GetTruncated() bool { - if x != nil { - return x.Truncated - } - return false -} - -func (x *TerminalResponse) GetShellOptions() []*TerminalShellOption { - if x != nil { - return x.ShellOptions - } - return nil -} - -func (x *TerminalResponse) GetDefaultShell() string { - if x != nil { - return x.DefaultShell - } - return "" -} - -func (x *TerminalResponse) GetOutputStartOffset() uint64 { - if x != nil { - return x.OutputStartOffset - } - return 0 -} - -func (x *TerminalResponse) GetOutputEndOffset() uint64 { - if x != nil { - return x.OutputEndOffset - } - return 0 -} - -func (x *TerminalResponse) GetSshPrompt() *TerminalSshPrompt { - if x != nil { - return x.SshPrompt - } - return nil -} - -func (x *TerminalResponse) GetLatencyMs() uint32 { - if x != nil { - return x.LatencyMs - } - return 0 -} - -func (x *TerminalResponse) GetSshTabs() *TerminalSshTabsSnapshot { - if x != nil { - return x.SshTabs - } - return nil -} - -func (x *TerminalResponse) GetSshLocalForwards() *TerminalSshLocalForwardsSnapshot { - if x != nil { - return x.SshLocalForwards - } - return nil -} - -func (x *TerminalResponse) GetSshLocalForward() *TerminalSshLocalForwardAction { - if x != nil { - return x.SshLocalForward - } - return nil -} - -func (x *TerminalResponse) GetSshLocalForwardPortAvailable() bool { - if x != nil { - return x.SshLocalForwardPortAvailable - } - return false -} - -type TerminalEvent struct { - state protoimpl.MessageState `protogen:"open.v1"` - Kind string `protobuf:"bytes,1,opt,name=kind,proto3" json:"kind,omitempty"` - SessionId string `protobuf:"bytes,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` - ProjectPathKey string `protobuf:"bytes,3,opt,name=project_path_key,json=projectPathKey,proto3" json:"project_path_key,omitempty"` - Session *TerminalSession `protobuf:"bytes,4,opt,name=session,proto3" json:"session,omitempty"` - Data []byte `protobuf:"bytes,5,opt,name=data,proto3" json:"data,omitempty"` - OutputStartOffset uint64 `protobuf:"varint,6,opt,name=output_start_offset,json=outputStartOffset,proto3" json:"output_start_offset,omitempty"` - OutputEndOffset uint64 `protobuf:"varint,7,opt,name=output_end_offset,json=outputEndOffset,proto3" json:"output_end_offset,omitempty"` - SshTabs *TerminalSshTabsSnapshot `protobuf:"bytes,8,opt,name=ssh_tabs,json=sshTabs,proto3" json:"ssh_tabs,omitempty"` - SshLocalForward *TerminalSshLocalForwardAction `protobuf:"bytes,9,opt,name=ssh_local_forward,json=sshLocalForward,proto3" json:"ssh_local_forward,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TerminalEvent) Reset() { - *x = TerminalEvent{} - mi := &file_proto_v2_gateway_proto_msgTypes[45] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TerminalEvent) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TerminalEvent) ProtoMessage() {} - -func (x *TerminalEvent) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[45] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TerminalEvent.ProtoReflect.Descriptor instead. -func (*TerminalEvent) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{45} -} - -func (x *TerminalEvent) GetKind() string { - if x != nil { - return x.Kind - } - return "" -} - -func (x *TerminalEvent) GetSessionId() string { - if x != nil { - return x.SessionId - } - return "" -} - -func (x *TerminalEvent) GetProjectPathKey() string { - if x != nil { - return x.ProjectPathKey - } - return "" -} - -func (x *TerminalEvent) GetSession() *TerminalSession { - if x != nil { - return x.Session - } - return nil -} - -func (x *TerminalEvent) GetData() []byte { - if x != nil { - return x.Data - } - return nil -} - -func (x *TerminalEvent) GetOutputStartOffset() uint64 { - if x != nil { - return x.OutputStartOffset - } - return 0 -} - -func (x *TerminalEvent) GetOutputEndOffset() uint64 { - if x != nil { - return x.OutputEndOffset - } - return 0 -} - -func (x *TerminalEvent) GetSshTabs() *TerminalSshTabsSnapshot { - if x != nil { - return x.SshTabs - } - return nil -} - -func (x *TerminalEvent) GetSshLocalForward() *TerminalSshLocalForwardAction { - if x != nil { - return x.SshLocalForward - } - return nil -} - -type TerminalStreamFrame struct { - state protoimpl.MessageState `protogen:"open.v1"` - Kind string `protobuf:"bytes,1,opt,name=kind,proto3" json:"kind,omitempty"` - StreamId string `protobuf:"bytes,2,opt,name=stream_id,json=streamId,proto3" json:"stream_id,omitempty"` - SessionId string `protobuf:"bytes,3,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` - ProjectPathKey string `protobuf:"bytes,4,opt,name=project_path_key,json=projectPathKey,proto3" json:"project_path_key,omitempty"` - Seq uint64 `protobuf:"varint,5,opt,name=seq,proto3" json:"seq,omitempty"` - StartOffset uint64 `protobuf:"varint,6,opt,name=start_offset,json=startOffset,proto3" json:"start_offset,omitempty"` - EndOffset uint64 `protobuf:"varint,7,opt,name=end_offset,json=endOffset,proto3" json:"end_offset,omitempty"` - Cols uint32 `protobuf:"varint,8,opt,name=cols,proto3" json:"cols,omitempty"` - Rows uint32 `protobuf:"varint,9,opt,name=rows,proto3" json:"rows,omitempty"` - MaxBytes uint32 `protobuf:"varint,10,opt,name=max_bytes,json=maxBytes,proto3" json:"max_bytes,omitempty"` - Truncated bool `protobuf:"varint,11,opt,name=truncated,proto3" json:"truncated,omitempty"` - Error string `protobuf:"bytes,12,opt,name=error,proto3" json:"error,omitempty"` - Session *TerminalSession `protobuf:"bytes,13,opt,name=session,proto3" json:"session,omitempty"` - Data []byte `protobuf:"bytes,14,opt,name=data,proto3" json:"data,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TerminalStreamFrame) Reset() { - *x = TerminalStreamFrame{} - mi := &file_proto_v2_gateway_proto_msgTypes[46] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TerminalStreamFrame) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TerminalStreamFrame) ProtoMessage() {} - -func (x *TerminalStreamFrame) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[46] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TerminalStreamFrame.ProtoReflect.Descriptor instead. -func (*TerminalStreamFrame) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{46} -} - -func (x *TerminalStreamFrame) GetKind() string { - if x != nil { - return x.Kind - } - return "" -} - -func (x *TerminalStreamFrame) GetStreamId() string { - if x != nil { - return x.StreamId - } - return "" -} - -func (x *TerminalStreamFrame) GetSessionId() string { - if x != nil { - return x.SessionId - } - return "" -} - -func (x *TerminalStreamFrame) GetProjectPathKey() string { - if x != nil { - return x.ProjectPathKey - } - return "" -} - -func (x *TerminalStreamFrame) GetSeq() uint64 { - if x != nil { - return x.Seq - } - return 0 -} - -func (x *TerminalStreamFrame) GetStartOffset() uint64 { - if x != nil { - return x.StartOffset - } - return 0 -} - -func (x *TerminalStreamFrame) GetEndOffset() uint64 { - if x != nil { - return x.EndOffset - } - return 0 -} - -func (x *TerminalStreamFrame) GetCols() uint32 { - if x != nil { - return x.Cols - } - return 0 -} - -func (x *TerminalStreamFrame) GetRows() uint32 { - if x != nil { - return x.Rows - } - return 0 -} - -func (x *TerminalStreamFrame) GetMaxBytes() uint32 { - if x != nil { - return x.MaxBytes - } - return 0 -} - -func (x *TerminalStreamFrame) GetTruncated() bool { - if x != nil { - return x.Truncated - } - return false -} - -func (x *TerminalStreamFrame) GetError() string { - if x != nil { - return x.Error - } - return "" -} - -func (x *TerminalStreamFrame) GetSession() *TerminalSession { - if x != nil { - return x.Session - } - return nil -} - -func (x *TerminalStreamFrame) GetData() []byte { - if x != nil { - return x.Data - } - return nil -} - -type GitRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Action string `protobuf:"bytes,1,opt,name=action,proto3" json:"action,omitempty"` - Workdir string `protobuf:"bytes,2,opt,name=workdir,proto3" json:"workdir,omitempty"` - ArgsJson string `protobuf:"bytes,3,opt,name=args_json,json=argsJson,proto3" json:"args_json,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GitRequest) Reset() { - *x = GitRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[47] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GitRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GitRequest) ProtoMessage() {} - -func (x *GitRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[47] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GitRequest.ProtoReflect.Descriptor instead. -func (*GitRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{47} -} - -func (x *GitRequest) GetAction() string { - if x != nil { - return x.Action - } - return "" -} - -func (x *GitRequest) GetWorkdir() string { - if x != nil { - return x.Workdir - } - return "" -} - -func (x *GitRequest) GetArgsJson() string { - if x != nil { - return x.ArgsJson - } - return "" -} - -type GitResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Action string `protobuf:"bytes,1,opt,name=action,proto3" json:"action,omitempty"` - ResultJson string `protobuf:"bytes,2,opt,name=result_json,json=resultJson,proto3" json:"result_json,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GitResponse) Reset() { - *x = GitResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[48] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GitResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GitResponse) ProtoMessage() {} - -func (x *GitResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[48] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GitResponse.ProtoReflect.Descriptor instead. -func (*GitResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{48} -} - -func (x *GitResponse) GetAction() string { - if x != nil { - return x.Action - } - return "" -} - -func (x *GitResponse) GetResultJson() string { - if x != nil { - return x.ResultJson - } - return "" -} - -type ChatRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` - SelectedModel *ChatSelectedModel `protobuf:"bytes,3,opt,name=selected_model,json=selectedModel,proto3" json:"selected_model,omitempty"` - ExecutionMode string `protobuf:"bytes,4,opt,name=execution_mode,json=executionMode,proto3" json:"execution_mode,omitempty"` - Workdir string `protobuf:"bytes,5,opt,name=workdir,proto3" json:"workdir,omitempty"` - UploadedFiles []*ChatUploadedFile `protobuf:"bytes,7,rep,name=uploaded_files,json=uploadedFiles,proto3" json:"uploaded_files,omitempty"` - ClientRequestId string `protobuf:"bytes,8,opt,name=client_request_id,json=clientRequestId,proto3" json:"client_request_id,omitempty"` - RuntimeControls *ChatRuntimeControls `protobuf:"bytes,9,opt,name=runtime_controls,json=runtimeControls,proto3" json:"runtime_controls,omitempty"` - QueuePolicy string `protobuf:"bytes,10,opt,name=queue_policy,json=queuePolicy,proto3" json:"queue_policy,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatRequest) Reset() { - *x = ChatRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[49] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatRequest) ProtoMessage() {} - -func (x *ChatRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[49] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatRequest.ProtoReflect.Descriptor instead. -func (*ChatRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{49} -} - -func (x *ChatRequest) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -func (x *ChatRequest) GetMessage() string { - if x != nil { - return x.Message - } - return "" -} - -func (x *ChatRequest) GetSelectedModel() *ChatSelectedModel { - if x != nil { - return x.SelectedModel - } - return nil -} - -func (x *ChatRequest) GetExecutionMode() string { - if x != nil { - return x.ExecutionMode - } - return "" -} - -func (x *ChatRequest) GetWorkdir() string { - if x != nil { - return x.Workdir - } - return "" -} - -func (x *ChatRequest) GetUploadedFiles() []*ChatUploadedFile { - if x != nil { - return x.UploadedFiles - } - return nil -} - -func (x *ChatRequest) GetClientRequestId() string { - if x != nil { - return x.ClientRequestId - } - return "" -} - -func (x *ChatRequest) GetRuntimeControls() *ChatRuntimeControls { - if x != nil { - return x.RuntimeControls - } - return nil -} - -func (x *ChatRequest) GetQueuePolicy() string { - if x != nil { - return x.QueuePolicy - } - return "" -} - -type ChatMessageRef struct { - state protoimpl.MessageState `protogen:"open.v1"` - SegmentIndex int32 `protobuf:"varint,1,opt,name=segment_index,json=segmentIndex,proto3" json:"segment_index,omitempty"` - MessageIndex int32 `protobuf:"varint,2,opt,name=message_index,json=messageIndex,proto3" json:"message_index,omitempty"` - SegmentId string `protobuf:"bytes,3,opt,name=segment_id,json=segmentId,proto3" json:"segment_id,omitempty"` - MessageId string `protobuf:"bytes,4,opt,name=message_id,json=messageId,proto3" json:"message_id,omitempty"` - Role string `protobuf:"bytes,5,opt,name=role,proto3" json:"role,omitempty"` - ContentHash string `protobuf:"bytes,6,opt,name=content_hash,json=contentHash,proto3" json:"content_hash,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatMessageRef) Reset() { - *x = ChatMessageRef{} - mi := &file_proto_v2_gateway_proto_msgTypes[50] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatMessageRef) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatMessageRef) ProtoMessage() {} - -func (x *ChatMessageRef) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[50] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatMessageRef.ProtoReflect.Descriptor instead. -func (*ChatMessageRef) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{50} -} - -func (x *ChatMessageRef) GetSegmentIndex() int32 { - if x != nil { - return x.SegmentIndex - } - return 0 -} - -func (x *ChatMessageRef) GetMessageIndex() int32 { - if x != nil { - return x.MessageIndex - } - return 0 -} - -func (x *ChatMessageRef) GetSegmentId() string { - if x != nil { - return x.SegmentId - } - return "" -} - -func (x *ChatMessageRef) GetMessageId() string { - if x != nil { - return x.MessageId - } - return "" -} - -func (x *ChatMessageRef) GetRole() string { - if x != nil { - return x.Role - } - return "" -} - -func (x *ChatMessageRef) GetContentHash() string { - if x != nil { - return x.ContentHash - } - return "" -} - -type CancelChatRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - // 可选运行 id 提示:v2 浏览器链路用它消除同会话并发运行的歧义;桌面端可忽略。 - RunId string `protobuf:"bytes,2,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CancelChatRequest) Reset() { - *x = CancelChatRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[51] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CancelChatRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CancelChatRequest) ProtoMessage() {} - -func (x *CancelChatRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[51] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use CancelChatRequest.ProtoReflect.Descriptor instead. -func (*CancelChatRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{51} -} - -func (x *CancelChatRequest) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -func (x *CancelChatRequest) GetRunId() string { - if x != nil { - return x.RunId - } - return "" -} - -type ChatCommandRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` - Request *ChatRequest `protobuf:"bytes,2,opt,name=request,proto3" json:"request,omitempty"` - BaseMessageRef *ChatMessageRef `protobuf:"bytes,3,opt,name=base_message_ref,json=baseMessageRef,proto3" json:"base_message_ref,omitempty"` - Cancel *CancelChatRequest `protobuf:"bytes,4,opt,name=cancel,proto3" json:"cancel,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatCommandRequest) Reset() { - *x = ChatCommandRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[52] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatCommandRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatCommandRequest) ProtoMessage() {} - -func (x *ChatCommandRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[52] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatCommandRequest.ProtoReflect.Descriptor instead. -func (*ChatCommandRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{52} -} - -func (x *ChatCommandRequest) GetType() string { - if x != nil { - return x.Type - } - return "" -} - -func (x *ChatCommandRequest) GetRequest() *ChatRequest { - if x != nil { - return x.Request - } - return nil -} - -func (x *ChatCommandRequest) GetBaseMessageRef() *ChatMessageRef { - if x != nil { - return x.BaseMessageRef - } - return nil -} - -func (x *ChatCommandRequest) GetCancel() *CancelChatRequest { - if x != nil { - return x.Cancel - } - return nil -} - -type ChatQueueRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Action string `protobuf:"bytes,1,opt,name=action,proto3" json:"action,omitempty"` - ConversationId string `protobuf:"bytes,2,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - ItemId string `protobuf:"bytes,3,opt,name=item_id,json=itemId,proto3" json:"item_id,omitempty"` - Direction string `protobuf:"bytes,4,opt,name=direction,proto3" json:"direction,omitempty"` - Revision uint64 `protobuf:"varint,5,opt,name=revision,proto3" json:"revision,omitempty"` - DraftJson string `protobuf:"bytes,6,opt,name=draft_json,json=draftJson,proto3" json:"draft_json,omitempty"` - UploadedFilesJson string `protobuf:"bytes,7,opt,name=uploaded_files_json,json=uploadedFilesJson,proto3" json:"uploaded_files_json,omitempty"` - RequestJson string `protobuf:"bytes,8,opt,name=request_json,json=requestJson,proto3" json:"request_json,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatQueueRequest) Reset() { - *x = ChatQueueRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[53] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatQueueRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatQueueRequest) ProtoMessage() {} - -func (x *ChatQueueRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[53] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatQueueRequest.ProtoReflect.Descriptor instead. -func (*ChatQueueRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{53} -} - -func (x *ChatQueueRequest) GetAction() string { - if x != nil { - return x.Action - } - return "" -} - -func (x *ChatQueueRequest) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -func (x *ChatQueueRequest) GetItemId() string { - if x != nil { - return x.ItemId - } - return "" -} - -func (x *ChatQueueRequest) GetDirection() string { - if x != nil { - return x.Direction - } - return "" -} - -func (x *ChatQueueRequest) GetRevision() uint64 { - if x != nil { - return x.Revision - } - return 0 -} - -func (x *ChatQueueRequest) GetDraftJson() string { - if x != nil { - return x.DraftJson - } - return "" -} - -func (x *ChatQueueRequest) GetUploadedFilesJson() string { - if x != nil { - return x.UploadedFilesJson - } - return "" -} - -func (x *ChatQueueRequest) GetRequestJson() string { - if x != nil { - return x.RequestJson - } - return "" -} - -type ChatQueueResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Accepted bool `protobuf:"varint,1,opt,name=accepted,proto3" json:"accepted,omitempty"` - Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` - SnapshotJson string `protobuf:"bytes,3,opt,name=snapshot_json,json=snapshotJson,proto3" json:"snapshot_json,omitempty"` - ItemJson string `protobuf:"bytes,4,opt,name=item_json,json=itemJson,proto3" json:"item_json,omitempty"` - ErrorCode string `protobuf:"bytes,5,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"` - Revision uint64 `protobuf:"varint,6,opt,name=revision,proto3" json:"revision,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatQueueResponse) Reset() { - *x = ChatQueueResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[54] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatQueueResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatQueueResponse) ProtoMessage() {} - -func (x *ChatQueueResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[54] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatQueueResponse.ProtoReflect.Descriptor instead. -func (*ChatQueueResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{54} -} - -func (x *ChatQueueResponse) GetAccepted() bool { - if x != nil { - return x.Accepted - } - return false -} - -func (x *ChatQueueResponse) GetMessage() string { - if x != nil { - return x.Message - } - return "" -} - -func (x *ChatQueueResponse) GetSnapshotJson() string { - if x != nil { - return x.SnapshotJson - } - return "" -} - -func (x *ChatQueueResponse) GetItemJson() string { - if x != nil { - return x.ItemJson - } - return "" -} - -func (x *ChatQueueResponse) GetErrorCode() string { - if x != nil { - return x.ErrorCode - } - return "" -} - -func (x *ChatQueueResponse) GetRevision() uint64 { - if x != nil { - return x.Revision - } - return 0 -} - -type ChatQueueEvent struct { - state protoimpl.MessageState `protogen:"open.v1"` - ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - SnapshotJson string `protobuf:"bytes,2,opt,name=snapshot_json,json=snapshotJson,proto3" json:"snapshot_json,omitempty"` - Revision uint64 `protobuf:"varint,3,opt,name=revision,proto3" json:"revision,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatQueueEvent) Reset() { - *x = ChatQueueEvent{} - mi := &file_proto_v2_gateway_proto_msgTypes[55] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatQueueEvent) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatQueueEvent) ProtoMessage() {} - -func (x *ChatQueueEvent) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[55] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatQueueEvent.ProtoReflect.Descriptor instead. -func (*ChatQueueEvent) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{55} -} - -func (x *ChatQueueEvent) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -func (x *ChatQueueEvent) GetSnapshotJson() string { - if x != nil { - return x.SnapshotJson - } - return "" -} - -func (x *ChatQueueEvent) GetRevision() uint64 { - if x != nil { - return x.Revision - } - return 0 -} - -type ChatEvent struct { - state protoimpl.MessageState `protogen:"open.v1"` - Type ChatEvent_ChatEventType `protobuf:"varint,1,opt,name=type,proto3,enum=liveagent.gateway.v2.ChatEvent_ChatEventType" json:"type,omitempty"` - ConversationId string `protobuf:"bytes,2,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - Data string `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatEvent) Reset() { - *x = ChatEvent{} - mi := &file_proto_v2_gateway_proto_msgTypes[56] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatEvent) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatEvent) ProtoMessage() {} - -func (x *ChatEvent) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[56] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatEvent.ProtoReflect.Descriptor instead. -func (*ChatEvent) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{56} -} - -func (x *ChatEvent) GetType() ChatEvent_ChatEventType { - if x != nil { - return x.Type - } - return ChatEvent_TOKEN -} - -func (x *ChatEvent) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -func (x *ChatEvent) GetData() string { - if x != nil { - return x.Data - } - return "" -} - -type ChatControlEvent struct { - state protoimpl.MessageState `protogen:"open.v1"` - RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` - ClientRequestId string `protobuf:"bytes,2,opt,name=client_request_id,json=clientRequestId,proto3" json:"client_request_id,omitempty"` - ConversationId string `protobuf:"bytes,3,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - RunEpoch int64 `protobuf:"varint,4,opt,name=run_epoch,json=runEpoch,proto3" json:"run_epoch,omitempty"` - Type string `protobuf:"bytes,5,opt,name=type,proto3" json:"type,omitempty"` - State string `protobuf:"bytes,6,opt,name=state,proto3" json:"state,omitempty"` - ErrorCode string `protobuf:"bytes,7,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"` - Message string `protobuf:"bytes,8,opt,name=message,proto3" json:"message,omitempty"` - Seq int64 `protobuf:"varint,9,opt,name=seq,proto3" json:"seq,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatControlEvent) Reset() { - *x = ChatControlEvent{} - mi := &file_proto_v2_gateway_proto_msgTypes[57] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatControlEvent) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatControlEvent) ProtoMessage() {} - -func (x *ChatControlEvent) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[57] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatControlEvent.ProtoReflect.Descriptor instead. -func (*ChatControlEvent) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{57} -} - -func (x *ChatControlEvent) GetRequestId() string { - if x != nil { - return x.RequestId - } - return "" -} - -func (x *ChatControlEvent) GetClientRequestId() string { - if x != nil { - return x.ClientRequestId - } - return "" -} - -func (x *ChatControlEvent) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -func (x *ChatControlEvent) GetRunEpoch() int64 { - if x != nil { - return x.RunEpoch - } - return 0 -} - -func (x *ChatControlEvent) GetType() string { - if x != nil { - return x.Type - } - return "" -} - -func (x *ChatControlEvent) GetState() string { - if x != nil { - return x.State - } - return "" -} - -func (x *ChatControlEvent) GetErrorCode() string { - if x != nil { - return x.ErrorCode - } - return "" -} - -func (x *ChatControlEvent) GetMessage() string { - if x != nil { - return x.Message - } - return "" -} - -func (x *ChatControlEvent) GetSeq() int64 { - if x != nil { - return x.Seq - } - return 0 -} - -type ChatRuntimeSnapshot struct { - state protoimpl.MessageState `protogen:"open.v1"` - ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - RunId string `protobuf:"bytes,2,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` - ClientRequestId string `protobuf:"bytes,3,opt,name=client_request_id,json=clientRequestId,proto3" json:"client_request_id,omitempty"` - WorkerId string `protobuf:"bytes,4,opt,name=worker_id,json=workerId,proto3" json:"worker_id,omitempty"` - State string `protobuf:"bytes,5,opt,name=state,proto3" json:"state,omitempty"` - Cwd string `protobuf:"bytes,6,opt,name=cwd,proto3" json:"cwd,omitempty"` - UpdatedAt int64 `protobuf:"varint,7,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` - Revision int64 `protobuf:"varint,8,opt,name=revision,proto3" json:"revision,omitempty"` - EntriesJson string `protobuf:"bytes,9,opt,name=entries_json,json=entriesJson,proto3" json:"entries_json,omitempty"` - ToolStatus string `protobuf:"bytes,10,opt,name=tool_status,json=toolStatus,proto3" json:"tool_status,omitempty"` - ToolStatusIsCompaction bool `protobuf:"varint,11,opt,name=tool_status_is_compaction,json=toolStatusIsCompaction,proto3" json:"tool_status_is_compaction,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatRuntimeSnapshot) Reset() { - *x = ChatRuntimeSnapshot{} - mi := &file_proto_v2_gateway_proto_msgTypes[58] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatRuntimeSnapshot) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatRuntimeSnapshot) ProtoMessage() {} - -func (x *ChatRuntimeSnapshot) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[58] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatRuntimeSnapshot.ProtoReflect.Descriptor instead. -func (*ChatRuntimeSnapshot) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{58} -} - -func (x *ChatRuntimeSnapshot) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -func (x *ChatRuntimeSnapshot) GetRunId() string { - if x != nil { - return x.RunId - } - return "" -} - -func (x *ChatRuntimeSnapshot) GetClientRequestId() string { - if x != nil { - return x.ClientRequestId - } - return "" -} - -func (x *ChatRuntimeSnapshot) GetWorkerId() string { - if x != nil { - return x.WorkerId - } - return "" -} - -func (x *ChatRuntimeSnapshot) GetState() string { - if x != nil { - return x.State - } - return "" -} - -func (x *ChatRuntimeSnapshot) GetCwd() string { - if x != nil { - return x.Cwd - } - return "" -} - -func (x *ChatRuntimeSnapshot) GetUpdatedAt() int64 { - if x != nil { - return x.UpdatedAt - } - return 0 -} - -func (x *ChatRuntimeSnapshot) GetRevision() int64 { - if x != nil { - return x.Revision - } - return 0 -} - -func (x *ChatRuntimeSnapshot) GetEntriesJson() string { - if x != nil { - return x.EntriesJson - } - return "" -} - -func (x *ChatRuntimeSnapshot) GetToolStatus() string { - if x != nil { - return x.ToolStatus - } - return "" -} - -func (x *ChatRuntimeSnapshot) GetToolStatusIsCompaction() bool { - if x != nil { - return x.ToolStatusIsCompaction - } - return false -} - -type RuntimeStatusEvent struct { - state protoimpl.MessageState `protogen:"open.v1"` - WorkerId string `protobuf:"bytes,1,opt,name=worker_id,json=workerId,proto3" json:"worker_id,omitempty"` - State string `protobuf:"bytes,2,opt,name=state,proto3" json:"state,omitempty"` - Visible bool `protobuf:"varint,3,opt,name=visible,proto3" json:"visible,omitempty"` - ActiveRunCount uint32 `protobuf:"varint,4,opt,name=active_run_count,json=activeRunCount,proto3" json:"active_run_count,omitempty"` - Timestamp int64 `protobuf:"varint,5,opt,name=timestamp,proto3" json:"timestamp,omitempty"` - ActiveRuns []*ChatRunReport `protobuf:"bytes,6,rep,name=active_runs,json=activeRuns,proto3" json:"active_runs,omitempty"` - FinishedRuns []*ChatRunReport `protobuf:"bytes,7,rep,name=finished_runs,json=finishedRuns,proto3" json:"finished_runs,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *RuntimeStatusEvent) Reset() { - *x = RuntimeStatusEvent{} - mi := &file_proto_v2_gateway_proto_msgTypes[59] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *RuntimeStatusEvent) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*RuntimeStatusEvent) ProtoMessage() {} - -func (x *RuntimeStatusEvent) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[59] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use RuntimeStatusEvent.ProtoReflect.Descriptor instead. -func (*RuntimeStatusEvent) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{59} -} - -func (x *RuntimeStatusEvent) GetWorkerId() string { - if x != nil { - return x.WorkerId - } - return "" -} - -func (x *RuntimeStatusEvent) GetState() string { - if x != nil { - return x.State - } - return "" -} - -func (x *RuntimeStatusEvent) GetVisible() bool { - if x != nil { - return x.Visible - } - return false -} - -func (x *RuntimeStatusEvent) GetActiveRunCount() uint32 { - if x != nil { - return x.ActiveRunCount - } - return 0 -} - -func (x *RuntimeStatusEvent) GetTimestamp() int64 { - if x != nil { - return x.Timestamp - } - return 0 -} - -func (x *RuntimeStatusEvent) GetActiveRuns() []*ChatRunReport { - if x != nil { - return x.ActiveRuns - } - return nil -} - -func (x *RuntimeStatusEvent) GetFinishedRuns() []*ChatRunReport { - if x != nil { - return x.FinishedRuns - } - return nil -} - -type ChatRunReport struct { - state protoimpl.MessageState `protogen:"open.v1"` - RunId string `protobuf:"bytes,1,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` - ConversationId string `protobuf:"bytes,2,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - State string `protobuf:"bytes,3,opt,name=state,proto3" json:"state,omitempty"` - ErrorCode string `protobuf:"bytes,4,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"` - Message string `protobuf:"bytes,5,opt,name=message,proto3" json:"message,omitempty"` - UpdatedAt int64 `protobuf:"varint,6,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatRunReport) Reset() { - *x = ChatRunReport{} - mi := &file_proto_v2_gateway_proto_msgTypes[60] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatRunReport) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatRunReport) ProtoMessage() {} - -func (x *ChatRunReport) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[60] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatRunReport.ProtoReflect.Descriptor instead. -func (*ChatRunReport) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{60} -} - -func (x *ChatRunReport) GetRunId() string { - if x != nil { - return x.RunId - } - return "" -} - -func (x *ChatRunReport) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -func (x *ChatRunReport) GetState() string { - if x != nil { - return x.State - } - return "" -} - -func (x *ChatRunReport) GetErrorCode() string { - if x != nil { - return x.ErrorCode - } - return "" -} - -func (x *ChatRunReport) GetMessage() string { - if x != nil { - return x.Message - } - return "" -} - -func (x *ChatRunReport) GetUpdatedAt() int64 { - if x != nil { - return x.UpdatedAt - } - return 0 -} - -type CronManageRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Action string `protobuf:"bytes,1,opt,name=action,proto3" json:"action,omitempty"` - TaskId string `protobuf:"bytes,2,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` - TaskJson string `protobuf:"bytes,3,opt,name=task_json,json=taskJson,proto3" json:"task_json,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CronManageRequest) Reset() { - *x = CronManageRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[61] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CronManageRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CronManageRequest) ProtoMessage() {} - -func (x *CronManageRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[61] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use CronManageRequest.ProtoReflect.Descriptor instead. -func (*CronManageRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{61} -} - -func (x *CronManageRequest) GetAction() string { - if x != nil { - return x.Action - } - return "" -} - -func (x *CronManageRequest) GetTaskId() string { - if x != nil { - return x.TaskId - } - return "" -} - -func (x *CronManageRequest) GetTaskJson() string { - if x != nil { - return x.TaskJson - } - return "" -} - -type CronManageResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Action string `protobuf:"bytes,1,opt,name=action,proto3" json:"action,omitempty"` - ResultJson string `protobuf:"bytes,2,opt,name=result_json,json=resultJson,proto3" json:"result_json,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CronManageResponse) Reset() { - *x = CronManageResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[62] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CronManageResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CronManageResponse) ProtoMessage() {} - -func (x *CronManageResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[62] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use CronManageResponse.ProtoReflect.Descriptor instead. -func (*CronManageResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{62} -} - -func (x *CronManageResponse) GetAction() string { - if x != nil { - return x.Action - } - return "" -} - -func (x *CronManageResponse) GetResultJson() string { - if x != nil { - return x.ResultJson - } - return "" -} - -type HistoryListRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Page int32 `protobuf:"varint,1,opt,name=page,proto3" json:"page,omitempty"` - PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` - Cwd string `protobuf:"bytes,3,opt,name=cwd,proto3" json:"cwd,omitempty"` - CwdEmpty bool `protobuf:"varint,4,opt,name=cwd_empty,json=cwdEmpty,proto3" json:"cwd_empty,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HistoryListRequest) Reset() { - *x = HistoryListRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[63] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HistoryListRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*HistoryListRequest) ProtoMessage() {} - -func (x *HistoryListRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[63] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use HistoryListRequest.ProtoReflect.Descriptor instead. -func (*HistoryListRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{63} -} - -func (x *HistoryListRequest) GetPage() int32 { - if x != nil { - return x.Page - } - return 0 -} - -func (x *HistoryListRequest) GetPageSize() int32 { - if x != nil { - return x.PageSize - } - return 0 -} - -func (x *HistoryListRequest) GetCwd() string { - if x != nil { - return x.Cwd - } - return "" -} - -func (x *HistoryListRequest) GetCwdEmpty() bool { - if x != nil { - return x.CwdEmpty - } - return false -} - -type HistoryListResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Conversations []*ConversationSummary `protobuf:"bytes,1,rep,name=conversations,proto3" json:"conversations,omitempty"` - TotalCount int32 `protobuf:"varint,2,opt,name=total_count,json=totalCount,proto3" json:"total_count,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HistoryListResponse) Reset() { - *x = HistoryListResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[64] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HistoryListResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*HistoryListResponse) ProtoMessage() {} - -func (x *HistoryListResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[64] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use HistoryListResponse.ProtoReflect.Descriptor instead. -func (*HistoryListResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{64} -} - -func (x *HistoryListResponse) GetConversations() []*ConversationSummary { - if x != nil { - return x.Conversations - } - return nil -} - -func (x *HistoryListResponse) GetTotalCount() int32 { - if x != nil { - return x.TotalCount - } - return 0 -} - -type ConversationSummary struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` - CreatedAt int64 `protobuf:"varint,3,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` - UpdatedAt int64 `protobuf:"varint,4,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` - MessageCount int32 `protobuf:"varint,5,opt,name=message_count,json=messageCount,proto3" json:"message_count,omitempty"` - ProviderId string `protobuf:"bytes,6,opt,name=provider_id,json=providerId,proto3" json:"provider_id,omitempty"` - Model string `protobuf:"bytes,7,opt,name=model,proto3" json:"model,omitempty"` - SessionId string `protobuf:"bytes,8,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` - Cwd string `protobuf:"bytes,9,opt,name=cwd,proto3" json:"cwd,omitempty"` - IsPinned bool `protobuf:"varint,10,opt,name=is_pinned,json=isPinned,proto3" json:"is_pinned,omitempty"` - PinnedAt int64 `protobuf:"varint,11,opt,name=pinned_at,json=pinnedAt,proto3" json:"pinned_at,omitempty"` - IsShared bool `protobuf:"varint,12,opt,name=is_shared,json=isShared,proto3" json:"is_shared,omitempty"` - SelectedModelJson string `protobuf:"bytes,13,opt,name=selected_model_json,json=selectedModelJson,proto3" json:"selected_model_json,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ConversationSummary) Reset() { - *x = ConversationSummary{} - mi := &file_proto_v2_gateway_proto_msgTypes[65] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ConversationSummary) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ConversationSummary) ProtoMessage() {} - -func (x *ConversationSummary) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[65] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ConversationSummary.ProtoReflect.Descriptor instead. -func (*ConversationSummary) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{65} -} - -func (x *ConversationSummary) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *ConversationSummary) GetTitle() string { - if x != nil { - return x.Title - } - return "" -} - -func (x *ConversationSummary) GetCreatedAt() int64 { - if x != nil { - return x.CreatedAt - } - return 0 -} - -func (x *ConversationSummary) GetUpdatedAt() int64 { - if x != nil { - return x.UpdatedAt - } - return 0 -} - -func (x *ConversationSummary) GetMessageCount() int32 { - if x != nil { - return x.MessageCount - } - return 0 -} - -func (x *ConversationSummary) GetProviderId() string { - if x != nil { - return x.ProviderId - } - return "" -} - -func (x *ConversationSummary) GetModel() string { - if x != nil { - return x.Model - } - return "" -} - -func (x *ConversationSummary) GetSessionId() string { - if x != nil { - return x.SessionId - } - return "" -} - -func (x *ConversationSummary) GetCwd() string { - if x != nil { - return x.Cwd - } - return "" -} - -func (x *ConversationSummary) GetIsPinned() bool { - if x != nil { - return x.IsPinned - } - return false -} - -func (x *ConversationSummary) GetPinnedAt() int64 { - if x != nil { - return x.PinnedAt - } - return 0 -} - -func (x *ConversationSummary) GetIsShared() bool { - if x != nil { - return x.IsShared - } - return false -} - -func (x *ConversationSummary) GetSelectedModelJson() string { - if x != nil { - return x.SelectedModelJson - } - return "" -} - -type HistoryGetRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - MaxMessages int32 `protobuf:"varint,2,opt,name=max_messages,json=maxMessages,proto3" json:"max_messages,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HistoryGetRequest) Reset() { - *x = HistoryGetRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[66] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HistoryGetRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*HistoryGetRequest) ProtoMessage() {} - -func (x *HistoryGetRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[66] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use HistoryGetRequest.ProtoReflect.Descriptor instead. -func (*HistoryGetRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{66} -} - -func (x *HistoryGetRequest) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -func (x *HistoryGetRequest) GetMaxMessages() int32 { - if x != nil { - return x.MaxMessages - } - return 0 -} - -type HistoryGetResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - MessagesJson string `protobuf:"bytes,2,opt,name=messages_json,json=messagesJson,proto3" json:"messages_json,omitempty"` - TotalMessageCount int32 `protobuf:"varint,3,opt,name=total_message_count,json=totalMessageCount,proto3" json:"total_message_count,omitempty"` - ReturnedMessageCount int32 `protobuf:"varint,4,opt,name=returned_message_count,json=returnedMessageCount,proto3" json:"returned_message_count,omitempty"` - HasMore bool `protobuf:"varint,5,opt,name=has_more,json=hasMore,proto3" json:"has_more,omitempty"` - Conversation *ConversationSummary `protobuf:"bytes,6,opt,name=conversation,proto3" json:"conversation,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HistoryGetResponse) Reset() { - *x = HistoryGetResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[67] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HistoryGetResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*HistoryGetResponse) ProtoMessage() {} - -func (x *HistoryGetResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[67] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use HistoryGetResponse.ProtoReflect.Descriptor instead. -func (*HistoryGetResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{67} -} - -func (x *HistoryGetResponse) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -func (x *HistoryGetResponse) GetMessagesJson() string { - if x != nil { - return x.MessagesJson - } - return "" -} - -func (x *HistoryGetResponse) GetTotalMessageCount() int32 { - if x != nil { - return x.TotalMessageCount - } - return 0 -} - -func (x *HistoryGetResponse) GetReturnedMessageCount() int32 { - if x != nil { - return x.ReturnedMessageCount - } - return 0 -} - -func (x *HistoryGetResponse) GetHasMore() bool { - if x != nil { - return x.HasMore - } - return false -} - -func (x *HistoryGetResponse) GetConversation() *ConversationSummary { - if x != nil { - return x.Conversation - } - return nil -} - -type HistoryPrefixRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - MaxMessages int32 `protobuf:"varint,2,opt,name=max_messages,json=maxMessages,proto3" json:"max_messages,omitempty"` - BaseMessageRef *ChatMessageRef `protobuf:"bytes,3,opt,name=base_message_ref,json=baseMessageRef,proto3" json:"base_message_ref,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HistoryPrefixRequest) Reset() { - *x = HistoryPrefixRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[68] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HistoryPrefixRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*HistoryPrefixRequest) ProtoMessage() {} - -func (x *HistoryPrefixRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[68] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use HistoryPrefixRequest.ProtoReflect.Descriptor instead. -func (*HistoryPrefixRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{68} -} - -func (x *HistoryPrefixRequest) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -func (x *HistoryPrefixRequest) GetMaxMessages() int32 { - if x != nil { - return x.MaxMessages - } - return 0 -} - -func (x *HistoryPrefixRequest) GetBaseMessageRef() *ChatMessageRef { - if x != nil { - return x.BaseMessageRef - } - return nil -} - -type HistoryPrefixResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - MessagesJson string `protobuf:"bytes,2,opt,name=messages_json,json=messagesJson,proto3" json:"messages_json,omitempty"` - TotalMessageCount int32 `protobuf:"varint,3,opt,name=total_message_count,json=totalMessageCount,proto3" json:"total_message_count,omitempty"` - ReturnedMessageCount int32 `protobuf:"varint,4,opt,name=returned_message_count,json=returnedMessageCount,proto3" json:"returned_message_count,omitempty"` - HasMore bool `protobuf:"varint,5,opt,name=has_more,json=hasMore,proto3" json:"has_more,omitempty"` - Conversation *ConversationSummary `protobuf:"bytes,6,opt,name=conversation,proto3" json:"conversation,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HistoryPrefixResponse) Reset() { - *x = HistoryPrefixResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[69] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HistoryPrefixResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*HistoryPrefixResponse) ProtoMessage() {} - -func (x *HistoryPrefixResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[69] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use HistoryPrefixResponse.ProtoReflect.Descriptor instead. -func (*HistoryPrefixResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{69} -} - -func (x *HistoryPrefixResponse) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -func (x *HistoryPrefixResponse) GetMessagesJson() string { - if x != nil { - return x.MessagesJson - } - return "" -} - -func (x *HistoryPrefixResponse) GetTotalMessageCount() int32 { - if x != nil { - return x.TotalMessageCount - } - return 0 -} - -func (x *HistoryPrefixResponse) GetReturnedMessageCount() int32 { - if x != nil { - return x.ReturnedMessageCount - } - return 0 -} - -func (x *HistoryPrefixResponse) GetHasMore() bool { - if x != nil { - return x.HasMore - } - return false -} - -func (x *HistoryPrefixResponse) GetConversation() *ConversationSummary { - if x != nil { - return x.Conversation - } - return nil -} - -type HistoryRenameRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HistoryRenameRequest) Reset() { - *x = HistoryRenameRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[70] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HistoryRenameRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*HistoryRenameRequest) ProtoMessage() {} - -func (x *HistoryRenameRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[70] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use HistoryRenameRequest.ProtoReflect.Descriptor instead. -func (*HistoryRenameRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{70} -} - -func (x *HistoryRenameRequest) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -func (x *HistoryRenameRequest) GetTitle() string { - if x != nil { - return x.Title - } - return "" -} - -type HistoryRenameResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Conversation *ConversationSummary `protobuf:"bytes,1,opt,name=conversation,proto3" json:"conversation,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HistoryRenameResponse) Reset() { - *x = HistoryRenameResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[71] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HistoryRenameResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*HistoryRenameResponse) ProtoMessage() {} - -func (x *HistoryRenameResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[71] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use HistoryRenameResponse.ProtoReflect.Descriptor instead. -func (*HistoryRenameResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{71} -} - -func (x *HistoryRenameResponse) GetConversation() *ConversationSummary { - if x != nil { - return x.Conversation - } - return nil -} - -// Copies the conversation prefix up to and including the assistant response -// that answers the anchored user message (base_message_ref) into a brand-new -// conversation titled "新分支". -type HistoryBranchRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - BaseMessageRef *ChatMessageRef `protobuf:"bytes,2,opt,name=base_message_ref,json=baseMessageRef,proto3" json:"base_message_ref,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HistoryBranchRequest) Reset() { - *x = HistoryBranchRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[72] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HistoryBranchRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*HistoryBranchRequest) ProtoMessage() {} - -func (x *HistoryBranchRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[72] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use HistoryBranchRequest.ProtoReflect.Descriptor instead. -func (*HistoryBranchRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{72} -} - -func (x *HistoryBranchRequest) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -func (x *HistoryBranchRequest) GetBaseMessageRef() *ChatMessageRef { - if x != nil { - return x.BaseMessageRef - } - return nil -} - -type HistoryBranchResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Conversation *ConversationSummary `protobuf:"bytes,1,opt,name=conversation,proto3" json:"conversation,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HistoryBranchResponse) Reset() { - *x = HistoryBranchResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[73] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HistoryBranchResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*HistoryBranchResponse) ProtoMessage() {} - -func (x *HistoryBranchResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[73] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use HistoryBranchResponse.ProtoReflect.Descriptor instead. -func (*HistoryBranchResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{73} -} - -func (x *HistoryBranchResponse) GetConversation() *ConversationSummary { - if x != nil { - return x.Conversation - } - return nil -} - -type HistoryPinRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - IsPinned bool `protobuf:"varint,2,opt,name=is_pinned,json=isPinned,proto3" json:"is_pinned,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HistoryPinRequest) Reset() { - *x = HistoryPinRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[74] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HistoryPinRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*HistoryPinRequest) ProtoMessage() {} - -func (x *HistoryPinRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[74] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use HistoryPinRequest.ProtoReflect.Descriptor instead. -func (*HistoryPinRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{74} -} - -func (x *HistoryPinRequest) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -func (x *HistoryPinRequest) GetIsPinned() bool { - if x != nil { - return x.IsPinned - } - return false -} - -type HistoryPinResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Conversation *ConversationSummary `protobuf:"bytes,1,opt,name=conversation,proto3" json:"conversation,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HistoryPinResponse) Reset() { - *x = HistoryPinResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[75] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HistoryPinResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*HistoryPinResponse) ProtoMessage() {} - -func (x *HistoryPinResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[75] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use HistoryPinResponse.ProtoReflect.Descriptor instead. -func (*HistoryPinResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{75} -} - -func (x *HistoryPinResponse) GetConversation() *ConversationSummary { - if x != nil { - return x.Conversation - } - return nil -} - -type HistoryShareStatus struct { - state protoimpl.MessageState `protogen:"open.v1"` - ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - Enabled bool `protobuf:"varint,2,opt,name=enabled,proto3" json:"enabled,omitempty"` - Token string `protobuf:"bytes,3,opt,name=token,proto3" json:"token,omitempty"` - CreatedAt int64 `protobuf:"varint,4,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` - UpdatedAt int64 `protobuf:"varint,5,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` - RedactToolContent bool `protobuf:"varint,6,opt,name=redact_tool_content,json=redactToolContent,proto3" json:"redact_tool_content,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HistoryShareStatus) Reset() { - *x = HistoryShareStatus{} - mi := &file_proto_v2_gateway_proto_msgTypes[76] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HistoryShareStatus) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*HistoryShareStatus) ProtoMessage() {} - -func (x *HistoryShareStatus) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[76] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use HistoryShareStatus.ProtoReflect.Descriptor instead. -func (*HistoryShareStatus) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{76} -} - -func (x *HistoryShareStatus) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -func (x *HistoryShareStatus) GetEnabled() bool { - if x != nil { - return x.Enabled - } - return false -} - -func (x *HistoryShareStatus) GetToken() string { - if x != nil { - return x.Token - } - return "" -} - -func (x *HistoryShareStatus) GetCreatedAt() int64 { - if x != nil { - return x.CreatedAt - } - return 0 -} - -func (x *HistoryShareStatus) GetUpdatedAt() int64 { - if x != nil { - return x.UpdatedAt - } - return 0 -} - -func (x *HistoryShareStatus) GetRedactToolContent() bool { - if x != nil { - return x.RedactToolContent - } - return false -} - -type HistoryShareGetRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HistoryShareGetRequest) Reset() { - *x = HistoryShareGetRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[77] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HistoryShareGetRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*HistoryShareGetRequest) ProtoMessage() {} - -func (x *HistoryShareGetRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[77] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use HistoryShareGetRequest.ProtoReflect.Descriptor instead. -func (*HistoryShareGetRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{77} -} - -func (x *HistoryShareGetRequest) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -type HistoryShareGetResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Share *HistoryShareStatus `protobuf:"bytes,1,opt,name=share,proto3" json:"share,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HistoryShareGetResponse) Reset() { - *x = HistoryShareGetResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[78] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HistoryShareGetResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*HistoryShareGetResponse) ProtoMessage() {} - -func (x *HistoryShareGetResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[78] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use HistoryShareGetResponse.ProtoReflect.Descriptor instead. -func (*HistoryShareGetResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{78} -} - -func (x *HistoryShareGetResponse) GetShare() *HistoryShareStatus { - if x != nil { - return x.Share - } - return nil -} - -type HistoryShareSetRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - Enabled bool `protobuf:"varint,2,opt,name=enabled,proto3" json:"enabled,omitempty"` - RedactToolContent *bool `protobuf:"varint,3,opt,name=redact_tool_content,json=redactToolContent,proto3,oneof" json:"redact_tool_content,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HistoryShareSetRequest) Reset() { - *x = HistoryShareSetRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[79] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HistoryShareSetRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*HistoryShareSetRequest) ProtoMessage() {} - -func (x *HistoryShareSetRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[79] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use HistoryShareSetRequest.ProtoReflect.Descriptor instead. -func (*HistoryShareSetRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{79} -} - -func (x *HistoryShareSetRequest) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -func (x *HistoryShareSetRequest) GetEnabled() bool { - if x != nil { - return x.Enabled - } - return false -} - -func (x *HistoryShareSetRequest) GetRedactToolContent() bool { - if x != nil && x.RedactToolContent != nil { - return *x.RedactToolContent - } - return false -} - -type HistoryShareSetResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Share *HistoryShareStatus `protobuf:"bytes,1,opt,name=share,proto3" json:"share,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HistoryShareSetResponse) Reset() { - *x = HistoryShareSetResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[80] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HistoryShareSetResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*HistoryShareSetResponse) ProtoMessage() {} - -func (x *HistoryShareSetResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[80] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use HistoryShareSetResponse.ProtoReflect.Descriptor instead. -func (*HistoryShareSetResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{80} -} - -func (x *HistoryShareSetResponse) GetShare() *HistoryShareStatus { - if x != nil { - return x.Share - } - return nil -} - -type HistoryShareResolveRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HistoryShareResolveRequest) Reset() { - *x = HistoryShareResolveRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[81] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HistoryShareResolveRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*HistoryShareResolveRequest) ProtoMessage() {} - -func (x *HistoryShareResolveRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[81] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use HistoryShareResolveRequest.ProtoReflect.Descriptor instead. -func (*HistoryShareResolveRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{81} -} - -func (x *HistoryShareResolveRequest) GetToken() string { - if x != nil { - return x.Token - } - return "" -} - -type HistoryShareResolveResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - MessagesJson string `protobuf:"bytes,2,opt,name=messages_json,json=messagesJson,proto3" json:"messages_json,omitempty"` - TotalMessageCount int32 `protobuf:"varint,3,opt,name=total_message_count,json=totalMessageCount,proto3" json:"total_message_count,omitempty"` - Conversation *ConversationSummary `protobuf:"bytes,4,opt,name=conversation,proto3" json:"conversation,omitempty"` - RedactToolContent bool `protobuf:"varint,5,opt,name=redact_tool_content,json=redactToolContent,proto3" json:"redact_tool_content,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HistoryShareResolveResponse) Reset() { - *x = HistoryShareResolveResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[82] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HistoryShareResolveResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*HistoryShareResolveResponse) ProtoMessage() {} - -func (x *HistoryShareResolveResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[82] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use HistoryShareResolveResponse.ProtoReflect.Descriptor instead. -func (*HistoryShareResolveResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{82} -} - -func (x *HistoryShareResolveResponse) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -func (x *HistoryShareResolveResponse) GetMessagesJson() string { - if x != nil { - return x.MessagesJson - } - return "" -} - -func (x *HistoryShareResolveResponse) GetTotalMessageCount() int32 { - if x != nil { - return x.TotalMessageCount - } - return 0 -} - -func (x *HistoryShareResolveResponse) GetConversation() *ConversationSummary { - if x != nil { - return x.Conversation - } - return nil -} - -func (x *HistoryShareResolveResponse) GetRedactToolContent() bool { - if x != nil { - return x.RedactToolContent - } - return false -} - -type HistoryWorkdirsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HistoryWorkdirsRequest) Reset() { - *x = HistoryWorkdirsRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[83] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HistoryWorkdirsRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*HistoryWorkdirsRequest) ProtoMessage() {} - -func (x *HistoryWorkdirsRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[83] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use HistoryWorkdirsRequest.ProtoReflect.Descriptor instead. -func (*HistoryWorkdirsRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{83} -} - -type HistoryWorkdirSummary struct { - state protoimpl.MessageState `protogen:"open.v1"` - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` - ConversationCount int32 `protobuf:"varint,2,opt,name=conversation_count,json=conversationCount,proto3" json:"conversation_count,omitempty"` - UpdatedAt int64 `protobuf:"varint,3,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HistoryWorkdirSummary) Reset() { - *x = HistoryWorkdirSummary{} - mi := &file_proto_v2_gateway_proto_msgTypes[84] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HistoryWorkdirSummary) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*HistoryWorkdirSummary) ProtoMessage() {} - -func (x *HistoryWorkdirSummary) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[84] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use HistoryWorkdirSummary.ProtoReflect.Descriptor instead. -func (*HistoryWorkdirSummary) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{84} -} - -func (x *HistoryWorkdirSummary) GetPath() string { - if x != nil { - return x.Path - } - return "" -} - -func (x *HistoryWorkdirSummary) GetConversationCount() int32 { - if x != nil { - return x.ConversationCount - } - return 0 -} - -func (x *HistoryWorkdirSummary) GetUpdatedAt() int64 { - if x != nil { - return x.UpdatedAt - } - return 0 -} - -type HistoryWorkdirsResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Workdirs []*HistoryWorkdirSummary `protobuf:"bytes,1,rep,name=workdirs,proto3" json:"workdirs,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HistoryWorkdirsResponse) Reset() { - *x = HistoryWorkdirsResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[85] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HistoryWorkdirsResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*HistoryWorkdirsResponse) ProtoMessage() {} - -func (x *HistoryWorkdirsResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[85] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use HistoryWorkdirsResponse.ProtoReflect.Descriptor instead. -func (*HistoryWorkdirsResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{85} -} - -func (x *HistoryWorkdirsResponse) GetWorkdirs() []*HistoryWorkdirSummary { - if x != nil { - return x.Workdirs - } - return nil -} - -type HistoryDeleteRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HistoryDeleteRequest) Reset() { - *x = HistoryDeleteRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[86] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HistoryDeleteRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*HistoryDeleteRequest) ProtoMessage() {} - -func (x *HistoryDeleteRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[86] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use HistoryDeleteRequest.ProtoReflect.Descriptor instead. -func (*HistoryDeleteRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{86} -} - -func (x *HistoryDeleteRequest) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -type HistoryDeleteResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HistoryDeleteResponse) Reset() { - *x = HistoryDeleteResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[87] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HistoryDeleteResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*HistoryDeleteResponse) ProtoMessage() {} - -func (x *HistoryDeleteResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[87] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use HistoryDeleteResponse.ProtoReflect.Descriptor instead. -func (*HistoryDeleteResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{87} -} - -type HistorySyncEvent struct { - state protoimpl.MessageState `protogen:"open.v1"` - Kind string `protobuf:"bytes,1,opt,name=kind,proto3" json:"kind,omitempty"` - Conversation *ConversationSummary `protobuf:"bytes,2,opt,name=conversation,proto3" json:"conversation,omitempty"` - ConversationId string `protobuf:"bytes,3,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HistorySyncEvent) Reset() { - *x = HistorySyncEvent{} - mi := &file_proto_v2_gateway_proto_msgTypes[88] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HistorySyncEvent) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*HistorySyncEvent) ProtoMessage() {} - -func (x *HistorySyncEvent) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[88] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use HistorySyncEvent.ProtoReflect.Descriptor instead. -func (*HistorySyncEvent) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{88} -} - -func (x *HistorySyncEvent) GetKind() string { - if x != nil { - return x.Kind - } - return "" -} - -func (x *HistorySyncEvent) GetConversation() *ConversationSummary { - if x != nil { - return x.Conversation - } - return nil -} - -func (x *HistorySyncEvent) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -type ProviderListRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ProviderListRequest) Reset() { - *x = ProviderListRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[89] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ProviderListRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ProviderListRequest) ProtoMessage() {} - -func (x *ProviderListRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[89] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ProviderListRequest.ProtoReflect.Descriptor instead. -func (*ProviderListRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{89} -} - -type ProviderListResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - ProvidersJson string `protobuf:"bytes,1,opt,name=providers_json,json=providersJson,proto3" json:"providers_json,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ProviderListResponse) Reset() { - *x = ProviderListResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[90] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ProviderListResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ProviderListResponse) ProtoMessage() {} - -func (x *ProviderListResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[90] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ProviderListResponse.ProtoReflect.Descriptor instead. -func (*ProviderListResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{90} -} - -func (x *ProviderListResponse) GetProvidersJson() string { - if x != nil { - return x.ProvidersJson - } - return "" -} - -type SettingsGetRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SettingsGetRequest) Reset() { - *x = SettingsGetRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[91] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SettingsGetRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SettingsGetRequest) ProtoMessage() {} - -func (x *SettingsGetRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[91] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SettingsGetRequest.ProtoReflect.Descriptor instead. -func (*SettingsGetRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{91} -} - -type SettingsGetResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - SettingsJson string `protobuf:"bytes,1,opt,name=settings_json,json=settingsJson,proto3" json:"settings_json,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SettingsGetResponse) Reset() { - *x = SettingsGetResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[92] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SettingsGetResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SettingsGetResponse) ProtoMessage() {} - -func (x *SettingsGetResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[92] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SettingsGetResponse.ProtoReflect.Descriptor instead. -func (*SettingsGetResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{92} -} - -func (x *SettingsGetResponse) GetSettingsJson() string { - if x != nil { - return x.SettingsJson - } - return "" -} - -type SettingsUpdateRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - SettingsJson string `protobuf:"bytes,1,opt,name=settings_json,json=settingsJson,proto3" json:"settings_json,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SettingsUpdateRequest) Reset() { - *x = SettingsUpdateRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[93] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SettingsUpdateRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SettingsUpdateRequest) ProtoMessage() {} - -func (x *SettingsUpdateRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[93] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SettingsUpdateRequest.ProtoReflect.Descriptor instead. -func (*SettingsUpdateRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{93} -} - -func (x *SettingsUpdateRequest) GetSettingsJson() string { - if x != nil { - return x.SettingsJson - } - return "" -} - -type SettingsUpdateResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Accepted bool `protobuf:"varint,1,opt,name=accepted,proto3" json:"accepted,omitempty"` - Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SettingsUpdateResponse) Reset() { - *x = SettingsUpdateResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[94] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SettingsUpdateResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SettingsUpdateResponse) ProtoMessage() {} - -func (x *SettingsUpdateResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[94] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SettingsUpdateResponse.ProtoReflect.Descriptor instead. -func (*SettingsUpdateResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{94} -} - -func (x *SettingsUpdateResponse) GetAccepted() bool { - if x != nil { - return x.Accepted - } - return false -} - -func (x *SettingsUpdateResponse) GetMessage() string { - if x != nil { - return x.Message - } - return "" -} - -type SettingsResetSshKnownHostRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Host string `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"` - Port uint32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SettingsResetSshKnownHostRequest) Reset() { - *x = SettingsResetSshKnownHostRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[95] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SettingsResetSshKnownHostRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SettingsResetSshKnownHostRequest) ProtoMessage() {} - -func (x *SettingsResetSshKnownHostRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[95] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SettingsResetSshKnownHostRequest.ProtoReflect.Descriptor instead. -func (*SettingsResetSshKnownHostRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{95} -} - -func (x *SettingsResetSshKnownHostRequest) GetHost() string { - if x != nil { - return x.Host - } - return "" -} - -func (x *SettingsResetSshKnownHostRequest) GetPort() uint32 { - if x != nil { - return x.Port - } - return 0 -} - -type SettingsResetSshKnownHostResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Deleted uint32 `protobuf:"varint,1,opt,name=deleted,proto3" json:"deleted,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SettingsResetSshKnownHostResponse) Reset() { - *x = SettingsResetSshKnownHostResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[96] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SettingsResetSshKnownHostResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SettingsResetSshKnownHostResponse) ProtoMessage() {} - -func (x *SettingsResetSshKnownHostResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[96] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SettingsResetSshKnownHostResponse.ProtoReflect.Descriptor instead. -func (*SettingsResetSshKnownHostResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{96} -} - -func (x *SettingsResetSshKnownHostResponse) GetDeleted() uint32 { - if x != nil { - return x.Deleted - } - return 0 -} - -type SettingsSyncEvent struct { - state protoimpl.MessageState `protogen:"open.v1"` - SettingsJson string `protobuf:"bytes,1,opt,name=settings_json,json=settingsJson,proto3" json:"settings_json,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SettingsSyncEvent) Reset() { - *x = SettingsSyncEvent{} - mi := &file_proto_v2_gateway_proto_msgTypes[97] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SettingsSyncEvent) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SettingsSyncEvent) ProtoMessage() {} - -func (x *SettingsSyncEvent) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[97] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SettingsSyncEvent.ProtoReflect.Descriptor instead. -func (*SettingsSyncEvent) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{97} -} - -func (x *SettingsSyncEvent) GetSettingsJson() string { - if x != nil { - return x.SettingsJson - } - return "" -} - -type SkillFilesListRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SkillFilesListRequest) Reset() { - *x = SkillFilesListRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[98] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SkillFilesListRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SkillFilesListRequest) ProtoMessage() {} - -func (x *SkillFilesListRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[98] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SkillFilesListRequest.ProtoReflect.Descriptor instead. -func (*SkillFilesListRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{98} -} - -type SkillFilesListResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - RootDir string `protobuf:"bytes,1,opt,name=root_dir,json=rootDir,proto3" json:"root_dir,omitempty"` - Paths []string `protobuf:"bytes,2,rep,name=paths,proto3" json:"paths,omitempty"` - Truncated bool `protobuf:"varint,3,opt,name=truncated,proto3" json:"truncated,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SkillFilesListResponse) Reset() { - *x = SkillFilesListResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[99] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SkillFilesListResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SkillFilesListResponse) ProtoMessage() {} - -func (x *SkillFilesListResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[99] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SkillFilesListResponse.ProtoReflect.Descriptor instead. -func (*SkillFilesListResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{99} -} - -func (x *SkillFilesListResponse) GetRootDir() string { - if x != nil { - return x.RootDir - } - return "" -} - -func (x *SkillFilesListResponse) GetPaths() []string { - if x != nil { - return x.Paths - } - return nil -} - -func (x *SkillFilesListResponse) GetTruncated() bool { - if x != nil { - return x.Truncated - } - return false -} - -type SkillMetadataReadRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SkillMetadataReadRequest) Reset() { - *x = SkillMetadataReadRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[100] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SkillMetadataReadRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SkillMetadataReadRequest) ProtoMessage() {} - -func (x *SkillMetadataReadRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[100] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SkillMetadataReadRequest.ProtoReflect.Descriptor instead. -func (*SkillMetadataReadRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{100} -} - -func (x *SkillMetadataReadRequest) GetPath() string { - if x != nil { - return x.Path - } - return "" -} - -type SkillMetadataReadResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SkillMetadataReadResponse) Reset() { - *x = SkillMetadataReadResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[101] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SkillMetadataReadResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SkillMetadataReadResponse) ProtoMessage() {} - -func (x *SkillMetadataReadResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[101] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SkillMetadataReadResponse.ProtoReflect.Descriptor instead. -func (*SkillMetadataReadResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{101} -} - -func (x *SkillMetadataReadResponse) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *SkillMetadataReadResponse) GetDescription() string { - if x != nil { - return x.Description - } - return "" -} - -type SkillTextReadRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` - Offset uint32 `protobuf:"varint,2,opt,name=offset,proto3" json:"offset,omitempty"` - Length uint32 `protobuf:"varint,3,opt,name=length,proto3" json:"length,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SkillTextReadRequest) Reset() { - *x = SkillTextReadRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[102] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SkillTextReadRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SkillTextReadRequest) ProtoMessage() {} - -func (x *SkillTextReadRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[102] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SkillTextReadRequest.ProtoReflect.Descriptor instead. -func (*SkillTextReadRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{102} -} - -func (x *SkillTextReadRequest) GetPath() string { - if x != nil { - return x.Path - } - return "" -} - -func (x *SkillTextReadRequest) GetOffset() uint32 { - if x != nil { - return x.Offset - } - return 0 -} - -func (x *SkillTextReadRequest) GetLength() uint32 { - if x != nil { - return x.Length - } - return 0 -} - -type SkillTextReadResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Content string `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` - Truncated bool `protobuf:"varint,2,opt,name=truncated,proto3" json:"truncated,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SkillTextReadResponse) Reset() { - *x = SkillTextReadResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[103] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SkillTextReadResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SkillTextReadResponse) ProtoMessage() {} - -func (x *SkillTextReadResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[103] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SkillTextReadResponse.ProtoReflect.Descriptor instead. -func (*SkillTextReadResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{103} -} - -func (x *SkillTextReadResponse) GetContent() string { - if x != nil { - return x.Content - } - return "" -} - -func (x *SkillTextReadResponse) GetTruncated() bool { - if x != nil { - return x.Truncated - } - return false -} - -type SkillManageRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - PayloadJson string `protobuf:"bytes,1,opt,name=payload_json,json=payloadJson,proto3" json:"payload_json,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SkillManageRequest) Reset() { - *x = SkillManageRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[104] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SkillManageRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SkillManageRequest) ProtoMessage() {} - -func (x *SkillManageRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[104] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SkillManageRequest.ProtoReflect.Descriptor instead. -func (*SkillManageRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{104} -} - -func (x *SkillManageRequest) GetPayloadJson() string { - if x != nil { - return x.PayloadJson - } - return "" -} - -type SkillManageResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - ResultJson string `protobuf:"bytes,1,opt,name=result_json,json=resultJson,proto3" json:"result_json,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SkillManageResponse) Reset() { - *x = SkillManageResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[105] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SkillManageResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SkillManageResponse) ProtoMessage() {} - -func (x *SkillManageResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[105] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SkillManageResponse.ProtoReflect.Descriptor instead. -func (*SkillManageResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{105} -} - -func (x *SkillManageResponse) GetResultJson() string { - if x != nil { - return x.ResultJson - } - return "" -} - -type FileMentionListRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Workdir string `protobuf:"bytes,1,opt,name=workdir,proto3" json:"workdir,omitempty"` - MaxResults uint32 `protobuf:"varint,2,opt,name=max_results,json=maxResults,proto3" json:"max_results,omitempty"` - Query string `protobuf:"bytes,3,opt,name=query,proto3" json:"query,omitempty"` - ShowHidden *bool `protobuf:"varint,4,opt,name=show_hidden,json=showHidden,proto3,oneof" json:"show_hidden,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *FileMentionListRequest) Reset() { - *x = FileMentionListRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[106] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *FileMentionListRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FileMentionListRequest) ProtoMessage() {} - -func (x *FileMentionListRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[106] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use FileMentionListRequest.ProtoReflect.Descriptor instead. -func (*FileMentionListRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{106} -} - -func (x *FileMentionListRequest) GetWorkdir() string { - if x != nil { - return x.Workdir - } - return "" -} - -func (x *FileMentionListRequest) GetMaxResults() uint32 { - if x != nil { - return x.MaxResults - } - return 0 -} - -func (x *FileMentionListRequest) GetQuery() string { - if x != nil { - return x.Query - } - return "" -} - -func (x *FileMentionListRequest) GetShowHidden() bool { - if x != nil && x.ShowHidden != nil { - return *x.ShowHidden - } - return false -} - -type FileMentionEntry struct { - state protoimpl.MessageState `protogen:"open.v1"` - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` - Kind string `protobuf:"bytes,2,opt,name=kind,proto3" json:"kind,omitempty"` - Hidden bool `protobuf:"varint,3,opt,name=hidden,proto3" json:"hidden,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *FileMentionEntry) Reset() { - *x = FileMentionEntry{} - mi := &file_proto_v2_gateway_proto_msgTypes[107] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *FileMentionEntry) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FileMentionEntry) ProtoMessage() {} - -func (x *FileMentionEntry) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[107] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use FileMentionEntry.ProtoReflect.Descriptor instead. -func (*FileMentionEntry) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{107} -} - -func (x *FileMentionEntry) GetPath() string { - if x != nil { - return x.Path - } - return "" -} - -func (x *FileMentionEntry) GetKind() string { - if x != nil { - return x.Kind - } - return "" -} - -func (x *FileMentionEntry) GetHidden() bool { - if x != nil { - return x.Hidden - } - return false -} - -type FileMentionListResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Entries []*FileMentionEntry `protobuf:"bytes,1,rep,name=entries,proto3" json:"entries,omitempty"` - Truncated bool `protobuf:"varint,2,opt,name=truncated,proto3" json:"truncated,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *FileMentionListResponse) Reset() { - *x = FileMentionListResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[108] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *FileMentionListResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FileMentionListResponse) ProtoMessage() {} - -func (x *FileMentionListResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[108] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use FileMentionListResponse.ProtoReflect.Descriptor instead. -func (*FileMentionListResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{108} -} - -func (x *FileMentionListResponse) GetEntries() []*FileMentionEntry { - if x != nil { - return x.Entries - } - return nil -} - -func (x *FileMentionListResponse) GetTruncated() bool { - if x != nil { - return x.Truncated - } - return false -} - -type FsRoot struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` - Kind string `protobuf:"bytes,3,opt,name=kind,proto3" json:"kind,omitempty"` - Label string `protobuf:"bytes,4,opt,name=label,proto3" json:"label,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *FsRoot) Reset() { - *x = FsRoot{} - mi := &file_proto_v2_gateway_proto_msgTypes[109] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *FsRoot) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FsRoot) ProtoMessage() {} - -func (x *FsRoot) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[109] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use FsRoot.ProtoReflect.Descriptor instead. -func (*FsRoot) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{109} -} - -func (x *FsRoot) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *FsRoot) GetPath() string { - if x != nil { - return x.Path - } - return "" -} - -func (x *FsRoot) GetKind() string { - if x != nil { - return x.Kind - } - return "" -} - -func (x *FsRoot) GetLabel() string { - if x != nil { - return x.Label - } - return "" -} - -type FsRootsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *FsRootsRequest) Reset() { - *x = FsRootsRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[110] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *FsRootsRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FsRootsRequest) ProtoMessage() {} - -func (x *FsRootsRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[110] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use FsRootsRequest.ProtoReflect.Descriptor instead. -func (*FsRootsRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{110} -} - -type FsRootsResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Roots []*FsRoot `protobuf:"bytes,1,rep,name=roots,proto3" json:"roots,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *FsRootsResponse) Reset() { - *x = FsRootsResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[111] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *FsRootsResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FsRootsResponse) ProtoMessage() {} - -func (x *FsRootsResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[111] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use FsRootsResponse.ProtoReflect.Descriptor instead. -func (*FsRootsResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{111} -} - -func (x *FsRootsResponse) GetRoots() []*FsRoot { - if x != nil { - return x.Roots - } - return nil -} - -type FsListDirsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` - MaxResults uint32 `protobuf:"varint,2,opt,name=max_results,json=maxResults,proto3" json:"max_results,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *FsListDirsRequest) Reset() { - *x = FsListDirsRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[112] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *FsListDirsRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FsListDirsRequest) ProtoMessage() {} - -func (x *FsListDirsRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[112] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use FsListDirsRequest.ProtoReflect.Descriptor instead. -func (*FsListDirsRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{112} -} - -func (x *FsListDirsRequest) GetPath() string { - if x != nil { - return x.Path - } - return "" -} - -func (x *FsListDirsRequest) GetMaxResults() uint32 { - if x != nil { - return x.MaxResults - } - return 0 -} - -type FsDirEntry struct { - state protoimpl.MessageState `protogen:"open.v1"` - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *FsDirEntry) Reset() { - *x = FsDirEntry{} - mi := &file_proto_v2_gateway_proto_msgTypes[113] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *FsDirEntry) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FsDirEntry) ProtoMessage() {} - -func (x *FsDirEntry) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[113] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use FsDirEntry.ProtoReflect.Descriptor instead. -func (*FsDirEntry) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{113} -} - -func (x *FsDirEntry) GetPath() string { - if x != nil { - return x.Path - } - return "" -} - -func (x *FsDirEntry) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -type FsListDirsResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` - Entries []*FsDirEntry `protobuf:"bytes,2,rep,name=entries,proto3" json:"entries,omitempty"` - Truncated bool `protobuf:"varint,3,opt,name=truncated,proto3" json:"truncated,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *FsListDirsResponse) Reset() { - *x = FsListDirsResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[114] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *FsListDirsResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FsListDirsResponse) ProtoMessage() {} - -func (x *FsListDirsResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[114] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use FsListDirsResponse.ProtoReflect.Descriptor instead. -func (*FsListDirsResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{114} -} - -func (x *FsListDirsResponse) GetPath() string { - if x != nil { - return x.Path - } - return "" -} - -func (x *FsListDirsResponse) GetEntries() []*FsDirEntry { - if x != nil { - return x.Entries - } - return nil -} - -func (x *FsListDirsResponse) GetTruncated() bool { - if x != nil { - return x.Truncated - } - return false -} - -type FsCreateProjectFolderRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"` - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *FsCreateProjectFolderRequest) Reset() { - *x = FsCreateProjectFolderRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[115] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *FsCreateProjectFolderRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FsCreateProjectFolderRequest) ProtoMessage() {} - -func (x *FsCreateProjectFolderRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[115] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use FsCreateProjectFolderRequest.ProtoReflect.Descriptor instead. -func (*FsCreateProjectFolderRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{115} -} - -func (x *FsCreateProjectFolderRequest) GetParent() string { - if x != nil { - return x.Parent - } - return "" -} - -func (x *FsCreateProjectFolderRequest) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -type FsCreateProjectFolderResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *FsCreateProjectFolderResponse) Reset() { - *x = FsCreateProjectFolderResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[116] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *FsCreateProjectFolderResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FsCreateProjectFolderResponse) ProtoMessage() {} - -func (x *FsCreateProjectFolderResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[116] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use FsCreateProjectFolderResponse.ProtoReflect.Descriptor instead. -func (*FsCreateProjectFolderResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{116} -} - -func (x *FsCreateProjectFolderResponse) GetPath() string { - if x != nil { - return x.Path - } - return "" -} - -type FsListRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Workdir string `protobuf:"bytes,1,opt,name=workdir,proto3" json:"workdir,omitempty"` - Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` - Depth uint32 `protobuf:"varint,3,opt,name=depth,proto3" json:"depth,omitempty"` - Offset uint32 `protobuf:"varint,4,opt,name=offset,proto3" json:"offset,omitempty"` - MaxResults uint32 `protobuf:"varint,5,opt,name=max_results,json=maxResults,proto3" json:"max_results,omitempty"` - ShowHidden *bool `protobuf:"varint,6,opt,name=show_hidden,json=showHidden,proto3,oneof" json:"show_hidden,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *FsListRequest) Reset() { - *x = FsListRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[117] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *FsListRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FsListRequest) ProtoMessage() {} - -func (x *FsListRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[117] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use FsListRequest.ProtoReflect.Descriptor instead. -func (*FsListRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{117} -} - -func (x *FsListRequest) GetWorkdir() string { - if x != nil { - return x.Workdir - } - return "" -} - -func (x *FsListRequest) GetPath() string { - if x != nil { - return x.Path - } - return "" -} - -func (x *FsListRequest) GetDepth() uint32 { - if x != nil { - return x.Depth - } - return 0 -} - -func (x *FsListRequest) GetOffset() uint32 { - if x != nil { - return x.Offset - } - return 0 -} - -func (x *FsListRequest) GetMaxResults() uint32 { - if x != nil { - return x.MaxResults - } - return 0 -} - -func (x *FsListRequest) GetShowHidden() bool { - if x != nil && x.ShowHidden != nil { - return *x.ShowHidden - } - return false -} - -type FsListEntry struct { - state protoimpl.MessageState `protogen:"open.v1"` - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` - Kind string `protobuf:"bytes,2,opt,name=kind,proto3" json:"kind,omitempty"` - Hidden bool `protobuf:"varint,3,opt,name=hidden,proto3" json:"hidden,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *FsListEntry) Reset() { - *x = FsListEntry{} - mi := &file_proto_v2_gateway_proto_msgTypes[118] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *FsListEntry) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FsListEntry) ProtoMessage() {} - -func (x *FsListEntry) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[118] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use FsListEntry.ProtoReflect.Descriptor instead. -func (*FsListEntry) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{118} -} - -func (x *FsListEntry) GetPath() string { - if x != nil { - return x.Path - } - return "" -} - -func (x *FsListEntry) GetKind() string { - if x != nil { - return x.Kind - } - return "" -} - -func (x *FsListEntry) GetHidden() bool { - if x != nil { - return x.Hidden - } - return false -} - -type FsListResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` - HasPath bool `protobuf:"varint,2,opt,name=has_path,json=hasPath,proto3" json:"has_path,omitempty"` - Depth uint32 `protobuf:"varint,3,opt,name=depth,proto3" json:"depth,omitempty"` - Offset uint32 `protobuf:"varint,4,opt,name=offset,proto3" json:"offset,omitempty"` - MaxResults uint32 `protobuf:"varint,5,opt,name=max_results,json=maxResults,proto3" json:"max_results,omitempty"` - Total uint32 `protobuf:"varint,6,opt,name=total,proto3" json:"total,omitempty"` - HasMore bool `protobuf:"varint,7,opt,name=has_more,json=hasMore,proto3" json:"has_more,omitempty"` - Entries []*FsListEntry `protobuf:"bytes,8,rep,name=entries,proto3" json:"entries,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *FsListResponse) Reset() { - *x = FsListResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[119] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *FsListResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FsListResponse) ProtoMessage() {} - -func (x *FsListResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[119] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use FsListResponse.ProtoReflect.Descriptor instead. -func (*FsListResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{119} -} - -func (x *FsListResponse) GetPath() string { - if x != nil { - return x.Path - } - return "" -} - -func (x *FsListResponse) GetHasPath() bool { - if x != nil { - return x.HasPath - } - return false -} - -func (x *FsListResponse) GetDepth() uint32 { - if x != nil { - return x.Depth - } - return 0 -} - -func (x *FsListResponse) GetOffset() uint32 { - if x != nil { - return x.Offset - } - return 0 -} - -func (x *FsListResponse) GetMaxResults() uint32 { - if x != nil { - return x.MaxResults - } - return 0 -} - -func (x *FsListResponse) GetTotal() uint32 { - if x != nil { - return x.Total - } - return 0 -} - -func (x *FsListResponse) GetHasMore() bool { - if x != nil { - return x.HasMore - } - return false -} - -func (x *FsListResponse) GetEntries() []*FsListEntry { - if x != nil { - return x.Entries - } - return nil -} - -type FsReadEditableTextRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Workdir string `protobuf:"bytes,1,opt,name=workdir,proto3" json:"workdir,omitempty"` - Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *FsReadEditableTextRequest) Reset() { - *x = FsReadEditableTextRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[120] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *FsReadEditableTextRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FsReadEditableTextRequest) ProtoMessage() {} - -func (x *FsReadEditableTextRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[120] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use FsReadEditableTextRequest.ProtoReflect.Descriptor instead. -func (*FsReadEditableTextRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{120} -} - -func (x *FsReadEditableTextRequest) GetWorkdir() string { - if x != nil { - return x.Workdir - } - return "" -} - -func (x *FsReadEditableTextRequest) GetPath() string { - if x != nil { - return x.Path - } - return "" -} - -type FsReadEditableTextResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` - Content string `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"` - MtimeMs uint64 `protobuf:"varint,3,opt,name=mtime_ms,json=mtimeMs,proto3" json:"mtime_ms,omitempty"` - ContentHash string `protobuf:"bytes,4,opt,name=content_hash,json=contentHash,proto3" json:"content_hash,omitempty"` - SizeBytes uint64 `protobuf:"varint,5,opt,name=size_bytes,json=sizeBytes,proto3" json:"size_bytes,omitempty"` - TotalLines uint64 `protobuf:"varint,6,opt,name=total_lines,json=totalLines,proto3" json:"total_lines,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *FsReadEditableTextResponse) Reset() { - *x = FsReadEditableTextResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[121] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *FsReadEditableTextResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FsReadEditableTextResponse) ProtoMessage() {} - -func (x *FsReadEditableTextResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[121] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use FsReadEditableTextResponse.ProtoReflect.Descriptor instead. -func (*FsReadEditableTextResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{121} -} - -func (x *FsReadEditableTextResponse) GetPath() string { - if x != nil { - return x.Path - } - return "" -} - -func (x *FsReadEditableTextResponse) GetContent() string { - if x != nil { - return x.Content - } - return "" -} - -func (x *FsReadEditableTextResponse) GetMtimeMs() uint64 { - if x != nil { - return x.MtimeMs - } - return 0 -} - -func (x *FsReadEditableTextResponse) GetContentHash() string { - if x != nil { - return x.ContentHash - } - return "" -} - -func (x *FsReadEditableTextResponse) GetSizeBytes() uint64 { - if x != nil { - return x.SizeBytes - } - return 0 -} - -func (x *FsReadEditableTextResponse) GetTotalLines() uint64 { - if x != nil { - return x.TotalLines - } - return 0 -} - -type FsReadWorkspaceImageRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Workdir string `protobuf:"bytes,1,opt,name=workdir,proto3" json:"workdir,omitempty"` - Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *FsReadWorkspaceImageRequest) Reset() { - *x = FsReadWorkspaceImageRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[122] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *FsReadWorkspaceImageRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FsReadWorkspaceImageRequest) ProtoMessage() {} - -func (x *FsReadWorkspaceImageRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[122] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use FsReadWorkspaceImageRequest.ProtoReflect.Descriptor instead. -func (*FsReadWorkspaceImageRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{122} -} - -func (x *FsReadWorkspaceImageRequest) GetWorkdir() string { - if x != nil { - return x.Workdir - } - return "" -} - -func (x *FsReadWorkspaceImageRequest) GetPath() string { - if x != nil { - return x.Path - } - return "" -} - -type FsReadWorkspaceImageResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` - MimeType string `protobuf:"bytes,2,opt,name=mime_type,json=mimeType,proto3" json:"mime_type,omitempty"` - Data string `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` - SizeBytes uint64 `protobuf:"varint,4,opt,name=size_bytes,json=sizeBytes,proto3" json:"size_bytes,omitempty"` - MtimeMs uint64 `protobuf:"varint,5,opt,name=mtime_ms,json=mtimeMs,proto3" json:"mtime_ms,omitempty"` - ContentHash string `protobuf:"bytes,6,opt,name=content_hash,json=contentHash,proto3" json:"content_hash,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *FsReadWorkspaceImageResponse) Reset() { - *x = FsReadWorkspaceImageResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[123] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *FsReadWorkspaceImageResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FsReadWorkspaceImageResponse) ProtoMessage() {} - -func (x *FsReadWorkspaceImageResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[123] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use FsReadWorkspaceImageResponse.ProtoReflect.Descriptor instead. -func (*FsReadWorkspaceImageResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{123} -} - -func (x *FsReadWorkspaceImageResponse) GetPath() string { - if x != nil { - return x.Path - } - return "" -} - -func (x *FsReadWorkspaceImageResponse) GetMimeType() string { - if x != nil { - return x.MimeType - } - return "" -} - -func (x *FsReadWorkspaceImageResponse) GetData() string { - if x != nil { - return x.Data - } - return "" -} - -func (x *FsReadWorkspaceImageResponse) GetSizeBytes() uint64 { - if x != nil { - return x.SizeBytes - } - return 0 -} - -func (x *FsReadWorkspaceImageResponse) GetMtimeMs() uint64 { - if x != nil { - return x.MtimeMs - } - return 0 -} - -func (x *FsReadWorkspaceImageResponse) GetContentHash() string { - if x != nil { - return x.ContentHash - } - return "" -} - -type ChatFileOpenRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - Workdir string `protobuf:"bytes,2,opt,name=workdir,proto3" json:"workdir,omitempty"` - Path string `protobuf:"bytes,3,opt,name=path,proto3" json:"path,omitempty"` - Source string `protobuf:"bytes,4,opt,name=source,proto3" json:"source,omitempty"` - Line *uint32 `protobuf:"varint,5,opt,name=line,proto3,oneof" json:"line,omitempty"` - EndLine *uint32 `protobuf:"varint,6,opt,name=end_line,json=endLine,proto3,oneof" json:"end_line,omitempty"` - Column *uint32 `protobuf:"varint,7,opt,name=column,proto3,oneof" json:"column,omitempty"` - OpenInFileManager bool `protobuf:"varint,8,opt,name=open_in_file_manager,json=openInFileManager,proto3" json:"open_in_file_manager,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatFileOpenRequest) Reset() { - *x = ChatFileOpenRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[124] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatFileOpenRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatFileOpenRequest) ProtoMessage() {} - -func (x *ChatFileOpenRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[124] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatFileOpenRequest.ProtoReflect.Descriptor instead. -func (*ChatFileOpenRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{124} -} - -func (x *ChatFileOpenRequest) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -func (x *ChatFileOpenRequest) GetWorkdir() string { - if x != nil { - return x.Workdir - } - return "" -} - -func (x *ChatFileOpenRequest) GetPath() string { - if x != nil { - return x.Path - } - return "" -} - -func (x *ChatFileOpenRequest) GetSource() string { - if x != nil { - return x.Source - } - return "" -} - -func (x *ChatFileOpenRequest) GetLine() uint32 { - if x != nil && x.Line != nil { - return *x.Line - } - return 0 -} - -func (x *ChatFileOpenRequest) GetEndLine() uint32 { - if x != nil && x.EndLine != nil { - return *x.EndLine - } - return 0 -} - -func (x *ChatFileOpenRequest) GetColumn() uint32 { - if x != nil && x.Column != nil { - return *x.Column - } - return 0 -} - -func (x *ChatFileOpenRequest) GetOpenInFileManager() bool { - if x != nil { - return x.OpenInFileManager - } - return false -} - -type ChatFileOpenResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Action string `protobuf:"bytes,1,opt,name=action,proto3" json:"action,omitempty"` - Kind string `protobuf:"bytes,2,opt,name=kind,proto3" json:"kind,omitempty"` - Workdir string `protobuf:"bytes,3,opt,name=workdir,proto3" json:"workdir,omitempty"` - Path string `protobuf:"bytes,4,opt,name=path,proto3" json:"path,omitempty"` - Line *uint32 `protobuf:"varint,5,opt,name=line,proto3,oneof" json:"line,omitempty"` - EndLine *uint32 `protobuf:"varint,6,opt,name=end_line,json=endLine,proto3,oneof" json:"end_line,omitempty"` - Column *uint32 `protobuf:"varint,7,opt,name=column,proto3,oneof" json:"column,omitempty"` - OutsideWorkspace bool `protobuf:"varint,8,opt,name=outside_workspace,json=outsideWorkspace,proto3" json:"outside_workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatFileOpenResponse) Reset() { - *x = ChatFileOpenResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[125] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatFileOpenResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatFileOpenResponse) ProtoMessage() {} - -func (x *ChatFileOpenResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[125] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatFileOpenResponse.ProtoReflect.Descriptor instead. -func (*ChatFileOpenResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{125} -} - -func (x *ChatFileOpenResponse) GetAction() string { - if x != nil { - return x.Action - } - return "" -} - -func (x *ChatFileOpenResponse) GetKind() string { - if x != nil { - return x.Kind - } - return "" -} - -func (x *ChatFileOpenResponse) GetWorkdir() string { - if x != nil { - return x.Workdir - } - return "" -} - -func (x *ChatFileOpenResponse) GetPath() string { - if x != nil { - return x.Path - } - return "" -} - -func (x *ChatFileOpenResponse) GetLine() uint32 { - if x != nil && x.Line != nil { - return *x.Line - } - return 0 -} - -func (x *ChatFileOpenResponse) GetEndLine() uint32 { - if x != nil && x.EndLine != nil { - return *x.EndLine - } - return 0 -} - -func (x *ChatFileOpenResponse) GetColumn() uint32 { - if x != nil && x.Column != nil { - return *x.Column - } - return 0 -} - -func (x *ChatFileOpenResponse) GetOutsideWorkspace() bool { - if x != nil { - return x.OutsideWorkspace - } - return false -} - -type FsWriteTextRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Workdir string `protobuf:"bytes,1,opt,name=workdir,proto3" json:"workdir,omitempty"` - Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` - Content string `protobuf:"bytes,3,opt,name=content,proto3" json:"content,omitempty"` - Mode string `protobuf:"bytes,4,opt,name=mode,proto3" json:"mode,omitempty"` - ExpectedMtimeMs uint64 `protobuf:"varint,5,opt,name=expected_mtime_ms,json=expectedMtimeMs,proto3" json:"expected_mtime_ms,omitempty"` - ExpectedContentHash string `protobuf:"bytes,6,opt,name=expected_content_hash,json=expectedContentHash,proto3" json:"expected_content_hash,omitempty"` - HasExpectedMtimeMs bool `protobuf:"varint,7,opt,name=has_expected_mtime_ms,json=hasExpectedMtimeMs,proto3" json:"has_expected_mtime_ms,omitempty"` - HasExpectedContentHash bool `protobuf:"varint,8,opt,name=has_expected_content_hash,json=hasExpectedContentHash,proto3" json:"has_expected_content_hash,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *FsWriteTextRequest) Reset() { - *x = FsWriteTextRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[126] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *FsWriteTextRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FsWriteTextRequest) ProtoMessage() {} - -func (x *FsWriteTextRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[126] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use FsWriteTextRequest.ProtoReflect.Descriptor instead. -func (*FsWriteTextRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{126} -} - -func (x *FsWriteTextRequest) GetWorkdir() string { - if x != nil { - return x.Workdir - } - return "" -} - -func (x *FsWriteTextRequest) GetPath() string { - if x != nil { - return x.Path - } - return "" -} - -func (x *FsWriteTextRequest) GetContent() string { - if x != nil { - return x.Content - } - return "" -} - -func (x *FsWriteTextRequest) GetMode() string { - if x != nil { - return x.Mode - } - return "" -} - -func (x *FsWriteTextRequest) GetExpectedMtimeMs() uint64 { - if x != nil { - return x.ExpectedMtimeMs - } - return 0 -} - -func (x *FsWriteTextRequest) GetExpectedContentHash() string { - if x != nil { - return x.ExpectedContentHash - } - return "" -} - -func (x *FsWriteTextRequest) GetHasExpectedMtimeMs() bool { - if x != nil { - return x.HasExpectedMtimeMs - } - return false -} - -func (x *FsWriteTextRequest) GetHasExpectedContentHash() bool { - if x != nil { - return x.HasExpectedContentHash - } - return false -} - -type FsWriteTextResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` - Mode string `protobuf:"bytes,2,opt,name=mode,proto3" json:"mode,omitempty"` - ExistedBefore bool `protobuf:"varint,3,opt,name=existed_before,json=existedBefore,proto3" json:"existed_before,omitempty"` - BytesWritten uint64 `protobuf:"varint,4,opt,name=bytes_written,json=bytesWritten,proto3" json:"bytes_written,omitempty"` - MtimeMs uint64 `protobuf:"varint,5,opt,name=mtime_ms,json=mtimeMs,proto3" json:"mtime_ms,omitempty"` - ContentHash string `protobuf:"bytes,6,opt,name=content_hash,json=contentHash,proto3" json:"content_hash,omitempty"` - TotalLines uint64 `protobuf:"varint,7,opt,name=total_lines,json=totalLines,proto3" json:"total_lines,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *FsWriteTextResponse) Reset() { - *x = FsWriteTextResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[127] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *FsWriteTextResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FsWriteTextResponse) ProtoMessage() {} - -func (x *FsWriteTextResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[127] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use FsWriteTextResponse.ProtoReflect.Descriptor instead. -func (*FsWriteTextResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{127} -} - -func (x *FsWriteTextResponse) GetPath() string { - if x != nil { - return x.Path - } - return "" -} - -func (x *FsWriteTextResponse) GetMode() string { - if x != nil { - return x.Mode - } - return "" -} - -func (x *FsWriteTextResponse) GetExistedBefore() bool { - if x != nil { - return x.ExistedBefore - } - return false -} - -func (x *FsWriteTextResponse) GetBytesWritten() uint64 { - if x != nil { - return x.BytesWritten - } - return 0 -} - -func (x *FsWriteTextResponse) GetMtimeMs() uint64 { - if x != nil { - return x.MtimeMs - } - return 0 -} - -func (x *FsWriteTextResponse) GetContentHash() string { - if x != nil { - return x.ContentHash - } - return "" -} - -func (x *FsWriteTextResponse) GetTotalLines() uint64 { - if x != nil { - return x.TotalLines - } - return 0 -} - -type FsCreateDirRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Workdir string `protobuf:"bytes,1,opt,name=workdir,proto3" json:"workdir,omitempty"` - Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *FsCreateDirRequest) Reset() { - *x = FsCreateDirRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[128] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *FsCreateDirRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FsCreateDirRequest) ProtoMessage() {} - -func (x *FsCreateDirRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[128] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use FsCreateDirRequest.ProtoReflect.Descriptor instead. -func (*FsCreateDirRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{128} -} - -func (x *FsCreateDirRequest) GetWorkdir() string { - if x != nil { - return x.Workdir - } - return "" -} - -func (x *FsCreateDirRequest) GetPath() string { - if x != nil { - return x.Path - } - return "" -} - -type FsCreateDirResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` - Kind string `protobuf:"bytes,2,opt,name=kind,proto3" json:"kind,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *FsCreateDirResponse) Reset() { - *x = FsCreateDirResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[129] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *FsCreateDirResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FsCreateDirResponse) ProtoMessage() {} - -func (x *FsCreateDirResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[129] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use FsCreateDirResponse.ProtoReflect.Descriptor instead. -func (*FsCreateDirResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{129} -} - -func (x *FsCreateDirResponse) GetPath() string { - if x != nil { - return x.Path - } - return "" -} - -func (x *FsCreateDirResponse) GetKind() string { - if x != nil { - return x.Kind - } - return "" -} - -type FsRenameRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Workdir string `protobuf:"bytes,1,opt,name=workdir,proto3" json:"workdir,omitempty"` - FromPath string `protobuf:"bytes,2,opt,name=from_path,json=fromPath,proto3" json:"from_path,omitempty"` - ToPath string `protobuf:"bytes,3,opt,name=to_path,json=toPath,proto3" json:"to_path,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *FsRenameRequest) Reset() { - *x = FsRenameRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[130] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *FsRenameRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FsRenameRequest) ProtoMessage() {} - -func (x *FsRenameRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[130] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use FsRenameRequest.ProtoReflect.Descriptor instead. -func (*FsRenameRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{130} -} - -func (x *FsRenameRequest) GetWorkdir() string { - if x != nil { - return x.Workdir - } - return "" -} - -func (x *FsRenameRequest) GetFromPath() string { - if x != nil { - return x.FromPath - } - return "" -} - -func (x *FsRenameRequest) GetToPath() string { - if x != nil { - return x.ToPath - } - return "" -} - -type FsRenameResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - FromPath string `protobuf:"bytes,1,opt,name=from_path,json=fromPath,proto3" json:"from_path,omitempty"` - Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` - Kind string `protobuf:"bytes,3,opt,name=kind,proto3" json:"kind,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *FsRenameResponse) Reset() { - *x = FsRenameResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[131] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *FsRenameResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FsRenameResponse) ProtoMessage() {} - -func (x *FsRenameResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[131] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use FsRenameResponse.ProtoReflect.Descriptor instead. -func (*FsRenameResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{131} -} - -func (x *FsRenameResponse) GetFromPath() string { - if x != nil { - return x.FromPath - } - return "" -} - -func (x *FsRenameResponse) GetPath() string { - if x != nil { - return x.Path - } - return "" -} - -func (x *FsRenameResponse) GetKind() string { - if x != nil { - return x.Kind - } - return "" -} - -type FsDeleteRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Workdir string `protobuf:"bytes,1,opt,name=workdir,proto3" json:"workdir,omitempty"` - Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *FsDeleteRequest) Reset() { - *x = FsDeleteRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[132] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *FsDeleteRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FsDeleteRequest) ProtoMessage() {} - -func (x *FsDeleteRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[132] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use FsDeleteRequest.ProtoReflect.Descriptor instead. -func (*FsDeleteRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{132} -} - -func (x *FsDeleteRequest) GetWorkdir() string { - if x != nil { - return x.Workdir - } - return "" -} - -func (x *FsDeleteRequest) GetPath() string { - if x != nil { - return x.Path - } - return "" -} - -type FsDeleteResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` - Kind string `protobuf:"bytes,2,opt,name=kind,proto3" json:"kind,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *FsDeleteResponse) Reset() { - *x = FsDeleteResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[133] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *FsDeleteResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FsDeleteResponse) ProtoMessage() {} - -func (x *FsDeleteResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[133] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use FsDeleteResponse.ProtoReflect.Descriptor instead. -func (*FsDeleteResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{133} -} - -func (x *FsDeleteResponse) GetPath() string { - if x != nil { - return x.Path - } - return "" -} - -func (x *FsDeleteResponse) GetKind() string { - if x != nil { - return x.Kind - } - return "" -} - -type PingRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Timestamp int64 `protobuf:"varint,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *PingRequest) Reset() { - *x = PingRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[134] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *PingRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*PingRequest) ProtoMessage() {} - -func (x *PingRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[134] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use PingRequest.ProtoReflect.Descriptor instead. -func (*PingRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{134} -} - -func (x *PingRequest) GetTimestamp() int64 { - if x != nil { - return x.Timestamp - } - return 0 -} - -type PongResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Timestamp int64 `protobuf:"varint,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *PongResponse) Reset() { - *x = PongResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[135] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *PongResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*PongResponse) ProtoMessage() {} - -func (x *PongResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[135] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use PongResponse.ProtoReflect.Descriptor instead. -func (*PongResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{135} -} - -func (x *PongResponse) GetTimestamp() int64 { - if x != nil { - return x.Timestamp - } - return 0 -} - -type ErrorResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` - Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ErrorResponse) Reset() { - *x = ErrorResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[136] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ErrorResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ErrorResponse) ProtoMessage() {} - -func (x *ErrorResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[136] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ErrorResponse.ProtoReflect.Descriptor instead. -func (*ErrorResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{136} -} - -func (x *ErrorResponse) GetCode() int32 { - if x != nil { - return x.Code - } - return 0 -} - -func (x *ErrorResponse) GetMessage() string { - if x != nil { - return x.Message - } - return "" -} - -type ProviderModelsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ProviderType string `protobuf:"bytes,1,opt,name=provider_type,json=providerType,proto3" json:"provider_type,omitempty"` - BaseUrl string `protobuf:"bytes,2,opt,name=base_url,json=baseUrl,proto3" json:"base_url,omitempty"` - ApiKey string `protobuf:"bytes,3,opt,name=api_key,json=apiKey,proto3" json:"api_key,omitempty"` - UseSystemProxy bool `protobuf:"varint,4,opt,name=use_system_proxy,json=useSystemProxy,proto3" json:"use_system_proxy,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ProviderModelsRequest) Reset() { - *x = ProviderModelsRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[137] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ProviderModelsRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ProviderModelsRequest) ProtoMessage() {} - -func (x *ProviderModelsRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[137] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ProviderModelsRequest.ProtoReflect.Descriptor instead. -func (*ProviderModelsRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{137} -} - -func (x *ProviderModelsRequest) GetProviderType() string { - if x != nil { - return x.ProviderType - } - return "" -} - -func (x *ProviderModelsRequest) GetBaseUrl() string { - if x != nil { - return x.BaseUrl - } - return "" -} - -func (x *ProviderModelsRequest) GetApiKey() string { - if x != nil { - return x.ApiKey - } - return "" -} - -func (x *ProviderModelsRequest) GetUseSystemProxy() bool { - if x != nil { - return x.UseSystemProxy - } - return false -} - -type ProviderModelsResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - ModelsJson string `protobuf:"bytes,1,opt,name=models_json,json=modelsJson,proto3" json:"models_json,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ProviderModelsResponse) Reset() { - *x = ProviderModelsResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[138] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ProviderModelsResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ProviderModelsResponse) ProtoMessage() {} - -func (x *ProviderModelsResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[138] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ProviderModelsResponse.ProtoReflect.Descriptor instead. -func (*ProviderModelsResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{138} -} - -func (x *ProviderModelsResponse) GetModelsJson() string { - if x != nil { - return x.ModelsJson - } - return "" -} - -type ProviderUsageRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ProviderId string `protobuf:"bytes,1,opt,name=provider_id,json=providerId,proto3" json:"provider_id,omitempty"` - Refresh bool `protobuf:"varint,2,opt,name=refresh,proto3" json:"refresh,omitempty"` - // 非空时为「按草稿测试」:桌面端按此 JSON 配置(UsageQueryConfig 形状)执行 - // 一次查询——忽略启用开关、不落库、不读写缓存;空串为常规查询。 - ConfigJson string `protobuf:"bytes,3,opt,name=config_json,json=configJson,proto3" json:"config_json,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ProviderUsageRequest) Reset() { - *x = ProviderUsageRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[139] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ProviderUsageRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ProviderUsageRequest) ProtoMessage() {} - -func (x *ProviderUsageRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[139] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ProviderUsageRequest.ProtoReflect.Descriptor instead. -func (*ProviderUsageRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{139} -} - -func (x *ProviderUsageRequest) GetProviderId() string { - if x != nil { - return x.ProviderId - } - return "" -} - -func (x *ProviderUsageRequest) GetRefresh() bool { - if x != nil { - return x.Refresh - } - return false -} - -func (x *ProviderUsageRequest) GetConfigJson() string { - if x != nil { - return x.ConfigJson - } - return "" -} - -type ProviderUsageResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - ResultJson string `protobuf:"bytes,1,opt,name=result_json,json=resultJson,proto3" json:"result_json,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ProviderUsageResponse) Reset() { - *x = ProviderUsageResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[140] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ProviderUsageResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ProviderUsageResponse) ProtoMessage() {} - -func (x *ProviderUsageResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[140] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ProviderUsageResponse.ProtoReflect.Descriptor instead. -func (*ProviderUsageResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{140} -} - -func (x *ProviderUsageResponse) GetResultJson() string { - if x != nil { - return x.ResultJson - } - return "" -} - -// ChatIngressBatch carries contiguous logical records for one run. Each record -// occupies exactly one sequence number beginning at first_seq. -type ChatIngressBatch struct { - state protoimpl.MessageState `protogen:"open.v1"` - RunId string `protobuf:"bytes,1,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` - ConversationId string `protobuf:"bytes,2,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - FirstSeq uint64 `protobuf:"varint,3,opt,name=first_seq,json=firstSeq,proto3" json:"first_seq,omitempty"` - Records []*ChatIngressRecord `protobuf:"bytes,4,rep,name=records,proto3" json:"records,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatIngressBatch) Reset() { - *x = ChatIngressBatch{} - mi := &file_proto_v2_gateway_proto_msgTypes[141] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatIngressBatch) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatIngressBatch) ProtoMessage() {} - -func (x *ChatIngressBatch) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[141] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatIngressBatch.ProtoReflect.Descriptor instead. -func (*ChatIngressBatch) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{141} -} - -func (x *ChatIngressBatch) GetRunId() string { - if x != nil { - return x.RunId - } - return "" -} - -func (x *ChatIngressBatch) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -func (x *ChatIngressBatch) GetFirstSeq() uint64 { - if x != nil { - return x.FirstSeq - } - return 0 -} - -func (x *ChatIngressBatch) GetRecords() []*ChatIngressRecord { - if x != nil { - return x.Records - } - return nil -} - -type ChatIngressRecord struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Types that are valid to be assigned to Payload: - // - // *ChatIngressRecord_Delta - // *ChatIngressRecord_Checkpoint - // *ChatIngressRecord_Terminal - // *ChatIngressRecord_Heartbeat - Payload isChatIngressRecord_Payload `protobuf_oneof:"payload"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatIngressRecord) Reset() { - *x = ChatIngressRecord{} - mi := &file_proto_v2_gateway_proto_msgTypes[142] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatIngressRecord) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatIngressRecord) ProtoMessage() {} - -func (x *ChatIngressRecord) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[142] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatIngressRecord.ProtoReflect.Descriptor instead. -func (*ChatIngressRecord) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{142} -} - -func (x *ChatIngressRecord) GetPayload() isChatIngressRecord_Payload { - if x != nil { - return x.Payload - } - return nil -} - -func (x *ChatIngressRecord) GetDelta() *ChatIngressDelta { - if x != nil { - if x, ok := x.Payload.(*ChatIngressRecord_Delta); ok { - return x.Delta - } - } - return nil -} - -func (x *ChatIngressRecord) GetCheckpoint() *ChatIngressCheckpoint { - if x != nil { - if x, ok := x.Payload.(*ChatIngressRecord_Checkpoint); ok { - return x.Checkpoint - } - } - return nil -} - -func (x *ChatIngressRecord) GetTerminal() *ChatIngressTerminal { - if x != nil { - if x, ok := x.Payload.(*ChatIngressRecord_Terminal); ok { - return x.Terminal - } - } - return nil -} - -func (x *ChatIngressRecord) GetHeartbeat() *ChatIngressHeartbeat { - if x != nil { - if x, ok := x.Payload.(*ChatIngressRecord_Heartbeat); ok { - return x.Heartbeat - } - } - return nil -} - -type isChatIngressRecord_Payload interface { - isChatIngressRecord_Payload() -} - -type ChatIngressRecord_Delta struct { - Delta *ChatIngressDelta `protobuf:"bytes,1,opt,name=delta,proto3,oneof"` -} - -type ChatIngressRecord_Checkpoint struct { - Checkpoint *ChatIngressCheckpoint `protobuf:"bytes,2,opt,name=checkpoint,proto3,oneof"` -} - -type ChatIngressRecord_Terminal struct { - Terminal *ChatIngressTerminal `protobuf:"bytes,3,opt,name=terminal,proto3,oneof"` -} - -type ChatIngressRecord_Heartbeat struct { - Heartbeat *ChatIngressHeartbeat `protobuf:"bytes,4,opt,name=heartbeat,proto3,oneof"` -} - -func (*ChatIngressRecord_Delta) isChatIngressRecord_Payload() {} - -func (*ChatIngressRecord_Checkpoint) isChatIngressRecord_Payload() {} - -func (*ChatIngressRecord_Terminal) isChatIngressRecord_Payload() {} - -func (*ChatIngressRecord_Heartbeat) isChatIngressRecord_Payload() {} - -type ChatIngressDelta struct { - state protoimpl.MessageState `protogen:"open.v1"` - EventJson string `protobuf:"bytes,1,opt,name=event_json,json=eventJson,proto3" json:"event_json,omitempty"` - WorkerId string `protobuf:"bytes,2,opt,name=worker_id,json=workerId,proto3" json:"worker_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatIngressDelta) Reset() { - *x = ChatIngressDelta{} - mi := &file_proto_v2_gateway_proto_msgTypes[143] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatIngressDelta) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatIngressDelta) ProtoMessage() {} - -func (x *ChatIngressDelta) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[143] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatIngressDelta.ProtoReflect.Descriptor instead. -func (*ChatIngressDelta) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{143} -} - -func (x *ChatIngressDelta) GetEventJson() string { - if x != nil { - return x.EventJson - } - return "" -} - -func (x *ChatIngressDelta) GetWorkerId() string { - if x != nil { - return x.WorkerId - } - return "" -} - -type ChatIngressHeartbeat struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Unix milliseconds. - UpdatedAt int64 `protobuf:"varint,1,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatIngressHeartbeat) Reset() { - *x = ChatIngressHeartbeat{} - mi := &file_proto_v2_gateway_proto_msgTypes[144] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatIngressHeartbeat) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatIngressHeartbeat) ProtoMessage() {} - -func (x *ChatIngressHeartbeat) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[144] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatIngressHeartbeat.ProtoReflect.Descriptor instead. -func (*ChatIngressHeartbeat) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{144} -} - -func (x *ChatIngressHeartbeat) GetUpdatedAt() int64 { - if x != nil { - return x.UpdatedAt - } - return 0 -} - -type ChatIngressCheckpoint struct { - state protoimpl.MessageState `protogen:"open.v1"` - CoversThroughSeq uint64 `protobuf:"varint,1,opt,name=covers_through_seq,json=coversThroughSeq,proto3" json:"covers_through_seq,omitempty"` - Revision uint64 `protobuf:"varint,2,opt,name=revision,proto3" json:"revision,omitempty"` - CompressedProjection []byte `protobuf:"bytes,3,opt,name=compressed_projection,json=compressedProjection,proto3" json:"compressed_projection,omitempty"` - UncompressedBytes uint64 `protobuf:"varint,4,opt,name=uncompressed_bytes,json=uncompressedBytes,proto3" json:"uncompressed_bytes,omitempty"` - Sha256 string `protobuf:"bytes,5,opt,name=sha256,proto3" json:"sha256,omitempty"` - ContentComplete bool `protobuf:"varint,6,opt,name=content_complete,json=contentComplete,proto3" json:"content_complete,omitempty"` - HistoryRequired bool `protobuf:"varint,7,opt,name=history_required,json=historyRequired,proto3" json:"history_required,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatIngressCheckpoint) Reset() { - *x = ChatIngressCheckpoint{} - mi := &file_proto_v2_gateway_proto_msgTypes[145] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatIngressCheckpoint) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatIngressCheckpoint) ProtoMessage() {} - -func (x *ChatIngressCheckpoint) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[145] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatIngressCheckpoint.ProtoReflect.Descriptor instead. -func (*ChatIngressCheckpoint) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{145} -} - -func (x *ChatIngressCheckpoint) GetCoversThroughSeq() uint64 { - if x != nil { - return x.CoversThroughSeq - } - return 0 -} - -func (x *ChatIngressCheckpoint) GetRevision() uint64 { - if x != nil { - return x.Revision - } - return 0 -} - -func (x *ChatIngressCheckpoint) GetCompressedProjection() []byte { - if x != nil { - return x.CompressedProjection - } - return nil -} - -func (x *ChatIngressCheckpoint) GetUncompressedBytes() uint64 { - if x != nil { - return x.UncompressedBytes - } - return 0 -} - -func (x *ChatIngressCheckpoint) GetSha256() string { - if x != nil { - return x.Sha256 - } - return "" -} - -func (x *ChatIngressCheckpoint) GetContentComplete() bool { - if x != nil { - return x.ContentComplete - } - return false -} - -func (x *ChatIngressCheckpoint) GetHistoryRequired() bool { - if x != nil { - return x.HistoryRequired - } - return false -} - -type ChatIngressTerminal struct { - state protoimpl.MessageState `protogen:"open.v1"` - CoversThroughSeq uint64 `protobuf:"varint,1,opt,name=covers_through_seq,json=coversThroughSeq,proto3" json:"covers_through_seq,omitempty"` - Revision uint64 `protobuf:"varint,2,opt,name=revision,proto3" json:"revision,omitempty"` - CompressedProjection []byte `protobuf:"bytes,3,opt,name=compressed_projection,json=compressedProjection,proto3" json:"compressed_projection,omitempty"` - UncompressedBytes uint64 `protobuf:"varint,4,opt,name=uncompressed_bytes,json=uncompressedBytes,proto3" json:"uncompressed_bytes,omitempty"` - Sha256 string `protobuf:"bytes,5,opt,name=sha256,proto3" json:"sha256,omitempty"` - ContentComplete bool `protobuf:"varint,6,opt,name=content_complete,json=contentComplete,proto3" json:"content_complete,omitempty"` - HistoryRequired bool `protobuf:"varint,7,opt,name=history_required,json=historyRequired,proto3" json:"history_required,omitempty"` - State string `protobuf:"bytes,8,opt,name=state,proto3" json:"state,omitempty"` - ErrorCode string `protobuf:"bytes,9,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"` - ErrorMessage string `protobuf:"bytes,10,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatIngressTerminal) Reset() { - *x = ChatIngressTerminal{} - mi := &file_proto_v2_gateway_proto_msgTypes[146] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatIngressTerminal) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatIngressTerminal) ProtoMessage() {} - -func (x *ChatIngressTerminal) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[146] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatIngressTerminal.ProtoReflect.Descriptor instead. -func (*ChatIngressTerminal) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{146} -} - -func (x *ChatIngressTerminal) GetCoversThroughSeq() uint64 { - if x != nil { - return x.CoversThroughSeq - } - return 0 -} - -func (x *ChatIngressTerminal) GetRevision() uint64 { - if x != nil { - return x.Revision - } - return 0 -} - -func (x *ChatIngressTerminal) GetCompressedProjection() []byte { - if x != nil { - return x.CompressedProjection - } - return nil -} - -func (x *ChatIngressTerminal) GetUncompressedBytes() uint64 { - if x != nil { - return x.UncompressedBytes - } - return 0 -} - -func (x *ChatIngressTerminal) GetSha256() string { - if x != nil { - return x.Sha256 - } - return "" -} - -func (x *ChatIngressTerminal) GetContentComplete() bool { - if x != nil { - return x.ContentComplete - } - return false -} - -func (x *ChatIngressTerminal) GetHistoryRequired() bool { - if x != nil { - return x.HistoryRequired - } - return false -} - -func (x *ChatIngressTerminal) GetState() string { - if x != nil { - return x.State - } - return "" -} - -func (x *ChatIngressTerminal) GetErrorCode() string { - if x != nil { - return x.ErrorCode - } - return "" -} - -func (x *ChatIngressTerminal) GetErrorMessage() string { - if x != nil { - return x.ErrorMessage - } - return "" -} - -// ChatIngressResume declares the replay window retained by the desktop after a -// reconnect. The gateway answers each run with ChatIngressAck. -type ChatIngressResume struct { - state protoimpl.MessageState `protogen:"open.v1"` - Runs []*ChatIngressRunResume `protobuf:"bytes,1,rep,name=runs,proto3" json:"runs,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatIngressResume) Reset() { - *x = ChatIngressResume{} - mi := &file_proto_v2_gateway_proto_msgTypes[147] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatIngressResume) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatIngressResume) ProtoMessage() {} - -func (x *ChatIngressResume) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[147] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatIngressResume.ProtoReflect.Descriptor instead. -func (*ChatIngressResume) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{147} -} - -func (x *ChatIngressResume) GetRuns() []*ChatIngressRunResume { - if x != nil { - return x.Runs - } - return nil -} - -type ChatIngressRunResume struct { - state protoimpl.MessageState `protogen:"open.v1"` - RunId string `protobuf:"bytes,1,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` - ConversationId string `protobuf:"bytes,2,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - ReplayFromSeq uint64 `protobuf:"varint,3,opt,name=replay_from_seq,json=replayFromSeq,proto3" json:"replay_from_seq,omitempty"` - ReplayThroughSeq uint64 `protobuf:"varint,4,opt,name=replay_through_seq,json=replayThroughSeq,proto3" json:"replay_through_seq,omitempty"` - NextSeq uint64 `protobuf:"varint,5,opt,name=next_seq,json=nextSeq,proto3" json:"next_seq,omitempty"` - LatestCheckpointSeq uint64 `protobuf:"varint,6,opt,name=latest_checkpoint_seq,json=latestCheckpointSeq,proto3" json:"latest_checkpoint_seq,omitempty"` - TerminalSeq uint64 `protobuf:"varint,7,opt,name=terminal_seq,json=terminalSeq,proto3" json:"terminal_seq,omitempty"` - TerminalPending bool `protobuf:"varint,8,opt,name=terminal_pending,json=terminalPending,proto3" json:"terminal_pending,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatIngressRunResume) Reset() { - *x = ChatIngressRunResume{} - mi := &file_proto_v2_gateway_proto_msgTypes[148] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatIngressRunResume) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatIngressRunResume) ProtoMessage() {} - -func (x *ChatIngressRunResume) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[148] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatIngressRunResume.ProtoReflect.Descriptor instead. -func (*ChatIngressRunResume) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{148} -} - -func (x *ChatIngressRunResume) GetRunId() string { - if x != nil { - return x.RunId - } - return "" -} - -func (x *ChatIngressRunResume) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -func (x *ChatIngressRunResume) GetReplayFromSeq() uint64 { - if x != nil { - return x.ReplayFromSeq - } - return 0 -} - -func (x *ChatIngressRunResume) GetReplayThroughSeq() uint64 { - if x != nil { - return x.ReplayThroughSeq - } - return 0 -} - -func (x *ChatIngressRunResume) GetNextSeq() uint64 { - if x != nil { - return x.NextSeq - } - return 0 -} - -func (x *ChatIngressRunResume) GetLatestCheckpointSeq() uint64 { - if x != nil { - return x.LatestCheckpointSeq - } - return 0 -} - -func (x *ChatIngressRunResume) GetTerminalSeq() uint64 { - if x != nil { - return x.TerminalSeq - } - return 0 -} - -func (x *ChatIngressRunResume) GetTerminalPending() bool { - if x != nil { - return x.TerminalPending - } - return false -} - -// ChatIngressFragment transports one encoded ChatIngressRecord that exceeds a -// normal batch frame. Fragments do not consume additional logical sequence -// numbers; source_seq is the sequence of the reconstructed record. -type ChatIngressFragment struct { - state protoimpl.MessageState `protogen:"open.v1"` - RunId string `protobuf:"bytes,1,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` - ConversationId string `protobuf:"bytes,2,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - SourceSeq uint64 `protobuf:"varint,3,opt,name=source_seq,json=sourceSeq,proto3" json:"source_seq,omitempty"` - FragmentIndex uint32 `protobuf:"varint,4,opt,name=fragment_index,json=fragmentIndex,proto3" json:"fragment_index,omitempty"` - FragmentCount uint32 `protobuf:"varint,5,opt,name=fragment_count,json=fragmentCount,proto3" json:"fragment_count,omitempty"` - EncodedRecordChunk []byte `protobuf:"bytes,6,opt,name=encoded_record_chunk,json=encodedRecordChunk,proto3" json:"encoded_record_chunk,omitempty"` - EncodedRecordBytes uint64 `protobuf:"varint,7,opt,name=encoded_record_bytes,json=encodedRecordBytes,proto3" json:"encoded_record_bytes,omitempty"` - Sha256 string `protobuf:"bytes,8,opt,name=sha256,proto3" json:"sha256,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatIngressFragment) Reset() { - *x = ChatIngressFragment{} - mi := &file_proto_v2_gateway_proto_msgTypes[149] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatIngressFragment) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatIngressFragment) ProtoMessage() {} - -func (x *ChatIngressFragment) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[149] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatIngressFragment.ProtoReflect.Descriptor instead. -func (*ChatIngressFragment) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{149} -} - -func (x *ChatIngressFragment) GetRunId() string { - if x != nil { - return x.RunId - } - return "" -} - -func (x *ChatIngressFragment) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -func (x *ChatIngressFragment) GetSourceSeq() uint64 { - if x != nil { - return x.SourceSeq - } - return 0 -} - -func (x *ChatIngressFragment) GetFragmentIndex() uint32 { - if x != nil { - return x.FragmentIndex - } - return 0 -} - -func (x *ChatIngressFragment) GetFragmentCount() uint32 { - if x != nil { - return x.FragmentCount - } - return 0 -} - -func (x *ChatIngressFragment) GetEncodedRecordChunk() []byte { - if x != nil { - return x.EncodedRecordChunk - } - return nil -} - -func (x *ChatIngressFragment) GetEncodedRecordBytes() uint64 { - if x != nil { - return x.EncodedRecordBytes - } - return 0 -} - -func (x *ChatIngressFragment) GetSha256() string { - if x != nil { - return x.Sha256 - } - return "" -} - -type ChatIngressAck struct { - state protoimpl.MessageState `protogen:"open.v1"` - RunId string `protobuf:"bytes,1,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` - ConversationId string `protobuf:"bytes,2,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - CommittedThrough uint64 `protobuf:"varint,3,opt,name=committed_through,json=committedThrough,proto3" json:"committed_through,omitempty"` - ExpectedNext uint64 `protobuf:"varint,4,opt,name=expected_next,json=expectedNext,proto3" json:"expected_next,omitempty"` - Action ChatIngressAck_Action `protobuf:"varint,5,opt,name=action,proto3,enum=liveagent.gateway.v2.ChatIngressAck_Action" json:"action,omitempty"` - TerminalCommitted bool `protobuf:"varint,6,opt,name=terminal_committed,json=terminalCommitted,proto3" json:"terminal_committed,omitempty"` - ErrorCode string `protobuf:"bytes,7,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"` - ErrorMessage string `protobuf:"bytes,8,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatIngressAck) Reset() { - *x = ChatIngressAck{} - mi := &file_proto_v2_gateway_proto_msgTypes[150] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatIngressAck) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatIngressAck) ProtoMessage() {} - -func (x *ChatIngressAck) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[150] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatIngressAck.ProtoReflect.Descriptor instead. -func (*ChatIngressAck) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{150} -} - -func (x *ChatIngressAck) GetRunId() string { - if x != nil { - return x.RunId - } - return "" -} - -func (x *ChatIngressAck) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -func (x *ChatIngressAck) GetCommittedThrough() uint64 { - if x != nil { - return x.CommittedThrough - } - return 0 -} - -func (x *ChatIngressAck) GetExpectedNext() uint64 { - if x != nil { - return x.ExpectedNext - } - return 0 -} - -func (x *ChatIngressAck) GetAction() ChatIngressAck_Action { - if x != nil { - return x.Action - } - return ChatIngressAck_ACTION_UNSPECIFIED -} - -func (x *ChatIngressAck) GetTerminalCommitted() bool { - if x != nil { - return x.TerminalCommitted - } - return false -} - -func (x *ChatIngressAck) GetErrorCode() string { - if x != nil { - return x.ErrorCode - } - return "" -} - -func (x *ChatIngressAck) GetErrorMessage() string { - if x != nil { - return x.ErrorMessage - } - return "" -} - -var File_proto_v2_gateway_proto protoreflect.FileDescriptor - -const file_proto_v2_gateway_proto_rawDesc = "" + - "\n" + - "\x16proto/v2/gateway.proto\x12\x14liveagent.gateway.v2\"\xb2!\n" + - "\x0fGatewayEnvelope\x12\x1d\n" + - "\n" + - "request_id\x18\x01 \x01(\tR\trequestId\x12\x1c\n" + - "\ttimestamp\x18\x02 \x01(\x03R\ttimestamp\x12M\n" + - "\fchat_command\x18\n" + - " \x01(\v2(.liveagent.gateway.v2.ChatCommandRequestH\x00R\vchatCommand\x12J\n" + - "\vcron_manage\x18\x14 \x01(\v2'.liveagent.gateway.v2.CronManageRequestH\x00R\n" + - "cronManage\x12M\n" + - "\fhistory_list\x18\x1e \x01(\v2(.liveagent.gateway.v2.HistoryListRequestH\x00R\vhistoryList\x12J\n" + - "\vhistory_get\x18\x1f \x01(\v2'.liveagent.gateway.v2.HistoryGetRequestH\x00R\n" + - "historyGet\x12S\n" + - "\x0ehistory_rename\x18 \x01(\v2*.liveagent.gateway.v2.HistoryRenameRequestH\x00R\rhistoryRename\x12S\n" + - "\x0ehistory_delete\x18! \x01(\v2*.liveagent.gateway.v2.HistoryDeleteRequestH\x00R\rhistoryDelete\x12S\n" + - "\x0ehistory_prefix\x18\" \x01(\v2*.liveagent.gateway.v2.HistoryPrefixRequestH\x00R\rhistoryPrefix\x12J\n" + - "\vhistory_pin\x18# \x01(\v2'.liveagent.gateway.v2.HistoryPinRequestH\x00R\n" + - "historyPin\x12Z\n" + - "\x11history_share_get\x18$ \x01(\v2,.liveagent.gateway.v2.HistoryShareGetRequestH\x00R\x0fhistoryShareGet\x12Z\n" + - "\x11history_share_set\x18% \x01(\v2,.liveagent.gateway.v2.HistoryShareSetRequestH\x00R\x0fhistoryShareSet\x12f\n" + - "\x15history_share_resolve\x18& \x01(\v20.liveagent.gateway.v2.HistoryShareResolveRequestH\x00R\x13historyShareResolve\x12Y\n" + - "\x10history_workdirs\x18' \x01(\v2,.liveagent.gateway.v2.HistoryWorkdirsRequestH\x00R\x0fhistoryWorkdirs\x12P\n" + - "\rprovider_list\x18( \x01(\v2).liveagent.gateway.v2.ProviderListRequestH\x00R\fproviderList\x12M\n" + - "\fsettings_get\x18) \x01(\v2(.liveagent.gateway.v2.SettingsGetRequestH\x00R\vsettingsGet\x12V\n" + - "\x0fsettings_update\x18* \x01(\v2+.liveagent.gateway.v2.SettingsUpdateRequestH\x00R\x0esettingsUpdate\x12W\n" + - "\x10skill_files_list\x18+ \x01(\v2+.liveagent.gateway.v2.SkillFilesListRequestH\x00R\x0eskillFilesList\x12`\n" + - "\x13skill_metadata_read\x18, \x01(\v2..liveagent.gateway.v2.SkillMetadataReadRequestH\x00R\x11skillMetadataRead\x12T\n" + - "\x0fskill_text_read\x18- \x01(\v2*.liveagent.gateway.v2.SkillTextReadRequestH\x00R\rskillTextRead\x12Z\n" + - "\x11file_mention_list\x18. \x01(\v2,.liveagent.gateway.v2.FileMentionListRequestH\x00R\x0ffileMentionList\x12f\n" + - "\x15upload_readable_files\x18/ \x01(\v20.liveagent.gateway.v2.UploadReadableFilesRequestH\x00R\x13uploadReadableFiles\x12A\n" + - "\bfs_roots\x180 \x01(\v2$.liveagent.gateway.v2.FsRootsRequestH\x00R\afsRoots\x12K\n" + - "\ffs_list_dirs\x181 \x01(\v2'.liveagent.gateway.v2.FsListDirsRequestH\x00R\n" + - "fsListDirs\x127\n" + - "\x04ping\x182 \x01(\v2!.liveagent.gateway.v2.PingRequestH\x00R\x04ping\x12i\n" + - "\x16uploaded_image_preview\x183 \x01(\v21.liveagent.gateway.v2.UploadedImagePreviewRequestH\x00R\x14uploadedImagePreview\x12P\n" + - "\rmemory_manage\x184 \x01(\v2).liveagent.gateway.v2.MemoryManageRequestH\x00R\fmemoryManage\x12M\n" + - "\fskill_manage\x185 \x01(\v2(.liveagent.gateway.v2.SkillManageRequestH\x00R\vskillManage\x12m\n" + - "\x18fs_create_project_folder\x186 \x01(\v22.liveagent.gateway.v2.FsCreateProjectFolderRequestH\x00R\x15fsCreateProjectFolder\x12R\n" + - "\x10terminal_request\x187 \x01(\v2%.liveagent.gateway.v2.TerminalRequestH\x00R\x0fterminalRequest\x12>\n" + - "\afs_list\x188 \x01(\v2#.liveagent.gateway.v2.FsListRequestH\x00R\x06fsList\x12N\n" + - "\rfs_write_text\x189 \x01(\v2(.liveagent.gateway.v2.FsWriteTextRequestH\x00R\vfsWriteText\x12N\n" + - "\rfs_create_dir\x18: \x01(\v2(.liveagent.gateway.v2.FsCreateDirRequestH\x00R\vfsCreateDir\x12D\n" + - "\tfs_rename\x18; \x01(\v2%.liveagent.gateway.v2.FsRenameRequestH\x00R\bfsRename\x12D\n" + - "\tfs_delete\x18< \x01(\v2%.liveagent.gateway.v2.FsDeleteRequestH\x00R\bfsDelete\x12C\n" + - "\vgit_request\x18= \x01(\v2 .liveagent.gateway.v2.GitRequestH\x00R\n" + - "gitRequest\x12d\n" + - "\x15fs_read_editable_text\x18> \x01(\v2/.liveagent.gateway.v2.FsReadEditableTextRequestH\x00R\x12fsReadEditableText\x12j\n" + - "\x17fs_read_workspace_image\x18? \x01(\v21.liveagent.gateway.v2.FsReadWorkspaceImageRequestH\x00R\x14fsReadWorkspaceImage\x12F\n" + - "\fsftp_request\x18@ \x01(\v2!.liveagent.gateway.v2.SftpRequestH\x00R\vsftpRequest\x12V\n" + - "\x0fprovider_models\x18A \x01(\v2+.liveagent.gateway.v2.ProviderModelsRequestH\x00R\x0eproviderModels\x12z\n" + - "\x1dsettings_reset_ssh_known_host\x18H \x01(\v26.liveagent.gateway.v2.SettingsResetSshKnownHostRequestH\x00R\x19settingsResetSshKnownHost\x12G\n" + - "\n" + - "chat_queue\x18I \x01(\v2&.liveagent.gateway.v2.ChatQueueRequestH\x00R\tchatQueue\x12P\n" + - "\x10chat_ingress_ack\x18K \x01(\v2$.liveagent.gateway.v2.ChatIngressAckH\x00R\x0echatIngressAck\x12N\n" + - "\ftunnel_state\x18P \x01(\v2).liveagent.gateway.v2.TunnelStateSnapshotH\x00R\vtunnelState\x12O\n" + - "\x0ftunnel_mutation\x18Q \x01(\v2$.liveagent.gateway.v2.TunnelMutationH\x00R\x0etunnelMutation\x12F\n" + - "\ftunnel_frame\x18R \x01(\v2!.liveagent.gateway.v2.TunnelFrameH\x00R\vtunnelFrame\x12V\n" + - "\x0fworkspace_watch\x18Z \x01(\v2+.liveagent.gateway.v2.WorkspaceWatchRequestH\x00R\x0eworkspaceWatch\x12e\n" + - "\x17managed_process_request\x18[ \x01(\v2+.liveagent.gateway.v2.ManagedProcessRequestH\x00R\x15managedProcessRequest\x12S\n" + - "\x0ehistory_branch\x18\\ \x01(\v2*.liveagent.gateway.v2.HistoryBranchRequestH\x00R\rhistoryBranch\x12S\n" + - "\x0eprovider_usage\x18] \x01(\v2*.liveagent.gateway.v2.ProviderUsageRequestH\x00R\rproviderUsage\x12Q\n" + - "\x0echat_file_open\x18^ \x01(\v2).liveagent.gateway.v2.ChatFileOpenRequestH\x00R\fchatFileOpenB\t\n" + - "\apayloadJ\x04\bC\x10DJ\x04\bD\x10EJ\x04\bE\x10FJ\x04\bJ\x10K\"\x8e-\n" + - "\rAgentEnvelope\x12\x1d\n" + - "\n" + - "request_id\x18\x01 \x01(\tR\trequestId\x12\x1c\n" + - "\ttimestamp\x18\x02 \x01(\x03R\ttimestamp\x12@\n" + - "\n" + - "chat_event\x18\n" + - " \x01(\v2\x1f.liveagent.gateway.v2.ChatEventH\x00R\tchatEvent\x12T\n" + - "\x10cron_manage_resp\x18\x14 \x01(\v2(.liveagent.gateway.v2.CronManageResponseH\x00R\x0ecronManageResp\x12W\n" + - "\x11history_list_resp\x18\x1e \x01(\v2).liveagent.gateway.v2.HistoryListResponseH\x00R\x0fhistoryListResp\x12T\n" + - "\x10history_get_resp\x18\x1f \x01(\v2(.liveagent.gateway.v2.HistoryGetResponseH\x00R\x0ehistoryGetResp\x12]\n" + - "\x13history_rename_resp\x18 \x01(\v2+.liveagent.gateway.v2.HistoryRenameResponseH\x00R\x11historyRenameResp\x12]\n" + - "\x13history_delete_resp\x18! \x01(\v2+.liveagent.gateway.v2.HistoryDeleteResponseH\x00R\x11historyDeleteResp\x12K\n" + - "\fhistory_sync\x18\" \x01(\v2&.liveagent.gateway.v2.HistorySyncEventH\x00R\vhistorySync\x12]\n" + - "\x13history_prefix_resp\x18# \x01(\v2+.liveagent.gateway.v2.HistoryPrefixResponseH\x00R\x11historyPrefixResp\x12T\n" + - "\x10history_pin_resp\x18$ \x01(\v2(.liveagent.gateway.v2.HistoryPinResponseH\x00R\x0ehistoryPinResp\x12d\n" + - "\x16history_share_get_resp\x18% \x01(\v2-.liveagent.gateway.v2.HistoryShareGetResponseH\x00R\x13historyShareGetResp\x12d\n" + - "\x16history_share_set_resp\x18& \x01(\v2-.liveagent.gateway.v2.HistoryShareSetResponseH\x00R\x13historyShareSetResp\x12p\n" + - "\x1ahistory_share_resolve_resp\x18' \x01(\v21.liveagent.gateway.v2.HistoryShareResolveResponseH\x00R\x17historyShareResolveResp\x12c\n" + - "\x15history_workdirs_resp\x188 \x01(\v2-.liveagent.gateway.v2.HistoryWorkdirsResponseH\x00R\x13historyWorkdirsResp\x12Z\n" + - "\x12provider_list_resp\x18( \x01(\v2*.liveagent.gateway.v2.ProviderListResponseH\x00R\x10providerListResp\x12W\n" + - "\x11settings_get_resp\x18) \x01(\v2).liveagent.gateway.v2.SettingsGetResponseH\x00R\x0fsettingsGetResp\x12`\n" + - "\x14settings_update_resp\x18* \x01(\v2,.liveagent.gateway.v2.SettingsUpdateResponseH\x00R\x12settingsUpdateResp\x12N\n" + - "\rsettings_sync\x18+ \x01(\v2'.liveagent.gateway.v2.SettingsSyncEventH\x00R\fsettingsSync\x12a\n" + - "\x15skill_files_list_resp\x18, \x01(\v2,.liveagent.gateway.v2.SkillFilesListResponseH\x00R\x12skillFilesListResp\x12j\n" + - "\x18skill_metadata_read_resp\x18- \x01(\v2/.liveagent.gateway.v2.SkillMetadataReadResponseH\x00R\x15skillMetadataReadResp\x12^\n" + - "\x14skill_text_read_resp\x18. \x01(\v2+.liveagent.gateway.v2.SkillTextReadResponseH\x00R\x11skillTextReadResp\x12d\n" + - "\x16file_mention_list_resp\x18/ \x01(\v2-.liveagent.gateway.v2.FileMentionListResponseH\x00R\x13fileMentionListResp\x12p\n" + - "\x1aupload_readable_files_resp\x180 \x01(\v21.liveagent.gateway.v2.UploadReadableFilesResponseH\x00R\x17uploadReadableFilesResp\x12K\n" + - "\rfs_roots_resp\x181 \x01(\v2%.liveagent.gateway.v2.FsRootsResponseH\x00R\vfsRootsResp\x128\n" + - "\x04pong\x182 \x01(\v2\".liveagent.gateway.v2.PongResponseH\x00R\x04pong\x12U\n" + - "\x11fs_list_dirs_resp\x183 \x01(\v2(.liveagent.gateway.v2.FsListDirsResponseH\x00R\x0efsListDirsResp\x12s\n" + - "\x1buploaded_image_preview_resp\x184 \x01(\v22.liveagent.gateway.v2.UploadedImagePreviewResponseH\x00R\x18uploadedImagePreviewResp\x12Z\n" + - "\x12memory_manage_resp\x185 \x01(\v2*.liveagent.gateway.v2.MemoryManageResponseH\x00R\x10memoryManageResp\x12W\n" + - "\x11skill_manage_resp\x186 \x01(\v2).liveagent.gateway.v2.SkillManageResponseH\x00R\x0fskillManageResp\x12w\n" + - "\x1dfs_create_project_folder_resp\x187 \x01(\v23.liveagent.gateway.v2.FsCreateProjectFolderResponseH\x00R\x19fsCreateProjectFolderResp\x12U\n" + - "\x11terminal_response\x189 \x01(\v2&.liveagent.gateway.v2.TerminalResponseH\x00R\x10terminalResponse\x12L\n" + - "\x0eterminal_event\x18: \x01(\v2#.liveagent.gateway.v2.TerminalEventH\x00R\rterminalEvent\x12H\n" + - "\ffs_list_resp\x18; \x01(\v2$.liveagent.gateway.v2.FsListResponseH\x00R\n" + - "fsListResp\x12X\n" + - "\x12fs_write_text_resp\x18< \x01(\v2).liveagent.gateway.v2.FsWriteTextResponseH\x00R\x0ffsWriteTextResp\x12X\n" + - "\x12fs_create_dir_resp\x18= \x01(\v2).liveagent.gateway.v2.FsCreateDirResponseH\x00R\x0ffsCreateDirResp\x12N\n" + - "\x0efs_rename_resp\x18> \x01(\v2&.liveagent.gateway.v2.FsRenameResponseH\x00R\ffsRenameResp\x12N\n" + - "\x0efs_delete_resp\x18? \x01(\v2&.liveagent.gateway.v2.FsDeleteResponseH\x00R\ffsDeleteResp\x12F\n" + - "\fgit_response\x18@ \x01(\v2!.liveagent.gateway.v2.GitResponseH\x00R\vgitResponse\x12n\n" + - "\x1afs_read_editable_text_resp\x18A \x01(\v20.liveagent.gateway.v2.FsReadEditableTextResponseH\x00R\x16fsReadEditableTextResp\x12t\n" + - "\x1cfs_read_workspace_image_resp\x18B \x01(\v22.liveagent.gateway.v2.FsReadWorkspaceImageResponseH\x00R\x18fsReadWorkspaceImageResp\x12I\n" + - "\rsftp_response\x18I \x01(\v2\".liveagent.gateway.v2.SftpResponseH\x00R\fsftpResponse\x12@\n" + - "\n" + - "sftp_event\x18J \x01(\v2\x1f.liveagent.gateway.v2.SftpEventH\x00R\tsftpEvent\x12Q\n" + - "\x0fchat_queue_resp\x18K \x01(\v2'.liveagent.gateway.v2.ChatQueueResponseH\x00R\rchatQueueResp\x12P\n" + - "\x10chat_queue_event\x18L \x01(\v2$.liveagent.gateway.v2.ChatQueueEventH\x00R\x0echatQueueEvent\x12K\n" + - "\fchat_control\x18F \x01(\v2&.liveagent.gateway.v2.ChatControlEventH\x00R\vchatControl\x12Q\n" + - "\x0eruntime_status\x18G \x01(\v2(.liveagent.gateway.v2.RuntimeStatusEventH\x00R\rruntimeStatus\x12\x84\x01\n" + - "\"settings_reset_ssh_known_host_resp\x18H \x01(\v27.liveagent.gateway.v2.SettingsResetSshKnownHostResponseH\x00R\x1dsettingsResetSshKnownHostResp\x12_\n" + - "\x15chat_runtime_snapshot\x18M \x01(\v2).liveagent.gateway.v2.ChatRuntimeSnapshotH\x00R\x13chatRuntimeSnapshot\x12`\n" + - "\x14provider_models_resp\x18O \x01(\v2,.liveagent.gateway.v2.ProviderModelsResponseH\x00R\x12providerModelsResp\x12Q\n" + - "\x0etunnel_desired\x18P \x01(\v2(.liveagent.gateway.v2.TunnelDesiredStateH\x00R\rtunnelDesired\x12b\n" + - "\x16tunnel_mutation_result\x18Q \x01(\v2*.liveagent.gateway.v2.TunnelMutationResultH\x00R\x14tunnelMutationResult\x12F\n" + - "\ftunnel_frame\x18R \x01(\v2!.liveagent.gateway.v2.TunnelFrameH\x00R\vtunnelFrame\x12Y\n" + - "\x13tunnel_probe_report\x18S \x01(\v2'.liveagent.gateway.v2.TunnelProbeReportH\x00R\x11tunnelProbeReport\x12]\n" + - "\x12workspace_activity\x18Z \x01(\v2,.liveagent.gateway.v2.WorkspaceActivityEventH\x00R\x11workspaceActivity\x12h\n" + - "\x18managed_process_response\x18[ \x01(\v2,.liveagent.gateway.v2.ManagedProcessResponseH\x00R\x16managedProcessResponse\x12h\n" + - "\x18managed_process_snapshot\x18\\ \x01(\v2,.liveagent.gateway.v2.ManagedProcessSnapshotH\x00R\x16managedProcessSnapshot\x12]\n" + - "\x13history_branch_resp\x18] \x01(\v2+.liveagent.gateway.v2.HistoryBranchResponseH\x00R\x11historyBranchResp\x12]\n" + - "\x13provider_usage_resp\x18^ \x01(\v2+.liveagent.gateway.v2.ProviderUsageResponseH\x00R\x11providerUsageResp\x12V\n" + - "\x12chat_ingress_batch\x18_ \x01(\v2&.liveagent.gateway.v2.ChatIngressBatchH\x00R\x10chatIngressBatch\x12Y\n" + - "\x13chat_ingress_resume\x18` \x01(\v2'.liveagent.gateway.v2.ChatIngressResumeH\x00R\x11chatIngressResume\x12_\n" + - "\x15chat_ingress_fragment\x18a \x01(\v2).liveagent.gateway.v2.ChatIngressFragmentH\x00R\x13chatIngressFragment\x12[\n" + - "\x13chat_file_open_resp\x18b \x01(\v2*.liveagent.gateway.v2.ChatFileOpenResponseH\x00R\x10chatFileOpenResp\x12;\n" + - "\x05error\x18c \x01(\v2#.liveagent.gateway.v2.ErrorResponseH\x00R\x05errorB\t\n" + - "\apayloadJ\x04\bC\x10DJ\x04\bD\x10EJ\x04\bE\x10FJ\x04\bN\x10O\"|\n" + - "\x11ChatSelectedModel\x12,\n" + - "\x12custom_provider_id\x18\x01 \x01(\tR\x10customProviderId\x12\x14\n" + - "\x05model\x18\x02 \x01(\tR\x05model\x12#\n" + - "\rprovider_type\x18\x03 \x01(\tR\fproviderType\"\x99\x01\n" + - "\x13ChatRuntimeControls\x12)\n" + - "\x10thinking_enabled\x18\x01 \x01(\bR\x0fthinkingEnabled\x129\n" + - "\x19native_web_search_enabled\x18\x02 \x01(\bR\x16nativeWebSearchEnabled\x12\x1c\n" + - "\treasoning\x18\x03 \x01(\tR\treasoning\"\xac\x01\n" + - "\x10ChatUploadedFile\x12#\n" + - "\rrelative_path\x18\x01 \x01(\tR\frelativePath\x12#\n" + - "\rabsolute_path\x18\x02 \x01(\tR\fabsolutePath\x12\x1b\n" + - "\tfile_name\x18\x03 \x01(\tR\bfileName\x12\x12\n" + - "\x04kind\x18\x04 \x01(\tR\x04kind\x12\x1d\n" + - "\n" + - "size_bytes\x18\x05 \x01(\x03R\tsizeBytes\"h\n" + - "\x12UploadReadableFile\x12\x1b\n" + - "\tfile_name\x18\x01 \x01(\tR\bfileName\x12\x1b\n" + - "\tmime_type\x18\x02 \x01(\tR\bmimeType\x12\x18\n" + - "\acontent\x18\x03 \x01(\fR\acontent\"v\n" + - "\x1aUploadReadableFilesRequest\x12\x18\n" + - "\aworkdir\x18\x01 \x01(\tR\aworkdir\x12>\n" + - "\x05files\x18\x02 \x03(\v2(.liveagent.gateway.v2.UploadReadableFileR\x05files\"u\n" + - "\x1bUploadReadableFilesResponse\x12<\n" + - "\x05files\x18\x01 \x03(\v2&.liveagent.gateway.v2.ChatUploadedFileR\x05files\x12\x18\n" + - "\askipped\x18\x02 \x03(\tR\askipped\"\\\n" + - "\x1bUploadedImagePreviewRequest\x12\x18\n" + - "\aworkdir\x18\x01 \x01(\tR\aworkdir\x12#\n" + - "\rabsolute_path\x18\x02 \x01(\tR\fabsolutePath\"O\n" + - "\x1cUploadedImagePreviewResponse\x12\x1b\n" + - "\tmime_type\x18\x01 \x01(\tR\bmimeType\x12\x12\n" + - "\x04data\x18\x02 \x01(\tR\x04data\"\xb5\x01\n" + - "\n" + - "TunnelSpec\x12\x0e\n" + - "\x02id\x18\x01 \x01(\tR\x02id\x12\x1b\n" + - "\tslug_hint\x18\x02 \x01(\tR\bslugHint\x12\x12\n" + - "\x04name\x18\x03 \x01(\tR\x04name\x12\x1d\n" + - "\n" + - "target_url\x18\x04 \x01(\tR\ttargetUrl\x12\x1d\n" + - "\n" + - "expires_at\x18\x05 \x01(\x03R\texpiresAt\x12(\n" + - "\x10project_path_key\x18\x06 \x01(\tR\x0eprojectPathKey\"l\n" + - "\x12TunnelDesiredState\x12:\n" + - "\atunnels\x18\x01 \x03(\v2 .liveagent.gateway.v2.TunnelSpecR\atunnels\x12\x1a\n" + - "\brevision\x18\x02 \x01(\x04R\brevision\"\x93\x01\n" + - "\fTunnelHealth\x12\x16\n" + - "\x06status\x18\x01 \x01(\tR\x06status\x12\x1f\n" + - "\vhttp_status\x18\x02 \x01(\rR\n" + - "httpStatus\x12\x14\n" + - "\x05error\x18\x03 \x01(\tR\x05error\x12\x1d\n" + - "\n" + - "checked_at\x18\x04 \x01(\x03R\tcheckedAt\x12\x15\n" + - "\x06rtt_ms\x18\x05 \x01(\rR\x05rttMs\"\xd7\x02\n" + - "\fTunnelStatus\x12\x0e\n" + - "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + - "\x04slug\x18\x02 \x01(\tR\x04slug\x12\x12\n" + - "\x04name\x18\x03 \x01(\tR\x04name\x12\x1d\n" + - "\n" + - "target_url\x18\x04 \x01(\tR\ttargetUrl\x12\x1f\n" + - "\vpublic_path\x18\x05 \x01(\tR\n" + - "publicPath\x12\x1d\n" + - "\n" + - "created_at\x18\x06 \x01(\x03R\tcreatedAt\x12\x1d\n" + - "\n" + - "expires_at\x18\a \x01(\x03R\texpiresAt\x12-\n" + - "\x12active_connections\x18\b \x01(\rR\x11activeConnections\x12(\n" + - "\x10project_path_key\x18\t \x01(\tR\x0eprojectPathKey\x128\n" + - "\x05local\x18\n" + - " \x01(\v2\".liveagent.gateway.v2.TunnelHealthR\x05local\"\xcc\x01\n" + - "\x13TunnelStateSnapshot\x12<\n" + - "\atunnels\x18\x01 \x03(\v2\".liveagent.gateway.v2.TunnelStatusR\atunnels\x12\x1a\n" + - "\brevision\x18\x02 \x01(\x04R\brevision\x12!\n" + - "\fagent_online\x18\x03 \x01(\bR\vagentOnline\x128\n" + - "\x05relay\x18\x04 \x01(\v2\".liveagent.gateway.v2.TunnelHealthR\x05relay\"\xd8\x01\n" + - "\x0eTunnelMutation\x12\x16\n" + - "\x06action\x18\x01 \x01(\tR\x06action\x12\x1b\n" + - "\ttunnel_id\x18\x02 \x01(\tR\btunnelId\x12\x1d\n" + - "\n" + - "target_url\x18\x03 \x01(\tR\ttargetUrl\x12\x12\n" + - "\x04name\x18\x04 \x01(\tR\x04name\x12$\n" + - "\vttl_seconds\x18\x05 \x01(\rH\x00R\n" + - "ttlSeconds\x88\x01\x01\x12(\n" + - "\x10project_path_key\x18\x06 \x01(\tR\x0eprojectPathKeyB\x0e\n" + - "\f_ttl_seconds\"w\n" + - "\x14TunnelMutationResult\x12\x1b\n" + - "\ttunnel_id\x18\x01 \x01(\tR\btunnelId\x12\x1d\n" + - "\n" + - "error_code\x18\x02 \x01(\tR\terrorCode\x12#\n" + - "\rerror_message\x18\x03 \x01(\tR\ferrorMessage\"j\n" + - "\x11TunnelProbeResult\x12\x1b\n" + - "\ttunnel_id\x18\x01 \x01(\tR\btunnelId\x128\n" + - "\x05local\x18\x02 \x01(\v2\".liveagent.gateway.v2.TunnelHealthR\x05local\"V\n" + - "\x11TunnelProbeReport\x12A\n" + - "\aresults\x18\x01 \x03(\v2'.liveagent.gateway.v2.TunnelProbeResultR\aresults\"8\n" + - "\fTunnelHeader\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value\"\xf6\x03\n" + - "\vTunnelFrame\x12\x1b\n" + - "\tstream_id\x18\x01 \x01(\tR\bstreamId\x129\n" + - "\x04kind\x18\x02 \x01(\x0e2%.liveagent.gateway.v2.TunnelFrameKindR\x04kind\x12\x1d\n" + - "\n" + - "target_url\x18\x03 \x01(\tR\ttargetUrl\x12\x16\n" + - "\x06method\x18\x04 \x01(\tR\x06method\x12\x12\n" + - "\x04path\x18\x05 \x01(\tR\x04path\x12<\n" + - "\aheaders\x18\x06 \x03(\v2\".liveagent.gateway.v2.TunnelHeaderR\aheaders\x12\x16\n" + - "\x06status\x18\a \x01(\rR\x06status\x12\x12\n" + - "\x04body\x18\b \x01(\fR\x04body\x12\x14\n" + - "\x05error\x18\t \x01(\tR\x05error\x12Q\n" + - "\x0fws_message_type\x18\n" + - " \x01(\x0e2).liveagent.gateway.v2.TunnelWsMessageTypeR\rwsMessageType\x12%\n" + - "\x0ews_subprotocol\x18\v \x01(\tR\rwsSubprotocol\x12\"\n" + - "\rws_close_code\x18\f \x01(\rR\vwsCloseCode\x12&\n" + - "\x0fws_close_reason\x18\r \x01(\tR\rwsCloseReason\"3\n" + - "\x15WorkspaceWatchRequest\x12\x1a\n" + - "\bworkdirs\x18\x01 \x03(\tR\bworkdirs\"\xb3\x01\n" + - "\x16WorkspaceActivityEvent\x12\x18\n" + - "\aworkdir\x18\x01 \x01(\tR\aworkdir\x12\x1a\n" + - "\brevision\x18\x02 \x01(\x04R\brevision\x12\x0e\n" + - "\x02fs\x18\x03 \x01(\bR\x02fs\x12\x10\n" + - "\x03git\x18\x04 \x01(\bR\x03git\x12#\n" + - "\rchanged_paths\x18\x05 \x03(\tR\fchangedPaths\x12\x1c\n" + - "\ttruncated\x18\x06 \x01(\bR\ttruncated\"\x82\x03\n" + - "\x14ManagedProcessRecord\x12\x0e\n" + - "\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" + - "\x05label\x18\x02 \x01(\tR\x05label\x12\x18\n" + - "\acommand\x18\x03 \x01(\tR\acommand\x12\x10\n" + - "\x03cwd\x18\x04 \x01(\tR\x03cwd\x12\x14\n" + - "\x05shell\x18\x05 \x01(\tR\x05shell\x12\x10\n" + - "\x03pid\x18\x06 \x01(\rR\x03pid\x12\x19\n" + - "\blog_path\x18\a \x01(\tR\alogPath\x12\x1d\n" + - "\n" + - "started_at\x18\b \x01(\x03R\tstartedAt\x12$\n" + - "\vfinished_at\x18\t \x01(\x03H\x00R\n" + - "finishedAt\x88\x01\x01\x12 \n" + - "\texit_code\x18\n" + - " \x01(\x05H\x01R\bexitCode\x88\x01\x01\x12\x18\n" + - "\arunning\x18\v \x01(\bR\arunning\x12\x1a\n" + - "\bisolated\x18\f \x01(\bR\bisolated\x12\x1a\n" + - "\brestored\x18\r \x01(\bR\brestoredB\x0e\n" + - "\f_finished_atB\f\n" + - "\n" + - "_exit_code\"~\n" + - "\x16ManagedProcessSnapshot\x12H\n" + - "\tprocesses\x18\x01 \x03(\v2*.liveagent.gateway.v2.ManagedProcessRecordR\tprocesses\x12\x1a\n" + - "\brevision\x18\x02 \x01(\x04R\brevision\"k\n" + - "\x15ManagedProcessRequest\x12\x16\n" + - "\x06action\x18\x01 \x01(\tR\x06action\x12\x1d\n" + - "\n" + - "process_id\x18\x02 \x01(\tR\tprocessId\x12\x1b\n" + - "\tmax_bytes\x18\x03 \x01(\rR\bmaxBytes\"\xf5\x01\n" + - "\x16ManagedProcessResponse\x12\x16\n" + - "\x06action\x18\x01 \x01(\tR\x06action\x12H\n" + - "\bsnapshot\x18\x02 \x01(\v2,.liveagent.gateway.v2.ManagedProcessSnapshotR\bsnapshot\x12\x1f\n" + - "\vlog_content\x18\x03 \x01(\tR\n" + - "logContent\x12\x19\n" + - "\blog_path\x18\x04 \x01(\tR\alogPath\x12#\n" + - "\rlog_truncated\x18\x05 \x01(\bR\flogTruncated\x12\x18\n" + - "\astopped\x18\x06 \x01(\bR\astopped\"L\n" + - "\x13MemoryManageRequest\x12\x18\n" + - "\acommand\x18\x01 \x01(\tR\acommand\x12\x1b\n" + - "\targs_json\x18\x02 \x01(\tR\bargsJson\"7\n" + - "\x14MemoryManageResponse\x12\x1f\n" + - "\vresult_json\x18\x01 \x01(\tR\n" + - "resultJson\"\xe6\x04\n" + - "\x0fTerminalRequest\x12\x16\n" + - "\x06action\x18\x01 \x01(\tR\x06action\x12\x1d\n" + - "\n" + - "session_id\x18\x02 \x01(\tR\tsessionId\x12(\n" + - "\x10project_path_key\x18\x03 \x01(\tR\x0eprojectPathKey\x12\x10\n" + - "\x03cwd\x18\x04 \x01(\tR\x03cwd\x12\x14\n" + - "\x05shell\x18\x05 \x01(\tR\x05shell\x12\x14\n" + - "\x05title\x18\x06 \x01(\tR\x05title\x12\x12\n" + - "\x04data\x18\a \x01(\tR\x04data\x12\x12\n" + - "\x04cols\x18\b \x01(\rR\x04cols\x12\x12\n" + - "\x04rows\x18\t \x01(\rR\x04rows\x12\x1b\n" + - "\tmax_bytes\x18\n" + - " \x01(\rR\bmaxBytes\x12\x1e\n" + - "\vssh_host_id\x18\v \x01(\tR\tsshHostId\x12\x1b\n" + - "\tprompt_id\x18\f \x01(\tR\bpromptId\x12#\n" + - "\rprompt_answer\x18\r \x01(\tR\fpromptAnswer\x12$\n" + - "\x0etrust_host_key\x18\x0e \x01(\bR\ftrustHostKey\x12!\n" + - "\fsftp_enabled\x18\x0f \x01(\bR\vsftpEnabled\x12\x15\n" + - "\x06tab_id\x18\x10 \x01(\tR\x05tabId\x12\x19\n" + - "\btab_kind\x18\x11 \x01(\tR\atabKind\x12\x1f\n" + - "\vremote_host\x18\x12 \x01(\tR\n" + - "remoteHost\x12\x1f\n" + - "\vremote_port\x18\x13 \x01(\rR\n" + - "remotePort\x12\x1d\n" + - "\n" + - "local_port\x18\x14 \x01(\rR\tlocalPort\x12\x1d\n" + - "\n" + - "forward_id\x18\x15 \x01(\tR\tforwardId\"\xaa\x03\n" + - "\x0fTerminalSession\x12\x0e\n" + - "\x02id\x18\x01 \x01(\tR\x02id\x12(\n" + - "\x10project_path_key\x18\x02 \x01(\tR\x0eprojectPathKey\x12\x10\n" + - "\x03cwd\x18\x03 \x01(\tR\x03cwd\x12\x14\n" + - "\x05shell\x18\x04 \x01(\tR\x05shell\x12\x14\n" + - "\x05title\x18\x05 \x01(\tR\x05title\x12\x10\n" + - "\x03pid\x18\x06 \x01(\rR\x03pid\x12\x12\n" + - "\x04cols\x18\a \x01(\rR\x04cols\x12\x12\n" + - "\x04rows\x18\b \x01(\rR\x04rows\x12\x1d\n" + - "\n" + - "created_at\x18\t \x01(\x04R\tcreatedAt\x12\x1d\n" + - "\n" + - "updated_at\x18\n" + - " \x01(\x04R\tupdatedAt\x12\x1f\n" + - "\vfinished_at\x18\v \x01(\x04R\n" + - "finishedAt\x12\x1b\n" + - "\texit_code\x18\f \x01(\x05R\bexitCode\x12\x18\n" + - "\arunning\x18\r \x01(\bR\arunning\x12\x12\n" + - "\x04kind\x18\x0e \x01(\tR\x04kind\x12;\n" + - "\x03ssh\x18\x0f \x01(\v2).liveagent.gateway.v2.TerminalSshMetadataR\x03ssh\"\xca\x02\n" + - "\x13TerminalSshMetadata\x12\x17\n" + - "\ahost_id\x18\x01 \x01(\tR\x06hostId\x12\x1b\n" + - "\thost_name\x18\x02 \x01(\tR\bhostName\x12\x1a\n" + - "\busername\x18\x03 \x01(\tR\busername\x12\x12\n" + - "\x04host\x18\x04 \x01(\tR\x04host\x12\x12\n" + - "\x04port\x18\x05 \x01(\rR\x04port\x12\x1b\n" + - "\tauth_type\x18\x06 \x01(\tR\bauthType\x12\x16\n" + - "\x06status\x18\a \x01(\tR\x06status\x12+\n" + - "\x11reconnect_attempt\x18\b \x01(\rR\x10reconnectAttempt\x124\n" + - "\x16reconnect_max_attempts\x18\t \x01(\rR\x14reconnectMaxAttempts\x12!\n" + - "\fsftp_enabled\x18\n" + - " \x01(\bR\vsftpEnabled\"\xf9\x02\n" + - "\vSftpRequest\x12\x16\n" + - "\x06action\x18\x01 \x01(\tR\x06action\x12\x1d\n" + - "\n" + - "session_id\x18\x02 \x01(\tR\tsessionId\x12(\n" + - "\x10project_path_key\x18\x03 \x01(\tR\x0eprojectPathKey\x12\x18\n" + - "\aworkdir\x18\x04 \x01(\tR\aworkdir\x12\x1d\n" + - "\n" + - "local_path\x18\x05 \x01(\tR\tlocalPath\x12\x1f\n" + - "\vremote_path\x18\x06 \x01(\tR\n" + - "remotePath\x12\x1b\n" + - "\tfrom_path\x18\a \x01(\tR\bfromPath\x12\x17\n" + - "\ato_path\x18\b \x01(\tR\x06toPath\x12\x1c\n" + - "\tdirection\x18\t \x01(\tR\tdirection\x12\x1f\n" + - "\vtarget_path\x18\n" + - " \x01(\tR\n" + - "targetPath\x12\x1c\n" + - "\trecursive\x18\v \x01(\bR\trecursive\x12\x1c\n" + - "\toverwrite\x18\f \x01(\bR\toverwrite\"|\n" + - "\tSftpEntry\x12\x12\n" + - "\x04path\x18\x01 \x01(\tR\x04path\x12\x12\n" + - "\x04name\x18\x02 \x01(\tR\x04name\x12\x12\n" + - "\x04kind\x18\x03 \x01(\tR\x04kind\x12\x1d\n" + - "\n" + - "size_bytes\x18\x04 \x01(\x04R\tsizeBytes\x12\x14\n" + - "\x05mtime\x18\x05 \x01(\x04R\x05mtime\"\xee\x02\n" + - "\fSftpTransfer\x12\x0e\n" + - "\x02id\x18\x01 \x01(\tR\x02id\x12\x1d\n" + - "\n" + - "session_id\x18\x02 \x01(\tR\tsessionId\x12\x1c\n" + - "\tdirection\x18\x03 \x01(\tR\tdirection\x12\x16\n" + - "\x06status\x18\x04 \x01(\tR\x06status\x12\x1f\n" + - "\vsource_path\x18\x05 \x01(\tR\n" + - "sourcePath\x12\x1f\n" + - "\vtarget_path\x18\x06 \x01(\tR\n" + - "targetPath\x12!\n" + - "\fcurrent_path\x18\a \x01(\tR\vcurrentPath\x12\x1d\n" + - "\n" + - "bytes_done\x18\b \x01(\x04R\tbytesDone\x12\x1f\n" + - "\vbytes_total\x18\t \x01(\x04R\n" + - "bytesTotal\x12\x1d\n" + - "\n" + - "files_done\x18\n" + - " \x01(\rR\tfilesDone\x12\x1f\n" + - "\vfiles_total\x18\v \x01(\rR\n" + - "filesTotal\x12\x14\n" + - "\x05error\x18\f \x01(\tR\x05error\"\x84\x02\n" + - "\fSftpResponse\x12\x16\n" + - "\x06action\x18\x01 \x01(\tR\x06action\x12\x12\n" + - "\x04path\x18\x02 \x01(\tR\x04path\x129\n" + - "\aentries\x18\x03 \x03(\v2\x1f.liveagent.gateway.v2.SftpEntryR\aentries\x125\n" + - "\x05entry\x18\x04 \x01(\v2\x1f.liveagent.gateway.v2.SftpEntryR\x05entry\x12\x16\n" + - "\x06exists\x18\x05 \x01(\bR\x06exists\x12>\n" + - "\btransfer\x18\x06 \x01(\v2\".liveagent.gateway.v2.SftpTransferR\btransfer\"_\n" + - "\tSftpEvent\x12\x12\n" + - "\x04kind\x18\x01 \x01(\tR\x04kind\x12>\n" + - "\btransfer\x18\x02 \x01(\v2\".liveagent.gateway.v2.SftpTransferR\btransfer\"\x9a\x02\n" + - "\x11TerminalSshPrompt\x12\x0e\n" + - "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + - "\x04kind\x18\x02 \x01(\tR\x04kind\x12\x17\n" + - "\ahost_id\x18\x03 \x01(\tR\x06hostId\x12\x1b\n" + - "\thost_name\x18\x04 \x01(\tR\bhostName\x12\x12\n" + - "\x04host\x18\x05 \x01(\tR\x04host\x12\x12\n" + - "\x04port\x18\x06 \x01(\rR\x04port\x12\x18\n" + - "\amessage\x18\a \x01(\tR\amessage\x12-\n" + - "\x12fingerprint_sha256\x18\b \x01(\tR\x11fingerprintSha256\x12\x19\n" + - "\bkey_type\x18\t \x01(\tR\akeyType\x12\x1f\n" + - "\vanswer_echo\x18\n" + - " \x01(\bR\n" + - "answerEcho\"U\n" + - "\x13TerminalShellOption\x12\x0e\n" + - "\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" + - "\x05label\x18\x02 \x01(\tR\x05label\x12\x18\n" + - "\acommand\x18\x03 \x01(\tR\acommand\"\xbb\x01\n" + - "\x0eTerminalSshTab\x12\x0e\n" + - "\x02id\x18\x01 \x01(\tR\x02id\x12\x1d\n" + - "\n" + - "session_id\x18\x02 \x01(\tR\tsessionId\x12(\n" + - "\x10project_path_key\x18\x03 \x01(\tR\x0eprojectPathKey\x12\x12\n" + - "\x04kind\x18\x04 \x01(\tR\x04kind\x12\x1d\n" + - "\n" + - "created_at\x18\x05 \x01(\x04R\tcreatedAt\x12\x1d\n" + - "\n" + - "updated_at\x18\x06 \x01(\x04R\tupdatedAt\"\x9f\x01\n" + - "\x17TerminalSshTabsSnapshot\x12(\n" + - "\x10project_path_key\x18\x01 \x01(\tR\x0eprojectPathKey\x128\n" + - "\x04tabs\x18\x02 \x03(\v2$.liveagent.gateway.v2.TerminalSshTabR\x04tabs\x12\x1a\n" + - "\brevision\x18\x04 \x01(\x04R\brevisionJ\x04\b\x03\x10\x04\"\xf8\x02\n" + - "\x17TerminalSshLocalForward\x12\x0e\n" + - "\x02id\x18\x01 \x01(\tR\x02id\x12\x1d\n" + - "\n" + - "session_id\x18\x02 \x01(\tR\tsessionId\x12(\n" + - "\x10project_path_key\x18\x03 \x01(\tR\x0eprojectPathKey\x12\x1d\n" + - "\n" + - "local_host\x18\x04 \x01(\tR\tlocalHost\x12\x1d\n" + - "\n" + - "local_port\x18\x05 \x01(\rR\tlocalPort\x12\x18\n" + - "\aaddress\x18\x06 \x01(\tR\aaddress\x12\x1f\n" + - "\vremote_host\x18\a \x01(\tR\n" + - "remoteHost\x12\x1f\n" + - "\vremote_port\x18\b \x01(\rR\n" + - "remotePort\x12\x16\n" + - "\x06status\x18\t \x01(\tR\x06status\x12\x1d\n" + - "\n" + - "created_at\x18\n" + - " \x01(\x04R\tcreatedAt\x12\x1d\n" + - "\n" + - "updated_at\x18\v \x01(\x04R\tupdatedAt\x12\x14\n" + - "\x05error\x18\f \x01(\tR\x05error\"\x89\x01\n" + - " TerminalSshLocalForwardsSnapshot\x12I\n" + - "\bforwards\x18\x01 \x03(\v2-.liveagent.gateway.v2.TerminalSshLocalForwardR\bforwards\x12\x1a\n" + - "\brevision\x18\x02 \x01(\x04R\brevision\"\x98\x01\n" + - "\x1dTerminalSshLocalForwardAction\x12G\n" + - "\aforward\x18\x01 \x01(\v2-.liveagent.gateway.v2.TerminalSshLocalForwardR\aforward\x12\x1a\n" + - "\brevision\x18\x02 \x01(\x04R\brevision\x12\x12\n" + - "\x04kind\x18\x03 \x01(\tR\x04kind\"\xf5\x06\n" + - "\x10TerminalResponse\x12\x16\n" + - "\x06action\x18\x01 \x01(\tR\x06action\x12A\n" + - "\bsessions\x18\x02 \x03(\v2%.liveagent.gateway.v2.TerminalSessionR\bsessions\x12?\n" + - "\asession\x18\x03 \x01(\v2%.liveagent.gateway.v2.TerminalSessionR\asession\x12\x16\n" + - "\x06output\x18\x04 \x01(\fR\x06output\x12\x1c\n" + - "\ttruncated\x18\x05 \x01(\bR\ttruncated\x12N\n" + - "\rshell_options\x18\x06 \x03(\v2).liveagent.gateway.v2.TerminalShellOptionR\fshellOptions\x12#\n" + - "\rdefault_shell\x18\a \x01(\tR\fdefaultShell\x12.\n" + - "\x13output_start_offset\x18\b \x01(\x04R\x11outputStartOffset\x12*\n" + - "\x11output_end_offset\x18\t \x01(\x04R\x0foutputEndOffset\x12F\n" + - "\n" + - "ssh_prompt\x18\n" + - " \x01(\v2'.liveagent.gateway.v2.TerminalSshPromptR\tsshPrompt\x12\x1d\n" + - "\n" + - "latency_ms\x18\v \x01(\rR\tlatencyMs\x12H\n" + - "\bssh_tabs\x18\f \x01(\v2-.liveagent.gateway.v2.TerminalSshTabsSnapshotR\asshTabs\x12d\n" + - "\x12ssh_local_forwards\x18\r \x01(\v26.liveagent.gateway.v2.TerminalSshLocalForwardsSnapshotR\x10sshLocalForwards\x12_\n" + - "\x11ssh_local_forward\x18\x0e \x01(\v23.liveagent.gateway.v2.TerminalSshLocalForwardActionR\x0fsshLocalForward\x12F\n" + - " ssh_local_forward_port_available\x18\x0f \x01(\bR\x1csshLocalForwardPortAvailable\"\xc8\x03\n" + - "\rTerminalEvent\x12\x12\n" + - "\x04kind\x18\x01 \x01(\tR\x04kind\x12\x1d\n" + - "\n" + - "session_id\x18\x02 \x01(\tR\tsessionId\x12(\n" + - "\x10project_path_key\x18\x03 \x01(\tR\x0eprojectPathKey\x12?\n" + - "\asession\x18\x04 \x01(\v2%.liveagent.gateway.v2.TerminalSessionR\asession\x12\x12\n" + - "\x04data\x18\x05 \x01(\fR\x04data\x12.\n" + - "\x13output_start_offset\x18\x06 \x01(\x04R\x11outputStartOffset\x12*\n" + - "\x11output_end_offset\x18\a \x01(\x04R\x0foutputEndOffset\x12H\n" + - "\bssh_tabs\x18\b \x01(\v2-.liveagent.gateway.v2.TerminalSshTabsSnapshotR\asshTabs\x12_\n" + - "\x11ssh_local_forward\x18\t \x01(\v23.liveagent.gateway.v2.TerminalSshLocalForwardActionR\x0fsshLocalForward\"\xb1\x03\n" + - "\x13TerminalStreamFrame\x12\x12\n" + - "\x04kind\x18\x01 \x01(\tR\x04kind\x12\x1b\n" + - "\tstream_id\x18\x02 \x01(\tR\bstreamId\x12\x1d\n" + - "\n" + - "session_id\x18\x03 \x01(\tR\tsessionId\x12(\n" + - "\x10project_path_key\x18\x04 \x01(\tR\x0eprojectPathKey\x12\x10\n" + - "\x03seq\x18\x05 \x01(\x04R\x03seq\x12!\n" + - "\fstart_offset\x18\x06 \x01(\x04R\vstartOffset\x12\x1d\n" + - "\n" + - "end_offset\x18\a \x01(\x04R\tendOffset\x12\x12\n" + - "\x04cols\x18\b \x01(\rR\x04cols\x12\x12\n" + - "\x04rows\x18\t \x01(\rR\x04rows\x12\x1b\n" + - "\tmax_bytes\x18\n" + - " \x01(\rR\bmaxBytes\x12\x1c\n" + - "\ttruncated\x18\v \x01(\bR\ttruncated\x12\x14\n" + - "\x05error\x18\f \x01(\tR\x05error\x12?\n" + - "\asession\x18\r \x01(\v2%.liveagent.gateway.v2.TerminalSessionR\asession\x12\x12\n" + - "\x04data\x18\x0e \x01(\fR\x04data\"[\n" + - "\n" + - "GitRequest\x12\x16\n" + - "\x06action\x18\x01 \x01(\tR\x06action\x12\x18\n" + - "\aworkdir\x18\x02 \x01(\tR\aworkdir\x12\x1b\n" + - "\targs_json\x18\x03 \x01(\tR\bargsJson\"F\n" + - "\vGitResponse\x12\x16\n" + - "\x06action\x18\x01 \x01(\tR\x06action\x12\x1f\n" + - "\vresult_json\x18\x02 \x01(\tR\n" + - "resultJson\"\xf2\x03\n" + - "\vChatRequest\x12'\n" + - "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\x12\x18\n" + - "\amessage\x18\x02 \x01(\tR\amessage\x12N\n" + - "\x0eselected_model\x18\x03 \x01(\v2'.liveagent.gateway.v2.ChatSelectedModelR\rselectedModel\x12%\n" + - "\x0eexecution_mode\x18\x04 \x01(\tR\rexecutionMode\x12\x18\n" + - "\aworkdir\x18\x05 \x01(\tR\aworkdir\x12M\n" + - "\x0euploaded_files\x18\a \x03(\v2&.liveagent.gateway.v2.ChatUploadedFileR\ruploadedFiles\x12*\n" + - "\x11client_request_id\x18\b \x01(\tR\x0fclientRequestId\x12T\n" + - "\x10runtime_controls\x18\t \x01(\v2).liveagent.gateway.v2.ChatRuntimeControlsR\x0fruntimeControls\x12!\n" + - "\fqueue_policy\x18\n" + - " \x01(\tR\vqueuePolicyJ\x04\b\x06\x10\aR\x15selected_system_tools\"\xcf\x01\n" + - "\x0eChatMessageRef\x12#\n" + - "\rsegment_index\x18\x01 \x01(\x05R\fsegmentIndex\x12#\n" + - "\rmessage_index\x18\x02 \x01(\x05R\fmessageIndex\x12\x1d\n" + - "\n" + - "segment_id\x18\x03 \x01(\tR\tsegmentId\x12\x1d\n" + - "\n" + - "message_id\x18\x04 \x01(\tR\tmessageId\x12\x12\n" + - "\x04role\x18\x05 \x01(\tR\x04role\x12!\n" + - "\fcontent_hash\x18\x06 \x01(\tR\vcontentHash\"S\n" + - "\x11CancelChatRequest\x12'\n" + - "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\x12\x15\n" + - "\x06run_id\x18\x02 \x01(\tR\x05runId\"\xf6\x01\n" + - "\x12ChatCommandRequest\x12\x12\n" + - "\x04type\x18\x01 \x01(\tR\x04type\x12;\n" + - "\arequest\x18\x02 \x01(\v2!.liveagent.gateway.v2.ChatRequestR\arequest\x12N\n" + - "\x10base_message_ref\x18\x03 \x01(\v2$.liveagent.gateway.v2.ChatMessageRefR\x0ebaseMessageRef\x12?\n" + - "\x06cancel\x18\x04 \x01(\v2'.liveagent.gateway.v2.CancelChatRequestR\x06cancel\"\x98\x02\n" + - "\x10ChatQueueRequest\x12\x16\n" + - "\x06action\x18\x01 \x01(\tR\x06action\x12'\n" + - "\x0fconversation_id\x18\x02 \x01(\tR\x0econversationId\x12\x17\n" + - "\aitem_id\x18\x03 \x01(\tR\x06itemId\x12\x1c\n" + - "\tdirection\x18\x04 \x01(\tR\tdirection\x12\x1a\n" + - "\brevision\x18\x05 \x01(\x04R\brevision\x12\x1d\n" + - "\n" + - "draft_json\x18\x06 \x01(\tR\tdraftJson\x12.\n" + - "\x13uploaded_files_json\x18\a \x01(\tR\x11uploadedFilesJson\x12!\n" + - "\frequest_json\x18\b \x01(\tR\vrequestJson\"\xc6\x01\n" + - "\x11ChatQueueResponse\x12\x1a\n" + - "\baccepted\x18\x01 \x01(\bR\baccepted\x12\x18\n" + - "\amessage\x18\x02 \x01(\tR\amessage\x12#\n" + - "\rsnapshot_json\x18\x03 \x01(\tR\fsnapshotJson\x12\x1b\n" + - "\titem_json\x18\x04 \x01(\tR\bitemJson\x12\x1d\n" + - "\n" + - "error_code\x18\x05 \x01(\tR\terrorCode\x12\x1a\n" + - "\brevision\x18\x06 \x01(\x04R\brevision\"z\n" + - "\x0eChatQueueEvent\x12'\n" + - "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\x12#\n" + - "\rsnapshot_json\x18\x02 \x01(\tR\fsnapshotJson\x12\x1a\n" + - "\brevision\x18\x03 \x01(\x04R\brevision\"\xa1\x02\n" + - "\tChatEvent\x12A\n" + - "\x04type\x18\x01 \x01(\x0e2-.liveagent.gateway.v2.ChatEvent.ChatEventTypeR\x04type\x12'\n" + - "\x0fconversation_id\x18\x02 \x01(\tR\x0econversationId\x12\x12\n" + - "\x04data\x18\x03 \x01(\tR\x04data\"\x93\x01\n" + - "\rChatEventType\x12\t\n" + - "\x05TOKEN\x10\x00\x12\f\n" + - "\bTHINKING\x10\x01\x12\r\n" + - "\tTOOL_CALL\x10\x02\x12\x0f\n" + - "\vTOOL_RESULT\x10\x03\x12\b\n" + - "\x04DONE\x10\x04\x12\t\n" + - "\x05ERROR\x10\x05\x12\x0f\n" + - "\vTOOL_STATUS\x10\x06\x12\x11\n" + - "\rHOSTED_SEARCH\x10\a\x12\x10\n" + - "\fUSER_MESSAGE\x10\b\"\x98\x02\n" + - "\x10ChatControlEvent\x12\x1d\n" + - "\n" + - "request_id\x18\x01 \x01(\tR\trequestId\x12*\n" + - "\x11client_request_id\x18\x02 \x01(\tR\x0fclientRequestId\x12'\n" + - "\x0fconversation_id\x18\x03 \x01(\tR\x0econversationId\x12\x1b\n" + - "\trun_epoch\x18\x04 \x01(\x03R\brunEpoch\x12\x12\n" + - "\x04type\x18\x05 \x01(\tR\x04type\x12\x14\n" + - "\x05state\x18\x06 \x01(\tR\x05state\x12\x1d\n" + - "\n" + - "error_code\x18\a \x01(\tR\terrorCode\x12\x18\n" + - "\amessage\x18\b \x01(\tR\amessage\x12\x10\n" + - "\x03seq\x18\t \x01(\x03R\x03seq\"\x80\x03\n" + - "\x13ChatRuntimeSnapshot\x12'\n" + - "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\x12\x15\n" + - "\x06run_id\x18\x02 \x01(\tR\x05runId\x12*\n" + - "\x11client_request_id\x18\x03 \x01(\tR\x0fclientRequestId\x12\x1b\n" + - "\tworker_id\x18\x04 \x01(\tR\bworkerId\x12\x14\n" + - "\x05state\x18\x05 \x01(\tR\x05state\x12\x10\n" + - "\x03cwd\x18\x06 \x01(\tR\x03cwd\x12\x1d\n" + - "\n" + - "updated_at\x18\a \x01(\x03R\tupdatedAt\x12\x1a\n" + - "\brevision\x18\b \x01(\x03R\brevision\x12!\n" + - "\fentries_json\x18\t \x01(\tR\ventriesJson\x12\x1f\n" + - "\vtool_status\x18\n" + - " \x01(\tR\n" + - "toolStatus\x129\n" + - "\x19tool_status_is_compaction\x18\v \x01(\bR\x16toolStatusIsCompaction\"\xb9\x02\n" + - "\x12RuntimeStatusEvent\x12\x1b\n" + - "\tworker_id\x18\x01 \x01(\tR\bworkerId\x12\x14\n" + - "\x05state\x18\x02 \x01(\tR\x05state\x12\x18\n" + - "\avisible\x18\x03 \x01(\bR\avisible\x12(\n" + - "\x10active_run_count\x18\x04 \x01(\rR\x0eactiveRunCount\x12\x1c\n" + - "\ttimestamp\x18\x05 \x01(\x03R\ttimestamp\x12D\n" + - "\vactive_runs\x18\x06 \x03(\v2#.liveagent.gateway.v2.ChatRunReportR\n" + - "activeRuns\x12H\n" + - "\rfinished_runs\x18\a \x03(\v2#.liveagent.gateway.v2.ChatRunReportR\ffinishedRuns\"\xbd\x01\n" + - "\rChatRunReport\x12\x15\n" + - "\x06run_id\x18\x01 \x01(\tR\x05runId\x12'\n" + - "\x0fconversation_id\x18\x02 \x01(\tR\x0econversationId\x12\x14\n" + - "\x05state\x18\x03 \x01(\tR\x05state\x12\x1d\n" + - "\n" + - "error_code\x18\x04 \x01(\tR\terrorCode\x12\x18\n" + - "\amessage\x18\x05 \x01(\tR\amessage\x12\x1d\n" + - "\n" + - "updated_at\x18\x06 \x01(\x03R\tupdatedAt\"a\n" + - "\x11CronManageRequest\x12\x16\n" + - "\x06action\x18\x01 \x01(\tR\x06action\x12\x17\n" + - "\atask_id\x18\x02 \x01(\tR\x06taskId\x12\x1b\n" + - "\ttask_json\x18\x03 \x01(\tR\btaskJson\"M\n" + - "\x12CronManageResponse\x12\x16\n" + - "\x06action\x18\x01 \x01(\tR\x06action\x12\x1f\n" + - "\vresult_json\x18\x02 \x01(\tR\n" + - "resultJson\"t\n" + - "\x12HistoryListRequest\x12\x12\n" + - "\x04page\x18\x01 \x01(\x05R\x04page\x12\x1b\n" + - "\tpage_size\x18\x02 \x01(\x05R\bpageSize\x12\x10\n" + - "\x03cwd\x18\x03 \x01(\tR\x03cwd\x12\x1b\n" + - "\tcwd_empty\x18\x04 \x01(\bR\bcwdEmpty\"\x87\x01\n" + - "\x13HistoryListResponse\x12O\n" + - "\rconversations\x18\x01 \x03(\v2).liveagent.gateway.v2.ConversationSummaryR\rconversations\x12\x1f\n" + - "\vtotal_count\x18\x02 \x01(\x05R\n" + - "totalCount\"\x8d\x03\n" + - "\x13ConversationSummary\x12\x0e\n" + - "\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" + - "\x05title\x18\x02 \x01(\tR\x05title\x12\x1d\n" + - "\n" + - "created_at\x18\x03 \x01(\x03R\tcreatedAt\x12\x1d\n" + - "\n" + - "updated_at\x18\x04 \x01(\x03R\tupdatedAt\x12#\n" + - "\rmessage_count\x18\x05 \x01(\x05R\fmessageCount\x12\x1f\n" + - "\vprovider_id\x18\x06 \x01(\tR\n" + - "providerId\x12\x14\n" + - "\x05model\x18\a \x01(\tR\x05model\x12\x1d\n" + - "\n" + - "session_id\x18\b \x01(\tR\tsessionId\x12\x10\n" + - "\x03cwd\x18\t \x01(\tR\x03cwd\x12\x1b\n" + - "\tis_pinned\x18\n" + - " \x01(\bR\bisPinned\x12\x1b\n" + - "\tpinned_at\x18\v \x01(\x03R\bpinnedAt\x12\x1b\n" + - "\tis_shared\x18\f \x01(\bR\bisShared\x12.\n" + - "\x13selected_model_json\x18\r \x01(\tR\x11selectedModelJson\"_\n" + - "\x11HistoryGetRequest\x12'\n" + - "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\x12!\n" + - "\fmax_messages\x18\x02 \x01(\x05R\vmaxMessages\"\xb2\x02\n" + - "\x12HistoryGetResponse\x12'\n" + - "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\x12#\n" + - "\rmessages_json\x18\x02 \x01(\tR\fmessagesJson\x12.\n" + - "\x13total_message_count\x18\x03 \x01(\x05R\x11totalMessageCount\x124\n" + - "\x16returned_message_count\x18\x04 \x01(\x05R\x14returnedMessageCount\x12\x19\n" + - "\bhas_more\x18\x05 \x01(\bR\ahasMore\x12M\n" + - "\fconversation\x18\x06 \x01(\v2).liveagent.gateway.v2.ConversationSummaryR\fconversation\"\xb2\x01\n" + - "\x14HistoryPrefixRequest\x12'\n" + - "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\x12!\n" + - "\fmax_messages\x18\x02 \x01(\x05R\vmaxMessages\x12N\n" + - "\x10base_message_ref\x18\x03 \x01(\v2$.liveagent.gateway.v2.ChatMessageRefR\x0ebaseMessageRef\"\xb5\x02\n" + - "\x15HistoryPrefixResponse\x12'\n" + - "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\x12#\n" + - "\rmessages_json\x18\x02 \x01(\tR\fmessagesJson\x12.\n" + - "\x13total_message_count\x18\x03 \x01(\x05R\x11totalMessageCount\x124\n" + - "\x16returned_message_count\x18\x04 \x01(\x05R\x14returnedMessageCount\x12\x19\n" + - "\bhas_more\x18\x05 \x01(\bR\ahasMore\x12M\n" + - "\fconversation\x18\x06 \x01(\v2).liveagent.gateway.v2.ConversationSummaryR\fconversation\"U\n" + - "\x14HistoryRenameRequest\x12'\n" + - "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\x12\x14\n" + - "\x05title\x18\x02 \x01(\tR\x05title\"f\n" + - "\x15HistoryRenameResponse\x12M\n" + - "\fconversation\x18\x01 \x01(\v2).liveagent.gateway.v2.ConversationSummaryR\fconversation\"\x8f\x01\n" + - "\x14HistoryBranchRequest\x12'\n" + - "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\x12N\n" + - "\x10base_message_ref\x18\x02 \x01(\v2$.liveagent.gateway.v2.ChatMessageRefR\x0ebaseMessageRef\"f\n" + - "\x15HistoryBranchResponse\x12M\n" + - "\fconversation\x18\x01 \x01(\v2).liveagent.gateway.v2.ConversationSummaryR\fconversation\"Y\n" + - "\x11HistoryPinRequest\x12'\n" + - "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\x12\x1b\n" + - "\tis_pinned\x18\x02 \x01(\bR\bisPinned\"c\n" + - "\x12HistoryPinResponse\x12M\n" + - "\fconversation\x18\x01 \x01(\v2).liveagent.gateway.v2.ConversationSummaryR\fconversation\"\xdb\x01\n" + - "\x12HistoryShareStatus\x12'\n" + - "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\x12\x18\n" + - "\aenabled\x18\x02 \x01(\bR\aenabled\x12\x14\n" + - "\x05token\x18\x03 \x01(\tR\x05token\x12\x1d\n" + - "\n" + - "created_at\x18\x04 \x01(\x03R\tcreatedAt\x12\x1d\n" + - "\n" + - "updated_at\x18\x05 \x01(\x03R\tupdatedAt\x12.\n" + - "\x13redact_tool_content\x18\x06 \x01(\bR\x11redactToolContent\"A\n" + - "\x16HistoryShareGetRequest\x12'\n" + - "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\"Y\n" + - "\x17HistoryShareGetResponse\x12>\n" + - "\x05share\x18\x01 \x01(\v2(.liveagent.gateway.v2.HistoryShareStatusR\x05share\"\xa8\x01\n" + - "\x16HistoryShareSetRequest\x12'\n" + - "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\x12\x18\n" + - "\aenabled\x18\x02 \x01(\bR\aenabled\x123\n" + - "\x13redact_tool_content\x18\x03 \x01(\bH\x00R\x11redactToolContent\x88\x01\x01B\x16\n" + - "\x14_redact_tool_content\"Y\n" + - "\x17HistoryShareSetResponse\x12>\n" + - "\x05share\x18\x01 \x01(\v2(.liveagent.gateway.v2.HistoryShareStatusR\x05share\"2\n" + - "\x1aHistoryShareResolveRequest\x12\x14\n" + - "\x05token\x18\x01 \x01(\tR\x05token\"\x9a\x02\n" + - "\x1bHistoryShareResolveResponse\x12'\n" + - "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\x12#\n" + - "\rmessages_json\x18\x02 \x01(\tR\fmessagesJson\x12.\n" + - "\x13total_message_count\x18\x03 \x01(\x05R\x11totalMessageCount\x12M\n" + - "\fconversation\x18\x04 \x01(\v2).liveagent.gateway.v2.ConversationSummaryR\fconversation\x12.\n" + - "\x13redact_tool_content\x18\x05 \x01(\bR\x11redactToolContent\"\x18\n" + - "\x16HistoryWorkdirsRequest\"y\n" + - "\x15HistoryWorkdirSummary\x12\x12\n" + - "\x04path\x18\x01 \x01(\tR\x04path\x12-\n" + - "\x12conversation_count\x18\x02 \x01(\x05R\x11conversationCount\x12\x1d\n" + - "\n" + - "updated_at\x18\x03 \x01(\x03R\tupdatedAt\"b\n" + - "\x17HistoryWorkdirsResponse\x12G\n" + - "\bworkdirs\x18\x01 \x03(\v2+.liveagent.gateway.v2.HistoryWorkdirSummaryR\bworkdirs\"?\n" + - "\x14HistoryDeleteRequest\x12'\n" + - "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\"\x17\n" + - "\x15HistoryDeleteResponse\"\x9e\x01\n" + - "\x10HistorySyncEvent\x12\x12\n" + - "\x04kind\x18\x01 \x01(\tR\x04kind\x12M\n" + - "\fconversation\x18\x02 \x01(\v2).liveagent.gateway.v2.ConversationSummaryR\fconversation\x12'\n" + - "\x0fconversation_id\x18\x03 \x01(\tR\x0econversationId\"\x15\n" + - "\x13ProviderListRequest\"=\n" + - "\x14ProviderListResponse\x12%\n" + - "\x0eproviders_json\x18\x01 \x01(\tR\rprovidersJson\"\x14\n" + - "\x12SettingsGetRequest\":\n" + - "\x13SettingsGetResponse\x12#\n" + - "\rsettings_json\x18\x01 \x01(\tR\fsettingsJson\"<\n" + - "\x15SettingsUpdateRequest\x12#\n" + - "\rsettings_json\x18\x01 \x01(\tR\fsettingsJson\"N\n" + - "\x16SettingsUpdateResponse\x12\x1a\n" + - "\baccepted\x18\x01 \x01(\bR\baccepted\x12\x18\n" + - "\amessage\x18\x02 \x01(\tR\amessage\"J\n" + - " SettingsResetSshKnownHostRequest\x12\x12\n" + - "\x04host\x18\x01 \x01(\tR\x04host\x12\x12\n" + - "\x04port\x18\x02 \x01(\rR\x04port\"=\n" + - "!SettingsResetSshKnownHostResponse\x12\x18\n" + - "\adeleted\x18\x01 \x01(\rR\adeleted\"8\n" + - "\x11SettingsSyncEvent\x12#\n" + - "\rsettings_json\x18\x01 \x01(\tR\fsettingsJson\"\x17\n" + - "\x15SkillFilesListRequest\"g\n" + - "\x16SkillFilesListResponse\x12\x19\n" + - "\broot_dir\x18\x01 \x01(\tR\arootDir\x12\x14\n" + - "\x05paths\x18\x02 \x03(\tR\x05paths\x12\x1c\n" + - "\ttruncated\x18\x03 \x01(\bR\ttruncated\".\n" + - "\x18SkillMetadataReadRequest\x12\x12\n" + - "\x04path\x18\x01 \x01(\tR\x04path\"Q\n" + - "\x19SkillMetadataReadResponse\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12 \n" + - "\vdescription\x18\x02 \x01(\tR\vdescription\"Z\n" + - "\x14SkillTextReadRequest\x12\x12\n" + - "\x04path\x18\x01 \x01(\tR\x04path\x12\x16\n" + - "\x06offset\x18\x02 \x01(\rR\x06offset\x12\x16\n" + - "\x06length\x18\x03 \x01(\rR\x06length\"O\n" + - "\x15SkillTextReadResponse\x12\x18\n" + - "\acontent\x18\x01 \x01(\tR\acontent\x12\x1c\n" + - "\ttruncated\x18\x02 \x01(\bR\ttruncated\"7\n" + - "\x12SkillManageRequest\x12!\n" + - "\fpayload_json\x18\x01 \x01(\tR\vpayloadJson\"6\n" + - "\x13SkillManageResponse\x12\x1f\n" + - "\vresult_json\x18\x01 \x01(\tR\n" + - "resultJson\"\x9f\x01\n" + - "\x16FileMentionListRequest\x12\x18\n" + - "\aworkdir\x18\x01 \x01(\tR\aworkdir\x12\x1f\n" + - "\vmax_results\x18\x02 \x01(\rR\n" + - "maxResults\x12\x14\n" + - "\x05query\x18\x03 \x01(\tR\x05query\x12$\n" + - "\vshow_hidden\x18\x04 \x01(\bH\x00R\n" + - "showHidden\x88\x01\x01B\x0e\n" + - "\f_show_hidden\"R\n" + - "\x10FileMentionEntry\x12\x12\n" + - "\x04path\x18\x01 \x01(\tR\x04path\x12\x12\n" + - "\x04kind\x18\x02 \x01(\tR\x04kind\x12\x16\n" + - "\x06hidden\x18\x03 \x01(\bR\x06hidden\"y\n" + - "\x17FileMentionListResponse\x12@\n" + - "\aentries\x18\x01 \x03(\v2&.liveagent.gateway.v2.FileMentionEntryR\aentries\x12\x1c\n" + - "\ttruncated\x18\x02 \x01(\bR\ttruncated\"V\n" + - "\x06FsRoot\x12\x0e\n" + - "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + - "\x04path\x18\x02 \x01(\tR\x04path\x12\x12\n" + - "\x04kind\x18\x03 \x01(\tR\x04kind\x12\x14\n" + - "\x05label\x18\x04 \x01(\tR\x05label\"\x10\n" + - "\x0eFsRootsRequest\"E\n" + - "\x0fFsRootsResponse\x122\n" + - "\x05roots\x18\x01 \x03(\v2\x1c.liveagent.gateway.v2.FsRootR\x05roots\"H\n" + - "\x11FsListDirsRequest\x12\x12\n" + - "\x04path\x18\x01 \x01(\tR\x04path\x12\x1f\n" + - "\vmax_results\x18\x02 \x01(\rR\n" + - "maxResults\"4\n" + - "\n" + - "FsDirEntry\x12\x12\n" + - "\x04path\x18\x01 \x01(\tR\x04path\x12\x12\n" + - "\x04name\x18\x02 \x01(\tR\x04name\"\x82\x01\n" + - "\x12FsListDirsResponse\x12\x12\n" + - "\x04path\x18\x01 \x01(\tR\x04path\x12:\n" + - "\aentries\x18\x02 \x03(\v2 .liveagent.gateway.v2.FsDirEntryR\aentries\x12\x1c\n" + - "\ttruncated\x18\x03 \x01(\bR\ttruncated\"J\n" + - "\x1cFsCreateProjectFolderRequest\x12\x16\n" + - "\x06parent\x18\x01 \x01(\tR\x06parent\x12\x12\n" + - "\x04name\x18\x02 \x01(\tR\x04name\"3\n" + - "\x1dFsCreateProjectFolderResponse\x12\x12\n" + - "\x04path\x18\x01 \x01(\tR\x04path\"\xc2\x01\n" + - "\rFsListRequest\x12\x18\n" + - "\aworkdir\x18\x01 \x01(\tR\aworkdir\x12\x12\n" + - "\x04path\x18\x02 \x01(\tR\x04path\x12\x14\n" + - "\x05depth\x18\x03 \x01(\rR\x05depth\x12\x16\n" + - "\x06offset\x18\x04 \x01(\rR\x06offset\x12\x1f\n" + - "\vmax_results\x18\x05 \x01(\rR\n" + - "maxResults\x12$\n" + - "\vshow_hidden\x18\x06 \x01(\bH\x00R\n" + - "showHidden\x88\x01\x01B\x0e\n" + - "\f_show_hidden\"M\n" + - "\vFsListEntry\x12\x12\n" + - "\x04path\x18\x01 \x01(\tR\x04path\x12\x12\n" + - "\x04kind\x18\x02 \x01(\tR\x04kind\x12\x16\n" + - "\x06hidden\x18\x03 \x01(\bR\x06hidden\"\xfc\x01\n" + - "\x0eFsListResponse\x12\x12\n" + - "\x04path\x18\x01 \x01(\tR\x04path\x12\x19\n" + - "\bhas_path\x18\x02 \x01(\bR\ahasPath\x12\x14\n" + - "\x05depth\x18\x03 \x01(\rR\x05depth\x12\x16\n" + - "\x06offset\x18\x04 \x01(\rR\x06offset\x12\x1f\n" + - "\vmax_results\x18\x05 \x01(\rR\n" + - "maxResults\x12\x14\n" + - "\x05total\x18\x06 \x01(\rR\x05total\x12\x19\n" + - "\bhas_more\x18\a \x01(\bR\ahasMore\x12;\n" + - "\aentries\x18\b \x03(\v2!.liveagent.gateway.v2.FsListEntryR\aentries\"I\n" + - "\x19FsReadEditableTextRequest\x12\x18\n" + - "\aworkdir\x18\x01 \x01(\tR\aworkdir\x12\x12\n" + - "\x04path\x18\x02 \x01(\tR\x04path\"\xc8\x01\n" + - "\x1aFsReadEditableTextResponse\x12\x12\n" + - "\x04path\x18\x01 \x01(\tR\x04path\x12\x18\n" + - "\acontent\x18\x02 \x01(\tR\acontent\x12\x19\n" + - "\bmtime_ms\x18\x03 \x01(\x04R\amtimeMs\x12!\n" + - "\fcontent_hash\x18\x04 \x01(\tR\vcontentHash\x12\x1d\n" + - "\n" + - "size_bytes\x18\x05 \x01(\x04R\tsizeBytes\x12\x1f\n" + - "\vtotal_lines\x18\x06 \x01(\x04R\n" + - "totalLines\"K\n" + - "\x1bFsReadWorkspaceImageRequest\x12\x18\n" + - "\aworkdir\x18\x01 \x01(\tR\aworkdir\x12\x12\n" + - "\x04path\x18\x02 \x01(\tR\x04path\"\xc0\x01\n" + - "\x1cFsReadWorkspaceImageResponse\x12\x12\n" + - "\x04path\x18\x01 \x01(\tR\x04path\x12\x1b\n" + - "\tmime_type\x18\x02 \x01(\tR\bmimeType\x12\x12\n" + - "\x04data\x18\x03 \x01(\tR\x04data\x12\x1d\n" + - "\n" + - "size_bytes\x18\x04 \x01(\x04R\tsizeBytes\x12\x19\n" + - "\bmtime_ms\x18\x05 \x01(\x04R\amtimeMs\x12!\n" + - "\fcontent_hash\x18\x06 \x01(\tR\vcontentHash\"\xac\x02\n" + - "\x13ChatFileOpenRequest\x12'\n" + - "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\x12\x18\n" + - "\aworkdir\x18\x02 \x01(\tR\aworkdir\x12\x12\n" + - "\x04path\x18\x03 \x01(\tR\x04path\x12\x16\n" + - "\x06source\x18\x04 \x01(\tR\x06source\x12\x17\n" + - "\x04line\x18\x05 \x01(\rH\x00R\x04line\x88\x01\x01\x12\x1e\n" + - "\bend_line\x18\x06 \x01(\rH\x01R\aendLine\x88\x01\x01\x12\x1b\n" + - "\x06column\x18\a \x01(\rH\x02R\x06column\x88\x01\x01\x12/\n" + - "\x14open_in_file_manager\x18\b \x01(\bR\x11openInFileManagerB\a\n" + - "\x05_lineB\v\n" + - "\t_end_lineB\t\n" + - "\a_column\"\x94\x02\n" + - "\x14ChatFileOpenResponse\x12\x16\n" + - "\x06action\x18\x01 \x01(\tR\x06action\x12\x12\n" + - "\x04kind\x18\x02 \x01(\tR\x04kind\x12\x18\n" + - "\aworkdir\x18\x03 \x01(\tR\aworkdir\x12\x12\n" + - "\x04path\x18\x04 \x01(\tR\x04path\x12\x17\n" + - "\x04line\x18\x05 \x01(\rH\x00R\x04line\x88\x01\x01\x12\x1e\n" + - "\bend_line\x18\x06 \x01(\rH\x01R\aendLine\x88\x01\x01\x12\x1b\n" + - "\x06column\x18\a \x01(\rH\x02R\x06column\x88\x01\x01\x12+\n" + - "\x11outside_workspace\x18\b \x01(\bR\x10outsideWorkspaceB\a\n" + - "\x05_lineB\v\n" + - "\t_end_lineB\t\n" + - "\a_column\"\xbe\x02\n" + - "\x12FsWriteTextRequest\x12\x18\n" + - "\aworkdir\x18\x01 \x01(\tR\aworkdir\x12\x12\n" + - "\x04path\x18\x02 \x01(\tR\x04path\x12\x18\n" + - "\acontent\x18\x03 \x01(\tR\acontent\x12\x12\n" + - "\x04mode\x18\x04 \x01(\tR\x04mode\x12*\n" + - "\x11expected_mtime_ms\x18\x05 \x01(\x04R\x0fexpectedMtimeMs\x122\n" + - "\x15expected_content_hash\x18\x06 \x01(\tR\x13expectedContentHash\x121\n" + - "\x15has_expected_mtime_ms\x18\a \x01(\bR\x12hasExpectedMtimeMs\x129\n" + - "\x19has_expected_content_hash\x18\b \x01(\bR\x16hasExpectedContentHash\"\xe8\x01\n" + - "\x13FsWriteTextResponse\x12\x12\n" + - "\x04path\x18\x01 \x01(\tR\x04path\x12\x12\n" + - "\x04mode\x18\x02 \x01(\tR\x04mode\x12%\n" + - "\x0eexisted_before\x18\x03 \x01(\bR\rexistedBefore\x12#\n" + - "\rbytes_written\x18\x04 \x01(\x04R\fbytesWritten\x12\x19\n" + - "\bmtime_ms\x18\x05 \x01(\x04R\amtimeMs\x12!\n" + - "\fcontent_hash\x18\x06 \x01(\tR\vcontentHash\x12\x1f\n" + - "\vtotal_lines\x18\a \x01(\x04R\n" + - "totalLines\"B\n" + - "\x12FsCreateDirRequest\x12\x18\n" + - "\aworkdir\x18\x01 \x01(\tR\aworkdir\x12\x12\n" + - "\x04path\x18\x02 \x01(\tR\x04path\"=\n" + - "\x13FsCreateDirResponse\x12\x12\n" + - "\x04path\x18\x01 \x01(\tR\x04path\x12\x12\n" + - "\x04kind\x18\x02 \x01(\tR\x04kind\"a\n" + - "\x0fFsRenameRequest\x12\x18\n" + - "\aworkdir\x18\x01 \x01(\tR\aworkdir\x12\x1b\n" + - "\tfrom_path\x18\x02 \x01(\tR\bfromPath\x12\x17\n" + - "\ato_path\x18\x03 \x01(\tR\x06toPath\"W\n" + - "\x10FsRenameResponse\x12\x1b\n" + - "\tfrom_path\x18\x01 \x01(\tR\bfromPath\x12\x12\n" + - "\x04path\x18\x02 \x01(\tR\x04path\x12\x12\n" + - "\x04kind\x18\x03 \x01(\tR\x04kind\"?\n" + - "\x0fFsDeleteRequest\x12\x18\n" + - "\aworkdir\x18\x01 \x01(\tR\aworkdir\x12\x12\n" + - "\x04path\x18\x02 \x01(\tR\x04path\":\n" + - "\x10FsDeleteResponse\x12\x12\n" + - "\x04path\x18\x01 \x01(\tR\x04path\x12\x12\n" + - "\x04kind\x18\x02 \x01(\tR\x04kind\"+\n" + - "\vPingRequest\x12\x1c\n" + - "\ttimestamp\x18\x01 \x01(\x03R\ttimestamp\",\n" + - "\fPongResponse\x12\x1c\n" + - "\ttimestamp\x18\x01 \x01(\x03R\ttimestamp\"=\n" + - "\rErrorResponse\x12\x12\n" + - "\x04code\x18\x01 \x01(\x05R\x04code\x12\x18\n" + - "\amessage\x18\x02 \x01(\tR\amessage\"\x9a\x01\n" + - "\x15ProviderModelsRequest\x12#\n" + - "\rprovider_type\x18\x01 \x01(\tR\fproviderType\x12\x19\n" + - "\bbase_url\x18\x02 \x01(\tR\abaseUrl\x12\x17\n" + - "\aapi_key\x18\x03 \x01(\tR\x06apiKey\x12(\n" + - "\x10use_system_proxy\x18\x04 \x01(\bR\x0euseSystemProxy\"9\n" + - "\x16ProviderModelsResponse\x12\x1f\n" + - "\vmodels_json\x18\x01 \x01(\tR\n" + - "modelsJson\"r\n" + - "\x14ProviderUsageRequest\x12\x1f\n" + - "\vprovider_id\x18\x01 \x01(\tR\n" + - "providerId\x12\x18\n" + - "\arefresh\x18\x02 \x01(\bR\arefresh\x12\x1f\n" + - "\vconfig_json\x18\x03 \x01(\tR\n" + - "configJson\"8\n" + - "\x15ProviderUsageResponse\x12\x1f\n" + - "\vresult_json\x18\x01 \x01(\tR\n" + - "resultJson\"\xb2\x01\n" + - "\x10ChatIngressBatch\x12\x15\n" + - "\x06run_id\x18\x01 \x01(\tR\x05runId\x12'\n" + - "\x0fconversation_id\x18\x02 \x01(\tR\x0econversationId\x12\x1b\n" + - "\tfirst_seq\x18\x03 \x01(\x04R\bfirstSeq\x12A\n" + - "\arecords\x18\x04 \x03(\v2'.liveagent.gateway.v2.ChatIngressRecordR\arecords\"\xc2\x02\n" + - "\x11ChatIngressRecord\x12>\n" + - "\x05delta\x18\x01 \x01(\v2&.liveagent.gateway.v2.ChatIngressDeltaH\x00R\x05delta\x12M\n" + - "\n" + - "checkpoint\x18\x02 \x01(\v2+.liveagent.gateway.v2.ChatIngressCheckpointH\x00R\n" + - "checkpoint\x12G\n" + - "\bterminal\x18\x03 \x01(\v2).liveagent.gateway.v2.ChatIngressTerminalH\x00R\bterminal\x12J\n" + - "\theartbeat\x18\x04 \x01(\v2*.liveagent.gateway.v2.ChatIngressHeartbeatH\x00R\theartbeatB\t\n" + - "\apayload\"N\n" + - "\x10ChatIngressDelta\x12\x1d\n" + - "\n" + - "event_json\x18\x01 \x01(\tR\teventJson\x12\x1b\n" + - "\tworker_id\x18\x02 \x01(\tR\bworkerId\"5\n" + - "\x14ChatIngressHeartbeat\x12\x1d\n" + - "\n" + - "updated_at\x18\x01 \x01(\x03R\tupdatedAt\"\xb3\x02\n" + - "\x15ChatIngressCheckpoint\x12,\n" + - "\x12covers_through_seq\x18\x01 \x01(\x04R\x10coversThroughSeq\x12\x1a\n" + - "\brevision\x18\x02 \x01(\x04R\brevision\x123\n" + - "\x15compressed_projection\x18\x03 \x01(\fR\x14compressedProjection\x12-\n" + - "\x12uncompressed_bytes\x18\x04 \x01(\x04R\x11uncompressedBytes\x12\x16\n" + - "\x06sha256\x18\x05 \x01(\tR\x06sha256\x12)\n" + - "\x10content_complete\x18\x06 \x01(\bR\x0fcontentComplete\x12)\n" + - "\x10history_required\x18\a \x01(\bR\x0fhistoryRequired\"\x8b\x03\n" + - "\x13ChatIngressTerminal\x12,\n" + - "\x12covers_through_seq\x18\x01 \x01(\x04R\x10coversThroughSeq\x12\x1a\n" + - "\brevision\x18\x02 \x01(\x04R\brevision\x123\n" + - "\x15compressed_projection\x18\x03 \x01(\fR\x14compressedProjection\x12-\n" + - "\x12uncompressed_bytes\x18\x04 \x01(\x04R\x11uncompressedBytes\x12\x16\n" + - "\x06sha256\x18\x05 \x01(\tR\x06sha256\x12)\n" + - "\x10content_complete\x18\x06 \x01(\bR\x0fcontentComplete\x12)\n" + - "\x10history_required\x18\a \x01(\bR\x0fhistoryRequired\x12\x14\n" + - "\x05state\x18\b \x01(\tR\x05state\x12\x1d\n" + - "\n" + - "error_code\x18\t \x01(\tR\terrorCode\x12#\n" + - "\rerror_message\x18\n" + - " \x01(\tR\ferrorMessage\"S\n" + - "\x11ChatIngressResume\x12>\n" + - "\x04runs\x18\x01 \x03(\v2*.liveagent.gateway.v2.ChatIngressRunResumeR\x04runs\"\xc9\x02\n" + - "\x14ChatIngressRunResume\x12\x15\n" + - "\x06run_id\x18\x01 \x01(\tR\x05runId\x12'\n" + - "\x0fconversation_id\x18\x02 \x01(\tR\x0econversationId\x12&\n" + - "\x0freplay_from_seq\x18\x03 \x01(\x04R\rreplayFromSeq\x12,\n" + - "\x12replay_through_seq\x18\x04 \x01(\x04R\x10replayThroughSeq\x12\x19\n" + - "\bnext_seq\x18\x05 \x01(\x04R\anextSeq\x122\n" + - "\x15latest_checkpoint_seq\x18\x06 \x01(\x04R\x13latestCheckpointSeq\x12!\n" + - "\fterminal_seq\x18\a \x01(\x04R\vterminalSeq\x12)\n" + - "\x10terminal_pending\x18\b \x01(\bR\x0fterminalPending\"\xbe\x02\n" + - "\x13ChatIngressFragment\x12\x15\n" + - "\x06run_id\x18\x01 \x01(\tR\x05runId\x12'\n" + - "\x0fconversation_id\x18\x02 \x01(\tR\x0econversationId\x12\x1d\n" + - "\n" + - "source_seq\x18\x03 \x01(\x04R\tsourceSeq\x12%\n" + - "\x0efragment_index\x18\x04 \x01(\rR\rfragmentIndex\x12%\n" + - "\x0efragment_count\x18\x05 \x01(\rR\rfragmentCount\x120\n" + - "\x14encoded_record_chunk\x18\x06 \x01(\fR\x12encodedRecordChunk\x120\n" + - "\x14encoded_record_bytes\x18\a \x01(\x04R\x12encodedRecordBytes\x12\x16\n" + - "\x06sha256\x18\b \x01(\tR\x06sha256\"\xc7\x03\n" + - "\x0eChatIngressAck\x12\x15\n" + - "\x06run_id\x18\x01 \x01(\tR\x05runId\x12'\n" + - "\x0fconversation_id\x18\x02 \x01(\tR\x0econversationId\x12+\n" + - "\x11committed_through\x18\x03 \x01(\x04R\x10committedThrough\x12#\n" + - "\rexpected_next\x18\x04 \x01(\x04R\fexpectedNext\x12C\n" + - "\x06action\x18\x05 \x01(\x0e2+.liveagent.gateway.v2.ChatIngressAck.ActionR\x06action\x12-\n" + - "\x12terminal_committed\x18\x06 \x01(\bR\x11terminalCommitted\x12\x1d\n" + - "\n" + - "error_code\x18\a \x01(\tR\terrorCode\x12#\n" + - "\rerror_message\x18\b \x01(\tR\ferrorMessage\"k\n" + - "\x06Action\x12\x16\n" + - "\x12ACTION_UNSPECIFIED\x10\x00\x12\f\n" + - "\bCONTINUE\x10\x01\x12\x18\n" + - "\x14REPLAY_FROM_EXPECTED\x10\x02\x12\x13\n" + - "\x0fSEND_CHECKPOINT\x10\x03\x12\f\n" + - "\bREJECTED\x10\x04*\xc6\x04\n" + - "\x0fTunnelFrameKind\x12!\n" + - "\x1dTUNNEL_FRAME_KIND_UNSPECIFIED\x10\x00\x12(\n" + - "$TUNNEL_FRAME_KIND_HTTP_REQUEST_START\x10\x01\x12'\n" + - "#TUNNEL_FRAME_KIND_HTTP_REQUEST_BODY\x10\x02\x12&\n" + - "\"TUNNEL_FRAME_KIND_HTTP_REQUEST_END\x10\x03\x12)\n" + - "%TUNNEL_FRAME_KIND_HTTP_RESPONSE_START\x10\x04\x12(\n" + - "$TUNNEL_FRAME_KIND_HTTP_RESPONSE_BODY\x10\x05\x12'\n" + - "#TUNNEL_FRAME_KIND_HTTP_RESPONSE_END\x10\x06\x12\x1d\n" + - "\x19TUNNEL_FRAME_KIND_WS_DIAL\x10\a\x12 \n" + - "\x1cTUNNEL_FRAME_KIND_WS_DIAL_OK\x10\b\x12#\n" + - "\x1fTUNNEL_FRAME_KIND_WS_DIAL_ERROR\x10\t\x12\x1e\n" + - "\x1aTUNNEL_FRAME_KIND_WS_FRAME\x10\n" + - "\x12\x1e\n" + - "\x1aTUNNEL_FRAME_KIND_WS_CLOSE\x10\v\x12\x1b\n" + - "\x17TUNNEL_FRAME_KIND_ERROR\x10\f\x12\x1c\n" + - "\x18TUNNEL_FRAME_KIND_CANCEL\x10\r\x12\x1a\n" + - "\x16TUNNEL_FRAME_KIND_PING\x10\x0e\x12\x1a\n" + - "\x16TUNNEL_FRAME_KIND_PONG\x10\x0f*\x81\x01\n" + - "\x13TunnelWsMessageType\x12&\n" + - "\"TUNNEL_WS_MESSAGE_TYPE_UNSPECIFIED\x10\x00\x12\x1f\n" + - "\x1bTUNNEL_WS_MESSAGE_TYPE_TEXT\x10\x01\x12!\n" + - "\x1dTUNNEL_WS_MESSAGE_TYPE_BINARY\x10\x02B@Z>github.com/liveagent/agent-gateway/internal/proto/v2;gatewayv2b\x06proto3" - -var ( - file_proto_v2_gateway_proto_rawDescOnce sync.Once - file_proto_v2_gateway_proto_rawDescData []byte -) - -func file_proto_v2_gateway_proto_rawDescGZIP() []byte { - file_proto_v2_gateway_proto_rawDescOnce.Do(func() { - file_proto_v2_gateway_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_v2_gateway_proto_rawDesc), len(file_proto_v2_gateway_proto_rawDesc))) - }) - return file_proto_v2_gateway_proto_rawDescData -} - -var file_proto_v2_gateway_proto_enumTypes = make([]protoimpl.EnumInfo, 4) -var file_proto_v2_gateway_proto_msgTypes = make([]protoimpl.MessageInfo, 151) -var file_proto_v2_gateway_proto_goTypes = []any{ - (TunnelFrameKind)(0), // 0: liveagent.gateway.v2.TunnelFrameKind - (TunnelWsMessageType)(0), // 1: liveagent.gateway.v2.TunnelWsMessageType - (ChatEvent_ChatEventType)(0), // 2: liveagent.gateway.v2.ChatEvent.ChatEventType - (ChatIngressAck_Action)(0), // 3: liveagent.gateway.v2.ChatIngressAck.Action - (*GatewayEnvelope)(nil), // 4: liveagent.gateway.v2.GatewayEnvelope - (*AgentEnvelope)(nil), // 5: liveagent.gateway.v2.AgentEnvelope - (*ChatSelectedModel)(nil), // 6: liveagent.gateway.v2.ChatSelectedModel - (*ChatRuntimeControls)(nil), // 7: liveagent.gateway.v2.ChatRuntimeControls - (*ChatUploadedFile)(nil), // 8: liveagent.gateway.v2.ChatUploadedFile - (*UploadReadableFile)(nil), // 9: liveagent.gateway.v2.UploadReadableFile - (*UploadReadableFilesRequest)(nil), // 10: liveagent.gateway.v2.UploadReadableFilesRequest - (*UploadReadableFilesResponse)(nil), // 11: liveagent.gateway.v2.UploadReadableFilesResponse - (*UploadedImagePreviewRequest)(nil), // 12: liveagent.gateway.v2.UploadedImagePreviewRequest - (*UploadedImagePreviewResponse)(nil), // 13: liveagent.gateway.v2.UploadedImagePreviewResponse - (*TunnelSpec)(nil), // 14: liveagent.gateway.v2.TunnelSpec - (*TunnelDesiredState)(nil), // 15: liveagent.gateway.v2.TunnelDesiredState - (*TunnelHealth)(nil), // 16: liveagent.gateway.v2.TunnelHealth - (*TunnelStatus)(nil), // 17: liveagent.gateway.v2.TunnelStatus - (*TunnelStateSnapshot)(nil), // 18: liveagent.gateway.v2.TunnelStateSnapshot - (*TunnelMutation)(nil), // 19: liveagent.gateway.v2.TunnelMutation - (*TunnelMutationResult)(nil), // 20: liveagent.gateway.v2.TunnelMutationResult - (*TunnelProbeResult)(nil), // 21: liveagent.gateway.v2.TunnelProbeResult - (*TunnelProbeReport)(nil), // 22: liveagent.gateway.v2.TunnelProbeReport - (*TunnelHeader)(nil), // 23: liveagent.gateway.v2.TunnelHeader - (*TunnelFrame)(nil), // 24: liveagent.gateway.v2.TunnelFrame - (*WorkspaceWatchRequest)(nil), // 25: liveagent.gateway.v2.WorkspaceWatchRequest - (*WorkspaceActivityEvent)(nil), // 26: liveagent.gateway.v2.WorkspaceActivityEvent - (*ManagedProcessRecord)(nil), // 27: liveagent.gateway.v2.ManagedProcessRecord - (*ManagedProcessSnapshot)(nil), // 28: liveagent.gateway.v2.ManagedProcessSnapshot - (*ManagedProcessRequest)(nil), // 29: liveagent.gateway.v2.ManagedProcessRequest - (*ManagedProcessResponse)(nil), // 30: liveagent.gateway.v2.ManagedProcessResponse - (*MemoryManageRequest)(nil), // 31: liveagent.gateway.v2.MemoryManageRequest - (*MemoryManageResponse)(nil), // 32: liveagent.gateway.v2.MemoryManageResponse - (*TerminalRequest)(nil), // 33: liveagent.gateway.v2.TerminalRequest - (*TerminalSession)(nil), // 34: liveagent.gateway.v2.TerminalSession - (*TerminalSshMetadata)(nil), // 35: liveagent.gateway.v2.TerminalSshMetadata - (*SftpRequest)(nil), // 36: liveagent.gateway.v2.SftpRequest - (*SftpEntry)(nil), // 37: liveagent.gateway.v2.SftpEntry - (*SftpTransfer)(nil), // 38: liveagent.gateway.v2.SftpTransfer - (*SftpResponse)(nil), // 39: liveagent.gateway.v2.SftpResponse - (*SftpEvent)(nil), // 40: liveagent.gateway.v2.SftpEvent - (*TerminalSshPrompt)(nil), // 41: liveagent.gateway.v2.TerminalSshPrompt - (*TerminalShellOption)(nil), // 42: liveagent.gateway.v2.TerminalShellOption - (*TerminalSshTab)(nil), // 43: liveagent.gateway.v2.TerminalSshTab - (*TerminalSshTabsSnapshot)(nil), // 44: liveagent.gateway.v2.TerminalSshTabsSnapshot - (*TerminalSshLocalForward)(nil), // 45: liveagent.gateway.v2.TerminalSshLocalForward - (*TerminalSshLocalForwardsSnapshot)(nil), // 46: liveagent.gateway.v2.TerminalSshLocalForwardsSnapshot - (*TerminalSshLocalForwardAction)(nil), // 47: liveagent.gateway.v2.TerminalSshLocalForwardAction - (*TerminalResponse)(nil), // 48: liveagent.gateway.v2.TerminalResponse - (*TerminalEvent)(nil), // 49: liveagent.gateway.v2.TerminalEvent - (*TerminalStreamFrame)(nil), // 50: liveagent.gateway.v2.TerminalStreamFrame - (*GitRequest)(nil), // 51: liveagent.gateway.v2.GitRequest - (*GitResponse)(nil), // 52: liveagent.gateway.v2.GitResponse - (*ChatRequest)(nil), // 53: liveagent.gateway.v2.ChatRequest - (*ChatMessageRef)(nil), // 54: liveagent.gateway.v2.ChatMessageRef - (*CancelChatRequest)(nil), // 55: liveagent.gateway.v2.CancelChatRequest - (*ChatCommandRequest)(nil), // 56: liveagent.gateway.v2.ChatCommandRequest - (*ChatQueueRequest)(nil), // 57: liveagent.gateway.v2.ChatQueueRequest - (*ChatQueueResponse)(nil), // 58: liveagent.gateway.v2.ChatQueueResponse - (*ChatQueueEvent)(nil), // 59: liveagent.gateway.v2.ChatQueueEvent - (*ChatEvent)(nil), // 60: liveagent.gateway.v2.ChatEvent - (*ChatControlEvent)(nil), // 61: liveagent.gateway.v2.ChatControlEvent - (*ChatRuntimeSnapshot)(nil), // 62: liveagent.gateway.v2.ChatRuntimeSnapshot - (*RuntimeStatusEvent)(nil), // 63: liveagent.gateway.v2.RuntimeStatusEvent - (*ChatRunReport)(nil), // 64: liveagent.gateway.v2.ChatRunReport - (*CronManageRequest)(nil), // 65: liveagent.gateway.v2.CronManageRequest - (*CronManageResponse)(nil), // 66: liveagent.gateway.v2.CronManageResponse - (*HistoryListRequest)(nil), // 67: liveagent.gateway.v2.HistoryListRequest - (*HistoryListResponse)(nil), // 68: liveagent.gateway.v2.HistoryListResponse - (*ConversationSummary)(nil), // 69: liveagent.gateway.v2.ConversationSummary - (*HistoryGetRequest)(nil), // 70: liveagent.gateway.v2.HistoryGetRequest - (*HistoryGetResponse)(nil), // 71: liveagent.gateway.v2.HistoryGetResponse - (*HistoryPrefixRequest)(nil), // 72: liveagent.gateway.v2.HistoryPrefixRequest - (*HistoryPrefixResponse)(nil), // 73: liveagent.gateway.v2.HistoryPrefixResponse - (*HistoryRenameRequest)(nil), // 74: liveagent.gateway.v2.HistoryRenameRequest - (*HistoryRenameResponse)(nil), // 75: liveagent.gateway.v2.HistoryRenameResponse - (*HistoryBranchRequest)(nil), // 76: liveagent.gateway.v2.HistoryBranchRequest - (*HistoryBranchResponse)(nil), // 77: liveagent.gateway.v2.HistoryBranchResponse - (*HistoryPinRequest)(nil), // 78: liveagent.gateway.v2.HistoryPinRequest - (*HistoryPinResponse)(nil), // 79: liveagent.gateway.v2.HistoryPinResponse - (*HistoryShareStatus)(nil), // 80: liveagent.gateway.v2.HistoryShareStatus - (*HistoryShareGetRequest)(nil), // 81: liveagent.gateway.v2.HistoryShareGetRequest - (*HistoryShareGetResponse)(nil), // 82: liveagent.gateway.v2.HistoryShareGetResponse - (*HistoryShareSetRequest)(nil), // 83: liveagent.gateway.v2.HistoryShareSetRequest - (*HistoryShareSetResponse)(nil), // 84: liveagent.gateway.v2.HistoryShareSetResponse - (*HistoryShareResolveRequest)(nil), // 85: liveagent.gateway.v2.HistoryShareResolveRequest - (*HistoryShareResolveResponse)(nil), // 86: liveagent.gateway.v2.HistoryShareResolveResponse - (*HistoryWorkdirsRequest)(nil), // 87: liveagent.gateway.v2.HistoryWorkdirsRequest - (*HistoryWorkdirSummary)(nil), // 88: liveagent.gateway.v2.HistoryWorkdirSummary - (*HistoryWorkdirsResponse)(nil), // 89: liveagent.gateway.v2.HistoryWorkdirsResponse - (*HistoryDeleteRequest)(nil), // 90: liveagent.gateway.v2.HistoryDeleteRequest - (*HistoryDeleteResponse)(nil), // 91: liveagent.gateway.v2.HistoryDeleteResponse - (*HistorySyncEvent)(nil), // 92: liveagent.gateway.v2.HistorySyncEvent - (*ProviderListRequest)(nil), // 93: liveagent.gateway.v2.ProviderListRequest - (*ProviderListResponse)(nil), // 94: liveagent.gateway.v2.ProviderListResponse - (*SettingsGetRequest)(nil), // 95: liveagent.gateway.v2.SettingsGetRequest - (*SettingsGetResponse)(nil), // 96: liveagent.gateway.v2.SettingsGetResponse - (*SettingsUpdateRequest)(nil), // 97: liveagent.gateway.v2.SettingsUpdateRequest - (*SettingsUpdateResponse)(nil), // 98: liveagent.gateway.v2.SettingsUpdateResponse - (*SettingsResetSshKnownHostRequest)(nil), // 99: liveagent.gateway.v2.SettingsResetSshKnownHostRequest - (*SettingsResetSshKnownHostResponse)(nil), // 100: liveagent.gateway.v2.SettingsResetSshKnownHostResponse - (*SettingsSyncEvent)(nil), // 101: liveagent.gateway.v2.SettingsSyncEvent - (*SkillFilesListRequest)(nil), // 102: liveagent.gateway.v2.SkillFilesListRequest - (*SkillFilesListResponse)(nil), // 103: liveagent.gateway.v2.SkillFilesListResponse - (*SkillMetadataReadRequest)(nil), // 104: liveagent.gateway.v2.SkillMetadataReadRequest - (*SkillMetadataReadResponse)(nil), // 105: liveagent.gateway.v2.SkillMetadataReadResponse - (*SkillTextReadRequest)(nil), // 106: liveagent.gateway.v2.SkillTextReadRequest - (*SkillTextReadResponse)(nil), // 107: liveagent.gateway.v2.SkillTextReadResponse - (*SkillManageRequest)(nil), // 108: liveagent.gateway.v2.SkillManageRequest - (*SkillManageResponse)(nil), // 109: liveagent.gateway.v2.SkillManageResponse - (*FileMentionListRequest)(nil), // 110: liveagent.gateway.v2.FileMentionListRequest - (*FileMentionEntry)(nil), // 111: liveagent.gateway.v2.FileMentionEntry - (*FileMentionListResponse)(nil), // 112: liveagent.gateway.v2.FileMentionListResponse - (*FsRoot)(nil), // 113: liveagent.gateway.v2.FsRoot - (*FsRootsRequest)(nil), // 114: liveagent.gateway.v2.FsRootsRequest - (*FsRootsResponse)(nil), // 115: liveagent.gateway.v2.FsRootsResponse - (*FsListDirsRequest)(nil), // 116: liveagent.gateway.v2.FsListDirsRequest - (*FsDirEntry)(nil), // 117: liveagent.gateway.v2.FsDirEntry - (*FsListDirsResponse)(nil), // 118: liveagent.gateway.v2.FsListDirsResponse - (*FsCreateProjectFolderRequest)(nil), // 119: liveagent.gateway.v2.FsCreateProjectFolderRequest - (*FsCreateProjectFolderResponse)(nil), // 120: liveagent.gateway.v2.FsCreateProjectFolderResponse - (*FsListRequest)(nil), // 121: liveagent.gateway.v2.FsListRequest - (*FsListEntry)(nil), // 122: liveagent.gateway.v2.FsListEntry - (*FsListResponse)(nil), // 123: liveagent.gateway.v2.FsListResponse - (*FsReadEditableTextRequest)(nil), // 124: liveagent.gateway.v2.FsReadEditableTextRequest - (*FsReadEditableTextResponse)(nil), // 125: liveagent.gateway.v2.FsReadEditableTextResponse - (*FsReadWorkspaceImageRequest)(nil), // 126: liveagent.gateway.v2.FsReadWorkspaceImageRequest - (*FsReadWorkspaceImageResponse)(nil), // 127: liveagent.gateway.v2.FsReadWorkspaceImageResponse - (*ChatFileOpenRequest)(nil), // 128: liveagent.gateway.v2.ChatFileOpenRequest - (*ChatFileOpenResponse)(nil), // 129: liveagent.gateway.v2.ChatFileOpenResponse - (*FsWriteTextRequest)(nil), // 130: liveagent.gateway.v2.FsWriteTextRequest - (*FsWriteTextResponse)(nil), // 131: liveagent.gateway.v2.FsWriteTextResponse - (*FsCreateDirRequest)(nil), // 132: liveagent.gateway.v2.FsCreateDirRequest - (*FsCreateDirResponse)(nil), // 133: liveagent.gateway.v2.FsCreateDirResponse - (*FsRenameRequest)(nil), // 134: liveagent.gateway.v2.FsRenameRequest - (*FsRenameResponse)(nil), // 135: liveagent.gateway.v2.FsRenameResponse - (*FsDeleteRequest)(nil), // 136: liveagent.gateway.v2.FsDeleteRequest - (*FsDeleteResponse)(nil), // 137: liveagent.gateway.v2.FsDeleteResponse - (*PingRequest)(nil), // 138: liveagent.gateway.v2.PingRequest - (*PongResponse)(nil), // 139: liveagent.gateway.v2.PongResponse - (*ErrorResponse)(nil), // 140: liveagent.gateway.v2.ErrorResponse - (*ProviderModelsRequest)(nil), // 141: liveagent.gateway.v2.ProviderModelsRequest - (*ProviderModelsResponse)(nil), // 142: liveagent.gateway.v2.ProviderModelsResponse - (*ProviderUsageRequest)(nil), // 143: liveagent.gateway.v2.ProviderUsageRequest - (*ProviderUsageResponse)(nil), // 144: liveagent.gateway.v2.ProviderUsageResponse - (*ChatIngressBatch)(nil), // 145: liveagent.gateway.v2.ChatIngressBatch - (*ChatIngressRecord)(nil), // 146: liveagent.gateway.v2.ChatIngressRecord - (*ChatIngressDelta)(nil), // 147: liveagent.gateway.v2.ChatIngressDelta - (*ChatIngressHeartbeat)(nil), // 148: liveagent.gateway.v2.ChatIngressHeartbeat - (*ChatIngressCheckpoint)(nil), // 149: liveagent.gateway.v2.ChatIngressCheckpoint - (*ChatIngressTerminal)(nil), // 150: liveagent.gateway.v2.ChatIngressTerminal - (*ChatIngressResume)(nil), // 151: liveagent.gateway.v2.ChatIngressResume - (*ChatIngressRunResume)(nil), // 152: liveagent.gateway.v2.ChatIngressRunResume - (*ChatIngressFragment)(nil), // 153: liveagent.gateway.v2.ChatIngressFragment - (*ChatIngressAck)(nil), // 154: liveagent.gateway.v2.ChatIngressAck -} -var file_proto_v2_gateway_proto_depIdxs = []int32{ - 56, // 0: liveagent.gateway.v2.GatewayEnvelope.chat_command:type_name -> liveagent.gateway.v2.ChatCommandRequest - 65, // 1: liveagent.gateway.v2.GatewayEnvelope.cron_manage:type_name -> liveagent.gateway.v2.CronManageRequest - 67, // 2: liveagent.gateway.v2.GatewayEnvelope.history_list:type_name -> liveagent.gateway.v2.HistoryListRequest - 70, // 3: liveagent.gateway.v2.GatewayEnvelope.history_get:type_name -> liveagent.gateway.v2.HistoryGetRequest - 74, // 4: liveagent.gateway.v2.GatewayEnvelope.history_rename:type_name -> liveagent.gateway.v2.HistoryRenameRequest - 90, // 5: liveagent.gateway.v2.GatewayEnvelope.history_delete:type_name -> liveagent.gateway.v2.HistoryDeleteRequest - 72, // 6: liveagent.gateway.v2.GatewayEnvelope.history_prefix:type_name -> liveagent.gateway.v2.HistoryPrefixRequest - 78, // 7: liveagent.gateway.v2.GatewayEnvelope.history_pin:type_name -> liveagent.gateway.v2.HistoryPinRequest - 81, // 8: liveagent.gateway.v2.GatewayEnvelope.history_share_get:type_name -> liveagent.gateway.v2.HistoryShareGetRequest - 83, // 9: liveagent.gateway.v2.GatewayEnvelope.history_share_set:type_name -> liveagent.gateway.v2.HistoryShareSetRequest - 85, // 10: liveagent.gateway.v2.GatewayEnvelope.history_share_resolve:type_name -> liveagent.gateway.v2.HistoryShareResolveRequest - 87, // 11: liveagent.gateway.v2.GatewayEnvelope.history_workdirs:type_name -> liveagent.gateway.v2.HistoryWorkdirsRequest - 93, // 12: liveagent.gateway.v2.GatewayEnvelope.provider_list:type_name -> liveagent.gateway.v2.ProviderListRequest - 95, // 13: liveagent.gateway.v2.GatewayEnvelope.settings_get:type_name -> liveagent.gateway.v2.SettingsGetRequest - 97, // 14: liveagent.gateway.v2.GatewayEnvelope.settings_update:type_name -> liveagent.gateway.v2.SettingsUpdateRequest - 102, // 15: liveagent.gateway.v2.GatewayEnvelope.skill_files_list:type_name -> liveagent.gateway.v2.SkillFilesListRequest - 104, // 16: liveagent.gateway.v2.GatewayEnvelope.skill_metadata_read:type_name -> liveagent.gateway.v2.SkillMetadataReadRequest - 106, // 17: liveagent.gateway.v2.GatewayEnvelope.skill_text_read:type_name -> liveagent.gateway.v2.SkillTextReadRequest - 110, // 18: liveagent.gateway.v2.GatewayEnvelope.file_mention_list:type_name -> liveagent.gateway.v2.FileMentionListRequest - 10, // 19: liveagent.gateway.v2.GatewayEnvelope.upload_readable_files:type_name -> liveagent.gateway.v2.UploadReadableFilesRequest - 114, // 20: liveagent.gateway.v2.GatewayEnvelope.fs_roots:type_name -> liveagent.gateway.v2.FsRootsRequest - 116, // 21: liveagent.gateway.v2.GatewayEnvelope.fs_list_dirs:type_name -> liveagent.gateway.v2.FsListDirsRequest - 138, // 22: liveagent.gateway.v2.GatewayEnvelope.ping:type_name -> liveagent.gateway.v2.PingRequest - 12, // 23: liveagent.gateway.v2.GatewayEnvelope.uploaded_image_preview:type_name -> liveagent.gateway.v2.UploadedImagePreviewRequest - 31, // 24: liveagent.gateway.v2.GatewayEnvelope.memory_manage:type_name -> liveagent.gateway.v2.MemoryManageRequest - 108, // 25: liveagent.gateway.v2.GatewayEnvelope.skill_manage:type_name -> liveagent.gateway.v2.SkillManageRequest - 119, // 26: liveagent.gateway.v2.GatewayEnvelope.fs_create_project_folder:type_name -> liveagent.gateway.v2.FsCreateProjectFolderRequest - 33, // 27: liveagent.gateway.v2.GatewayEnvelope.terminal_request:type_name -> liveagent.gateway.v2.TerminalRequest - 121, // 28: liveagent.gateway.v2.GatewayEnvelope.fs_list:type_name -> liveagent.gateway.v2.FsListRequest - 130, // 29: liveagent.gateway.v2.GatewayEnvelope.fs_write_text:type_name -> liveagent.gateway.v2.FsWriteTextRequest - 132, // 30: liveagent.gateway.v2.GatewayEnvelope.fs_create_dir:type_name -> liveagent.gateway.v2.FsCreateDirRequest - 134, // 31: liveagent.gateway.v2.GatewayEnvelope.fs_rename:type_name -> liveagent.gateway.v2.FsRenameRequest - 136, // 32: liveagent.gateway.v2.GatewayEnvelope.fs_delete:type_name -> liveagent.gateway.v2.FsDeleteRequest - 51, // 33: liveagent.gateway.v2.GatewayEnvelope.git_request:type_name -> liveagent.gateway.v2.GitRequest - 124, // 34: liveagent.gateway.v2.GatewayEnvelope.fs_read_editable_text:type_name -> liveagent.gateway.v2.FsReadEditableTextRequest - 126, // 35: liveagent.gateway.v2.GatewayEnvelope.fs_read_workspace_image:type_name -> liveagent.gateway.v2.FsReadWorkspaceImageRequest - 36, // 36: liveagent.gateway.v2.GatewayEnvelope.sftp_request:type_name -> liveagent.gateway.v2.SftpRequest - 141, // 37: liveagent.gateway.v2.GatewayEnvelope.provider_models:type_name -> liveagent.gateway.v2.ProviderModelsRequest - 99, // 38: liveagent.gateway.v2.GatewayEnvelope.settings_reset_ssh_known_host:type_name -> liveagent.gateway.v2.SettingsResetSshKnownHostRequest - 57, // 39: liveagent.gateway.v2.GatewayEnvelope.chat_queue:type_name -> liveagent.gateway.v2.ChatQueueRequest - 154, // 40: liveagent.gateway.v2.GatewayEnvelope.chat_ingress_ack:type_name -> liveagent.gateway.v2.ChatIngressAck - 18, // 41: liveagent.gateway.v2.GatewayEnvelope.tunnel_state:type_name -> liveagent.gateway.v2.TunnelStateSnapshot - 19, // 42: liveagent.gateway.v2.GatewayEnvelope.tunnel_mutation:type_name -> liveagent.gateway.v2.TunnelMutation - 24, // 43: liveagent.gateway.v2.GatewayEnvelope.tunnel_frame:type_name -> liveagent.gateway.v2.TunnelFrame - 25, // 44: liveagent.gateway.v2.GatewayEnvelope.workspace_watch:type_name -> liveagent.gateway.v2.WorkspaceWatchRequest - 29, // 45: liveagent.gateway.v2.GatewayEnvelope.managed_process_request:type_name -> liveagent.gateway.v2.ManagedProcessRequest - 76, // 46: liveagent.gateway.v2.GatewayEnvelope.history_branch:type_name -> liveagent.gateway.v2.HistoryBranchRequest - 143, // 47: liveagent.gateway.v2.GatewayEnvelope.provider_usage:type_name -> liveagent.gateway.v2.ProviderUsageRequest - 128, // 48: liveagent.gateway.v2.GatewayEnvelope.chat_file_open:type_name -> liveagent.gateway.v2.ChatFileOpenRequest - 60, // 49: liveagent.gateway.v2.AgentEnvelope.chat_event:type_name -> liveagent.gateway.v2.ChatEvent - 66, // 50: liveagent.gateway.v2.AgentEnvelope.cron_manage_resp:type_name -> liveagent.gateway.v2.CronManageResponse - 68, // 51: liveagent.gateway.v2.AgentEnvelope.history_list_resp:type_name -> liveagent.gateway.v2.HistoryListResponse - 71, // 52: liveagent.gateway.v2.AgentEnvelope.history_get_resp:type_name -> liveagent.gateway.v2.HistoryGetResponse - 75, // 53: liveagent.gateway.v2.AgentEnvelope.history_rename_resp:type_name -> liveagent.gateway.v2.HistoryRenameResponse - 91, // 54: liveagent.gateway.v2.AgentEnvelope.history_delete_resp:type_name -> liveagent.gateway.v2.HistoryDeleteResponse - 92, // 55: liveagent.gateway.v2.AgentEnvelope.history_sync:type_name -> liveagent.gateway.v2.HistorySyncEvent - 73, // 56: liveagent.gateway.v2.AgentEnvelope.history_prefix_resp:type_name -> liveagent.gateway.v2.HistoryPrefixResponse - 79, // 57: liveagent.gateway.v2.AgentEnvelope.history_pin_resp:type_name -> liveagent.gateway.v2.HistoryPinResponse - 82, // 58: liveagent.gateway.v2.AgentEnvelope.history_share_get_resp:type_name -> liveagent.gateway.v2.HistoryShareGetResponse - 84, // 59: liveagent.gateway.v2.AgentEnvelope.history_share_set_resp:type_name -> liveagent.gateway.v2.HistoryShareSetResponse - 86, // 60: liveagent.gateway.v2.AgentEnvelope.history_share_resolve_resp:type_name -> liveagent.gateway.v2.HistoryShareResolveResponse - 89, // 61: liveagent.gateway.v2.AgentEnvelope.history_workdirs_resp:type_name -> liveagent.gateway.v2.HistoryWorkdirsResponse - 94, // 62: liveagent.gateway.v2.AgentEnvelope.provider_list_resp:type_name -> liveagent.gateway.v2.ProviderListResponse - 96, // 63: liveagent.gateway.v2.AgentEnvelope.settings_get_resp:type_name -> liveagent.gateway.v2.SettingsGetResponse - 98, // 64: liveagent.gateway.v2.AgentEnvelope.settings_update_resp:type_name -> liveagent.gateway.v2.SettingsUpdateResponse - 101, // 65: liveagent.gateway.v2.AgentEnvelope.settings_sync:type_name -> liveagent.gateway.v2.SettingsSyncEvent - 103, // 66: liveagent.gateway.v2.AgentEnvelope.skill_files_list_resp:type_name -> liveagent.gateway.v2.SkillFilesListResponse - 105, // 67: liveagent.gateway.v2.AgentEnvelope.skill_metadata_read_resp:type_name -> liveagent.gateway.v2.SkillMetadataReadResponse - 107, // 68: liveagent.gateway.v2.AgentEnvelope.skill_text_read_resp:type_name -> liveagent.gateway.v2.SkillTextReadResponse - 112, // 69: liveagent.gateway.v2.AgentEnvelope.file_mention_list_resp:type_name -> liveagent.gateway.v2.FileMentionListResponse - 11, // 70: liveagent.gateway.v2.AgentEnvelope.upload_readable_files_resp:type_name -> liveagent.gateway.v2.UploadReadableFilesResponse - 115, // 71: liveagent.gateway.v2.AgentEnvelope.fs_roots_resp:type_name -> liveagent.gateway.v2.FsRootsResponse - 139, // 72: liveagent.gateway.v2.AgentEnvelope.pong:type_name -> liveagent.gateway.v2.PongResponse - 118, // 73: liveagent.gateway.v2.AgentEnvelope.fs_list_dirs_resp:type_name -> liveagent.gateway.v2.FsListDirsResponse - 13, // 74: liveagent.gateway.v2.AgentEnvelope.uploaded_image_preview_resp:type_name -> liveagent.gateway.v2.UploadedImagePreviewResponse - 32, // 75: liveagent.gateway.v2.AgentEnvelope.memory_manage_resp:type_name -> liveagent.gateway.v2.MemoryManageResponse - 109, // 76: liveagent.gateway.v2.AgentEnvelope.skill_manage_resp:type_name -> liveagent.gateway.v2.SkillManageResponse - 120, // 77: liveagent.gateway.v2.AgentEnvelope.fs_create_project_folder_resp:type_name -> liveagent.gateway.v2.FsCreateProjectFolderResponse - 48, // 78: liveagent.gateway.v2.AgentEnvelope.terminal_response:type_name -> liveagent.gateway.v2.TerminalResponse - 49, // 79: liveagent.gateway.v2.AgentEnvelope.terminal_event:type_name -> liveagent.gateway.v2.TerminalEvent - 123, // 80: liveagent.gateway.v2.AgentEnvelope.fs_list_resp:type_name -> liveagent.gateway.v2.FsListResponse - 131, // 81: liveagent.gateway.v2.AgentEnvelope.fs_write_text_resp:type_name -> liveagent.gateway.v2.FsWriteTextResponse - 133, // 82: liveagent.gateway.v2.AgentEnvelope.fs_create_dir_resp:type_name -> liveagent.gateway.v2.FsCreateDirResponse - 135, // 83: liveagent.gateway.v2.AgentEnvelope.fs_rename_resp:type_name -> liveagent.gateway.v2.FsRenameResponse - 137, // 84: liveagent.gateway.v2.AgentEnvelope.fs_delete_resp:type_name -> liveagent.gateway.v2.FsDeleteResponse - 52, // 85: liveagent.gateway.v2.AgentEnvelope.git_response:type_name -> liveagent.gateway.v2.GitResponse - 125, // 86: liveagent.gateway.v2.AgentEnvelope.fs_read_editable_text_resp:type_name -> liveagent.gateway.v2.FsReadEditableTextResponse - 127, // 87: liveagent.gateway.v2.AgentEnvelope.fs_read_workspace_image_resp:type_name -> liveagent.gateway.v2.FsReadWorkspaceImageResponse - 39, // 88: liveagent.gateway.v2.AgentEnvelope.sftp_response:type_name -> liveagent.gateway.v2.SftpResponse - 40, // 89: liveagent.gateway.v2.AgentEnvelope.sftp_event:type_name -> liveagent.gateway.v2.SftpEvent - 58, // 90: liveagent.gateway.v2.AgentEnvelope.chat_queue_resp:type_name -> liveagent.gateway.v2.ChatQueueResponse - 59, // 91: liveagent.gateway.v2.AgentEnvelope.chat_queue_event:type_name -> liveagent.gateway.v2.ChatQueueEvent - 61, // 92: liveagent.gateway.v2.AgentEnvelope.chat_control:type_name -> liveagent.gateway.v2.ChatControlEvent - 63, // 93: liveagent.gateway.v2.AgentEnvelope.runtime_status:type_name -> liveagent.gateway.v2.RuntimeStatusEvent - 100, // 94: liveagent.gateway.v2.AgentEnvelope.settings_reset_ssh_known_host_resp:type_name -> liveagent.gateway.v2.SettingsResetSshKnownHostResponse - 62, // 95: liveagent.gateway.v2.AgentEnvelope.chat_runtime_snapshot:type_name -> liveagent.gateway.v2.ChatRuntimeSnapshot - 142, // 96: liveagent.gateway.v2.AgentEnvelope.provider_models_resp:type_name -> liveagent.gateway.v2.ProviderModelsResponse - 15, // 97: liveagent.gateway.v2.AgentEnvelope.tunnel_desired:type_name -> liveagent.gateway.v2.TunnelDesiredState - 20, // 98: liveagent.gateway.v2.AgentEnvelope.tunnel_mutation_result:type_name -> liveagent.gateway.v2.TunnelMutationResult - 24, // 99: liveagent.gateway.v2.AgentEnvelope.tunnel_frame:type_name -> liveagent.gateway.v2.TunnelFrame - 22, // 100: liveagent.gateway.v2.AgentEnvelope.tunnel_probe_report:type_name -> liveagent.gateway.v2.TunnelProbeReport - 26, // 101: liveagent.gateway.v2.AgentEnvelope.workspace_activity:type_name -> liveagent.gateway.v2.WorkspaceActivityEvent - 30, // 102: liveagent.gateway.v2.AgentEnvelope.managed_process_response:type_name -> liveagent.gateway.v2.ManagedProcessResponse - 28, // 103: liveagent.gateway.v2.AgentEnvelope.managed_process_snapshot:type_name -> liveagent.gateway.v2.ManagedProcessSnapshot - 77, // 104: liveagent.gateway.v2.AgentEnvelope.history_branch_resp:type_name -> liveagent.gateway.v2.HistoryBranchResponse - 144, // 105: liveagent.gateway.v2.AgentEnvelope.provider_usage_resp:type_name -> liveagent.gateway.v2.ProviderUsageResponse - 145, // 106: liveagent.gateway.v2.AgentEnvelope.chat_ingress_batch:type_name -> liveagent.gateway.v2.ChatIngressBatch - 151, // 107: liveagent.gateway.v2.AgentEnvelope.chat_ingress_resume:type_name -> liveagent.gateway.v2.ChatIngressResume - 153, // 108: liveagent.gateway.v2.AgentEnvelope.chat_ingress_fragment:type_name -> liveagent.gateway.v2.ChatIngressFragment - 129, // 109: liveagent.gateway.v2.AgentEnvelope.chat_file_open_resp:type_name -> liveagent.gateway.v2.ChatFileOpenResponse - 140, // 110: liveagent.gateway.v2.AgentEnvelope.error:type_name -> liveagent.gateway.v2.ErrorResponse - 9, // 111: liveagent.gateway.v2.UploadReadableFilesRequest.files:type_name -> liveagent.gateway.v2.UploadReadableFile - 8, // 112: liveagent.gateway.v2.UploadReadableFilesResponse.files:type_name -> liveagent.gateway.v2.ChatUploadedFile - 14, // 113: liveagent.gateway.v2.TunnelDesiredState.tunnels:type_name -> liveagent.gateway.v2.TunnelSpec - 16, // 114: liveagent.gateway.v2.TunnelStatus.local:type_name -> liveagent.gateway.v2.TunnelHealth - 17, // 115: liveagent.gateway.v2.TunnelStateSnapshot.tunnels:type_name -> liveagent.gateway.v2.TunnelStatus - 16, // 116: liveagent.gateway.v2.TunnelStateSnapshot.relay:type_name -> liveagent.gateway.v2.TunnelHealth - 16, // 117: liveagent.gateway.v2.TunnelProbeResult.local:type_name -> liveagent.gateway.v2.TunnelHealth - 21, // 118: liveagent.gateway.v2.TunnelProbeReport.results:type_name -> liveagent.gateway.v2.TunnelProbeResult - 0, // 119: liveagent.gateway.v2.TunnelFrame.kind:type_name -> liveagent.gateway.v2.TunnelFrameKind - 23, // 120: liveagent.gateway.v2.TunnelFrame.headers:type_name -> liveagent.gateway.v2.TunnelHeader - 1, // 121: liveagent.gateway.v2.TunnelFrame.ws_message_type:type_name -> liveagent.gateway.v2.TunnelWsMessageType - 27, // 122: liveagent.gateway.v2.ManagedProcessSnapshot.processes:type_name -> liveagent.gateway.v2.ManagedProcessRecord - 28, // 123: liveagent.gateway.v2.ManagedProcessResponse.snapshot:type_name -> liveagent.gateway.v2.ManagedProcessSnapshot - 35, // 124: liveagent.gateway.v2.TerminalSession.ssh:type_name -> liveagent.gateway.v2.TerminalSshMetadata - 37, // 125: liveagent.gateway.v2.SftpResponse.entries:type_name -> liveagent.gateway.v2.SftpEntry - 37, // 126: liveagent.gateway.v2.SftpResponse.entry:type_name -> liveagent.gateway.v2.SftpEntry - 38, // 127: liveagent.gateway.v2.SftpResponse.transfer:type_name -> liveagent.gateway.v2.SftpTransfer - 38, // 128: liveagent.gateway.v2.SftpEvent.transfer:type_name -> liveagent.gateway.v2.SftpTransfer - 43, // 129: liveagent.gateway.v2.TerminalSshTabsSnapshot.tabs:type_name -> liveagent.gateway.v2.TerminalSshTab - 45, // 130: liveagent.gateway.v2.TerminalSshLocalForwardsSnapshot.forwards:type_name -> liveagent.gateway.v2.TerminalSshLocalForward - 45, // 131: liveagent.gateway.v2.TerminalSshLocalForwardAction.forward:type_name -> liveagent.gateway.v2.TerminalSshLocalForward - 34, // 132: liveagent.gateway.v2.TerminalResponse.sessions:type_name -> liveagent.gateway.v2.TerminalSession - 34, // 133: liveagent.gateway.v2.TerminalResponse.session:type_name -> liveagent.gateway.v2.TerminalSession - 42, // 134: liveagent.gateway.v2.TerminalResponse.shell_options:type_name -> liveagent.gateway.v2.TerminalShellOption - 41, // 135: liveagent.gateway.v2.TerminalResponse.ssh_prompt:type_name -> liveagent.gateway.v2.TerminalSshPrompt - 44, // 136: liveagent.gateway.v2.TerminalResponse.ssh_tabs:type_name -> liveagent.gateway.v2.TerminalSshTabsSnapshot - 46, // 137: liveagent.gateway.v2.TerminalResponse.ssh_local_forwards:type_name -> liveagent.gateway.v2.TerminalSshLocalForwardsSnapshot - 47, // 138: liveagent.gateway.v2.TerminalResponse.ssh_local_forward:type_name -> liveagent.gateway.v2.TerminalSshLocalForwardAction - 34, // 139: liveagent.gateway.v2.TerminalEvent.session:type_name -> liveagent.gateway.v2.TerminalSession - 44, // 140: liveagent.gateway.v2.TerminalEvent.ssh_tabs:type_name -> liveagent.gateway.v2.TerminalSshTabsSnapshot - 47, // 141: liveagent.gateway.v2.TerminalEvent.ssh_local_forward:type_name -> liveagent.gateway.v2.TerminalSshLocalForwardAction - 34, // 142: liveagent.gateway.v2.TerminalStreamFrame.session:type_name -> liveagent.gateway.v2.TerminalSession - 6, // 143: liveagent.gateway.v2.ChatRequest.selected_model:type_name -> liveagent.gateway.v2.ChatSelectedModel - 8, // 144: liveagent.gateway.v2.ChatRequest.uploaded_files:type_name -> liveagent.gateway.v2.ChatUploadedFile - 7, // 145: liveagent.gateway.v2.ChatRequest.runtime_controls:type_name -> liveagent.gateway.v2.ChatRuntimeControls - 53, // 146: liveagent.gateway.v2.ChatCommandRequest.request:type_name -> liveagent.gateway.v2.ChatRequest - 54, // 147: liveagent.gateway.v2.ChatCommandRequest.base_message_ref:type_name -> liveagent.gateway.v2.ChatMessageRef - 55, // 148: liveagent.gateway.v2.ChatCommandRequest.cancel:type_name -> liveagent.gateway.v2.CancelChatRequest - 2, // 149: liveagent.gateway.v2.ChatEvent.type:type_name -> liveagent.gateway.v2.ChatEvent.ChatEventType - 64, // 150: liveagent.gateway.v2.RuntimeStatusEvent.active_runs:type_name -> liveagent.gateway.v2.ChatRunReport - 64, // 151: liveagent.gateway.v2.RuntimeStatusEvent.finished_runs:type_name -> liveagent.gateway.v2.ChatRunReport - 69, // 152: liveagent.gateway.v2.HistoryListResponse.conversations:type_name -> liveagent.gateway.v2.ConversationSummary - 69, // 153: liveagent.gateway.v2.HistoryGetResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary - 54, // 154: liveagent.gateway.v2.HistoryPrefixRequest.base_message_ref:type_name -> liveagent.gateway.v2.ChatMessageRef - 69, // 155: liveagent.gateway.v2.HistoryPrefixResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary - 69, // 156: liveagent.gateway.v2.HistoryRenameResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary - 54, // 157: liveagent.gateway.v2.HistoryBranchRequest.base_message_ref:type_name -> liveagent.gateway.v2.ChatMessageRef - 69, // 158: liveagent.gateway.v2.HistoryBranchResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary - 69, // 159: liveagent.gateway.v2.HistoryPinResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary - 80, // 160: liveagent.gateway.v2.HistoryShareGetResponse.share:type_name -> liveagent.gateway.v2.HistoryShareStatus - 80, // 161: liveagent.gateway.v2.HistoryShareSetResponse.share:type_name -> liveagent.gateway.v2.HistoryShareStatus - 69, // 162: liveagent.gateway.v2.HistoryShareResolveResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary - 88, // 163: liveagent.gateway.v2.HistoryWorkdirsResponse.workdirs:type_name -> liveagent.gateway.v2.HistoryWorkdirSummary - 69, // 164: liveagent.gateway.v2.HistorySyncEvent.conversation:type_name -> liveagent.gateway.v2.ConversationSummary - 111, // 165: liveagent.gateway.v2.FileMentionListResponse.entries:type_name -> liveagent.gateway.v2.FileMentionEntry - 113, // 166: liveagent.gateway.v2.FsRootsResponse.roots:type_name -> liveagent.gateway.v2.FsRoot - 117, // 167: liveagent.gateway.v2.FsListDirsResponse.entries:type_name -> liveagent.gateway.v2.FsDirEntry - 122, // 168: liveagent.gateway.v2.FsListResponse.entries:type_name -> liveagent.gateway.v2.FsListEntry - 146, // 169: liveagent.gateway.v2.ChatIngressBatch.records:type_name -> liveagent.gateway.v2.ChatIngressRecord - 147, // 170: liveagent.gateway.v2.ChatIngressRecord.delta:type_name -> liveagent.gateway.v2.ChatIngressDelta - 149, // 171: liveagent.gateway.v2.ChatIngressRecord.checkpoint:type_name -> liveagent.gateway.v2.ChatIngressCheckpoint - 150, // 172: liveagent.gateway.v2.ChatIngressRecord.terminal:type_name -> liveagent.gateway.v2.ChatIngressTerminal - 148, // 173: liveagent.gateway.v2.ChatIngressRecord.heartbeat:type_name -> liveagent.gateway.v2.ChatIngressHeartbeat - 152, // 174: liveagent.gateway.v2.ChatIngressResume.runs:type_name -> liveagent.gateway.v2.ChatIngressRunResume - 3, // 175: liveagent.gateway.v2.ChatIngressAck.action:type_name -> liveagent.gateway.v2.ChatIngressAck.Action - 176, // [176:176] is the sub-list for method output_type - 176, // [176:176] is the sub-list for method input_type - 176, // [176:176] is the sub-list for extension type_name - 176, // [176:176] is the sub-list for extension extendee - 0, // [0:176] is the sub-list for field type_name -} - -func init() { file_proto_v2_gateway_proto_init() } -func file_proto_v2_gateway_proto_init() { - if File_proto_v2_gateway_proto != nil { - return - } - file_proto_v2_gateway_proto_msgTypes[0].OneofWrappers = []any{ - (*GatewayEnvelope_ChatCommand)(nil), - (*GatewayEnvelope_CronManage)(nil), - (*GatewayEnvelope_HistoryList)(nil), - (*GatewayEnvelope_HistoryGet)(nil), - (*GatewayEnvelope_HistoryRename)(nil), - (*GatewayEnvelope_HistoryDelete)(nil), - (*GatewayEnvelope_HistoryPrefix)(nil), - (*GatewayEnvelope_HistoryPin)(nil), - (*GatewayEnvelope_HistoryShareGet)(nil), - (*GatewayEnvelope_HistoryShareSet)(nil), - (*GatewayEnvelope_HistoryShareResolve)(nil), - (*GatewayEnvelope_HistoryWorkdirs)(nil), - (*GatewayEnvelope_ProviderList)(nil), - (*GatewayEnvelope_SettingsGet)(nil), - (*GatewayEnvelope_SettingsUpdate)(nil), - (*GatewayEnvelope_SkillFilesList)(nil), - (*GatewayEnvelope_SkillMetadataRead)(nil), - (*GatewayEnvelope_SkillTextRead)(nil), - (*GatewayEnvelope_FileMentionList)(nil), - (*GatewayEnvelope_UploadReadableFiles)(nil), - (*GatewayEnvelope_FsRoots)(nil), - (*GatewayEnvelope_FsListDirs)(nil), - (*GatewayEnvelope_Ping)(nil), - (*GatewayEnvelope_UploadedImagePreview)(nil), - (*GatewayEnvelope_MemoryManage)(nil), - (*GatewayEnvelope_SkillManage)(nil), - (*GatewayEnvelope_FsCreateProjectFolder)(nil), - (*GatewayEnvelope_TerminalRequest)(nil), - (*GatewayEnvelope_FsList)(nil), - (*GatewayEnvelope_FsWriteText)(nil), - (*GatewayEnvelope_FsCreateDir)(nil), - (*GatewayEnvelope_FsRename)(nil), - (*GatewayEnvelope_FsDelete)(nil), - (*GatewayEnvelope_GitRequest)(nil), - (*GatewayEnvelope_FsReadEditableText)(nil), - (*GatewayEnvelope_FsReadWorkspaceImage)(nil), - (*GatewayEnvelope_SftpRequest)(nil), - (*GatewayEnvelope_ProviderModels)(nil), - (*GatewayEnvelope_SettingsResetSshKnownHost)(nil), - (*GatewayEnvelope_ChatQueue)(nil), - (*GatewayEnvelope_ChatIngressAck)(nil), - (*GatewayEnvelope_TunnelState)(nil), - (*GatewayEnvelope_TunnelMutation)(nil), - (*GatewayEnvelope_TunnelFrame)(nil), - (*GatewayEnvelope_WorkspaceWatch)(nil), - (*GatewayEnvelope_ManagedProcessRequest)(nil), - (*GatewayEnvelope_HistoryBranch)(nil), - (*GatewayEnvelope_ProviderUsage)(nil), - (*GatewayEnvelope_ChatFileOpen)(nil), - } - file_proto_v2_gateway_proto_msgTypes[1].OneofWrappers = []any{ - (*AgentEnvelope_ChatEvent)(nil), - (*AgentEnvelope_CronManageResp)(nil), - (*AgentEnvelope_HistoryListResp)(nil), - (*AgentEnvelope_HistoryGetResp)(nil), - (*AgentEnvelope_HistoryRenameResp)(nil), - (*AgentEnvelope_HistoryDeleteResp)(nil), - (*AgentEnvelope_HistorySync)(nil), - (*AgentEnvelope_HistoryPrefixResp)(nil), - (*AgentEnvelope_HistoryPinResp)(nil), - (*AgentEnvelope_HistoryShareGetResp)(nil), - (*AgentEnvelope_HistoryShareSetResp)(nil), - (*AgentEnvelope_HistoryShareResolveResp)(nil), - (*AgentEnvelope_HistoryWorkdirsResp)(nil), - (*AgentEnvelope_ProviderListResp)(nil), - (*AgentEnvelope_SettingsGetResp)(nil), - (*AgentEnvelope_SettingsUpdateResp)(nil), - (*AgentEnvelope_SettingsSync)(nil), - (*AgentEnvelope_SkillFilesListResp)(nil), - (*AgentEnvelope_SkillMetadataReadResp)(nil), - (*AgentEnvelope_SkillTextReadResp)(nil), - (*AgentEnvelope_FileMentionListResp)(nil), - (*AgentEnvelope_UploadReadableFilesResp)(nil), - (*AgentEnvelope_FsRootsResp)(nil), - (*AgentEnvelope_Pong)(nil), - (*AgentEnvelope_FsListDirsResp)(nil), - (*AgentEnvelope_UploadedImagePreviewResp)(nil), - (*AgentEnvelope_MemoryManageResp)(nil), - (*AgentEnvelope_SkillManageResp)(nil), - (*AgentEnvelope_FsCreateProjectFolderResp)(nil), - (*AgentEnvelope_TerminalResponse)(nil), - (*AgentEnvelope_TerminalEvent)(nil), - (*AgentEnvelope_FsListResp)(nil), - (*AgentEnvelope_FsWriteTextResp)(nil), - (*AgentEnvelope_FsCreateDirResp)(nil), - (*AgentEnvelope_FsRenameResp)(nil), - (*AgentEnvelope_FsDeleteResp)(nil), - (*AgentEnvelope_GitResponse)(nil), - (*AgentEnvelope_FsReadEditableTextResp)(nil), - (*AgentEnvelope_FsReadWorkspaceImageResp)(nil), - (*AgentEnvelope_SftpResponse)(nil), - (*AgentEnvelope_SftpEvent)(nil), - (*AgentEnvelope_ChatQueueResp)(nil), - (*AgentEnvelope_ChatQueueEvent)(nil), - (*AgentEnvelope_ChatControl)(nil), - (*AgentEnvelope_RuntimeStatus)(nil), - (*AgentEnvelope_SettingsResetSshKnownHostResp)(nil), - (*AgentEnvelope_ChatRuntimeSnapshot)(nil), - (*AgentEnvelope_ProviderModelsResp)(nil), - (*AgentEnvelope_TunnelDesired)(nil), - (*AgentEnvelope_TunnelMutationResult)(nil), - (*AgentEnvelope_TunnelFrame)(nil), - (*AgentEnvelope_TunnelProbeReport)(nil), - (*AgentEnvelope_WorkspaceActivity)(nil), - (*AgentEnvelope_ManagedProcessResponse)(nil), - (*AgentEnvelope_ManagedProcessSnapshot)(nil), - (*AgentEnvelope_HistoryBranchResp)(nil), - (*AgentEnvelope_ProviderUsageResp)(nil), - (*AgentEnvelope_ChatIngressBatch)(nil), - (*AgentEnvelope_ChatIngressResume)(nil), - (*AgentEnvelope_ChatIngressFragment)(nil), - (*AgentEnvelope_ChatFileOpenResp)(nil), - (*AgentEnvelope_Error)(nil), - } - file_proto_v2_gateway_proto_msgTypes[15].OneofWrappers = []any{} - file_proto_v2_gateway_proto_msgTypes[23].OneofWrappers = []any{} - file_proto_v2_gateway_proto_msgTypes[79].OneofWrappers = []any{} - file_proto_v2_gateway_proto_msgTypes[106].OneofWrappers = []any{} - file_proto_v2_gateway_proto_msgTypes[117].OneofWrappers = []any{} - file_proto_v2_gateway_proto_msgTypes[124].OneofWrappers = []any{} - file_proto_v2_gateway_proto_msgTypes[125].OneofWrappers = []any{} - file_proto_v2_gateway_proto_msgTypes[142].OneofWrappers = []any{ - (*ChatIngressRecord_Delta)(nil), - (*ChatIngressRecord_Checkpoint)(nil), - (*ChatIngressRecord_Terminal)(nil), - (*ChatIngressRecord_Heartbeat)(nil), - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_v2_gateway_proto_rawDesc), len(file_proto_v2_gateway_proto_rawDesc)), - NumEnums: 4, - NumMessages: 151, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_proto_v2_gateway_proto_goTypes, - DependencyIndexes: file_proto_v2_gateway_proto_depIdxs, - EnumInfos: file_proto_v2_gateway_proto_enumTypes, - MessageInfos: file_proto_v2_gateway_proto_msgTypes, - }.Build() - File_proto_v2_gateway_proto = out.File - file_proto_v2_gateway_proto_goTypes = nil - file_proto_v2_gateway_proto_depIdxs = nil -} diff --git a/crates/agent-gateway/internal/proto/v2/gateway_ws.pb.go b/crates/agent-gateway/internal/proto/v2/gateway_ws.pb.go deleted file mode 100644 index 9014834d7..000000000 --- a/crates/agent-gateway/internal/proto/v2/gateway_ws.pb.go +++ /dev/null @@ -1,3169 +0,0 @@ -// v2 统一线协议(WebSocket+Protobuf):/ws/v2(浏览器)、/ws/v2/agent(桌面端)、 -// /ws/v2/terminal(终端数据面)三链路的帧壳。一条 WS 二进制消息承载一条帧,文本帧即协议错误; -// 首帧必须为 hello,鉴权失败以 close code 4401 关闭。业务载荷复用 gateway.proto 的 v2 消息(三端唯一事实源), -// 本文件仅定义帧壳与网关本地载荷。 - -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc (unknown) -// source: proto/v2/gateway_ws.proto - -package gatewayv2 - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -// ClientRole 区分 /ws/v2/terminal 上连接的所属端(该链路两端共用一条路径,靠 hello.role 区分)。 -type ClientRole int32 - -const ( - ClientRole_CLIENT_ROLE_UNSPECIFIED ClientRole = 0 - ClientRole_CLIENT_ROLE_BROWSER ClientRole = 1 - ClientRole_CLIENT_ROLE_AGENT ClientRole = 2 -) - -// Enum value maps for ClientRole. -var ( - ClientRole_name = map[int32]string{ - 0: "CLIENT_ROLE_UNSPECIFIED", - 1: "CLIENT_ROLE_BROWSER", - 2: "CLIENT_ROLE_AGENT", - } - ClientRole_value = map[string]int32{ - "CLIENT_ROLE_UNSPECIFIED": 0, - "CLIENT_ROLE_BROWSER": 1, - "CLIENT_ROLE_AGENT": 2, - } -) - -func (x ClientRole) Enum() *ClientRole { - p := new(ClientRole) - *p = x - return p -} - -func (x ClientRole) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (ClientRole) Descriptor() protoreflect.EnumDescriptor { - return file_proto_v2_gateway_ws_proto_enumTypes[0].Descriptor() -} - -func (ClientRole) Type() protoreflect.EnumType { - return &file_proto_v2_gateway_ws_proto_enumTypes[0] -} - -func (x ClientRole) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use ClientRole.Descriptor instead. -func (ClientRole) EnumDescriptor() ([]byte, []int) { - return file_proto_v2_gateway_ws_proto_rawDescGZIP(), []int{0} -} - -// ClientHello 是所有 v2 连接的第一帧。 -type ClientHello struct { - state protoimpl.MessageState `protogen:"open.v1"` - // 协议版本,当前恒为 2;未知版本被服务端拒绝。 - ProtocolVersion uint32 `protobuf:"varint,1,opt,name=protocol_version,json=protocolVersion,proto3" json:"protocol_version,omitempty"` - Role ClientRole `protobuf:"varint,2,opt,name=role,proto3,enum=liveagent.gateway.v2.ClientRole" json:"role,omitempty"` - // 网关访问令牌,服务端做常量时间比较。 - Token string `protobuf:"bytes,3,opt,name=token,proto3" json:"token,omitempty"` - // CLIENT_ROLE_AGENT 必须以 agent_id 声明自身身份;浏览器角色在 - // /ws/v2/terminal 上也必须以 agent_id 显式绑定数据面目标。 - AgentId string `protobuf:"bytes,4,opt,name=agent_id,json=agentId,proto3" json:"agent_id,omitempty"` - AgentVersion string `protobuf:"bytes,5,opt,name=agent_version,json=agentVersion,proto3" json:"agent_version,omitempty"` - // 客户端标识(如 "webui" / "desktop"),仅用于观测与日志。 - ClientName string `protobuf:"bytes,6,opt,name=client_name,json=clientName,proto3" json:"client_name,omitempty"` - ClientVersion string `protobuf:"bytes,7,opt,name=client_version,json=clientVersion,proto3" json:"client_version,omitempty"` - // Optional feature identifiers supported by this client. Reliable desktop - // chat ingress is negotiated with "CHAT_INGRESS_V1". - Capabilities []string `protobuf:"bytes,8,rep,name=capabilities,proto3" json:"capabilities,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ClientHello) Reset() { - *x = ClientHello{} - mi := &file_proto_v2_gateway_ws_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ClientHello) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ClientHello) ProtoMessage() {} - -func (x *ClientHello) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_ws_proto_msgTypes[0] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ClientHello.ProtoReflect.Descriptor instead. -func (*ClientHello) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_ws_proto_rawDescGZIP(), []int{0} -} - -func (x *ClientHello) GetProtocolVersion() uint32 { - if x != nil { - return x.ProtocolVersion - } - return 0 -} - -func (x *ClientHello) GetRole() ClientRole { - if x != nil { - return x.Role - } - return ClientRole_CLIENT_ROLE_UNSPECIFIED -} - -func (x *ClientHello) GetToken() string { - if x != nil { - return x.Token - } - return "" -} - -func (x *ClientHello) GetAgentId() string { - if x != nil { - return x.AgentId - } - return "" -} - -func (x *ClientHello) GetAgentVersion() string { - if x != nil { - return x.AgentVersion - } - return "" -} - -func (x *ClientHello) GetClientName() string { - if x != nil { - return x.ClientName - } - return "" -} - -func (x *ClientHello) GetClientVersion() string { - if x != nil { - return x.ClientVersion - } - return "" -} - -func (x *ClientHello) GetCapabilities() []string { - if x != nil { - return x.Capabilities - } - return nil -} - -// ServerHello 是服务端对 ClientHello 的应答;ok=false 时随即关闭连接。 -type ServerHello struct { - state protoimpl.MessageState `protogen:"open.v1"` - Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` - Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` - // 仅 agent 角色返回。 - SessionId string `protobuf:"bytes,3,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` - // 服务端 Unix 秒时间戳,供客户端校准。 - ServerTime int64 `protobuf:"varint,4,opt,name=server_time,json=serverTime,proto3" json:"server_time,omitempty"` - // 服务端心跳周期与消息大小上限,客户端应据此配置本地看门狗与分片。 - HeartbeatPeriodSeconds uint32 `protobuf:"varint,5,opt,name=heartbeat_period_seconds,json=heartbeatPeriodSeconds,proto3" json:"heartbeat_period_seconds,omitempty"` - MaxMessageBytes uint64 `protobuf:"varint,6,opt,name=max_message_bytes,json=maxMessageBytes,proto3" json:"max_message_bytes,omitempty"` - // Feature identifiers supported by the gateway. - Capabilities []string `protobuf:"bytes,7,rep,name=capabilities,proto3" json:"capabilities,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ServerHello) Reset() { - *x = ServerHello{} - mi := &file_proto_v2_gateway_ws_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ServerHello) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ServerHello) ProtoMessage() {} - -func (x *ServerHello) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_ws_proto_msgTypes[1] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ServerHello.ProtoReflect.Descriptor instead. -func (*ServerHello) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_ws_proto_rawDescGZIP(), []int{1} -} - -func (x *ServerHello) GetOk() bool { - if x != nil { - return x.Ok - } - return false -} - -func (x *ServerHello) GetMessage() string { - if x != nil { - return x.Message - } - return "" -} - -func (x *ServerHello) GetSessionId() string { - if x != nil { - return x.SessionId - } - return "" -} - -func (x *ServerHello) GetServerTime() int64 { - if x != nil { - return x.ServerTime - } - return 0 -} - -func (x *ServerHello) GetHeartbeatPeriodSeconds() uint32 { - if x != nil { - return x.HeartbeatPeriodSeconds - } - return 0 -} - -func (x *ServerHello) GetMaxMessageBytes() uint64 { - if x != nil { - return x.MaxMessageBytes - } - return 0 -} - -func (x *ServerHello) GetCapabilities() []string { - if x != nil { - return x.Capabilities - } - return nil -} - -// PingFrame / PongFrame 是应用层心跳:WS 控制帧 ping 探测网络栈,本帧探测页面 JS/事件循环存活。 -type PingFrame struct { - state protoimpl.MessageState `protogen:"open.v1"` - Timestamp int64 `protobuf:"varint,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *PingFrame) Reset() { - *x = PingFrame{} - mi := &file_proto_v2_gateway_ws_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *PingFrame) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*PingFrame) ProtoMessage() {} - -func (x *PingFrame) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_ws_proto_msgTypes[2] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use PingFrame.ProtoReflect.Descriptor instead. -func (*PingFrame) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_ws_proto_rawDescGZIP(), []int{2} -} - -func (x *PingFrame) GetTimestamp() int64 { - if x != nil { - return x.Timestamp - } - return 0 -} - -type PongFrame struct { - state protoimpl.MessageState `protogen:"open.v1"` - Timestamp int64 `protobuf:"varint,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *PongFrame) Reset() { - *x = PongFrame{} - mi := &file_proto_v2_gateway_ws_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *PongFrame) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*PongFrame) ProtoMessage() {} - -func (x *PongFrame) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_ws_proto_msgTypes[3] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use PongFrame.ProtoReflect.Descriptor instead. -func (*PongFrame) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_ws_proto_rawDescGZIP(), []int{3} -} - -func (x *PongFrame) GetTimestamp() int64 { - if x != nil { - return x.Timestamp - } - return 0 -} - -// AckResult 是本地操作的通用确认应答。 -type AckResult struct { - state protoimpl.MessageState `protogen:"open.v1"` - Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *AckResult) Reset() { - *x = AckResult{} - mi := &file_proto_v2_gateway_ws_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AckResult) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AckResult) ProtoMessage() {} - -func (x *AckResult) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_ws_proto_msgTypes[4] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use AckResult.ProtoReflect.Descriptor instead. -func (*AckResult) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_ws_proto_rawDescGZIP(), []int{4} -} - -func (x *AckResult) GetOk() bool { - if x != nil { - return x.Ok - } - return false -} - -// WebClientFrame 为浏览器 → 网关方向的帧。除 agent_request 直通臂外,其余臂均为 -// 网关本地操作(由网关自身状态应答,不经桌面端往返)。 -type WebClientFrame struct { - state protoimpl.MessageState `protogen:"open.v1"` - // 请求关联 id,客户端生成、响应帧回携;广播事件与心跳帧为空。 - RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` - // 目标 Agent id。agent_request / status_get / chat_command / chat_prepare / - // workspace_* 必填;hello / pong / agent_list 与全局会话查询臂忽略本字段。 - AgentId string `protobuf:"bytes,13,opt,name=agent_id,json=agentId,proto3" json:"agent_id,omitempty"` - // Types that are valid to be assigned to Payload: - // - // *WebClientFrame_Hello - // *WebClientFrame_AgentRequest - // *WebClientFrame_StatusGet - // *WebClientFrame_ChatCommand - // *WebClientFrame_ChatPrepare - // *WebClientFrame_ChatSubscribe - // *WebClientFrame_ChatUnsubscribe - // *WebClientFrame_ChatActivities - // *WebClientFrame_WorkspaceSubscribe - // *WebClientFrame_WorkspaceUnsubscribe - // *WebClientFrame_Pong - // *WebClientFrame_AgentList - Payload isWebClientFrame_Payload `protobuf_oneof:"payload"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *WebClientFrame) Reset() { - *x = WebClientFrame{} - mi := &file_proto_v2_gateway_ws_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *WebClientFrame) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*WebClientFrame) ProtoMessage() {} - -func (x *WebClientFrame) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_ws_proto_msgTypes[5] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use WebClientFrame.ProtoReflect.Descriptor instead. -func (*WebClientFrame) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_ws_proto_rawDescGZIP(), []int{5} -} - -func (x *WebClientFrame) GetRequestId() string { - if x != nil { - return x.RequestId - } - return "" -} - -func (x *WebClientFrame) GetAgentId() string { - if x != nil { - return x.AgentId - } - return "" -} - -func (x *WebClientFrame) GetPayload() isWebClientFrame_Payload { - if x != nil { - return x.Payload - } - return nil -} - -func (x *WebClientFrame) GetHello() *ClientHello { - if x != nil { - if x, ok := x.Payload.(*WebClientFrame_Hello); ok { - return x.Hello - } - } - return nil -} - -func (x *WebClientFrame) GetAgentRequest() *GatewayEnvelope { - if x != nil { - if x, ok := x.Payload.(*WebClientFrame_AgentRequest); ok { - return x.AgentRequest - } - } - return nil -} - -func (x *WebClientFrame) GetStatusGet() *StatusGetRequest { - if x != nil { - if x, ok := x.Payload.(*WebClientFrame_StatusGet); ok { - return x.StatusGet - } - } - return nil -} - -func (x *WebClientFrame) GetChatCommand() *ChatCommandRequest { - if x != nil { - if x, ok := x.Payload.(*WebClientFrame_ChatCommand); ok { - return x.ChatCommand - } - } - return nil -} - -func (x *WebClientFrame) GetChatPrepare() *ChatPrepareRequest { - if x != nil { - if x, ok := x.Payload.(*WebClientFrame_ChatPrepare); ok { - return x.ChatPrepare - } - } - return nil -} - -func (x *WebClientFrame) GetChatSubscribe() *ChatSubscribeRequest { - if x != nil { - if x, ok := x.Payload.(*WebClientFrame_ChatSubscribe); ok { - return x.ChatSubscribe - } - } - return nil -} - -func (x *WebClientFrame) GetChatUnsubscribe() *ChatUnsubscribeRequest { - if x != nil { - if x, ok := x.Payload.(*WebClientFrame_ChatUnsubscribe); ok { - return x.ChatUnsubscribe - } - } - return nil -} - -func (x *WebClientFrame) GetChatActivities() *ChatActivitiesRequest { - if x != nil { - if x, ok := x.Payload.(*WebClientFrame_ChatActivities); ok { - return x.ChatActivities - } - } - return nil -} - -func (x *WebClientFrame) GetWorkspaceSubscribe() *WorkspaceSubscribeRequest { - if x != nil { - if x, ok := x.Payload.(*WebClientFrame_WorkspaceSubscribe); ok { - return x.WorkspaceSubscribe - } - } - return nil -} - -func (x *WebClientFrame) GetWorkspaceUnsubscribe() *WorkspaceUnsubscribeRequest { - if x != nil { - if x, ok := x.Payload.(*WebClientFrame_WorkspaceUnsubscribe); ok { - return x.WorkspaceUnsubscribe - } - } - return nil -} - -func (x *WebClientFrame) GetPong() *PongFrame { - if x != nil { - if x, ok := x.Payload.(*WebClientFrame_Pong); ok { - return x.Pong - } - } - return nil -} - -func (x *WebClientFrame) GetAgentList() *AgentListRequest { - if x != nil { - if x, ok := x.Payload.(*WebClientFrame_AgentList); ok { - return x.AgentList - } - } - return nil -} - -type isWebClientFrame_Payload interface { - isWebClientFrame_Payload() -} - -type WebClientFrame_Hello struct { - Hello *ClientHello `protobuf:"bytes,2,opt,name=hello,proto3,oneof"` -} - -type WebClientFrame_AgentRequest struct { - // 直通请求:网关校验白名单与限额后转发桌面端(request_id 按连接命名空间化防冲突)。 - AgentRequest *GatewayEnvelope `protobuf:"bytes,3,opt,name=agent_request,json=agentRequest,proto3,oneof"` -} - -type WebClientFrame_StatusGet struct { - StatusGet *StatusGetRequest `protobuf:"bytes,4,opt,name=status_get,json=statusGet,proto3,oneof"` -} - -type WebClientFrame_ChatCommand struct { - ChatCommand *ChatCommandRequest `protobuf:"bytes,5,opt,name=chat_command,json=chatCommand,proto3,oneof"` -} - -type WebClientFrame_ChatPrepare struct { - ChatPrepare *ChatPrepareRequest `protobuf:"bytes,6,opt,name=chat_prepare,json=chatPrepare,proto3,oneof"` -} - -type WebClientFrame_ChatSubscribe struct { - ChatSubscribe *ChatSubscribeRequest `protobuf:"bytes,7,opt,name=chat_subscribe,json=chatSubscribe,proto3,oneof"` -} - -type WebClientFrame_ChatUnsubscribe struct { - ChatUnsubscribe *ChatUnsubscribeRequest `protobuf:"bytes,8,opt,name=chat_unsubscribe,json=chatUnsubscribe,proto3,oneof"` -} - -type WebClientFrame_ChatActivities struct { - ChatActivities *ChatActivitiesRequest `protobuf:"bytes,9,opt,name=chat_activities,json=chatActivities,proto3,oneof"` -} - -type WebClientFrame_WorkspaceSubscribe struct { - WorkspaceSubscribe *WorkspaceSubscribeRequest `protobuf:"bytes,10,opt,name=workspace_subscribe,json=workspaceSubscribe,proto3,oneof"` -} - -type WebClientFrame_WorkspaceUnsubscribe struct { - WorkspaceUnsubscribe *WorkspaceUnsubscribeRequest `protobuf:"bytes,11,opt,name=workspace_unsubscribe,json=workspaceUnsubscribe,proto3,oneof"` -} - -type WebClientFrame_Pong struct { - Pong *PongFrame `protobuf:"bytes,12,opt,name=pong,proto3,oneof"` -} - -type WebClientFrame_AgentList struct { - // Agent 目录查询:全部已登记 Agent(含离线)的状态列表。 - AgentList *AgentListRequest `protobuf:"bytes,14,opt,name=agent_list,json=agentList,proto3,oneof"` -} - -func (*WebClientFrame_Hello) isWebClientFrame_Payload() {} - -func (*WebClientFrame_AgentRequest) isWebClientFrame_Payload() {} - -func (*WebClientFrame_StatusGet) isWebClientFrame_Payload() {} - -func (*WebClientFrame_ChatCommand) isWebClientFrame_Payload() {} - -func (*WebClientFrame_ChatPrepare) isWebClientFrame_Payload() {} - -func (*WebClientFrame_ChatSubscribe) isWebClientFrame_Payload() {} - -func (*WebClientFrame_ChatUnsubscribe) isWebClientFrame_Payload() {} - -func (*WebClientFrame_ChatActivities) isWebClientFrame_Payload() {} - -func (*WebClientFrame_WorkspaceSubscribe) isWebClientFrame_Payload() {} - -func (*WebClientFrame_WorkspaceUnsubscribe) isWebClientFrame_Payload() {} - -func (*WebClientFrame_Pong) isWebClientFrame_Payload() {} - -func (*WebClientFrame_AgentList) isWebClientFrame_Payload() {} - -// WebServerFrame 为网关 → 浏览器方向的帧。 -type WebServerFrame struct { - state protoimpl.MessageState `protogen:"open.v1"` - // 关联响应回填请求 id;服务端主动推送(广播/心跳)时为空。 - RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` - // 来源/目标 Agent id:目标型响应回填请求声明的目标,Agent 事件广播标注来源; - // hello / ping / ack / agent_list 与全局聚合帧为空。客户端按此字段过滤 - // 非活跃 Agent 的事件。 - AgentId string `protobuf:"bytes,16,opt,name=agent_id,json=agentId,proto3" json:"agent_id,omitempty"` - // Types that are valid to be assigned to Payload: - // - // *WebServerFrame_Hello - // *WebServerFrame_AgentResponse - // *WebServerFrame_LocalError - // *WebServerFrame_Ping - // *WebServerFrame_Status - // *WebServerFrame_ChatSubscribed - // *WebServerFrame_ChatAccepted - // *WebServerFrame_ChatActivities - // *WebServerFrame_ChatEvent - // *WebServerFrame_ChatCommandUpdate - // *WebServerFrame_ChatSubscriptionReset - // *WebServerFrame_ChatActivity - // *WebServerFrame_Ack - // *WebServerFrame_ChatCancelled - // *WebServerFrame_AgentList - // *WebServerFrame_HistoryEvent - // *WebServerFrame_SettingsEvent - // *WebServerFrame_TerminalEvent - // *WebServerFrame_SftpEvent - // *WebServerFrame_ChatQueueEvent - // *WebServerFrame_TunnelState - // *WebServerFrame_ProcessState - // *WebServerFrame_WorkspaceActivity - Payload isWebServerFrame_Payload `protobuf_oneof:"payload"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *WebServerFrame) Reset() { - *x = WebServerFrame{} - mi := &file_proto_v2_gateway_ws_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *WebServerFrame) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*WebServerFrame) ProtoMessage() {} - -func (x *WebServerFrame) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_ws_proto_msgTypes[6] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use WebServerFrame.ProtoReflect.Descriptor instead. -func (*WebServerFrame) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_ws_proto_rawDescGZIP(), []int{6} -} - -func (x *WebServerFrame) GetRequestId() string { - if x != nil { - return x.RequestId - } - return "" -} - -func (x *WebServerFrame) GetAgentId() string { - if x != nil { - return x.AgentId - } - return "" -} - -func (x *WebServerFrame) GetPayload() isWebServerFrame_Payload { - if x != nil { - return x.Payload - } - return nil -} - -func (x *WebServerFrame) GetHello() *ServerHello { - if x != nil { - if x, ok := x.Payload.(*WebServerFrame_Hello); ok { - return x.Hello - } - } - return nil -} - -func (x *WebServerFrame) GetAgentResponse() *AgentEnvelope { - if x != nil { - if x, ok := x.Payload.(*WebServerFrame_AgentResponse); ok { - return x.AgentResponse - } - } - return nil -} - -func (x *WebServerFrame) GetLocalError() *ErrorResponse { - if x != nil { - if x, ok := x.Payload.(*WebServerFrame_LocalError); ok { - return x.LocalError - } - } - return nil -} - -func (x *WebServerFrame) GetPing() *PingFrame { - if x != nil { - if x, ok := x.Payload.(*WebServerFrame_Ping); ok { - return x.Ping - } - } - return nil -} - -func (x *WebServerFrame) GetStatus() *StatusEvent { - if x != nil { - if x, ok := x.Payload.(*WebServerFrame_Status); ok { - return x.Status - } - } - return nil -} - -func (x *WebServerFrame) GetChatSubscribed() *ChatSubscribeResult { - if x != nil { - if x, ok := x.Payload.(*WebServerFrame_ChatSubscribed); ok { - return x.ChatSubscribed - } - } - return nil -} - -func (x *WebServerFrame) GetChatAccepted() *ChatCommandAccepted { - if x != nil { - if x, ok := x.Payload.(*WebServerFrame_ChatAccepted); ok { - return x.ChatAccepted - } - } - return nil -} - -func (x *WebServerFrame) GetChatActivities() *ChatActivitiesResult { - if x != nil { - if x, ok := x.Payload.(*WebServerFrame_ChatActivities); ok { - return x.ChatActivities - } - } - return nil -} - -func (x *WebServerFrame) GetChatEvent() *ChatStreamEvent { - if x != nil { - if x, ok := x.Payload.(*WebServerFrame_ChatEvent); ok { - return x.ChatEvent - } - } - return nil -} - -func (x *WebServerFrame) GetChatCommandUpdate() *ChatCommandUpdate { - if x != nil { - if x, ok := x.Payload.(*WebServerFrame_ChatCommandUpdate); ok { - return x.ChatCommandUpdate - } - } - return nil -} - -func (x *WebServerFrame) GetChatSubscriptionReset() *ChatSubscriptionReset { - if x != nil { - if x, ok := x.Payload.(*WebServerFrame_ChatSubscriptionReset); ok { - return x.ChatSubscriptionReset - } - } - return nil -} - -func (x *WebServerFrame) GetChatActivity() *ChatActivityEvent { - if x != nil { - if x, ok := x.Payload.(*WebServerFrame_ChatActivity); ok { - return x.ChatActivity - } - } - return nil -} - -func (x *WebServerFrame) GetAck() *AckResult { - if x != nil { - if x, ok := x.Payload.(*WebServerFrame_Ack); ok { - return x.Ack - } - } - return nil -} - -func (x *WebServerFrame) GetChatCancelled() *ChatCancelResult { - if x != nil { - if x, ok := x.Payload.(*WebServerFrame_ChatCancelled); ok { - return x.ChatCancelled - } - } - return nil -} - -func (x *WebServerFrame) GetAgentList() *AgentListResult { - if x != nil { - if x, ok := x.Payload.(*WebServerFrame_AgentList); ok { - return x.AgentList - } - } - return nil -} - -func (x *WebServerFrame) GetHistoryEvent() *HistorySyncEvent { - if x != nil { - if x, ok := x.Payload.(*WebServerFrame_HistoryEvent); ok { - return x.HistoryEvent - } - } - return nil -} - -func (x *WebServerFrame) GetSettingsEvent() *SettingsSyncEvent { - if x != nil { - if x, ok := x.Payload.(*WebServerFrame_SettingsEvent); ok { - return x.SettingsEvent - } - } - return nil -} - -func (x *WebServerFrame) GetTerminalEvent() *TerminalEvent { - if x != nil { - if x, ok := x.Payload.(*WebServerFrame_TerminalEvent); ok { - return x.TerminalEvent - } - } - return nil -} - -func (x *WebServerFrame) GetSftpEvent() *SftpEvent { - if x != nil { - if x, ok := x.Payload.(*WebServerFrame_SftpEvent); ok { - return x.SftpEvent - } - } - return nil -} - -func (x *WebServerFrame) GetChatQueueEvent() *ChatQueueEvent { - if x != nil { - if x, ok := x.Payload.(*WebServerFrame_ChatQueueEvent); ok { - return x.ChatQueueEvent - } - } - return nil -} - -func (x *WebServerFrame) GetTunnelState() *TunnelStateSnapshot { - if x != nil { - if x, ok := x.Payload.(*WebServerFrame_TunnelState); ok { - return x.TunnelState - } - } - return nil -} - -func (x *WebServerFrame) GetProcessState() *ManagedProcessSnapshot { - if x != nil { - if x, ok := x.Payload.(*WebServerFrame_ProcessState); ok { - return x.ProcessState - } - } - return nil -} - -func (x *WebServerFrame) GetWorkspaceActivity() *WorkspaceActivityEvent { - if x != nil { - if x, ok := x.Payload.(*WebServerFrame_WorkspaceActivity); ok { - return x.WorkspaceActivity - } - } - return nil -} - -type isWebServerFrame_Payload interface { - isWebServerFrame_Payload() -} - -type WebServerFrame_Hello struct { - Hello *ServerHello `protobuf:"bytes,2,opt,name=hello,proto3,oneof"` -} - -type WebServerFrame_AgentResponse struct { - // 直通响应:桌面端返回的业务信封(含 error=99 错误臂)。 - AgentResponse *AgentEnvelope `protobuf:"bytes,3,opt,name=agent_response,json=agentResponse,proto3,oneof"` -} - -type WebServerFrame_LocalError struct { - // 网关本地错误(鉴权、校验、离线等)。 - LocalError *ErrorResponse `protobuf:"bytes,4,opt,name=local_error,json=localError,proto3,oneof"` -} - -type WebServerFrame_Ping struct { - Ping *PingFrame `protobuf:"bytes,5,opt,name=ping,proto3,oneof"` -} - -type WebServerFrame_Status struct { - // status_get / chat_prepare 的响应与 status 广播共用一个状态归一化器。 - Status *StatusEvent `protobuf:"bytes,6,opt,name=status,proto3,oneof"` -} - -type WebServerFrame_ChatSubscribed struct { - ChatSubscribed *ChatSubscribeResult `protobuf:"bytes,7,opt,name=chat_subscribed,json=chatSubscribed,proto3,oneof"` -} - -type WebServerFrame_ChatAccepted struct { - ChatAccepted *ChatCommandAccepted `protobuf:"bytes,8,opt,name=chat_accepted,json=chatAccepted,proto3,oneof"` -} - -type WebServerFrame_ChatActivities struct { - ChatActivities *ChatActivitiesResult `protobuf:"bytes,9,opt,name=chat_activities,json=chatActivities,proto3,oneof"` -} - -type WebServerFrame_ChatEvent struct { - ChatEvent *ChatStreamEvent `protobuf:"bytes,10,opt,name=chat_event,json=chatEvent,proto3,oneof"` -} - -type WebServerFrame_ChatCommandUpdate struct { - ChatCommandUpdate *ChatCommandUpdate `protobuf:"bytes,11,opt,name=chat_command_update,json=chatCommandUpdate,proto3,oneof"` -} - -type WebServerFrame_ChatSubscriptionReset struct { - ChatSubscriptionReset *ChatSubscriptionReset `protobuf:"bytes,12,opt,name=chat_subscription_reset,json=chatSubscriptionReset,proto3,oneof"` -} - -type WebServerFrame_ChatActivity struct { - ChatActivity *ChatActivityEvent `protobuf:"bytes,13,opt,name=chat_activity,json=chatActivity,proto3,oneof"` -} - -type WebServerFrame_Ack struct { - Ack *AckResult `protobuf:"bytes,14,opt,name=ack,proto3,oneof"` -} - -type WebServerFrame_ChatCancelled struct { - ChatCancelled *ChatCancelResult `protobuf:"bytes,15,opt,name=chat_cancelled,json=chatCancelled,proto3,oneof"` -} - -type WebServerFrame_AgentList struct { - AgentList *AgentListResult `protobuf:"bytes,17,opt,name=agent_list,json=agentList,proto3,oneof"` -} - -type WebServerFrame_HistoryEvent struct { - // 广播事件:session 层吐出的业务消息零塑形直转。 - HistoryEvent *HistorySyncEvent `protobuf:"bytes,20,opt,name=history_event,json=historyEvent,proto3,oneof"` -} - -type WebServerFrame_SettingsEvent struct { - SettingsEvent *SettingsSyncEvent `protobuf:"bytes,21,opt,name=settings_event,json=settingsEvent,proto3,oneof"` -} - -type WebServerFrame_TerminalEvent struct { - TerminalEvent *TerminalEvent `protobuf:"bytes,22,opt,name=terminal_event,json=terminalEvent,proto3,oneof"` -} - -type WebServerFrame_SftpEvent struct { - SftpEvent *SftpEvent `protobuf:"bytes,23,opt,name=sftp_event,json=sftpEvent,proto3,oneof"` -} - -type WebServerFrame_ChatQueueEvent struct { - ChatQueueEvent *ChatQueueEvent `protobuf:"bytes,24,opt,name=chat_queue_event,json=chatQueueEvent,proto3,oneof"` -} - -type WebServerFrame_TunnelState struct { - TunnelState *TunnelStateSnapshot `protobuf:"bytes,25,opt,name=tunnel_state,json=tunnelState,proto3,oneof"` -} - -type WebServerFrame_ProcessState struct { - ProcessState *ManagedProcessSnapshot `protobuf:"bytes,26,opt,name=process_state,json=processState,proto3,oneof"` -} - -type WebServerFrame_WorkspaceActivity struct { - WorkspaceActivity *WorkspaceActivityEvent `protobuf:"bytes,27,opt,name=workspace_activity,json=workspaceActivity,proto3,oneof"` -} - -func (*WebServerFrame_Hello) isWebServerFrame_Payload() {} - -func (*WebServerFrame_AgentResponse) isWebServerFrame_Payload() {} - -func (*WebServerFrame_LocalError) isWebServerFrame_Payload() {} - -func (*WebServerFrame_Ping) isWebServerFrame_Payload() {} - -func (*WebServerFrame_Status) isWebServerFrame_Payload() {} - -func (*WebServerFrame_ChatSubscribed) isWebServerFrame_Payload() {} - -func (*WebServerFrame_ChatAccepted) isWebServerFrame_Payload() {} - -func (*WebServerFrame_ChatActivities) isWebServerFrame_Payload() {} - -func (*WebServerFrame_ChatEvent) isWebServerFrame_Payload() {} - -func (*WebServerFrame_ChatCommandUpdate) isWebServerFrame_Payload() {} - -func (*WebServerFrame_ChatSubscriptionReset) isWebServerFrame_Payload() {} - -func (*WebServerFrame_ChatActivity) isWebServerFrame_Payload() {} - -func (*WebServerFrame_Ack) isWebServerFrame_Payload() {} - -func (*WebServerFrame_ChatCancelled) isWebServerFrame_Payload() {} - -func (*WebServerFrame_AgentList) isWebServerFrame_Payload() {} - -func (*WebServerFrame_HistoryEvent) isWebServerFrame_Payload() {} - -func (*WebServerFrame_SettingsEvent) isWebServerFrame_Payload() {} - -func (*WebServerFrame_TerminalEvent) isWebServerFrame_Payload() {} - -func (*WebServerFrame_SftpEvent) isWebServerFrame_Payload() {} - -func (*WebServerFrame_ChatQueueEvent) isWebServerFrame_Payload() {} - -func (*WebServerFrame_TunnelState) isWebServerFrame_Payload() {} - -func (*WebServerFrame_ProcessState) isWebServerFrame_Payload() {} - -func (*WebServerFrame_WorkspaceActivity) isWebServerFrame_Payload() {} - -// AgentListRequest 查询 Agent 目录;响应为 AgentListResult。 -type AgentListRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *AgentListRequest) Reset() { - *x = AgentListRequest{} - mi := &file_proto_v2_gateway_ws_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AgentListRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AgentListRequest) ProtoMessage() {} - -func (x *AgentListRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_ws_proto_msgTypes[7] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use AgentListRequest.ProtoReflect.Descriptor instead. -func (*AgentListRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_ws_proto_rawDescGZIP(), []int{7} -} - -// AgentListResult 返回全部已登记 Agent 的状态(含离线项,供目录渲染), -// 复用 StatusEvent 归一化器,按 agent_id 排序。 -type AgentListResult struct { - state protoimpl.MessageState `protogen:"open.v1"` - Agents []*StatusEvent `protobuf:"bytes,1,rep,name=agents,proto3" json:"agents,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *AgentListResult) Reset() { - *x = AgentListResult{} - mi := &file_proto_v2_gateway_ws_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AgentListResult) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AgentListResult) ProtoMessage() {} - -func (x *AgentListResult) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_ws_proto_msgTypes[8] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use AgentListResult.ProtoReflect.Descriptor instead. -func (*AgentListResult) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_ws_proto_rawDescGZIP(), []int{8} -} - -func (x *AgentListResult) GetAgents() []*StatusEvent { - if x != nil { - return x.Agents - } - return nil -} - -// AgentClientFrame 为桌面端 → 网关方向的帧。 -type AgentClientFrame struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Types that are valid to be assigned to Payload: - // - // *AgentClientFrame_Hello - // *AgentClientFrame_Envelope - Payload isAgentClientFrame_Payload `protobuf_oneof:"payload"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *AgentClientFrame) Reset() { - *x = AgentClientFrame{} - mi := &file_proto_v2_gateway_ws_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AgentClientFrame) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AgentClientFrame) ProtoMessage() {} - -func (x *AgentClientFrame) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_ws_proto_msgTypes[9] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use AgentClientFrame.ProtoReflect.Descriptor instead. -func (*AgentClientFrame) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_ws_proto_rawDescGZIP(), []int{9} -} - -func (x *AgentClientFrame) GetPayload() isAgentClientFrame_Payload { - if x != nil { - return x.Payload - } - return nil -} - -func (x *AgentClientFrame) GetHello() *ClientHello { - if x != nil { - if x, ok := x.Payload.(*AgentClientFrame_Hello); ok { - return x.Hello - } - } - return nil -} - -func (x *AgentClientFrame) GetEnvelope() *AgentEnvelope { - if x != nil { - if x, ok := x.Payload.(*AgentClientFrame_Envelope); ok { - return x.Envelope - } - } - return nil -} - -type isAgentClientFrame_Payload interface { - isAgentClientFrame_Payload() -} - -type AgentClientFrame_Hello struct { - Hello *ClientHello `protobuf:"bytes,1,opt,name=hello,proto3,oneof"` -} - -type AgentClientFrame_Envelope struct { - Envelope *AgentEnvelope `protobuf:"bytes,2,opt,name=envelope,proto3,oneof"` -} - -func (*AgentClientFrame_Hello) isAgentClientFrame_Payload() {} - -func (*AgentClientFrame_Envelope) isAgentClientFrame_Payload() {} - -// AgentServerFrame 为网关 → 桌面端方向的帧。 -type AgentServerFrame struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Types that are valid to be assigned to Payload: - // - // *AgentServerFrame_Hello - // *AgentServerFrame_Envelope - Payload isAgentServerFrame_Payload `protobuf_oneof:"payload"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *AgentServerFrame) Reset() { - *x = AgentServerFrame{} - mi := &file_proto_v2_gateway_ws_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AgentServerFrame) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AgentServerFrame) ProtoMessage() {} - -func (x *AgentServerFrame) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_ws_proto_msgTypes[10] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use AgentServerFrame.ProtoReflect.Descriptor instead. -func (*AgentServerFrame) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_ws_proto_rawDescGZIP(), []int{10} -} - -func (x *AgentServerFrame) GetPayload() isAgentServerFrame_Payload { - if x != nil { - return x.Payload - } - return nil -} - -func (x *AgentServerFrame) GetHello() *ServerHello { - if x != nil { - if x, ok := x.Payload.(*AgentServerFrame_Hello); ok { - return x.Hello - } - } - return nil -} - -func (x *AgentServerFrame) GetEnvelope() *GatewayEnvelope { - if x != nil { - if x, ok := x.Payload.(*AgentServerFrame_Envelope); ok { - return x.Envelope - } - } - return nil -} - -type isAgentServerFrame_Payload interface { - isAgentServerFrame_Payload() -} - -type AgentServerFrame_Hello struct { - Hello *ServerHello `protobuf:"bytes,1,opt,name=hello,proto3,oneof"` -} - -type AgentServerFrame_Envelope struct { - Envelope *GatewayEnvelope `protobuf:"bytes,2,opt,name=envelope,proto3,oneof"` -} - -func (*AgentServerFrame_Hello) isAgentServerFrame_Payload() {} - -func (*AgentServerFrame_Envelope) isAgentServerFrame_Payload() {} - -// TerminalClientFrame 为客户端(浏览器或桌面端)→ 网关方向的帧。 -type TerminalClientFrame struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Types that are valid to be assigned to Payload: - // - // *TerminalClientFrame_Hello - // *TerminalClientFrame_Frame - Payload isTerminalClientFrame_Payload `protobuf_oneof:"payload"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TerminalClientFrame) Reset() { - *x = TerminalClientFrame{} - mi := &file_proto_v2_gateway_ws_proto_msgTypes[11] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TerminalClientFrame) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TerminalClientFrame) ProtoMessage() {} - -func (x *TerminalClientFrame) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_ws_proto_msgTypes[11] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TerminalClientFrame.ProtoReflect.Descriptor instead. -func (*TerminalClientFrame) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_ws_proto_rawDescGZIP(), []int{11} -} - -func (x *TerminalClientFrame) GetPayload() isTerminalClientFrame_Payload { - if x != nil { - return x.Payload - } - return nil -} - -func (x *TerminalClientFrame) GetHello() *ClientHello { - if x != nil { - if x, ok := x.Payload.(*TerminalClientFrame_Hello); ok { - return x.Hello - } - } - return nil -} - -func (x *TerminalClientFrame) GetFrame() *TerminalStreamFrame { - if x != nil { - if x, ok := x.Payload.(*TerminalClientFrame_Frame); ok { - return x.Frame - } - } - return nil -} - -type isTerminalClientFrame_Payload interface { - isTerminalClientFrame_Payload() -} - -type TerminalClientFrame_Hello struct { - Hello *ClientHello `protobuf:"bytes,1,opt,name=hello,proto3,oneof"` -} - -type TerminalClientFrame_Frame struct { - Frame *TerminalStreamFrame `protobuf:"bytes,2,opt,name=frame,proto3,oneof"` -} - -func (*TerminalClientFrame_Hello) isTerminalClientFrame_Payload() {} - -func (*TerminalClientFrame_Frame) isTerminalClientFrame_Payload() {} - -// TerminalServerFrame 为网关 → 客户端方向的帧。 -type TerminalServerFrame struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Types that are valid to be assigned to Payload: - // - // *TerminalServerFrame_Hello - // *TerminalServerFrame_Frame - Payload isTerminalServerFrame_Payload `protobuf_oneof:"payload"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TerminalServerFrame) Reset() { - *x = TerminalServerFrame{} - mi := &file_proto_v2_gateway_ws_proto_msgTypes[12] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TerminalServerFrame) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TerminalServerFrame) ProtoMessage() {} - -func (x *TerminalServerFrame) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_ws_proto_msgTypes[12] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TerminalServerFrame.ProtoReflect.Descriptor instead. -func (*TerminalServerFrame) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_ws_proto_rawDescGZIP(), []int{12} -} - -func (x *TerminalServerFrame) GetPayload() isTerminalServerFrame_Payload { - if x != nil { - return x.Payload - } - return nil -} - -func (x *TerminalServerFrame) GetHello() *ServerHello { - if x != nil { - if x, ok := x.Payload.(*TerminalServerFrame_Hello); ok { - return x.Hello - } - } - return nil -} - -func (x *TerminalServerFrame) GetFrame() *TerminalStreamFrame { - if x != nil { - if x, ok := x.Payload.(*TerminalServerFrame_Frame); ok { - return x.Frame - } - } - return nil -} - -type isTerminalServerFrame_Payload interface { - isTerminalServerFrame_Payload() -} - -type TerminalServerFrame_Hello struct { - Hello *ServerHello `protobuf:"bytes,1,opt,name=hello,proto3,oneof"` -} - -type TerminalServerFrame_Frame struct { - Frame *TerminalStreamFrame `protobuf:"bytes,2,opt,name=frame,proto3,oneof"` -} - -func (*TerminalServerFrame_Hello) isTerminalServerFrame_Payload() {} - -func (*TerminalServerFrame_Frame) isTerminalServerFrame_Payload() {} - -// StatusGetRequest 请求网关侧运行状态快照(操作类型:"status.get")。 -type StatusGetRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *StatusGetRequest) Reset() { - *x = StatusGetRequest{} - mi := &file_proto_v2_gateway_ws_proto_msgTypes[13] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *StatusGetRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*StatusGetRequest) ProtoMessage() {} - -func (x *StatusGetRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_ws_proto_msgTypes[13] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use StatusGetRequest.ProtoReflect.Descriptor instead. -func (*StatusGetRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_ws_proto_rawDescGZIP(), []int{13} -} - -// StatusEvent 镜像 session.Status 的 JSON 形状(字段一一对应)。 -type StatusEvent struct { - state protoimpl.MessageState `protogen:"open.v1"` - Online bool `protobuf:"varint,1,opt,name=online,proto3" json:"online,omitempty"` - AgentReady bool `protobuf:"varint,2,opt,name=agent_ready,json=agentReady,proto3" json:"agent_ready,omitempty"` - ChatRuntimeReady bool `protobuf:"varint,3,opt,name=chat_runtime_ready,json=chatRuntimeReady,proto3" json:"chat_runtime_ready,omitempty"` - AgentId string `protobuf:"bytes,4,opt,name=agent_id,json=agentId,proto3" json:"agent_id,omitempty"` - AgentVersion string `protobuf:"bytes,5,opt,name=agent_version,json=agentVersion,proto3" json:"agent_version,omitempty"` - SessionId string `protobuf:"bytes,6,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` - ConnectedSince int64 `protobuf:"varint,7,opt,name=connected_since,json=connectedSince,proto3" json:"connected_since,omitempty"` - LastHeartbeat int64 `protobuf:"varint,8,opt,name=last_heartbeat,json=lastHeartbeat,proto3" json:"last_heartbeat,omitempty"` - RuntimeState string `protobuf:"bytes,9,opt,name=runtime_state,json=runtimeState,proto3" json:"runtime_state,omitempty"` - RuntimeLastHeartbeat int64 `protobuf:"varint,10,opt,name=runtime_last_heartbeat,json=runtimeLastHeartbeat,proto3" json:"runtime_last_heartbeat,omitempty"` - RuntimeWorkerId string `protobuf:"bytes,11,opt,name=runtime_worker_id,json=runtimeWorkerId,proto3" json:"runtime_worker_id,omitempty"` - RuntimeVisible bool `protobuf:"varint,12,opt,name=runtime_visible,json=runtimeVisible,proto3" json:"runtime_visible,omitempty"` - RuntimeActiveRunCount uint32 `protobuf:"varint,13,opt,name=runtime_active_run_count,json=runtimeActiveRunCount,proto3" json:"runtime_active_run_count,omitempty"` - // 仅 agent_list 目录响应填充;实时状态响应与广播保持为空,避免状态热路径访问数据库。 - Name string `protobuf:"bytes,14,opt,name=name,proto3" json:"name,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *StatusEvent) Reset() { - *x = StatusEvent{} - mi := &file_proto_v2_gateway_ws_proto_msgTypes[14] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *StatusEvent) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*StatusEvent) ProtoMessage() {} - -func (x *StatusEvent) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_ws_proto_msgTypes[14] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use StatusEvent.ProtoReflect.Descriptor instead. -func (*StatusEvent) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_ws_proto_rawDescGZIP(), []int{14} -} - -func (x *StatusEvent) GetOnline() bool { - if x != nil { - return x.Online - } - return false -} - -func (x *StatusEvent) GetAgentReady() bool { - if x != nil { - return x.AgentReady - } - return false -} - -func (x *StatusEvent) GetChatRuntimeReady() bool { - if x != nil { - return x.ChatRuntimeReady - } - return false -} - -func (x *StatusEvent) GetAgentId() string { - if x != nil { - return x.AgentId - } - return "" -} - -func (x *StatusEvent) GetAgentVersion() string { - if x != nil { - return x.AgentVersion - } - return "" -} - -func (x *StatusEvent) GetSessionId() string { - if x != nil { - return x.SessionId - } - return "" -} - -func (x *StatusEvent) GetConnectedSince() int64 { - if x != nil { - return x.ConnectedSince - } - return 0 -} - -func (x *StatusEvent) GetLastHeartbeat() int64 { - if x != nil { - return x.LastHeartbeat - } - return 0 -} - -func (x *StatusEvent) GetRuntimeState() string { - if x != nil { - return x.RuntimeState - } - return "" -} - -func (x *StatusEvent) GetRuntimeLastHeartbeat() int64 { - if x != nil { - return x.RuntimeLastHeartbeat - } - return 0 -} - -func (x *StatusEvent) GetRuntimeWorkerId() string { - if x != nil { - return x.RuntimeWorkerId - } - return "" -} - -func (x *StatusEvent) GetRuntimeVisible() bool { - if x != nil { - return x.RuntimeVisible - } - return false -} - -func (x *StatusEvent) GetRuntimeActiveRunCount() uint32 { - if x != nil { - return x.RuntimeActiveRunCount - } - return 0 -} - -func (x *StatusEvent) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -// ChatPrepareRequest 唤醒/探活桌面端 chat 运行时(操作类型:"chat.prepare");响应为 StatusEvent。 -type ChatPrepareRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Reason string `protobuf:"bytes,1,opt,name=reason,proto3" json:"reason,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatPrepareRequest) Reset() { - *x = ChatPrepareRequest{} - mi := &file_proto_v2_gateway_ws_proto_msgTypes[15] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatPrepareRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatPrepareRequest) ProtoMessage() {} - -func (x *ChatPrepareRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_ws_proto_msgTypes[15] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatPrepareRequest.ProtoReflect.Descriptor instead. -func (*ChatPrepareRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_ws_proto_rawDescGZIP(), []int{15} -} - -func (x *ChatPrepareRequest) GetReason() string { - if x != nil { - return x.Reason - } - return "" -} - -// ChatSubscribeRequest 订阅会话事件流(操作类型:"chat.subscribe");外层 WebClientFrame.agent_id 必须非空, -// 会话按 (agent_id, conversation_id) 隔离。after_seq + stream_epoch -// 支持断线重放:epoch 不匹配或序号超界时服务端置 reset 并从头回放。 -type ChatSubscribeRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - AfterSeq int64 `protobuf:"varint,2,opt,name=after_seq,json=afterSeq,proto3" json:"after_seq,omitempty"` - StreamEpoch string `protobuf:"bytes,3,opt,name=stream_epoch,json=streamEpoch,proto3" json:"stream_epoch,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatSubscribeRequest) Reset() { - *x = ChatSubscribeRequest{} - mi := &file_proto_v2_gateway_ws_proto_msgTypes[16] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatSubscribeRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatSubscribeRequest) ProtoMessage() {} - -func (x *ChatSubscribeRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_ws_proto_msgTypes[16] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatSubscribeRequest.ProtoReflect.Descriptor instead. -func (*ChatSubscribeRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_ws_proto_rawDescGZIP(), []int{16} -} - -func (x *ChatSubscribeRequest) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -func (x *ChatSubscribeRequest) GetAfterSeq() int64 { - if x != nil { - return x.AfterSeq - } - return 0 -} - -func (x *ChatSubscribeRequest) GetStreamEpoch() string { - if x != nil { - return x.StreamEpoch - } - return "" -} - -// ChatRunActivity 镜像 session.RunActivity 的 JSON 形状。 -type ChatRunActivity struct { - state protoimpl.MessageState `protogen:"open.v1"` - RunId string `protobuf:"bytes,1,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` - State string `protobuf:"bytes,2,opt,name=state,proto3" json:"state,omitempty"` - StartedSeq int64 `protobuf:"varint,3,opt,name=started_seq,json=startedSeq,proto3" json:"started_seq,omitempty"` - // Unix 毫秒时间戳。 - UpdatedAtMs int64 `protobuf:"varint,4,opt,name=updated_at_ms,json=updatedAtMs,proto3" json:"updated_at_ms,omitempty"` - ToolStatus string `protobuf:"bytes,5,opt,name=tool_status,json=toolStatus,proto3" json:"tool_status,omitempty"` - ToolStatusIsCompaction bool `protobuf:"varint,6,opt,name=tool_status_is_compaction,json=toolStatusIsCompaction,proto3" json:"tool_status_is_compaction,omitempty"` - ClientRequestId string `protobuf:"bytes,7,opt,name=client_request_id,json=clientRequestId,proto3" json:"client_request_id,omitempty"` - // 以下字段仅在 chat_activities 列表中填充(agent_id 标注运行所在 Agent)。 - ConversationId string `protobuf:"bytes,8,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - Workdir string `protobuf:"bytes,9,opt,name=workdir,proto3" json:"workdir,omitempty"` - AgentId string `protobuf:"bytes,10,opt,name=agent_id,json=agentId,proto3" json:"agent_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatRunActivity) Reset() { - *x = ChatRunActivity{} - mi := &file_proto_v2_gateway_ws_proto_msgTypes[17] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatRunActivity) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatRunActivity) ProtoMessage() {} - -func (x *ChatRunActivity) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_ws_proto_msgTypes[17] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatRunActivity.ProtoReflect.Descriptor instead. -func (*ChatRunActivity) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_ws_proto_rawDescGZIP(), []int{17} -} - -func (x *ChatRunActivity) GetRunId() string { - if x != nil { - return x.RunId - } - return "" -} - -func (x *ChatRunActivity) GetState() string { - if x != nil { - return x.State - } - return "" -} - -func (x *ChatRunActivity) GetStartedSeq() int64 { - if x != nil { - return x.StartedSeq - } - return 0 -} - -func (x *ChatRunActivity) GetUpdatedAtMs() int64 { - if x != nil { - return x.UpdatedAtMs - } - return 0 -} - -func (x *ChatRunActivity) GetToolStatus() string { - if x != nil { - return x.ToolStatus - } - return "" -} - -func (x *ChatRunActivity) GetToolStatusIsCompaction() bool { - if x != nil { - return x.ToolStatusIsCompaction - } - return false -} - -func (x *ChatRunActivity) GetClientRequestId() string { - if x != nil { - return x.ClientRequestId - } - return "" -} - -func (x *ChatRunActivity) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -func (x *ChatRunActivity) GetWorkdir() string { - if x != nil { - return x.Workdir - } - return "" -} - -func (x *ChatRunActivity) GetAgentId() string { - if x != nil { - return x.AgentId - } - return "" -} - -// ChatRunSnapshot 镜像 session.RunSnapshot 的 JSON 形状。 -type ChatRunSnapshot struct { - state protoimpl.MessageState `protogen:"open.v1"` - RunId string `protobuf:"bytes,1,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` - Revision int64 `protobuf:"varint,2,opt,name=revision,proto3" json:"revision,omitempty"` - // 桌面端渲染快照,内容为动态 JSON(按字符串携带,不建模)。 - EntriesJson string `protobuf:"bytes,3,opt,name=entries_json,json=entriesJson,proto3" json:"entries_json,omitempty"` - ToolStatus string `protobuf:"bytes,4,opt,name=tool_status,json=toolStatus,proto3" json:"tool_status,omitempty"` - ToolStatusIsCompaction bool `protobuf:"varint,5,opt,name=tool_status_is_compaction,json=toolStatusIsCompaction,proto3" json:"tool_status_is_compaction,omitempty"` - AsOfSeq int64 `protobuf:"varint,6,opt,name=as_of_seq,json=asOfSeq,proto3" json:"as_of_seq,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatRunSnapshot) Reset() { - *x = ChatRunSnapshot{} - mi := &file_proto_v2_gateway_ws_proto_msgTypes[18] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatRunSnapshot) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatRunSnapshot) ProtoMessage() {} - -func (x *ChatRunSnapshot) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_ws_proto_msgTypes[18] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatRunSnapshot.ProtoReflect.Descriptor instead. -func (*ChatRunSnapshot) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_ws_proto_rawDescGZIP(), []int{18} -} - -func (x *ChatRunSnapshot) GetRunId() string { - if x != nil { - return x.RunId - } - return "" -} - -func (x *ChatRunSnapshot) GetRevision() int64 { - if x != nil { - return x.Revision - } - return 0 -} - -func (x *ChatRunSnapshot) GetEntriesJson() string { - if x != nil { - return x.EntriesJson - } - return "" -} - -func (x *ChatRunSnapshot) GetToolStatus() string { - if x != nil { - return x.ToolStatus - } - return "" -} - -func (x *ChatRunSnapshot) GetToolStatusIsCompaction() bool { - if x != nil { - return x.ToolStatusIsCompaction - } - return false -} - -func (x *ChatRunSnapshot) GetAsOfSeq() int64 { - if x != nil { - return x.AsOfSeq - } - return 0 -} - -// ChatSubscribeResult 是 chat_subscribe 的响应。 -type ChatSubscribeResult struct { - state protoimpl.MessageState `protogen:"open.v1"` - ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - StreamEpoch string `protobuf:"bytes,2,opt,name=stream_epoch,json=streamEpoch,proto3" json:"stream_epoch,omitempty"` - LatestSeq int64 `protobuf:"varint,3,opt,name=latest_seq,json=latestSeq,proto3" json:"latest_seq,omitempty"` - Reset_ bool `protobuf:"varint,4,opt,name=reset,proto3" json:"reset,omitempty"` - Activity *ChatRunActivity `protobuf:"bytes,5,opt,name=activity,proto3" json:"activity,omitempty"` - Snapshot *ChatRunSnapshot `protobuf:"bytes,6,opt,name=snapshot,proto3" json:"snapshot,omitempty"` - // 回放的事件序列。载荷由 chatwire 塑形为深度动态 JSON,按字节携带、不额外 proto 化 - // (重建模会分叉 chatwire 且对压缩后的二进制帧无实际收益)。 - EventsJson [][]byte `protobuf:"bytes,7,rep,name=events_json,json=eventsJson,proto3" json:"events_json,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatSubscribeResult) Reset() { - *x = ChatSubscribeResult{} - mi := &file_proto_v2_gateway_ws_proto_msgTypes[19] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatSubscribeResult) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatSubscribeResult) ProtoMessage() {} - -func (x *ChatSubscribeResult) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_ws_proto_msgTypes[19] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatSubscribeResult.ProtoReflect.Descriptor instead. -func (*ChatSubscribeResult) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_ws_proto_rawDescGZIP(), []int{19} -} - -func (x *ChatSubscribeResult) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -func (x *ChatSubscribeResult) GetStreamEpoch() string { - if x != nil { - return x.StreamEpoch - } - return "" -} - -func (x *ChatSubscribeResult) GetLatestSeq() int64 { - if x != nil { - return x.LatestSeq - } - return 0 -} - -func (x *ChatSubscribeResult) GetReset_() bool { - if x != nil { - return x.Reset_ - } - return false -} - -func (x *ChatSubscribeResult) GetActivity() *ChatRunActivity { - if x != nil { - return x.Activity - } - return nil -} - -func (x *ChatSubscribeResult) GetSnapshot() *ChatRunSnapshot { - if x != nil { - return x.Snapshot - } - return nil -} - -func (x *ChatSubscribeResult) GetEventsJson() [][]byte { - if x != nil { - return x.EventsJson - } - return nil -} - -// ChatUnsubscribeRequest 取消订阅(操作类型:"chat.unsubscribe");响应 AckResult。 -type ChatUnsubscribeRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatUnsubscribeRequest) Reset() { - *x = ChatUnsubscribeRequest{} - mi := &file_proto_v2_gateway_ws_proto_msgTypes[20] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatUnsubscribeRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatUnsubscribeRequest) ProtoMessage() {} - -func (x *ChatUnsubscribeRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_ws_proto_msgTypes[20] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatUnsubscribeRequest.ProtoReflect.Descriptor instead. -func (*ChatUnsubscribeRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_ws_proto_rawDescGZIP(), []int{20} -} - -func (x *ChatUnsubscribeRequest) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -// ChatActivitiesRequest 查询运行中会话(操作类型:"chat.activities");仅由网关状态应答,桌面端离线时亦可用。 -type ChatActivitiesRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatActivitiesRequest) Reset() { - *x = ChatActivitiesRequest{} - mi := &file_proto_v2_gateway_ws_proto_msgTypes[21] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatActivitiesRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatActivitiesRequest) ProtoMessage() {} - -func (x *ChatActivitiesRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_ws_proto_msgTypes[21] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatActivitiesRequest.ProtoReflect.Descriptor instead. -func (*ChatActivitiesRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_ws_proto_rawDescGZIP(), []int{21} -} - -type ChatActivitiesResult struct { - state protoimpl.MessageState `protogen:"open.v1"` - RunningConversations []*ChatRunActivity `protobuf:"bytes,1,rep,name=running_conversations,json=runningConversations,proto3" json:"running_conversations,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatActivitiesResult) Reset() { - *x = ChatActivitiesResult{} - mi := &file_proto_v2_gateway_ws_proto_msgTypes[22] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatActivitiesResult) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatActivitiesResult) ProtoMessage() {} - -func (x *ChatActivitiesResult) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_ws_proto_msgTypes[22] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatActivitiesResult.ProtoReflect.Descriptor instead. -func (*ChatActivitiesResult) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_ws_proto_rawDescGZIP(), []int{22} -} - -func (x *ChatActivitiesResult) GetRunningConversations() []*ChatRunActivity { - if x != nil { - return x.RunningConversations - } - return nil -} - -// ChatStreamEvent 是订阅后推送的单条会话事件。 -type ChatStreamEvent struct { - state protoimpl.MessageState `protogen:"open.v1"` - ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - // 事件序号(与 payload_json 内的 seq 一致,便于不解析载荷即可去重)。 - Seq int64 `protobuf:"varint,2,opt,name=seq,proto3" json:"seq,omitempty"` - PayloadJson []byte `protobuf:"bytes,3,opt,name=payload_json,json=payloadJson,proto3" json:"payload_json,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatStreamEvent) Reset() { - *x = ChatStreamEvent{} - mi := &file_proto_v2_gateway_ws_proto_msgTypes[23] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatStreamEvent) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatStreamEvent) ProtoMessage() {} - -func (x *ChatStreamEvent) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_ws_proto_msgTypes[23] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatStreamEvent.ProtoReflect.Descriptor instead. -func (*ChatStreamEvent) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_ws_proto_rawDescGZIP(), []int{23} -} - -func (x *ChatStreamEvent) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -func (x *ChatStreamEvent) GetSeq() int64 { - if x != nil { - return x.Seq - } - return 0 -} - -func (x *ChatStreamEvent) GetPayloadJson() []byte { - if x != nil { - return x.PayloadJson - } - return nil -} - -// ChatCommandAccepted 是 chat_command 提交被接受的响应(chat_command 的接受应答)。 -type ChatCommandAccepted struct { - state protoimpl.MessageState `protogen:"open.v1"` - RunId string `protobuf:"bytes,1,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` - ConversationId string `protobuf:"bytes,2,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - AcceptedSeq int64 `protobuf:"varint,3,opt,name=accepted_seq,json=acceptedSeq,proto3" json:"accepted_seq,omitempty"` - Deduped bool `protobuf:"varint,4,opt,name=deduped,proto3" json:"deduped,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatCommandAccepted) Reset() { - *x = ChatCommandAccepted{} - mi := &file_proto_v2_gateway_ws_proto_msgTypes[24] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatCommandAccepted) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatCommandAccepted) ProtoMessage() {} - -func (x *ChatCommandAccepted) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_ws_proto_msgTypes[24] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatCommandAccepted.ProtoReflect.Descriptor instead. -func (*ChatCommandAccepted) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_ws_proto_rawDescGZIP(), []int{24} -} - -func (x *ChatCommandAccepted) GetRunId() string { - if x != nil { - return x.RunId - } - return "" -} - -func (x *ChatCommandAccepted) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -func (x *ChatCommandAccepted) GetAcceptedSeq() int64 { - if x != nil { - return x.AcceptedSeq - } - return 0 -} - -func (x *ChatCommandAccepted) GetDeduped() bool { - if x != nil { - return x.Deduped - } - return false -} - -// ChatCommandUpdate 推送命令的前置阶段结果(bound / queued_in_gui / failed),镜像 session.ChatCommandUpdate。 -type ChatCommandUpdate struct { - state protoimpl.MessageState `protogen:"open.v1"` - RunId string `protobuf:"bytes,1,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` - ClientRequestId string `protobuf:"bytes,2,opt,name=client_request_id,json=clientRequestId,proto3" json:"client_request_id,omitempty"` - ConversationId string `protobuf:"bytes,3,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - Phase string `protobuf:"bytes,4,opt,name=phase,proto3" json:"phase,omitempty"` - ErrorCode string `protobuf:"bytes,5,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"` - Message string `protobuf:"bytes,6,opt,name=message,proto3" json:"message,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatCommandUpdate) Reset() { - *x = ChatCommandUpdate{} - mi := &file_proto_v2_gateway_ws_proto_msgTypes[25] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatCommandUpdate) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatCommandUpdate) ProtoMessage() {} - -func (x *ChatCommandUpdate) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_ws_proto_msgTypes[25] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatCommandUpdate.ProtoReflect.Descriptor instead. -func (*ChatCommandUpdate) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_ws_proto_rawDescGZIP(), []int{25} -} - -func (x *ChatCommandUpdate) GetRunId() string { - if x != nil { - return x.RunId - } - return "" -} - -func (x *ChatCommandUpdate) GetClientRequestId() string { - if x != nil { - return x.ClientRequestId - } - return "" -} - -func (x *ChatCommandUpdate) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -func (x *ChatCommandUpdate) GetPhase() string { - if x != nil { - return x.Phase - } - return "" -} - -func (x *ChatCommandUpdate) GetErrorCode() string { - if x != nil { - return x.ErrorCode - } - return "" -} - -func (x *ChatCommandUpdate) GetMessage() string { - if x != nil { - return x.Message - } - return "" -} - -// ChatSubscriptionReset 通知客户端某会话流已被限流丢弃,需重新订阅(after_seq 断点续传)。 -type ChatSubscriptionReset struct { - state protoimpl.MessageState `protogen:"open.v1"` - ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatSubscriptionReset) Reset() { - *x = ChatSubscriptionReset{} - mi := &file_proto_v2_gateway_ws_proto_msgTypes[26] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatSubscriptionReset) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatSubscriptionReset) ProtoMessage() {} - -func (x *ChatSubscriptionReset) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_ws_proto_msgTypes[26] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatSubscriptionReset.ProtoReflect.Descriptor instead. -func (*ChatSubscriptionReset) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_ws_proto_rawDescGZIP(), []int{26} -} - -func (x *ChatSubscriptionReset) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -// ChatCancelResult 是 chat.cancel 的响应。 -type ChatCancelResult struct { - state protoimpl.MessageState `protogen:"open.v1"` - Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` - RunId string `protobuf:"bytes,2,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` - ConversationId string `protobuf:"bytes,3,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatCancelResult) Reset() { - *x = ChatCancelResult{} - mi := &file_proto_v2_gateway_ws_proto_msgTypes[27] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatCancelResult) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatCancelResult) ProtoMessage() {} - -func (x *ChatCancelResult) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_ws_proto_msgTypes[27] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatCancelResult.ProtoReflect.Descriptor instead. -func (*ChatCancelResult) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_ws_proto_rawDescGZIP(), []int{27} -} - -func (x *ChatCancelResult) GetOk() bool { - if x != nil { - return x.Ok - } - return false -} - -func (x *ChatCancelResult) GetRunId() string { - if x != nil { - return x.RunId - } - return "" -} - -func (x *ChatCancelResult) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -// ChatActivityEvent 广播会话活动状态变化,镜像 session.ConversationActivityEvent。 -type ChatActivityEvent struct { - state protoimpl.MessageState `protogen:"open.v1"` - ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` - RunId string `protobuf:"bytes,2,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` - ClientRequestId string `protobuf:"bytes,3,opt,name=client_request_id,json=clientRequestId,proto3" json:"client_request_id,omitempty"` - Running bool `protobuf:"varint,4,opt,name=running,proto3" json:"running,omitempty"` - State string `protobuf:"bytes,5,opt,name=state,proto3" json:"state,omitempty"` - Workdir string `protobuf:"bytes,6,opt,name=workdir,proto3" json:"workdir,omitempty"` - UpdatedAtMs int64 `protobuf:"varint,7,opt,name=updated_at_ms,json=updatedAtMs,proto3" json:"updated_at_ms,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatActivityEvent) Reset() { - *x = ChatActivityEvent{} - mi := &file_proto_v2_gateway_ws_proto_msgTypes[28] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatActivityEvent) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatActivityEvent) ProtoMessage() {} - -func (x *ChatActivityEvent) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_ws_proto_msgTypes[28] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChatActivityEvent.ProtoReflect.Descriptor instead. -func (*ChatActivityEvent) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_ws_proto_rawDescGZIP(), []int{28} -} - -func (x *ChatActivityEvent) GetConversationId() string { - if x != nil { - return x.ConversationId - } - return "" -} - -func (x *ChatActivityEvent) GetRunId() string { - if x != nil { - return x.RunId - } - return "" -} - -func (x *ChatActivityEvent) GetClientRequestId() string { - if x != nil { - return x.ClientRequestId - } - return "" -} - -func (x *ChatActivityEvent) GetRunning() bool { - if x != nil { - return x.Running - } - return false -} - -func (x *ChatActivityEvent) GetState() string { - if x != nil { - return x.State - } - return "" -} - -func (x *ChatActivityEvent) GetWorkdir() string { - if x != nil { - return x.Workdir - } - return "" -} - -func (x *ChatActivityEvent) GetUpdatedAtMs() int64 { - if x != nil { - return x.UpdatedAtMs - } - return 0 -} - -// WorkspaceSubscribeRequest 订阅工作区活动(操作类型:"workspace.subscribe");响应 AckResult,事件经 workspace_activity 臂广播。 -type WorkspaceSubscribeRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Workdir string `protobuf:"bytes,1,opt,name=workdir,proto3" json:"workdir,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *WorkspaceSubscribeRequest) Reset() { - *x = WorkspaceSubscribeRequest{} - mi := &file_proto_v2_gateway_ws_proto_msgTypes[29] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *WorkspaceSubscribeRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*WorkspaceSubscribeRequest) ProtoMessage() {} - -func (x *WorkspaceSubscribeRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_ws_proto_msgTypes[29] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use WorkspaceSubscribeRequest.ProtoReflect.Descriptor instead. -func (*WorkspaceSubscribeRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_ws_proto_rawDescGZIP(), []int{29} -} - -func (x *WorkspaceSubscribeRequest) GetWorkdir() string { - if x != nil { - return x.Workdir - } - return "" -} - -// WorkspaceUnsubscribeRequest 取消订阅;响应 AckResult。 -type WorkspaceUnsubscribeRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Workdir string `protobuf:"bytes,1,opt,name=workdir,proto3" json:"workdir,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *WorkspaceUnsubscribeRequest) Reset() { - *x = WorkspaceUnsubscribeRequest{} - mi := &file_proto_v2_gateway_ws_proto_msgTypes[30] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *WorkspaceUnsubscribeRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*WorkspaceUnsubscribeRequest) ProtoMessage() {} - -func (x *WorkspaceUnsubscribeRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_ws_proto_msgTypes[30] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use WorkspaceUnsubscribeRequest.ProtoReflect.Descriptor instead. -func (*WorkspaceUnsubscribeRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_ws_proto_rawDescGZIP(), []int{30} -} - -func (x *WorkspaceUnsubscribeRequest) GetWorkdir() string { - if x != nil { - return x.Workdir - } - return "" -} - -var File_proto_v2_gateway_ws_proto protoreflect.FileDescriptor - -const file_proto_v2_gateway_ws_proto_rawDesc = "" + - "\n" + - "\x19proto/v2/gateway_ws.proto\x12\x14liveagent.gateway.v2\x1a\x16proto/v2/gateway.proto\"\xb0\x02\n" + - "\vClientHello\x12)\n" + - "\x10protocol_version\x18\x01 \x01(\rR\x0fprotocolVersion\x124\n" + - "\x04role\x18\x02 \x01(\x0e2 .liveagent.gateway.v2.ClientRoleR\x04role\x12\x14\n" + - "\x05token\x18\x03 \x01(\tR\x05token\x12\x19\n" + - "\bagent_id\x18\x04 \x01(\tR\aagentId\x12#\n" + - "\ragent_version\x18\x05 \x01(\tR\fagentVersion\x12\x1f\n" + - "\vclient_name\x18\x06 \x01(\tR\n" + - "clientName\x12%\n" + - "\x0eclient_version\x18\a \x01(\tR\rclientVersion\x12\"\n" + - "\fcapabilities\x18\b \x03(\tR\fcapabilities\"\x81\x02\n" + - "\vServerHello\x12\x0e\n" + - "\x02ok\x18\x01 \x01(\bR\x02ok\x12\x18\n" + - "\amessage\x18\x02 \x01(\tR\amessage\x12\x1d\n" + - "\n" + - "session_id\x18\x03 \x01(\tR\tsessionId\x12\x1f\n" + - "\vserver_time\x18\x04 \x01(\x03R\n" + - "serverTime\x128\n" + - "\x18heartbeat_period_seconds\x18\x05 \x01(\rR\x16heartbeatPeriodSeconds\x12*\n" + - "\x11max_message_bytes\x18\x06 \x01(\x04R\x0fmaxMessageBytes\x12\"\n" + - "\fcapabilities\x18\a \x03(\tR\fcapabilities\")\n" + - "\tPingFrame\x12\x1c\n" + - "\ttimestamp\x18\x01 \x01(\x03R\ttimestamp\")\n" + - "\tPongFrame\x12\x1c\n" + - "\ttimestamp\x18\x01 \x01(\x03R\ttimestamp\"\x1b\n" + - "\tAckResult\x12\x0e\n" + - "\x02ok\x18\x01 \x01(\bR\x02ok\"\x9b\b\n" + - "\x0eWebClientFrame\x12\x1d\n" + - "\n" + - "request_id\x18\x01 \x01(\tR\trequestId\x12\x19\n" + - "\bagent_id\x18\r \x01(\tR\aagentId\x129\n" + - "\x05hello\x18\x02 \x01(\v2!.liveagent.gateway.v2.ClientHelloH\x00R\x05hello\x12L\n" + - "\ragent_request\x18\x03 \x01(\v2%.liveagent.gateway.v2.GatewayEnvelopeH\x00R\fagentRequest\x12G\n" + - "\n" + - "status_get\x18\x04 \x01(\v2&.liveagent.gateway.v2.StatusGetRequestH\x00R\tstatusGet\x12M\n" + - "\fchat_command\x18\x05 \x01(\v2(.liveagent.gateway.v2.ChatCommandRequestH\x00R\vchatCommand\x12M\n" + - "\fchat_prepare\x18\x06 \x01(\v2(.liveagent.gateway.v2.ChatPrepareRequestH\x00R\vchatPrepare\x12S\n" + - "\x0echat_subscribe\x18\a \x01(\v2*.liveagent.gateway.v2.ChatSubscribeRequestH\x00R\rchatSubscribe\x12Y\n" + - "\x10chat_unsubscribe\x18\b \x01(\v2,.liveagent.gateway.v2.ChatUnsubscribeRequestH\x00R\x0fchatUnsubscribe\x12V\n" + - "\x0fchat_activities\x18\t \x01(\v2+.liveagent.gateway.v2.ChatActivitiesRequestH\x00R\x0echatActivities\x12b\n" + - "\x13workspace_subscribe\x18\n" + - " \x01(\v2/.liveagent.gateway.v2.WorkspaceSubscribeRequestH\x00R\x12workspaceSubscribe\x12h\n" + - "\x15workspace_unsubscribe\x18\v \x01(\v21.liveagent.gateway.v2.WorkspaceUnsubscribeRequestH\x00R\x14workspaceUnsubscribe\x125\n" + - "\x04pong\x18\f \x01(\v2\x1f.liveagent.gateway.v2.PongFrameH\x00R\x04pong\x12G\n" + - "\n" + - "agent_list\x18\x0e \x01(\v2&.liveagent.gateway.v2.AgentListRequestH\x00R\tagentListB\t\n" + - "\apayload\"\xc8\x0e\n" + - "\x0eWebServerFrame\x12\x1d\n" + - "\n" + - "request_id\x18\x01 \x01(\tR\trequestId\x12\x19\n" + - "\bagent_id\x18\x10 \x01(\tR\aagentId\x129\n" + - "\x05hello\x18\x02 \x01(\v2!.liveagent.gateway.v2.ServerHelloH\x00R\x05hello\x12L\n" + - "\x0eagent_response\x18\x03 \x01(\v2#.liveagent.gateway.v2.AgentEnvelopeH\x00R\ragentResponse\x12F\n" + - "\vlocal_error\x18\x04 \x01(\v2#.liveagent.gateway.v2.ErrorResponseH\x00R\n" + - "localError\x125\n" + - "\x04ping\x18\x05 \x01(\v2\x1f.liveagent.gateway.v2.PingFrameH\x00R\x04ping\x12;\n" + - "\x06status\x18\x06 \x01(\v2!.liveagent.gateway.v2.StatusEventH\x00R\x06status\x12T\n" + - "\x0fchat_subscribed\x18\a \x01(\v2).liveagent.gateway.v2.ChatSubscribeResultH\x00R\x0echatSubscribed\x12P\n" + - "\rchat_accepted\x18\b \x01(\v2).liveagent.gateway.v2.ChatCommandAcceptedH\x00R\fchatAccepted\x12U\n" + - "\x0fchat_activities\x18\t \x01(\v2*.liveagent.gateway.v2.ChatActivitiesResultH\x00R\x0echatActivities\x12F\n" + - "\n" + - "chat_event\x18\n" + - " \x01(\v2%.liveagent.gateway.v2.ChatStreamEventH\x00R\tchatEvent\x12Y\n" + - "\x13chat_command_update\x18\v \x01(\v2'.liveagent.gateway.v2.ChatCommandUpdateH\x00R\x11chatCommandUpdate\x12e\n" + - "\x17chat_subscription_reset\x18\f \x01(\v2+.liveagent.gateway.v2.ChatSubscriptionResetH\x00R\x15chatSubscriptionReset\x12N\n" + - "\rchat_activity\x18\r \x01(\v2'.liveagent.gateway.v2.ChatActivityEventH\x00R\fchatActivity\x123\n" + - "\x03ack\x18\x0e \x01(\v2\x1f.liveagent.gateway.v2.AckResultH\x00R\x03ack\x12O\n" + - "\x0echat_cancelled\x18\x0f \x01(\v2&.liveagent.gateway.v2.ChatCancelResultH\x00R\rchatCancelled\x12F\n" + - "\n" + - "agent_list\x18\x11 \x01(\v2%.liveagent.gateway.v2.AgentListResultH\x00R\tagentList\x12M\n" + - "\rhistory_event\x18\x14 \x01(\v2&.liveagent.gateway.v2.HistorySyncEventH\x00R\fhistoryEvent\x12P\n" + - "\x0esettings_event\x18\x15 \x01(\v2'.liveagent.gateway.v2.SettingsSyncEventH\x00R\rsettingsEvent\x12L\n" + - "\x0eterminal_event\x18\x16 \x01(\v2#.liveagent.gateway.v2.TerminalEventH\x00R\rterminalEvent\x12@\n" + - "\n" + - "sftp_event\x18\x17 \x01(\v2\x1f.liveagent.gateway.v2.SftpEventH\x00R\tsftpEvent\x12P\n" + - "\x10chat_queue_event\x18\x18 \x01(\v2$.liveagent.gateway.v2.ChatQueueEventH\x00R\x0echatQueueEvent\x12N\n" + - "\ftunnel_state\x18\x19 \x01(\v2).liveagent.gateway.v2.TunnelStateSnapshotH\x00R\vtunnelState\x12S\n" + - "\rprocess_state\x18\x1a \x01(\v2,.liveagent.gateway.v2.ManagedProcessSnapshotH\x00R\fprocessState\x12]\n" + - "\x12workspace_activity\x18\x1b \x01(\v2,.liveagent.gateway.v2.WorkspaceActivityEventH\x00R\x11workspaceActivityB\t\n" + - "\apayload\"\x12\n" + - "\x10AgentListRequest\"L\n" + - "\x0fAgentListResult\x129\n" + - "\x06agents\x18\x01 \x03(\v2!.liveagent.gateway.v2.StatusEventR\x06agents\"\x9b\x01\n" + - "\x10AgentClientFrame\x129\n" + - "\x05hello\x18\x01 \x01(\v2!.liveagent.gateway.v2.ClientHelloH\x00R\x05hello\x12A\n" + - "\benvelope\x18\x02 \x01(\v2#.liveagent.gateway.v2.AgentEnvelopeH\x00R\benvelopeB\t\n" + - "\apayload\"\x9d\x01\n" + - "\x10AgentServerFrame\x129\n" + - "\x05hello\x18\x01 \x01(\v2!.liveagent.gateway.v2.ServerHelloH\x00R\x05hello\x12C\n" + - "\benvelope\x18\x02 \x01(\v2%.liveagent.gateway.v2.GatewayEnvelopeH\x00R\benvelopeB\t\n" + - "\apayload\"\x9e\x01\n" + - "\x13TerminalClientFrame\x129\n" + - "\x05hello\x18\x01 \x01(\v2!.liveagent.gateway.v2.ClientHelloH\x00R\x05hello\x12A\n" + - "\x05frame\x18\x02 \x01(\v2).liveagent.gateway.v2.TerminalStreamFrameH\x00R\x05frameB\t\n" + - "\apayload\"\x9e\x01\n" + - "\x13TerminalServerFrame\x129\n" + - "\x05hello\x18\x01 \x01(\v2!.liveagent.gateway.v2.ServerHelloH\x00R\x05hello\x12A\n" + - "\x05frame\x18\x02 \x01(\v2).liveagent.gateway.v2.TerminalStreamFrameH\x00R\x05frameB\t\n" + - "\apayload\"\x12\n" + - "\x10StatusGetRequest\"\xa0\x04\n" + - "\vStatusEvent\x12\x16\n" + - "\x06online\x18\x01 \x01(\bR\x06online\x12\x1f\n" + - "\vagent_ready\x18\x02 \x01(\bR\n" + - "agentReady\x12,\n" + - "\x12chat_runtime_ready\x18\x03 \x01(\bR\x10chatRuntimeReady\x12\x19\n" + - "\bagent_id\x18\x04 \x01(\tR\aagentId\x12#\n" + - "\ragent_version\x18\x05 \x01(\tR\fagentVersion\x12\x1d\n" + - "\n" + - "session_id\x18\x06 \x01(\tR\tsessionId\x12'\n" + - "\x0fconnected_since\x18\a \x01(\x03R\x0econnectedSince\x12%\n" + - "\x0elast_heartbeat\x18\b \x01(\x03R\rlastHeartbeat\x12#\n" + - "\rruntime_state\x18\t \x01(\tR\fruntimeState\x124\n" + - "\x16runtime_last_heartbeat\x18\n" + - " \x01(\x03R\x14runtimeLastHeartbeat\x12*\n" + - "\x11runtime_worker_id\x18\v \x01(\tR\x0fruntimeWorkerId\x12'\n" + - "\x0fruntime_visible\x18\f \x01(\bR\x0eruntimeVisible\x127\n" + - "\x18runtime_active_run_count\x18\r \x01(\rR\x15runtimeActiveRunCount\x12\x12\n" + - "\x04name\x18\x0e \x01(\tR\x04name\",\n" + - "\x12ChatPrepareRequest\x12\x16\n" + - "\x06reason\x18\x01 \x01(\tR\x06reason\"\x7f\n" + - "\x14ChatSubscribeRequest\x12'\n" + - "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\x12\x1b\n" + - "\tafter_seq\x18\x02 \x01(\x03R\bafterSeq\x12!\n" + - "\fstream_epoch\x18\x03 \x01(\tR\vstreamEpoch\"\xe9\x02\n" + - "\x0fChatRunActivity\x12\x15\n" + - "\x06run_id\x18\x01 \x01(\tR\x05runId\x12\x14\n" + - "\x05state\x18\x02 \x01(\tR\x05state\x12\x1f\n" + - "\vstarted_seq\x18\x03 \x01(\x03R\n" + - "startedSeq\x12\"\n" + - "\rupdated_at_ms\x18\x04 \x01(\x03R\vupdatedAtMs\x12\x1f\n" + - "\vtool_status\x18\x05 \x01(\tR\n" + - "toolStatus\x129\n" + - "\x19tool_status_is_compaction\x18\x06 \x01(\bR\x16toolStatusIsCompaction\x12*\n" + - "\x11client_request_id\x18\a \x01(\tR\x0fclientRequestId\x12'\n" + - "\x0fconversation_id\x18\b \x01(\tR\x0econversationId\x12\x18\n" + - "\aworkdir\x18\t \x01(\tR\aworkdir\x12\x19\n" + - "\bagent_id\x18\n" + - " \x01(\tR\aagentId\"\xdf\x01\n" + - "\x0fChatRunSnapshot\x12\x15\n" + - "\x06run_id\x18\x01 \x01(\tR\x05runId\x12\x1a\n" + - "\brevision\x18\x02 \x01(\x03R\brevision\x12!\n" + - "\fentries_json\x18\x03 \x01(\tR\ventriesJson\x12\x1f\n" + - "\vtool_status\x18\x04 \x01(\tR\n" + - "toolStatus\x129\n" + - "\x19tool_status_is_compaction\x18\x05 \x01(\bR\x16toolStatusIsCompaction\x12\x1a\n" + - "\tas_of_seq\x18\x06 \x01(\x03R\aasOfSeq\"\xbd\x02\n" + - "\x13ChatSubscribeResult\x12'\n" + - "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\x12!\n" + - "\fstream_epoch\x18\x02 \x01(\tR\vstreamEpoch\x12\x1d\n" + - "\n" + - "latest_seq\x18\x03 \x01(\x03R\tlatestSeq\x12\x14\n" + - "\x05reset\x18\x04 \x01(\bR\x05reset\x12A\n" + - "\bactivity\x18\x05 \x01(\v2%.liveagent.gateway.v2.ChatRunActivityR\bactivity\x12A\n" + - "\bsnapshot\x18\x06 \x01(\v2%.liveagent.gateway.v2.ChatRunSnapshotR\bsnapshot\x12\x1f\n" + - "\vevents_json\x18\a \x03(\fR\n" + - "eventsJson\"A\n" + - "\x16ChatUnsubscribeRequest\x12'\n" + - "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\"\x17\n" + - "\x15ChatActivitiesRequest\"r\n" + - "\x14ChatActivitiesResult\x12Z\n" + - "\x15running_conversations\x18\x01 \x03(\v2%.liveagent.gateway.v2.ChatRunActivityR\x14runningConversations\"o\n" + - "\x0fChatStreamEvent\x12'\n" + - "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\x12\x10\n" + - "\x03seq\x18\x02 \x01(\x03R\x03seq\x12!\n" + - "\fpayload_json\x18\x03 \x01(\fR\vpayloadJson\"\x92\x01\n" + - "\x13ChatCommandAccepted\x12\x15\n" + - "\x06run_id\x18\x01 \x01(\tR\x05runId\x12'\n" + - "\x0fconversation_id\x18\x02 \x01(\tR\x0econversationId\x12!\n" + - "\faccepted_seq\x18\x03 \x01(\x03R\vacceptedSeq\x12\x18\n" + - "\adeduped\x18\x04 \x01(\bR\adeduped\"\xce\x01\n" + - "\x11ChatCommandUpdate\x12\x15\n" + - "\x06run_id\x18\x01 \x01(\tR\x05runId\x12*\n" + - "\x11client_request_id\x18\x02 \x01(\tR\x0fclientRequestId\x12'\n" + - "\x0fconversation_id\x18\x03 \x01(\tR\x0econversationId\x12\x14\n" + - "\x05phase\x18\x04 \x01(\tR\x05phase\x12\x1d\n" + - "\n" + - "error_code\x18\x05 \x01(\tR\terrorCode\x12\x18\n" + - "\amessage\x18\x06 \x01(\tR\amessage\"@\n" + - "\x15ChatSubscriptionReset\x12'\n" + - "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\"b\n" + - "\x10ChatCancelResult\x12\x0e\n" + - "\x02ok\x18\x01 \x01(\bR\x02ok\x12\x15\n" + - "\x06run_id\x18\x02 \x01(\tR\x05runId\x12'\n" + - "\x0fconversation_id\x18\x03 \x01(\tR\x0econversationId\"\xed\x01\n" + - "\x11ChatActivityEvent\x12'\n" + - "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\x12\x15\n" + - "\x06run_id\x18\x02 \x01(\tR\x05runId\x12*\n" + - "\x11client_request_id\x18\x03 \x01(\tR\x0fclientRequestId\x12\x18\n" + - "\arunning\x18\x04 \x01(\bR\arunning\x12\x14\n" + - "\x05state\x18\x05 \x01(\tR\x05state\x12\x18\n" + - "\aworkdir\x18\x06 \x01(\tR\aworkdir\x12\"\n" + - "\rupdated_at_ms\x18\a \x01(\x03R\vupdatedAtMs\"5\n" + - "\x19WorkspaceSubscribeRequest\x12\x18\n" + - "\aworkdir\x18\x01 \x01(\tR\aworkdir\"7\n" + - "\x1bWorkspaceUnsubscribeRequest\x12\x18\n" + - "\aworkdir\x18\x01 \x01(\tR\aworkdir*Y\n" + - "\n" + - "ClientRole\x12\x1b\n" + - "\x17CLIENT_ROLE_UNSPECIFIED\x10\x00\x12\x17\n" + - "\x13CLIENT_ROLE_BROWSER\x10\x01\x12\x15\n" + - "\x11CLIENT_ROLE_AGENT\x10\x02B@Z>github.com/liveagent/agent-gateway/internal/proto/v2;gatewayv2b\x06proto3" - -var ( - file_proto_v2_gateway_ws_proto_rawDescOnce sync.Once - file_proto_v2_gateway_ws_proto_rawDescData []byte -) - -func file_proto_v2_gateway_ws_proto_rawDescGZIP() []byte { - file_proto_v2_gateway_ws_proto_rawDescOnce.Do(func() { - file_proto_v2_gateway_ws_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_v2_gateway_ws_proto_rawDesc), len(file_proto_v2_gateway_ws_proto_rawDesc))) - }) - return file_proto_v2_gateway_ws_proto_rawDescData -} - -var file_proto_v2_gateway_ws_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_proto_v2_gateway_ws_proto_msgTypes = make([]protoimpl.MessageInfo, 31) -var file_proto_v2_gateway_ws_proto_goTypes = []any{ - (ClientRole)(0), // 0: liveagent.gateway.v2.ClientRole - (*ClientHello)(nil), // 1: liveagent.gateway.v2.ClientHello - (*ServerHello)(nil), // 2: liveagent.gateway.v2.ServerHello - (*PingFrame)(nil), // 3: liveagent.gateway.v2.PingFrame - (*PongFrame)(nil), // 4: liveagent.gateway.v2.PongFrame - (*AckResult)(nil), // 5: liveagent.gateway.v2.AckResult - (*WebClientFrame)(nil), // 6: liveagent.gateway.v2.WebClientFrame - (*WebServerFrame)(nil), // 7: liveagent.gateway.v2.WebServerFrame - (*AgentListRequest)(nil), // 8: liveagent.gateway.v2.AgentListRequest - (*AgentListResult)(nil), // 9: liveagent.gateway.v2.AgentListResult - (*AgentClientFrame)(nil), // 10: liveagent.gateway.v2.AgentClientFrame - (*AgentServerFrame)(nil), // 11: liveagent.gateway.v2.AgentServerFrame - (*TerminalClientFrame)(nil), // 12: liveagent.gateway.v2.TerminalClientFrame - (*TerminalServerFrame)(nil), // 13: liveagent.gateway.v2.TerminalServerFrame - (*StatusGetRequest)(nil), // 14: liveagent.gateway.v2.StatusGetRequest - (*StatusEvent)(nil), // 15: liveagent.gateway.v2.StatusEvent - (*ChatPrepareRequest)(nil), // 16: liveagent.gateway.v2.ChatPrepareRequest - (*ChatSubscribeRequest)(nil), // 17: liveagent.gateway.v2.ChatSubscribeRequest - (*ChatRunActivity)(nil), // 18: liveagent.gateway.v2.ChatRunActivity - (*ChatRunSnapshot)(nil), // 19: liveagent.gateway.v2.ChatRunSnapshot - (*ChatSubscribeResult)(nil), // 20: liveagent.gateway.v2.ChatSubscribeResult - (*ChatUnsubscribeRequest)(nil), // 21: liveagent.gateway.v2.ChatUnsubscribeRequest - (*ChatActivitiesRequest)(nil), // 22: liveagent.gateway.v2.ChatActivitiesRequest - (*ChatActivitiesResult)(nil), // 23: liveagent.gateway.v2.ChatActivitiesResult - (*ChatStreamEvent)(nil), // 24: liveagent.gateway.v2.ChatStreamEvent - (*ChatCommandAccepted)(nil), // 25: liveagent.gateway.v2.ChatCommandAccepted - (*ChatCommandUpdate)(nil), // 26: liveagent.gateway.v2.ChatCommandUpdate - (*ChatSubscriptionReset)(nil), // 27: liveagent.gateway.v2.ChatSubscriptionReset - (*ChatCancelResult)(nil), // 28: liveagent.gateway.v2.ChatCancelResult - (*ChatActivityEvent)(nil), // 29: liveagent.gateway.v2.ChatActivityEvent - (*WorkspaceSubscribeRequest)(nil), // 30: liveagent.gateway.v2.WorkspaceSubscribeRequest - (*WorkspaceUnsubscribeRequest)(nil), // 31: liveagent.gateway.v2.WorkspaceUnsubscribeRequest - (*GatewayEnvelope)(nil), // 32: liveagent.gateway.v2.GatewayEnvelope - (*ChatCommandRequest)(nil), // 33: liveagent.gateway.v2.ChatCommandRequest - (*AgentEnvelope)(nil), // 34: liveagent.gateway.v2.AgentEnvelope - (*ErrorResponse)(nil), // 35: liveagent.gateway.v2.ErrorResponse - (*HistorySyncEvent)(nil), // 36: liveagent.gateway.v2.HistorySyncEvent - (*SettingsSyncEvent)(nil), // 37: liveagent.gateway.v2.SettingsSyncEvent - (*TerminalEvent)(nil), // 38: liveagent.gateway.v2.TerminalEvent - (*SftpEvent)(nil), // 39: liveagent.gateway.v2.SftpEvent - (*ChatQueueEvent)(nil), // 40: liveagent.gateway.v2.ChatQueueEvent - (*TunnelStateSnapshot)(nil), // 41: liveagent.gateway.v2.TunnelStateSnapshot - (*ManagedProcessSnapshot)(nil), // 42: liveagent.gateway.v2.ManagedProcessSnapshot - (*WorkspaceActivityEvent)(nil), // 43: liveagent.gateway.v2.WorkspaceActivityEvent - (*TerminalStreamFrame)(nil), // 44: liveagent.gateway.v2.TerminalStreamFrame -} -var file_proto_v2_gateway_ws_proto_depIdxs = []int32{ - 0, // 0: liveagent.gateway.v2.ClientHello.role:type_name -> liveagent.gateway.v2.ClientRole - 1, // 1: liveagent.gateway.v2.WebClientFrame.hello:type_name -> liveagent.gateway.v2.ClientHello - 32, // 2: liveagent.gateway.v2.WebClientFrame.agent_request:type_name -> liveagent.gateway.v2.GatewayEnvelope - 14, // 3: liveagent.gateway.v2.WebClientFrame.status_get:type_name -> liveagent.gateway.v2.StatusGetRequest - 33, // 4: liveagent.gateway.v2.WebClientFrame.chat_command:type_name -> liveagent.gateway.v2.ChatCommandRequest - 16, // 5: liveagent.gateway.v2.WebClientFrame.chat_prepare:type_name -> liveagent.gateway.v2.ChatPrepareRequest - 17, // 6: liveagent.gateway.v2.WebClientFrame.chat_subscribe:type_name -> liveagent.gateway.v2.ChatSubscribeRequest - 21, // 7: liveagent.gateway.v2.WebClientFrame.chat_unsubscribe:type_name -> liveagent.gateway.v2.ChatUnsubscribeRequest - 22, // 8: liveagent.gateway.v2.WebClientFrame.chat_activities:type_name -> liveagent.gateway.v2.ChatActivitiesRequest - 30, // 9: liveagent.gateway.v2.WebClientFrame.workspace_subscribe:type_name -> liveagent.gateway.v2.WorkspaceSubscribeRequest - 31, // 10: liveagent.gateway.v2.WebClientFrame.workspace_unsubscribe:type_name -> liveagent.gateway.v2.WorkspaceUnsubscribeRequest - 4, // 11: liveagent.gateway.v2.WebClientFrame.pong:type_name -> liveagent.gateway.v2.PongFrame - 8, // 12: liveagent.gateway.v2.WebClientFrame.agent_list:type_name -> liveagent.gateway.v2.AgentListRequest - 2, // 13: liveagent.gateway.v2.WebServerFrame.hello:type_name -> liveagent.gateway.v2.ServerHello - 34, // 14: liveagent.gateway.v2.WebServerFrame.agent_response:type_name -> liveagent.gateway.v2.AgentEnvelope - 35, // 15: liveagent.gateway.v2.WebServerFrame.local_error:type_name -> liveagent.gateway.v2.ErrorResponse - 3, // 16: liveagent.gateway.v2.WebServerFrame.ping:type_name -> liveagent.gateway.v2.PingFrame - 15, // 17: liveagent.gateway.v2.WebServerFrame.status:type_name -> liveagent.gateway.v2.StatusEvent - 20, // 18: liveagent.gateway.v2.WebServerFrame.chat_subscribed:type_name -> liveagent.gateway.v2.ChatSubscribeResult - 25, // 19: liveagent.gateway.v2.WebServerFrame.chat_accepted:type_name -> liveagent.gateway.v2.ChatCommandAccepted - 23, // 20: liveagent.gateway.v2.WebServerFrame.chat_activities:type_name -> liveagent.gateway.v2.ChatActivitiesResult - 24, // 21: liveagent.gateway.v2.WebServerFrame.chat_event:type_name -> liveagent.gateway.v2.ChatStreamEvent - 26, // 22: liveagent.gateway.v2.WebServerFrame.chat_command_update:type_name -> liveagent.gateway.v2.ChatCommandUpdate - 27, // 23: liveagent.gateway.v2.WebServerFrame.chat_subscription_reset:type_name -> liveagent.gateway.v2.ChatSubscriptionReset - 29, // 24: liveagent.gateway.v2.WebServerFrame.chat_activity:type_name -> liveagent.gateway.v2.ChatActivityEvent - 5, // 25: liveagent.gateway.v2.WebServerFrame.ack:type_name -> liveagent.gateway.v2.AckResult - 28, // 26: liveagent.gateway.v2.WebServerFrame.chat_cancelled:type_name -> liveagent.gateway.v2.ChatCancelResult - 9, // 27: liveagent.gateway.v2.WebServerFrame.agent_list:type_name -> liveagent.gateway.v2.AgentListResult - 36, // 28: liveagent.gateway.v2.WebServerFrame.history_event:type_name -> liveagent.gateway.v2.HistorySyncEvent - 37, // 29: liveagent.gateway.v2.WebServerFrame.settings_event:type_name -> liveagent.gateway.v2.SettingsSyncEvent - 38, // 30: liveagent.gateway.v2.WebServerFrame.terminal_event:type_name -> liveagent.gateway.v2.TerminalEvent - 39, // 31: liveagent.gateway.v2.WebServerFrame.sftp_event:type_name -> liveagent.gateway.v2.SftpEvent - 40, // 32: liveagent.gateway.v2.WebServerFrame.chat_queue_event:type_name -> liveagent.gateway.v2.ChatQueueEvent - 41, // 33: liveagent.gateway.v2.WebServerFrame.tunnel_state:type_name -> liveagent.gateway.v2.TunnelStateSnapshot - 42, // 34: liveagent.gateway.v2.WebServerFrame.process_state:type_name -> liveagent.gateway.v2.ManagedProcessSnapshot - 43, // 35: liveagent.gateway.v2.WebServerFrame.workspace_activity:type_name -> liveagent.gateway.v2.WorkspaceActivityEvent - 15, // 36: liveagent.gateway.v2.AgentListResult.agents:type_name -> liveagent.gateway.v2.StatusEvent - 1, // 37: liveagent.gateway.v2.AgentClientFrame.hello:type_name -> liveagent.gateway.v2.ClientHello - 34, // 38: liveagent.gateway.v2.AgentClientFrame.envelope:type_name -> liveagent.gateway.v2.AgentEnvelope - 2, // 39: liveagent.gateway.v2.AgentServerFrame.hello:type_name -> liveagent.gateway.v2.ServerHello - 32, // 40: liveagent.gateway.v2.AgentServerFrame.envelope:type_name -> liveagent.gateway.v2.GatewayEnvelope - 1, // 41: liveagent.gateway.v2.TerminalClientFrame.hello:type_name -> liveagent.gateway.v2.ClientHello - 44, // 42: liveagent.gateway.v2.TerminalClientFrame.frame:type_name -> liveagent.gateway.v2.TerminalStreamFrame - 2, // 43: liveagent.gateway.v2.TerminalServerFrame.hello:type_name -> liveagent.gateway.v2.ServerHello - 44, // 44: liveagent.gateway.v2.TerminalServerFrame.frame:type_name -> liveagent.gateway.v2.TerminalStreamFrame - 18, // 45: liveagent.gateway.v2.ChatSubscribeResult.activity:type_name -> liveagent.gateway.v2.ChatRunActivity - 19, // 46: liveagent.gateway.v2.ChatSubscribeResult.snapshot:type_name -> liveagent.gateway.v2.ChatRunSnapshot - 18, // 47: liveagent.gateway.v2.ChatActivitiesResult.running_conversations:type_name -> liveagent.gateway.v2.ChatRunActivity - 48, // [48:48] is the sub-list for method output_type - 48, // [48:48] is the sub-list for method input_type - 48, // [48:48] is the sub-list for extension type_name - 48, // [48:48] is the sub-list for extension extendee - 0, // [0:48] is the sub-list for field type_name -} - -func init() { file_proto_v2_gateway_ws_proto_init() } -func file_proto_v2_gateway_ws_proto_init() { - if File_proto_v2_gateway_ws_proto != nil { - return - } - file_proto_v2_gateway_proto_init() - file_proto_v2_gateway_ws_proto_msgTypes[5].OneofWrappers = []any{ - (*WebClientFrame_Hello)(nil), - (*WebClientFrame_AgentRequest)(nil), - (*WebClientFrame_StatusGet)(nil), - (*WebClientFrame_ChatCommand)(nil), - (*WebClientFrame_ChatPrepare)(nil), - (*WebClientFrame_ChatSubscribe)(nil), - (*WebClientFrame_ChatUnsubscribe)(nil), - (*WebClientFrame_ChatActivities)(nil), - (*WebClientFrame_WorkspaceSubscribe)(nil), - (*WebClientFrame_WorkspaceUnsubscribe)(nil), - (*WebClientFrame_Pong)(nil), - (*WebClientFrame_AgentList)(nil), - } - file_proto_v2_gateway_ws_proto_msgTypes[6].OneofWrappers = []any{ - (*WebServerFrame_Hello)(nil), - (*WebServerFrame_AgentResponse)(nil), - (*WebServerFrame_LocalError)(nil), - (*WebServerFrame_Ping)(nil), - (*WebServerFrame_Status)(nil), - (*WebServerFrame_ChatSubscribed)(nil), - (*WebServerFrame_ChatAccepted)(nil), - (*WebServerFrame_ChatActivities)(nil), - (*WebServerFrame_ChatEvent)(nil), - (*WebServerFrame_ChatCommandUpdate)(nil), - (*WebServerFrame_ChatSubscriptionReset)(nil), - (*WebServerFrame_ChatActivity)(nil), - (*WebServerFrame_Ack)(nil), - (*WebServerFrame_ChatCancelled)(nil), - (*WebServerFrame_AgentList)(nil), - (*WebServerFrame_HistoryEvent)(nil), - (*WebServerFrame_SettingsEvent)(nil), - (*WebServerFrame_TerminalEvent)(nil), - (*WebServerFrame_SftpEvent)(nil), - (*WebServerFrame_ChatQueueEvent)(nil), - (*WebServerFrame_TunnelState)(nil), - (*WebServerFrame_ProcessState)(nil), - (*WebServerFrame_WorkspaceActivity)(nil), - } - file_proto_v2_gateway_ws_proto_msgTypes[9].OneofWrappers = []any{ - (*AgentClientFrame_Hello)(nil), - (*AgentClientFrame_Envelope)(nil), - } - file_proto_v2_gateway_ws_proto_msgTypes[10].OneofWrappers = []any{ - (*AgentServerFrame_Hello)(nil), - (*AgentServerFrame_Envelope)(nil), - } - file_proto_v2_gateway_ws_proto_msgTypes[11].OneofWrappers = []any{ - (*TerminalClientFrame_Hello)(nil), - (*TerminalClientFrame_Frame)(nil), - } - file_proto_v2_gateway_ws_proto_msgTypes[12].OneofWrappers = []any{ - (*TerminalServerFrame_Hello)(nil), - (*TerminalServerFrame_Frame)(nil), - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_v2_gateway_ws_proto_rawDesc), len(file_proto_v2_gateway_ws_proto_rawDesc)), - NumEnums: 1, - NumMessages: 31, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_proto_v2_gateway_ws_proto_goTypes, - DependencyIndexes: file_proto_v2_gateway_ws_proto_depIdxs, - EnumInfos: file_proto_v2_gateway_ws_proto_enumTypes, - MessageInfos: file_proto_v2_gateway_ws_proto_msgTypes, - }.Build() - File_proto_v2_gateway_ws_proto = out.File - file_proto_v2_gateway_ws_proto_goTypes = nil - file_proto_v2_gateway_ws_proto_depIdxs = nil -} diff --git a/crates/agent-gateway/internal/protocol/pbws/agent_conn.go b/crates/agent-gateway/internal/protocol/pbws/agent_conn.go deleted file mode 100644 index 61fc568f8..000000000 --- a/crates/agent-gateway/internal/protocol/pbws/agent_conn.go +++ /dev/null @@ -1,373 +0,0 @@ -package pbws - -import ( - "context" - "errors" - "log/slog" - "net/http" - "strings" - "sync/atomic" - "time" - - "github.com/google/uuid" - "github.com/gorilla/websocket" - "google.golang.org/protobuf/proto" - - "github.com/liveagent/agent-gateway/internal/auth/agenttoken" - "github.com/liveagent/agent-gateway/internal/observability" - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" - "github.com/liveagent/agent-gateway/internal/session" -) - -const ( - agentInboundQueueFrames = 512 - // Must admit any single frame the read limit allows (64 MiB default): - // interactive responses (fs reads, history payloads) arrive on this same - // queue, and an over-budget frame kills the session. The byte budget - // bounds queue memory, not frame size. - agentInboundQueueBytes = int64(64 * 1024 * 1024) -) - -type queuedAgentEnvelope struct { - envelope *gatewayv2.AgentEnvelope - encodedBytes int64 -} - -// AgentHandler 返回 /ws/v2/agent 的 HTTP 处理器:hello 一并完成鉴权与 -// 会话登记,之后进入双向信封流。 -func (s *Server) AgentHandler() http.Handler { - upgrader := s.upgrader() - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - release, ok := acquireConnSlot(&s.agentConns, s.maxAgentConnections()) - if !ok { - http.Error(w, "too many agent connections", http.StatusServiceUnavailable) - return - } - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - release() - return - } - defer release() - conn.SetReadLimit(s.readLimit()) - s.serveAgent(conn) - }) -} - -func (s *Server) serveAgent(conn *websocket.Conn) { - defer func() { _ = conn.Close() }() - - // ---- 握手:hello 同时完成鉴权与会话登记 ---- - frame, _, ok := readAgentFrame(conn) - if !ok { - return - } - hello := frame.GetHello() - verdict := s.vetHello(hello, gatewayv2.ClientRole_CLIENT_ROLE_AGENT) - if !verdict.ok { - _ = writeDirectMessage(conn, s.writeTimeout(), &gatewayv2.AgentServerFrame{ - Payload: &gatewayv2.AgentServerFrame_Hello{ - Hello: s.serverHello(false, verdict.message, "", s.readLimit()), - }, - }) - closeUnauthorized(conn, s.writeTimeout()) - return - } - authEpoch, err := s.authenticateAgentHello(hello) - if err != nil { - message := "gateway storage unavailable" - if errors.Is(err, agenttoken.ErrUnauthorized) { - message = "unauthorized" - } - _ = writeDirectMessage(conn, s.writeTimeout(), &gatewayv2.AgentServerFrame{ - Payload: &gatewayv2.AgentServerFrame_Hello{ - Hello: s.serverHello(false, message, "", s.readLimit()), - }, - }) - if errors.Is(err, agenttoken.ErrUnauthorized) { - closeUnauthorized(conn, s.writeTimeout()) - } - return - } - - sessionID := uuid.NewString() - authSnapshot := session.AuthSnapshot{ - AgentID: hello.GetAgentId(), - AgentVersion: hello.GetAgentVersion(), - SessionID: sessionID, - } - sess := session.NewAgentSession(authSnapshot) - sess.SetCapabilities(hello.GetCapabilities()) - toAgent := sess.Outbound() - if !s.sm.SetAuthenticatedSessionIfCurrent(sess, func() bool { - return s.tokens.AuthenticationCurrent(hello.GetAgentId(), authEpoch) - }) { - _ = writeDirectMessage(conn, s.writeTimeout(), &gatewayv2.AgentServerFrame{ - Payload: &gatewayv2.AgentServerFrame_Hello{ - Hello: s.serverHello(false, "unauthorized", "", s.readLimit()), - }, - }) - closeUnauthorized(conn, s.writeTimeout()) - return - } - defer s.sm.ClearSession(sess) - if err := writeDirectMessage(conn, s.writeTimeout(), &gatewayv2.AgentServerFrame{ - Payload: &gatewayv2.AgentServerFrame_Hello{ - Hello: s.serverHello(true, "", sessionID, s.readLimit()), - }, - }); err != nil { - return - } - - observability.Usage.V2AgentConnectsTotal.Add(1) - observability.Usage.V2AgentActive.Add(1) - defer observability.Usage.V2AgentActive.Add(-1) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - go func() { - select { - case <-ctx.Done(): - case <-sess.Done(): - cancel() - } - }() - // ctx 结束时关闭底层连接,解除读循环的阻塞。 - go func() { - <-ctx.Done() - _ = conn.Close() - }() - - go s.agentHeartbeatLoop(ctx, conn, sess) - - inbound := make(chan queuedAgentEnvelope, agentInboundQueueFrames) - var inboundBytes atomic.Int64 - go func() { - defer cancel() - for { - select { - case <-ctx.Done(): - return - case <-sess.Done(): - return - case queued := <-inbound: - if queued.envelope != nil { - s.sm.DispatchFromAgentForSession(sess, queued.envelope) - } - releaseAgentInboundBytes(&inboundBytes, queued.encodedBytes) - } - } - }() - - // WS 控制帧 pong 计入桌面端存活(对应 h2 keepalive 的职能)。 - conn.SetPongHandler(func(string) error { - s.sm.TouchHeartbeat(sess) - return nil - }) - - // ---- 出站泵:心跳专用通道优先,拥塞永远饿不死保活 ---- - go func() { - defer cancel() - pings := sess.Pings() - for { - select { - case ping := <-pings: - if !s.writeAgentEnvelope(conn, ping) { - return - } - continue - default: - } - select { - case <-ctx.Done(): - return - case <-sess.Done(): - return - case ping := <-pings: - if !s.writeAgentEnvelope(conn, ping) { - return - } - case outbound := <-toAgent: - if outbound == nil || outbound.GatewayEnvelope == nil { - continue - } - select { - case <-outbound.Context().Done(): - outbound.Ack(outbound.Context().Err()) - continue - default: - } - if !s.writeAgentEnvelope(conn, outbound.GatewayEnvelope) { - outbound.Ack(context.Canceled) - return - } - outbound.Ack(nil) - } - } - }() - - // ---- 入站循环 ---- - for { - frame, encodedBytes, ok := readAgentFrame(conn) - if !ok { - cancel() - return - } - env := frame.GetEnvelope() - if env == nil { - // 重复 hello 或空帧:忽略(仍计入存活)。 - s.sm.TouchHeartbeat(sess) - continue - } - // 任何入站信封都证明桌面端存活;活跃流式传输中的 agent 绝不能被判心跳过期。 - s.sm.TouchHeartbeat(sess) - // 单一有界 dispatcher 保持信封顺序,同时把 protobuf 读取从业务 - // 处理解耦。帧数或字节水位饱和时立即废弃当前 session;可靠聊天由 - // Agent 在新连接重放,不能让慢 handler 反向阻塞 reader 和心跳。 - queuedBytes := int64(encodedBytes) - if !reserveAgentInboundBytes(&inboundBytes, queuedBytes) { - noteAgentInboundOverflow(sess, queuedBytes, "byte_limit") - cancel() - return - } - select { - case <-ctx.Done(): - releaseAgentInboundBytes(&inboundBytes, queuedBytes) - return - case <-sess.Done(): - releaseAgentInboundBytes(&inboundBytes, queuedBytes) - return - case inbound <- queuedAgentEnvelope{envelope: env, encodedBytes: queuedBytes}: - default: - releaseAgentInboundBytes(&inboundBytes, queuedBytes) - noteAgentInboundOverflow(sess, queuedBytes, "frame_limit") - cancel() - return - } - } -} - -func noteAgentInboundOverflow(sess *session.AgentSession, size int64, reason string) { - agentID := "" - sessionID := "" - if sess != nil { - agentID = strings.TrimSpace(sess.AgentID) - sessionID = strings.TrimSpace(sess.SessionID) - } - observability.Usage.V2AgentInboundOverflowsTotal.Add(1) - slog.Warn("agent_inbound_overflow", - "agent_id", agentID, - "session_id", sessionID, - "lane", "agent_inbound", - "size", size, - "reason", reason, - ) -} - -func readAgentFrame(conn *websocket.Conn) (*gatewayv2.AgentClientFrame, int, bool) { - for { - messageType, data, err := conn.ReadMessage() - if err != nil { - return nil, 0, false - } - if messageType != websocket.BinaryMessage { - continue - } - var frame gatewayv2.AgentClientFrame - if err := proto.Unmarshal(data, &frame); err != nil { - return nil, 0, false - } - return &frame, len(data), true - } -} - -func reserveAgentInboundBytes(queuedBytes *atomic.Int64, frameBytes int64) bool { - if frameBytes < 0 || frameBytes > agentInboundQueueBytes { - return false - } - for { - current := queuedBytes.Load() - if frameBytes > agentInboundQueueBytes-current { - return false - } - if queuedBytes.CompareAndSwap(current, current+frameBytes) { - return true - } - } -} - -func releaseAgentInboundBytes(queuedBytes *atomic.Int64, frameBytes int64) { - for { - current := queuedBytes.Load() - next := current - frameBytes - if next < 0 { - next = 0 - } - if queuedBytes.CompareAndSwap(current, next) { - return - } - } -} - -// writeAgentEnvelope 序列化并写出一条 GatewayEnvelope 帧(单写者无需互斥;WriteControl 与之并发安全)。 -func (s *Server) writeAgentEnvelope(conn *websocket.Conn, env *gatewayv2.GatewayEnvelope) bool { - data, err := proto.Marshal(&gatewayv2.AgentServerFrame{ - Payload: &gatewayv2.AgentServerFrame_Envelope{Envelope: env}, - }) - if err != nil { - return false - } - if timeout := s.writeTimeout(); timeout > 0 { - if err := conn.SetWriteDeadline(time.Now().Add(timeout)); err != nil { - return false - } - defer func() { _ = conn.SetWriteDeadline(time.Time{}) }() - } - return conn.WriteMessage(websocket.BinaryMessage, data) == nil -} - -// agentHeartbeatLoop:周期发应用层 Ping(走专用心跳通道)、 -// 驱逐心跳过期会话;额外补发 WS 控制帧 ping,由 tokio-tungstenite 自动 pong 承担传输层保活。 -func (s *Server) agentHeartbeatLoop(ctx context.Context, conn *websocket.Conn, sess *session.AgentSession) { - period := 30 * time.Second - if s.cfg != nil && s.cfg.HeartbeatPeriod > 0 { - period = s.cfg.HeartbeatPeriod - } - ticker := time.NewTicker(period) - defer ticker.Stop() - - if !s.sendAgentHeartbeat(sess) { - return - } - - timeout := period * 3 - for { - select { - case <-ctx.Done(): - return - case <-sess.Done(): - return - case <-ticker.C: - if s.sm.ClearSessionIfHeartbeatStale(sess, timeout) { - return - } - deadline := time.Now().Add(s.writeTimeout()) - _ = conn.WriteControl(websocket.PingMessage, nil, deadline) - if !s.sendAgentHeartbeat(sess) { - return - } - } - } -} - -func (s *Server) sendAgentHeartbeat(sess *session.AgentSession) bool { - return sess.SendPing(&gatewayv2.GatewayEnvelope{ - RequestId: "ping-" + uuid.NewString(), - Timestamp: time.Now().Unix(), - Payload: &gatewayv2.GatewayEnvelope_Ping{ - Ping: &gatewayv2.PingRequest{ - Timestamp: time.Now().Unix(), - }, - }, - }) == nil -} diff --git a/crates/agent-gateway/internal/protocol/pbws/agent_conn_test.go b/crates/agent-gateway/internal/protocol/pbws/agent_conn_test.go deleted file mode 100644 index ed5308373..000000000 --- a/crates/agent-gateway/internal/protocol/pbws/agent_conn_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package pbws - -import ( - "sync/atomic" - "testing" - - "github.com/liveagent/agent-gateway/internal/observability" - "github.com/liveagent/agent-gateway/internal/session" -) - -func TestAgentInboundByteBudgetIsBoundedAndReleased(t *testing.T) { - t.Parallel() - - var queued atomic.Int64 - if !reserveAgentInboundBytes(&queued, agentInboundQueueBytes) { - t.Fatal("reserve exact inbound byte budget = false, want true") - } - if reserveAgentInboundBytes(&queued, 1) { - t.Fatal("reserve beyond inbound byte budget = true, want false") - } - releaseAgentInboundBytes(&queued, agentInboundQueueBytes) - if got := queued.Load(); got != 0 { - t.Fatalf("queued inbound bytes after release = %d, want 0", got) - } - if !reserveAgentInboundBytes(&queued, 1) { - t.Fatal("reserve after release = false, want true") - } -} - -func TestAgentInboundRejectsOversizedFrame(t *testing.T) { - t.Parallel() - - var queued atomic.Int64 - if reserveAgentInboundBytes(&queued, agentInboundQueueBytes+1) { - t.Fatal("oversized inbound frame reserve = true, want false") - } - if got := queued.Load(); got != 0 { - t.Fatalf("queued inbound bytes after oversized frame = %d, want 0", got) - } -} - -func TestAgentInboundOverflowIncrementsProtocolUsage(t *testing.T) { - before := observability.Usage.V2AgentInboundOverflowsTotal.Load() - agentSession := session.NewAgentSession(session.AuthSnapshot{ - AgentID: "agent-observe", - SessionID: "session-observe", - }) - defer agentSession.Close() - noteAgentInboundOverflow(agentSession, 1024, "frame_limit") - if got := observability.Usage.V2AgentInboundOverflowsTotal.Load() - before; got != 1 { - t.Fatalf("agent inbound overflow metric delta = %d, want 1", got) - } -} diff --git a/crates/agent-gateway/internal/protocol/pbws/browser_conn.go b/crates/agent-gateway/internal/protocol/pbws/browser_conn.go deleted file mode 100644 index e3b464706..000000000 --- a/crates/agent-gateway/internal/protocol/pbws/browser_conn.go +++ /dev/null @@ -1,336 +0,0 @@ -package pbws - -import ( - "net/http" - "strconv" - "strings" - "sync" - "sync/atomic" - "time" - - "github.com/gorilla/websocket" - "google.golang.org/protobuf/proto" - - "github.com/liveagent/agent-gateway/internal/config" - "github.com/liveagent/agent-gateway/internal/observability" - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" - "github.com/liveagent/agent-gateway/internal/protocol/shared" - "github.com/liveagent/agent-gateway/internal/session" - "github.com/liveagent/agent-gateway/internal/transport/wscore" -) - -// browserConnSeq 为每条浏览器连接分配 request_id 命名空间前缀,消除多标签页并发时 -// agent 侧关联 id 冲突的可能。 -var browserConnSeq atomic.Uint64 - -// browserConn 是 /ws/v2 上的一条浏览器连接。 -type browserConn struct { - cfg *config.Config - sm *session.Manager - srv *Server - - conn *websocket.Conn - core *wscore.Conn - done <-chan struct{} - - // idPrefix + 原始 request_id 构成转发给桌面端的关联 id;回程剥离。 - idPrefix string - - terminalInterest *shared.TerminalInterestTracker - - // dispatchLimiter 限制在途派发数(慢请求 goroutine 上限);rateLimiter 限制 - // 入站帧速率(快帧 CPU 上限)。两者合围单连接的资源占用。 - dispatchLimiter *wscore.DispatchLimiter - rateLimiter *wscore.InboundRateLimiter - - chatStreamsMu sync.Mutex - chatStreams map[string]func() // agent_id + conversation_id -> 订阅取消 - - workspaceSubsMu sync.Mutex - workspaceSubs map[string]*workspaceSubscription -} - -// BrowserHandler 返回 /ws/v2 的 HTTP 处理器。 -func (s *Server) BrowserHandler() http.Handler { - upgrader := s.upgrader() - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - release, ok := acquireConnSlot(&s.browserConns, s.maxBrowserConnections()) - if !ok { - http.Error(w, "too many browser connections", http.StatusServiceUnavailable) - return - } - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - release() - return - } - defer release() - conn.SetReadLimit(browserReadLimit) - - c := &browserConn{ - cfg: s.cfg, - sm: s.sm, - srv: s, - conn: conn, - idPrefix: browserIDPrefix(), - terminalInterest: shared.NewTerminalInterestTracker(), - dispatchLimiter: wscore.NewDispatchLimiter(maxInflightDispatches), - rateLimiter: wscore.NewInboundRateLimiter( - browserInboundFramesPerSecond, browserInboundBurst, browserRateLimitMaxViolations, - ), - } - c.core = wscore.NewConn(conn, wscore.Config{ - WriteTimeout: s.cfg.WebSocketWriteTimeout, - QueueSize: s.cfg.WebSocketWriteQueueSize, - // The chat_subscribed replay is a single FrameResponse that can - // carry a near-full event ring (8 MiB approx-counted, JSON-marshal - // expansion ~1.5x) plus a projection snapshot (≤4 MiB); size the - // data-queue byte budget with headroom so a legitimate replay - // never trips the frame_too_large connection close. - QueueBytes: 24 * 1024 * 1024, - HeartbeatPeriod: s.cfg.WebSocketHeartbeatPeriod, - HeartbeatGrace: s.cfg.WebSocketHeartbeatGrace, - Remote: r.RemoteAddr, - OnClose: c.releaseSubscriptions, - }) - c.done = c.core.Done() - // WS 控制帧 pong 由浏览器网络栈应答(后台节流标签页亦然),必须计入存活证据。 - conn.SetPongHandler(func(string) error { - c.core.TouchInboundActivity() - return nil - }) - _ = conn.SetReadDeadline(time.Now().Add(c.core.IdleTimeout())) - defer c.core.Close() - c.serve() - }) -} - -func browserIDPrefix() string { - // 前缀短且进程内唯一即可;对端只做等值回显。 - return "b" + strconv.FormatUint(browserConnSeq.Add(1), 10) + ":" -} - -// serve 是读循环:首帧必须 hello,之后按载荷臂分发。chat/workspace 订阅生命周期帧在读循环 -// 内联执行以保帧序(重订阅连发 [unsubscribe, subscribe],并发分发会让旧退订取消新订阅); -// 其余请求各自 goroutine 处理。 -func (c *browserConn) serve() { - if !c.handshake() { - return - } - - observability.Usage.V2BrowserConnectionsTotal.Add(1) - observability.Usage.V2BrowserConnectionsActive.Add(1) - defer observability.Usage.V2BrowserConnectionsActive.Add(-1) - - for { - frame, ok := c.readFrame() - if !ok { - return - } - c.core.TouchInboundActivity() - - // 入站限速:超限丢帧回错,连续违规判定失控客户端、关闭连接。 - if allowed, exceeded := c.rateLimiter.Allow(); !allowed { - if exceeded { - return - } - _ = c.sendLocalError(frame.GetRequestId(), "too many requests") - continue - } - - switch payload := frame.GetPayload().(type) { - case *gatewayv2.WebClientFrame_Pong: - continue - case *gatewayv2.WebClientFrame_Hello: - _ = c.sendLocalError(frame.GetRequestId(), "already authenticated") - continue - case *gatewayv2.WebClientFrame_ChatSubscribe, - *gatewayv2.WebClientFrame_ChatUnsubscribe, - *gatewayv2.WebClientFrame_WorkspaceSubscribe, - *gatewayv2.WebClientFrame_WorkspaceUnsubscribe: - c.dispatch(frame) - case nil: - _ = c.sendLocalError(frame.GetRequestId(), "frame payload is required") - continue - default: - _ = payload - // try-acquire 失败即拒绝:绝不阻塞读循环等槽位(会拖死 pong/存活检测)。 - if !c.dispatchLimiter.TryAcquire() { - _ = c.sendLocalError(frame.GetRequestId(), "too many concurrent requests") - continue - } - go func(frame *gatewayv2.WebClientFrame) { - defer c.dispatchLimiter.Release() - c.dispatch(frame) - }(frame) - } - } -} - -// readFrame 读取并解码一帧;解码失败说明帧流已破坏,直接关闭连接。 -func (c *browserConn) readFrame() (*gatewayv2.WebClientFrame, bool) { - for { - messageType, data, err := c.conn.ReadMessage() - if err != nil { - return nil, false - } - if messageType != websocket.BinaryMessage { - // v2 链路上文本帧无意义;容忍并忽略(仍计入存活)。 - c.core.TouchInboundActivity() - continue - } - var frame gatewayv2.WebClientFrame - if err := proto.Unmarshal(data, &frame); err != nil { - return nil, false - } - return &frame, true - } -} - -// handshake 处理首帧 hello;失败时写出失败应答并关闭。 -func (c *browserConn) handshake() bool { - frame, ok := c.readFrame() - if !ok { - return false - } - hello := frame.GetHello() - verdict := c.srv.vetHello(hello, gatewayv2.ClientRole_CLIENT_ROLE_BROWSER) - if !verdict.ok { - _ = writeDirectMessage(c.conn, c.srv.writeTimeout(), &gatewayv2.WebServerFrame{ - RequestId: frame.GetRequestId(), - Payload: &gatewayv2.WebServerFrame_Hello{ - Hello: c.srv.serverHello(false, verdict.message, "", browserReadLimit), - }, - }) - closeUnauthorized(c.conn, c.srv.writeTimeout()) - return false - } - - c.core.SetAuthorized() - // 握手前的读超时刻意未刷新;成功后立即续期。 - c.core.TouchInboundActivity() - c.core.StartWriteLoop() - c.startEventForwarders() - c.core.StartHeartbeat(c.buildHeartbeatPing) - - // hello 应答走数据队列(FrameResponse):与快照回放同队 FIFO,保证客户端先收 hello - // 再收回放帧(跨队列只有优先级、无顺序保证)。 - if err := c.send(wscore.FrameResponse, "hello", &gatewayv2.WebServerFrame{ - RequestId: frame.GetRequestId(), - Payload: &gatewayv2.WebServerFrame_Hello{ - Hello: c.srv.serverHello(true, "", "", browserReadLimit), - }, - }); err != nil { - c.core.Close() - return false - } - c.replaySnapshots() - return true -} - -func (c *browserConn) dispatch(frame *gatewayv2.WebClientFrame) { - observability.Usage.V2BrowserRequestsTotal.Add(1) - requestID := strings.TrimSpace(frame.GetRequestId()) - // 目标型请求必须显式声明 Agent;目录与全局会话查询不需要目标 id。 - agentID := strings.TrimSpace(frame.GetAgentId()) - - switch payload := frame.GetPayload().(type) { - case *gatewayv2.WebClientFrame_AgentRequest: - if !c.requireAgentID(requestID, agentID) { - return - } - c.handleAgentRequest(requestID, agentID, payload.AgentRequest) - case *gatewayv2.WebClientFrame_StatusGet: - if !c.requireAgentID(requestID, agentID) { - return - } - c.handleStatusGet(requestID, agentID) - case *gatewayv2.WebClientFrame_ChatCommand: - if !c.requireAgentID(requestID, agentID) { - return - } - c.handleChatCommand(requestID, agentID, payload.ChatCommand) - case *gatewayv2.WebClientFrame_ChatPrepare: - if !c.requireAgentID(requestID, agentID) { - return - } - c.handleChatPrepare(requestID, agentID, payload.ChatPrepare) - case *gatewayv2.WebClientFrame_ChatSubscribe: - if !c.requireAgentID(requestID, agentID) { - return - } - c.handleChatSubscribe(requestID, agentID, payload.ChatSubscribe) - case *gatewayv2.WebClientFrame_ChatUnsubscribe: - if !c.requireAgentID(requestID, agentID) { - return - } - c.handleChatUnsubscribe(requestID, agentID, payload.ChatUnsubscribe) - case *gatewayv2.WebClientFrame_ChatActivities: - c.handleChatActivities(requestID) - case *gatewayv2.WebClientFrame_WorkspaceSubscribe: - if !c.requireAgentID(requestID, agentID) { - return - } - c.handleWorkspaceSubscribe(requestID, agentID, payload.WorkspaceSubscribe) - case *gatewayv2.WebClientFrame_WorkspaceUnsubscribe: - if !c.requireAgentID(requestID, agentID) { - return - } - c.handleWorkspaceUnsubscribe(requestID, agentID, payload.WorkspaceUnsubscribe) - case *gatewayv2.WebClientFrame_AgentList: - c.handleAgentList(requestID) - default: - _ = c.sendLocalError(requestID, "unsupported frame payload") - } -} - -func (c *browserConn) requireAgentID(requestID, agentID string) bool { - if agentID != "" { - return true - } - _ = c.sendLocalError(requestID, "agent_id is required") - return false -} - -// send 编码并投递一帧(拥塞策略由帧类别声明,wscore 统一执行)。 -func (c *browserConn) send(class wscore.FrameClass, kind string, frame *gatewayv2.WebServerFrame) error { - data, err := proto.Marshal(frame) - if err != nil { - return err - } - return c.core.Enqueue(wscore.Frame{ - Class: class, - RequestID: frame.GetRequestId(), - Kind: kind, - MessageType: websocket.BinaryMessage, - Data: data, - }) -} - -// sendLocalError 回送网关本地结构化错误,走控制队列保证拥塞下可达。 -func (c *browserConn) sendLocalError(requestID string, message string) error { - return c.send(wscore.FrameControl, "local_error", &gatewayv2.WebServerFrame{ - RequestId: requestID, - Payload: &gatewayv2.WebServerFrame_LocalError{ - LocalError: &gatewayv2.ErrorResponse{Message: message}, - }, - }) -} - -// buildHeartbeatPing 为共享心跳循环构造应用层 PingFrame。 -func (c *browserConn) buildHeartbeatPing() (wscore.Frame, bool) { - data, err := proto.Marshal(&gatewayv2.WebServerFrame{ - Payload: &gatewayv2.WebServerFrame_Ping{ - Ping: &gatewayv2.PingFrame{Timestamp: time.Now().Unix()}, - }, - }) - if err != nil { - return wscore.Frame{}, false - } - return wscore.Frame{ - Class: wscore.FramePing, - Kind: "ping", - MessageType: websocket.BinaryMessage, - Data: data, - }, true -} diff --git a/crates/agent-gateway/internal/protocol/pbws/browser_events.go b/crates/agent-gateway/internal/protocol/pbws/browser_events.go deleted file mode 100644 index 0d19b6da2..000000000 --- a/crates/agent-gateway/internal/protocol/pbws/browser_events.go +++ /dev/null @@ -1,456 +0,0 @@ -package pbws - -import ( - "encoding/json" - "errors" - "strings" - "sync" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" - "github.com/liveagent/agent-gateway/internal/protocol/shared" - "github.com/liveagent/agent-gateway/internal/session" - "github.com/liveagent/agent-gateway/internal/transport/wscore" -) - -// 浏览器连接的订阅生命周期、九路广播转发与连接后快照回放: -// 广播帧可掉(errWriteQueueFull 跳过继续),chat 会话流掉帧则发订阅重置信号让客户端按 -// after_seq 断点续传。 - -// workspaceSubscription 是一个 workdir 的活动订阅。 -type workspaceSubscription struct { - cancel func() - done chan struct{} - once sync.Once -} - -func (s *workspaceSubscription) close() { - s.once.Do(func() { - close(s.done) - s.cancel() - }) -} - -// releaseSubscriptions 由 core 关闭回调(恰好一次),释放 chat/workspace 订阅; -// 九路广播转发器各自监听 done 退出并 defer cleanup。 -func (c *browserConn) releaseSubscriptions() { - c.chatStreamsMu.Lock() - for subKey, cancel := range c.chatStreams { - cancel() - delete(c.chatStreams, subKey) - } - c.chatStreamsMu.Unlock() - - c.workspaceSubsMu.Lock() - for workdir, sub := range c.workspaceSubs { - sub.close() - delete(c.workspaceSubs, workdir) - } - c.workspaceSubsMu.Unlock() -} - -// --------------------------------------------------------------------------- -// chat 会话流订阅 -// --------------------------------------------------------------------------- - -// handleChatSubscribe 处理 chat.subscribe(读循环内联执行以保帧序)。 -func (c *browserConn) handleChatSubscribe(requestID, agentID string, req *gatewayv2.ChatSubscribeRequest) { - agentID = strings.TrimSpace(agentID) - conversationID := strings.TrimSpace(req.GetConversationId()) - if conversationID == "" { - _ = c.sendLocalError(requestID, "conversation_id is required") - return - } - - sub := c.sm.SubscribeConversationStream(agentID, conversationID, req.GetAfterSeq(), req.GetStreamEpoch()) - if sub == nil { - _ = c.sendLocalError(requestID, "agent_id and conversation_id are required") - return - } - subKey := agentID + "\x00" + conversationID - - events := make([][]byte, 0, len(sub.Events)) - for _, event := range sub.Events { - payload, err := json.Marshal(event.Payload) - if err != nil { - continue - } - events = append(events, payload) - } - result := &gatewayv2.ChatSubscribeResult{ - ConversationId: sub.ConversationID, - StreamEpoch: sub.StreamEpoch, - LatestSeq: sub.LatestSeq, - Reset_: sub.Reset, - Activity: chatRunActivity(sub.Activity), - Snapshot: chatRunSnapshot(sub.Snapshot), - EventsJson: events, - } - - // 先登记(替换同会话旧订阅)再应答,避免回放边界之后发布的事件被漏。 - c.chatStreamsMu.Lock() - if c.chatStreams == nil { - c.chatStreams = make(map[string]func()) - } - if previous := c.chatStreams[subKey]; previous != nil { - previous() - } - c.chatStreams[subKey] = sub.Cleanup - c.chatStreamsMu.Unlock() - - if err := c.send(wscore.FrameResponse, "chat_subscribed", &gatewayv2.WebServerFrame{ - RequestId: requestID, - AgentId: sub.AgentID, - Payload: &gatewayv2.WebServerFrame_ChatSubscribed{ChatSubscribed: result}, - }); err != nil { - sub.Cleanup() - c.chatStreamsMu.Lock() - // Cleanup 幂等:仅当仍指向本次订阅时移除登记。 - delete(c.chatStreams, subKey) - c.chatStreamsMu.Unlock() - // 被掉帧的订阅响应会让客户端干等到超时且无人重订阅;控制队列上的重置信号重新武装其恢复循环。 - if errors.Is(err, wscore.ErrWriteQueueFull) { - c.sendSubscriptionResetOrClose(sub.AgentID, conversationID) - } - return - } - - go c.forwardConversationEvents(sub.AgentID, conversationID, sub) -} - -// handleChatUnsubscribe 处理 chat.unsubscribe。 -func (c *browserConn) handleChatUnsubscribe(requestID, agentID string, req *gatewayv2.ChatUnsubscribeRequest) { - agentID = strings.TrimSpace(agentID) - conversationID := strings.TrimSpace(req.GetConversationId()) - if conversationID == "" { - _ = c.sendLocalError(requestID, "conversation_id is required") - return - } - subKey := agentID + "\x00" + conversationID - - c.chatStreamsMu.Lock() - if cancel := c.chatStreams[subKey]; cancel != nil { - cancel() - delete(c.chatStreams, subKey) - } - c.chatStreamsMu.Unlock() - - _ = c.sendAck(requestID) -} - -func (c *browserConn) sendAck(requestID string) error { - return c.send(wscore.FrameResponse, "ack", &gatewayv2.WebServerFrame{ - RequestId: requestID, - Payload: &gatewayv2.WebServerFrame_Ack{Ack: &gatewayv2.AckResult{Ok: true}}, - }) -} - -// forwardConversationEvents 推送订阅后的实时会话事件;订阅通道溢出或写队列持续拥塞时 -// 通知客户端重订阅(after_seq 从缓冲重放缺口),拥塞只牺牲该订阅、不牺牲连接。 -func (c *browserConn) forwardConversationEvents( - agentID string, - conversationID string, - sub *session.ConversationSubscription, -) { - defer sub.Cleanup() - for { - select { - case <-c.done: - return - case event, ok := <-sub.EventCh: - if !ok { - if sub.Overflowed() { - c.sendSubscriptionResetOrClose(agentID, conversationID) - } - return - } - payload, err := json.Marshal(event.Payload) - if err != nil { - continue - } - if err := c.send(wscore.FrameData, "chat_event", &gatewayv2.WebServerFrame{ - AgentId: agentID, - Payload: &gatewayv2.WebServerFrame_ChatEvent{ - ChatEvent: &gatewayv2.ChatStreamEvent{ - ConversationId: conversationID, - Seq: event.Seq, - PayloadJson: payload, - }, - }, - }); err != nil { - if errors.Is(err, wscore.ErrWriteQueueFull) || errors.Is(err, wscore.ErrWriteFrameTooLarge) { - // 重置帧走控制队列越过拥塞积压;客户端重同步后按 seq 去重在途旧事件。 - // 超限单帧同样只牺牲该订阅:重订阅回放/快照与 history 收敛负责补内容。 - c.sendSubscriptionResetOrClose(agentID, conversationID) - } - return - } - } - } -} - -// sendSubscriptionResetOrClose 送出恢复被掉订阅的唯一信号;连控制队列都容不下时关闭连接, -// 重连后的重订阅(after_seq)是仅剩的不可丢路径。 -func (c *browserConn) sendSubscriptionResetOrClose(agentID string, conversationID string) { - if err := c.send(wscore.FrameControl, "chat_subscription_reset", &gatewayv2.WebServerFrame{ - AgentId: agentID, - Payload: &gatewayv2.WebServerFrame_ChatSubscriptionReset{ - ChatSubscriptionReset: &gatewayv2.ChatSubscriptionReset{ConversationId: conversationID}, - }, - }); err != nil { - c.core.Close() - } -} - -// --------------------------------------------------------------------------- -// workspace 活动订阅 -// --------------------------------------------------------------------------- - -// handleWorkspaceSubscribe 处理 workspace.subscribe(读循环内联)。订阅按 -// (agent, workdir) 作用域;分派层已保证 agent_id 非空。 -func (c *browserConn) handleWorkspaceSubscribe(requestID, agentID string, req *gatewayv2.WorkspaceSubscribeRequest) { - workdir := strings.TrimSpace(req.GetWorkdir()) - if workdir == "" { - _ = c.sendLocalError(requestID, "workdir is required") - return - } - requestedAgentID := strings.TrimSpace(agentID) - resolvedAgentID, err := c.sm.ResolveAgentID(requestedAgentID) - if err != nil { - _ = c.sendLocalError(requestID, errorMessage(err)) - return - } - subKey := requestedAgentID + "\x00" + workdir - - events, cancel := c.sm.SubscribeWorkspaceActivity(resolvedAgentID, workdir) - sub := &workspaceSubscription{ - cancel: cancel, - done: make(chan struct{}), - } - - c.workspaceSubsMu.Lock() - if c.workspaceSubs == nil { - c.workspaceSubs = make(map[string]*workspaceSubscription) - } - if previous := c.workspaceSubs[subKey]; previous != nil { - previous.close() - } - c.workspaceSubs[subKey] = sub - c.workspaceSubsMu.Unlock() - - if err := c.sendAck(requestID); err != nil { - sub.close() - c.workspaceSubsMu.Lock() - if c.workspaceSubs[subKey] == sub { - delete(c.workspaceSubs, subKey) - } - c.workspaceSubsMu.Unlock() - return - } - - go func() { - for { - select { - case <-c.done: - return - case <-sub.done: - return - case event, ok := <-events: - if !ok { - return - } - if err := c.send(wscore.FrameData, "workspace_activity", &gatewayv2.WebServerFrame{ - AgentId: resolvedAgentID, - Payload: &gatewayv2.WebServerFrame_WorkspaceActivity{WorkspaceActivity: event}, - }); err != nil { - if errors.Is(err, wscore.ErrWriteQueueFull) { - continue - } - return - } - } - } - }() -} - -// handleWorkspaceUnsubscribe 处理 workspace.unsubscribe。 -func (c *browserConn) handleWorkspaceUnsubscribe(requestID, agentID string, req *gatewayv2.WorkspaceUnsubscribeRequest) { - subKey := strings.TrimSpace(agentID) + "\x00" + strings.TrimSpace(req.GetWorkdir()) - - c.workspaceSubsMu.Lock() - if sub := c.workspaceSubs[subKey]; sub != nil { - sub.close() - delete(c.workspaceSubs, subKey) - } - c.workspaceSubsMu.Unlock() - - _ = c.sendAck(requestID) -} - -// --------------------------------------------------------------------------- -// 广播事件扇出与快照回放 -// --------------------------------------------------------------------------- - -// startEventForwarders 启动九路广播转发; -// 泛型 forward 统一可掉帧广播骨架,各路只提供订阅与帧构造。广播帧盖来源 -// agent_id(服务端不过滤,客户端按活跃 Agent 过滤);功能门控按来源 Agent 判定。 -func (c *browserConn) startEventForwarders() { - forward(c, c.sm.SubscribeHistorySync, func(event session.Tagged[*gatewayv2.HistorySyncEvent]) (*gatewayv2.WebServerFrame, bool) { - return &gatewayv2.WebServerFrame{ - AgentId: event.AgentID, - Payload: &gatewayv2.WebServerFrame_HistoryEvent{HistoryEvent: event.Event}, - }, true - }, "history_event") - - forward(c, c.sm.SubscribeSettingsSync, func(event session.Tagged[*gatewayv2.SettingsSyncEvent]) (*gatewayv2.WebServerFrame, bool) { - return &gatewayv2.WebServerFrame{ - AgentId: event.AgentID, - Payload: &gatewayv2.WebServerFrame_SettingsEvent{SettingsEvent: event.Event}, - }, true - }, "settings_event") - - forward(c, c.sm.SubscribeTerminalEvents, func(event session.Tagged[*gatewayv2.TerminalEvent]) (*gatewayv2.WebServerFrame, bool) { - if !shared.TerminalEventAllowed(c.sm.AgentView(event.AgentID), event.Event) || !c.terminalInterest.ShouldForward(event.Event) { - return nil, false - } - return &gatewayv2.WebServerFrame{ - AgentId: event.AgentID, - Payload: &gatewayv2.WebServerFrame_TerminalEvent{TerminalEvent: event.Event}, - }, true - }, "terminal_event") - - forward(c, c.sm.SubscribeSftpEvents, func(event session.Tagged[*gatewayv2.SftpEvent]) (*gatewayv2.WebServerFrame, bool) { - if !c.sm.WebSshTerminalEnabled(event.AgentID) { - return nil, false - } - return &gatewayv2.WebServerFrame{ - AgentId: event.AgentID, - Payload: &gatewayv2.WebServerFrame_SftpEvent{SftpEvent: event.Event}, - }, true - }, "sftp_event") - - forward(c, c.sm.SubscribeChatQueueEvents, func(event session.Tagged[*gatewayv2.ChatQueueEvent]) (*gatewayv2.WebServerFrame, bool) { - return &gatewayv2.WebServerFrame{ - AgentId: event.AgentID, - Payload: &gatewayv2.WebServerFrame_ChatQueueEvent{ChatQueueEvent: event.Event}, - }, true - }, "chat_queue_event") - - forward(c, c.sm.SubscribeChatActivity, func(event session.ConversationActivityEvent) (*gatewayv2.WebServerFrame, bool) { - return &gatewayv2.WebServerFrame{ - AgentId: event.AgentID, - Payload: &gatewayv2.WebServerFrame_ChatActivity{ChatActivity: chatActivityEvent(event)}, - }, true - }, "chat_activity") - - forward(c, c.sm.SubscribeTunnelState, func(event session.Tagged[*gatewayv2.TunnelStateSnapshot]) (*gatewayv2.WebServerFrame, bool) { - return &gatewayv2.WebServerFrame{ - AgentId: event.AgentID, - Payload: &gatewayv2.WebServerFrame_TunnelState{TunnelState: event.Event}, - }, true - }, "tunnel_state") - - forward(c, c.sm.SubscribeManagedProcessState, func(event session.Tagged[*gatewayv2.ManagedProcessSnapshot]) (*gatewayv2.WebServerFrame, bool) { - return &gatewayv2.WebServerFrame{ - AgentId: event.AgentID, - Payload: &gatewayv2.WebServerFrame_ProcessState{ProcessState: event.Event}, - }, true - }, "process_state") - - forward(c, c.sm.SubscribeStatus, func(status session.Tagged[session.Status]) (*gatewayv2.WebServerFrame, bool) { - return &gatewayv2.WebServerFrame{ - AgentId: status.AgentID, - Payload: &gatewayv2.WebServerFrame_Status{Status: statusEvent(status.Event)}, - }, true - }, "status") -} - -// forward 是可掉帧广播转发的共用骨架:subscribe 建立订阅(cleanup 随 goroutine 退出执行), -// build 过滤并构造帧;掉帧跳过继续,其他写错误结束转发。 -func forward[T any]( - c *browserConn, - subscribe func() (<-chan T, func()), - build func(T) (*gatewayv2.WebServerFrame, bool), - kind string, -) { - events, cleanup := subscribe() - go func() { - defer cleanup() - for { - select { - case <-c.done: - return - case event, ok := <-events: - if !ok { - return - } - frame, send := build(event) - if !send { - continue - } - if err := c.send(wscore.FrameData, kind, frame); err != nil { - if errors.Is(err, wscore.ErrWriteQueueFull) { - continue - } - return - } - } - } - }() -} - -// replaySnapshots 在鉴权后把当前状态画到新连接上,免去首轮轮询。 -// 逐个在线 Agent 回放各自快照并打标;每个在线 Agent 补发一条状态帧(目录渲染), -// 所有回放帧均携带明确来源,不再发送无标的单 Agent 兼容帧。 -func (c *browserConn) replaySnapshots() { - for _, agentID := range c.sm.ConnectedAgentIDs() { - view := c.sm.AgentView(agentID) - // 终端会话快照:以 created 事件逐条回放(按各 Agent 的门控独立判定)。 - if shared.TerminalFeaturesEnabled(view) { - for _, terminalSession := range view.TerminalSessionSnapshot("") { - if !shared.TerminalSessionAllowed(view, terminalSession) { - continue - } - if err := c.send(wscore.FrameData, "terminal_event", &gatewayv2.WebServerFrame{ - AgentId: agentID, - Payload: &gatewayv2.WebServerFrame_TerminalEvent{ - TerminalEvent: &gatewayv2.TerminalEvent{ - Kind: "created", - SessionId: terminalSession.GetId(), - ProjectPathKey: terminalSession.GetProjectPathKey(), - Session: terminalSession, - }, - }, - }); err != nil { - return - } - } - } - // 进程快照按 Agent 回放。 - if processSnapshot := c.sm.ManagedProcessSnapshotCached(agentID); processSnapshot != nil { - _ = c.send(wscore.FrameData, "process_state", &gatewayv2.WebServerFrame{ - AgentId: agentID, - Payload: &gatewayv2.WebServerFrame_ProcessState{ProcessState: processSnapshot}, - }) - } - // 每 Agent 一条状态帧:新客户端由此渲染 Agent 目录,无需先发 agent_list。 - agentStatus := c.sm.Status(agentID) - _ = c.send(wscore.FrameData, "status", &gatewayv2.WebServerFrame{ - AgentId: agentID, - Payload: &gatewayv2.WebServerFrame_Status{Status: statusEvent(agentStatus)}, - }) - } - - // 每个已登记 Agent 回放自己的隧道快照并打标;离线 Agent 也保留其隧道目录。 - for _, status := range c.sm.AgentStatuses() { - agentID := strings.TrimSpace(status.AgentID) - if agentID == "" { - continue - } - _ = c.send(wscore.FrameData, "tunnel_state", &gatewayv2.WebServerFrame{ - AgentId: agentID, - Payload: &gatewayv2.WebServerFrame_TunnelState{ - TunnelState: c.sm.TunnelStateSnapshot(agentID), - }, - }) - } -} diff --git a/crates/agent-gateway/internal/protocol/pbws/browser_local.go b/crates/agent-gateway/internal/protocol/pbws/browser_local.go deleted file mode 100644 index 6c1169eb1..000000000 --- a/crates/agent-gateway/internal/protocol/pbws/browser_local.go +++ /dev/null @@ -1,311 +0,0 @@ -package pbws - -import ( - "context" - "sort" - "strings" - "time" - - "github.com/google/uuid" - - "github.com/liveagent/agent-gateway/internal/chatcmd" - "github.com/liveagent/agent-gateway/internal/config" - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" - "github.com/liveagent/agent-gateway/internal/session" - "github.com/liveagent/agent-gateway/internal/transport/wscore" -) - -// 由网关状态直接应答(或由网关编排)的本地操作;chat 编排复用 internal/chatcmd。 - -// handleStatusGet 处理指定 Agent 的 status.get。 -func (c *browserConn) handleStatusGet(requestID, agentID string) { - status := c.sm.Status(agentID) - _ = c.send(wscore.FrameResponse, "status", &gatewayv2.WebServerFrame{ - RequestId: requestID, - AgentId: status.AgentID, - Payload: &gatewayv2.WebServerFrame_Status{ - Status: statusEvent(status), - }, - }) -} - -// handleAgentList 返回全部已登记 Agent 的状态目录(含离线项);持久化目录补全 -// 网关重启后尚未重连的 Agent,供 webui 渲染完整列表。 -func (c *browserConn) handleAgentList(requestID string) { - registered, err := c.srv.tokens.Registered() - if err != nil { - _ = c.sendLocalError(requestID, "agent directory unavailable") - return - } - registeredByID := make(map[string]string, len(registered)) - for _, entry := range registered { - registeredByID[entry.AgentID] = entry.Name - } - - statuses := c.sm.AgentStatuses() - known := make(map[string]bool, len(statuses)) - agents := make([]*gatewayv2.StatusEvent, 0, len(statuses)+len(registered)) - for _, status := range statuses { - known[status.AgentID] = true - event := statusEvent(status) - event.Name = registeredByID[status.AgentID] - agents = append(agents, event) - } - for _, entry := range registered { - if !known[entry.AgentID] { - agents = append(agents, &gatewayv2.StatusEvent{AgentId: entry.AgentID, Name: entry.Name}) - } - } - sort.Slice(agents, func(i, j int) bool { return agents[i].GetAgentId() < agents[j].GetAgentId() }) - _ = c.send(wscore.FrameResponse, "agent_list", &gatewayv2.WebServerFrame{ - RequestId: requestID, - Payload: &gatewayv2.WebServerFrame_AgentList{ - AgentList: &gatewayv2.AgentListResult{Agents: agents}, - }, - }) -} - -// handleChatPrepare 处理 chat.prepare:探活/唤醒目标桌面运行时后返回与 status_get -// 同构的状态(客户端共享一个状态归一化器)。 -func (c *browserConn) handleChatPrepare(requestID, agentID string, _ *gatewayv2.ChatPrepareRequest) { - if c.sm.IsOnline(agentID) && !c.sm.ChatIngressV1Ready(agentID) { - status := c.sm.Status(agentID) - _ = c.send(wscore.FrameControl, "status", &gatewayv2.WebServerFrame{ - RequestId: requestID, - AgentId: status.AgentID, - Payload: &gatewayv2.WebServerFrame_Status{ - Status: statusEvent(status), - }, - }) - return - } - ctx, cancel := context.WithTimeout(context.Background(), chatcmd.PrepareTimeout(c.cfg)) - defer cancel() - if err := chatcmd.ProbeRuntime(ctx, c.sm, agentID); err != nil { - _ = c.sendLocalError(requestID, errorMessage(err)) - return - } - status := c.sm.Status(agentID) - // 响应走控制队列,避免被数据积压饿死。 - _ = c.send(wscore.FrameControl, "status", &gatewayv2.WebServerFrame{ - RequestId: requestID, - AgentId: status.AgentID, - Payload: &gatewayv2.WebServerFrame_Status{ - Status: statusEvent(status), - }, - }) -} - -// handleChatActivities 处理 chat.activities:仅由网关状态应答,桌面端离线时亦可用。 -func (c *browserConn) handleChatActivities(requestID string) { - activities := c.sm.ActiveConversationActivities() - running := make([]*gatewayv2.ChatRunActivity, 0, len(activities)) - for _, activity := range activities { - running = append(running, chatRunActivityListItem(activity)) - } - _ = c.send(wscore.FrameResponse, "chat_activities", &gatewayv2.WebServerFrame{ - RequestId: requestID, - Payload: &gatewayv2.WebServerFrame_ChatActivities{ - ChatActivities: &gatewayv2.ChatActivitiesResult{RunningConversations: running}, - }, - }) -} - -// handleChatCommand 处理 chat.command:submit / edit_resend 经网关编排 -// (去重、接受即回执、命令更新观察、启动看门狗、投递),cancel 单独处理。 -// agentID 是已由分派层校验过的显式目标 Agent。 -func (c *browserConn) handleChatCommand(requestID, agentID string, cmd *gatewayv2.ChatCommandRequest) { - commandType := strings.TrimSpace(cmd.GetType()) - body := chatcmd.RequestBodyFromProto(cmd.GetRequest()) - baseMessageRef := chatcmd.MessageRefFromProto(cmd.GetBaseMessageRef()) - - switch commandType { - case "chat.submit": - baseMessageRef = nil - case "chat.edit_resend": - if baseMessageRef == nil { - _ = c.sendLocalError(requestID, "base_message_ref is required") - return - } - if err := chatcmd.ValidateMessageRef(baseMessageRef); err != nil { - _ = c.sendLocalError(requestID, err.Error()) - return - } - case "chat.cancel": - c.handleChatCancel(requestID, agentID, cmd.GetCancel()) - return - default: - _ = c.sendLocalError(requestID, "unsupported chat command") - return - } - - if err := chatcmd.NormalizeRequestBody(&body); err != nil { - _ = c.sendLocalError(requestID, err.Error()) - return - } - - if existing, ok := c.sm.LookupChatCommand(agentID, body.ClientRequestID); ok { - c.respondChatCommandDeduped(requestID, existing) - return - } - - if !c.sm.IsOnline(agentID) { - _ = c.sendLocalError(requestID, "agent offline") - return - } - probeCtx, probeCancel := context.WithTimeout( - context.Background(), chatcmd.PrepareTimeout(c.cfg), - ) - probeErr := chatcmd.ProbeRuntimeForCommand(probeCtx, c.sm, agentID) - probeCancel() - if probeErr != nil { - _ = c.sendLocalError(requestID, errorMessage(probeErr)) - return - } - - runID := "chat-command-" + uuid.NewString() - start := c.sm.StartChatCommand( - agentID, - runID, - body.ConversationID, - body.Workdir, - body.ClientRequestID, - chatcmd.BuildAcceptedCommandPayloads(body, baseMessageRef), - ) - if start.Deduped { - c.respondChatCommandDeduped(requestID, start) - return - } - updates, cleanupWatch := c.sm.WatchChatCommand(start.AgentID, start.RunID) - - _ = c.sendChatCommandAccepted(requestID, start) - - go c.forwardChatCommandUpdates(updates, cleanupWatch) - go chatcmd.DispatchAcceptedCommand( - context.Background(), c.cfg, c.sm, agentID, cleanupWatch, start, body, baseMessageRef, chatcmd.NewTraceID(), - ) -} - -// respondChatCommandDeduped 用既有运行应答重复的 client_request_id 并转发其(回放的) -// 前置阶段更新;观察流由看门狗窗口兜底关闭。 -func (c *browserConn) respondChatCommandDeduped(requestID string, start session.ChatCommandStart) { - updates, cleanupWatch := c.sm.WatchChatCommand(start.AgentID, start.RunID) - _ = c.sendChatCommandAccepted(requestID, start) - go c.forwardChatCommandUpdates(updates, cleanupWatch) - cleanupChatCommandWatchAfter(c.cfg, cleanupWatch) -} - -func (c *browserConn) sendChatCommandAccepted(requestID string, start session.ChatCommandStart) error { - // 接受回执延迟敏感,走控制队列。 - return c.send(wscore.FrameControl, "chat_accepted", &gatewayv2.WebServerFrame{ - RequestId: requestID, - AgentId: start.AgentID, - Payload: &gatewayv2.WebServerFrame_ChatAccepted{ - ChatAccepted: &gatewayv2.ChatCommandAccepted{ - RunId: start.RunID, - ConversationId: start.ConversationID, - AcceptedSeq: start.AcceptedSeq, - Deduped: start.Deduped, - }, - }, - }) -} - -// forwardChatCommandUpdates 把前置阶段结果(bound / queued_in_gui / failed)推给 -// 发起命令的连接(走控制队列)。 -func (c *browserConn) forwardChatCommandUpdates( - updates <-chan session.ChatCommandUpdate, - cleanup func(), -) { - if cleanup != nil { - defer cleanup() - } - for { - select { - case <-c.done: - return - case update, ok := <-updates: - if !ok { - return - } - if err := c.send(wscore.FrameControl, "chat_command_update", &gatewayv2.WebServerFrame{ - AgentId: update.AgentID, - Payload: &gatewayv2.WebServerFrame_ChatCommandUpdate{ - ChatCommandUpdate: chatCommandUpdate(update), - }, - }); err != nil { - return - } - } - } -} - -// cleanupChatCommandWatchAfter 为去重提交的更新观察流设兜底关闭窗口 -// (AfterFunc 不占 goroutine,cleanup 幂等)。 -func cleanupChatCommandWatchAfter(cfg *config.Config, cleanup func()) { - if cleanup == nil { - return - } - timeout := chatcmd.StartTimeout(cfg) + chatcmd.RenderStartTimeout(cfg) - if timeout <= 0 { - timeout = 15 * time.Second - } - time.AfterFunc(timeout, cleanup) -} - -const chatCancelWatchdogTimeout = 15 * time.Second - -// handleChatCancel 处理 chat.cancel。取消只作用于请求显式声明的 Agent, -// 即使其他 Agent 恰好有同名 conversation_id 也不会被跨 Agent 取消。 -func (c *browserConn) handleChatCancel(requestID, agentID string, cancelReq *gatewayv2.CancelChatRequest) { - conversationID := strings.TrimSpace(cancelReq.GetConversationId()) - if conversationID == "" { - _ = c.sendLocalError(requestID, "conversation_id is required") - return - } - if !c.sm.IsOnline(agentID) { - _ = c.sendLocalError(requestID, "agent offline") - return - } - - // 不终结运行:活动状态翻为 cancelling,以桌面端终态信号为准,超时由看门狗强制收尾。 - runID, active := c.sm.MarkConversationCancelling(agentID, conversationID, strings.TrimSpace(cancelReq.GetRunId())) - if !active { - _ = c.sendChatCancelResult(requestID, true, "", conversationID) - return - } - - ctx, cancel := context.WithTimeout(context.Background(), c.srv.writeTimeout()) - defer cancel() - - if err := c.sm.SendToAgentContext(ctx, agentID, &gatewayv2.GatewayEnvelope{ - RequestId: runID, - Timestamp: time.Now().Unix(), - Payload: chatcmd.BuildCancelCommandPayload(conversationID), - }); err != nil { - _ = c.sendLocalError(requestID, errorMessage(err)) - return - } - - go watchChatCancel(c.sm, agentID, runID) - _ = c.sendChatCancelResult(requestID, true, runID, conversationID) -} - -func (c *browserConn) sendChatCancelResult(requestID string, ok bool, runID, conversationID string) error { - return c.send(wscore.FrameResponse, "chat_cancelled", &gatewayv2.WebServerFrame{ - RequestId: requestID, - Payload: &gatewayv2.WebServerFrame_ChatCancelled{ - ChatCancelled: &gatewayv2.ChatCancelResult{ - Ok: ok, - RunId: runID, - ConversationId: conversationID, - }, - }, - }) -} - -func watchChatCancel(sm *session.Manager, agentID string, runID string) { - time.Sleep(chatCancelWatchdogTimeout) - sm.ForceFinishRun(agentID, runID, "cancelled", "cancel_timeout", - "The desktop runtime did not confirm the cancellation in time.") -} diff --git a/crates/agent-gateway/internal/protocol/pbws/browser_relay.go b/crates/agent-gateway/internal/protocol/pbws/browser_relay.go deleted file mode 100644 index 9717503b0..000000000 --- a/crates/agent-gateway/internal/protocol/pbws/browser_relay.go +++ /dev/null @@ -1,73 +0,0 @@ -package pbws - -import ( - "context" - "strings" - "time" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" - "github.com/liveagent/agent-gateway/internal/protocol/shared" - "github.com/liveagent/agent-gateway/internal/transport/wscore" -) - -// handleAgentRequest 直通转发一条浏览器构造的 GatewayEnvelope:白名单/限额校验 → -// request_id 按连接命名空间化 → 经 session 层等待关联响应 → list 类共享后处理 → -// 还原 request_id 回送。载荷在浏览器与 Agent 之间保持 proto 直通。 -// agentID 是已由分派层校验过的显式目标 Agent。 -func (c *browserConn) handleAgentRequest(requestID, agentID string, env *gatewayv2.GatewayEnvelope) { - if requestID == "" { - _ = c.sendLocalError(requestID, "request id is required") - return - } - view := c.sm.AgentView(agentID) - if err := vetAgentRequest(view, env); err != nil { - _ = c.sendLocalError(requestID, err.Error()) - return - } - - // 命名空间化:多标签页共享一个桌面端,透传 id 必须按连接隔离;回程剥离前缀还原。 - agentRequestID := c.idPrefix + requestID - env.RequestId = agentRequestID - if env.GetTimestamp() == 0 { - env.Timestamp = time.Now().Unix() - } - - ctx, cancel := context.WithTimeout(context.Background(), c.srv.requestTimeout()) - defer cancel() - go func() { - select { - case <-c.done: - cancel() - case <-ctx.Done(): - } - }() - - response, err := c.sm.AwaitUnaryResponse(ctx, agentID, agentRequestID, env) - if err != nil { - _ = c.sendLocalError(requestID, errorMessage(err)) - return - } - - // list 类终端响应的合并/过滤与兴趣登记(按目标 Agent 视图执行共享域逻辑)。 - if terminalResp := response.GetTerminalResponse(); terminalResp != nil { - req := env.GetTerminalRequest() - finalized := shared.FinalizeTerminalResponse( - view, - c.terminalInterest, - strings.TrimSpace(req.GetAction()), - strings.TrimSpace(req.GetProjectPathKey()), - terminalResp, - ) - if finalized != terminalResp { - response.Payload = &gatewayv2.AgentEnvelope_TerminalResponse{TerminalResponse: finalized} - } - } - - // 还原关联 id 后原样回送;error=99 臂保留结构化错误码并交由客户端处理。 - response.RequestId = requestID - _ = c.send(wscore.FrameResponse, "agent_response", &gatewayv2.WebServerFrame{ - RequestId: requestID, - AgentId: agentID, - Payload: &gatewayv2.WebServerFrame_AgentResponse{AgentResponse: response}, - }) -} diff --git a/crates/agent-gateway/internal/protocol/pbws/guard.go b/crates/agent-gateway/internal/protocol/pbws/guard.go deleted file mode 100644 index 26edd31a8..000000000 --- a/crates/agent-gateway/internal/protocol/pbws/guard.go +++ /dev/null @@ -1,163 +0,0 @@ -package pbws - -import ( - "errors" - "strings" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" - "github.com/liveagent/agent-gateway/internal/protocol/shared" - "github.com/liveagent/agent-gateway/internal/session" -) - -// 直通白名单与限额校验:本文件明确限定浏览器可发起的操作——未列入白名单的载荷臂(内部推送臂、须走网关编排的 chat_command、ping 等) -// 一律拒绝;功能开关门控与字段限额在转发前施加;list 类响应后处理经 finalize 钩子执行。 - -const ( - maxHistoryListLimit = 200 - defaultHistoryListPage = 1 - defaultHistoryListPageSize = 80 -) - -// vetAgentRequest 校验并(必要时)原地修正一条直通请求;返回错误则拒绝转发,错误信息面向客户端。 -// 门控按目标 Agent 的视图判定(sm 为绑定 agent_id 的只读视图)。 -func vetAgentRequest(sm session.AgentView, env *gatewayv2.GatewayEnvelope) error { - switch payload := env.GetPayload().(type) { - case nil: - return errors.New("agent_request payload is required") - - // ---- 普通直通臂(无门控) ---- - case *gatewayv2.GatewayEnvelope_HistoryList: - clampHistoryList(payload.HistoryList) - return nil - case *gatewayv2.GatewayEnvelope_HistoryGet, - *gatewayv2.GatewayEnvelope_HistoryRename, - *gatewayv2.GatewayEnvelope_HistoryDelete, - *gatewayv2.GatewayEnvelope_HistoryPrefix, - *gatewayv2.GatewayEnvelope_HistoryPin, - *gatewayv2.GatewayEnvelope_HistoryShareGet, - *gatewayv2.GatewayEnvelope_HistoryShareSet, - *gatewayv2.GatewayEnvelope_HistoryWorkdirs, - *gatewayv2.GatewayEnvelope_HistoryBranch, - *gatewayv2.GatewayEnvelope_ProviderList, - *gatewayv2.GatewayEnvelope_ProviderUsage, - *gatewayv2.GatewayEnvelope_ProviderModels, - *gatewayv2.GatewayEnvelope_SettingsGet, - *gatewayv2.GatewayEnvelope_SettingsUpdate, - *gatewayv2.GatewayEnvelope_SettingsResetSshKnownHost, - *gatewayv2.GatewayEnvelope_SkillFilesList, - *gatewayv2.GatewayEnvelope_SkillMetadataRead, - *gatewayv2.GatewayEnvelope_SkillTextRead, - *gatewayv2.GatewayEnvelope_SkillManage, - *gatewayv2.GatewayEnvelope_FileMentionList, - *gatewayv2.GatewayEnvelope_UploadedImagePreview, - *gatewayv2.GatewayEnvelope_MemoryManage, - *gatewayv2.GatewayEnvelope_CronManage, - *gatewayv2.GatewayEnvelope_FsRoots, - *gatewayv2.GatewayEnvelope_FsListDirs, - *gatewayv2.GatewayEnvelope_FsCreateProjectFolder, - *gatewayv2.GatewayEnvelope_FsList, - *gatewayv2.GatewayEnvelope_FsWriteText, - *gatewayv2.GatewayEnvelope_FsCreateDir, - *gatewayv2.GatewayEnvelope_FsRename, - *gatewayv2.GatewayEnvelope_FsDelete, - *gatewayv2.GatewayEnvelope_FsReadEditableText, - *gatewayv2.GatewayEnvelope_FsReadWorkspaceImage, - *gatewayv2.GatewayEnvelope_ChatQueue: - return nil - case *gatewayv2.GatewayEnvelope_ChatFileOpen: - return vetChatFileOpen(payload.ChatFileOpen) - - // ---- 带功能门控 / 限额的直通臂 ---- - case *gatewayv2.GatewayEnvelope_GitRequest: - action := strings.TrimSpace(payload.GitRequest.GetAction()) - if gitActionIsWrite(action) && !sm.WebGitEnabled() { - return errors.New("web git is disabled in desktop Remote settings") - } - return nil - case *gatewayv2.GatewayEnvelope_TerminalRequest: - req := payload.TerminalRequest - action := strings.TrimSpace(req.GetAction()) - if !shared.TerminalRequestAllowed(sm, action, strings.TrimSpace(req.GetSessionId())) { - return errors.New(shared.TerminalPermissionError(action)) - } - return nil - case *gatewayv2.GatewayEnvelope_SftpRequest: - if !sm.WebSshTerminalEnabled() { - return errors.New("web SSH SFTP is disabled in desktop Remote settings") - } - return nil - case *gatewayv2.GatewayEnvelope_TunnelMutation: - if !sm.WebTunnelsEnabled() { - return errors.New("web tunnels are disabled in desktop Remote settings") - } - return nil - case *gatewayv2.GatewayEnvelope_ManagedProcessRequest: - req := payload.ManagedProcessRequest - action := strings.TrimSpace(req.GetAction()) - if strings.TrimSpace(req.GetProcessId()) == "" && action != "clear" && action != "snapshot" { - return errors.New("process_id is required") - } - return nil - - // ---- 明确拒绝的臂 ---- - default: - // 含 chat_command(须走网关编排)、ping(探活由网关发起)、upload_readable_files - // (走 HTTP 上传)、history_share_resolve(公共分享端点专用)及网关内部推送臂。 - return errors.New("unsupported agent_request payload") - } -} - -func vetChatFileOpen(req *gatewayv2.ChatFileOpenRequest) error { - if req == nil || strings.TrimSpace(req.GetConversationId()) == "" || len(req.GetConversationId()) > 256 { - return errors.New("conversation is unavailable") - } - if strings.TrimSpace(req.GetWorkdir()) == "" || strings.TrimSpace(req.GetPath()) == "" { - return errors.New("linked file request is incomplete") - } - if len(req.GetWorkdir()) > 32768 || len(req.GetPath()) > 32768 { - return errors.New("linked file request is too large") - } - switch strings.TrimSpace(req.GetSource()) { - case "absolute", "relative", "file-url": - default: - return errors.New("linked file source is invalid") - } - if (req.Line != nil && req.GetLine() == 0) || - (req.EndLine != nil && req.GetEndLine() == 0) || - (req.Column != nil && req.GetColumn() == 0) { - return errors.New("linked file location is invalid") - } - if req.Line == nil && (req.EndLine != nil || req.Column != nil) { - return errors.New("linked file location is invalid") - } - if req.Line != nil && req.EndLine != nil && req.GetEndLine() < req.GetLine() { - return errors.New("linked file location is invalid") - } - return nil -} - -// gitActionIsWrite 判定 git 直通请求是否为写操作:写操作受桌面端 Remote 设置 -// enable_web_git 门控,读操作(status/log/diff 等)始终放行。 -func gitActionIsWrite(action string) bool { - switch action { - case "clone", "clone_start", "clone_cancel", "clone_dismiss", "init", "switch_branch", "create_branch", "stage", "stage_all", "unstage", "unstage_all", "discard", "discard_all", "add_to_gitignore", "commit", "fetch", "pull", "set_remote", "push", "delete_branch", "rename_branch", "stash_push", "stash_pop": - return true - default: - return false - } -} - -// clampHistoryList 施加历史列表的分页默认值与上限。 -func clampHistoryList(req *gatewayv2.HistoryListRequest) { - if req == nil { - return - } - if req.GetPage() <= 0 { - req.Page = defaultHistoryListPage - } - if req.GetPageSize() <= 0 { - req.PageSize = defaultHistoryListPageSize - } else if req.GetPageSize() > maxHistoryListLimit { - req.PageSize = maxHistoryListLimit - } -} diff --git a/crates/agent-gateway/internal/protocol/pbws/guard_test.go b/crates/agent-gateway/internal/protocol/pbws/guard_test.go deleted file mode 100644 index 9e4a6dc0f..000000000 --- a/crates/agent-gateway/internal/protocol/pbws/guard_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package pbws - -import ( - "testing" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" - "github.com/liveagent/agent-gateway/internal/session" -) - -func TestVetAgentRequestAllowsProviderUsage(t *testing.T) { - env := &gatewayv2.GatewayEnvelope{ - Payload: &gatewayv2.GatewayEnvelope_ProviderUsage{ - ProviderUsage: &gatewayv2.ProviderUsageRequest{ - ProviderId: "provider-1", - Refresh: true, - }, - }, - } - - if err := vetAgentRequest(session.AgentView{}, env); err != nil { - t.Fatalf("vetAgentRequest() error = %v", err) - } -} - -func TestVetAgentRequestAllowsValidChatFileOpen(t *testing.T) { - line := uint32(12) - column := uint32(4) - env := &gatewayv2.GatewayEnvelope{ - Payload: &gatewayv2.GatewayEnvelope_ChatFileOpen{ - ChatFileOpen: &gatewayv2.ChatFileOpenRequest{ - ConversationId: "conversation-1", - Workdir: `C:\work`, - Path: `src\a.ts`, - Source: "relative", - Line: &line, - Column: &column, - }, - }, - } - - if err := vetAgentRequest(session.AgentView{}, env); err != nil { - t.Fatalf("vetAgentRequest() error = %v", err) - } -} - -func TestVetAgentRequestRejectsMalformedChatFileOpen(t *testing.T) { - zero := uint32(0) - tests := []*gatewayv2.ChatFileOpenRequest{ - nil, - {ConversationId: "", Workdir: "/work", Path: "a.ts", Source: "relative"}, - {ConversationId: "conversation-1", Workdir: "/work", Path: "a.ts", Source: "javascript"}, - {ConversationId: "conversation-1", Workdir: "/work", Path: "a.ts", Source: "relative", Line: &zero}, - } - for _, request := range tests { - env := &gatewayv2.GatewayEnvelope{ - Payload: &gatewayv2.GatewayEnvelope_ChatFileOpen{ChatFileOpen: request}, - } - if err := vetAgentRequest(session.AgentView{}, env); err == nil { - t.Fatalf("vetAgentRequest(%+v) unexpectedly succeeded", request) - } - } -} diff --git a/crates/agent-gateway/internal/protocol/pbws/handshake.go b/crates/agent-gateway/internal/protocol/pbws/handshake.go deleted file mode 100644 index 69169344f..000000000 --- a/crates/agent-gateway/internal/protocol/pbws/handshake.go +++ /dev/null @@ -1,81 +0,0 @@ -package pbws - -import ( - "strings" - "time" - - "github.com/gorilla/websocket" - - "github.com/liveagent/agent-gateway/internal/auth" - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -// helloVerdict 是握手校验结果;ok=false 时 message 面向客户端。 -type helloVerdict struct { - ok bool - message string -} - -// vetHello 校验 ClientHello 的协议版本、角色与浏览器凭证。 -// 角色-凭证绑定:浏览器角色只接受网关 token;Agent 角色必须声明 agent_id。 -// Agent 凭证由 authenticateAgentHello 在存储锁内只校验一次,主链路与终端 -// 数据链路复用该入口。凭证失败统一报 "unauthorized",防止枚举 Agent ID。 -func (s *Server) vetHello(hello *gatewayv2.ClientHello, wantRole gatewayv2.ClientRole) helloVerdict { - if hello == nil { - return helloVerdict{message: "hello frame is required"} - } - if hello.GetProtocolVersion() != ProtocolVersion { - return helloVerdict{message: "unsupported protocol version"} - } - role := hello.GetRole() - // hello 缺省角色按端点预期补齐(路径已可区分);显式错误角色拒绝,防止 agent 帧被当浏览器帧处理。 - if role != gatewayv2.ClientRole_CLIENT_ROLE_UNSPECIFIED && role != wantRole { - return helloVerdict{message: "unexpected client role"} - } - switch wantRole { - case gatewayv2.ClientRole_CLIENT_ROLE_AGENT: - if strings.TrimSpace(hello.GetAgentId()) == "" { - return helloVerdict{message: "agent_id is required"} - } - default: - if !auth.ValidateToken(hello.GetToken(), s.cfg.Token) { - return helloVerdict{message: "unauthorized"} - } - } - return helloVerdict{ok: true} -} - -// authenticateAgentHello 在 Store 内完成唯一一次独立 Token 查询、共享 Token 判定、 -// 自动登记及按 Agent 凭证纪元快照。调用方必须在注册传输时校验返回纪元。 -func (s *Server) authenticateAgentHello(hello *gatewayv2.ClientHello) (uint64, error) { - return s.tokens.AuthenticateAndRegister( - hello.GetAgentId(), - hello.GetToken(), - auth.ValidateToken(hello.GetToken(), s.cfg.Token), - ) -} - -// serverHello 构造握手应答;sessionID 仅 agent 角色使用,maxMessageBytes 按链路 -// 报告实际读限额(各链路收紧后不再统一)。 -func (s *Server) serverHello(ok bool, message string, sessionID string, maxMessageBytes int64) *gatewayv2.ServerHello { - return &gatewayv2.ServerHello{ - Ok: ok, - Message: strings.TrimSpace(message), - SessionId: strings.TrimSpace(sessionID), - ServerTime: time.Now().Unix(), - HeartbeatPeriodSeconds: uint32(s.heartbeatPeriod() / time.Second), - MaxMessageBytes: uint64(maxMessageBytes), - Capabilities: []string{gatewayv2.ChatIngressV1Capability}, - } -} - -// closeUnauthorized 以鉴权失败码关闭连接(调用方已写出失败 hello)。 -func closeUnauthorized(conn *websocket.Conn, timeout time.Duration) { - deadline := time.Now().Add(timeout) - _ = conn.WriteControl( - websocket.CloseMessage, - websocket.FormatCloseMessage(closeCodeUnauthorized, "unauthorized"), - deadline, - ) - _ = conn.Close() -} diff --git a/crates/agent-gateway/internal/protocol/pbws/handshake_test.go b/crates/agent-gateway/internal/protocol/pbws/handshake_test.go deleted file mode 100644 index a960aa124..000000000 --- a/crates/agent-gateway/internal/protocol/pbws/handshake_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package pbws - -import ( - "testing" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -func TestServerHelloAdvertisesChatIngressV1(t *testing.T) { - hello := (&Server{}).serverHello(true, "", "session-1", 1024) - - if got := hello.GetCapabilities(); len(got) != 1 || got[0] != gatewayv2.ChatIngressV1Capability { - t.Fatalf("server hello capabilities = %v, want [%q]", got, gatewayv2.ChatIngressV1Capability) - } -} diff --git a/crates/agent-gateway/internal/protocol/pbws/seam.go b/crates/agent-gateway/internal/protocol/pbws/seam.go deleted file mode 100644 index 5d73e268f..000000000 --- a/crates/agent-gateway/internal/protocol/pbws/seam.go +++ /dev/null @@ -1,91 +0,0 @@ -package pbws - -import ( - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" - "github.com/liveagent/agent-gateway/internal/session" -) - -// session 层 Go seam 类型到 v2 proto 消息的映射。 - -// statusEvent 映射 session.Status。 -func statusEvent(status session.Status) *gatewayv2.StatusEvent { - return &gatewayv2.StatusEvent{ - Online: status.Online, - AgentReady: status.AgentReady, - ChatRuntimeReady: status.ChatRuntimeReady, - AgentId: status.AgentID, - AgentVersion: status.AgentVersion, - SessionId: status.SessionID, - ConnectedSince: status.ConnectedSince, - LastHeartbeat: status.LastHeartbeat, - RuntimeState: status.RuntimeState, - RuntimeLastHeartbeat: status.RuntimeLastHeartbeat, - RuntimeWorkerId: status.RuntimeWorkerID, - RuntimeVisible: status.RuntimeVisible, - RuntimeActiveRunCount: status.RuntimeActiveRunCount, - } -} - -// chatActivityEvent 映射 session.ConversationActivityEvent。 -func chatActivityEvent(event session.ConversationActivityEvent) *gatewayv2.ChatActivityEvent { - return &gatewayv2.ChatActivityEvent{ - ConversationId: event.ConversationID, - RunId: event.RunID, - ClientRequestId: event.ClientRequestID, - Running: event.Running, - State: event.State, - Workdir: event.Workdir, - UpdatedAtMs: event.UpdatedAt.UnixMilli(), - } -} - -// chatRunActivity 映射 session.RunActivity,并保留空值字段语义。 -func chatRunActivity(activity *session.RunActivity) *gatewayv2.ChatRunActivity { - if activity == nil { - return nil - } - return &gatewayv2.ChatRunActivity{ - RunId: activity.RunID, - State: activity.State, - StartedSeq: activity.StartedSeq, - UpdatedAtMs: activity.UpdatedAt.UnixMilli(), - ToolStatus: activity.ToolStatus, - ToolStatusIsCompaction: activity.ToolStatusIsCompaction, - ClientRequestId: activity.ClientRequestID, - } -} - -// chatRunActivityListItem 映射运行中会话列表项(含会话与工作目录)。 -func chatRunActivityListItem(activity session.RunActivity) *gatewayv2.ChatRunActivity { - item := chatRunActivity(&activity) - item.ConversationId = activity.ConversationID - item.Workdir = activity.Workdir - return item -} - -// chatRunSnapshot 映射 session.RunSnapshot。 -func chatRunSnapshot(snapshot *session.RunSnapshot) *gatewayv2.ChatRunSnapshot { - if snapshot == nil { - return nil - } - return &gatewayv2.ChatRunSnapshot{ - RunId: snapshot.RunID, - Revision: snapshot.Revision, - EntriesJson: snapshot.EntriesJSON, - ToolStatus: snapshot.ToolStatus, - ToolStatusIsCompaction: snapshot.ToolStatusIsCompaction, - AsOfSeq: snapshot.AsOfSeq, - } -} - -// chatCommandUpdate 映射 session.ChatCommandUpdate。 -func chatCommandUpdate(update session.ChatCommandUpdate) *gatewayv2.ChatCommandUpdate { - return &gatewayv2.ChatCommandUpdate{ - RunId: update.RunID, - ClientRequestId: update.ClientRequestID, - ConversationId: update.ConversationID, - Phase: update.Phase, - ErrorCode: update.ErrorCode, - Message: update.Message, - } -} diff --git a/crates/agent-gateway/internal/protocol/pbws/server.go b/crates/agent-gateway/internal/protocol/pbws/server.go deleted file mode 100644 index f49b172c8..000000000 --- a/crates/agent-gateway/internal/protocol/pbws/server.go +++ /dev/null @@ -1,176 +0,0 @@ -// Package pbws 实现 v2 统一线协议(WebSocket+Protobuf)服务端的三条链路(见 proto/v2/gateway_ws.proto): -// /ws/v2 浏览器直通、/ws/v2/agent 桌面端信封流、/ws/v2/terminal 终端数据面。 -// 本包只做帧编解码、鉴权握手、直通白名单与事件扇出;会话状态复用 session, -// 传输运行时复用 wscore,跨协议域逻辑复用 shared 与 chatcmd。 -package pbws - -import ( - "context" - "errors" - "net/http" - "sync/atomic" - "time" - - "github.com/gorilla/websocket" - "google.golang.org/protobuf/proto" - - "github.com/liveagent/agent-gateway/internal/auth/agenttoken" - "github.com/liveagent/agent-gateway/internal/config" - "github.com/liveagent/agent-gateway/internal/protocol/shared" - "github.com/liveagent/agent-gateway/internal/session" -) - -// Subprotocol 是 v2 的 WebSocket 子协议名;服务端必须回显,否则浏览器主动断开握手。 -const Subprotocol = "liveagent.v2.pb" - -// ProtocolVersion 是本包实现的协议版本号(ClientHello.protocol_version)。 -const ProtocolVersion = 2 - -// closeCodeUnauthorized 是鉴权失败时的自定义关闭码(4000-4999 为应用保留段)。 -const closeCodeUnauthorized = 4401 - -// 加固上限:单个连接(bug 或凭证被盗)的损害必须被限制在该连接内,不能升级成 -// 全网关故障。并发连接上限已改为配置项(config.DefaultMax*Connections 为默认值), -// 以下为每连接粒度的固定值,与总台数无关。 -const ( - // 每浏览器连接在途派发上限:直通请求可在 AwaitUnaryResponse 上阻塞至 - // requestTimeout(默认 2 分钟),无上限时重试风暴即 goroutine 泄漏。 - maxInflightDispatches = 16 - - // 浏览器链路入站限速(帧/秒):正常 webui 远低于此,不误伤。 - browserInboundFramesPerSecond = 100 - browserInboundBurst = 200 - browserRateLimitMaxViolations = 3 - - // 读限额按链路收紧:浏览器控制帧合法场景仅数百 KB,64 MiB 上限是内存放大 - // 攻击面;Agent 链路维持配置值(上传需要)。 - browserReadLimit = 4 << 20 - terminalBrowserReadLimit = 1 << 20 - terminalAgentReadLimit = 16 << 20 -) - -// Server 聚合三条 v2 链路的依赖,由 http 路由层构造一次、复用于全部连接。 -type Server struct { - cfg *config.Config - sm *session.Manager - // tokens 是每 Agent 凭证存储;生产网关启动时始终非 nil,nil 仅供轻量测试构造。 - tokens *agenttoken.Store - - agentConns atomic.Int64 - browserConns atomic.Int64 - terminalConns atomic.Int64 -} - -// NewServer 构造 v2 协议服务端;tokens 传 nil 仅用于不涉及持久化的单元测试。 -func NewServer(cfg *config.Config, sm *session.Manager, tokens *agenttoken.Store) *Server { - return &Server{cfg: cfg, sm: sm, tokens: tokens} -} - -// acquireConnSlot 在升级前占用一个连接槽位;超限返回 false(调用方回 503)。 -func acquireConnSlot(counter *atomic.Int64, limit int64) (func(), bool) { - if counter.Add(1) > limit { - counter.Add(-1) - return nil, false - } - released := &atomic.Bool{} - return func() { - if released.CompareAndSwap(false, true) { - counter.Add(-1) - } - }, true -} - -// 三条链路的并发连接上限:取配置值,未配置回落默认(Load 已兜底,此处再防 -// 测试直接构造 Config 的零值)。 -func (s *Server) maxAgentConnections() int64 { - if s.cfg != nil && s.cfg.MaxAgentConnections > 0 { - return int64(s.cfg.MaxAgentConnections) - } - return config.DefaultMaxAgentConnections -} - -func (s *Server) maxBrowserConnections() int64 { - if s.cfg != nil && s.cfg.MaxBrowserConnections > 0 { - return int64(s.cfg.MaxBrowserConnections) - } - return config.DefaultMaxBrowserConnections -} - -func (s *Server) maxTerminalConnections() int64 { - if s.cfg != nil && s.cfg.MaxTerminalConnections > 0 { - return int64(s.cfg.MaxTerminalConnections) - } - return config.DefaultMaxTerminalConnections -} - -func (s *Server) upgrader() websocket.Upgrader { - return websocket.Upgrader{ - Subprotocols: []string{Subprotocol}, - CheckOrigin: func(r *http.Request) bool { - return shared.OriginAllowed(r) - }, - } -} - -// readLimit 复用 MaxMessageBytes 配置(历史命名保留,语义为消息大小上限)。 -func (s *Server) readLimit() int64 { - if s.cfg != nil && s.cfg.MaxMessageBytes > 0 { - return int64(s.cfg.MaxMessageBytes) - } - return int64(config.DefaultMaxMessageBytes) -} - -func (s *Server) heartbeatPeriod() time.Duration { - if s.cfg != nil && s.cfg.WebSocketHeartbeatPeriod > 0 { - return s.cfg.WebSocketHeartbeatPeriod - } - return 15 * time.Second -} - -func (s *Server) writeTimeout() time.Duration { - if s.cfg != nil && s.cfg.WebSocketWriteTimeout > 0 { - return s.cfg.WebSocketWriteTimeout - } - return 10 * time.Second -} - -func (s *Server) requestTimeout() time.Duration { - if s.cfg != nil && s.cfg.RequestTimeout > 0 { - return s.cfg.RequestTimeout - } - return 2 * time.Minute -} - -// errorMessage 把内部错误映射为对客户端友好的信息。 -func errorMessage(err error) string { - if err == nil { - return "request failed" - } - if errors.Is(err, context.DeadlineExceeded) { - return "request timed out" - } - if errors.Is(err, context.Canceled) { - return "request canceled" - } - if errors.Is(err, session.ErrAgentOffline) { - return "agent offline" - } - return err.Error() -} - -// writeDirectMessage 在写泵启动前(握手阶段)直接写出一条二进制帧。 -func writeDirectMessage(conn *websocket.Conn, timeout time.Duration, msg proto.Message) error { - data, err := proto.Marshal(msg) - if err != nil { - return err - } - if timeout > 0 { - if err := conn.SetWriteDeadline(time.Now().Add(timeout)); err != nil { - return err - } - defer func() { - _ = conn.SetWriteDeadline(time.Time{}) - }() - } - return conn.WriteMessage(websocket.BinaryMessage, data) -} diff --git a/crates/agent-gateway/internal/protocol/pbws/terminal_conn.go b/crates/agent-gateway/internal/protocol/pbws/terminal_conn.go deleted file mode 100644 index 37b19dc32..000000000 --- a/crates/agent-gateway/internal/protocol/pbws/terminal_conn.go +++ /dev/null @@ -1,470 +0,0 @@ -package pbws - -import ( - "context" - "errors" - "net/http" - "strings" - "sync" - "time" - - "github.com/gorilla/websocket" - "google.golang.org/protobuf/proto" - - "github.com/liveagent/agent-gateway/internal/auth/agenttoken" - "github.com/liveagent/agent-gateway/internal/observability" - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" - "github.com/liveagent/agent-gateway/internal/protocol/shared" - "github.com/liveagent/agent-gateway/internal/session" -) - -// 终端数据面(/ws/v2/terminal):两端共用一条路径,角色由 hello 区分。浏览器 -// 角色维护 attach/detach 订阅并校验 input/resize;Agent 角色登记数据通道并广播 -// 入站帧。两端直接传输 proto TerminalStreamFrame。 - -const terminalWriteQueueSize = 1024 - -// TerminalHandler 返回 /ws/v2/terminal 的 HTTP 处理器。 -func (s *Server) TerminalHandler() http.Handler { - upgrader := s.upgrader() - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - release, ok := acquireConnSlot(&s.terminalConns, s.maxTerminalConnections()) - if !ok { - http.Error(w, "too many terminal connections", http.StatusServiceUnavailable) - return - } - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - release() - return - } - defer release() - // 角色未知前先按浏览器(更严)限额;hello 判定为 Agent 角色后再放宽。 - conn.SetReadLimit(terminalBrowserReadLimit) - s.serveTerminal(conn) - }) -} - -func (s *Server) serveTerminal(conn *websocket.Conn) { - defer func() { _ = conn.Close() }() - - frame, ok := readTerminalFrame(conn) - if !ok { - return - } - hello := frame.GetHello() - // 终端路径两端共用:按 hello 声明的角色校验(未声明按浏览器处理)。 - wantRole := hello.GetRole() - if wantRole == gatewayv2.ClientRole_CLIENT_ROLE_UNSPECIFIED { - wantRole = gatewayv2.ClientRole_CLIENT_ROLE_BROWSER - } - verdict := s.vetHello(hello, wantRole) - if verdict.ok && strings.TrimSpace(hello.GetAgentId()) == "" { - verdict = helloVerdict{message: "agent_id is required"} - } - if !verdict.ok { - _ = writeDirectMessage(conn, s.writeTimeout(), &gatewayv2.TerminalServerFrame{ - Payload: &gatewayv2.TerminalServerFrame_Hello{ - Hello: s.serverHello(false, verdict.message, "", terminalBrowserReadLimit), - }, - }) - closeUnauthorized(conn, s.writeTimeout()) - return - } - - boundAgentID := strings.TrimSpace(hello.GetAgentId()) - var ( - authEpoch uint64 - toAgent chan *gatewayv2.TerminalStreamFrame - agentCtx context.Context - cancel context.CancelFunc - cleanup func() - ) - if wantRole == gatewayv2.ClientRole_CLIENT_ROLE_AGENT { - var err error - authEpoch, err = s.authenticateAgentHello(hello) - if err != nil { - message := "gateway storage unavailable" - if errors.Is(err, agenttoken.ErrUnauthorized) { - message = "unauthorized" - } - _ = writeDirectMessage(conn, s.writeTimeout(), &gatewayv2.TerminalServerFrame{ - Payload: &gatewayv2.TerminalServerFrame_Hello{ - Hello: s.serverHello(false, message, "", terminalAgentReadLimit), - }, - }) - if errors.Is(err, agenttoken.ErrUnauthorized) { - closeUnauthorized(conn, s.writeTimeout()) - } - return - } - - agentCtx, cancel = context.WithCancel(context.Background()) - defer cancel() - go func() { - <-agentCtx.Done() - _ = conn.Close() - }() - toAgent = make(chan *gatewayv2.TerminalStreamFrame, 4096) - var registered bool - cleanup, registered = s.sm.RegisterTerminalStreamToAgentIfCurrent( - boundAgentID, - toAgent, - cancel, - func() bool { - return s.tokens.AuthenticationCurrent(boundAgentID, authEpoch) - }, - ) - if !registered { - _ = writeDirectMessage(conn, s.writeTimeout(), &gatewayv2.TerminalServerFrame{ - Payload: &gatewayv2.TerminalServerFrame_Hello{ - Hello: s.serverHello(false, "unauthorized", "", terminalAgentReadLimit), - }, - }) - closeUnauthorized(conn, s.writeTimeout()) - return - } - defer cleanup() - } - - // 角色确定后按链路调整读限额并在 hello 中报告实际值。 - roleReadLimit := int64(terminalBrowserReadLimit) - if wantRole == gatewayv2.ClientRole_CLIENT_ROLE_AGENT { - roleReadLimit = terminalAgentReadLimit - } - conn.SetReadLimit(roleReadLimit) - if err := writeDirectMessage(conn, s.writeTimeout(), &gatewayv2.TerminalServerFrame{ - Payload: &gatewayv2.TerminalServerFrame_Hello{ - Hello: s.serverHello(true, "", "", roleReadLimit), - }, - }); err != nil { - return - } - - if wantRole == gatewayv2.ClientRole_CLIENT_ROLE_AGENT { - observability.Usage.V2TerminalConnectsTotal.Add(1) - s.serveTerminalAgent(conn, boundAgentID, agentCtx, cancel, toAgent) - return - } - observability.Usage.V2TerminalConnectsTotal.Add(1) - s.serveTerminalBrowser(conn, boundAgentID) -} - -func readTerminalFrame(conn *websocket.Conn) (*gatewayv2.TerminalClientFrame, bool) { - for { - messageType, data, err := conn.ReadMessage() - if err != nil { - return nil, false - } - if messageType != websocket.BinaryMessage { - continue - } - var frame gatewayv2.TerminalClientFrame - if err := proto.Unmarshal(data, &frame); err != nil { - return nil, false - } - return &frame, true - } -} - -// --------------------------------------------------------------------------- -// Agent 角色 -// --------------------------------------------------------------------------- - -func (s *Server) serveTerminalAgent( - conn *websocket.Conn, - agentID string, - ctx context.Context, - cancel context.CancelFunc, - toAgent <-chan *gatewayv2.TerminalStreamFrame, -) { - go func() { - defer cancel() - for { - select { - case <-ctx.Done(): - return - case frame := <-toAgent: - if frame == nil { - continue - } - if !s.writeTerminalFrame(conn, frame) { - return - } - } - } - }() - - for { - frame, ok := readTerminalFrame(conn) - if !ok { - cancel() - return - } - if streamFrame := frame.GetFrame(); streamFrame != nil { - s.sm.BroadcastTerminalStreamFrame(agentID, streamFrame) - } - } -} - -func (s *Server) writeTerminalFrame(conn *websocket.Conn, frame *gatewayv2.TerminalStreamFrame) bool { - data, err := proto.Marshal(&gatewayv2.TerminalServerFrame{ - Payload: &gatewayv2.TerminalServerFrame_Frame{Frame: frame}, - }) - if err != nil { - return false - } - if timeout := s.writeTimeout(); timeout > 0 { - if err := conn.SetWriteDeadline(time.Now().Add(timeout)); err != nil { - return false - } - defer func() { _ = conn.SetWriteDeadline(time.Time{}) }() - } - return conn.WriteMessage(websocket.BinaryMessage, data) == nil -} - -// --------------------------------------------------------------------------- -// 浏览器角色 -// --------------------------------------------------------------------------- - -type terminalBrowserConn struct { - srv *Server - sm *session.Manager - conn *websocket.Conn - // agentID 是连接通过 hello.agent_id 显式绑定的目标 Agent;出站按它路由, - // 入站只放行同源帧。 - agentID string - - out chan []byte - done chan struct{} - once sync.Once - - mu sync.RWMutex - attached map[string]struct{} - streams map[string]struct{} -} - -func (s *Server) serveTerminalBrowser(conn *websocket.Conn, agentID string) { - c := &terminalBrowserConn{ - srv: s, - sm: s.sm, - conn: conn, - agentID: agentID, - out: make(chan []byte, terminalWriteQueueSize), - done: make(chan struct{}), - attached: make(map[string]struct{}), - streams: make(map[string]struct{}), - } - defer c.close() - - go c.writeLoop() - c.startForwarder() - - for { - frame, ok := readTerminalFrame(conn) - if !ok { - return - } - streamFrame := frame.GetFrame() - if streamFrame == nil { - continue - } - c.handleFrame(streamFrame) - } -} - -func (c *terminalBrowserConn) handleFrame(frame *gatewayv2.TerminalStreamFrame) { - kind := strings.TrimSpace(frame.GetKind()) - if !c.frameAllowed(frame) { - c.enqueueFrame(terminalErrorFrame(frame, shared.TerminalPermissionError(kind))) - return - } - - switch kind { - case "attach": - c.remember(frame.GetSessionId(), frame.GetStreamId()) - case "detach": - c.forget(frame.GetSessionId(), frame.GetStreamId()) - case "input", "resize": - if !c.isAttached(frame.GetSessionId()) { - c.enqueueFrame(terminalErrorFrame(frame, "terminal stream is not attached")) - return - } - default: - c.enqueueFrame(terminalErrorFrame(frame, "unsupported terminal stream frame")) - return - } - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if err := c.sm.SendTerminalFrameToAgent(ctx, c.agentID, frame); err != nil { - message := "desktop agent is offline" - if !errors.Is(err, session.ErrAgentOffline) { - message = err.Error() - } - c.enqueueFrame(terminalErrorFrame(frame, message)) - } -} - -func (c *terminalBrowserConn) frameAllowed(frame *gatewayv2.TerminalStreamFrame) bool { - if frame == nil { - return false - } - view := c.sm.AgentView(c.agentID) - sessionID := strings.TrimSpace(frame.GetSessionId()) - switch view.TerminalSessionKind(sessionID) { - case "ssh": - return view.WebSshTerminalEnabled() - case "local": - return view.WebTerminalEnabled() - default: - return view.WebTerminalEnabled() || view.WebSshTerminalEnabled() - } -} - -func (c *terminalBrowserConn) startForwarder() { - frames, cleanup := c.sm.SubscribeTerminalStreamFrames() - go func() { - defer cleanup() - for { - select { - case <-c.done: - return - case frame, ok := <-frames: - if !ok { - c.close() - return - } - if !c.fromBoundAgent(frame.AgentID) || !c.shouldForward(frame.Event) { - continue - } - c.enqueueFrame(frame.Event) - } - } - }() -} - -// fromBoundAgent 判断帧来源是否为本连接显式绑定的 Agent。 -func (c *terminalBrowserConn) fromBoundAgent(frameAgentID string) bool { - return frameAgentID == c.agentID -} - -func (c *terminalBrowserConn) shouldForward(frame *gatewayv2.TerminalStreamFrame) bool { - if frame == nil { - return false - } - kind := strings.TrimSpace(frame.GetKind()) - if kind == "snapshot" || kind == "error" { - return c.knowsStream(frame.GetStreamId()) - } - if kind != "output" { - return false - } - return c.isAttached(frame.GetSessionId()) -} - -func (c *terminalBrowserConn) remember(sessionID string, streamID string) { - sessionID = strings.TrimSpace(sessionID) - streamID = strings.TrimSpace(streamID) - if sessionID == "" && streamID == "" { - return - } - c.mu.Lock() - if sessionID != "" { - c.attached[sessionID] = struct{}{} - } - if streamID != "" { - c.streams[streamID] = struct{}{} - } - c.mu.Unlock() -} - -func (c *terminalBrowserConn) forget(sessionID string, streamID string) { - sessionID = strings.TrimSpace(sessionID) - streamID = strings.TrimSpace(streamID) - if sessionID == "" && streamID == "" { - return - } - c.mu.Lock() - if sessionID != "" { - delete(c.attached, sessionID) - } - if streamID != "" { - delete(c.streams, streamID) - } - c.mu.Unlock() -} - -func (c *terminalBrowserConn) isAttached(sessionID string) bool { - sessionID = strings.TrimSpace(sessionID) - if sessionID == "" { - return false - } - c.mu.RLock() - _, ok := c.attached[sessionID] - c.mu.RUnlock() - return ok -} - -func (c *terminalBrowserConn) knowsStream(streamID string) bool { - streamID = strings.TrimSpace(streamID) - if streamID == "" { - return false - } - c.mu.RLock() - _, ok := c.streams[streamID] - c.mu.RUnlock() - return ok -} - -// enqueueFrame 在队列满时关闭连接(终端输出无可容忍的丢帧语义, -// 客户端重连后 attach + snapshot 恢复)。 -func (c *terminalBrowserConn) enqueueFrame(frame *gatewayv2.TerminalStreamFrame) { - data, err := proto.Marshal(&gatewayv2.TerminalServerFrame{ - Payload: &gatewayv2.TerminalServerFrame_Frame{Frame: frame}, - }) - if err != nil { - return - } - select { - case <-c.done: - case c.out <- data: - default: - c.close() - } -} - -func (c *terminalBrowserConn) writeLoop() { - for { - select { - case <-c.done: - return - case payload := <-c.out: - if timeout := c.srv.writeTimeout(); timeout > 0 { - _ = c.conn.SetWriteDeadline(time.Now().Add(timeout)) - } - if err := c.conn.WriteMessage(websocket.BinaryMessage, payload); err != nil { - c.close() - return - } - _ = c.conn.SetWriteDeadline(time.Time{}) - } - } -} - -func (c *terminalBrowserConn) close() { - c.once.Do(func() { - close(c.done) - _ = c.conn.Close() - }) -} - -func terminalErrorFrame(source *gatewayv2.TerminalStreamFrame, message string) *gatewayv2.TerminalStreamFrame { - return &gatewayv2.TerminalStreamFrame{ - Kind: "error", - StreamId: strings.TrimSpace(source.GetStreamId()), - SessionId: strings.TrimSpace(source.GetSessionId()), - ProjectPathKey: strings.TrimSpace(source.GetProjectPathKey()), - Error: strings.TrimSpace(message), - } -} diff --git a/crates/agent-gateway/internal/protocol/shared/origin.go b/crates/agent-gateway/internal/protocol/shared/origin.go deleted file mode 100644 index e770e62f2..000000000 --- a/crates/agent-gateway/internal/protocol/shared/origin.go +++ /dev/null @@ -1,98 +0,0 @@ -package shared - -import ( - "net" - "net/http" - "net/url" - "strings" -) - -// OriginAllowed 是 WebSocket 升级的同源校验(v2 共用,自 internal/server/http_origin.go -// 平移,行为不变):无 Origin 头(非浏览器)、同源、或两端均为回环地址(本机开发)时放行。 -func OriginAllowed(r *http.Request) bool { - origin := strings.TrimSpace(r.Header.Get("Origin")) - if origin == "" { - return true - } - parsed, err := url.Parse(origin) - if err != nil || parsed.Scheme == "" || parsed.Host == "" { - return false - } - requestURL := requestURLForOriginCheck(r) - if requestURL == nil { - return false - } - if sameOrigin(parsed, requestURL) { - return true - } - originHost := strings.TrimSpace(parsed.Hostname()) - requestHost := strings.TrimSpace(requestURL.Hostname()) - if originHost == "" || requestHost == "" { - return false - } - return isLoopbackHost(originHost) && isLoopbackHost(requestHost) -} - -func requestURLForOriginCheck(r *http.Request) *url.URL { - if r == nil { - return nil - } - scheme := strings.TrimSpace(r.Header.Get("X-Forwarded-Proto")) - if scheme == "" { - if r.TLS != nil { - scheme = "https" - } else { - scheme = "http" - } - } - scheme = strings.ToLower(strings.TrimSpace(strings.Split(scheme, ",")[0])) - switch scheme { - case "http", "https": - default: - return nil - } - host := strings.TrimSpace(r.Host) - if host == "" { - return nil - } - return &url.URL{Scheme: scheme, Host: host} -} - -func sameOrigin(a *url.URL, b *url.URL) bool { - if a == nil || b == nil { - return false - } - if !strings.EqualFold(strings.TrimSpace(a.Scheme), strings.TrimSpace(b.Scheme)) { - return false - } - if !strings.EqualFold(strings.TrimSpace(a.Hostname()), strings.TrimSpace(b.Hostname())) { - return false - } - return originPort(a) == originPort(b) -} - -func originPort(u *url.URL) string { - if u == nil { - return "" - } - if port := strings.TrimSpace(u.Port()); port != "" { - return port - } - switch strings.ToLower(strings.TrimSpace(u.Scheme)) { - case "http", "ws": - return "80" - case "https", "wss": - return "443" - default: - return "" - } -} - -func isLoopbackHost(host string) bool { - host = strings.Trim(strings.ToLower(strings.TrimSpace(host)), "[]") - if host == "localhost" { - return true - } - ip := net.ParseIP(host) - return ip != nil && ip.IsLoopback() -} diff --git a/crates/agent-gateway/internal/protocol/shared/terminal_interest.go b/crates/agent-gateway/internal/protocol/shared/terminal_interest.go deleted file mode 100644 index a28c13216..000000000 --- a/crates/agent-gateway/internal/protocol/shared/terminal_interest.go +++ /dev/null @@ -1,88 +0,0 @@ -// Package shared 存放 v2 协议层共用、且不属于 session 或 wscore 的连接级构件。 -package shared - -import ( - "strings" - "sync" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -// TerminalInterestTracker 记录单条连接的终端会话/项目关注集,决定事件是否转发: -// 元数据事件广播,原始输出仅推给显式附着的连接。沿用既有实现,行为不变;并发安全。 -type TerminalInterestTracker struct { - mu sync.RWMutex - projects map[string]struct{} - sessions map[string]struct{} -} - -// NewTerminalInterestTracker 构造空关注集。 -func NewTerminalInterestTracker() *TerminalInterestTracker { - return &TerminalInterestTracker{ - projects: make(map[string]struct{}), - sessions: make(map[string]struct{}), - } -} - -// RememberProject 登记对某项目终端列表的关注。 -func (t *TerminalInterestTracker) RememberProject(projectPathKey string) { - projectPathKey = strings.TrimSpace(projectPathKey) - if projectPathKey == "" { - return - } - t.mu.Lock() - t.projects[projectPathKey] = struct{}{} - t.mu.Unlock() -} - -// RememberSession 登记对某终端会话(及其项目)的附着。 -func (t *TerminalInterestTracker) RememberSession(sessionID string, projectPathKey string) { - sessionID = strings.TrimSpace(sessionID) - projectPathKey = strings.TrimSpace(projectPathKey) - if sessionID == "" && projectPathKey == "" { - return - } - t.mu.Lock() - if sessionID != "" { - t.sessions[sessionID] = struct{}{} - } - if projectPathKey != "" { - t.projects[projectPathKey] = struct{}{} - } - t.mu.Unlock() -} - -// Forget 解除会话附着;仅给出项目键时解除项目关注。 -func (t *TerminalInterestTracker) Forget(sessionID string, projectPathKey string) { - sessionID = strings.TrimSpace(sessionID) - projectPathKey = strings.TrimSpace(projectPathKey) - t.mu.Lock() - if sessionID != "" { - delete(t.sessions, sessionID) - } - if sessionID == "" && projectPathKey != "" { - delete(t.projects, projectPathKey) - } - t.mu.Unlock() -} - -// ShouldForward 判定终端事件是否应推送给本连接。 -func (t *TerminalInterestTracker) ShouldForward(event *gatewayv2.TerminalEvent) bool { - if event == nil { - return false - } - sessionID := strings.TrimSpace(event.GetSessionId()) - projectPathKey := strings.TrimSpace(event.GetProjectPathKey()) - kind := strings.TrimSpace(event.GetKind()) - - // 元数据变化广播给所有标签页保持列表新鲜;原始输出只推给显式附着的连接。 - if kind != "output" { - return sessionID != "" || projectPathKey != "" - } - - t.mu.RLock() - _, sessionSubscribed := t.sessions[sessionID] - t.mu.RUnlock() - - return sessionID != "" && sessionSubscribed -} diff --git a/crates/agent-gateway/internal/protocol/shared/terminal_interest_test.go b/crates/agent-gateway/internal/protocol/shared/terminal_interest_test.go deleted file mode 100644 index a10071603..000000000 --- a/crates/agent-gateway/internal/protocol/shared/terminal_interest_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package shared - -import ( - "testing" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -func TestTerminalInterestTrackerFiltersOutputBySession(t *testing.T) { - t.Parallel() - - tracker := NewTerminalInterestTracker() - outputEvent := &gatewayv2.TerminalEvent{ - Kind: "output", - SessionId: "session-1", - ProjectPathKey: "project-1", - } - metadataEvent := &gatewayv2.TerminalEvent{ - Kind: "created", - SessionId: "session-1", - ProjectPathKey: "project-1", - } - - if tracker.ShouldForward(outputEvent) { - t.Fatal("output should not forward before a session is attached") - } - if !tracker.ShouldForward(metadataEvent) { - t.Fatal("metadata should forward so project/session lists stay fresh") - } - - tracker.RememberSession("session-1", "project-1") - if !tracker.ShouldForward(outputEvent) { - t.Fatal("output should forward after attaching the session") - } - - tracker.Forget("session-1", "project-1") - if tracker.ShouldForward(outputEvent) { - t.Fatal("output should stop forwarding after detaching the session") - } -} diff --git a/crates/agent-gateway/internal/protocol/shared/terminal_relay.go b/crates/agent-gateway/internal/protocol/shared/terminal_relay.go deleted file mode 100644 index b3927432a..000000000 --- a/crates/agent-gateway/internal/protocol/shared/terminal_relay.go +++ /dev/null @@ -1,201 +0,0 @@ -package shared - -import ( - "strings" - - "google.golang.org/protobuf/proto" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" - "github.com/liveagent/agent-gateway/internal/session" -) - -// 终端转发域逻辑(权限门控、列表合并/过滤、兴趣登记):沿用既有处理器实现、行为不变, -// v2 共用以免各自复制一份门控规则。 - -// TerminalFeaturesEnabled 判断任一 Web 终端功能是否开启。 -func TerminalFeaturesEnabled(sm session.AgentView) bool { - return sm.WebTerminalEnabled() || sm.WebSshTerminalEnabled() -} - -// TerminalSessionAllowed 按会话类型(local/ssh)检查其对 Web 端是否可见。 -func TerminalSessionAllowed(sm session.AgentView, ts *gatewayv2.TerminalSession) bool { - if ts == nil { - return false - } - if TerminalSessionKindOf(ts) == "ssh" { - return sm.WebSshTerminalEnabled() - } - return sm.WebTerminalEnabled() -} - -// TerminalSessionKindOf 归一化会话类型(空值按 local 处理)。 -func TerminalSessionKindOf(ts *gatewayv2.TerminalSession) string { - if strings.TrimSpace(ts.GetKind()) == "ssh" { - return "ssh" - } - return "local" -} - -// TerminalEventAllowed 判断终端事件是否允许推送给 Web 端。 -func TerminalEventAllowed(sm session.AgentView, event *gatewayv2.TerminalEvent) bool { - if event == nil { - return false - } - if strings.TrimSpace(event.GetKind()) == "ssh_tabs_updated" { - return sm.WebSshTerminalEnabled() - } - // 端口转发事件只随 SSH 权限走:session 缓存缺失时不得回落到本地终端门。 - if strings.TrimSpace(event.GetKind()) == "ssh_local_forward" { - return sm.WebSshTerminalEnabled() - } - if ts := event.GetSession(); ts != nil { - return TerminalSessionAllowed(sm, ts) - } - sessionID := strings.TrimSpace(event.GetSessionId()) - if sessionID != "" && sm.TerminalSessionKind(sessionID) == "ssh" { - return sm.WebSshTerminalEnabled() - } - return sm.WebTerminalEnabled() -} - -// TerminalRequestAllowed 按动作与目标会话类型做权限门控。 -func TerminalRequestAllowed(sm session.AgentView, action string, sessionID string) bool { - switch action { - case "create_ssh", "answer_ssh_prompt", "cancel_ssh_prompt", "ssh_latency", - "ssh_reconnect", "ssh_tabs_list", "ssh_tab_open", "ssh_tab_close", - "ssh_local_forward_start", "ssh_local_forward_list", "ssh_local_forward_stop", - "ssh_local_forward_check_port": - return sm.WebSshTerminalEnabled() - case "list", "close_project": - return sm.WebTerminalEnabled() || sm.WebSshTerminalEnabled() - case "rename", "close": - if sm.TerminalSessionKind(sessionID) == "ssh" { - return sm.WebSshTerminalEnabled() - } - return sm.WebTerminalEnabled() - default: - return sm.WebTerminalEnabled() - } -} - -// TerminalPermissionError 返回动作被拒时的用户可读错误信息。 -func TerminalPermissionError(action string) string { - switch action { - case "create_ssh", "answer_ssh_prompt", "cancel_ssh_prompt", "ssh_latency", - "ssh_reconnect", "ssh_tabs_list", "ssh_tab_open", "ssh_tab_close", - "ssh_local_forward_start", "ssh_local_forward_list", "ssh_local_forward_stop", - "ssh_local_forward_check_port": - return "web SSH terminal is disabled in desktop Remote settings" - default: - return "web terminal is disabled in desktop Remote settings" - } -} - -// FinalizeTerminalResponse 统一后处理:list 结果与缓存快照合并、快照回写、按权限过滤并登记项目兴趣。 -func FinalizeTerminalResponse( - sm session.AgentView, - tracker *TerminalInterestTracker, - action string, - projectPathKey string, - resp *gatewayv2.TerminalResponse, -) *gatewayv2.TerminalResponse { - resp = MergeTerminalListWithCachedSnapshot(sm, action, projectPathKey, resp) - sm.ApplyTerminalResponseSnapshot(action, projectPathKey, resp) - resp = FilterTerminalResponseForPermissions(sm, action, resp) - RememberTerminalInterest(tracker, action, projectPathKey, resp) - return resp -} - -// MergeTerminalListWithCachedSnapshot 把桌面端 list 响应缺失、网关缓存尚存的会话并入结果 -// (桌面端重连早期列表可能不全)。 -func MergeTerminalListWithCachedSnapshot( - sm session.AgentView, - action string, - projectPathKey string, - resp *gatewayv2.TerminalResponse, -) *gatewayv2.TerminalResponse { - if resp == nil || strings.TrimSpace(action) != "list" { - return resp - } - cachedSessions := sm.TerminalSessionSnapshot(projectPathKey) - if len(cachedSessions) == 0 { - return resp - } - seen := make(map[string]struct{}, len(resp.GetSessions())) - for _, ts := range resp.GetSessions() { - id := strings.TrimSpace(ts.GetId()) - if id != "" { - seen[id] = struct{}{} - } - } - merged := make([]*gatewayv2.TerminalSession, 0, len(resp.GetSessions())+len(cachedSessions)) - merged = append(merged, resp.GetSessions()...) - changed := false - for _, ts := range cachedSessions { - id := strings.TrimSpace(ts.GetId()) - if id == "" { - continue - } - if _, ok := seen[id]; ok { - continue - } - seen[id] = struct{}{} - merged = append(merged, ts) - changed = true - } - if !changed { - return resp - } - clone := proto.CloneOf(resp) - clone.Sessions = merged - return clone -} - -// FilterTerminalResponseForPermissions 过滤掉 Web 端无权看到的会话。 -func FilterTerminalResponseForPermissions( - sm session.AgentView, - action string, - resp *gatewayv2.TerminalResponse, -) *gatewayv2.TerminalResponse { - if resp == nil || action != "list" { - return resp - } - filtered := make([]*gatewayv2.TerminalSession, 0, len(resp.GetSessions())) - changed := false - for _, ts := range resp.GetSessions() { - if TerminalSessionAllowed(sm, ts) { - filtered = append(filtered, ts) - } else { - changed = true - } - } - if !changed { - return resp - } - clone := proto.CloneOf(resp) - clone.Sessions = filtered - return clone -} - -// RememberTerminalInterest 在列表/创建类动作后登记项目兴趣,供终端事件过滤使用。 -func RememberTerminalInterest( - tracker *TerminalInterestTracker, - action string, - projectPathKey string, - resp *gatewayv2.TerminalResponse, -) { - if tracker == nil { - return - } - projectPathKey = strings.TrimSpace(projectPathKey) - if respSession := resp.GetSession(); respSession != nil { - if projectPathKey == "" { - projectPathKey = strings.TrimSpace(respSession.GetProjectPathKey()) - } - } - - switch action { - case "list", "create", "create_ssh", "answer_ssh_prompt", "close_project": - tracker.RememberProject(projectPathKey) - } -} diff --git a/crates/agent-gateway/internal/protocol/shared/terminal_relay_test.go b/crates/agent-gateway/internal/protocol/shared/terminal_relay_test.go deleted file mode 100644 index fa9ec500a..000000000 --- a/crates/agent-gateway/internal/protocol/shared/terminal_relay_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package shared - -import ( - "testing" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" - "github.com/liveagent/agent-gateway/internal/session" -) - -// SSH 本地端口转发动作必须走 SSH 终端门:漏加白名单会落入 default 分支、 -// 被本地终端开关(enableWebTerminal)错误放行/拦截。 -func TestTerminalRequestAllowedGatesSshLocalForwardOnSshToggle(t *testing.T) { - actions := []string{ - "ssh_local_forward_start", - "ssh_local_forward_list", - "ssh_local_forward_stop", - "ssh_local_forward_check_port", - } - - manager := session.NewManager() - manager.ApplySettingsJSON("test-agent", `{"remote":{"enableWebTerminal":true,"enableWebSshTerminal":false}}`) - view := manager.AgentView("test-agent") - for _, action := range actions { - if TerminalRequestAllowed(view, action, "") { - t.Fatalf("action %q must not be allowed by the local terminal toggle", action) - } - if TerminalPermissionError(action) != "web SSH terminal is disabled in desktop Remote settings" { - t.Fatalf("action %q must report the SSH permission error", action) - } - } - - manager.ApplySettingsJSON("test-agent", `{"remote":{"enableWebTerminal":false,"enableWebSshTerminal":true}}`) - for _, action := range actions { - if !TerminalRequestAllowed(view, action, "") { - t.Fatalf("action %q must be allowed once web SSH terminal is enabled", action) - } - } -} - -// 转发事件不携带 session 载荷,门控必须直接按 kind 判 SSH 开关, -// 不得回落到 session 缓存推断出的本地终端门。 -func TestTerminalEventAllowedGatesSshLocalForwardKind(t *testing.T) { - event := &gatewayv2.TerminalEvent{ - Kind: "ssh_local_forward", - SessionId: "ssh-1", - ProjectPathKey: "/project", - } - - manager := session.NewManager() - manager.ApplySettingsJSON("test-agent", `{"remote":{"enableWebTerminal":true,"enableWebSshTerminal":false}}`) - view := manager.AgentView("test-agent") - if TerminalEventAllowed(view, event) { - t.Fatal("ssh_local_forward events must not pass with only the local terminal enabled") - } - - manager.ApplySettingsJSON("test-agent", `{"remote":{"enableWebTerminal":false,"enableWebSshTerminal":true}}`) - if !TerminalEventAllowed(view, event) { - t.Fatal("ssh_local_forward events must pass once web SSH terminal is enabled") - } -} diff --git a/crates/agent-gateway/internal/server/http.go b/crates/agent-gateway/internal/server/http.go deleted file mode 100644 index 38bcbdc68..000000000 --- a/crates/agent-gateway/internal/server/http.go +++ /dev/null @@ -1,225 +0,0 @@ -package server - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "io/fs" - "net/http" - "path" - "strings" - "time" - - "github.com/google/uuid" - gateway "github.com/liveagent/agent-gateway" - "github.com/liveagent/agent-gateway/internal/auth" - "github.com/liveagent/agent-gateway/internal/auth/agenttoken" - "github.com/liveagent/agent-gateway/internal/config" - "github.com/liveagent/agent-gateway/internal/handler" - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" - "github.com/liveagent/agent-gateway/internal/protocol/pbws" - "github.com/liveagent/agent-gateway/internal/session" -) - -// NewHTTPServer 构造 HTTP 路由;生产启动时 tokens 始终是已初始化的 Agent 目录与凭证存储。 -func NewHTTPServer(cfg *config.Config, sm *session.Manager, tokens *agenttoken.Store) http.Handler { - rootMux := http.NewServeMux() - rootMux.HandleFunc("GET /healthz", handler.Health()) - - // v2 统一协议(WebSocket+Protobuf)三链路。 - v2 := pbws.NewServer(cfg, sm, tokens) - rootMux.Handle("/ws/v2", v2.BrowserHandler()) - rootMux.Handle("/ws/v2/agent", v2.AgentHandler()) - rootMux.Handle("/ws/v2/terminal", v2.TerminalHandler()) - - rootMux.HandleFunc("/t/", publicTunnelProxy(sm)) - rootMux.HandleFunc("GET /image-proxy", handler.ImageProxy(cfg.RequestTimeout)) - rootMux.HandleFunc("GET /api/public/history-shares/{token}", publicHistoryShare(cfg, sm)) - - apiMux := http.NewServeMux() - apiMux.HandleFunc("GET /api/status", handler.Status(sm)) - apiMux.HandleFunc("POST /api/files/import", handler.ImportReadableFiles(sm, cfg.RequestTimeout)) - // Agent 目录与凭证管理,仅管理 token 可访问。 - apiMux.HandleFunc("GET /api/agents", handler.ListAgents(sm, tokens)) - apiMux.HandleFunc("POST /api/agents/{id}/token", handler.IssueAgentToken(sm, tokens)) - apiMux.HandleFunc("PATCH /api/agents/{id}", handler.UpdateAgentName(tokens)) - apiMux.HandleFunc("DELETE /api/agents/{id}", handler.DeleteAgent(sm, tokens)) - rootMux.Handle("/api/", auth.HTTPMiddleware(cfg.Token, apiMux)) - - webFS, err := fs.Sub(gateway.WebUIAssets, "web/dist") - if err != nil { - panic(err) - } - indexHTML, err := fs.ReadFile(webFS, "index.html") - if err != nil { - panic(err) - } - fileServer := http.FileServer(http.FS(webFS)) - serveIndex := func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") - w.Header().Set("Content-Type", "text/html; charset=utf-8") - http.ServeContent(w, r, "index.html", time.Time{}, bytes.NewReader(indexHTML)) - } - - rootMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - cleanPath := path.Clean(strings.TrimPrefix(r.URL.Path, "/")) - if cleanPath == "." || cleanPath == "" || cleanPath == "index.html" { - serveIndex(w, r) - return - } - - file, err := webFS.Open(cleanPath) - if err == nil { - if stat, statErr := file.Stat(); statErr == nil && !stat.IsDir() { - _ = file.Close() - if strings.HasPrefix(cleanPath, "assets/") { - w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") - } - fileServer.ServeHTTP(w, r) - return - } - _ = file.Close() - } - - if isWebUIStaticAssetPath(cleanPath) { - http.NotFound(w, r) - return - } - - serveIndex(w, r) - }) - - return rootMux -} - -func isWebUIStaticAssetPath(cleanPath string) bool { - cleanPath = strings.TrimSpace(cleanPath) - if cleanPath == "" || cleanPath == "." || cleanPath == "index.html" { - return false - } - return strings.HasPrefix(cleanPath, "assets/") || path.Ext(cleanPath) != "" -} - -func publicHistoryShare(cfg *config.Config, sm *session.Manager) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - token := strings.TrimSpace(r.PathValue("token")) - if token == "" { - writePublicHistoryShareError(w, http.StatusNotFound, "share not found") - return - } - - timeout := cfg.RequestTimeout - if timeout <= 0 { - timeout = 2 * time.Minute - } - ctx, cancel := context.WithTimeout(r.Context(), timeout) - defer cancel() - - requestID := "public-history-share-" + uuid.NewString() - response, err := resolveHistoryShareAcrossAgents(ctx, sm, requestID, token) - if err != nil { - switch { - case errors.Is(err, session.ErrAgentOffline): - writePublicHistoryShareError(w, http.StatusServiceUnavailable, "agent offline") - case errors.Is(err, context.DeadlineExceeded): - writePublicHistoryShareError(w, http.StatusGatewayTimeout, "request timed out") - default: - writePublicHistoryShareError(w, http.StatusInternalServerError, "share request failed") - } - return - } - if errResp := response.GetError(); errResp != nil { - writePublicHistoryShareError(w, handler.GatewayErrorStatus(errResp), errResp.GetMessage()) - return - } - - share := response.GetHistoryShareResolveResp() - if share == nil { - writePublicHistoryShareError(w, http.StatusBadGateway, "unexpected agent response") - return - } - - writeJSON(w, http.StatusOK, map[string]any{ - "conversation_id": share.GetConversationId(), - "messages_json": share.GetMessagesJson(), - "total_message_count": share.GetTotalMessageCount(), - "conversation": conversationSummaryPayload(share.GetConversation()), - "redact_tool_content": share.GetRedactToolContent(), - }) - } -} - -func writePublicHistoryShareError(w http.ResponseWriter, status int, message string) { - writeJSON(w, status, map[string]any{ - "error": strings.TrimSpace(message), - }) -} - -// resolveHistoryShareAcrossAgents 依次向各在线 Agent 解析公开分享 token(分享属于 -// 某一台桌面端,URL 不携带 agent 信息):首个成功命中者胜;全部未命中返回最后一个 -// 分享层错误(error=99 臂)以保留 not-found 语义。≤10 Agent 且是公开低频端点, -// 串行短超时探询已足够。 -func resolveHistoryShareAcrossAgents( - ctx context.Context, - sm *session.Manager, - requestID string, - token string, -) (*gatewayv2.AgentEnvelope, error) { - agentIDs := sm.ConnectedAgentIDs() - if len(agentIDs) == 0 { - return nil, session.ErrAgentOffline - } - var lastResponse *gatewayv2.AgentEnvelope - var lastErr error - for index, agentID := range agentIDs { - probeCtx := ctx - var cancel context.CancelFunc - if len(agentIDs) > 1 { - // 均分剩余时限,避免第一个无响应的 Agent 吃满整个窗口。 - probeCtx, cancel = context.WithTimeout(ctx, perAgentShareTimeout(ctx, len(agentIDs)-index)) - } - response, err := sm.AwaitUnaryResponse(probeCtx, agentID, requestID+"-"+agentID, &gatewayv2.GatewayEnvelope{ - RequestId: requestID + "-" + agentID, - Timestamp: time.Now().Unix(), - Payload: &gatewayv2.GatewayEnvelope_HistoryShareResolve{ - HistoryShareResolve: &gatewayv2.HistoryShareResolveRequest{ - Token: token, - }, - }, - }) - if cancel != nil { - cancel() - } - if err != nil { - lastErr = err - continue - } - if response.GetError() == nil && response.GetHistoryShareResolveResp() != nil { - return response, nil - } - lastResponse = response - } - if lastResponse != nil { - return lastResponse, nil - } - return nil, lastErr -} - -func perAgentShareTimeout(ctx context.Context, remaining int) time.Duration { - deadline, ok := ctx.Deadline() - if !ok || remaining <= 0 { - return 5 * time.Second - } - share := time.Until(deadline) / time.Duration(remaining) - if share < time.Second { - return time.Second - } - return share -} - -func writeJSON(w http.ResponseWriter, status int, payload any) { - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.WriteHeader(status) - _ = json.NewEncoder(w).Encode(payload) -} diff --git a/crates/agent-gateway/internal/server/http_test.go b/crates/agent-gateway/internal/server/http_test.go deleted file mode 100644 index adaf242b3..000000000 --- a/crates/agent-gateway/internal/server/http_test.go +++ /dev/null @@ -1,286 +0,0 @@ -package server - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" - - "github.com/gorilla/websocket" - "github.com/liveagent/agent-gateway/internal/config" - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" - "github.com/liveagent/agent-gateway/internal/protocol/shared" - "github.com/liveagent/agent-gateway/internal/session" -) - -func TestNewHTTPServerServesRootWithoutRedirect(t *testing.T) { - handler := NewHTTPServer(&config.Config{Token: "dev-token"}, session.NewManager(), nil) - - req := httptest.NewRequest(http.MethodGet, "http://gateway.test/", nil) - rec := httptest.NewRecorder() - handler.ServeHTTP(rec, req) - - if rec.Code != http.StatusOK { - t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code) - } - if location := rec.Header().Get("Location"); location != "" { - t.Fatalf("expected no redirect location, got %q", location) - } - if !strings.Contains(rec.Body.String(), "LiveAgent Gateway") { - t.Fatalf("expected WebUI index.html, got body %q", rec.Body.String()) - } - if cacheControl := rec.Header().Get("Cache-Control"); !strings.Contains(cacheControl, "no-store") { - t.Fatalf("Cache-Control = %q, want no-store for index.html", cacheControl) - } -} - -func TestNewHTTPServerServesSpaFallbackWithoutRedirect(t *testing.T) { - handler := NewHTTPServer(&config.Config{Token: "dev-token"}, session.NewManager(), nil) - - req := httptest.NewRequest(http.MethodGet, "http://gateway.test/history/session-123", nil) - rec := httptest.NewRecorder() - handler.ServeHTTP(rec, req) - - if rec.Code != http.StatusOK { - t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code) - } - if location := rec.Header().Get("Location"); location != "" { - t.Fatalf("expected no redirect location, got %q", location) - } - if !strings.Contains(rec.Body.String(), "LiveAgent Gateway") { - t.Fatalf("expected WebUI index.html, got body %q", rec.Body.String()) - } -} - -func TestNewHTTPServerDoesNotFallbackMissingStaticAssetsToIndex(t *testing.T) { - handler := NewHTTPServer(&config.Config{Token: "dev-token"}, session.NewManager(), nil) - - for _, target := range []string{ - "http://gateway.test/assets/missing-module.js", - "http://gateway.test/assets/missing-style.css", - "http://gateway.test/missing-icon.svg", - } { - req := httptest.NewRequest(http.MethodGet, target, nil) - rec := httptest.NewRecorder() - handler.ServeHTTP(rec, req) - - if rec.Code != http.StatusNotFound { - t.Fatalf("%s status = %d, want %d", target, rec.Code, http.StatusNotFound) - } - if strings.Contains(rec.Body.String(), "LiveAgent Gateway") { - t.Fatalf("%s returned SPA index fallback for a missing static asset", target) - } - if contentType := rec.Header().Get("Content-Type"); strings.Contains(contentType, "text/html") { - t.Fatalf("%s Content-Type = %q, want non-html 404", target, contentType) - } - } -} - -func TestWebSocketRejectsForeignOrigin(t *testing.T) { - ts := httptest.NewServer(NewHTTPServer(&config.Config{Token: "dev-token"}, session.NewManager(), nil)) - defer ts.Close() - - wsURL := "ws" + strings.TrimPrefix(ts.URL, "http") + "/ws/v2" - conn, resp, err := websocket.DefaultDialer.Dial(wsURL, http.Header{ - "Origin": []string{"https://evil.example"}, - }) - if err == nil { - _ = conn.Close() - t.Fatal("expected websocket handshake with foreign origin to be rejected") - } - if resp == nil { - t.Fatalf("expected forbidden websocket response, got nil response and error %v", err) - } - if resp.StatusCode != http.StatusForbidden { - t.Fatalf("websocket response status = %d, want %d", resp.StatusCode, http.StatusForbidden) - } -} - -func TestPublicHistoryShareResolvesWithoutAuthorization(t *testing.T) { - sm := session.NewManager() - sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1") - agentSession := session.NewAgentSession(sm.LatestAuthSnapshot("desktop-agent")) - sm.SetSession(agentSession) - - handler := NewHTTPServer(&config.Config{ - Token: "dev-token", - RequestTimeout: time.Second, - }, sm, nil) - req := httptest.NewRequest(http.MethodGet, "http://gateway.test/api/public/history-shares/share-token", nil) - rec := httptest.NewRecorder() - done := make(chan struct{}) - go func() { - handler.ServeHTTP(rec, req) - close(done) - }() - - var outbound *gatewayv2.GatewayEnvelope - select { - case delivered := <-agentSession.Outbound(): - delivered.Ack(nil) - outbound = delivered.GatewayEnvelope - case <-time.After(time.Second): - t.Fatal("timed out waiting for public share request") - } - shareReq := outbound.GetHistoryShareResolve() - if shareReq == nil { - t.Fatalf("public share outbound payload = %T, want HistoryShareResolveRequest", outbound.GetPayload()) - } - if shareReq.GetToken() != "share-token" { - t.Fatalf("public share token = %q", shareReq.GetToken()) - } - - sm.DispatchFromAgent("desktop-agent", &gatewayv2.AgentEnvelope{ - RequestId: outbound.GetRequestId(), - Timestamp: time.Now().Unix(), - Payload: &gatewayv2.AgentEnvelope_HistoryShareResolveResp{ - HistoryShareResolveResp: &gatewayv2.HistoryShareResolveResponse{ - ConversationId: "conversation-1", - MessagesJson: `[{"role":"user","content":"hello"}]`, - TotalMessageCount: 1, - RedactToolContent: true, - Conversation: &gatewayv2.ConversationSummary{ - Id: "conversation-1", - Title: "Shared conversation", - CreatedAt: 10, - UpdatedAt: 11, - MessageCount: 1, - }, - }, - }, - }) - - select { - case <-done: - case <-time.After(time.Second): - t.Fatal("timed out waiting for public share response") - } - if rec.Code != http.StatusOK { - t.Fatalf("expected status %d, got %d body %s", http.StatusOK, rec.Code, rec.Body.String()) - } - var payload map[string]any - if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { - t.Fatalf("decode public share payload: %v", err) - } - if payload["conversation_id"] != "conversation-1" || - payload["messages_json"] != `[{"role":"user","content":"hello"}]` || - payload["total_message_count"] != float64(1) || - payload["redact_tool_content"] != true { - t.Fatalf("public share payload = %#v", payload) - } -} - -func TestPublicHistoryShareReturnsNotFoundForDisabledToken(t *testing.T) { - status := publicHistoryShareErrorStatusForTest(t, http.StatusNotFound, "分享链接不存在或已关闭") - if status != http.StatusNotFound { - t.Fatalf("expected status %d, got %d", http.StatusNotFound, status) - } -} - -func TestPublicHistoryShareReturnsBadRequestFromAgentCode(t *testing.T) { - status := publicHistoryShareErrorStatusForTest(t, http.StatusBadRequest, "分享 token 不能为空") - if status != http.StatusBadRequest { - t.Fatalf("expected status %d, got %d", http.StatusBadRequest, status) - } -} - -func TestPublicHistoryShareDoesNotInferStatusFromLegacyMessage(t *testing.T) { - status := publicHistoryShareErrorStatusForTest(t, http.StatusInternalServerError, "分享链接不存在或已关闭") - if status != http.StatusBadGateway { - t.Fatalf("expected status %d, got %d", http.StatusBadGateway, status) - } -} - -func publicHistoryShareErrorStatusForTest(t *testing.T, code int, message string) int { - t.Helper() - - sm := session.NewManager() - sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1") - agentSession := session.NewAgentSession(sm.LatestAuthSnapshot("desktop-agent")) - sm.SetSession(agentSession) - - handler := NewHTTPServer(&config.Config{ - Token: "dev-token", - RequestTimeout: time.Second, - }, sm, nil) - req := httptest.NewRequest(http.MethodGet, "http://gateway.test/api/public/history-shares/disabled-token", nil) - rec := httptest.NewRecorder() - done := make(chan struct{}) - go func() { - handler.ServeHTTP(rec, req) - close(done) - }() - - var outbound *gatewayv2.GatewayEnvelope - select { - case delivered := <-agentSession.Outbound(): - delivered.Ack(nil) - outbound = delivered.GatewayEnvelope - case <-time.After(time.Second): - t.Fatal("timed out waiting for public share request") - } - - sm.DispatchFromAgent("desktop-agent", &gatewayv2.AgentEnvelope{ - RequestId: outbound.GetRequestId(), - Timestamp: time.Now().Unix(), - Payload: &gatewayv2.AgentEnvelope_Error{ - Error: &gatewayv2.ErrorResponse{ - Code: int32(code), - Message: message, - }, - }, - }) - - select { - case <-done: - case <-time.After(time.Second): - t.Fatal("timed out waiting for public share response") - } - return rec.Code -} - -func TestPublicHistoryShareReturnsUnavailableWhenAgentOffline(t *testing.T) { - handler := NewHTTPServer(&config.Config{ - Token: "dev-token", - RequestTimeout: time.Second, - }, session.NewManager(), nil) - - req := httptest.NewRequest(http.MethodGet, "http://gateway.test/api/public/history-shares/share-token", nil) - rec := httptest.NewRecorder() - handler.ServeHTTP(rec, req) - - if rec.Code != http.StatusServiceUnavailable { - t.Fatalf("expected status %d, got %d body %s", http.StatusServiceUnavailable, rec.Code, rec.Body.String()) - } -} - -func TestOriginAllowedRequiresStrictOriginForPublicHosts(t *testing.T) { - req := httptest.NewRequest(http.MethodPost, "http://gateway.test:8080/api/chat/commands", nil) - req.Header.Set("Origin", "http://gateway.test:5173") - - if shared.OriginAllowed(req) { - t.Fatal("expected same hostname with different public port to be rejected") - } -} - -func TestOriginAllowedPermitsLoopbackDevPorts(t *testing.T) { - req := httptest.NewRequest(http.MethodPost, "http://127.0.0.1:8080/api/chat/commands", nil) - req.Header.Set("Origin", "http://localhost:5173") - - if !shared.OriginAllowed(req) { - t.Fatal("expected loopback development origins to be allowed across ports") - } -} - -func TestOriginAllowedUsesForwardedProtoForSameOrigin(t *testing.T) { - req := httptest.NewRequest(http.MethodPost, "http://gateway.test/api/chat/commands", nil) - req.Host = "gateway.test" - req.Header.Set("X-Forwarded-Proto", "https") - req.Header.Set("Origin", "https://gateway.test") - - if !shared.OriginAllowed(req) { - t.Fatal("expected forwarded https origin to be allowed") - } -} diff --git a/crates/agent-gateway/internal/server/proto_json.go b/crates/agent-gateway/internal/server/proto_json.go deleted file mode 100644 index 14ec19f56..000000000 --- a/crates/agent-gateway/internal/server/proto_json.go +++ /dev/null @@ -1,106 +0,0 @@ -// proto 消息 → JSON map 塑形,供 HTTP JSON 端点(public share)使用。 -// protojson 会把 int64/uint64 编成字符串、int32 编成 float64;这里按描述符 -// 递归矫正为原生数值,保持对外 JSON 形状与历史线格式一致(公开分享页合同)。 -package server - -import ( - "encoding/json" - "reflect" - "strconv" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" - "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/reflect/protoreflect" -) - -func protoJSONPayload(message proto.Message, useProtoNames bool) map[string]any { - if message == nil || (reflect.ValueOf(message).Kind() == reflect.Pointer && reflect.ValueOf(message).IsNil()) { - return nil - } - raw, err := protojson.MarshalOptions{ - UseProtoNames: useProtoNames, - EmitUnpopulated: true, - }.Marshal(message) - if err != nil { - return map[string]any{} - } - var payload map[string]any - if err := json.Unmarshal(raw, &payload); err != nil { - return map[string]any{} - } - coerceProtoJSONNumbers(payload, message.ProtoReflect().Descriptor(), useProtoNames) - return payload -} - -func coerceProtoJSONNumbers(payload map[string]any, descriptor protoreflect.MessageDescriptor, useProtoNames bool) { - if payload == nil || descriptor == nil { - return - } - fields := descriptor.Fields() - for i := 0; i < fields.Len(); i++ { - field := fields.Get(i) - key := field.JSONName() - if useProtoNames { - key = field.TextName() - } - value, ok := payload[key] - if !ok { - continue - } - payload[key] = coerceProtoJSONField(value, field, useProtoNames) - } -} - -func coerceProtoJSONField(value any, field protoreflect.FieldDescriptor, useProtoNames bool) any { - if field == nil || value == nil { - return value - } - if field.IsList() { - items, ok := value.([]any) - if !ok { - return value - } - for i, item := range items { - items[i] = coerceProtoJSONScalarOrMessage(item, field, useProtoNames) - } - return items - } - return coerceProtoJSONScalarOrMessage(value, field, useProtoNames) -} - -func coerceProtoJSONScalarOrMessage(value any, field protoreflect.FieldDescriptor, useProtoNames bool) any { - if field.Kind() == protoreflect.MessageKind || field.Kind() == protoreflect.GroupKind { - if nested, ok := value.(map[string]any); ok { - coerceProtoJSONNumbers(nested, field.Message(), useProtoNames) - } - return value - } - switch field.Kind() { - case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind: - if number, ok := value.(float64); ok { - return int32(number) - } - case protoreflect.Uint32Kind, protoreflect.Fixed32Kind: - if number, ok := value.(float64); ok { - return uint32(number) - } - case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind: - if text, ok := value.(string); ok { - if parsed, err := strconv.ParseInt(text, 10, 64); err == nil { - return parsed - } - } - case protoreflect.Uint64Kind, protoreflect.Fixed64Kind: - if text, ok := value.(string); ok { - if parsed, err := strconv.ParseUint(text, 10, 64); err == nil { - return parsed - } - } - } - return value -} - -func conversationSummaryPayload(conversation *gatewayv2.ConversationSummary) map[string]any { - return protoJSONPayload(conversation, true) -} diff --git a/crates/agent-gateway/internal/server/proto_json_test.go b/crates/agent-gateway/internal/server/proto_json_test.go deleted file mode 100644 index 921fbe26a..000000000 --- a/crates/agent-gateway/internal/server/proto_json_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package server - -import ( - "testing" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -// 公开分享页 JSON 合同:protojson 会把 int64 编成字符串、int32 编成 float64, -// coerce 链必须矫正为原生数值(前端时间戳/计数渲染依赖)。 -func TestProtoJSONPayloadPreservesFrontendNumberTypes(t *testing.T) { - payload := conversationSummaryPayload(&gatewayv2.ConversationSummary{ - Id: "conversation-1", - CreatedAt: 42, - UpdatedAt: 84, - MessageCount: 3, - }) - - if got := payload["created_at"]; got != int64(42) { - t.Fatalf("created_at = %#v (%T), want int64(42)", got, got) - } - if got := payload["updated_at"]; got != int64(84) { - t.Fatalf("updated_at = %#v (%T), want int64(84)", got, got) - } - if got := payload["message_count"]; got != int32(3) { - t.Fatalf("message_count = %#v (%T), want int32(3)", got, got) - } - if got := payload["id"]; got != "conversation-1" { - t.Fatalf("id = %#v, want conversation-1", got) - } -} - -func TestProtoJSONPayloadPreservesNilPayloads(t *testing.T) { - if payload := conversationSummaryPayload(nil); payload != nil { - t.Fatalf("conversation nil payload = %#v, want nil", payload) - } - if payload := protoJSONPayload(nil, true); payload != nil { - t.Fatalf("nil message payload = %#v, want nil", payload) - } -} diff --git a/crates/agent-gateway/internal/server/tunnel_proxy.go b/crates/agent-gateway/internal/server/tunnel_proxy.go deleted file mode 100644 index 0ed38ddee..000000000 --- a/crates/agent-gateway/internal/server/tunnel_proxy.go +++ /dev/null @@ -1,781 +0,0 @@ -package server - -import ( - "context" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "strings" - "time" - - "github.com/google/uuid" - "github.com/gorilla/websocket" - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" - "github.com/liveagent/agent-gateway/internal/session" -) - -const ( - tunnelBodyChunkSize = 64 * 1024 - tunnelRequestBodyMaxBytes = 32 * 1024 * 1024 - tunnelWebSocketDialTimeout = 30 * time.Second - tunnelDataPlaneWSReadLimit = 16 * 1024 * 1024 -) - -var errTunnelRequestBodyTooLarge = errors.New("tunnel request body too large") - -func publicTunnelProxy(sm *session.Manager) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - if slug, ok := parseTunnelPublicPathWithoutTrailingSlash(r.URL.Path); ok { - target := "/t/" + slug + "/" - if r.URL.RawQuery != "" { - target += "?" + r.URL.RawQuery - } - http.Redirect(w, r, target, http.StatusPermanentRedirect) - return - } - - slug, restPath, ok := parseTunnelPublicPath(r.URL.Path) - if !ok { - writeTunnelError(w, http.StatusNotFound, "tunnel not found") - return - } - if r.URL.RawQuery != "" { - restPath += "?" + r.URL.RawQuery - } - - if isWebSocketUpgrade(r) { - serveTunnelWebSocket(w, r, sm, slug, restPath) - return - } - serveTunnelHTTP(w, r, sm, slug, restPath) - } -} - -func serveTunnelHTTP( - w http.ResponseWriter, - r *http.Request, - sm *session.Manager, - slug string, - restPath string, -) { - streamID := "h-" + uuid.NewString() - lease, err := sm.AcquireTunnel(slug, streamID) - if err != nil { - writeTunnelAcquireError(w, err) - return - } - rewrite := tunnelRewrite{slug: lease.Slug(), targetURL: lease.TargetURL()} - - ctx, cancel := context.WithCancel(r.Context()) - completed := false - defer func() { - cancel() - if !completed { - _ = sm.SendTunnelFrameToAgent(lease.AgentID(), &gatewayv2.TunnelFrame{ - StreamId: streamID, - Kind: gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_CANCEL, - }) - } - lease.Release() - }() - - start := &gatewayv2.TunnelFrame{ - StreamId: streamID, - Kind: gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_REQUEST_START, - TargetUrl: lease.TargetURL(), - Method: r.Method, - Path: restPath, - Headers: tunnelRequestHeaders(r, lease.Slug()), - } - if err := sm.SendTunnelFrameToAgent(lease.AgentID(), start); err != nil { - writeTunnelAcquireError(w, err) - return - } - - bodyResult := make(chan error, 1) - go streamTunnelHTTPRequestBody(ctx, sm, lease.AgentID(), streamID, r.Body, bodyResult) - - responseStarted := false - responseHeadersWritten := false - responseStatus := http.StatusOK - responseHeaders := http.Header{} - rewriteKind := tunnelResponseRewriteNone - var rewriteBuffer []byte - - writeResponseHeaders := func() { - if responseHeadersWritten { - return - } - writeTunnelHTTPHeaders(w, responseHeaders) - w.WriteHeader(responseStatus) - responseHeadersWritten = true - } - // flushRewriteBuffer abandons rewriting and streams what was buffered. - // Content-Length/ETag were already dropped at RESPONSE_START, so a - // mid-stream abort terminates the chunked stream instead of lying about - // the body length. - flushRewriteBuffer := func() bool { - rewriteKind = tunnelResponseRewriteNone - writeResponseHeaders() - if len(rewriteBuffer) > 0 { - if _, err := w.Write(rewriteBuffer); err != nil { - return false - } - rewriteBuffer = nil - } - flushTunnelResponse(w) - return true - } - - for { - select { - case <-r.Context().Done(): - return - case <-lease.Done(): - if !responseStarted { - writeTunnelError(w, http.StatusBadGateway, "tunnel stream closed") - } else if rewriteKind != tunnelResponseRewriteNone { - flushRewriteBuffer() - } - return - case err := <-bodyResult: - bodyResult = nil - if errors.Is(err, errTunnelRequestBodyTooLarge) && !responseStarted { - writeTunnelError(w, http.StatusRequestEntityTooLarge, "request body too large") - return - } - case frame := <-lease.Frames(): - if frame == nil { - continue - } - switch frame.GetKind() { - case gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_RESPONSE_START: - if responseStarted { - continue - } - responseStarted = true - if status := int(frame.GetStatus()); status > 0 { - responseStatus = status - } - responseHeaders = tunnelResponseHeaders(frame, rewrite) - rewriteKind = tunnelResponseRewriteKindFor(r.Method, responseStatus, responseHeaders) - if rewriteKind != tunnelResponseRewriteNone { - responseHeaders.Del("Content-Length") - responseHeaders.Del("Etag") - } else { - writeResponseHeaders() - flushTunnelResponse(w) - } - case gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_RESPONSE_BODY: - if !responseStarted { - responseStarted = true - responseHeaders = http.Header{} - writeResponseHeaders() - } - body := frame.GetBody() - if len(body) == 0 { - continue - } - if rewriteKind != tunnelResponseRewriteNone { - if len(rewriteBuffer)+len(body) <= tunnelRewriteBodyMaxBytes { - rewriteBuffer = append(rewriteBuffer, body...) - continue - } - if !flushRewriteBuffer() { - return - } - } - writeResponseHeaders() - if _, err := w.Write(body); err != nil { - return - } - flushTunnelResponse(w) - case gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_RESPONSE_END: - if rewriteKind != tunnelResponseRewriteNone { - body := rewriteBuffer - if rewritten, changed := rewriteTunnelResponseBody(body, rewrite, rewriteKind); changed { - body = rewritten - if rewriteKind == tunnelResponseRewriteHTML { - amendTunnelCSP(responseHeaders, tunnelShimScriptBody(rewrite)) - } - } - writeResponseHeaders() - if len(body) > 0 { - if _, err := w.Write(body); err != nil { - return - } - } - rewriteBuffer = nil - } else if responseStarted && !responseHeadersWritten { - writeResponseHeaders() - } - completed = true - flushTunnelResponse(w) - return - case gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_ERROR: - if !responseStarted { - writeTunnelError(w, http.StatusBadGateway, frame.GetError()) - } else if rewriteKind != tunnelResponseRewriteNone { - flushRewriteBuffer() - } - return - case gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_CANCEL: - if !responseStarted { - writeTunnelError(w, http.StatusServiceUnavailable, "tunnel canceled") - } else if rewriteKind != tunnelResponseRewriteNone { - flushRewriteBuffer() - } - return - } - } - } -} - -func serveTunnelWebSocket( - w http.ResponseWriter, - r *http.Request, - sm *session.Manager, - slug string, - restPath string, -) { - streamID := "w-" + uuid.NewString() - lease, err := sm.AcquireTunnel(slug, streamID) - if err != nil { - writeTunnelAcquireError(w, err) - return - } - - if err := sm.SendTunnelFrameToAgent(lease.AgentID(), &gatewayv2.TunnelFrame{ - StreamId: streamID, - Kind: gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_WS_DIAL, - TargetUrl: lease.TargetURL(), - Method: r.Method, - Path: restPath, - Headers: tunnelWebSocketRequestHeaders(r, lease.Slug()), - }); err != nil { - lease.Release() - writeTunnelAcquireError(w, err) - return - } - - wsSubprotocol, dialErr := awaitTunnelWebSocketDial(lease) - if dialErr != nil { - _ = sm.SendTunnelFrameToAgent(lease.AgentID(), &gatewayv2.TunnelFrame{ - StreamId: streamID, - Kind: gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_CANCEL, - Error: dialErr.Error(), - }) - lease.Release() - writeTunnelError(w, http.StatusBadGateway, dialErr.Error()) - return - } - - upgrader := websocket.Upgrader{ - CheckOrigin: func(_ *http.Request) bool { - return true - }, - } - if wsSubprotocol != "" { - upgrader.Subprotocols = []string{wsSubprotocol} - } - ws, err := upgrader.Upgrade(w, r, nil) - if err != nil { - _ = sm.SendTunnelFrameToAgent(lease.AgentID(), &gatewayv2.TunnelFrame{ - StreamId: streamID, - Kind: gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_WS_CLOSE, - WsCloseCode: websocket.CloseGoingAway, - }) - lease.Release() - return - } - ws.SetReadLimit(tunnelDataPlaneWSReadLimit) - defer lease.Release() - pumpTunnelWebSocket(ws, sm, lease, streamID) -} - -func pumpTunnelWebSocket( - ws *websocket.Conn, - sm *session.Manager, - lease *session.TunnelStreamLease, - streamID string, -) { - closeSent := false - defer func() { - if !closeSent { - _ = sm.SendTunnelFrameToAgent(lease.AgentID(), &gatewayv2.TunnelFrame{ - StreamId: streamID, - Kind: gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_WS_CLOSE, - WsCloseCode: websocket.CloseGoingAway, - }) - } - _ = ws.Close() - }() - - visitorFrames := make(chan *gatewayv2.TunnelFrame, 64) - visitorClose := make(chan *gatewayv2.TunnelFrame, 1) - readerDone := make(chan struct{}) - go func() { - defer close(readerDone) - for { - messageType, body, err := ws.ReadMessage() - if err != nil { - closeFrame := &gatewayv2.TunnelFrame{ - StreamId: streamID, - Kind: gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_WS_CLOSE, - WsCloseCode: websocket.CloseGoingAway, - } - var closeErr *websocket.CloseError - if errors.As(err, &closeErr) { - closeFrame.WsCloseCode = uint32(closeErr.Code) - closeFrame.WsCloseReason = closeErr.Text - } - visitorClose <- closeFrame - return - } - wireType := gatewayv2.TunnelWsMessageType_TUNNEL_WS_MESSAGE_TYPE_BINARY - if messageType == websocket.TextMessage { - wireType = gatewayv2.TunnelWsMessageType_TUNNEL_WS_MESSAGE_TYPE_TEXT - } - frame := &gatewayv2.TunnelFrame{ - StreamId: streamID, - Kind: gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_WS_FRAME, - Body: body, - WsMessageType: wireType, - } - select { - case visitorFrames <- frame: - case <-lease.Done(): - return - } - } - }() - - for { - select { - case <-lease.Done(): - closeSent = true - return - case closeFrame := <-visitorClose: - closeSent = true - _ = sm.SendTunnelFrameToAgent(lease.AgentID(), closeFrame) - return - case <-readerDone: - closeSent = true - _ = sm.SendTunnelFrameToAgent(lease.AgentID(), &gatewayv2.TunnelFrame{ - StreamId: streamID, - Kind: gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_WS_CLOSE, - WsCloseCode: websocket.CloseGoingAway, - }) - return - case frame := <-visitorFrames: - if frame != nil { - _ = sm.SendTunnelFrameToAgent(lease.AgentID(), frame) - } - case frame := <-lease.Frames(): - if frame == nil { - continue - } - switch frame.GetKind() { - case gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_WS_FRAME: - messageType := websocket.BinaryMessage - if frame.GetWsMessageType() == gatewayv2.TunnelWsMessageType_TUNNEL_WS_MESSAGE_TYPE_TEXT { - messageType = websocket.TextMessage - } - if err := ws.WriteMessage(messageType, frame.GetBody()); err != nil { - return - } - case gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_WS_CLOSE: - closeSent = true - code := int(frame.GetWsCloseCode()) - if code == 0 { - code = websocket.CloseNormalClosure - } - deadline := time.Now().Add(time.Second) - _ = ws.WriteControl( - websocket.CloseMessage, - websocket.FormatCloseMessage(code, frame.GetWsCloseReason()), - deadline, - ) - return - case gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_CANCEL, - gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_ERROR: - closeSent = true - deadline := time.Now().Add(time.Second) - _ = ws.WriteControl( - websocket.CloseMessage, - websocket.FormatCloseMessage(websocket.CloseInternalServerErr, ""), - deadline, - ) - return - } - } - } -} - -func awaitTunnelWebSocketDial(lease *session.TunnelStreamLease) (string, error) { - timer := time.NewTimer(tunnelWebSocketDialTimeout) - defer timer.Stop() - for { - select { - case <-timer.C: - return "", errors.New("local tunnel websocket dial timed out") - case <-lease.Done(): - return "", errors.New("tunnel stream closed before websocket dial completed") - case frame := <-lease.Frames(): - if frame == nil { - continue - } - switch frame.GetKind() { - case gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_WS_DIAL_OK: - return strings.TrimSpace(frame.GetWsSubprotocol()), nil - case gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_WS_DIAL_ERROR, - gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_ERROR, - gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_CANCEL, - gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_WS_CLOSE: - message := strings.TrimSpace(frame.GetError()) - if message == "" { - message = "local tunnel websocket dial failed" - } - return "", errors.New(message) - } - } - } -} - -func streamTunnelHTTPRequestBody( - ctx context.Context, - sm *session.Manager, - agentID string, - streamID string, - body io.ReadCloser, - result chan<- error, -) { - var resultErr error - defer func() { - result <- resultErr - }() - defer func() { _ = body.Close() }() - - buffer := make([]byte, tunnelBodyChunkSize) - sent := 0 - for { - n, err := body.Read(buffer) - if n > 0 { - sent += n - if sent > tunnelRequestBodyMaxBytes { - resultErr = errTunnelRequestBodyTooLarge - _ = sm.SendTunnelFrameToAgent(agentID, &gatewayv2.TunnelFrame{ - StreamId: streamID, - Kind: gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_CANCEL, - Error: errTunnelRequestBodyTooLarge.Error(), - }) - return - } - chunk := make([]byte, n) - copy(chunk, buffer[:n]) - if sendErr := sm.SendTunnelFrameToAgent(agentID, &gatewayv2.TunnelFrame{ - StreamId: streamID, - Kind: gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_REQUEST_BODY, - Body: chunk, - }); sendErr != nil { - resultErr = sendErr - return - } - } - if errors.Is(err, io.EOF) { - _ = sm.SendTunnelFrameToAgent(agentID, &gatewayv2.TunnelFrame{ - StreamId: streamID, - Kind: gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_REQUEST_END, - }) - return - } - if err != nil { - resultErr = err - _ = sm.SendTunnelFrameToAgent(agentID, &gatewayv2.TunnelFrame{ - StreamId: streamID, - Kind: gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_CANCEL, - Error: err.Error(), - }) - return - } - select { - case <-ctx.Done(): - resultErr = ctx.Err() - return - default: - } - } -} - -func parseTunnelPublicPath(rawPath string) (string, string, bool) { - if !strings.HasPrefix(rawPath, "/t/") { - return "", "", false - } - trimmed := strings.TrimPrefix(rawPath, "/t/") - parts := strings.SplitN(trimmed, "/", 2) - slug := strings.TrimSpace(parts[0]) - if slug == "" { - return "", "", false - } - if len(parts) == 1 || parts[1] == "" { - return slug, "/", true - } - return slug, "/" + parts[1], true -} - -func parseTunnelPublicPathWithoutTrailingSlash(rawPath string) (string, bool) { - if !strings.HasPrefix(rawPath, "/t/") { - return "", false - } - trimmed := strings.TrimPrefix(rawPath, "/t/") - if trimmed == "" || strings.Contains(trimmed, "/") { - return "", false - } - return strings.TrimSpace(trimmed), strings.TrimSpace(trimmed) != "" -} - -func writeTunnelAcquireError(w http.ResponseWriter, err error) { - switch { - case errors.Is(err, session.ErrTunnelNotFound), errors.Is(err, session.ErrTunnelExpired): - writeTunnelError(w, http.StatusNotFound, "tunnel not found") - case errors.Is(err, session.ErrAgentOffline): - writeTunnelError(w, http.StatusServiceUnavailable, "agent offline") - case errors.Is(err, session.ErrTunnelOverLimit): - writeTunnelError(w, http.StatusTooManyRequests, "tunnel connection limit exceeded") - default: - writeTunnelError(w, http.StatusBadGateway, err.Error()) - } -} - -func writeTunnelError(w http.ResponseWriter, status int, message string) { - message = strings.TrimSpace(message) - if message == "" { - message = http.StatusText(status) - } - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - w.WriteHeader(status) - _, _ = w.Write([]byte(message)) -} - -func isWebSocketUpgrade(r *http.Request) bool { - return strings.EqualFold(r.Header.Get("Upgrade"), "websocket") && - strings.Contains(strings.ToLower(r.Header.Get("Connection")), "upgrade") -} - -func tunnelRequestHeaders(r *http.Request, slug string) []*gatewayv2.TunnelHeader { - headers := filteredTunnelRequestHeaders(r.Header, false) - return appendTunnelForwardedHeaders(headers, r, slug) -} - -func tunnelWebSocketRequestHeaders(r *http.Request, slug string) []*gatewayv2.TunnelHeader { - headers := filteredTunnelRequestHeaders(r.Header, true) - return appendTunnelForwardedHeaders(headers, r, slug) -} - -func filteredTunnelRequestHeaders(headers http.Header, websocketUpgrade bool) []*gatewayv2.TunnelHeader { - out := make([]*gatewayv2.TunnelHeader, 0, len(headers)) - for name, values := range headers { - canonical := http.CanonicalHeaderKey(strings.TrimSpace(name)) - if canonical == "" || shouldDropTunnelRequestHeader(canonical, websocketUpgrade) { - continue - } - for _, value := range values { - out = append(out, &gatewayv2.TunnelHeader{ - Name: canonical, - Value: value, - }) - } - } - return out -} - -func shouldDropTunnelRequestHeader(name string, websocketUpgrade bool) bool { - lower := strings.ToLower(name) - // Visitor-supplied forwarding headers are stripped so the local service - // only ever sees the gateway's own X-Forwarded-* values. - if strings.HasPrefix(lower, "x-forwarded-") || lower == "forwarded" { - return true - } - switch lower { - case "connection", - "keep-alive", - "proxy-authenticate", - "proxy-authorization", - "proxy-connection", - "te", - "trailer", - "transfer-encoding", - "upgrade", - "host": - return true - } - if websocketUpgrade { - switch lower { - case "sec-websocket-key", "sec-websocket-version", "sec-websocket-extensions", "sec-websocket-accept": - return true - } - } - return false -} - -func shouldDropTunnelResponseHeader(name string) bool { - switch strings.ToLower(name) { - case "connection", - "keep-alive", - "proxy-authenticate", - "proxy-authorization", - "proxy-connection", - "te", - "trailer", - "transfer-encoding", - "upgrade": - return true - default: - return false - } -} - -func appendTunnelForwardedHeaders( - headers []*gatewayv2.TunnelHeader, - r *http.Request, - slug string, -) []*gatewayv2.TunnelHeader { - if r == nil { - return headers - } - proto := "http" - if r.TLS != nil { - proto = "https" - } - headers = append(headers, - &gatewayv2.TunnelHeader{Name: "X-Forwarded-Host", Value: r.Host}, - &gatewayv2.TunnelHeader{Name: "X-Forwarded-Proto", Value: proto}, - ) - if origin := strings.TrimSpace(r.Header.Get("Origin")); origin != "" { - headers = append(headers, &gatewayv2.TunnelHeader{Name: "X-Forwarded-Origin", Value: origin}) - } - if slug = strings.TrimSpace(slug); slug != "" { - headers = append(headers, &gatewayv2.TunnelHeader{Name: "X-Forwarded-Prefix", Value: "/t/" + slug}) - } - return headers -} - -func tunnelResponseHeaders(frame *gatewayv2.TunnelFrame, rw tunnelRewrite) http.Header { - headers := http.Header{} - for _, header := range frame.GetHeaders() { - name := http.CanonicalHeaderKey(strings.TrimSpace(header.GetName())) - if name == "" || shouldDropTunnelResponseHeader(name) { - continue - } - value := header.GetValue() - if strings.EqualFold(name, "Location") { - value = rewriteTunnelLocation(value, rw) - } - if strings.EqualFold(name, "Set-Cookie") { - value = rewriteTunnelSetCookiePath(value, rw) - } - headers.Add(name, value) - } - return headers -} - -func writeTunnelHTTPHeaders(w http.ResponseWriter, headers http.Header) { - for name, values := range headers { - for _, value := range values { - w.Header().Add(name, value) - } - } -} - -func flushTunnelResponse(w http.ResponseWriter) { - if flusher, ok := w.(http.Flusher); ok { - flusher.Flush() - } -} - -func rewriteTunnelLocation(value string, rw tunnelRewrite) string { - publicPrefix := rw.publicPrefix() - if publicPrefix == "" { - return value - } - target, err := rw.parseTarget() - if err != nil || target.Host == "" { - return value - } - parsed, err := url.Parse(value) - if err != nil { - return value - } - if parsed.IsAbs() { - if !strings.EqualFold(parsed.Scheme, target.Scheme) || !strings.EqualFold(parsed.Host, target.Host) { - return value - } - path := stripTunnelTargetBasePath(parsed.EscapedPath(), target.EscapedPath()) - return publicPrefix + appendTunnelURLQueryAndFragment(pathOrRoot(path), parsed) - } - if strings.HasPrefix(value, "/") { - path := stripTunnelTargetBasePath(parsed.EscapedPath(), target.EscapedPath()) - return publicPrefix + appendTunnelURLQueryAndFragment(pathOrRoot(path), parsed) - } - return value -} - -func rewriteTunnelSetCookiePath(value string, rw tunnelRewrite) string { - slug := strings.TrimSpace(rw.slug) - if slug == "" { - return value - } - parts := strings.Split(value, ";") - targetBasePath := "/" - if target, err := rw.parseTarget(); err == nil { - targetBasePath = target.EscapedPath() - } - for index, part := range parts { - trimmed := strings.TrimSpace(part) - if !strings.HasPrefix(strings.ToLower(trimmed), "path=") { - continue - } - cookiePath := strings.TrimSpace(trimmed[len("path="):]) - if cookiePath == "" { - cookiePath = "/" - } - rest := stripTunnelTargetBasePath(cookiePath, targetBasePath) - if rest == "" { - rest = "/" - } - prefix := "" - if leading := len(part) - len(strings.TrimLeft(part, " \t")); leading > 0 { - prefix = part[:leading] - } - parts[index] = fmt.Sprintf("%sPath=/t/%s%s", prefix, slug, rest) - } - return strings.Join(parts, ";") -} - -func stripTunnelTargetBasePath(pathValue string, basePath string) string { - pathValue = normalizeTunnelPath(pathValue) - basePath = normalizeTunnelPath(basePath) - if basePath == "/" { - return pathValue - } - if pathValue == basePath { - return "/" - } - if strings.HasPrefix(pathValue, strings.TrimRight(basePath, "/")+"/") { - return strings.TrimPrefix(pathValue, strings.TrimRight(basePath, "/")) - } - return pathValue -} - -func normalizeTunnelPath(value string) string { - value = strings.TrimSpace(value) - if value == "" { - return "/" - } - if !strings.HasPrefix(value, "/") { - value = "/" + value - } - return value -} diff --git a/crates/agent-gateway/internal/server/tunnel_proxy_test.go b/crates/agent-gateway/internal/server/tunnel_proxy_test.go deleted file mode 100644 index 146c68447..000000000 --- a/crates/agent-gateway/internal/server/tunnel_proxy_test.go +++ /dev/null @@ -1,207 +0,0 @@ -package server - -import ( - "net/http" - "net/http/httptest" - "strings" - "testing" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" - "github.com/liveagent/agent-gateway/internal/session" -) - -func TestParseTunnelPublicPath(t *testing.T) { - t.Parallel() - - tests := []struct { - raw string - slug string - rest string - ok bool - }{ - {"/t/abc/", "abc", "/", true}, - {"/t/abc/app/index.html", "abc", "/app/index.html", true}, - {"/t/abc", "abc", "/", true}, - {"/t/", "", "", false}, - {"/other", "", "", false}, - } - for _, tt := range tests { - slug, rest, ok := parseTunnelPublicPath(tt.raw) - if slug != tt.slug || rest != tt.rest || ok != tt.ok { - t.Fatalf("parseTunnelPublicPath(%q) = (%q, %q, %v), want (%q, %q, %v)", - tt.raw, slug, rest, ok, tt.slug, tt.rest, tt.ok) - } - } - - if slug, ok := parseTunnelPublicPathWithoutTrailingSlash("/t/abc"); !ok || slug != "abc" { - t.Fatalf("no-trailing-slash parse = (%q, %v)", slug, ok) - } - if _, ok := parseTunnelPublicPathWithoutTrailingSlash("/t/abc/x"); ok { - t.Fatal("nested path must not match the redirect case") - } -} - -func TestTunnelRequestHeadersStripForwardedAndHopByHop(t *testing.T) { - t.Parallel() - - req := httptest.NewRequest(http.MethodGet, "http://public.example/t/abc/", nil) - req.Header.Set("X-Forwarded-Host", "evil.example") - req.Header.Set("X-Forwarded-For", "1.2.3.4") - req.Header.Set("Forwarded", "for=1.2.3.4") - req.Header.Set("Connection", "keep-alive") - req.Header.Set("Transfer-Encoding", "chunked") - req.Header.Set("Accept", "text/html") - req.Header.Set("Origin", "https://public.example") - - headers := tunnelRequestHeaders(req, "abc") - byName := map[string][]string{} - for _, header := range headers { - byName[header.GetName()] = append(byName[header.GetName()], header.GetValue()) - } - - if got := byName["X-Forwarded-Host"]; len(got) != 1 || got[0] != "public.example" { - t.Fatalf("X-Forwarded-Host = %v, want the gateway-derived host only", got) - } - for _, banned := range []string{"X-Forwarded-For", "Forwarded", "Connection", "Transfer-Encoding", "Host"} { - if _, present := byName[banned]; present { - t.Fatalf("header %q must be stripped", banned) - } - } - if got := byName["X-Forwarded-Prefix"]; len(got) != 1 || got[0] != "/t/abc" { - t.Fatalf("X-Forwarded-Prefix = %v", got) - } - if got := byName["X-Forwarded-Origin"]; len(got) != 1 || got[0] != "https://public.example" { - t.Fatalf("X-Forwarded-Origin = %v", got) - } - if got := byName["Accept"]; len(got) != 1 || got[0] != "text/html" { - t.Fatalf("Accept = %v, want passthrough", got) - } -} - -func TestTunnelWebSocketRequestHeadersStripSecWebSocket(t *testing.T) { - t.Parallel() - - req := httptest.NewRequest(http.MethodGet, "http://public.example/t/abc/ws", nil) - req.Header.Set("Sec-Websocket-Key", "k") - req.Header.Set("Sec-Websocket-Version", "13") - req.Header.Set("Sec-Websocket-Protocol", "graphql-ws") - - headers := tunnelWebSocketRequestHeaders(req, "abc") - byName := map[string]string{} - for _, header := range headers { - byName[header.GetName()] = header.GetValue() - } - if _, present := byName["Sec-Websocket-Key"]; present { - t.Fatal("Sec-WebSocket-Key must be stripped") - } - if _, present := byName["Sec-Websocket-Version"]; present { - t.Fatal("Sec-WebSocket-Version must be stripped") - } - if got := byName["Sec-Websocket-Protocol"]; got != "graphql-ws" { - t.Fatalf("Sec-WebSocket-Protocol = %q, want passthrough", got) - } -} - -func TestWriteTunnelAcquireErrorStatusMapping(t *testing.T) { - t.Parallel() - - tests := []struct { - err error - status int - }{ - {session.ErrTunnelNotFound, http.StatusNotFound}, - {session.ErrTunnelExpired, http.StatusNotFound}, - {session.ErrAgentOffline, http.StatusServiceUnavailable}, - {session.ErrTunnelOverLimit, http.StatusTooManyRequests}, - } - for _, tt := range tests { - recorder := httptest.NewRecorder() - writeTunnelAcquireError(recorder, tt.err) - if recorder.Code != tt.status { - t.Fatalf("status for %v = %d, want %d", tt.err, recorder.Code, tt.status) - } - } -} - -func TestTunnelResponseHeadersRewriteLocationAndCookies(t *testing.T) { - t.Parallel() - - rw := tunnelRewrite{slug: "abc", targetURL: "http://localhost:3000"} - frame := &gatewayv2.TunnelFrame{ - Headers: []*gatewayv2.TunnelHeader{ - {Name: "Location", Value: "http://localhost:3000/login?next=%2F"}, - {Name: "Set-Cookie", Value: "sid=1; Path=/; HttpOnly"}, - {Name: "Transfer-Encoding", Value: "chunked"}, - {Name: "Content-Type", Value: "text/html"}, - }, - } - headers := tunnelResponseHeaders(frame, rw) - if got := headers.Get("Location"); got != "/t/abc/login?next=%2F" { - t.Fatalf("Location = %q", got) - } - if got := headers.Get("Set-Cookie"); !strings.Contains(got, "Path=/t/abc/") { - t.Fatalf("Set-Cookie = %q, want rewritten path", got) - } - if headers.Get("Transfer-Encoding") != "" { - t.Fatal("hop-by-hop response header must be dropped") - } - if headers.Get("Content-Type") != "text/html" { - t.Fatal("Content-Type must pass through") - } -} - -func TestAmendTunnelCSPHashAmendable(t *testing.T) { - t.Parallel() - - headers := http.Header{} - headers.Set("Content-Security-Policy", "default-src 'self'; script-src 'self'") - amendTunnelCSP(headers, "console.log(1)") - policy := headers.Get("Content-Security-Policy") - if !strings.Contains(policy, "script-src 'self' 'sha256-") { - t.Fatalf("policy = %q, want sha256 appended to script-src", policy) - } - if strings.Contains(strings.TrimPrefix(policy, "default-src 'self' 'sha256-"), "default-src 'self' 'sha256-") { - t.Fatalf("policy = %q, default-src must stay untouched when script-src exists", policy) - } -} - -func TestAmendTunnelCSPDefaultSrcFallback(t *testing.T) { - t.Parallel() - - headers := http.Header{} - headers.Set("Content-Security-Policy", "default-src 'self'") - amendTunnelCSP(headers, "console.log(1)") - if policy := headers.Get("Content-Security-Policy"); !strings.Contains(policy, "default-src 'self' 'sha256-") { - t.Fatalf("policy = %q, want sha256 appended to default-src", policy) - } -} - -func TestAmendTunnelCSPNonceStripped(t *testing.T) { - t.Parallel() - - headers := http.Header{} - headers.Set("Content-Security-Policy", "script-src 'nonce-abc123'") - headers.Set("Content-Security-Policy-Report-Only", "script-src 'self'") - amendTunnelCSP(headers, "console.log(1)") - if headers.Get("Content-Security-Policy") != "" { - t.Fatal("nonce policy must be stripped") - } - if headers.Get("Content-Security-Policy-Report-Only") != "" { - t.Fatal("report-only policy must be stripped alongside") - } - if headers.Get("X-Liveagent-Tunnel-Csp") != "stripped" { - t.Fatal("stripped marker header missing") - } -} - -func TestAmendTunnelCSPUnsafeInlineLeftAlone(t *testing.T) { - t.Parallel() - - headers := http.Header{} - headers.Set("Content-Security-Policy", "script-src 'self' 'unsafe-inline'") - amendTunnelCSP(headers, "console.log(1)") - policy := headers.Get("Content-Security-Policy") - if strings.Contains(policy, "sha256-") { - t.Fatalf("policy = %q; adding a hash would re-disable 'unsafe-inline'", policy) - } -} diff --git a/crates/agent-gateway/internal/server/tunnel_rewrite.go b/crates/agent-gateway/internal/server/tunnel_rewrite.go deleted file mode 100644 index b30e4601c..000000000 --- a/crates/agent-gateway/internal/server/tunnel_rewrite.go +++ /dev/null @@ -1,424 +0,0 @@ -package server - -import ( - "crypto/sha256" - "encoding/base64" - "encoding/json" - "io" - "mime" - "net/http" - "net/url" - "strings" - "unicode/utf8" - - "github.com/tdewolff/parse/v2" - "github.com/tdewolff/parse/v2/css" - "golang.org/x/net/html" -) - -const tunnelRewriteBodyMaxBytes = 4 * 1024 * 1024 - -type tunnelResponseRewriteKind int - -const ( - tunnelResponseRewriteNone tunnelResponseRewriteKind = iota - tunnelResponseRewriteHTML - tunnelResponseRewriteCSS -) - -func tunnelResponseRewriteKindFor( - method string, - status int, - headers http.Header, -) tunnelResponseRewriteKind { - if strings.EqualFold(strings.TrimSpace(method), http.MethodHead) { - return tunnelResponseRewriteNone - } - if status < http.StatusOK || - status == http.StatusNoContent || - status == http.StatusNotModified { - return tunnelResponseRewriteNone - } - if strings.TrimSpace(headers.Get("Content-Encoding")) != "" { - return tunnelResponseRewriteNone - } - - contentType := strings.TrimSpace(headers.Get("Content-Type")) - if contentType == "" { - return tunnelResponseRewriteNone - } - mediaType, _, err := mime.ParseMediaType(contentType) - if err != nil { - mediaType = contentType - } - mediaType = strings.ToLower(strings.TrimSpace(mediaType)) - - switch mediaType { - case "text/html", "application/xhtml+xml": - return tunnelResponseRewriteHTML - case "text/css": - return tunnelResponseRewriteCSS - default: - return tunnelResponseRewriteNone - } -} - -func rewriteTunnelResponseBody( - body []byte, - rw tunnelRewrite, - kind tunnelResponseRewriteKind, -) ([]byte, bool) { - if len(body) == 0 || kind == tunnelResponseRewriteNone || rw.publicPrefix() == "" { - return body, false - } - if !utf8.Valid(body) { - return body, false - } - - original := string(body) - rewritten := original - switch kind { - case tunnelResponseRewriteHTML: - rewritten = rewriteTunnelHTMLBody(rewritten, rw) - case tunnelResponseRewriteCSS: - rewritten = rewriteTunnelCSSBody(rewritten, rw) - } - if rewritten == original { - return body, false - } - return []byte(rewritten), true -} - -func rewriteTunnelHTMLBody(input string, rw tunnelRewrite) string { - tokenizer := html.NewTokenizer(strings.NewReader(input)) - var builder strings.Builder - changed := false - injected := false - shim := tunnelRuntimeBootstrapScript(rw) - - for { - tokenType := tokenizer.Next() - if tokenType == html.ErrorToken { - if errors := tokenizer.Err(); errors != nil && errors != io.EOF { - return input - } - break - } - - raw := string(tokenizer.Raw()) - if tokenType != html.StartTagToken && tokenType != html.SelfClosingTagToken { - builder.WriteString(raw) - continue - } - - token := tokenizer.Token() - tagName := strings.ToLower(strings.TrimSpace(token.Data)) - if !injected && shim != "" && tagName == "script" { - builder.WriteString(shim) - injected = true - changed = true - } - tokenChanged := false - for index := range token.Attr { - attr := &token.Attr[index] - key := strings.ToLower(strings.TrimSpace(attr.Key)) - switch { - case isTunnelHTMLURLAttribute(key): - rewritten := rewriteTunnelBodyURL(attr.Val, rw) - if rewritten != attr.Val { - attr.Val = rewritten - tokenChanged = true - } - case key == "style": - rewritten := rewriteTunnelCSSBody(attr.Val, rw) - if rewritten != attr.Val { - attr.Val = rewritten - tokenChanged = true - } - } - } - if tokenChanged { - builder.WriteString(token.String()) - changed = true - } else { - builder.WriteString(raw) - } - if !injected && shim != "" && tagName == "head" { - builder.WriteString(shim) - injected = true - changed = true - } - } - - if !injected && shim != "" { - return shim + builder.String() - } - if !changed { - return input - } - return builder.String() -} - -func rewriteTunnelCSSBody(input string, rw tunnelRewrite) string { - lexer := css.NewLexer(parse.NewInputString(input)) - var builder strings.Builder - changed := false - - for { - tokenType, data := lexer.Next() - if tokenType == css.ErrorToken { - if err := lexer.Err(); err != nil && err != io.EOF { - return input - } - break - } - - token := string(data) - if tokenType == css.URLToken { - if rewritten, ok := rewriteTunnelCSSURLToken(token, rw); ok { - builder.WriteString(rewritten) - changed = true - continue - } - } - builder.WriteString(token) - } - - if !changed { - return input - } - return builder.String() -} - -func isTunnelHTMLURLAttribute(key string) bool { - switch key { - case "href", "src", "action", "poster", "data", "formaction", "xlink:href": - return true - default: - return false - } -} - -func tunnelRuntimeBootstrapScript(rw tunnelRewrite) string { - body := tunnelShimScriptBody(rw) - if body == "" { - return "" - } - return `` -} - -// tunnelShimScriptBody is the raw JS between the shim's script tags; CSP -// hash amendment must digest exactly this string. -func tunnelShimScriptBody(rw tunnelRewrite) string { - prefix := rw.publicPrefix() - if prefix == "" { - return "" - } - config, err := json.Marshal(map[string]string{ - "basePath": prefix, - }) - if err != nil { - return "" - } - return `(function(config){` + - `if(window.__LIVEAGENT_TUNNEL__&&window.__LIVEAGENT_TUNNEL__.installed)return;` + - `var base=String(config.basePath||"").replace(/\/+$/,"");` + - `window.__LIVEAGENT_TUNNEL__={basePath:base,installed:true};` + - `function rw(input){if(input==null||!base)return input;var raw=input instanceof URL?input.href:String(input);var u;try{u=new URL(raw,location.href)}catch(_){return input}` + - `if(u.host!==location.host||!/^(http:|https:|ws:|wss:)$/i.test(u.protocol))return input;` + - `if(u.pathname===base||u.pathname.indexOf(base+"/")===0)return u.href;` + - `u.pathname=base+(u.pathname==="/"?"/":u.pathname);return u.href}` + - `function rwWs(input){var out=rw(input);try{var u=new URL(String(out),location.href);if(u.protocol==="http:")u.protocol="ws:";if(u.protocol==="https:")u.protocol="wss:";return u.href}catch(_){return out}}` + - `if(window.WebSocket){var NativeWebSocket=window.WebSocket;window.WebSocket=function(url,protocols){return new NativeWebSocket(rwWs(url),protocols)};window.WebSocket.prototype=NativeWebSocket.prototype;["CONNECTING","OPEN","CLOSING","CLOSED"].forEach(function(k){window.WebSocket[k]=NativeWebSocket[k]})}` + - `if(window.EventSource){var NativeEventSource=window.EventSource;window.EventSource=function(url,options){return new NativeEventSource(rw(url),options)};window.EventSource.prototype=NativeEventSource.prototype}` + - `if(window.fetch){var nativeFetch=window.fetch.bind(window);window.fetch=function(input,init){if(input instanceof Request)return nativeFetch(new Request(rw(input.url),input),init);return nativeFetch(rw(input),init)}}` + - `if(window.XMLHttpRequest){var open=window.XMLHttpRequest.prototype.open;window.XMLHttpRequest.prototype.open=function(method,url){arguments[1]=rw(url);return open.apply(this,arguments)}}` + - `})(` + string(config) + `);` -} - -func rewriteTunnelCSSURLToken(token string, rw tunnelRewrite) (string, bool) { - openIndex := strings.Index(token, "(") - closeIndex := strings.LastIndex(token, ")") - if openIndex < 0 || closeIndex < openIndex { - return token, false - } - - before := token[:openIndex+1] - inner := token[openIndex+1 : closeIndex] - after := token[closeIndex:] - leadingLen := len(inner) - len(strings.TrimLeft(inner, " \t\r\n\f")) - trailingLen := len(inner) - len(strings.TrimRight(inner, " \t\r\n\f")) - if leadingLen+trailingLen > len(inner) { - return token, false - } - leading := inner[:leadingLen] - trailing := inner[len(inner)-trailingLen:] - value := inner[leadingLen : len(inner)-trailingLen] - if value == "" { - return token, false - } - - quote := byte(0) - if len(value) >= 2 && (value[0] == '"' || value[0] == '\'') && value[len(value)-1] == value[0] { - quote = value[0] - value = value[1 : len(value)-1] - } - - rewritten := rewriteTunnelBodyURL(value, rw) - if rewritten == value { - return token, false - } - if quote == 0 && !css.IsURLUnquoted([]byte(rewritten)) { - quote = '"' - } - if quote != 0 { - rewritten = string(quote) + rewritten + string(quote) - } - return before + leading + rewritten + trailing + after, true -} - -func rewriteTunnelBodyURL(value string, rw tunnelRewrite) string { - prefix := rw.publicPrefix() - if prefix == "" { - return value - } - trimmed := strings.TrimSpace(value) - if trimmed == "" || - strings.HasPrefix(trimmed, "#") || - strings.HasPrefix(trimmed, "//") { - return value - } - - parsed, err := url.Parse(trimmed) - if err != nil { - return value - } - target, targetErr := rw.parseTarget() - if parsed.IsAbs() { - if targetErr != nil || target.Host == "" { - return value - } - if !strings.EqualFold(parsed.Scheme, target.Scheme) || - !strings.EqualFold(parsed.Host, target.Host) { - return value - } - path := stripTunnelTargetBasePath(parsed.EscapedPath(), target.EscapedPath()) - return appendTunnelURLQueryAndFragment(prefix+pathOrRoot(path), parsed) - } - if !strings.HasPrefix(trimmed, "/") { - return value - } - if trimmed == prefix || strings.HasPrefix(trimmed, prefix+"/") { - return value - } - - path := parsed.EscapedPath() - if targetErr == nil && target.Host != "" { - path = stripTunnelTargetBasePath(path, target.EscapedPath()) - } - return appendTunnelURLQueryAndFragment(prefix+pathOrRoot(path), parsed) -} - -// tunnelRewrite carries the two facts body/header rewriting needs: which -// public prefix the tunnel is mounted under and which local target it fronts. -type tunnelRewrite struct { - slug string - targetURL string -} - -func (rw tunnelRewrite) publicPrefix() string { - slug := strings.TrimSpace(rw.slug) - if slug == "" { - return "" - } - return "/t/" + slug -} - -func (rw tunnelRewrite) parseTarget() (*url.URL, error) { - return url.Parse(strings.TrimSpace(rw.targetURL)) -} - -// amendTunnelCSP makes the injected shim executable under the response's -// Content-Security-Policy. Hash-amendable policies get the shim's sha256; -// nonce/strict-dynamic policies cannot be amended without weakening them, so -// they are stripped with an explicit marker header instead. -func amendTunnelCSP(headers http.Header, shimScriptBody string) { - policies := headers.Values("Content-Security-Policy") - if len(policies) == 0 || strings.TrimSpace(shimScriptBody) == "" { - return - } - digest := sha256.Sum256([]byte(shimScriptBody)) - hash := "'sha256-" + base64.StdEncoding.EncodeToString(digest[:]) + "'" - - amended := make([]string, 0, len(policies)) - for _, policy := range policies { - lower := strings.ToLower(policy) - if strings.Contains(lower, "'nonce-") || strings.Contains(lower, "'strict-dynamic'") { - headers.Del("Content-Security-Policy") - headers.Del("Content-Security-Policy-Report-Only") - headers.Set("X-Liveagent-Tunnel-Csp", "stripped") - return - } - amended = append(amended, amendTunnelCSPPolicy(policy, hash)) - } - headers.Del("Content-Security-Policy") - for _, policy := range amended { - headers.Add("Content-Security-Policy", policy) - } -} - -func amendTunnelCSPPolicy(policy string, hash string) string { - directives := strings.Split(policy, ";") - scriptIndexes := make([]int, 0, 2) - defaultIndex := -1 - for index, directive := range directives { - fields := strings.Fields(directive) - if len(fields) == 0 { - continue - } - name := strings.ToLower(fields[0]) - switch name { - case "script-src", "script-src-elem": - scriptIndexes = append(scriptIndexes, index) - case "default-src": - defaultIndex = index - } - } - targets := scriptIndexes - if len(targets) == 0 { - if defaultIndex < 0 { - return policy // no script restriction to satisfy - } - targets = []int{defaultIndex} - } - for _, index := range targets { - lower := strings.ToLower(directives[index]) - // A hash would re-disable 'unsafe-inline' on policies that rely on it. - if strings.Contains(lower, "'unsafe-inline'") && - !strings.Contains(lower, "'sha") && !strings.Contains(lower, "'nonce-") { - continue - } - directives[index] = strings.TrimRight(directives[index], " ") + " " + hash - } - return strings.Join(directives, ";") -} - -func pathOrRoot(path string) string { - if strings.TrimSpace(path) == "" { - return "/" - } - return path -} - -func appendTunnelURLQueryAndFragment(path string, parsed *url.URL) string { - if parsed == nil { - return path - } - if parsed.RawQuery != "" { - path += "?" + parsed.RawQuery - } - if parsed.Fragment != "" { - path += "#" + parsed.EscapedFragment() - } - return path -} diff --git a/crates/agent-gateway/internal/server/tunnel_rewrite_test.go b/crates/agent-gateway/internal/server/tunnel_rewrite_test.go deleted file mode 100644 index c318f43d7..000000000 --- a/crates/agent-gateway/internal/server/tunnel_rewrite_test.go +++ /dev/null @@ -1,324 +0,0 @@ -package server - -import ( - "net/http" - "strings" - "testing" -) - -func TestTunnelResponseRewriteKindFor(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - method string - status int - headers http.Header - want tunnelResponseRewriteKind - }{ - { - name: "html", - method: http.MethodGet, - status: http.StatusOK, - headers: http.Header{ - "Content-Type": []string{"text/html; charset=utf-8"}, - }, - want: tunnelResponseRewriteHTML, - }, - { - name: "javascript", - method: http.MethodGet, - status: http.StatusOK, - headers: http.Header{ - "Content-Type": []string{"application/javascript"}, - }, - want: tunnelResponseRewriteNone, - }, - { - name: "css", - method: http.MethodGet, - status: http.StatusOK, - headers: http.Header{ - "Content-Type": []string{"text/css"}, - }, - want: tunnelResponseRewriteCSS, - }, - { - name: "compressed response", - method: http.MethodGet, - status: http.StatusOK, - headers: http.Header{ - "Content-Type": []string{"text/html; charset=utf-8"}, - "Content-Encoding": []string{"gzip"}, - }, - want: tunnelResponseRewriteNone, - }, - { - name: "head request", - method: http.MethodHead, - status: http.StatusOK, - headers: http.Header{ - "Content-Type": []string{"text/html; charset=utf-8"}, - }, - want: tunnelResponseRewriteNone, - }, - { - name: "json", - method: http.MethodGet, - status: http.StatusOK, - headers: http.Header{ - "Content-Type": []string{"application/json"}, - }, - want: tunnelResponseRewriteNone, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - if got := tunnelResponseRewriteKindFor(tt.method, tt.status, tt.headers); got != tt.want { - t.Fatalf("tunnelResponseRewriteKindFor() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestRewriteTunnelHTMLBodyPrefixesRootRelativeAttributes(t *testing.T) { - t.Parallel() - - tunnel := tunnelRewriteTestSummary() - input := strings.Join([]string{ - ``, - ``, - `
`, - `health`, - `cdn`, - `external`, - `already`, - `absolute target`, - ``, - }, "\n") - - body, changed := rewriteTunnelResponseBody([]byte(input), tunnel, tunnelResponseRewriteHTML) - if !changed { - t.Fatal("rewriteTunnelResponseBody() did not report a change") - } - output := string(body) - - assertContains(t, output, `href="/t/test-slug/styles.css"`) - assertContains(t, output, `data-liveagent-tunnel-shim`) - assertContains(t, output, `src="/t/test-slug/app.js"`) - assertContains(t, output, `action="/t/test-slug/api/messages"`) - assertContains(t, output, `href="/t/test-slug/api/health?check=1#ready"`) - assertContains(t, output, `href="//cdn.example.com/lib.js"`) - assertContains(t, output, `href="https://example.com/page"`) - assertContains(t, output, `href="/t/test-slug/already"`) - assertContains(t, output, `href="/t/test-slug/api/showcase"`) - assertContains(t, output, `xlink:href="/t/test-slug/icons.svg#check"`) - assertNotContains(t, output, `/t/test-slug/t/test-slug`) -} - -func TestRewriteTunnelBodyStripsTargetBasePath(t *testing.T) { - t.Parallel() - - tunnel := tunnelRewrite{ - slug: "base-slug", - targetURL: "http://127.0.0.1:3100/app", - } - input := strings.Join([]string{ - ``, - ``, - `root api`, - }, "\n") - - body, changed := rewriteTunnelResponseBody([]byte(input), tunnel, tunnelResponseRewriteHTML) - if !changed { - t.Fatal("rewriteTunnelResponseBody() did not report a change") - } - output := string(body) - - assertContains(t, output, `src="/t/base-slug/assets/main.js"`) - assertContains(t, output, `href="/t/base-slug/styles.css"`) - assertContains(t, output, `href="/t/base-slug/api/health"`) - assertNotContains(t, output, `/t/base-slug/app/`) - - cssBody, changed := rewriteTunnelResponseBody( - []byte(`body { background: url(/app/images/bg.png); }`), - tunnel, - tunnelResponseRewriteCSS, - ) - if !changed { - t.Fatal("rewriteTunnelResponseBody() did not report a CSS change") - } - assertContains(t, string(cssBody), `url(/t/base-slug/images/bg.png)`) -} - -func TestRewriteTunnelJavaScriptBodyIsNotRewritten(t *testing.T) { - t.Parallel() - - tunnel := tunnelRewriteTestSummary() - input := strings.Join([]string{ - `requestJson('/api/showcase')`, - `fetch("/api/health?check=1")`, - `const root = "/"`, - `const external = "https://example.com/api"`, - `const cdn = "//cdn.example.com/app.js"`, - `const already = "/t/test-slug/api/health"`, - }, "\n") - - body, changed := rewriteTunnelResponseBody([]byte(input), tunnel, tunnelResponseRewriteNone) - if changed { - t.Fatal("rewriteTunnelResponseBody() reported an unsafe JavaScript change") - } - output := string(body) - - assertContains(t, output, `requestJson('/api/showcase')`) - assertContains(t, output, `fetch("/api/health?check=1")`) - assertContains(t, output, `const root = "/"`) - assertContains(t, output, `const external = "https://example.com/api"`) - assertContains(t, output, `const cdn = "//cdn.example.com/app.js"`) - assertContains(t, output, `const already = "/t/test-slug/api/health"`) - assertNotContains(t, output, `/t/test-slug/t/test-slug`) -} - -func TestRewriteTunnelHTMLBodyUsesHTMLParsingBoundaries(t *testing.T) { - t.Parallel() - - tunnel := tunnelRewriteTestSummary() - input := strings.Join([]string{ - `
`, - ``, - }, "\n") - - body, changed := rewriteTunnelResponseBody([]byte(input), tunnel, tunnelResponseRewriteHTML) - if !changed { - t.Fatal("rewriteTunnelResponseBody() did not report a change") - } - output := string(body) - - assertContains(t, output, `style="background: url('/t/test-slug/images/bg.png')"`) - assertContains(t, output, ``) - assertContains(t, output, `data-liveagent-tunnel-shim`) - assertNotContains(t, output, `/t/test-slug/api/not-real`) -} - -func TestRewriteTunnelHTMLBodyInjectsRuntimeShimBeforeFirstScript(t *testing.T) { - t.Parallel() - - body, changed := rewriteTunnelResponseBody( - []byte(``), - tunnelRewriteTestSummary(), - tunnelResponseRewriteHTML, - ) - if !changed { - t.Fatal("rewriteTunnelResponseBody() did not inject runtime shim") - } - output := string(body) - shimIndex := strings.Index(output, `data-liveagent-tunnel-shim`) - appIndex := strings.Index(output, `new WebSocket`) - if shimIndex < 0 || appIndex < 0 || shimIndex > appIndex { - t.Fatalf("runtime shim was not injected before app script:\n%s", output) - } - assertContains(t, output, `"basePath":"/t/test-slug"`) - assertContains(t, output, `window.WebSocket=function`) - assertContains(t, output, `window.fetch=function`) - assertContains(t, output, `window.EventSource=function`) - assertContains(t, output, `XMLHttpRequest.prototype.open`) -} - -func TestRewriteTunnelCSSBodyPrefixesRootRelativeURLs(t *testing.T) { - t.Parallel() - - tunnel := tunnelRewriteTestSummary() - input := strings.Join([]string{ - `body { background: url(/images/bg.png); }`, - `.icon { mask-image: url('/icons/check.svg'); }`, - `.remote { background: url("https://example.com/bg.png"); }`, - `.cdn { background: url("//cdn.example.com/bg.png"); }`, - `.already { background: url(/t/test-slug/images/bg.png); }`, - }, "\n") - - body, changed := rewriteTunnelResponseBody([]byte(input), tunnel, tunnelResponseRewriteCSS) - if !changed { - t.Fatal("rewriteTunnelResponseBody() did not report a change") - } - output := string(body) - - assertContains(t, output, `url(/t/test-slug/images/bg.png)`) - assertContains(t, output, `url('/t/test-slug/icons/check.svg')`) - assertContains(t, output, `url("https://example.com/bg.png")`) - assertContains(t, output, `url("//cdn.example.com/bg.png")`) - assertContains(t, output, `url(/t/test-slug/images/bg.png)`) - assertNotContains(t, output, `/t/test-slug/t/test-slug`) -} - -func TestRewriteTunnelCSSBodyIgnoresEmptyURLTokens(t *testing.T) { - t.Parallel() - - tunnel := tunnelRewriteTestSummary() - input := strings.Join([]string{ - `.empty { background: url( ); }`, - `.icon { background: url(/icons/check.svg); }`, - }, "\n") - - body, changed := rewriteTunnelResponseBody([]byte(input), tunnel, tunnelResponseRewriteCSS) - if !changed { - t.Fatal("rewriteTunnelResponseBody() did not report a change") - } - output := string(body) - - assertContains(t, output, `url( )`) - assertContains(t, output, `url(/t/test-slug/icons/check.svg)`) -} - -func TestParseTunnelPublicPathWithoutTrailingSlash(t *testing.T) { - t.Parallel() - - slug, ok := parseTunnelPublicPathWithoutTrailingSlash("/t/test-slug") - if !ok || slug != "test-slug" { - t.Fatalf("parseTunnelPublicPathWithoutTrailingSlash() = %q, %v", slug, ok) - } - - for _, path := range []string{"/t/test-slug/", "/t/test-slug/api", "/t/", "/api/test-slug"} { - if slug, ok := parseTunnelPublicPathWithoutTrailingSlash(path); ok { - t.Fatalf("parseTunnelPublicPathWithoutTrailingSlash(%q) = %q, true; want false", path, slug) - } - } -} - -func TestRewriteTunnelLocationPreservesQueryAndFragment(t *testing.T) { - t.Parallel() - - tunnel := tunnelRewriteTestSummary() - if got := rewriteTunnelLocation("/api/health?check=1#ready", tunnel); got != "/t/test-slug/api/health?check=1#ready" { - t.Fatalf("rewriteTunnelLocation root path = %q", got) - } - if got := rewriteTunnelLocation("http://127.0.0.1:3100/api/showcase#item", tunnel); got != "/t/test-slug/api/showcase#item" { - t.Fatalf("rewriteTunnelLocation absolute target = %q", got) - } - if got := rewriteTunnelLocation("https://example.com/api#item", tunnel); got != "https://example.com/api#item" { - t.Fatalf("rewriteTunnelLocation external = %q", got) - } -} - -func tunnelRewriteTestSummary() tunnelRewrite { - return tunnelRewrite{ - slug: "test-slug", - targetURL: "http://127.0.0.1:3100", - } -} - -func assertContains(t *testing.T, value string, needle string) { - t.Helper() - if !strings.Contains(value, needle) { - t.Fatalf("expected output to contain %q, got:\n%s", needle, value) - } -} - -func assertNotContains(t *testing.T, value string, needle string) { - t.Helper() - if strings.Contains(value, needle) { - t.Fatalf("expected output not to contain %q, got:\n%s", needle, value) - } -} diff --git a/crates/agent-gateway/internal/session/activity_hub.go b/crates/agent-gateway/internal/session/activity_hub.go deleted file mode 100644 index 1750cbaa7..000000000 --- a/crates/agent-gateway/internal/session/activity_hub.go +++ /dev/null @@ -1,94 +0,0 @@ -package session - -import "sync" - -// chatActivityHub fans conversation activity transitions (running/idle with -// run ids) out to every authenticated webui connection. Events are composed -// inside the stream store's locked transitions, so per-conversation ordering -// is the log order. Activity is state-based: when a slow subscriber's buffer -// fills, the oldest pending event is dropped so the latest state still lands. -type chatActivityHub struct { - mu sync.Mutex - nextSubID int - subscribers map[int]chan ConversationActivityEvent -} - -func newChatActivityHub() *chatActivityHub { - return &chatActivityHub{ - subscribers: make(map[int]chan ConversationActivityEvent), - } -} - -// SubscribeChatActivity registers an activity listener. The current activity -// of every active conversation is replayed first so a fresh connection needs -// no separate hydration round-trip. -func (m *Manager) SubscribeChatActivity() (<-chan ConversationActivityEvent, func()) { - hub := m.convStreams.activityHub - - // Replay current activities before registering so a concurrent transition - // is delivered after its predecessor state, never before. The channel is - // sized to hold the whole replay: nothing reads it until this returns, so - // a blocking send here would wedge both mutexes. - m.convStreams.mu.Lock() - replay := make([]ConversationActivityEvent, 0, len(m.convStreams.streams)) - for _, stream := range m.convStreams.streams { - if stream.activity == nil { - continue - } - event := ConversationActivityEvent{ - ConversationID: stream.conversationID, - RunID: stream.activity.RunID, - ClientRequestID: stream.activity.ClientRequestID, - Running: true, - State: stream.activity.State, - Workdir: stream.activity.Workdir, - UpdatedAt: stream.activity.UpdatedAt, - } - if event.Workdir == "" { - event.Workdir = stream.workdir - } - replay = append(replay, event) - } - ch := make(chan ConversationActivityEvent, len(replay)+64) - hub.mu.Lock() - subID := hub.nextSubID - hub.nextSubID++ - hub.subscribers[subID] = ch - for _, event := range replay { - ch <- event - } - hub.mu.Unlock() - m.convStreams.mu.Unlock() - - cleanup := func() { - hub.mu.Lock() - // The channel is never closed: publish may hold a reference collected - // before cleanup ran. Subscribers exit via their own done signal. - delete(hub.subscribers, subID) - hub.mu.Unlock() - } - return ch, cleanup -} - -// publish is called while the stream store mutex is held (store.mu → hub.mu -// is the only lock order). Sends never block: on a full buffer the oldest -// pending event is discarded — activity is a state signal, latest wins. -func (hub *chatActivityHub) publish(event ConversationActivityEvent) { - hub.mu.Lock() - defer hub.mu.Unlock() - for _, ch := range hub.subscribers { - select { - case ch <- event: - continue - default: - } - select { - case <-ch: - default: - } - select { - case ch <- event: - default: - } - } -} diff --git a/crates/agent-gateway/internal/session/agent_session.go b/crates/agent-gateway/internal/session/agent_session.go deleted file mode 100644 index a9e2550c8..000000000 --- a/crates/agent-gateway/internal/session/agent_session.go +++ /dev/null @@ -1,250 +0,0 @@ -package session - -import ( - "context" - "strings" - "time" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -func NewAgentSession(auth AuthSnapshot) *AgentSession { - return &AgentSession{ - AgentID: auth.AgentID, - AgentVersion: auth.AgentVersion, - SessionID: auth.SessionID, - ConnectedAt: time.Now(), - LastPing: time.Now(), - toAgent: make(chan *OutboundEnvelope, 512), - pingCh: make(chan *gatewayv2.GatewayEnvelope, 1), - done: make(chan struct{}), - streams: make(map[string]*agentStream), - } -} - -// SetCapabilities records the immutable capability set declared by the -// authenticated ClientHello. Call it before registering the session. -func (s *AgentSession) SetCapabilities(capabilities []string) { - if s == nil { - return - } - s.capabilities = make(map[string]struct{}, len(capabilities)) - for _, capability := range capabilities { - capability = strings.TrimSpace(capability) - if capability != "" { - s.capabilities[capability] = struct{}{} - } - } -} - -func (s *AgentSession) SupportsCapability(capability string) bool { - if s == nil { - return false - } - _, ok := s.capabilities[strings.TrimSpace(capability)] - return ok -} - -type OutboundEnvelope struct { - *gatewayv2.GatewayEnvelope - - ctx context.Context - result chan error -} - -func (e *OutboundEnvelope) Context() context.Context { - if e == nil || e.ctx == nil { - return context.Background() - } - return e.ctx -} - -func (e *OutboundEnvelope) Ack(err error) { - if e == nil || e.result == nil { - return - } - select { - case e.result <- err: - default: - } -} - -func (s *AgentSession) Outbound() <-chan *OutboundEnvelope { - return s.toAgent -} - -func (s *AgentSession) Pings() <-chan *gatewayv2.GatewayEnvelope { - return s.pingCh -} - -// SendPing queues a heartbeat on a dedicated lane that can never be starved -// by the shared outbound queue. Single producer (heartbeatLoop): a still-queued -// older ping is replaced so the freshest timestamp wins. -func (s *AgentSession) SendPing(env *gatewayv2.GatewayEnvelope) error { - select { - case <-s.done: - return ErrAgentOffline - default: - } - select { - case s.pingCh <- env: - default: - select { - case <-s.pingCh: - default: - } - select { - case s.pingCh <- env: - default: - } - } - return nil -} - -func (s *AgentSession) Done() <-chan struct{} { - return s.done -} - -func (s *AgentSession) Close() { - s.closeOnce.Do(func() { - s.streamsMu.Lock() - s.closed = true - close(s.done) - for requestID, stream := range s.streams { - delete(s.streams, requestID) - stream.close() - } - s.streamsMu.Unlock() - }) -} - -func (s *AgentSession) SendToAgent(env *gatewayv2.GatewayEnvelope) error { - return s.enqueueToAgent(context.Background(), env, nil) -} - -func (s *AgentSession) SendToAgentContext(ctx context.Context, env *gatewayv2.GatewayEnvelope) error { - if ctx == nil { - ctx = context.Background() - } - result := make(chan error, 1) - if err := s.enqueueToAgent(ctx, env, result); err != nil { - return err - } - - select { - case err := <-result: - return err - case <-ctx.Done(): - // The envelope stays queued; the writer skips it once its context is - // expired. A congested-but-alive session must not be torn down here. - return ctx.Err() - case <-s.done: - return ErrAgentOffline - } -} - -func (s *AgentSession) enqueueToAgent( - ctx context.Context, - env *gatewayv2.GatewayEnvelope, - result chan error, -) error { - s.streamsMu.Lock() - closed := s.closed - s.streamsMu.Unlock() - if closed { - return ErrAgentOffline - } - - select { - case <-ctx.Done(): - return ctx.Err() - case <-s.done: - return ErrAgentOffline - case s.toAgent <- &OutboundEnvelope{ - GatewayEnvelope: env, - ctx: ctx, - result: result, - }: - return nil - } -} - -func (s *AgentSession) TrySendToAgent(env *gatewayv2.GatewayEnvelope) (bool, error) { - s.streamsMu.Lock() - closed := s.closed - s.streamsMu.Unlock() - if closed { - return false, ErrAgentOffline - } - - select { - case <-s.done: - return false, ErrAgentOffline - default: - } - - select { - case <-s.done: - return false, ErrAgentOffline - case s.toAgent <- &OutboundEnvelope{GatewayEnvelope: env}: - return true, nil - default: - return false, nil - } -} - -func (s *AgentSession) registerStream(requestID string) (*agentStream, error) { - stream := &agentStream{ - ch: make(chan *gatewayv2.AgentEnvelope, 64), - done: make(chan struct{}), - } - - s.streamsMu.Lock() - defer s.streamsMu.Unlock() - if s.closed { - stream.close() - return nil, ErrAgentOffline - } - if existing, ok := s.streams[requestID]; ok { - existing.close() - } - s.streams[requestID] = stream - return stream, nil -} - -func (s *AgentSession) unregisterStream(requestID string, stream *agentStream) { - s.streamsMu.Lock() - if existing, ok := s.streams[requestID]; ok && existing == stream { - delete(s.streams, requestID) - existing.close() - } - s.streamsMu.Unlock() -} - -func (s *AgentSession) dispatch(env *gatewayv2.AgentEnvelope) { - s.streamsMu.Lock() - stream := s.streams[env.GetRequestId()] - s.streamsMu.Unlock() - if stream == nil { - return - } - stream.send(env) -} - -func (s *agentStream) close() { - s.closeOnce.Do(func() { - close(s.done) - }) -} - -func (s *agentStream) send(env *gatewayv2.AgentEnvelope) bool { - select { - case <-s.done: - return false - case s.ch <- env: - return true - default: - s.close() - return false - } -} diff --git a/crates/agent-gateway/internal/session/agent_view.go b/crates/agent-gateway/internal/session/agent_view.go deleted file mode 100644 index e483f0ea8..000000000 --- a/crates/agent-gateway/internal/session/agent_view.go +++ /dev/null @@ -1,59 +0,0 @@ -package session - -import ( - "strings" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -// AgentView 是绑定到单个非空 agent_id 的只读适配视图:以统一的门控/快照方法 -// 暴露已按 Agent 作用域化的状态,协议层与 shared 域逻辑经它访问。 -type AgentView struct { - m *Manager - agentID string -} - -func (m *Manager) AgentView(agentID string) AgentView { - return AgentView{m: m, agentID: strings.TrimSpace(agentID)} -} - -func (v AgentView) resolvedID() string { - return v.agentID -} - -func (v AgentView) AgentID() string { return v.agentID } - -// ResolvedAgentID 返回视图绑定的 agent_id。 -func (v AgentView) ResolvedAgentID() string { return v.resolvedID() } - -func (v AgentView) WebTerminalEnabled() bool { - return v.m.WebTerminalEnabled(v.resolvedID()) -} - -func (v AgentView) WebSshTerminalEnabled() bool { - return v.m.WebSshTerminalEnabled(v.resolvedID()) -} - -func (v AgentView) WebGitEnabled() bool { - return v.m.WebGitEnabled(v.resolvedID()) -} - -func (v AgentView) WebTunnelsEnabled() bool { - return v.m.WebTunnelsEnabled(v.resolvedID()) -} - -func (v AgentView) TerminalSessionKind(sessionID string) string { - return v.m.TerminalSessionKind(v.resolvedID(), sessionID) -} - -func (v AgentView) TerminalSessionSnapshot(projectPathKey string) []*gatewayv2.TerminalSession { - return v.m.TerminalSessionSnapshot(v.resolvedID(), projectPathKey) -} - -func (v AgentView) ApplyTerminalResponseSnapshot( - action string, - projectPathKey string, - resp *gatewayv2.TerminalResponse, -) { - v.m.ApplyTerminalResponseSnapshot(v.resolvedID(), action, projectPathKey, resp) -} diff --git a/crates/agent-gateway/internal/session/conversation_ingress.go b/crates/agent-gateway/internal/session/conversation_ingress.go deleted file mode 100644 index dd6439f09..000000000 --- a/crates/agent-gateway/internal/session/conversation_ingress.go +++ /dev/null @@ -1,472 +0,0 @@ -package session - -import ( - "strings" - "time" - - "github.com/liveagent/agent-gateway/internal/chatwire" - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -// Ingress normalization: the three agent-facing envelope payloads (ChatEvent, -// ChatControlEvent, ChatRuntimeSnapshot) converge here into one append API on -// the conversation stream store. Payload shaping and tool-result trimming -// happen exactly once, so every subscriber observes identical events. - -func (m *Manager) ingestChatEvent(agentID, requestID string, event *gatewayv2.ChatEvent) { - if event == nil { - return - } - agentID = strings.TrimSpace(agentID) - if agentID == "" { - return - } - s := m.convStreams - runID := strings.TrimSpace(requestID) - if runID == "" { - return - } - now := time.Now() - epoch := m.sessionEpochOf(agentID) - - s.mu.Lock() - defer s.mu.Unlock() - - conversationID := s.resolveConversationLocked(agentID, runID, strings.TrimSpace(event.GetConversationId()), now) - if conversationID == "" { - return - } - existingStream := s.streams[conversationStreamKey(agentID, conversationID)] - streamWasUnknown := existingStream == nil || - (existingStream.lastSeq == 0 && existingStream.activity == nil) - stream := s.streamLocked(agentID, conversationID, now) - // 入站事件按已认证会话身份盖章会话流归属(伪造不可能:id 来自握手)。 - if agentID != "" { - stream.agentID = agentID - } - s.noteAgentEpochLocked(stream, epoch) - - payload := chatwire.EventPayload(event, 0) - eventType, _ := payload["type"].(string) - if eventType == "" { - eventType = chatwire.EventTypeName(event.GetType()) - } - if (event.GetType() == gatewayv2.ChatEvent_DONE || event.GetType() == gatewayv2.ChatEvent_ERROR) && - s.reliableIngressOwnsRunLocked(agentID, runID) { - return - } - - if event.GetType() == gatewayv2.ChatEvent_USER_MESSAGE { - if record := s.runs[agentScopedKey(agentID, runID)]; record != nil && record.userMessageSeeded { - messageID, _ := payload["message_id"].(string) - if strings.TrimSpace(messageID) == "" { - // The full stable ref (edit-resend rebase anchoring) carries - // the same id; either field proves the echo has new identity. - if ref, ok := payload["message_ref"].(map[string]any); ok { - refMessageID, _ := ref["message_id"].(string) - messageID = refMessageID - } - } - if strings.TrimSpace(messageID) == "" || record.userMessageIdentityForwarded { - // The gateway already appended this run's user_message at accept - // time. A plain or replayed agent echo adds no new identity. - return - } - // Forward one authoritative desktop echo carrying the stable message - // identity (message_id, plus message_ref so a follow-up edit-resend - // can anchor its rebase). WebUI upserts it into the run's single user - // slot, so this enriches identity without creating a second bubble. - record.userMessageIdentityForwarded = true - } - } - - if stream.runFinishedRecently(runID) { - // A live event for a run whose terminal was merely inferred proves the - // inference wrong — reopen the run instead of dropping its stream. - if !s.resurrectRunLocked(stream, runID) { - // Late straggler after a genuine or duplicate terminal; drop it. - return - } - } - - switch event.GetType() { - case gatewayv2.ChatEvent_DONE: - delete(payload, "type") - delete(payload, "seq") - s.runFinishedLocked(stream, runID, "completed", "", "", payload, now) - return - case gatewayv2.ChatEvent_ERROR: - message, _ := payload["message"].(string) - delete(payload, "type") - delete(payload, "seq") - delete(payload, "message") - s.runFinishedLocked(stream, runID, "failed", "", strings.TrimSpace(message), payload, now) - return - } - - if event.GetType() == gatewayv2.ChatEvent_USER_MESSAGE { - // A GUI-local edit-resend: the desktop truncated its own history and - // stamped the truncation base onto its user_message. Broadcast the - // same rebased event the webui edit path seeds, so every subscriber - // truncates before the new user message renders (webui commands never - // reach here — their echo was swallowed above). - if ref, ok := payload["base_message_ref"].(map[string]any); ok { - messageID, _ := ref["message_id"].(string) - contentHash, _ := ref["content_hash"].(string) - if strings.TrimSpace(messageID) != "" || strings.TrimSpace(contentHash) != "" { - record := s.runRecordLocked(agentID, runID, conversationID) - if !record.rebaseSeeded { - record.rebaseSeeded = true - s.appendSeededPayloadsLocked(stream, runID, record.clientRequestID, []map[string]any{{ - "type": StreamEventRebased, - "base_message_ref": ref, - "reason": "edit_resend", - }}, now) - } - } - } - } - - workdir, _ := payload["workdir"].(string) - s.runStartedLocked(stream, runID, strings.TrimSpace(workdir), now) - if stream.activity == nil || stream.activity.RunID != runID { - // runStartedLocked declined (e.g. the run finished during - // supersession bookkeeping); do not attribute events to another run. - return - } - if streamWasUnknown && event.GetType() != gatewayv2.ChatEvent_USER_MESSAGE { - // A mid-run delta recreated this stream (gateway restarted while the - // run was streaming): the run's earlier events are unrecoverable from - // the log, so late joiners must hydrate from the runtime snapshot. - stream.runNeedsSnapshot = true - } - - if event.GetType() == gatewayv2.ChatEvent_TOOL_STATUS { - status, _ := payload["status"].(string) - isCompaction, _ := payload["isCompaction"].(bool) - stream.activity.ToolStatus = strings.TrimSpace(status) - stream.activity.ToolStatusIsCompaction = isCompaction - stream.activity.UpdatedAt = now - } - - delete(payload, "seq") - s.appendEventLocked(stream, runID, eventType, payload, now) -} - -func (m *Manager) ingestChatControl(agentID, requestID string, control *gatewayv2.ChatControlEvent) { - if control == nil { - return - } - agentID = strings.TrimSpace(agentID) - if agentID == "" { - return - } - s := m.convStreams - runID := strings.TrimSpace(requestID) - if runID == "" { - runID = strings.TrimSpace(control.GetRequestId()) - } - if runID == "" { - return - } - controlType := strings.TrimSpace(control.GetType()) - if controlType == "" { - controlType = strings.TrimSpace(control.GetState()) - } - errorCode := strings.TrimSpace(control.GetErrorCode()) - message := strings.TrimSpace(control.GetMessage()) - now := time.Now() - epoch := m.sessionEpochOf(agentID) - - s.mu.Lock() - defer s.mu.Unlock() - - conversationID := s.resolveConversationLocked(agentID, runID, strings.TrimSpace(control.GetConversationId()), now) - if conversationID == "" { - // A control for a run the gateway has no conversation for yet (the - // binding signal must carry a conversation id); ignore. - return - } - stream := s.streamLocked(agentID, conversationID, now) - // 入站事件按已认证会话身份盖章会话流归属(伪造不可能:id 来自握手)。 - if agentID != "" { - stream.agentID = agentID - } - s.noteAgentEpochLocked(stream, epoch) - if (controlType == "completed" || controlType == "failed" || controlType == "cancelled") && - s.reliableIngressOwnsRunLocked(agentID, runID) { - return - } - - switch controlType { - case "started": - // A reconnect republish may re-anchor a run this store wrongly gave up - // on (inferred loss); resurrect before the started no-ops against the - // finished set. - if stream.runFinishedRecently(runID) && !s.resurrectRunLocked(stream, runID) { - return - } - s.runStartedLocked(stream, runID, "", now) - case "completed", "failed", "cancelled": - inferredLoss := controlType == "failed" && isInferredRunLossCode(errorCode) - if stream.runFinishedRecently(runID) { - if inferredLoss || !s.resurrectRunLocked(stream, runID) { - return - } - } - // The desktop ledger flushes inferred losses (desktop_run_lost & co) - // through this same channel. For the conversation's active run, ignore - // such a verdict while the run's own events are fresh or it was - // already falsified once — genuine terminals always pass. - if inferredLoss && - stream.activity != nil && stream.activity.RunID == runID { - record := s.runs[agentScopedKey(agentID, runID)] - eventsFresh := !stream.lastEventAt.IsZero() && - now.Sub(stream.lastEventAt) < s.runReportLostTimeout - if eventsFresh || (record != nil && record.revived) { - return - } - } - s.runFinishedLocked(stream, runID, controlType, errorCode, message, nil, now) - case "queued_in_gui": - s.markRunQueuedInGUILocked(stream, runID, now) - case "accepted", "delivered", "claimed", "starting": - record := s.runRecordLocked(agentID, runID, conversationID) - s.markRunQueuedLocked(stream, runID, record.clientRequestID, now) - } -} - -// markRunQueuedInGUILocked handles a command the desktop app parked in its -// prompt queue: the run will not start now. Any provisionally seeded entries -// are compensated with a run_queued event so clients drop them (the prompt is -// visible in the queue UI instead), and the agent's later user_message echo — -// when the queued item finally runs — must pass through. -func (s *conversationStreamStore) markRunQueuedInGUILocked( - stream *conversationStream, - runID string, - now time.Time, -) { - if stream.runFinishedRecently(runID) { - return - } - record := s.runRecordLocked(stream.agentID, runID, stream.conversationID) - record.queuedInGUI = true - seeded := record.userMessageSeeded - record.userMessageSeeded = false - // Seeds deferred at accept time never reached the log: drop them. The - // prompt now lives in the desktop queue (editable there), and the agent's - // echo is the authoritative text when the item eventually runs. - record.deferredSeeds = nil - - if seeded { - payload := map[string]any{} - if record.clientRequestID != "" { - payload["client_request_id"] = record.clientRequestID - } - s.appendEventLocked(stream, runID, StreamEventRunQueued, payload, now) - } - if stream.activity != nil && stream.activity.RunID == runID { - stream.activity = nil - s.publishActivityLocked(stream, now) - } - s.fireCommandUpdateLocked(ChatCommandUpdate{ - AgentID: stream.agentID, - RunID: runID, - ClientRequestID: record.clientRequestID, - ConversationID: stream.conversationID, - Phase: "queued_in_gui", - }) -} - -func (m *Manager) ingestRuntimeSnapshot(agentID string, snapshot *gatewayv2.ChatRuntimeSnapshot) { - if snapshot == nil { - return - } - agentID = strings.TrimSpace(agentID) - if agentID == "" { - return - } - s := m.convStreams - runID := strings.TrimSpace(snapshot.GetRunId()) - conversationID := strings.TrimSpace(snapshot.GetConversationId()) - if runID == "" || conversationID == "" { - return - } - state := strings.TrimSpace(snapshot.GetState()) - now := time.Now() - epoch := m.sessionEpochOf(agentID) - - s.mu.Lock() - defer s.mu.Unlock() - - conversationID = s.resolveConversationLocked(agentID, runID, conversationID, now) - existingStream := s.streams[conversationStreamKey(agentID, conversationID)] - streamWasUnknown := existingStream == nil || (existingStream.lastSeq == 0 && existingStream.activity == nil) - stream := s.streamLocked(agentID, conversationID, now) - // 入站事件按已认证会话身份盖章会话流归属(伪造不可能:id 来自握手)。 - if agentID != "" { - stream.agentID = agentID - } - s.noteAgentEpochLocked(stream, epoch) - if (state == "completed" || state == "failed" || state == "cancelled") && - s.reliableIngressOwnsRunLocked(agentID, runID) { - return - } - if stream.runFinishedRecently(runID) { - // Both running and terminal snapshots are authoritative runtime - // evidence. A terminal snapshot must be able to correct an earlier - // inferred loss even when no token arrived between the two verdicts. - if !s.resurrectRunLocked(stream, runID) { - return - } - } - - switch state { - case "completed", "failed", "cancelled": - s.runFinishedLocked(stream, runID, state, "", "", nil, now) - return - } - next := &RunSnapshot{ - RunID: runID, - Revision: snapshot.GetRevision(), - EntriesJSON: strings.TrimSpace(snapshot.GetEntriesJson()), - ToolStatus: strings.TrimSpace(snapshot.GetToolStatus()), - ToolStatusIsCompaction: snapshot.GetToolStatusIsCompaction(), - Workdir: strings.TrimSpace(snapshot.GetCwd()), - AsOfSeq: stream.lastSeq, - UpdatedAt: now, - } - if current := stream.latestSnapshot; current != nil && - current.RunID == runID && - current.Revision > next.Revision { - // Stale revision; keep the newer snapshot. - return - } - stream.latestSnapshot = next - stream.updatedAt = now - stream.lastEventAt = now - - if state == "running" || state == "" { - if streamWasUnknown { - // The gateway (re)started while this run was already streaming; - // buffered history is gone, so late joiners need the snapshot. - stream.runNeedsSnapshot = true - } - s.runStartedLocked(stream, runID, next.Workdir, now) - if stream.activity != nil && stream.activity.RunID == runID { - if next.ToolStatus != "" || stream.activity.ToolStatus != "" { - stream.activity.ToolStatus = next.ToolStatus - stream.activity.ToolStatusIsCompaction = next.ToolStatusIsCompaction - } - stream.activity.UpdatedAt = now - } - } - - if stream.snapshotDirty { - // The agent reconnected mid-run: tokens streamed during the outage are - // unrecoverable, so push the snapshot inline for attached subscribers. - stream.snapshotDirty = false - s.publishSnapshotLocked(stream, runID, next, now) - } -} - -// publishSnapshotLocked delivers a seq-less snapshot event to current -// subscribers without storing it in the log. -func (s *conversationStreamStore) publishSnapshotLocked( - stream *conversationStream, - runID string, - snapshot *RunSnapshot, - now time.Time, -) { - payload := map[string]any{ - "conversation_id": stream.conversationID, - "run_id": runID, - "type": StreamEventSnapshot, - "revision": snapshot.Revision, - "entries_json": snapshot.EntriesJSON, - "tool_status": snapshot.ToolStatus, - "tool_status_is_compaction": snapshot.ToolStatusIsCompaction, - "as_of_seq": snapshot.AsOfSeq, - } - event := &ConversationEvent{ - ConversationID: stream.conversationID, - RunID: runID, - Seq: 0, - Type: StreamEventSnapshot, - Payload: payload, - ReceivedAt: now, - } - s.publishLocked(stream, event) -} - -// resolveConversationLocked determines the conversation a run belongs to, -// binding a pending webui command when the first agent signal carries a -// conversation id. -func (s *conversationStreamStore) resolveConversationLocked( - agentID string, - runID string, - conversationID string, - now time.Time, -) string { - runKey := agentScopedKey(agentID, runID) - if pending := s.pendingRuns[runKey]; pending != nil && conversationID != "" { - s.bindPendingRunLocked(pending, conversationID, now) - } - if conversationID != "" { - s.runRecordLocked(agentID, runID, conversationID) - return conversationID - } - if record := s.runs[runKey]; record != nil { - return record.conversationID - } - return "" -} - -func (s *conversationStreamStore) bindPendingRunLocked( - pending *pendingChatRun, - conversationID string, - now time.Time, -) { - delete(s.pendingRuns, agentScopedKey(pending.agentID, pending.runID)) - stream := s.streamLocked(pending.agentID, conversationID, now) - if pending.workdir != "" { - stream.workdir = pending.workdir - } - record := s.runRecordLocked(pending.agentID, pending.runID, conversationID) - record.clientRequestID = pending.clientRequestID - s.markRunQueuedLocked(stream, pending.runID, pending.clientRequestID, now) - acceptedSeq := s.appendSeededPayloadsLocked( - stream, pending.runID, pending.clientRequestID, pending.seeded, now, - ) - record.userMessageSeeded = seededPayloadsIncludeUserMessage(pending.seeded) - record.rebaseSeeded = seededPayloadsIncludeRebased(pending.seeded) - s.updateChatCommandDedupeLocked( - pending.agentID, - pending.clientRequestID, - pending.runID, - conversationID, - acceptedSeq, - now, - ) - s.fireCommandUpdateLocked(ChatCommandUpdate{ - AgentID: pending.agentID, - RunID: pending.runID, - ClientRequestID: pending.clientRequestID, - ConversationID: conversationID, - Phase: "bound", - }) -} - -// noteAgentEpochLocked tracks the agent session epoch per stream: when the -// agent reconnects mid-run, tokens streamed during the outage are lost, so -// the next runtime snapshot is pushed inline and offered to late joiners. -func (s *conversationStreamStore) noteAgentEpochLocked(stream *conversationStream, epoch uint64) { - if epoch == 0 || stream.agentEpoch == epoch { - return - } - if stream.agentEpoch != 0 && stream.activity != nil { - stream.snapshotDirty = true - stream.runNeedsSnapshot = true - } - stream.agentEpoch = epoch -} diff --git a/crates/agent-gateway/internal/session/conversation_reliable_ingress.go b/crates/agent-gateway/internal/session/conversation_reliable_ingress.go deleted file mode 100644 index 06532c35c..000000000 --- a/crates/agent-gateway/internal/session/conversation_reliable_ingress.go +++ /dev/null @@ -1,917 +0,0 @@ -package session - -import ( - "bytes" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "fmt" - "io" - "log/slog" - "math" - "strings" - "time" - - "github.com/klauspost/compress/zstd" - "google.golang.org/protobuf/proto" - - "github.com/liveagent/agent-gateway/internal/chatwire" - "github.com/liveagent/agent-gateway/internal/observability" - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -const ( - chatIngressProjectionMaxBytes = 64 << 20 - chatIngressEncodedRecordMaxBytes = chatIngressProjectionMaxBytes + (1 << 20) - chatIngressFragmentDefaultBytes = 32 << 10 - chatIngressFragmentChunkBytes = 64 << 10 - chatIngressFragmentMaxCount = (chatIngressEncodedRecordMaxBytes + chatIngressFragmentDefaultBytes - 1) / chatIngressFragmentDefaultBytes - chatIngressFragmentTTL = 30 * time.Second - // chatIngressBrowserProjectionMaxBytes bounds the projection payload that - // is relayed to browser subscribers. A projection above this limit would - // exceed the browser write-queue frame budget and kill every subscriber's - // socket, so it degrades to a history_required marker instead — history - // convergence carries the content. - chatIngressBrowserProjectionMaxBytes = 4 << 20 -) - -type chatIngressRunState struct { - conversationID string - committedThrough uint64 - latestCheckpointRevision uint64 - latestCheckpointHash string - terminalSeq uint64 - terminalHash string - terminalCommitted bool - checkpointRequested bool - gapObservedAt uint64 - replayRequestedAt uint64 - updatedAt time.Time -} - -type chatIngressFragmentAssembly struct { - conversationID string - fragmentCount uint32 - encodedRecordBytes uint64 - sha256 string - chunks [][]byte - received uint32 - receivedBytes uint64 - expiresAt time.Time -} - -type chatIngressProjection interface { - GetCoversThroughSeq() uint64 - GetRevision() uint64 - GetCompressedProjection() []byte - GetUncompressedBytes() uint64 - GetSha256() string - GetContentComplete() bool - GetHistoryRequired() bool -} - -type chatIngressRecordError struct { - code string - message string -} - -func (e *chatIngressRecordError) Error() string { - return e.message -} - -func newChatIngressRecordError(code, format string, args ...any) error { - return &chatIngressRecordError{code: code, message: fmt.Sprintf(format, args...)} -} - -func (m *Manager) ingestChatIngressBatch(agentID string, batch *gatewayv2.ChatIngressBatch) *gatewayv2.ChatIngressAck { - if batch == nil { - return rejectedChatIngressAck("", "", nil, "invalid_batch", "chat ingress batch is required") - } - return m.ingestChatIngressRecords( - agentID, - strings.TrimSpace(batch.GetRunId()), - strings.TrimSpace(batch.GetConversationId()), - batch.GetFirstSeq(), - batch.GetRecords(), - ) -} - -func (m *Manager) ingestChatIngressRecords( - agentID string, - runID string, - conversationID string, - firstSeq uint64, - records []*gatewayv2.ChatIngressRecord, -) *gatewayv2.ChatIngressAck { - s := m.convStreams - now := time.Now() - agentID = strings.TrimSpace(agentID) - - s.mu.Lock() - defer s.mu.Unlock() - - state := s.ingressRuns[agentScopedKey(agentID, runID)] - wasAbsent := state == nil - if agentID == "" || runID == "" || conversationID == "" { - return rejectedChatIngressAck(runID, conversationID, state, "invalid_identity", "agent_id, run_id and conversation_id are required") - } - if firstSeq == 0 || len(records) == 0 { - return rejectedChatIngressAck(runID, conversationID, state, "invalid_batch", "first_seq and records are required") - } - if uint64(len(records)-1) > ^uint64(0)-firstSeq { - return rejectedChatIngressAck(runID, conversationID, state, "sequence_overflow", "chat ingress sequence overflows uint64") - } - if state == nil { - state = &chatIngressRunState{conversationID: conversationID, updatedAt: now} - s.ingressRuns[agentScopedKey(agentID, runID)] = state - } else if state.conversationID != conversationID { - return rejectedChatIngressAck(runID, conversationID, state, "conversation_mismatch", "run is already bound to another conversation") - } - state.updatedAt = now - - expected := state.committedThrough + 1 - lastSeq := firstSeq + uint64(len(records)) - 1 - if lastSeq == math.MaxUint64 { - return rejectedChatIngressAck(runID, conversationID, state, "sequence_overflow", "chat ingress sequence leaves no representable expected_next") - } - if lastSeq <= state.committedThrough { - if err := validateDuplicateTerminal(state, firstSeq, records); err != nil { - return rejectedChatIngressAck(runID, conversationID, state, ingressErrorCode(err), "%s", err.Error()) - } - return continueChatIngressAck(runID, conversationID, state) - } - if state.terminalCommitted { - return rejectedChatIngressAck(runID, conversationID, state, "terminal_already_committed", "run terminal is already committed") - } - if firstSeq > expected { - if wasAbsent { - noteChatIngressGap(agentID, runID, conversationID, state, expected, "missing_cursor") - requestChatIngressCheckpoint(agentID, runID, conversationID, state, expected, "missing_cursor") - return checkpointChatIngressAck(runID, conversationID, state) - } - if !state.checkpointRequested || !isChatIngressProjectionRecord(records[0]) { - noteChatIngressGap(agentID, runID, conversationID, state, expected, "producer_sequence_gap") - noteChatIngressReplay(agentID, runID, conversationID, state, expected, "producer_sequence_gap") - return replayChatIngressAck(runID, conversationID, state) - } - projection := projectionFromRecord(records[0]) - if projection == nil || projection.GetCoversThroughSeq() < firstSeq-1 { - return rejectedChatIngressAck(runID, conversationID, state, "checkpoint_does_not_cover_gap", "checkpoint does not cover the missing producer sequence range") - } - expected = firstSeq - } - - start := uint64(0) - if firstSeq < expected { - start = expected - firstSeq - } - for offset := start; offset < uint64(len(records)); offset++ { - producerSeq := firstSeq + offset - if err := s.commitChatIngressRecordLocked(agentID, runID, conversationID, producerSeq, records[offset], now); err != nil { - return rejectedChatIngressAck(runID, conversationID, state, ingressErrorCode(err), "%s", err.Error()) - } - state.committedThrough = producerSeq - state.updatedAt = now - state.checkpointRequested = false - state.gapObservedAt = 0 - state.replayRequestedAt = 0 - if records[offset].GetCheckpoint() != nil { - checkpoint := records[offset].GetCheckpoint() - state.latestCheckpointRevision = checkpoint.GetRevision() - state.latestCheckpointHash = normalizedProjectionHash(checkpoint) - noteChatIngressCheckpointCommitted(agentID, runID, conversationID, producerSeq, checkpoint) - } - if records[offset].GetTerminal() != nil { - terminal := records[offset].GetTerminal() - state.latestCheckpointRevision = terminal.GetRevision() - state.latestCheckpointHash = normalizedProjectionHash(terminal) - state.terminalSeq = producerSeq - state.terminalHash = state.latestCheckpointHash - state.terminalCommitted = true - noteChatIngressTerminalCommitted(agentID, runID, conversationID, producerSeq, terminal) - } - } - return continueChatIngressAck(runID, conversationID, state) -} - -func (s *conversationStreamStore) commitChatIngressRecordLocked( - agentID string, - runID string, - conversationID string, - producerSeq uint64, - record *gatewayv2.ChatIngressRecord, - now time.Time, -) error { - if record == nil || record.GetPayload() == nil { - return newChatIngressRecordError("invalid_record", "chat ingress record payload is required") - } - state := s.ingressRuns[agentScopedKey(agentID, runID)] - conversationID = s.resolveConversationLocked(agentID, runID, conversationID, now) - stream := s.streamLocked(agentID, conversationID, now) - stream.agentID = agentID - - switch payload := record.GetPayload().(type) { - case *gatewayv2.ChatIngressRecord_Delta: - return s.appendChatIngressDeltaLocked(stream, runID, payload.Delta, now) - case *gatewayv2.ChatIngressRecord_Checkpoint: - entriesJSON, err := decodeChatIngressProjection(payload.Checkpoint) - if err != nil { - return err - } - if err := validateProjectionRevision(state, payload.Checkpoint, "checkpoint"); err != nil { - return err - } - if payload.Checkpoint.GetRevision() > math.MaxInt64 { - return newChatIngressRecordError("invalid_checkpoint_revision", "checkpoint revision exceeds int64") - } - if payload.Checkpoint.GetCoversThroughSeq() != producerSeq-1 { - return newChatIngressRecordError("invalid_checkpoint_coverage", "checkpoint at sequence %d must cover exactly through %d, got %d", producerSeq, producerSeq-1, payload.Checkpoint.GetCoversThroughSeq()) - } - s.appendChatIngressProjectionLocked(stream, runID, payload.Checkpoint, entriesJSON, now) - return nil - case *gatewayv2.ChatIngressRecord_Terminal: - terminal := payload.Terminal - if stream.runFinishedRecently(runID) && !s.resurrectRunLocked(stream, runID) { - return newChatIngressRecordError("terminal_already_committed", "run terminal is already committed") - } - status := strings.TrimSpace(terminal.GetState()) - switch status { - case "completed", "failed", "cancelled": - default: - return newChatIngressRecordError("invalid_terminal_state", "unsupported terminal state %q", status) - } - entriesJSON, err := decodeChatIngressProjection(terminal) - if err != nil { - return err - } - if err := validateProjectionRevision(state, terminal, "terminal"); err != nil { - return err - } - if terminal.GetRevision() > math.MaxInt64 { - return newChatIngressRecordError("invalid_checkpoint_revision", "terminal revision exceeds int64") - } - if terminal.GetCoversThroughSeq() != producerSeq-1 { - return newChatIngressRecordError("invalid_checkpoint_coverage", "terminal at sequence %d must cover exactly through %d, got %d", producerSeq, producerSeq-1, terminal.GetCoversThroughSeq()) - } - s.appendChatIngressProjectionLocked(stream, runID, terminal, entriesJSON, now) - s.runFinishedLocked( - stream, - runID, - status, - strings.TrimSpace(terminal.GetErrorCode()), - strings.TrimSpace(terminal.GetErrorMessage()), - nil, - now, - ) - return nil - case *gatewayv2.ChatIngressRecord_Heartbeat: - s.touchChatIngressRunLocked(stream, runID, now) - return nil - default: - return newChatIngressRecordError("invalid_record", "unsupported chat ingress record payload") - } -} - -func (s *conversationStreamStore) appendChatIngressDeltaLocked( - stream *conversationStream, - runID string, - delta *gatewayv2.ChatIngressDelta, - now time.Time, -) error { - if delta == nil { - return newChatIngressRecordError("invalid_delta", "chat ingress delta is required") - } - var payload map[string]any - if err := json.Unmarshal([]byte(strings.TrimSpace(delta.GetEventJson())), &payload); err != nil || payload == nil { - return newChatIngressRecordError("invalid_delta_json", "delta event_json must be a JSON object") - } - eventType, _ := payload["type"].(string) - eventType = strings.TrimSpace(eventType) - if eventType == "" { - return newChatIngressRecordError("invalid_delta_type", "delta event type is required") - } - if eventType == "run_heartbeat" { - s.touchChatIngressRunLocked(stream, runID, now) - return nil - } - switch eventType { - case StreamEventRunStarted, StreamEventRunFinished, StreamEventContentSnapshot, "done", "error": - return newChatIngressRecordError("reserved_delta_type", "delta event type %q must use a lifecycle record", eventType) - } - if stream.runFinishedRecently(runID) && !s.resurrectRunLocked(stream, runID) { - return newChatIngressRecordError("terminal_already_committed", "run terminal is already committed") - } - s.runStartedLocked(stream, runID, "", now) - if stream.activity == nil || stream.activity.RunID != runID { - return newChatIngressRecordError("run_not_active", "run could not become active") - } - delete(payload, "conversation_id") - delete(payload, "run_id") - delete(payload, "seq") - delete(payload, "type") - if eventType == "user_message" && !s.prepareReliableUserMessageLocked(stream, runID, payload, now) { - return nil - } - if workerID := strings.TrimSpace(delta.GetWorkerId()); workerID != "" { - payload["worker_id"] = workerID - } - chatwire.TrimLargeToolResultContent(payload, eventType) - s.appendEventLocked(stream, runID, eventType, payload, now) - return nil -} - -func (s *conversationStreamStore) prepareReliableUserMessageLocked( - stream *conversationStream, - runID string, - payload map[string]any, - now time.Time, -) bool { - record := s.runRecordLocked(stream.agentID, runID, stream.conversationID) - if ref, ok := payload["base_message_ref"].(map[string]any); ok && !record.rebaseSeeded { - messageID, _ := ref["message_id"].(string) - contentHash, _ := ref["content_hash"].(string) - if strings.TrimSpace(messageID) != "" || strings.TrimSpace(contentHash) != "" { - record.rebaseSeeded = true - s.appendSeededPayloadsLocked(stream, runID, record.clientRequestID, []map[string]any{{ - "type": StreamEventRebased, - "base_message_ref": ref, - "reason": "edit_resend", - }}, now) - } - } - if !record.userMessageSeeded { - return true - } - messageID, _ := payload["message_id"].(string) - if strings.TrimSpace(messageID) == "" { - if ref, ok := payload["message_ref"].(map[string]any); ok { - messageID, _ = ref["message_id"].(string) - } - } - if strings.TrimSpace(messageID) == "" || record.userMessageIdentityForwarded { - return false - } - record.userMessageIdentityForwarded = true - return true -} - -func (s *conversationStreamStore) touchChatIngressRunLocked(stream *conversationStream, runID string, now time.Time) { - stream.lastEventAt = now - stream.updatedAt = now - if stream.activity != nil && stream.activity.RunID == runID { - stream.activity.UpdatedAt = now - } -} - -func (s *conversationStreamStore) appendChatIngressProjectionLocked( - stream *conversationStream, - runID string, - projection chatIngressProjection, - entriesJSON string, - now time.Time, -) { - s.runStartedLocked(stream, runID, "", now) - contentComplete := projection.GetContentComplete() - historyRequired := projection.GetHistoryRequired() - sha := strings.ToLower(strings.TrimSpace(projection.GetSha256())) - degraded := len(entriesJSON) > chatIngressBrowserProjectionMaxBytes - if degraded { - // Deliverability bound: browsers cannot receive a frame this large. - // The stream event degrades to a history_required marker; the sha of - // the replaced projection would be misleading, so it is dropped. - entriesJSON = "[]" - contentComplete = false - historyRequired = true - sha = "" - } - payload := map[string]any{ - "revision": projection.GetRevision(), - "entries_json": entriesJSON, - "content_complete": contentComplete, - "history_required": historyRequired, - "sha256": sha, - } - stream.latestContentSnapshotSeq = stream.lastSeq + 1 - event := s.appendEventLocked(stream, runID, StreamEventContentSnapshot, payload, now) - if degraded { - // RunSnapshot carries no history_required/content_complete flags, so - // hydration consumers would treat "[]" as authoritative and wipe the - // streamed content. Keep the previous snapshot state; the snapshot-less - // resubscribe path marks contentStale and history converges. - return - } - stream.latestSnapshot = &RunSnapshot{ - RunID: runID, - Revision: int64(projection.GetRevision()), - EntriesJSON: entriesJSON, - AsOfSeq: event.Seq, - UpdatedAt: now, - } - stream.runNeedsSnapshot = false - stream.snapshotDirty = false -} - -func decodeChatIngressProjection(projection chatIngressProjection) (string, error) { - if projection == nil { - return "", newChatIngressRecordError("invalid_checkpoint", "checkpoint projection is required") - } - declared := projection.GetUncompressedBytes() - if declared > chatIngressProjectionMaxBytes { - return "", newChatIngressRecordError("projection_too_large", "uncompressed projection exceeds 64 MiB") - } - if len(projection.GetCompressedProjection()) == 0 { - return "", newChatIngressRecordError("invalid_checkpoint", "compressed projection is required") - } - if len(projection.GetCompressedProjection()) > chatIngressProjectionMaxBytes { - return "", newChatIngressRecordError("projection_too_large", "compressed projection exceeds 64 MiB") - } - decoder, err := zstd.NewReader( - bytes.NewReader(projection.GetCompressedProjection()), - zstd.WithDecoderMaxMemory(chatIngressProjectionMaxBytes), - ) - if err != nil { - return "", newChatIngressRecordError("invalid_projection_compression", "cannot initialize zstd decoder: %v", err) - } - defer decoder.Close() - decoded, err := io.ReadAll(io.LimitReader(decoder, chatIngressProjectionMaxBytes+1)) - if err != nil { - return "", newChatIngressRecordError("invalid_projection_compression", "cannot decompress projection: %v", err) - } - if len(decoded) > chatIngressProjectionMaxBytes { - return "", newChatIngressRecordError("projection_too_large", "uncompressed projection exceeds 64 MiB") - } - if uint64(len(decoded)) != declared { - return "", newChatIngressRecordError("projection_size_mismatch", "projection size is %d bytes, expected %d", len(decoded), declared) - } - wantHash := strings.ToLower(strings.TrimSpace(projection.GetSha256())) - if len(wantHash) != sha256.Size*2 { - return "", newChatIngressRecordError("invalid_projection_sha256", "projection sha256 must be a 64-character hex digest") - } - if _, err := hex.DecodeString(wantHash); err != nil { - return "", newChatIngressRecordError("invalid_projection_sha256", "projection sha256 is not valid hexadecimal") - } - actualHash := sha256.Sum256(decoded) - if hex.EncodeToString(actualHash[:]) != wantHash { - return "", newChatIngressRecordError("projection_hash_mismatch", "projection sha256 does not match decompressed content") - } - var entries []json.RawMessage - if err := json.Unmarshal(decoded, &entries); err != nil || entries == nil { - return "", newChatIngressRecordError("invalid_projection_json", "projection must be a JSON array") - } - return string(decoded), nil -} - -func (m *Manager) ingestChatIngressResume(agentID string, resume *gatewayv2.ChatIngressResume) []*gatewayv2.ChatIngressAck { - if resume == nil { - return nil - } - s := m.convStreams - now := time.Now() - agentID = strings.TrimSpace(agentID) - s.mu.Lock() - defer s.mu.Unlock() - - acks := make([]*gatewayv2.ChatIngressAck, 0, len(resume.GetRuns())) - for _, run := range resume.GetRuns() { - if run == nil { - continue - } - runID := strings.TrimSpace(run.GetRunId()) - conversationID := strings.TrimSpace(run.GetConversationId()) - key := agentScopedKey(agentID, runID) - state := s.ingressRuns[key] - if agentID == "" || runID == "" || conversationID == "" || run.GetNextSeq() == 0 { - acks = append(acks, rejectedChatIngressAck(runID, conversationID, state, "invalid_resume", "run_id, conversation_id and next_seq are required")) - continue - } - if state == nil { - state = &chatIngressRunState{ - conversationID: conversationID, - updatedAt: now, - } - s.ingressRuns[key] = state - s.startReaper() - requestChatIngressCheckpoint(agentID, runID, conversationID, state, 1, "missing_cursor") - acks = append(acks, checkpointChatIngressAck(runID, conversationID, state)) - continue - } - if state.conversationID != conversationID { - acks = append(acks, rejectedChatIngressAck(runID, conversationID, state, "conversation_mismatch", "run is already bound to another conversation")) - continue - } - state.updatedAt = now - if state.terminalCommitted { - acks = append(acks, continueChatIngressAck(runID, conversationID, state)) - continue - } - expected := state.committedThrough + 1 - switch { - case expected == run.GetNextSeq(): - acks = append(acks, continueChatIngressAck(runID, conversationID, state)) - case expected > run.GetNextSeq(): - acks = append(acks, continueChatIngressAck(runID, conversationID, state)) - case run.GetReplayFromSeq() > 0 && expected >= run.GetReplayFromSeq() && expected <= run.GetReplayThroughSeq(): - noteChatIngressReplay(agentID, runID, conversationID, state, expected, "resume_replay") - acks = append(acks, replayChatIngressAck(runID, conversationID, state)) - default: - state.updatedAt = now - requestChatIngressCheckpoint(agentID, runID, conversationID, state, expected, "replay_window_miss") - acks = append(acks, checkpointChatIngressAck(runID, conversationID, state)) - } - } - return acks -} - -func (m *Manager) ingestChatIngressFragment(agentID string, fragment *gatewayv2.ChatIngressFragment) *gatewayv2.ChatIngressAck { - if fragment == nil { - return rejectedChatIngressFragmentAck(agentID, nil, nil, "invalid_fragment", "chat ingress fragment is required") - } - runID := strings.TrimSpace(fragment.GetRunId()) - conversationID := strings.TrimSpace(fragment.GetConversationId()) - fragmentHash := strings.ToLower(strings.TrimSpace(fragment.GetSha256())) - key := chatIngressFragmentKey(agentID, runID, fragment.GetSourceSeq()) - now := time.Now() - s := m.convStreams - - s.mu.Lock() - for fragmentKey, assembly := range s.ingressFragments { - if now.After(assembly.expiresAt) { - delete(s.ingressFragments, fragmentKey) - } - } - state := s.ingressRuns[agentScopedKey(agentID, runID)] - if strings.TrimSpace(agentID) == "" || runID == "" || conversationID == "" || fragment.GetSourceSeq() == 0 { - s.mu.Unlock() - return rejectedChatIngressFragmentAck(agentID, fragment, state, "invalid_fragment", "agent_id, run_id, conversation_id and source_seq are required") - } - if state != nil && state.conversationID != conversationID { - s.mu.Unlock() - return rejectedChatIngressFragmentAck(agentID, fragment, state, "conversation_mismatch", "run is already bound to another conversation") - } - if state != nil { - state.updatedAt = now - } - if fragment.GetFragmentCount() == 0 || fragment.GetFragmentCount() > chatIngressFragmentMaxCount || fragment.GetFragmentIndex() >= fragment.GetFragmentCount() { - s.mu.Unlock() - return rejectedChatIngressFragmentAck(agentID, fragment, state, "invalid_fragment_index", "fragment index or count is invalid") - } - if len(fragment.GetEncodedRecordChunk()) == 0 || len(fragment.GetEncodedRecordChunk()) > chatIngressFragmentChunkBytes { - s.mu.Unlock() - return rejectedChatIngressFragmentAck(agentID, fragment, state, "fragment_chunk_too_large", "fragment chunk must be between 1 byte and 64 KiB") - } - if fragment.GetEncodedRecordBytes() == 0 || fragment.GetEncodedRecordBytes() > chatIngressEncodedRecordMaxBytes { - s.mu.Unlock() - return rejectedChatIngressFragmentAck(agentID, fragment, state, "fragment_record_too_large", "encoded record exceeds the bounded projection framing limit") - } - if len(fragmentHash) != sha256.Size*2 { - s.mu.Unlock() - return rejectedChatIngressFragmentAck(agentID, fragment, state, "invalid_fragment_sha256", "fragment sha256 must be a 64-character hex digest") - } - if _, err := hex.DecodeString(fragmentHash); err != nil { - s.mu.Unlock() - return rejectedChatIngressFragmentAck(agentID, fragment, state, "invalid_fragment_sha256", "fragment sha256 is not valid hexadecimal") - } - if state != nil { - expected := state.committedThrough + 1 - if fragment.GetSourceSeq() < expected { - s.mu.Unlock() - return continueChatIngressAck(runID, conversationID, state) - } - if fragment.GetSourceSeq() > expected && !state.checkpointRequested { - noteChatIngressGap(agentID, runID, conversationID, state, expected, "fragment_sequence_gap") - noteChatIngressReplay(agentID, runID, conversationID, state, expected, "fragment_sequence_gap") - s.mu.Unlock() - return replayChatIngressAck(runID, conversationID, state) - } - } - assembly := s.ingressFragments[key] - if assembly == nil { - assembly = &chatIngressFragmentAssembly{ - conversationID: conversationID, - fragmentCount: fragment.GetFragmentCount(), - encodedRecordBytes: fragment.GetEncodedRecordBytes(), - sha256: fragmentHash, - chunks: make([][]byte, fragment.GetFragmentCount()), - expiresAt: now.Add(chatIngressFragmentTTL), - } - s.ingressFragments[key] = assembly - s.startReaper() - } - if assembly.conversationID != conversationID || - assembly.fragmentCount != fragment.GetFragmentCount() || - assembly.encodedRecordBytes != fragment.GetEncodedRecordBytes() || - assembly.sha256 != fragmentHash { - delete(s.ingressFragments, key) - s.mu.Unlock() - return rejectedChatIngressFragmentAck(agentID, fragment, state, "fragment_metadata_mismatch", "fragment metadata changed during assembly") - } - chunk := fragment.GetEncodedRecordChunk() - index := fragment.GetFragmentIndex() - if existing := assembly.chunks[index]; existing != nil { - if !bytes.Equal(existing, chunk) { - delete(s.ingressFragments, key) - s.mu.Unlock() - return rejectedChatIngressFragmentAck(agentID, fragment, state, "fragment_conflict", "duplicate fragment index contains different bytes") - } - } else { - assembly.chunks[index] = append([]byte(nil), chunk...) - assembly.received++ - assembly.receivedBytes += uint64(len(chunk)) - if assembly.receivedBytes > assembly.encodedRecordBytes { - delete(s.ingressFragments, key) - s.mu.Unlock() - return rejectedChatIngressFragmentAck(agentID, fragment, state, "fragment_size_mismatch", "assembled fragment bytes exceed encoded_record_bytes") - } - } - if assembly.received != assembly.fragmentCount { - s.mu.Unlock() - return nil - } - encoded := make([]byte, 0, assembly.receivedBytes) - for _, assembledChunk := range assembly.chunks { - encoded = append(encoded, assembledChunk...) - } - delete(s.ingressFragments, key) - stateSnapshot := cloneChatIngressRunState(state) - s.mu.Unlock() - - if uint64(len(encoded)) != assembly.encodedRecordBytes { - return rejectedChatIngressFragmentAck(agentID, fragment, stateSnapshot, "fragment_size_mismatch", "assembled record is %d bytes, expected %d", len(encoded), assembly.encodedRecordBytes) - } - actualHash := sha256.Sum256(encoded) - if hex.EncodeToString(actualHash[:]) != assembly.sha256 { - return rejectedChatIngressFragmentAck(agentID, fragment, stateSnapshot, "fragment_hash_mismatch", "assembled record sha256 does not match") - } - var record gatewayv2.ChatIngressRecord - if err := proto.Unmarshal(encoded, &record); err != nil { - return rejectedChatIngressFragmentAck(agentID, fragment, stateSnapshot, "invalid_fragment_record", "assembled record is not valid protobuf: %v", err) - } - return m.ingestChatIngressRecords(agentID, runID, conversationID, fragment.GetSourceSeq(), []*gatewayv2.ChatIngressRecord{&record}) -} - -func cloneChatIngressRunState(state *chatIngressRunState) *chatIngressRunState { - if state == nil { - return nil - } - cloned := *state - return &cloned -} - -func noteChatIngressGap( - agentID string, - runID string, - conversationID string, - state *chatIngressRunState, - expected uint64, - reason string, -) { - if state == nil || state.gapObservedAt == expected { - return - } - state.gapObservedAt = expected - observability.Usage.ChatIngressGapsTotal.Add(1) - slog.Warn("chat_ingress_gap", - "agent_id", agentID, - "run_id", runID, - "conversation_id", conversationID, - "seq", expected, - "reason", reason, - ) -} - -func requestChatIngressCheckpoint( - agentID string, - runID string, - conversationID string, - state *chatIngressRunState, - expected uint64, - reason string, -) { - if state == nil || state.checkpointRequested { - return - } - state.checkpointRequested = true - state.replayRequestedAt = 0 - observability.Usage.ChatIngressCheckpointRequestsTotal.Add(1) - slog.Info("chat_ingress_checkpoint_requested", - "agent_id", agentID, - "run_id", runID, - "conversation_id", conversationID, - "seq", expected, - "reason", reason, - ) -} - -func noteChatIngressReplay( - agentID string, - runID string, - conversationID string, - state *chatIngressRunState, - expected uint64, - reason string, -) { - if state == nil || state.replayRequestedAt == expected { - return - } - state.replayRequestedAt = expected - observability.Usage.ChatIngressReplayRequestsTotal.Add(1) - slog.Warn("chat_ingress_replay_requested", - "agent_id", agentID, - "run_id", runID, - "conversation_id", conversationID, - "seq", expected, - "reason", reason, - ) -} - -func noteChatIngressCheckpointCommitted( - agentID string, - runID string, - conversationID string, - producerSeq uint64, - checkpoint *gatewayv2.ChatIngressCheckpoint, -) { - observability.Usage.ChatIngressCheckpointsCommittedTotal.Add(1) - slog.Info("chat_ingress_checkpoint_committed", - "agent_id", agentID, - "run_id", runID, - "conversation_id", conversationID, - "seq", producerSeq, - "hash", normalizedProjectionHash(checkpoint), - "size", checkpoint.GetUncompressedBytes(), - "reason", "checkpoint_committed", - ) -} - -func noteChatIngressTerminalCommitted( - agentID string, - runID string, - conversationID string, - producerSeq uint64, - terminal *gatewayv2.ChatIngressTerminal, -) { - observability.Usage.ChatIngressTerminalsCommittedTotal.Add(1) - slog.Info("chat_ingress_terminal_committed", - "agent_id", agentID, - "run_id", runID, - "conversation_id", conversationID, - "seq", producerSeq, - "hash", normalizedProjectionHash(terminal), - "size", terminal.GetUncompressedBytes(), - "reason", "terminal_"+strings.TrimSpace(terminal.GetState()), - ) -} - -func rejectedChatIngressFragmentAck( - agentID string, - fragment *gatewayv2.ChatIngressFragment, - state *chatIngressRunState, - code string, - format string, - args ...any, -) *gatewayv2.ChatIngressAck { - runID := "" - conversationID := "" - var sourceSeq uint64 - var size uint64 - hash := "" - if fragment != nil { - runID = strings.TrimSpace(fragment.GetRunId()) - conversationID = strings.TrimSpace(fragment.GetConversationId()) - sourceSeq = fragment.GetSourceSeq() - size = fragment.GetEncodedRecordBytes() - hash = safeChatIngressHash(fragment.GetSha256()) - } - rejected := observability.Usage.ChatIngressFragmentRejectsTotal.Add(1) - if rejected == 1 || rejected%100 == 0 { - slog.Warn("chat_ingress_fragment_rejected", - "agent_id", strings.TrimSpace(agentID), - "run_id", runID, - "conversation_id", conversationID, - "seq", sourceSeq, - "hash", hash, - "size", size, - "reason", code, - ) - } - return rejectedChatIngressAck(runID, conversationID, state, code, format, args...) -} - -func safeChatIngressHash(value string) string { - normalized := strings.ToLower(strings.TrimSpace(value)) - if len(normalized) != sha256.Size*2 { - return "" - } - if _, err := hex.DecodeString(normalized); err != nil { - return "" - } - return normalized -} - -func (s *conversationStreamStore) reliableIngressOwnsRunLocked(agentID, runID string) bool { - return s.ingressRuns[agentScopedKey(agentID, runID)] != nil -} - -func chatIngressFragmentKey(agentID, runID string, sourceSeq uint64) string { - return fmt.Sprintf("%s\x00%s\x00%d", strings.TrimSpace(agentID), strings.TrimSpace(runID), sourceSeq) -} - -func isChatIngressProjectionRecord(record *gatewayv2.ChatIngressRecord) bool { - return record != nil && (record.GetCheckpoint() != nil || record.GetTerminal() != nil) -} - -func projectionFromRecord(record *gatewayv2.ChatIngressRecord) chatIngressProjection { - if record == nil { - return nil - } - if checkpoint := record.GetCheckpoint(); checkpoint != nil { - return checkpoint - } - if terminal := record.GetTerminal(); terminal != nil { - return terminal - } - return nil -} - -func normalizedProjectionHash(projection chatIngressProjection) string { - if projection == nil { - return "" - } - return strings.ToLower(strings.TrimSpace(projection.GetSha256())) -} - -func validateProjectionRevision(state *chatIngressRunState, projection chatIngressProjection, kind string) error { - if state == nil || projection == nil { - return nil - } - revision := projection.GetRevision() - if revision < state.latestCheckpointRevision { - return newChatIngressRecordError("stale_checkpoint", "%s revision %d is older than committed revision %d", kind, revision, state.latestCheckpointRevision) - } - if revision == state.latestCheckpointRevision && state.latestCheckpointHash != "" && normalizedProjectionHash(projection) != state.latestCheckpointHash { - return newChatIngressRecordError("conflicting_checkpoint_revision", "%s revision %d conflicts with the committed projection hash", kind, revision) - } - return nil -} - -func validateDuplicateTerminal(state *chatIngressRunState, firstSeq uint64, records []*gatewayv2.ChatIngressRecord) error { - if state == nil || !state.terminalCommitted || state.terminalSeq < firstSeq { - return nil - } - offset := state.terminalSeq - firstSeq - if offset >= uint64(len(records)) { - return nil - } - terminal := records[offset].GetTerminal() - if terminal == nil || normalizedProjectionHash(terminal) != state.terminalHash { - return newChatIngressRecordError("conflicting_terminal", "producer sequence %d conflicts with the committed terminal", state.terminalSeq) - } - return nil -} - -func ingressErrorCode(err error) string { - if ingressErr, ok := err.(*chatIngressRecordError); ok { - return ingressErr.code - } - return "invalid_record" -} - -func continueChatIngressAck(runID, conversationID string, state *chatIngressRunState) *gatewayv2.ChatIngressAck { - return chatIngressAck(runID, conversationID, state, gatewayv2.ChatIngressAck_CONTINUE, "", "") -} - -func replayChatIngressAck(runID, conversationID string, state *chatIngressRunState) *gatewayv2.ChatIngressAck { - return chatIngressAck(runID, conversationID, state, gatewayv2.ChatIngressAck_REPLAY_FROM_EXPECTED, "sequence_gap", "producer sequence gap detected") -} - -func checkpointChatIngressAck(runID, conversationID string, state *chatIngressRunState) *gatewayv2.ChatIngressAck { - return chatIngressAck(runID, conversationID, state, gatewayv2.ChatIngressAck_SEND_CHECKPOINT, "checkpoint_required", "gateway cursor is outside the retained replay window") -} - -func rejectedChatIngressAck(runID, conversationID string, state *chatIngressRunState, code, format string, args ...any) *gatewayv2.ChatIngressAck { - return chatIngressAck(runID, conversationID, state, gatewayv2.ChatIngressAck_REJECTED, code, fmt.Sprintf(format, args...)) -} - -func chatIngressAck( - runID string, - conversationID string, - state *chatIngressRunState, - action gatewayv2.ChatIngressAck_Action, - errorCode string, - errorMessage string, -) *gatewayv2.ChatIngressAck { - ack := &gatewayv2.ChatIngressAck{ - RunId: runID, - ConversationId: conversationID, - ExpectedNext: 1, - Action: action, - ErrorCode: errorCode, - ErrorMessage: errorMessage, - } - if state != nil { - ack.CommittedThrough = state.committedThrough - ack.ExpectedNext = state.committedThrough + 1 - ack.TerminalCommitted = state.terminalCommitted - } - return ack -} diff --git a/crates/agent-gateway/internal/session/conversation_reliable_ingress_test.go b/crates/agent-gateway/internal/session/conversation_reliable_ingress_test.go deleted file mode 100644 index 9d5431252..000000000 --- a/crates/agent-gateway/internal/session/conversation_reliable_ingress_test.go +++ /dev/null @@ -1,672 +0,0 @@ -package session - -import ( - "crypto/sha256" - "encoding/hex" - "strings" - "testing" - "time" - - "github.com/klauspost/compress/zstd" - "google.golang.org/protobuf/proto" - - "github.com/liveagent/agent-gateway/internal/observability" - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -func TestChatIngressDeduplicatesAndCommitsTerminalSnapshotBeforeFinish(t *testing.T) { - manager := NewManager() - projection := reliableIngressProjection(t, `[{"type":"assistant","content":"hello"}]`) - batch := &gatewayv2.ChatIngressBatch{ - RunId: "run-1", - ConversationId: "conv-1", - FirstSeq: 1, - Records: []*gatewayv2.ChatIngressRecord{ - reliableIngressDelta(`{"type":"token","text":"hello"}`), - reliableIngressTerminal(projection, 1, "completed"), - }, - } - - ack := manager.ingestChatIngressBatch("agent-1", batch) - if ack.GetAction() != gatewayv2.ChatIngressAck_CONTINUE || ack.GetCommittedThrough() != 2 || !ack.GetTerminalCommitted() { - t.Fatalf("first ack = %#v", ack) - } - duplicateAck := manager.ingestChatIngressBatch("agent-1", batch) - if duplicateAck.GetCommittedThrough() != 2 || !duplicateAck.GetTerminalCommitted() { - t.Fatalf("duplicate ack = %#v", duplicateAck) - } - - subscription := manager.SubscribeConversationStream("agent-1", "conv-1", 0, "") - defer subscription.Cleanup() - gotTypes := eventTypes(subscription.Events) - wantTypes := []string{"run_started", "token", StreamEventContentSnapshot, "run_finished"} - if strings.Join(gotTypes, ",") != strings.Join(wantTypes, ",") { - t.Fatalf("event types = %v, want %v", gotTypes, wantTypes) - } - snapshot := subscription.Events[2] - if snapshot.Payload["entries_json"] != `[{"type":"assistant","content":"hello"}]` { - t.Fatalf("snapshot entries_json = %#v", snapshot.Payload["entries_json"]) - } - if snapshot.Seq+1 != subscription.Events[3].Seq { - t.Fatalf("snapshot seq %d is not immediately before terminal seq %d", snapshot.Seq, subscription.Events[3].Seq) - } -} - -func TestChatIngressResumeAfterTerminalSnapshotReplaysFinish(t *testing.T) { - manager := NewManager() - projection := reliableIngressProjection(t, `[{"type":"assistant","content":"complete reply"}]`) - ack := manager.ingestChatIngressBatch("agent-1", &gatewayv2.ChatIngressBatch{ - RunId: "run-1", - ConversationId: "conv-1", - FirstSeq: 1, - Records: []*gatewayv2.ChatIngressRecord{ - reliableIngressDelta(`{"type":"token","text":"partial"}`), - reliableIngressTerminal(projection, 1, "completed"), - }, - }) - if !ack.GetTerminalCommitted() { - t.Fatalf("terminal ack = %#v", ack) - } - - initial := manager.SubscribeConversationStream("agent-1", "conv-1", 0, "") - var snapshotSeq int64 - for _, event := range initial.Events { - if event.Type == StreamEventContentSnapshot { - snapshotSeq = event.Seq - break - } - } - initial.Cleanup() - if snapshotSeq == 0 { - t.Fatalf("terminal snapshot missing from replay: %v", eventTypes(initial.Events)) - } - - resumed := manager.SubscribeConversationStream( - "agent-1", - "conv-1", - snapshotSeq, - initial.StreamEpoch, - ) - defer resumed.Cleanup() - if resumed.Reset { - t.Fatal("resume after retained snapshot unexpectedly reset") - } - if len(resumed.Events) != 1 { - t.Fatalf("resume events = %v, want only run_finished", eventTypes(resumed.Events)) - } - finished := resumed.Events[0] - if finished.Type != StreamEventRunFinished || finished.RunID != "run-1" { - t.Fatalf("resumed event = %s/%s, want run_finished/run-1", finished.Type, finished.RunID) - } - if finished.Seq != snapshotSeq+1 || finished.Payload["status"] != "completed" { - t.Fatalf("resumed finish seq/status = %d/%v, want %d/completed", finished.Seq, finished.Payload["status"], snapshotSeq+1) - } -} - -func TestChatIngressGapAndInvalidTerminalDoNotAdvanceCursor(t *testing.T) { - manager := NewManager() - first := manager.ingestChatIngressBatch("agent-1", &gatewayv2.ChatIngressBatch{ - RunId: "run-1", - ConversationId: "conv-1", - FirstSeq: 1, - Records: []*gatewayv2.ChatIngressRecord{reliableIngressDelta(`{"type":"token","text":"a"}`)}, - }) - if first.GetCommittedThrough() != 1 { - t.Fatalf("first ack = %#v", first) - } - - gap := manager.ingestChatIngressBatch("agent-1", &gatewayv2.ChatIngressBatch{ - RunId: "run-1", - ConversationId: "conv-1", - FirstSeq: 3, - Records: []*gatewayv2.ChatIngressRecord{reliableIngressDelta(`{"type":"token","text":"c"}`)}, - }) - if gap.GetAction() != gatewayv2.ChatIngressAck_REPLAY_FROM_EXPECTED || gap.GetExpectedNext() != 2 { - t.Fatalf("gap ack = %#v", gap) - } - - badProjection := reliableIngressProjection(t, `["complete"]`) - badProjection.sha256 = strings.Repeat("0", 64) - rejected := manager.ingestChatIngressBatch("agent-1", &gatewayv2.ChatIngressBatch{ - RunId: "run-1", - ConversationId: "conv-1", - FirstSeq: 2, - Records: []*gatewayv2.ChatIngressRecord{reliableIngressTerminal(badProjection, 1, "completed")}, - }) - if rejected.GetAction() != gatewayv2.ChatIngressAck_REJECTED || rejected.GetErrorCode() != "projection_hash_mismatch" { - t.Fatalf("rejected ack = %#v", rejected) - } - if rejected.GetCommittedThrough() != 1 || rejected.GetExpectedNext() != 2 || rejected.GetTerminalCommitted() { - t.Fatalf("rejected cursor = %#v", rejected) - } - - subscription := manager.SubscribeConversationStream("agent-1", "conv-1", 0, "") - defer subscription.Cleanup() - for _, event := range subscription.Events { - if event.Type == StreamEventContentSnapshot || event.Type == StreamEventRunFinished { - t.Fatalf("invalid terminal leaked event %q", event.Type) - } - } -} - -func TestChatIngressResumeRequestsAndAcceptsCheckpointBaseline(t *testing.T) { - manager := NewManager() - acks := manager.ingestChatIngressResume("agent-1", &gatewayv2.ChatIngressResume{ - Runs: []*gatewayv2.ChatIngressRunResume{{ - RunId: "run-1", - ConversationId: "conv-1", - ReplayFromSeq: 7, - ReplayThroughSeq: 9, - NextSeq: 10, - }}, - }) - if len(acks) != 1 || acks[0].GetAction() != gatewayv2.ChatIngressAck_SEND_CHECKPOINT { - t.Fatalf("resume acks = %#v", acks) - } - - projection := reliableIngressProjection(t, `[]`) - ack := manager.ingestChatIngressBatch("agent-1", &gatewayv2.ChatIngressBatch{ - RunId: "run-1", - ConversationId: "conv-1", - FirstSeq: 9, - Records: []*gatewayv2.ChatIngressRecord{{ - Payload: &gatewayv2.ChatIngressRecord_Checkpoint{ - Checkpoint: &gatewayv2.ChatIngressCheckpoint{ - CoversThroughSeq: 8, - Revision: 1, - CompressedProjection: projection.compressed, - UncompressedBytes: uint64(len(projection.raw)), - Sha256: projection.sha256, - ContentComplete: false, - HistoryRequired: true, - }, - }, - }}, - }) - if ack.GetAction() != gatewayv2.ChatIngressAck_CONTINUE || ack.GetCommittedThrough() != 9 || ack.GetExpectedNext() != 10 { - t.Fatalf("checkpoint baseline ack = %#v", ack) - } -} - -func TestChatIngressAbsentGapRequiresCheckpointAndExactCoverage(t *testing.T) { - manager := NewManager() - gap := manager.ingestChatIngressBatch("agent-1", &gatewayv2.ChatIngressBatch{ - RunId: "run-gap", - ConversationId: "conv-1", - FirstSeq: 3, - Records: []*gatewayv2.ChatIngressRecord{reliableIngressDelta(`{"type":"token","text":"late"}`)}, - }) - if gap.GetAction() != gatewayv2.ChatIngressAck_SEND_CHECKPOINT || gap.GetExpectedNext() != 1 { - t.Fatalf("absent gap ack = %#v", gap) - } - - projection := reliableIngressProjection(t, `[]`) - badCoverage := manager.ingestChatIngressBatch("agent-1", &gatewayv2.ChatIngressBatch{ - RunId: "run-gap", - ConversationId: "conv-1", - FirstSeq: 3, - Records: []*gatewayv2.ChatIngressRecord{reliableIngressCheckpoint(projection, 3, 1)}, - }) - if badCoverage.GetAction() != gatewayv2.ChatIngressAck_REJECTED || badCoverage.GetErrorCode() != "invalid_checkpoint_coverage" { - t.Fatalf("bad coverage ack = %#v", badCoverage) - } - if badCoverage.GetCommittedThrough() != 0 { - t.Fatalf("bad coverage advanced cursor: %#v", badCoverage) - } - - accepted := manager.ingestChatIngressBatch("agent-1", &gatewayv2.ChatIngressBatch{ - RunId: "run-gap", - ConversationId: "conv-1", - FirstSeq: 3, - Records: []*gatewayv2.ChatIngressRecord{reliableIngressCheckpoint(projection, 2, 1)}, - }) - if accepted.GetAction() != gatewayv2.ChatIngressAck_CONTINUE || accepted.GetCommittedThrough() != 3 { - t.Fatalf("checkpoint baseline ack = %#v", accepted) - } -} - -func TestChatIngressRejectsConflictingDuplicateTerminal(t *testing.T) { - manager := NewManager() - firstProjection := reliableIngressProjection(t, `[{"type":"assistant","content":"one"}]`) - first := manager.ingestChatIngressBatch("agent-1", &gatewayv2.ChatIngressBatch{ - RunId: "run-terminal", - ConversationId: "conv-1", - FirstSeq: 1, - Records: []*gatewayv2.ChatIngressRecord{reliableIngressTerminal(firstProjection, 0, "completed")}, - }) - if !first.GetTerminalCommitted() { - t.Fatalf("first terminal ack = %#v", first) - } - - conflictingProjection := reliableIngressProjection(t, `[{"type":"assistant","content":"two"}]`) - conflict := manager.ingestChatIngressBatch("agent-1", &gatewayv2.ChatIngressBatch{ - RunId: "run-terminal", - ConversationId: "conv-1", - FirstSeq: 1, - Records: []*gatewayv2.ChatIngressRecord{reliableIngressTerminal(conflictingProjection, 0, "completed")}, - }) - if conflict.GetAction() != gatewayv2.ChatIngressAck_REJECTED || conflict.GetErrorCode() != "conflicting_terminal" { - t.Fatalf("conflicting terminal ack = %#v", conflict) - } -} - -func TestChatIngressFragmentReassemblesOneLogicalRecord(t *testing.T) { - manager := NewManager() - record := reliableIngressDelta(`{"type":"token","text":"fragmented"}`) - encoded, err := proto.Marshal(record) - if err != nil { - t.Fatal(err) - } - hash := sha256.Sum256(encoded) - digest := hex.EncodeToString(hash[:]) - cut := len(encoded) / 2 - chunks := [][]byte{encoded[:cut], encoded[cut:]} - - for _, index := range []int{1, 0} { - ack := manager.ingestChatIngressFragment("agent-1", &gatewayv2.ChatIngressFragment{ - RunId: "run-1", - ConversationId: "conv-1", - SourceSeq: 1, - FragmentIndex: uint32(index), - FragmentCount: uint32(len(chunks)), - EncodedRecordChunk: chunks[index], - EncodedRecordBytes: uint64(len(encoded)), - Sha256: digest, - }) - if index == 1 && ack != nil { - t.Fatalf("incomplete fragment unexpectedly acked: %#v", ack) - } - if index == 0 && (ack == nil || ack.GetAction() != gatewayv2.ChatIngressAck_CONTINUE) { - t.Fatalf("fragment %d ack = %#v", index, ack) - } - } - - manager.convStreams.mu.Lock() - state := cloneChatIngressRunState(manager.convStreams.ingressRuns[agentScopedKey("agent-1", "run-1")]) - manager.convStreams.mu.Unlock() - if state == nil || state.committedThrough != 1 { - t.Fatalf("fragment cursor = %#v", state) - } - subscription := manager.SubscribeConversationStream("agent-1", "conv-1", 0, "") - defer subscription.Cleanup() - if got := eventTypes(subscription.Events); strings.Join(got, ",") != "run_started,token" { - t.Fatalf("fragment events = %v", got) - } -} - -func TestChatIngressFragmentLimitsCoverMaximumProjectionWithDefaultChunks(t *testing.T) { - if chatIngressEncodedRecordMaxBytes <= chatIngressProjectionMaxBytes { - t.Fatalf("encoded record limit %d must leave room above projection limit %d", chatIngressEncodedRecordMaxBytes, chatIngressProjectionMaxBytes) - } - required := (chatIngressEncodedRecordMaxBytes + chatIngressFragmentDefaultBytes - 1) / chatIngressFragmentDefaultBytes - if chatIngressFragmentMaxCount < required { - t.Fatalf("fragment count limit %d cannot carry %d bytes in %d-byte chunks", chatIngressFragmentMaxCount, chatIngressEncodedRecordMaxBytes, chatIngressFragmentDefaultBytes) - } -} - -func TestChatIngressRunHeartbeatAdvancesCursorWithoutBrowserEvent(t *testing.T) { - manager := NewManager() - ack := manager.ingestChatIngressBatch("agent-1", &gatewayv2.ChatIngressBatch{ - RunId: "run-1", - ConversationId: "conv-1", - FirstSeq: 1, - Records: []*gatewayv2.ChatIngressRecord{reliableIngressDelta(`{"type":"run_heartbeat","conversation_id":"conv-1"}`)}, - }) - if ack.GetCommittedThrough() != 1 || ack.GetExpectedNext() != 2 { - t.Fatalf("heartbeat ack = %#v", ack) - } - subscription := manager.SubscribeConversationStream("agent-1", "conv-1", 0, "") - defer subscription.Cleanup() - if len(subscription.Events) != 0 { - t.Fatalf("heartbeat leaked browser events: %v", eventTypes(subscription.Events)) - } -} - -func TestChatIngressOversizedProjectionDegradesToHistoryRequired(t *testing.T) { - manager := NewManager() - oversized := `[{"type":"assistant","content":"` + - strings.Repeat("x", chatIngressBrowserProjectionMaxBytes) + `"}]` - projection := reliableIngressProjection(t, oversized) - ack := manager.ingestChatIngressBatch("agent-1", &gatewayv2.ChatIngressBatch{ - RunId: "run-1", - ConversationId: "conv-1", - FirstSeq: 1, - Records: []*gatewayv2.ChatIngressRecord{ - reliableIngressDelta(`{"type":"token","text":"hello"}`), - reliableIngressCheckpoint(projection, 1, 1), - }, - }) - if ack.GetAction() != gatewayv2.ChatIngressAck_CONTINUE || ack.GetCommittedThrough() != 2 { - t.Fatalf("oversized projection ack = %#v", ack) - } - - subscription := manager.SubscribeConversationStream("agent-1", "conv-1", 0, "") - defer subscription.Cleanup() - var snapshot *ConversationEvent - for _, event := range subscription.Events { - if event.Payload["type"] == StreamEventContentSnapshot { - snapshot = event - } - } - if snapshot == nil { - t.Fatalf("no content snapshot event: %v", eventTypes(subscription.Events)) - } - if snapshot.Payload["entries_json"] != "[]" { - t.Fatalf("oversized projection was relayed to browsers: %d bytes", len(snapshot.Payload["entries_json"].(string))) - } - if snapshot.Payload["history_required"] != true || snapshot.Payload["content_complete"] != false { - t.Fatalf("degraded snapshot flags = %#v", snapshot.Payload) - } - if snapshot.Payload["sha256"] != "" { - t.Fatalf("degraded snapshot kept the original projection sha256: %#v", snapshot.Payload["sha256"]) - } - // RunSnapshot carries no degradation flags, so hydration consumers would - // treat "[]" as authoritative content; the degraded commit must leave - // latestSnapshot untouched (nil here) so the snapshot-less resubscribe - // path marks contentStale and history converges. - manager.convStreams.mu.Lock() - stream := manager.convStreams.streams[conversationStreamKey("agent-1", "conv-1")] - latest := stream.latestSnapshot - manager.convStreams.mu.Unlock() - if latest != nil { - t.Fatalf("degraded projection overwrote latestSnapshot: %#v", latest) - } -} - -func TestReliableIngressRunIgnoresLegacyTerminalSignals(t *testing.T) { - manager := NewManager() - manager.ingestChatIngressBatch("agent-1", &gatewayv2.ChatIngressBatch{ - RunId: "run-1", - ConversationId: "conv-1", - FirstSeq: 1, - Records: []*gatewayv2.ChatIngressRecord{reliableIngressDelta(`{"type":"token","text":"hello"}`)}, - }) - manager.ingestChatEvent("agent-1", "run-1", &gatewayv2.ChatEvent{ - Type: gatewayv2.ChatEvent_DONE, - ConversationId: "conv-1", - Data: `{}`, - }) - manager.ingestChatControl("agent-1", "run-1", &gatewayv2.ChatControlEvent{ - RequestId: "run-1", - ConversationId: "conv-1", - Type: "completed", - }) - manager.ingestRuntimeSnapshot("agent-1", &gatewayv2.ChatRuntimeSnapshot{ - RunId: "run-1", - ConversationId: "conv-1", - State: "completed", - }) - manager.convStreams.onRuntimeStatus("agent-1", &gatewayv2.RuntimeStatusEvent{ - FinishedRuns: []*gatewayv2.ChatRunReport{{ - RunId: "run-1", - ConversationId: "conv-1", - State: "completed", - }}, - }, time.Now()) - - subscription := manager.SubscribeConversationStream("agent-1", "conv-1", 0, "") - for _, event := range subscription.Events { - if event.Type == StreamEventRunFinished { - subscription.Cleanup() - t.Fatal("legacy signal finished a reliable ingress run") - } - } - subscription.Cleanup() - - projection := reliableIngressProjection(t, `[]`) - ack := manager.ingestChatIngressBatch("agent-1", &gatewayv2.ChatIngressBatch{ - RunId: "run-1", - ConversationId: "conv-1", - FirstSeq: 2, - Records: []*gatewayv2.ChatIngressRecord{reliableIngressTerminal(projection, 1, "completed")}, - }) - if !ack.GetTerminalCommitted() { - t.Fatalf("reliable terminal ack = %#v", ack) - } - finalSubscription := manager.SubscribeConversationStream("agent-1", "conv-1", 0, "") - defer finalSubscription.Cleanup() - finished := 0 - for _, event := range finalSubscription.Events { - if event.Type == StreamEventRunFinished { - finished++ - } - } - if finished != 1 { - t.Fatalf("run_finished count = %d, want 1", finished) - } -} - -func TestAgentStreamOverflowClosesOnlySlowStream(t *testing.T) { - stream := &agentStream{ - ch: make(chan *gatewayv2.AgentEnvelope, 1), - done: make(chan struct{}), - } - if !stream.send(&gatewayv2.AgentEnvelope{RequestId: "first"}) { - t.Fatal("first send failed") - } - result := make(chan bool, 1) - go func() { - result <- stream.send(&gatewayv2.AgentEnvelope{RequestId: "overflow"}) - }() - select { - case sent := <-result: - if sent { - t.Fatal("overflow send unexpectedly succeeded") - } - case <-time.After(time.Second): - t.Fatal("slow agent stream blocked dispatcher") - } - select { - case <-stream.done: - default: - t.Fatal("overflowed stream was not closed") - } -} - -func TestDispatchFromAgentQueuesChatIngressAck(t *testing.T) { - manager := NewManager() - agentSession := NewAgentSession(AuthSnapshot{AgentID: "agent-1", SessionID: "session-1"}) - manager.SetSession(agentSession) - defer manager.ClearSession(agentSession) - - manager.DispatchFromAgentForSession(agentSession, &gatewayv2.AgentEnvelope{ - RequestId: "ingress-1", - Payload: &gatewayv2.AgentEnvelope_ChatIngressBatch{ - ChatIngressBatch: &gatewayv2.ChatIngressBatch{ - RunId: "run-1", - ConversationId: "conv-1", - FirstSeq: 1, - Records: []*gatewayv2.ChatIngressRecord{reliableIngressDelta(`{"type":"token","text":"hello"}`)}, - }, - }, - }) - - select { - case outbound := <-agentSession.Outbound(): - if outbound.GetRequestId() != "ingress-1" { - t.Fatalf("ack request id = %q", outbound.GetRequestId()) - } - ack := outbound.GetChatIngressAck() - if ack == nil || ack.GetCommittedThrough() != 1 || ack.GetAction() != gatewayv2.ChatIngressAck_CONTINUE { - t.Fatalf("queued ack = %#v", ack) - } - case <-time.After(time.Second): - t.Fatal("chat ingress ack was not queued") - } -} - -func TestCapableSessionRejectsLegacyChatMirrorTerminalPaths(t *testing.T) { - manager := NewManager() - agentSession := NewAgentSession(AuthSnapshot{AgentID: "agent-1", SessionID: "session-1"}) - agentSession.SetCapabilities([]string{gatewayv2.ChatIngressV1Capability}) - manager.SetSession(agentSession) - defer manager.ClearSession(agentSession) - - manager.DispatchFromAgentForSession(agentSession, &gatewayv2.AgentEnvelope{ - RequestId: "run-legacy", - Payload: &gatewayv2.AgentEnvelope_ChatEvent{ChatEvent: &gatewayv2.ChatEvent{ - Type: gatewayv2.ChatEvent_DONE, - ConversationId: "conv-1", - }}, - }) - manager.DispatchFromAgentForSession(agentSession, &gatewayv2.AgentEnvelope{ - RequestId: "run-legacy", - Payload: &gatewayv2.AgentEnvelope_ChatRuntimeSnapshot{ChatRuntimeSnapshot: &gatewayv2.ChatRuntimeSnapshot{ - RunId: "run-legacy", - ConversationId: "conv-1", - State: "completed", - }}, - }) - manager.DispatchFromAgentForSession(agentSession, &gatewayv2.AgentEnvelope{ - RequestId: "run-legacy", - Payload: &gatewayv2.AgentEnvelope_ChatControl{ChatControl: &gatewayv2.ChatControlEvent{ - RequestId: "run-legacy", - ConversationId: "conv-1", - Type: "completed", - }}, - }) - - subscription := manager.SubscribeConversationStream("agent-1", "conv-1", 0, "") - defer subscription.Cleanup() - if len(subscription.Events) != 0 { - t.Fatalf("legacy mirror paths leaked events: %v", eventTypes(subscription.Events)) - } -} - -func TestReliableIngressObservabilityCountsStateTransitionsOnce(t *testing.T) { - manager := NewManager() - gapBefore := observability.Usage.ChatIngressGapsTotal.Load() - replayBefore := observability.Usage.ChatIngressReplayRequestsTotal.Load() - checkpointRequestBefore := observability.Usage.ChatIngressCheckpointRequestsTotal.Load() - checkpointCommittedBefore := observability.Usage.ChatIngressCheckpointsCommittedTotal.Load() - terminalBefore := observability.Usage.ChatIngressTerminalsCommittedTotal.Load() - fragmentRejectBefore := observability.Usage.ChatIngressFragmentRejectsTotal.Load() - - manager.ingestChatIngressBatch("agent-observe", &gatewayv2.ChatIngressBatch{ - RunId: "run-gap", - ConversationId: "conv-gap", - FirstSeq: 1, - Records: []*gatewayv2.ChatIngressRecord{reliableIngressDelta(`{"type":"token","text":"one"}`)}, - }) - gapBatch := &gatewayv2.ChatIngressBatch{ - RunId: "run-gap", - ConversationId: "conv-gap", - FirstSeq: 3, - Records: []*gatewayv2.ChatIngressRecord{reliableIngressDelta(`{"type":"token","text":"three"}`)}, - } - manager.ingestChatIngressBatch("agent-observe", gapBatch) - manager.ingestChatIngressBatch("agent-observe", gapBatch) - - resume := &gatewayv2.ChatIngressResume{Runs: []*gatewayv2.ChatIngressRunResume{{ - RunId: "run-checkpoint", - ConversationId: "conv-checkpoint", - ReplayFromSeq: 4, - ReplayThroughSeq: 4, - NextSeq: 5, - }}} - manager.ingestChatIngressResume("agent-observe", resume) - manager.ingestChatIngressResume("agent-observe", resume) - projection := reliableIngressProjection(t, `[]`) - manager.ingestChatIngressBatch("agent-observe", &gatewayv2.ChatIngressBatch{ - RunId: "run-checkpoint", - ConversationId: "conv-checkpoint", - FirstSeq: 4, - Records: []*gatewayv2.ChatIngressRecord{reliableIngressCheckpoint(projection, 3, 1)}, - }) - manager.ingestChatIngressBatch("agent-observe", &gatewayv2.ChatIngressBatch{ - RunId: "run-gap", - ConversationId: "conv-gap", - FirstSeq: 2, - Records: []*gatewayv2.ChatIngressRecord{reliableIngressTerminal(projection, 1, "completed")}, - }) - manager.ingestChatIngressFragment("agent-observe", &gatewayv2.ChatIngressFragment{ - RunId: "run-fragment", - ConversationId: "conv-fragment", - SourceSeq: 1, - FragmentIndex: 1, - FragmentCount: 1, - EncodedRecordChunk: []byte("x"), - EncodedRecordBytes: 1, - Sha256: strings.Repeat("0", 64), - }) - - if got := observability.Usage.ChatIngressGapsTotal.Load() - gapBefore; got != 1 { - t.Fatalf("gap metric delta = %d, want 1", got) - } - if got := observability.Usage.ChatIngressReplayRequestsTotal.Load() - replayBefore; got != 1 { - t.Fatalf("replay metric delta = %d, want 1", got) - } - if got := observability.Usage.ChatIngressCheckpointRequestsTotal.Load() - checkpointRequestBefore; got != 1 { - t.Fatalf("checkpoint request metric delta = %d, want 1", got) - } - if got := observability.Usage.ChatIngressCheckpointsCommittedTotal.Load() - checkpointCommittedBefore; got != 1 { - t.Fatalf("checkpoint committed metric delta = %d, want 1", got) - } - if got := observability.Usage.ChatIngressTerminalsCommittedTotal.Load() - terminalBefore; got != 1 { - t.Fatalf("terminal metric delta = %d, want 1", got) - } - if got := observability.Usage.ChatIngressFragmentRejectsTotal.Load() - fragmentRejectBefore; got != 1 { - t.Fatalf("fragment reject metric delta = %d, want 1", got) - } -} - -type reliableIngressProjectionData struct { - raw []byte - compressed []byte - sha256 string -} - -func reliableIngressProjection(t *testing.T, entriesJSON string) reliableIngressProjectionData { - t.Helper() - encoder, err := zstd.NewWriter(nil) - if err != nil { - t.Fatal(err) - } - defer encoder.Close() - raw := []byte(entriesJSON) - compressed := encoder.EncodeAll(raw, nil) - hash := sha256.Sum256(raw) - return reliableIngressProjectionData{ - raw: raw, - compressed: compressed, - sha256: hex.EncodeToString(hash[:]), - } -} - -func reliableIngressDelta(eventJSON string) *gatewayv2.ChatIngressRecord { - return &gatewayv2.ChatIngressRecord{ - Payload: &gatewayv2.ChatIngressRecord_Delta{ - Delta: &gatewayv2.ChatIngressDelta{EventJson: eventJSON}, - }, - } -} - -func reliableIngressTerminal(projection reliableIngressProjectionData, coversThrough uint64, state string) *gatewayv2.ChatIngressRecord { - return &gatewayv2.ChatIngressRecord{ - Payload: &gatewayv2.ChatIngressRecord_Terminal{ - Terminal: &gatewayv2.ChatIngressTerminal{ - CoversThroughSeq: coversThrough, - Revision: 1, - CompressedProjection: projection.compressed, - UncompressedBytes: uint64(len(projection.raw)), - Sha256: projection.sha256, - ContentComplete: true, - State: state, - }, - }, - } -} - -func reliableIngressCheckpoint(projection reliableIngressProjectionData, coversThrough, revision uint64) *gatewayv2.ChatIngressRecord { - return &gatewayv2.ChatIngressRecord{ - Payload: &gatewayv2.ChatIngressRecord_Checkpoint{ - Checkpoint: &gatewayv2.ChatIngressCheckpoint{ - CoversThroughSeq: coversThrough, - Revision: revision, - CompressedProjection: projection.compressed, - UncompressedBytes: uint64(len(projection.raw)), - Sha256: projection.sha256, - }, - }, - } -} diff --git a/crates/agent-gateway/internal/session/conversation_stream.go b/crates/agent-gateway/internal/session/conversation_stream.go deleted file mode 100644 index 96519b0a1..000000000 --- a/crates/agent-gateway/internal/session/conversation_stream.go +++ /dev/null @@ -1,1433 +0,0 @@ -package session - -import ( - "strings" - "sync" - "time" - - "github.com/google/uuid" - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -// The conversation stream store is the authoritative relay state for chat: -// one ordered event log per conversation with a monotonic seq, a single -// current-run activity record, and persistent per-conversation subscribers. -// Runs are events inside the stream, not stream boundaries. -// -// Invariants (all enforced under the single store mutex): -// 1. Seq is conversation-scoped and monotonic; runs do not own seq. -// 2. run_finished is emitted exactly once per run — the first terminal -// signal wins, later duplicates are swallowed via the finished-run ring. -// 3. Run handoff is supersession: run_started(B) while A is running -// atomically synthesizes run_finished(A) first. -// 4. Activity events are composed inside the locked transition that changed -// them, so they always carry the run id. -// 5. Subscriber sends happen under the mutex (non-blocking); an overflowing -// subscriber is closed and resumes by re-subscribing with after_seq. -const ( - conversationEventRetention = 10 * time.Minute - conversationMaxEvents = 4096 - conversationMaxEventBytes = 8 << 20 - conversationIdleRetention = 30 * time.Minute - conversationStaleRunTimeout = 10 * time.Minute - conversationOfflineRunTimeout = 30 * time.Minute - conversationReaperInterval = time.Minute - conversationFinishedRunMemory = 8 - conversationSubscriberBuffer = 256 - pendingChatRunRetention = 5 * time.Minute - chatCommandDedupeRetention = 24 * time.Hour - // conversationRunReportLostTimeout is the grace window before a run absent - // from the desktop's run reports is finalized as lost. - conversationRunReportLostTimeout = 15 * time.Second -) - -const ( - RunActivityQueued = "queued" - RunActivityRunning = "running" - RunActivityCancelling = "cancelling" -) - -// Normalized event types appended to the conversation log. -const ( - StreamEventRunStarted = "run_started" - StreamEventRunFinished = "run_finished" - StreamEventRunQueued = "run_queued" - StreamEventSnapshot = "snapshot" - StreamEventContentSnapshot = "run_content_snapshot" - // StreamEventRebased signals an edit-resend truncation: subscribers drop - // the edited user message and everything after it before the new - // user_message arrives. Seeded by the gateway for webui edit_resend - // commands and synthesized on ingress for GUI-local edits. - StreamEventRebased = "rebased" -) - -// RunActivity describes the current run of a conversation. A nil activity -// means the conversation is idle. -type RunActivity struct { - ConversationID string - AgentID string - RunID string - ClientRequestID string - State string - ToolStatus string - ToolStatusIsCompaction bool - StartedSeq int64 - Workdir string - UpdatedAt time.Time -} - -// RunSnapshot is the latest runtime snapshot for a conversation's run. It is -// not part of the seq log; it hydrates late joiners when the buffer cannot -// cover the active run from its start. -type RunSnapshot struct { - RunID string - Revision int64 - EntriesJSON string - ToolStatus string - ToolStatusIsCompaction bool - Workdir string - // AsOfSeq is the conversation's last log seq when this snapshot was - // ingested: the snapshot already represents every event up to and - // including it, so clients rebuilding from the snapshot must only apply - // replayed events with a higher seq. - AsOfSeq int64 - UpdatedAt time.Time -} - -// ConversationEvent is one entry of a conversation log. Payload is the final -// wire shape (including conversation_id/run_id/seq/type) and is frozen after -// append — subscribers must never mutate it. -type ConversationEvent struct { - ConversationID string - RunID string - Seq int64 - Type string - Payload map[string]any - ReceivedAt time.Time - - approxBytes int -} - -// ConversationActivityEvent is the broadcast shape for the chat.activity hub. -type ConversationActivityEvent struct { - ConversationID string - AgentID string - RunID string - ClientRequestID string - Running bool - State string - Workdir string - UpdatedAt time.Time -} - -// ChatCommandUpdate notifies the connection that issued a chat command about -// pre-stream outcomes. -type ChatCommandUpdate struct { - AgentID string - RunID string - ClientRequestID string - ConversationID string - Phase string // "bound" | "queued_in_gui" | "failed" - ErrorCode string - Message string -} - -type streamSubscriber struct { - id int - ch chan *ConversationEvent - overflowed bool - closed bool -} - -type conversationStream struct { - conversationID string - streamEpoch string - workdir string - // agentID 是会话流的归属 Agent;streams 以 agent_id + conversation_id 组合键索引。 - agentID string - lastSeq int64 - events []*ConversationEvent - eventsBytes int - evictedThroughSeq int64 - activity *RunActivity - finishedRuns []string - latestSnapshot *RunSnapshot - // latestContentSnapshotSeq protects the newest reliable content snapshot - // (and its immediately following terminal) from the ordinary 8 MiB cap. - // The snapshot itself is independently bounded to 64 MiB at ingress. - latestContentSnapshotSeq int64 - agentEpoch uint64 - snapshotDirty bool - // runNeedsSnapshot marks an active run whose early events the buffer - // cannot reproduce (gateway restarted mid-run, or the agent reconnected - // mid-run and tokens were lost) — late joiners hydrate from the snapshot. - runNeedsSnapshot bool - subscribers map[int]*streamSubscriber - lastEventAt time.Time - updatedAt time.Time -} - -type chatRunRecord struct { - agentID string - conversationID string - clientRequestID string - // userMessageSeeded marks runs whose user_message the gateway appended at - // accept time; the agent's later USER_MESSAGE echo is swallowed so the - // message appears exactly once. - userMessageSeeded bool - // firstSeededSeq is the seq of the run's first gateway-seeded event so a - // run started via supersession still protects its seeded user_message - // from retention eviction. - firstSeededSeq int64 - // userMessageIdentityForwarded records that the desktop's authoritative - // message_id enrichment was appended after a gateway-seeded user_message. - // Reconnect replays of the desktop echo are swallowed after the first one. - userMessageIdentityForwarded bool - // deferredSeeds holds seeded payloads of a command accepted while another - // run was active: appended only when this run actually starts (or fails), - // dropped when it parks in the desktop prompt queue — so a queue-bound - // prompt never flashes a transcript bubble. - deferredSeeds []map[string]any - // queuedInGUI marks commands the desktop app parked in its prompt queue; - // the startup watchdog must leave them alone. - queuedInGUI bool - // rebaseSeeded marks runs whose rebased event was already appended — from - // the agent's ref-bearing user_message (GUI-local edits) or from the - // gateway-seeded payloads of a webui edit_resend command — so neither a - // reconnect replay nor the identity-forwarded desktop echo can seed a - // second truncation. - rebaseSeeded bool - // lostInferred marks a run whose terminal was inferred from missing - // liveness reports (desktop_run_lost and friends) rather than delivered by - // the run itself. Such a terminal is falsifiable: fresh events for the run - // prove it wrong and resurrect the run instead of being dropped as - // stragglers. - lostInferred bool - // revived marks a run resurrected after a wrong inferred terminal; further - // inferred-loss signals for it are ignored (the desktop-side ledger may - // keep repeating the stale verdict) until a genuine terminal arrives. - revived bool -} - -// isInferredRunLossCode reports whether an error code represents a liveness -// inference (nobody vouched for the run) instead of an outcome the run itself -// produced. Inferred terminals must stay reversible: the run may well be alive. -func isInferredRunLossCode(errorCode string) bool { - switch errorCode { - case "desktop_run_lost", "stale_run", "agent_offline", "desktop_runtime_lease_expired": - return true - } - return false -} - -// chatCommandDedupeRecord is the process-local idempotency key for WebUI chat -// submissions. It is created atomically with the canonical run and retained -// long enough to cover WebSocket reconnect/retry windows without keeping full -// transcript state alive. -type chatCommandDedupeRecord struct { - runID string - conversationID string - acceptedSeq int64 - createdAt time.Time -} - -// chatCommandUpdateRecord carries the latest pre-stream update for a run with -// its own timestamp: updates can be fired for runs that never had a dedupe -// record in this process (desktop replays after a gateway restart, parked -// runs older than the dedupe retention), so they are reaped independently -// instead of relying on a paired dedupe record. -type chatCommandUpdateRecord struct { - update ChatCommandUpdate - at time.Time -} - -type pendingChatRun struct { - runID string - agentID string - clientRequestID string - workdir string - seeded []map[string]any - createdAt time.Time -} - -type conversationStreamStore struct { - mu sync.Mutex - streams map[string]*conversationStream - pendingRuns map[string]*pendingChatRun - runs map[string]*chatRunRecord - commandDedup map[string]*chatCommandDedupeRecord - commandWatchers map[string][]chan ChatCommandUpdate - commandUpdates map[string]chatCommandUpdateRecord - ingressRuns map[string]*chatIngressRunState - ingressFragments map[string]*chatIngressFragmentAssembly - nextSubID int - - activityHub *chatActivityHub - - reaperOnce sync.Once - isOnline func(string) bool - - // tunable in tests - eventRetention time.Duration - maxEvents int - maxEventBytes int - idleRetention time.Duration - staleRunTimeout time.Duration - offlineRunTimeout time.Duration - runReportLostTimeout time.Duration - reaperInterval time.Duration -} - -func newConversationStreamStore(isOnline func(string) bool) *conversationStreamStore { - return &conversationStreamStore{ - streams: make(map[string]*conversationStream), - pendingRuns: make(map[string]*pendingChatRun), - runs: make(map[string]*chatRunRecord), - commandDedup: make(map[string]*chatCommandDedupeRecord), - commandWatchers: make(map[string][]chan ChatCommandUpdate), - commandUpdates: make(map[string]chatCommandUpdateRecord), - ingressRuns: make(map[string]*chatIngressRunState), - ingressFragments: make(map[string]*chatIngressFragmentAssembly), - activityHub: newChatActivityHub(), - isOnline: isOnline, - eventRetention: conversationEventRetention, - maxEvents: conversationMaxEvents, - maxEventBytes: conversationMaxEventBytes, - idleRetention: conversationIdleRetention, - staleRunTimeout: conversationStaleRunTimeout, - offlineRunTimeout: conversationOfflineRunTimeout, - runReportLostTimeout: conversationRunReportLostTimeout, - reaperInterval: conversationReaperInterval, - } -} - -func agentScopedKey(agentID, value string) string { - return strings.TrimSpace(agentID) + "\x00" + strings.TrimSpace(value) -} - -func conversationStreamKey(agentID, conversationID string) string { - return agentScopedKey(agentID, conversationID) -} - -func (s *conversationStreamStore) streamLocked(agentID, conversationID string, now time.Time) *conversationStream { - agentID = strings.TrimSpace(agentID) - conversationID = strings.TrimSpace(conversationID) - key := conversationStreamKey(agentID, conversationID) - stream := s.streams[key] - if stream == nil { - stream = &conversationStream{ - agentID: agentID, - conversationID: conversationID, - streamEpoch: uuid.NewString(), - subscribers: make(map[int]*streamSubscriber), - updatedAt: now, - } - s.streams[key] = stream - s.startReaper() - } - return stream -} - -func (s *conversationStreamStore) evictStreamLocked(stream *conversationStream, now time.Time) { - cutoff := now.Add(-s.eventRetention) - activeStart := int64(0) - if stream.activity != nil { - activeStart = stream.activity.StartedSeq - } - protectedSnapshotSeq := stream.latestContentSnapshotSeq - if protectedSnapshotSeq > 0 { - protected := false - for _, event := range stream.events { - if event.Seq == protectedSnapshotSeq && !event.ReceivedAt.Before(cutoff) { - protected = true - break - } - } - if !protected { - protectedSnapshotSeq = 0 - stream.latestContentSnapshotSeq = 0 - } - } - drop := 0 - for drop < len(stream.events) { - event := stream.events[drop] - overCap := len(stream.events)-drop > s.maxEvents || - stream.eventsBytes > s.maxEventBytes - expired := event.ReceivedAt.Before(cutoff) - if !overCap && !expired { - break - } - if !overCap && activeStart > 0 && event.Seq >= activeStart { - // Retention never evicts events of the active run; only hard caps do. - break - } - if protectedSnapshotSeq > 0 && event.Seq >= protectedSnapshotSeq { - break - } - stream.eventsBytes -= event.approxBytes - if event.Seq > stream.evictedThroughSeq { - stream.evictedThroughSeq = event.Seq - } - drop++ - } - if drop > 0 { - remaining := len(stream.events) - drop - copy(stream.events, stream.events[drop:]) - for i := remaining; i < len(stream.events); i++ { - stream.events[i] = nil - } - stream.events = stream.events[:remaining] - } -} - -// ConversationSubscription is the result of subscribing to a conversation -// stream. The subscription persists across runs; EventCh closes only on -// Cleanup or when the subscriber overflows (check Overflowed, then -// re-subscribe with after_seq to resume without loss). -type ConversationSubscription struct { - AgentID string - ConversationID string - StreamEpoch string - LatestSeq int64 - Reset bool - Activity *RunActivity - Snapshot *RunSnapshot - Events []*ConversationEvent - EventCh <-chan *ConversationEvent - Cleanup func() - Overflowed func() bool -} - -func (m *Manager) SubscribeConversationStream( - agentID string, - conversationID string, - afterSeq int64, - clientEpoch string, -) *ConversationSubscription { - s := m.convStreams - agentID = strings.TrimSpace(agentID) - conversationID = strings.TrimSpace(conversationID) - clientEpoch = strings.TrimSpace(clientEpoch) - if agentID == "" || conversationID == "" { - return nil - } - if afterSeq < 0 { - afterSeq = 0 - } - now := time.Now() - - s.mu.Lock() - defer s.mu.Unlock() - stream := s.streamLocked(agentID, conversationID, now) - s.evictStreamLocked(stream, now) - - reset := clientEpoch != "" && clientEpoch != stream.streamEpoch - if afterSeq > stream.lastSeq { - reset = true - } - if afterSeq > 0 && afterSeq < stream.evictedThroughSeq { - reset = true - } - if reset { - afterSeq = 0 - } - - replay := make([]*ConversationEvent, 0, len(stream.events)) - for _, event := range stream.events { - if event.Seq > afterSeq { - replay = append(replay, event) - } - } - - var snapshot *RunSnapshot - if stream.activity != nil && - stream.latestSnapshot != nil && - stream.latestSnapshot.RunID == stream.activity.RunID && - afterSeq < stream.activity.StartedSeq && - (stream.evictedThroughSeq >= stream.activity.StartedSeq || stream.runNeedsSnapshot) { - // The buffer cannot reproduce the active run from its start; hand the - // client the runtime snapshot to rebuild the live tail. - snapshotCopy := *stream.latestSnapshot - snapshot = &snapshotCopy - } - - var activity *RunActivity - if stream.activity != nil { - activityCopy := *stream.activity - activity = &activityCopy - } - - s.nextSubID++ - sub := &streamSubscriber{ - id: s.nextSubID, - ch: make(chan *ConversationEvent, conversationSubscriberBuffer), - } - stream.subscribers[sub.id] = sub - - cleanup := func() { - s.mu.Lock() - defer s.mu.Unlock() - current := s.streams[conversationStreamKey(agentID, conversationID)] - if current == nil { - return - } - if existing, ok := current.subscribers[sub.id]; ok && existing == sub { - delete(current.subscribers, sub.id) - if !sub.closed { - sub.closed = true - close(sub.ch) - } - } - } - overflowed := func() bool { - s.mu.Lock() - defer s.mu.Unlock() - return sub.overflowed - } - - return &ConversationSubscription{ - AgentID: agentID, - ConversationID: conversationID, - StreamEpoch: stream.streamEpoch, - LatestSeq: stream.lastSeq, - Reset: reset, - Activity: activity, - Snapshot: snapshot, - Events: replay, - EventCh: sub.ch, - Cleanup: cleanup, - Overflowed: overflowed, - } -} - -// ActiveConversationActivities returns the current activity of every -// conversation with an active run (for history.list hydration). Each entry -// is stamped with its stream's owning agent. -func (m *Manager) ActiveConversationActivities() []RunActivity { - s := m.convStreams - s.mu.Lock() - defer s.mu.Unlock() - activities := make([]RunActivity, 0, len(s.streams)) - for _, stream := range s.streams { - if stream.activity != nil { - activity := *stream.activity - activity.AgentID = stream.agentID - activities = append(activities, activity) - } - } - return activities -} - -// appendEventLocked assigns the next seq, freezes the payload, stores the -// event, and fans it out to subscribers. -func (s *conversationStreamStore) appendEventLocked( - stream *conversationStream, - runID string, - eventType string, - payload map[string]any, - now time.Time, -) *ConversationEvent { - if payload == nil { - payload = make(map[string]any, 4) - } - stream.lastSeq++ - payload["conversation_id"] = stream.conversationID - payload["run_id"] = runID - payload["seq"] = stream.lastSeq - payload["type"] = eventType - event := &ConversationEvent{ - ConversationID: stream.conversationID, - RunID: runID, - Seq: stream.lastSeq, - Type: eventType, - Payload: payload, - ReceivedAt: now, - approxBytes: approxPayloadBytes(payload), - } - stream.events = append(stream.events, event) - stream.eventsBytes += event.approxBytes - stream.lastEventAt = now - stream.updatedAt = now - s.evictStreamLocked(stream, now) - s.publishLocked(stream, event) - return event -} - -// publishLocked delivers an event to every subscriber without blocking. A -// subscriber whose buffer is full is closed; the client resumes via -// re-subscribe with after_seq (the ring still holds the events). -func (s *conversationStreamStore) publishLocked(stream *conversationStream, event *ConversationEvent) { - for id, sub := range stream.subscribers { - if sub.closed { - continue - } - select { - case sub.ch <- event: - default: - sub.overflowed = true - sub.closed = true - close(sub.ch) - delete(stream.subscribers, id) - } - } -} - -func approxPayloadBytes(payload map[string]any) int { - total := 64 - for key, value := range payload { - total += len(key) + approxValueBytes(value, 2) - } - return total -} - -func approxValueBytes(value any, depth int) int { - switch v := value.(type) { - case string: - return len(v) + 8 - case map[string]any: - if depth <= 0 { - return 64 - } - total := 16 - for key, nested := range v { - total += len(key) + approxValueBytes(nested, depth-1) - } - return total - case []any: - if depth <= 0 { - return 64 - } - total := 16 - for _, nested := range v { - total += approxValueBytes(nested, depth-1) - } - return total - default: - return 16 - } -} - -func (stream *conversationStream) runFinishedRecently(runID string) bool { - for _, finished := range stream.finishedRuns { - if finished == runID { - return true - } - } - return false -} - -// runStartedLocked registers runID as the conversation's current run, -// superseding a still-active previous run. Idempotent per run. -func (s *conversationStreamStore) runStartedLocked( - stream *conversationStream, - runID string, - workdir string, - now time.Time, -) { - if runID == "" || stream.runFinishedRecently(runID) { - return - } - if stream.activity != nil && stream.activity.RunID == runID { - switch stream.activity.State { - case RunActivityQueued: - // The gateway-accepted command actually started: append the - // run_started log event now. StartedSeq keeps covering the seeded - // user_message so the whole run stays replayable. - s.flushDeferredSeedsLocked(stream, runID, s.runRecordLocked(stream.agentID, runID, stream.conversationID), now) - payload := map[string]any{} - if stream.activity.ClientRequestID != "" { - payload["client_request_id"] = stream.activity.ClientRequestID - } - if stream.workdir != "" { - payload["workdir"] = stream.workdir - } - s.appendEventLocked(stream, runID, StreamEventRunStarted, payload, now) - stream.activity.State = RunActivityRunning - stream.activity.UpdatedAt = now - s.publishActivityLocked(stream, now) - case RunActivityCancelling: - // A cancel is in flight; keep the cancelling state. - } - return - } - if stream.activity != nil && - (stream.activity.State == RunActivityRunning || stream.activity.State == RunActivityCancelling) { - // Supersession: the agent started a new run (e.g. a queued prompt - // auto-send) before the previous run's terminal signal arrived. - s.runFinishedLocked(stream, stream.activity.RunID, "completed", "", "", map[string]any{ - "reason": "superseded", - }, now) - } - if workdir = strings.TrimSpace(workdir); workdir != "" { - stream.workdir = workdir - } - record := s.runRecordLocked(stream.agentID, runID, stream.conversationID) - s.flushDeferredSeedsLocked(stream, runID, record, now) - payload := map[string]any{} - if record.clientRequestID != "" { - payload["client_request_id"] = record.clientRequestID - } - if stream.workdir != "" { - payload["workdir"] = stream.workdir - } - startEvent := s.appendEventLocked(stream, runID, StreamEventRunStarted, payload, now) - startedSeq := startEvent.Seq - if record.firstSeededSeq > 0 && record.firstSeededSeq < startedSeq { - // The run's user_message was seeded before it started (e.g. it - // started through supersession while another run was active); the - // eviction guard must cover the seed too. - startedSeq = record.firstSeededSeq - } - stream.activity = &RunActivity{ - ConversationID: stream.conversationID, - RunID: runID, - ClientRequestID: record.clientRequestID, - State: RunActivityRunning, - StartedSeq: startedSeq, - Workdir: stream.workdir, - UpdatedAt: now, - } - s.publishActivityLocked(stream, now) -} - -// runFinishedLocked appends run_finished exactly once per run and clears the -// activity when the finished run is the current one. -func (s *conversationStreamStore) runFinishedLocked( - stream *conversationStream, - runID string, - status string, - errorCode string, - message string, - extra map[string]any, - now time.Time, -) { - if runID == "" || stream.runFinishedRecently(runID) { - return - } - if stream.activity == nil || stream.activity.RunID != runID { - // Terminal signal for a run this stream never started (e.g. the - // gateway restarted mid-run). Synthesize the start so clients see a - // coherent pair, unless another run is currently active — then the - // stray terminal is recorded without touching the active run. - if stream.activity == nil { - s.runStartedLocked(stream, runID, "", now) - } - } - payload := map[string]any{ - "status": status, - } - if errorCode != "" { - payload["error_code"] = errorCode - } - if message != "" { - payload["message"] = message - } - for key, value := range extra { - if _, exists := payload[key]; !exists { - payload[key] = value - } - } - record := s.runRecordLocked(stream.agentID, runID, stream.conversationID) - if record.clientRequestID != "" { - payload["client_request_id"] = record.clientRequestID - } - // Inferred terminals (nobody vouched for the run) stay falsifiable: a - // later event for the run resurrects it instead of being dropped. Genuine - // terminals settle the run for good. - record.lostInferred = status == "failed" && isInferredRunLossCode(errorCode) - if !record.lostInferred { - record.revived = false - } - s.appendEventLocked(stream, runID, StreamEventRunFinished, payload, now) - stream.finishedRuns = append(stream.finishedRuns, runID) - if len(stream.finishedRuns) > conversationFinishedRunMemory { - evicted := stream.finishedRuns[0] - stream.finishedRuns = stream.finishedRuns[1:] - delete(s.runs, agentScopedKey(stream.agentID, evicted)) - } - if stream.latestSnapshot != nil && stream.latestSnapshot.RunID == runID { - stream.latestSnapshot = nil - } - if stream.activity != nil && stream.activity.RunID == runID { - stream.activity = nil - stream.runNeedsSnapshot = false - stream.snapshotDirty = false - s.publishActivityLocked(stream, now) - } -} - -// resurrectRunLocked reopens a run that was force-finished by a liveness -// inference: fresh agent traffic for the run proves the inference wrong. The -// run leaves the finished set (so runStartedLocked re-registers it), is -// flagged to ignore repeats of the stale verdict, and the stream is marked -// snapshot-hungry so subscribers rebuild the tail that was dropped while the -// run was considered dead. Refuses when another run owns the conversation — -// then the late events really are stragglers. -func (s *conversationStreamStore) resurrectRunLocked( - stream *conversationStream, - runID string, -) bool { - record := s.runs[agentScopedKey(stream.agentID, runID)] - if record == nil || !record.lostInferred { - return false - } - if stream.activity != nil { - return false - } - kept := stream.finishedRuns[:0] - for _, finished := range stream.finishedRuns { - if finished != runID { - kept = append(kept, finished) - } - } - stream.finishedRuns = kept - record.lostInferred = false - record.revived = true - // The events dropped between the wrong terminal and this resurrection are - // unrecoverable from the log; late joiners and current subscribers rebuild - // from the next runtime snapshot. - stream.runNeedsSnapshot = true - stream.snapshotDirty = true - return true -} - -// markRunQueuedLocked records that a run's command is pending in the gateway -// (accepted but not yet started). No log event — activity only. -func (s *conversationStreamStore) markRunQueuedLocked( - stream *conversationStream, - runID string, - clientRequestID string, - now time.Time, -) { - if runID == "" || stream.runFinishedRecently(runID) { - return - } - if stream.activity != nil { - return - } - stream.activity = &RunActivity{ - ConversationID: stream.conversationID, - RunID: runID, - ClientRequestID: clientRequestID, - State: RunActivityQueued, - StartedSeq: stream.lastSeq + 1, - Workdir: stream.workdir, - UpdatedAt: now, - } - s.publishActivityLocked(stream, now) -} - -func (s *conversationStreamStore) runRecordLocked(agentID, runID, conversationID string) *chatRunRecord { - key := agentScopedKey(agentID, runID) - record := s.runs[key] - if record == nil { - record = &chatRunRecord{agentID: agentID, conversationID: conversationID} - s.runs[key] = record - } else if record.conversationID == "" { - record.conversationID = conversationID - } - return record -} - -func (s *conversationStreamStore) publishActivityLocked(stream *conversationStream, now time.Time) { - event := ConversationActivityEvent{ - ConversationID: stream.conversationID, - AgentID: stream.agentID, - Workdir: stream.workdir, - UpdatedAt: now, - } - if stream.activity != nil { - event.RunID = stream.activity.RunID - event.ClientRequestID = stream.activity.ClientRequestID - event.Running = true - event.State = stream.activity.State - if stream.activity.Workdir != "" { - event.Workdir = stream.activity.Workdir - } - } - s.activityHub.publish(event) -} - -// --- command lifecycle ----------------------------------------------------- - -// WatchChatCommand registers a watcher for pre-stream command outcomes -// (bound / queued_in_gui / failed). The latest update is replayed immediately -// so a reconnecting deduplicated submit cannot miss an earlier transition. -func (m *Manager) WatchChatCommand(agentID string, runID string) (<-chan ChatCommandUpdate, func()) { - s := m.convStreams - agentID = strings.TrimSpace(agentID) - runID = strings.TrimSpace(runID) - key := agentScopedKey(agentID, runID) - ch := make(chan ChatCommandUpdate, 4) - if agentID == "" || runID == "" { - close(ch) - return ch, func() {} - } - - s.mu.Lock() - s.commandWatchers[key] = append(s.commandWatchers[key], ch) - if record, ok := s.commandUpdates[key]; ok { - ch <- record.update - } - s.mu.Unlock() - - cleanup := func() { - s.mu.Lock() - defer s.mu.Unlock() - watchers := s.commandWatchers[key] - for i, watcher := range watchers { - if watcher == ch { - s.commandWatchers[key] = append(watchers[:i], watchers[i+1:]...) - // All sends happen under s.mu after a registration check, so - // closing here is safe and releases the forwarder goroutine. - close(ch) - break - } - } - if len(s.commandWatchers[key]) == 0 { - delete(s.commandWatchers, key) - } - } - return ch, cleanup -} - -func (s *conversationStreamStore) fireCommandUpdateLocked(update ChatCommandUpdate) { - update.AgentID = strings.TrimSpace(update.AgentID) - update.RunID = strings.TrimSpace(update.RunID) - if update.AgentID == "" || update.RunID == "" { - return - } - key := agentScopedKey(update.AgentID, update.RunID) - s.commandUpdates[key] = chatCommandUpdateRecord{update: update, at: time.Now()} - for _, watcher := range s.commandWatchers[key] { - select { - case watcher <- update: - default: - } - } -} - -// ChatCommandStart is the accepted-command result returned to the transport. -type ChatCommandStart struct { - AgentID string - RunID string - ConversationID string - AcceptedSeq int64 - Deduped bool -} - -// LookupChatCommand returns the canonical run already assigned to a -// client_request_id. The lookup and StartChatCommand share the same store mutex; -// callers may use this as a fast path, while StartChatCommand remains the -// authoritative atomic check for concurrent submissions. -func (m *Manager) LookupChatCommand(agentID string, clientRequestID string) (ChatCommandStart, bool) { - s := m.convStreams - agentID = strings.TrimSpace(agentID) - clientRequestID = strings.TrimSpace(clientRequestID) - if agentID == "" || clientRequestID == "" { - return ChatCommandStart{}, false - } - s.mu.Lock() - defer s.mu.Unlock() - return s.lookupChatCommandLocked(agentID, clientRequestID) -} - -func (s *conversationStreamStore) lookupChatCommandLocked( - agentID string, - clientRequestID string, -) (ChatCommandStart, bool) { - record := s.commandDedup[agentScopedKey(agentID, clientRequestID)] - if record == nil || strings.TrimSpace(record.runID) == "" { - return ChatCommandStart{}, false - } - return ChatCommandStart{ - AgentID: agentID, - RunID: record.runID, - ConversationID: record.conversationID, - AcceptedSeq: record.acceptedSeq, - Deduped: true, - }, true -} - -func (s *conversationStreamStore) updateChatCommandDedupeLocked( - agentID string, - clientRequestID string, - runID string, - conversationID string, - acceptedSeq int64, - now time.Time, -) { - agentID = strings.TrimSpace(agentID) - clientRequestID = strings.TrimSpace(clientRequestID) - if agentID == "" || clientRequestID == "" || strings.TrimSpace(runID) == "" { - return - } - key := agentScopedKey(agentID, clientRequestID) - record := s.commandDedup[key] - if record == nil { - record = &chatCommandDedupeRecord{ - runID: strings.TrimSpace(runID), - createdAt: now, - } - s.commandDedup[key] = record - } - if record.runID != strings.TrimSpace(runID) { - return - } - if conversationID = strings.TrimSpace(conversationID); conversationID != "" { - record.conversationID = conversationID - } - if acceptedSeq > record.acceptedSeq { - record.acceptedSeq = acceptedSeq - } -} - -// StartChatCommand registers a webui-issued chat command. For a known -// conversation the seeded payloads (rebased/user_message) are appended to the -// log immediately; for a draft conversation they are buffered until the first -// agent signal binds the run to a real conversation id. agentID 是解析后的 -// 目标 Agent,盖到会话流上供事件打标与取消路由。 -func (m *Manager) StartChatCommand( - agentID string, - runID string, - conversationID string, - workdir string, - clientRequestID string, - seededPayloads []map[string]any, -) ChatCommandStart { - s := m.convStreams - agentID = strings.TrimSpace(agentID) - runID = strings.TrimSpace(runID) - key := agentScopedKey(agentID, runID) - conversationID = strings.TrimSpace(conversationID) - workdir = strings.TrimSpace(workdir) - clientRequestID = strings.TrimSpace(clientRequestID) - now := time.Now() - - if agentID == "" || runID == "" { - return ChatCommandStart{} - } - - s.mu.Lock() - defer s.mu.Unlock() - if existing, ok := s.lookupChatCommandLocked(agentID, clientRequestID); ok { - return existing - } - s.updateChatCommandDedupeLocked(agentID, clientRequestID, runID, conversationID, 0, now) - s.startReaper() - - if conversationID == "" { - s.pendingRuns[key] = &pendingChatRun{ - runID: runID, - agentID: agentID, - clientRequestID: clientRequestID, - workdir: workdir, - seeded: seededPayloads, - createdAt: now, - } - return ChatCommandStart{AgentID: agentID, RunID: runID} - } - - stream := s.streamLocked(agentID, conversationID, now) - if agentID != "" { - stream.agentID = agentID - } - if workdir != "" { - stream.workdir = workdir - } - record := s.runRecordLocked(agentID, runID, conversationID) - record.clientRequestID = clientRequestID - - if stream.activity != nil { - // A run is already active: this command is almost certainly headed - // for the desktop prompt queue. Seeding the user_message into the log - // now would flash a bubble on every viewer until the queued_in_gui - // compensation removes it — defer the seeds until the run actually - // starts (or fails); if it parks in the GUI queue they are dropped - // and the agent's own echo becomes authoritative. - record.deferredSeeds = seededPayloads - start := ChatCommandStart{ - AgentID: agentID, - RunID: runID, - ConversationID: conversationID, - AcceptedSeq: stream.lastSeq, - } - s.updateChatCommandDedupeLocked( - agentID, clientRequestID, start.RunID, start.ConversationID, start.AcceptedSeq, now, - ) - return start - } - - // Mark queued before seeding so the activity's StartedSeq covers the - // seeded user_message — the whole run replays from one cursor. - s.markRunQueuedLocked(stream, runID, clientRequestID, now) - acceptedSeq := s.appendSeededPayloadsLocked(stream, runID, clientRequestID, seededPayloads, now) - record.userMessageSeeded = seededPayloadsIncludeUserMessage(seededPayloads) - record.rebaseSeeded = seededPayloadsIncludeRebased(seededPayloads) - start := ChatCommandStart{ - AgentID: agentID, - RunID: runID, - ConversationID: conversationID, - AcceptedSeq: acceptedSeq, - } - s.updateChatCommandDedupeLocked( - agentID, clientRequestID, start.RunID, start.ConversationID, start.AcceptedSeq, now, - ) - return start -} - -// flushDeferredSeedsLocked appends seeds that were deferred because another -// run was active at accept time. Called right before the run's run_started -// event so the log keeps the normal [user_message, run_started, ...] shape. -func (s *conversationStreamStore) flushDeferredSeedsLocked( - stream *conversationStream, - runID string, - record *chatRunRecord, - now time.Time, -) { - if len(record.deferredSeeds) == 0 { - return - } - seeds := record.deferredSeeds - record.deferredSeeds = nil - s.appendSeededPayloadsLocked(stream, runID, record.clientRequestID, seeds, now) - record.userMessageSeeded = seededPayloadsIncludeUserMessage(seeds) - record.rebaseSeeded = seededPayloadsIncludeRebased(seeds) -} - -func (s *conversationStreamStore) appendSeededPayloadsLocked( - stream *conversationStream, - runID string, - clientRequestID string, - seededPayloads []map[string]any, - now time.Time, -) int64 { - acceptedSeq := stream.lastSeq - for _, payload := range seededPayloads { - if len(payload) == 0 { - continue - } - eventType, _ := payload["type"].(string) - if eventType == "" { - continue - } - cloned := make(map[string]any, len(payload)+5) - for key, value := range payload { - cloned[key] = value - } - if eventType == "user_message" && clientRequestID != "" { - cloned["client_request_id"] = clientRequestID - } - event := s.appendEventLocked(stream, runID, eventType, cloned, now) - acceptedSeq = event.Seq - if record := s.runs[agentScopedKey(stream.agentID, runID)]; record != nil && record.firstSeededSeq == 0 { - record.firstSeededSeq = event.Seq - } - } - return acceptedSeq -} - -// seededPayloadsIncludeRebased mirrors seededPayloadsIncludeUserMessage for -// the webui edit_resend truncation seed: marking rebaseSeeded at accept time -// keeps the identity-forwarded desktop echo (which still carries the same -// base_message_ref) from appending a second rebased to the log. -func seededPayloadsIncludeRebased(seededPayloads []map[string]any) bool { - for _, payload := range seededPayloads { - if eventType, _ := payload["type"].(string); eventType == StreamEventRebased { - return true - } - } - return false -} - -func seededPayloadsIncludeUserMessage(seededPayloads []map[string]any) bool { - for _, payload := range seededPayloads { - if eventType, _ := payload["type"].(string); eventType == "user_message" { - return true - } - } - return false -} - -// FailChatCommand fails a command that never produced a bound run (agent -// unreachable, startup watchdog) or force-finishes its run when bound. -func (m *Manager) FailChatCommand(agentID string, runID string, errorCode string, message string) { - s := m.convStreams - agentID = strings.TrimSpace(agentID) - runID = strings.TrimSpace(runID) - if agentID == "" || runID == "" { - return - } - key := agentScopedKey(agentID, runID) - now := time.Now() - - s.mu.Lock() - defer s.mu.Unlock() - - if pending := s.pendingRuns[key]; pending != nil { - delete(s.pendingRuns, key) - s.fireCommandUpdateLocked(ChatCommandUpdate{ - AgentID: agentID, - RunID: runID, - ClientRequestID: pending.clientRequestID, - Phase: "failed", - ErrorCode: errorCode, - Message: message, - }) - return - } - - record := s.runs[key] - if record == nil || record.conversationID == "" { - return - } - stream := s.streams[conversationStreamKey(agentID, record.conversationID)] - if stream == nil { - return - } - // Seeds deferred at accept time surface now so the failure has its user - // message for context; runFinishedLocked follows with the error. - s.flushDeferredSeedsLocked(stream, runID, record, now) - s.runFinishedLocked(stream, runID, "failed", errorCode, message, nil, now) -} - -// ChatCommandSettled reports whether a command reached a state the startup -// watchdog must not interfere with: its run started, finished, or was parked -// in the desktop prompt queue. -func (m *Manager) ChatCommandSettled(agentID string, runID string) bool { - s := m.convStreams - agentID = strings.TrimSpace(agentID) - runID = strings.TrimSpace(runID) - if agentID == "" || runID == "" { - return false - } - key := agentScopedKey(agentID, runID) - s.mu.Lock() - defer s.mu.Unlock() - record := s.runs[key] - if record == nil { - return false - } - if record.queuedInGUI { - return true - } - if record.conversationID == "" { - return false - } - stream := s.streams[conversationStreamKey(agentID, record.conversationID)] - if stream == nil { - return false - } - if stream.runFinishedRecently(runID) { - return true - } - return stream.activity != nil && - stream.activity.RunID == runID && - stream.activity.State != RunActivityQueued -} - -// MarkConversationCancelling flips the active run into the cancelling state -// and returns its run id for the caller's watchdog. The agent's real terminal -// signal wins; ForceFinishRun is the fallback. -func (m *Manager) MarkConversationCancelling(agentID string, conversationID string, runID string) (string, bool) { - s := m.convStreams - agentID = strings.TrimSpace(agentID) - conversationID = strings.TrimSpace(conversationID) - runID = strings.TrimSpace(runID) - if agentID == "" || conversationID == "" { - return "", false - } - now := time.Now() - - s.mu.Lock() - defer s.mu.Unlock() - stream := s.streams[conversationStreamKey(agentID, conversationID)] - if stream == nil || stream.activity == nil { - return "", false - } - if runID != "" && stream.activity.RunID != runID { - return "", false - } - stream.activity.State = RunActivityCancelling - stream.activity.UpdatedAt = now - s.publishActivityLocked(stream, now) - return stream.activity.RunID, true -} - -// ForceFinishRun finishes a run from a gateway-side watchdog. No-op when the -// run already finished (exactly-once guard). -func (m *Manager) ForceFinishRun(agentID string, runID string, status string, errorCode string, message string) { - s := m.convStreams - agentID = strings.TrimSpace(agentID) - runID = strings.TrimSpace(runID) - if agentID == "" || runID == "" { - return - } - key := agentScopedKey(agentID, runID) - now := time.Now() - - s.mu.Lock() - defer s.mu.Unlock() - record := s.runs[key] - if record == nil || record.conversationID == "" { - return - } - stream := s.streams[conversationStreamKey(agentID, record.conversationID)] - if stream == nil { - return - } - s.runFinishedLocked(stream, runID, status, errorCode, message, nil, now) -} - -// --- maintenance ----------------------------------------------------------- - -// onRuntimeStatus reconciles the desktop's diagnostic run ledger with tracked -// activities. Active and finished reports only vouch liveness; a finished -// report never has terminal authority. Reliable completion belongs exclusively -// to ChatIngressTerminal, while absence beyond the grace window remains an -// explicitly inferred fallback. -func (s *conversationStreamStore) onRuntimeStatus(agentID string, event *gatewayv2.RuntimeStatusEvent, now time.Time) { - agentID = strings.TrimSpace(agentID) - if agentID == "" || event == nil { - return - } - s.mu.Lock() - defer s.mu.Unlock() - - activeSet := make(map[string]bool, len(event.GetActiveRuns())) - for _, report := range event.GetActiveRuns() { - activeSet[report.GetRunId()] = true - } - finished := make(map[string]bool, len(event.GetFinishedRuns())) - for _, report := range event.GetFinishedRuns() { - finished[report.GetRunId()] = true - } - - // Reconcile only tracked activities; finished reports never resurrect a - // stream for a run this store is not tracking. - for _, stream := range s.streams { - if stream.activity == nil || (agentID != "" && stream.agentID != agentID) { - continue - } - runID := stream.activity.RunID - if stream.activity.State == RunActivityQueued { - // The accepted-command startup watchdog owns the queued phase; - // the desktop may not know the run yet. - continue - } - if activeSet[runID] { - stream.activity.UpdatedAt = now - continue - } - record := s.runs[agentScopedKey(stream.agentID, runID)] - revived := record != nil && record.revived - if finished[runID] { - stream.activity.UpdatedAt = now - if ingress := s.ingressRuns[agentScopedKey(stream.agentID, runID)]; ingress != nil { - ingress.checkpointRequested = true - ingress.updatedAt = now - } - continue - } - if revived { - // Resurrected after a wrong loss verdict: liveness inferences no - // longer end this run; the reaper's stale-run timeout is the - // backstop for a genuinely dead one. - continue - } - // Stream events vouch too: never finalize a run whose events are still - // flowing through the relay (mirrors the reaper's lastAlive logic). - eventsQuiet := stream.lastEventAt.IsZero() || - now.Sub(stream.lastEventAt) >= s.runReportLostTimeout - if eventsQuiet && now.Sub(stream.activity.UpdatedAt) >= s.runReportLostTimeout { - s.runFinishedLocked(stream, runID, "failed", "desktop_run_lost", - "The desktop runtime stopped reporting this run.", nil, now) - } - } -} - -func (s *conversationStreamStore) startReaper() { - s.reaperOnce.Do(func() { - interval := s.reaperInterval - if interval <= 0 { - interval = conversationReaperInterval - } - go func() { - ticker := time.NewTicker(interval) - defer ticker.Stop() - for range ticker.C { - s.reap(time.Now()) - } - }() - }) -} - -func (s *conversationStreamStore) reap(now time.Time) { - s.mu.Lock() - defer s.mu.Unlock() - - for streamKey, stream := range s.streams { - s.evictStreamLocked(stream, now) - - if stream.activity != nil { - online := s.isOnline != nil && s.isOnline(stream.agentID) - // A run is stale only when NOTHING vouches for it: no stream - // events and no activity transition/report-vouch within the - // timeout (onRuntimeStatus bumps UpdatedAt for reported runs). - lastAlive := stream.lastEventAt - if stream.activity.UpdatedAt.After(lastAlive) { - lastAlive = stream.activity.UpdatedAt - } - if online { - if !lastAlive.IsZero() && now.Sub(lastAlive) > s.staleRunTimeout { - s.runFinishedLocked(stream, stream.activity.RunID, "failed", "stale_run", - "The desktop runtime stopped reporting this run.", nil, now) - } - } else if !lastAlive.IsZero() && now.Sub(lastAlive) > s.offlineRunTimeout { - s.runFinishedLocked(stream, stream.activity.RunID, "failed", "agent_offline", - "The desktop agent went offline during this run.", nil, now) - } - } - - if stream.activity == nil && - len(stream.subscribers) == 0 && - now.Sub(stream.updatedAt) > s.idleRetention { - for _, finished := range stream.finishedRuns { - delete(s.runs, agentScopedKey(stream.agentID, finished)) - } - delete(s.streams, streamKey) - } - } - - for runID, pending := range s.pendingRuns { - if now.Sub(pending.createdAt) > pendingChatRunRetention { - delete(s.pendingRuns, runID) - } - } - - for clientRequestID, record := range s.commandDedup { - if record == nil || now.Sub(record.createdAt) > chatCommandDedupeRetention { - delete(s.commandDedup, clientRequestID) - } - } - - // Swept by their own timestamp: update entries exist for runs without a - // dedupe record in this process (post-restart replays, parked runs), so - // pairing deletion to dedupe records would leak them. - for runID, record := range s.commandUpdates { - if now.Sub(record.at) > chatCommandDedupeRetention { - delete(s.commandUpdates, runID) - } - } - - for runKey, state := range s.ingressRuns { - if state == nil || now.Sub(state.updatedAt) > s.idleRetention { - delete(s.ingressRuns, runKey) - } - } - for fragmentKey, assembly := range s.ingressFragments { - if assembly == nil || now.After(assembly.expiresAt) { - delete(s.ingressFragments, fragmentKey) - } - } -} diff --git a/crates/agent-gateway/internal/session/conversation_stream_reconcile_test.go b/crates/agent-gateway/internal/session/conversation_stream_reconcile_test.go deleted file mode 100644 index c3e666203..000000000 --- a/crates/agent-gateway/internal/session/conversation_stream_reconcile_test.go +++ /dev/null @@ -1,415 +0,0 @@ -package session - -import ( - "testing" - "time" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -func runReport(runID string, conversationID string, state string) *gatewayv2.ChatRunReport { - return &gatewayv2.ChatRunReport{ - RunId: runID, - ConversationId: conversationID, - State: state, - } -} - -func runsReport( - active []*gatewayv2.ChatRunReport, - finished []*gatewayv2.ChatRunReport, -) *gatewayv2.RuntimeStatusEvent { - return &gatewayv2.RuntimeStatusEvent{ - ActiveRunCount: uint32(len(active)), - ActiveRuns: active, - FinishedRuns: finished, - } -} - -func lastEvent(t *testing.T, m *Manager, conversationID string) *ConversationEvent { - t.Helper() - sub := m.SubscribeConversationStream(conversationTestAgentID, conversationID, 0, "") - sub.Cleanup() - if len(sub.Events) == 0 { - t.Fatalf("no events for %s", conversationID) - } - return sub.Events[len(sub.Events)-1] -} - -// finished_runs is diagnostic only: neither a valid nor malformed report may -// overtake the reliable terminal projection. -func TestRunReportDoesNotAdoptTerminal(t *testing.T) { - m := NewManager() - m.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-1")) - - m.convStreams.onRuntimeStatus(conversationTestAgentID, runsReport(nil, []*gatewayv2.ChatRunReport{ - runReport("run-1", "conv-1", "completed"), - }), time.Now()) - - last := lastEvent(t, m, "conv-1") - if last.Type == StreamEventRunFinished { - t.Fatalf("finished_runs produced terminal = %s %#v", last.Type, last.Payload) - } - if activities := m.ActiveConversationActivities(); len(activities) != 1 { - t.Fatalf("diagnostic finished report cleared activity, activities=%d", len(activities)) - } - - // A finished report with an unknown state is not trusted verbatim: the - // run fails with desktop_run_lost instead. - m2 := NewManager() - m2.ingestChatControl(conversationTestAgentID, "run-2", startedControl("run-2", "conv-2")) - m2.convStreams.onRuntimeStatus(conversationTestAgentID, runsReport(nil, []*gatewayv2.ChatRunReport{ - runReport("run-2", "conv-2", "exploded"), - }), time.Now()) - last2 := lastEvent(t, m2, "conv-2") - if last2.Type == StreamEventRunFinished { - t.Fatalf("invalid diagnostic state produced terminal = %s %#v", last2.Type, last2.Payload) - } -} - -// A run absent from the desktop's reports survives the grace window (measured -// from the last vouch/transition), then is finalized as lost; a vouch before -// expiry restarts the window. -func TestRunReportFinalizesLostRunAfterGrace(t *testing.T) { - m := NewManager() - m.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-1")) - t0 := time.Now() - empty := runsReport(nil, nil) - - m.convStreams.onRuntimeStatus(conversationTestAgentID, empty, t0.Add(8*time.Second)) - if activities := m.ActiveConversationActivities(); len(activities) != 1 { - t.Fatalf("run finalized below grace, activities=%d", len(activities)) - } - - m.convStreams.onRuntimeStatus(conversationTestAgentID, empty, t0.Add(16*time.Second)) - if activities := m.ActiveConversationActivities(); len(activities) != 0 { - t.Fatalf("lost run not finalized after grace, activities=%d", len(activities)) - } - last := lastEvent(t, m, "conv-1") - if last.Type != StreamEventRunFinished || - last.Payload["status"] != "failed" || - last.Payload["error_code"] != "desktop_run_lost" { - t.Fatalf("lost finish tail = %s %#v, want failed/desktop_run_lost", last.Type, last.Payload) - } - - // Reported active again before grace expiry: the run survives and the - // absence window restarts from the vouch. - m2 := NewManager() - m2.ingestChatControl(conversationTestAgentID, "run-2", startedControl("run-2", "conv-2")) - t1 := time.Now() - m2.convStreams.onRuntimeStatus(conversationTestAgentID, runsReport([]*gatewayv2.ChatRunReport{ - runReport("run-2", "conv-2", "running"), - }, nil), t1.Add(8*time.Second)) - m2.convStreams.onRuntimeStatus(conversationTestAgentID, runsReport(nil, nil), t1.Add(20*time.Second)) - if activities := m2.ActiveConversationActivities(); len(activities) != 1 { - t.Fatalf("grace window must restart after a vouch, activities=%d", len(activities)) - } -} - -// Even an inferred-loss finished report remains diagnostic while repeated; -// it vouches the run instead of becoming a second terminal path. -func TestInferredLossReportNeverAdopted(t *testing.T) { - m := NewManager() - m.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-1")) - m.ingestChatEvent(conversationTestAgentID, "run-1", tokenEvent("conv-1", "hello")) - - lost := runReport("run-1", "conv-1", "failed") - lost.ErrorCode = "desktop_run_lost" - lost.Message = "The desktop runtime stopped reporting this run." - report := runsReport(nil, []*gatewayv2.ChatRunReport{lost}) - - m.convStreams.onRuntimeStatus(conversationTestAgentID, report, time.Now()) - if activities := m.ActiveConversationActivities(); len(activities) != 1 { - t.Fatalf("inferred loss adopted despite fresh events, activities=%d", len(activities)) - } - - m.convStreams.onRuntimeStatus(conversationTestAgentID, report, time.Now().Add(16*time.Second)) - if activities := m.ActiveConversationActivities(); len(activities) != 1 { - t.Fatalf("diagnostic inferred loss cleared activity, activities=%d", len(activities)) - } - last := lastEvent(t, m, "conv-1") - if last.Type == StreamEventRunFinished { - t.Fatalf("diagnostic inferred loss produced terminal = %s %#v", last.Type, last.Payload) - } -} - -// The desktop ledger flushes inferred losses as failed control events too; the -// conversation's active run ignores that verdict while its events are fresh. A -// genuine failure the run produced always terminates it. -func TestInferredFailedControlIgnoredWhileEventsFlow(t *testing.T) { - m := NewManager() - m.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-1")) - m.ingestChatEvent(conversationTestAgentID, "run-1", tokenEvent("conv-1", "hello")) - - m.ingestChatControl(conversationTestAgentID, "run-1", &gatewayv2.ChatControlEvent{ - RequestId: "run-1", - ConversationId: "conv-1", - Type: "failed", - ErrorCode: "desktop_run_lost", - Message: "The desktop runtime stopped reporting this run.", - }) - if activities := m.ActiveConversationActivities(); len(activities) != 1 { - t.Fatalf("inferred failed control killed a streaming run, activities=%d", len(activities)) - } - - m.ingestChatControl(conversationTestAgentID, "run-1", &gatewayv2.ChatControlEvent{ - RequestId: "run-1", - ConversationId: "conv-1", - Type: "failed", - ErrorCode: "provider_error", - Message: "boom", - }) - if activities := m.ActiveConversationActivities(); len(activities) != 0 { - t.Fatalf("genuine failure must terminate the run, activities=%d", len(activities)) - } -} - -// A wrongly-lost run resurrects when its events resume: the finished set -// releases it, activity returns, the stream is marked snapshot-hungry so -// subscribers rebuild the tail, and repeats of the stale verdict are ignored -// until the genuine terminal arrives. -func TestChatEventResurrectsInferredLostRun(t *testing.T) { - m := NewManager() - m.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-1")) - m.convStreams.onRuntimeStatus(conversationTestAgentID, runsReport(nil, nil), time.Now().Add(16*time.Second)) - if activities := m.ActiveConversationActivities(); len(activities) != 0 { - t.Fatalf("precondition: run not finalized as lost, activities=%d", len(activities)) - } - - m.ingestChatEvent(conversationTestAgentID, "run-1", tokenEvent("conv-1", "still alive")) - - activities := m.ActiveConversationActivities() - if len(activities) != 1 || activities[0].RunID != "run-1" { - t.Fatalf("run not resurrected by its own event, activities=%#v", activities) - } - stream := m.convStreams.streams[conversationStreamKey(conversationTestAgentID, "conv-1")] - if stream == nil || !stream.snapshotDirty || !stream.runNeedsSnapshot { - t.Fatalf("resurrection must mark the stream snapshot-hungry") - } - sub := m.SubscribeConversationStream(conversationTestAgentID, "conv-1", 0, "") - sub.Cleanup() - if len(sub.Events) < 2 { - t.Fatalf("expected restart + token events, got %d", len(sub.Events)) - } - tail := sub.Events[len(sub.Events)-2:] - if tail[0].Type != StreamEventRunStarted || tail[1].Type != "token" { - t.Fatalf("resurrection tail = %s,%s, want run_started,token", tail[0].Type, tail[1].Type) - } - - // The desktop ledger may keep repeating the stale verdict; a revived run - // ignores it (the reaper stays the backstop for a genuinely dead run). - lost := runReport("run-1", "conv-1", "failed") - lost.ErrorCode = "desktop_run_lost" - m.convStreams.onRuntimeStatus( - conversationTestAgentID, - runsReport(nil, []*gatewayv2.ChatRunReport{lost}), - time.Now().Add(time.Hour), - ) - if activities := m.ActiveConversationActivities(); len(activities) != 1 { - t.Fatalf("revived run must ignore stale loss verdicts, activities=%d", len(activities)) - } - - // The genuine terminal still settles it. - m.ingestChatEvent(conversationTestAgentID, "run-1", doneEvent("conv-1")) - if activities := m.ActiveConversationActivities(); len(activities) != 0 { - t.Fatalf("genuine done must settle a revived run, activities=%d", len(activities)) - } - last := lastEvent(t, m, "conv-1") - if last.Type != StreamEventRunFinished || last.Payload["status"] != "completed" { - t.Fatalf("final tail = %s %#v, want run_finished/completed", last.Type, last.Payload) - } -} - -func TestAuthoritativeTerminalCorrectsInferredLostRun(t *testing.T) { - tests := []struct { - name string - finish func(*Manager) - wantStatus string - }{ - { - name: "done event", - finish: func(m *Manager) { - m.ingestChatEvent(conversationTestAgentID, "run-1", doneEvent("conv-1")) - }, - wantStatus: "completed", - }, - { - name: "error event", - finish: func(m *Manager) { - m.ingestChatEvent(conversationTestAgentID, "run-1", &gatewayv2.ChatEvent{ - Type: gatewayv2.ChatEvent_ERROR, - ConversationId: "conv-1", - Data: `{"message":"provider failed"}`, - }) - }, - wantStatus: "failed", - }, - { - name: "completed control", - finish: func(m *Manager) { - m.ingestChatControl(conversationTestAgentID, "run-1", completedControl("run-1", "conv-1")) - }, - wantStatus: "completed", - }, - { - name: "terminal snapshot", - finish: func(m *Manager) { - m.ingestRuntimeSnapshot(conversationTestAgentID, &gatewayv2.ChatRuntimeSnapshot{ - RunId: "run-1", - ConversationId: "conv-1", - State: "completed", - }) - }, - wantStatus: "completed", - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - m := NewManager() - m.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-1")) - m.convStreams.onRuntimeStatus(conversationTestAgentID, runsReport(nil, nil), time.Now().Add(16*time.Second)) - - test.finish(m) - - last := lastEvent(t, m, "conv-1") - if last.Type != StreamEventRunFinished || last.Payload["status"] != test.wantStatus { - t.Fatalf("corrected terminal = %s %#v, want run_finished/%s", last.Type, last.Payload, test.wantStatus) - } - if last.Payload["error_code"] == "desktop_run_lost" { - t.Fatalf("authoritative terminal retained inferred loss: %#v", last.Payload) - } - record := m.convStreams.runs[agentScopedKey(conversationTestAgentID, "run-1")] - if record == nil || record.lostInferred || record.revived { - t.Fatalf("terminal run flags not settled: %#v", record) - } - if activities := m.ActiveConversationActivities(); len(activities) != 0 { - t.Fatalf("authoritative terminal left activity behind: %#v", activities) - } - }) - } -} - -// Stragglers after a genuine terminal stay dropped — resurrection applies only -// to inferred losses. -func TestGenuineTerminalStragglersStayDropped(t *testing.T) { - m := NewManager() - m.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-1")) - m.ingestChatEvent(conversationTestAgentID, "run-1", doneEvent("conv-1")) - m.ingestChatEvent(conversationTestAgentID, "run-1", tokenEvent("conv-1", "late")) - - if activities := m.ActiveConversationActivities(); len(activities) != 0 { - t.Fatalf("straggler resurrected a completed run, activities=%d", len(activities)) - } - last := lastEvent(t, m, "conv-1") - if last.Type != StreamEventRunFinished { - t.Fatalf("tail after straggler = %s, want run_finished", last.Type) - } -} - -// A reconnect republish of "started" re-anchors a run this store wrongly gave -// up on. -func TestStartedControlResurrectsInferredLostRun(t *testing.T) { - m := NewManager() - m.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-1")) - m.convStreams.onRuntimeStatus(conversationTestAgentID, runsReport(nil, nil), time.Now().Add(16*time.Second)) - - m.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-1")) - activities := m.ActiveConversationActivities() - if len(activities) != 1 || activities[0].RunID != "run-1" { - t.Fatalf("started republish must resurrect the lost run, activities=%#v", activities) - } -} - -// Queued runs belong to the accepted-command startup watchdog; the desktop may -// not know them yet, so reconcile never finalizes them. -func TestRunReportSkipsQueuedRuns(t *testing.T) { - m := NewManager() - m.StartChatCommand(conversationTestAgentID, "run-1", "conv-1", "/workspace", "client-1", []map[string]any{ - {"type": "user_message", "message": "hello"}, - }) - - t0 := time.Now() - m.convStreams.onRuntimeStatus(conversationTestAgentID, runsReport(nil, nil), t0) - m.convStreams.onRuntimeStatus(conversationTestAgentID, runsReport(nil, nil), t0.Add(time.Hour)) - - activities := m.ActiveConversationActivities() - if len(activities) != 1 || activities[0].State != RunActivityQueued { - t.Fatalf("queued run must survive reconcile, activities=%#v", activities) - } -} - -// Liveness is per run: a vouched run keeps streaming while an absent run in -// another conversation is finalized at grace. -func TestRunReportPerConversationLiveness(t *testing.T) { - m := NewManager() - m.ingestChatControl(conversationTestAgentID, "run-a", startedControl("run-a", "conv-a")) - m.ingestChatControl(conversationTestAgentID, "run-b", startedControl("run-b", "conv-b")) - t0 := time.Now() - vouchA := runsReport([]*gatewayv2.ChatRunReport{ - runReport("run-a", "conv-a", "running"), - }, nil) - - m.convStreams.onRuntimeStatus(conversationTestAgentID, vouchA, t0) - m.convStreams.onRuntimeStatus(conversationTestAgentID, vouchA, t0.Add(16*time.Second)) - - activities := m.ActiveConversationActivities() - if len(activities) != 1 || activities[0].RunID != "run-a" { - t.Fatalf("vouched run must outlive the lost one, activities=%#v", activities) - } - last := lastEvent(t, m, "conv-b") - if last.Type != StreamEventRunFinished || last.Payload["error_code"] != "desktop_run_lost" { - t.Fatalf("conv-b tail = %s %#v, want failed/desktop_run_lost", last.Type, last.Payload) - } -} - -// The reaper is per run too: a report vouching only for another conversation -// must not shield a run the desktop stopped vouching for. -func TestReaperSparesOnlyVouchedRuns(t *testing.T) { - m := NewManager() - m.convStreams.staleRunTimeout = 10 * time.Millisecond - m.SetSession(&AgentSession{ - AgentID: conversationTestAgentID, - toAgent: make(chan *OutboundEnvelope, 1), - done: make(chan struct{}), - streams: make(map[string]*agentStream), - }) - - m.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-1")) - time.Sleep(20 * time.Millisecond) - - m.ingestChatControl(conversationTestAgentID, "run-other", startedControl("run-other", "conv-other")) - m.convStreams.onRuntimeStatus(conversationTestAgentID, runsReport([]*gatewayv2.ChatRunReport{ - runReport("run-other", "conv-other", "running"), - }, nil), time.Now()) - - m.convStreams.reap(time.Now()) - activities := m.ActiveConversationActivities() - if len(activities) != 1 || activities[0].RunID != "run-other" { - t.Fatalf("unvouched run must be reaped, activities=%#v", activities) - } -} - -// Offline runs are not immortal: past offlineRunTimeout they finalize as -// agent_offline; below it they are kept. -func TestReaperFinalizesRunsAfterOfflineTimeout(t *testing.T) { - m := NewManager() // no session: isOnline() == false - m.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-1")) - t0 := time.Now() - - m.convStreams.reap(t0.Add(29 * time.Minute)) - if activities := m.ActiveConversationActivities(); len(activities) != 1 { - t.Fatalf("run below offline timeout must be kept, activities=%d", len(activities)) - } - - m.convStreams.reap(t0.Add(31 * time.Minute)) - if activities := m.ActiveConversationActivities(); len(activities) != 0 { - t.Fatalf("run beyond offline timeout must be finalized, activities=%d", len(activities)) - } - last := lastEvent(t, m, "conv-1") - if last.Type != StreamEventRunFinished || - last.Payload["status"] != "failed" || - last.Payload["error_code"] != "agent_offline" { - t.Fatalf("offline finish tail = %s %#v, want failed/agent_offline", last.Type, last.Payload) - } -} diff --git a/crates/agent-gateway/internal/session/conversation_stream_test.go b/crates/agent-gateway/internal/session/conversation_stream_test.go deleted file mode 100644 index 81bbe9254..000000000 --- a/crates/agent-gateway/internal/session/conversation_stream_test.go +++ /dev/null @@ -1,1202 +0,0 @@ -package session - -import ( - "encoding/json" - "testing" - "time" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -const conversationTestAgentID = "test-agent" - -func tokenEvent(conversationID string, text string) *gatewayv2.ChatEvent { - data, _ := json.Marshal(map[string]any{"text": text}) - return &gatewayv2.ChatEvent{ - Type: gatewayv2.ChatEvent_TOKEN, - ConversationId: conversationID, - Data: string(data), - } -} - -func doneEvent(conversationID string) *gatewayv2.ChatEvent { - return &gatewayv2.ChatEvent{ - Type: gatewayv2.ChatEvent_DONE, - ConversationId: conversationID, - Data: `{"title":"Final title"}`, - } -} - -func startedControl(runID string, conversationID string) *gatewayv2.ChatControlEvent { - return &gatewayv2.ChatControlEvent{ - RequestId: runID, - ConversationId: conversationID, - Type: "started", - State: "running", - } -} - -func completedControl(runID string, conversationID string) *gatewayv2.ChatControlEvent { - return &gatewayv2.ChatControlEvent{ - RequestId: runID, - ConversationId: conversationID, - Type: "completed", - State: "completed", - } -} - -func drainEvents(t *testing.T, ch <-chan *ConversationEvent, count int) []*ConversationEvent { - t.Helper() - events := make([]*ConversationEvent, 0, count) - timeout := time.After(2 * time.Second) - for len(events) < count { - select { - case event, ok := <-ch: - if !ok { - t.Fatalf("event channel closed after %d events, want %d", len(events), count) - } - events = append(events, event) - case <-timeout: - t.Fatalf("timed out after %d events, want %d", len(events), count) - } - } - return events -} - -func eventTypes(events []*ConversationEvent) []string { - types := make([]string, 0, len(events)) - for _, event := range events { - types = append(types, event.Type) - } - return types -} - -func TestConversationStreamSeqMonotonicAcrossRuns(t *testing.T) { - m := NewManager() - m.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-1")) - m.ingestChatEvent(conversationTestAgentID, "run-1", tokenEvent("conv-1", "hello")) - m.ingestChatEvent(conversationTestAgentID, "run-1", doneEvent("conv-1")) - m.ingestChatControl(conversationTestAgentID, "run-2", startedControl("run-2", "conv-1")) - m.ingestChatEvent(conversationTestAgentID, "run-2", tokenEvent("conv-1", "again")) - - sub := m.SubscribeConversationStream(conversationTestAgentID, "conv-1", 0, "") - defer sub.Cleanup() - - var lastSeq int64 - for _, event := range sub.Events { - if event.Seq <= lastSeq { - t.Fatalf("seq not monotonic: %d after %d (types %v)", event.Seq, lastSeq, eventTypes(sub.Events)) - } - lastSeq = event.Seq - } - types := eventTypes(sub.Events) - want := []string{"run_started", "token", "run_finished", "run_started", "token"} - if len(types) != len(want) { - t.Fatalf("replayed types = %v, want %v", types, want) - } - for i := range want { - if types[i] != want[i] { - t.Fatalf("replayed types = %v, want %v", types, want) - } - } - if sub.Activity == nil || sub.Activity.RunID != "run-2" || sub.Activity.State != RunActivityRunning { - t.Fatalf("activity = %#v, want running run-2", sub.Activity) - } -} - -func TestRunFinishedExactlyOnceForDuplicateTerminals(t *testing.T) { - cases := []struct { - name string - first func(m *Manager) - second func(m *Manager) - }{ - { - name: "done event then completed control", - first: func(m *Manager) { m.ingestChatEvent(conversationTestAgentID, "run-1", doneEvent("conv-1")) }, - second: func(m *Manager) { - m.ingestChatControl(conversationTestAgentID, "run-1", completedControl("run-1", "conv-1")) - }, - }, - { - name: "completed control then terminal snapshot", - first: func(m *Manager) { - m.ingestChatControl(conversationTestAgentID, "run-1", completedControl("run-1", "conv-1")) - }, - second: func(m *Manager) { - m.ingestRuntimeSnapshot(conversationTestAgentID, &gatewayv2.ChatRuntimeSnapshot{ - RunId: "run-1", ConversationId: "conv-1", State: "completed", - }) - }, - }, - { - name: "terminal snapshot then done event", - first: func(m *Manager) { - m.ingestRuntimeSnapshot(conversationTestAgentID, &gatewayv2.ChatRuntimeSnapshot{ - RunId: "run-1", ConversationId: "conv-1", State: "cancelled", - }) - }, - second: func(m *Manager) { m.ingestChatEvent(conversationTestAgentID, "run-1", doneEvent("conv-1")) }, - }, - { - name: "force finish then late done", - first: func(m *Manager) { m.ForceFinishRun(conversationTestAgentID, "run-1", "cancelled", "", "") }, - second: func(m *Manager) { m.ingestChatEvent(conversationTestAgentID, "run-1", doneEvent("conv-1")) }, - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - m := NewManager() - m.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-1")) - m.ingestChatEvent(conversationTestAgentID, "run-1", tokenEvent("conv-1", "hello")) - tc.first(m) - tc.second(m) - - sub := m.SubscribeConversationStream(conversationTestAgentID, "conv-1", 0, "") - defer sub.Cleanup() - finished := 0 - for _, event := range sub.Events { - if event.Type == StreamEventRunFinished { - finished++ - } - } - if finished != 1 { - t.Fatalf("run_finished count = %d, want 1 (types %v)", finished, eventTypes(sub.Events)) - } - if sub.Activity != nil { - t.Fatalf("activity should be cleared, got %#v", sub.Activity) - } - }) - } -} - -func TestSupersessionFinishesPreviousRunFirst(t *testing.T) { - m := NewManager() - m.ingestChatControl(conversationTestAgentID, "run-a", startedControl("run-a", "conv-1")) - m.ingestChatEvent(conversationTestAgentID, "run-a", tokenEvent("conv-1", "a")) - - sub := m.SubscribeConversationStream(conversationTestAgentID, "conv-1", 0, "") - defer sub.Cleanup() - - // A queued prompt auto-send: run-b starts before run-a's terminal arrives. - m.ingestChatControl(conversationTestAgentID, "run-b", startedControl("run-b", "conv-1")) - m.ingestChatEvent(conversationTestAgentID, "run-b", tokenEvent("conv-1", "b")) - - live := drainEvents(t, sub.EventCh, 3) - types := eventTypes(live) - if types[0] != StreamEventRunFinished || live[0].RunID != "run-a" { - t.Fatalf("first live event = %s/%s, want run_finished/run-a", types[0], live[0].RunID) - } - if live[0].Payload["reason"] != "superseded" { - t.Fatalf("superseded reason missing: %#v", live[0].Payload) - } - if types[1] != StreamEventRunStarted || live[1].RunID != "run-b" { - t.Fatalf("second live event = %s/%s, want run_started/run-b", types[1], live[1].RunID) - } - if types[2] != "token" || live[2].RunID != "run-b" { - t.Fatalf("third live event = %s/%s, want token/run-b", types[2], live[2].RunID) - } - - // The late terminal for run-a is swallowed. - m.ingestChatControl(conversationTestAgentID, "run-a", completedControl("run-a", "conv-1")) - m.ingestChatEvent(conversationTestAgentID, "run-b", tokenEvent("conv-1", "b2")) - next := drainEvents(t, sub.EventCh, 1) - if next[0].Type != "token" || next[0].RunID != "run-b" { - t.Fatalf("late terminal leaked: got %s/%s", next[0].Type, next[0].RunID) - } -} - -func TestSubscribeResumeAndResetSemantics(t *testing.T) { - m := NewManager() - m.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-1")) - m.ingestChatEvent(conversationTestAgentID, "run-1", tokenEvent("conv-1", "one")) - m.ingestChatEvent(conversationTestAgentID, "run-1", tokenEvent("conv-1", "two")) - - base := m.SubscribeConversationStream(conversationTestAgentID, "conv-1", 0, "") - base.Cleanup() - epoch := base.StreamEpoch - latest := base.LatestSeq - - resume := m.SubscribeConversationStream(conversationTestAgentID, "conv-1", latest-1, epoch) - resume.Cleanup() - if resume.Reset { - t.Fatalf("resume within buffer should not reset") - } - if len(resume.Events) != 1 || resume.Events[0].Seq != latest { - t.Fatalf("resume replay = %v", eventTypes(resume.Events)) - } - - ahead := m.SubscribeConversationStream(conversationTestAgentID, "conv-1", latest+100, epoch) - ahead.Cleanup() - if !ahead.Reset || len(ahead.Events) != int(latest) { - t.Fatalf("client ahead of gateway must reset with full replay, got reset=%v events=%d", ahead.Reset, len(ahead.Events)) - } - - wrongEpoch := m.SubscribeConversationStream(conversationTestAgentID, "conv-1", latest, "different-epoch") - wrongEpoch.Cleanup() - if !wrongEpoch.Reset { - t.Fatalf("epoch mismatch must reset") - } - - // Gap: force eviction of the early events. - m.convStreams.mu.Lock() - stream := m.convStreams.streams[conversationStreamKey(conversationTestAgentID, "conv-1")] - stream.evictedThroughSeq = 2 - m.convStreams.mu.Unlock() - gap := m.SubscribeConversationStream(conversationTestAgentID, "conv-1", 1, epoch) - gap.Cleanup() - if !gap.Reset { - t.Fatalf("resume below evicted floor must reset") - } -} - -func TestStartChatCommandSeedsAndAgentEchoSwallowed(t *testing.T) { - m := NewManager() - start := m.StartChatCommand(conversationTestAgentID, "run-1", "conv-1", "/workspace", "client-1", []map[string]any{ - {"type": "user_message", "message": "hello"}, - }) - if start.AcceptedSeq <= 0 { - t.Fatalf("accepted seq = %d, want > 0", start.AcceptedSeq) - } - - sub := m.SubscribeConversationStream(conversationTestAgentID, "conv-1", 0, "") - defer sub.Cleanup() - if len(sub.Events) != 1 || sub.Events[0].Type != "user_message" { - t.Fatalf("seeded replay = %v", eventTypes(sub.Events)) - } - if sub.Events[0].Payload["client_request_id"] != "client-1" { - t.Fatalf("seeded user_message missing client_request_id: %#v", sub.Events[0].Payload) - } - if sub.Activity == nil || sub.Activity.State != RunActivityQueued { - t.Fatalf("activity = %#v, want queued", sub.Activity) - } - - // Agent starts the run and echoes the user message: the echo is swallowed. - m.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-1")) - userEcho, _ := json.Marshal(map[string]any{"message": "hello"}) - m.ingestChatEvent(conversationTestAgentID, "run-1", &gatewayv2.ChatEvent{ - Type: gatewayv2.ChatEvent_USER_MESSAGE, - ConversationId: "conv-1", - Data: string(userEcho), - }) - m.ingestChatEvent(conversationTestAgentID, "run-1", tokenEvent("conv-1", "hi")) - - live := drainEvents(t, sub.EventCh, 2) - types := eventTypes(live) - if types[0] != StreamEventRunStarted || types[1] != "token" { - t.Fatalf("live types = %v, want [run_started token]", types) - } -} - -func TestSeededUserMessageForwardsStableIdentityOnce(t *testing.T) { - m := NewManager() - m.StartChatCommand(conversationTestAgentID, "run-1", "conv-1", "/workspace", "client-1", []map[string]any{ - {"type": "user_message", "message": "hello"}, - }) - m.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-1")) - identityEcho, _ := json.Marshal(map[string]any{ - "message": "hello", - "message_id": "user-stable-1", - }) - event := &gatewayv2.ChatEvent{ - Type: gatewayv2.ChatEvent_USER_MESSAGE, - ConversationId: "conv-1", - Data: string(identityEcho), - } - m.ingestChatEvent(conversationTestAgentID, "run-1", event) - m.ingestChatEvent(conversationTestAgentID, "run-1", event) - - sub := m.SubscribeConversationStream(conversationTestAgentID, "conv-1", 0, "") - defer sub.Cleanup() - if got, want := eventTypes(sub.Events), []string{"user_message", StreamEventRunStarted, "user_message"}; len(got) != len(want) || got[0] != want[0] || got[1] != want[1] || got[2] != want[2] { - t.Fatalf("replay = %v, want %v", got, want) - } - identity := sub.Events[2] - if identity.Payload["message_id"] != "user-stable-1" { - t.Fatalf("identity payload = %#v", identity.Payload) - } -} - -func TestPendingRunBindsOnFirstAgentSignal(t *testing.T) { - m := NewManager() - updates, cleanupWatch := m.WatchChatCommand(conversationTestAgentID, "run-1") - defer cleanupWatch() - - start := m.StartChatCommand(conversationTestAgentID, "run-1", "", "/workspace", "client-1", []map[string]any{ - {"type": "user_message", "message": "hello"}, - }) - if start.ConversationID != "" || start.AcceptedSeq != 0 { - t.Fatalf("pending start = %#v", start) - } - - m.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-new")) - - select { - case update := <-updates: - if update.Phase != "bound" || update.ConversationID != "conv-new" || update.ClientRequestID != "client-1" { - t.Fatalf("bound update = %#v", update) - } - case <-time.After(2 * time.Second): - t.Fatalf("no bound update") - } - - sub := m.SubscribeConversationStream(conversationTestAgentID, "conv-new", 0, "") - defer sub.Cleanup() - types := eventTypes(sub.Events) - want := []string{"user_message", "run_started"} - if len(types) != len(want) || types[0] != want[0] || types[1] != want[1] { - t.Fatalf("bound replay = %v, want %v", types, want) - } -} - -func TestQueuedInGUICompensatesSeededEntries(t *testing.T) { - m := NewManager() - updates, cleanupWatch := m.WatchChatCommand(conversationTestAgentID, "run-1") - defer cleanupWatch() - - m.StartChatCommand(conversationTestAgentID, "run-1", "conv-1", "", "client-1", []map[string]any{ - {"type": "user_message", "message": "queued prompt"}, - }) - m.ingestChatControl(conversationTestAgentID, "run-1", &gatewayv2.ChatControlEvent{ - RequestId: "run-1", - ConversationId: "conv-1", - Type: "queued_in_gui", - }) - - select { - case update := <-updates: - if update.Phase != "queued_in_gui" { - t.Fatalf("update = %#v", update) - } - case <-time.After(2 * time.Second): - t.Fatalf("no queued_in_gui update") - } - - sub := m.SubscribeConversationStream(conversationTestAgentID, "conv-1", 0, "") - defer sub.Cleanup() - types := eventTypes(sub.Events) - if len(types) != 2 || types[0] != "user_message" || types[1] != StreamEventRunQueued { - t.Fatalf("replay = %v, want [user_message run_queued]", types) - } - if sub.Activity != nil { - t.Fatalf("queued_in_gui must clear activity, got %#v", sub.Activity) - } - if m.ChatCommandSettled(conversationTestAgentID, "run-1") != true { - t.Fatalf("queued_in_gui command must count as settled") - } - - // Later auto-send: the agent echo must now pass through. - m.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-1")) - userEcho, _ := json.Marshal(map[string]any{"message": "queued prompt"}) - m.ingestChatEvent(conversationTestAgentID, "run-1", &gatewayv2.ChatEvent{ - Type: gatewayv2.ChatEvent_USER_MESSAGE, - ConversationId: "conv-1", - Data: string(userEcho), - }) - live := drainEvents(t, sub.EventCh, 2) - types = eventTypes(live) - if types[0] != StreamEventRunStarted || types[1] != "user_message" { - t.Fatalf("auto-send live types = %v, want [run_started user_message]", types) - } -} - -func TestFailChatCommandPendingAndBound(t *testing.T) { - m := NewManager() - updates, cleanupWatch := m.WatchChatCommand(conversationTestAgentID, "run-pending") - defer cleanupWatch() - m.StartChatCommand(conversationTestAgentID, "run-pending", "", "", "client-1", nil) - m.FailChatCommand(conversationTestAgentID, "run-pending", "desktop_runtime_unavailable", "agent offline") - select { - case update := <-updates: - if update.Phase != "failed" || update.ErrorCode != "desktop_runtime_unavailable" { - t.Fatalf("failed update = %#v", update) - } - case <-time.After(2 * time.Second): - t.Fatalf("no failed update") - } - - m.StartChatCommand(conversationTestAgentID, "run-bound", "conv-1", "", "client-2", []map[string]any{ - {"type": "user_message", "message": "hello"}, - }) - m.FailChatCommand(conversationTestAgentID, "run-bound", "startup_timeout", "did not start") - sub := m.SubscribeConversationStream(conversationTestAgentID, "conv-1", 0, "") - defer sub.Cleanup() - last := sub.Events[len(sub.Events)-1] - if last.Type != StreamEventRunFinished || last.Payload["status"] != "failed" { - t.Fatalf("bound failure tail = %s %#v", last.Type, last.Payload) - } - if !m.ChatCommandSettled(conversationTestAgentID, "run-bound") { - t.Fatalf("failed command should be settled") - } -} - -func TestStartChatCommandDeduplicatesAtomically(t *testing.T) { - t.Parallel() - - m := NewManager() - results := make(chan ChatCommandStart, 2) - start := func(runID string, message string) { - results <- m.StartChatCommand(conversationTestAgentID, runID, "conv-1", "/workspace", "client-shared", []map[string]any{ - {"type": "user_message", "message": message}, - }) - } - go start("run-a", "first") - go start("run-b", "second") - first := <-results - second := <-results - - if first.RunID != second.RunID { - t.Fatalf("concurrent canonical runs = %q and %q", first.RunID, second.RunID) - } - if first.Deduped == second.Deduped { - t.Fatalf("dedupe flags = %v and %v, want exactly one canonical creator", first.Deduped, second.Deduped) - } - canonical, ok := m.LookupChatCommand(conversationTestAgentID, "client-shared") - if !ok || canonical.RunID != first.RunID || !canonical.Deduped { - t.Fatalf("canonical lookup = %#v, ok=%v", canonical, ok) - } - - sub := m.SubscribeConversationStream(conversationTestAgentID, "conv-1", 0, "") - defer sub.Cleanup() - userMessages := 0 - for _, event := range sub.Events { - if event.Type == "user_message" { - userMessages++ - } - } - if userMessages != 1 { - t.Fatalf("concurrent dedupe seeded %d user messages, want 1", userMessages) - } -} - -func TestDeduplicatedPendingCommandReplaysBoundUpdate(t *testing.T) { - t.Parallel() - - m := NewManager() - start := m.StartChatCommand(conversationTestAgentID, "run-original", "", "/workspace", "client-bound", []map[string]any{ - {"type": "user_message", "message": "hello"}, - }) - if start.Deduped { - t.Fatalf("initial command unexpectedly deduped: %#v", start) - } - m.ingestChatControl(conversationTestAgentID, "run-original", startedControl("run-original", "conv-bound")) - - retry := m.StartChatCommand(conversationTestAgentID, "run-retry", "", "/other", "client-bound", []map[string]any{ - {"type": "user_message", "message": "duplicate"}, - }) - if !retry.Deduped || retry.RunID != "run-original" || retry.ConversationID != "conv-bound" || retry.AcceptedSeq <= 0 { - t.Fatalf("deduplicated bound command = %#v", retry) - } - updates, cleanup := m.WatchChatCommand(conversationTestAgentID, retry.RunID) - defer cleanup() - select { - case update := <-updates: - if update.Phase != "bound" || update.ConversationID != "conv-bound" { - t.Fatalf("replayed bound update = %#v", update) - } - case <-time.After(time.Second): - t.Fatal("timed out waiting for replayed bound update") - } -} - -func TestDeduplicatedPendingCommandReplaysFailedUpdate(t *testing.T) { - t.Parallel() - - m := NewManager() - m.StartChatCommand(conversationTestAgentID, "run-failed", "", "", "client-failed", nil) - m.FailChatCommand(conversationTestAgentID, "run-failed", "desktop_runtime_unavailable", "delivery timed out") - - retry := m.StartChatCommand(conversationTestAgentID, "run-retry", "", "", "client-failed", nil) - if !retry.Deduped || retry.RunID != "run-failed" { - t.Fatalf("deduplicated failed command = %#v", retry) - } - updates, cleanup := m.WatchChatCommand(conversationTestAgentID, retry.RunID) - defer cleanup() - select { - case update := <-updates: - if update.Phase != "failed" || update.ErrorCode != "desktop_runtime_unavailable" { - t.Fatalf("replayed failed update = %#v", update) - } - case <-time.After(time.Second): - t.Fatal("timed out waiting for replayed failed update") - } -} - -func TestActivityHubCarriesRunIDs(t *testing.T) { - m := NewManager() - activity, cleanup := m.SubscribeChatActivity() - defer cleanup() - - m.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-1")) - m.ingestChatEvent(conversationTestAgentID, "run-1", doneEvent("conv-1")) - - running := <-activity - if !running.Running || running.RunID != "run-1" || running.State != RunActivityRunning { - t.Fatalf("running activity = %#v", running) - } - idle := <-activity - if idle.Running || idle.ConversationID != "conv-1" { - t.Fatalf("idle activity = %#v", idle) - } - - // A late subscriber replays current activity. - m.ingestChatControl(conversationTestAgentID, "run-2", startedControl("run-2", "conv-2")) - late, lateCleanup := m.SubscribeChatActivity() - defer lateCleanup() - replayed := <-late - if replayed.ConversationID != "conv-2" || replayed.RunID != "run-2" || !replayed.Running { - t.Fatalf("late replay = %#v", replayed) - } -} - -func TestEvictionProtectsActiveRunUntilHardCap(t *testing.T) { - m := NewManager() - m.convStreams.eventRetention = time.Millisecond - m.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-1")) - m.ingestChatEvent(conversationTestAgentID, "run-1", tokenEvent("conv-1", "one")) - time.Sleep(5 * time.Millisecond) - m.ingestChatEvent(conversationTestAgentID, "run-1", tokenEvent("conv-1", "two")) - - sub := m.SubscribeConversationStream(conversationTestAgentID, "conv-1", 0, "") - sub.Cleanup() - if len(sub.Events) != 3 { - t.Fatalf("active run events must survive retention, got %v", eventTypes(sub.Events)) - } - - // After the run finishes, retention applies again. - m.ingestChatEvent(conversationTestAgentID, "run-1", doneEvent("conv-1")) - time.Sleep(5 * time.Millisecond) - m.convStreams.mu.Lock() - stream := m.convStreams.streams[conversationStreamKey(conversationTestAgentID, "conv-1")] - m.convStreams.evictStreamLocked(stream, time.Now()) - remaining := len(stream.events) - evictedThrough := stream.evictedThroughSeq - m.convStreams.mu.Unlock() - if remaining != 0 || evictedThrough == 0 { - t.Fatalf("idle stream should evict all expired events, remaining=%d evictedThrough=%d", remaining, evictedThrough) - } - - // Hard cap evicts even active-run events and flags truncation via the - // evicted floor so subscribers get a reset. - m2 := NewManager() - m2.convStreams.maxEvents = 4 - m2.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-x")) - for i := 0; i < 10; i++ { - m2.ingestChatEvent(conversationTestAgentID, "run-1", tokenEvent("conv-x", "t")) - } - m2.convStreams.mu.Lock() - streamX := m2.convStreams.streams[conversationStreamKey(conversationTestAgentID, "conv-x")] - if len(streamX.events) > 4 { - m2.convStreams.mu.Unlock() - t.Fatalf("hard cap not enforced: %d events", len(streamX.events)) - } - if streamX.evictedThroughSeq == 0 { - m2.convStreams.mu.Unlock() - t.Fatalf("evictedThroughSeq not advanced under hard cap") - } - m2.convStreams.mu.Unlock() -} - -func TestReaperForceFinishesStaleRunsOnlyWhenOnline(t *testing.T) { - m := NewManager() - m.convStreams.staleRunTimeout = time.Millisecond - m.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-1")) - time.Sleep(5 * time.Millisecond) - - // Agent offline: the run is left alone. - m.convStreams.reap(time.Now()) - if activities := m.ActiveConversationActivities(); len(activities) != 1 { - t.Fatalf("offline reap must not finish runs, activities=%d", len(activities)) - } - - // Agent online: the silent run is force-finished. - m.SetSession(&AgentSession{ - AgentID: conversationTestAgentID, - toAgent: make(chan *OutboundEnvelope, 1), - done: make(chan struct{}), - streams: make(map[string]*agentStream), - }) - m.convStreams.reap(time.Now()) - if activities := m.ActiveConversationActivities(); len(activities) != 0 { - t.Fatalf("online reap must finish stale runs, activities=%d", len(activities)) - } - - sub := m.SubscribeConversationStream(conversationTestAgentID, "conv-1", 0, "") - sub.Cleanup() - last := sub.Events[len(sub.Events)-1] - if last.Type != StreamEventRunFinished || last.Payload["error_code"] != "stale_run" { - t.Fatalf("stale finish tail = %s %#v", last.Type, last.Payload) - } -} - -func TestCancellingStateAndWatchdog(t *testing.T) { - m := NewManager() - m.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-1")) - - runID, ok := m.MarkConversationCancelling(conversationTestAgentID, "conv-1", "") - if !ok || runID != "run-1" { - t.Fatalf("cancelling = %q %v", runID, ok) - } - sub := m.SubscribeConversationStream(conversationTestAgentID, "conv-1", 0, "") - defer sub.Cleanup() - if sub.Activity == nil || sub.Activity.State != RunActivityCancelling { - t.Fatalf("activity = %#v, want cancelling", sub.Activity) - } - - // The agent's real terminal wins over the watchdog. - m.ingestChatControl(conversationTestAgentID, "run-1", &gatewayv2.ChatControlEvent{ - RequestId: "run-1", ConversationId: "conv-1", Type: "cancelled", State: "cancelled", - }) - m.ForceFinishRun(conversationTestAgentID, "run-1", "cancelled", "cancel_timeout", "watchdog") - - finished := 0 - for _, event := range drainEvents(t, sub.EventCh, 1) { - if event.Type == StreamEventRunFinished { - finished++ - if event.Payload["error_code"] == "cancel_timeout" { - t.Fatalf("watchdog overrode the agent terminal: %#v", event.Payload) - } - } - } - if finished != 1 { - t.Fatalf("run_finished count = %d", finished) - } -} - -func TestGatewayRestartSnapshotRebuildsStream(t *testing.T) { - // A fresh manager simulates a restarted gateway: the first thing it sees - // for the conversation is a runtime snapshot of an in-flight run. - m := NewManager() - m.ingestRuntimeSnapshot(conversationTestAgentID, &gatewayv2.ChatRuntimeSnapshot{ - RunId: "run-1", - ConversationId: "conv-1", - State: "running", - Revision: 7, - EntriesJson: `[{"kind":"assistant","text":"partial"}]`, - ToolStatus: "Vibing", - }) - - sub := m.SubscribeConversationStream(conversationTestAgentID, "conv-1", 0, "") - defer sub.Cleanup() - if sub.Activity == nil || sub.Activity.RunID != "run-1" { - t.Fatalf("activity = %#v", sub.Activity) - } - if sub.Activity.ToolStatus != "Vibing" { - t.Fatalf("tool status = %q", sub.Activity.ToolStatus) - } - if sub.Snapshot == nil || sub.Snapshot.Revision != 7 { - t.Fatalf("late joiner must get the snapshot, got %#v", sub.Snapshot) - } - - // Live continuation streams normally afterwards. - m.ingestChatEvent(conversationTestAgentID, "run-1", tokenEvent("conv-1", "more")) - live := drainEvents(t, sub.EventCh, 1) - if live[0].Type != "token" { - t.Fatalf("live continuation = %v", eventTypes(live)) - } -} - -func TestSubscriberOverflowClosesAndResumes(t *testing.T) { - m := NewManager() - m.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-1")) - sub := m.SubscribeConversationStream(conversationTestAgentID, "conv-1", 0, "") - defer sub.Cleanup() - - for i := 0; i < conversationSubscriberBuffer+8; i++ { - m.ingestChatEvent(conversationTestAgentID, "run-1", tokenEvent("conv-1", "x")) - } - - deadline := time.After(2 * time.Second) - closed := false - received := 0 - for !closed { - select { - case _, ok := <-sub.EventCh: - if !ok { - closed = true - break - } - received++ - if received > conversationSubscriberBuffer+8 { - t.Fatalf("received more events than sent") - } - case <-deadline: - t.Fatalf("subscriber channel never closed on overflow") - } - } - if !sub.Overflowed() { - t.Fatalf("overflow flag not set") - } - - // Resume from the last seen seq replays the tail without loss. - resume := m.SubscribeConversationStream(conversationTestAgentID, "conv-1", int64(received)+1, sub.StreamEpoch) - resume.Cleanup() - if resume.Reset { - t.Fatalf("resume after overflow should not reset while buffer covers the gap") - } - total := received + len(resume.Events) - if total < conversationSubscriberBuffer+8 { - t.Fatalf("lost events across overflow: saw %d", total) - } -} - -func TestReaperSparesSilentRunsWhileReportsVouch(t *testing.T) { - m := NewManager() - m.convStreams.staleRunTimeout = 10 * time.Millisecond - m.SetSession(&AgentSession{ - AgentID: conversationTestAgentID, - toAgent: make(chan *OutboundEnvelope, 1), - done: make(chan struct{}), - streams: make(map[string]*agentStream), - }) - - m.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-1")) - time.Sleep(20 * time.Millisecond) - - // A silent long tool call: no events, but the run report vouches for it. - m.convStreams.onRuntimeStatus(conversationTestAgentID, &gatewayv2.RuntimeStatusEvent{ - ActiveRuns: []*gatewayv2.ChatRunReport{ - {RunId: "run-1", ConversationId: "conv-1", State: "running"}, - }, - }, time.Now()) - m.convStreams.reap(time.Now()) - if activities := m.ActiveConversationActivities(); len(activities) != 1 { - t.Fatalf("vouched silent run must be spared, activities=%d", len(activities)) - } - - // Once the reports stop vouching for it, the run is reaped after the - // timeout elapses again. - time.Sleep(20 * time.Millisecond) - m.convStreams.reap(time.Now()) - if activities := m.ActiveConversationActivities(); len(activities) != 0 { - t.Fatalf("stale run must be reaped once vouching stops, activities=%d", len(activities)) - } -} - -func TestSupersessionKeepsSeededUserMessageReplayable(t *testing.T) { - m := NewManager() - m.convStreams.eventRetention = time.Millisecond - - // Run A streams while a webui command for the same conversation is - // accepted and seeded; B later starts via supersession. - m.ingestChatControl(conversationTestAgentID, "run-a", startedControl("run-a", "conv-1")) - m.StartChatCommand(conversationTestAgentID, "run-b", "conv-1", "", "client-b", []map[string]any{ - {"type": "user_message", "message": "seeded prompt"}, - }) - m.ingestChatEvent(conversationTestAgentID, "run-a", tokenEvent("conv-1", "working")) - m.ingestChatControl(conversationTestAgentID, "run-b", startedControl("run-b", "conv-1")) - - // Age everything past retention, then trigger eviction: run B's activity - // window must still protect the seeded user_message. - time.Sleep(5 * time.Millisecond) - m.ingestChatEvent(conversationTestAgentID, "run-b", tokenEvent("conv-1", "reply")) - - sub := m.SubscribeConversationStream(conversationTestAgentID, "conv-1", 0, "") - defer sub.Cleanup() - hasSeed := false - for _, event := range sub.Events { - if event.Type == "user_message" && event.RunID == "run-b" { - hasSeed = true - } - } - if !hasSeed { - t.Fatalf("seeded user_message evicted despite active run: %v", eventTypes(sub.Events)) - } -} - -func TestWatchChatCommandCleanupClosesChannel(t *testing.T) { - m := NewManager() - updates, cleanup := m.WatchChatCommand(conversationTestAgentID, "run-1") - cleanup() - select { - case _, ok := <-updates: - if ok { - t.Fatalf("expected closed channel, got value") - } - case <-time.After(time.Second): - t.Fatalf("watch channel not closed by cleanup") - } -} - -func TestSeedsDeferredWhileAnotherRunIsActive(t *testing.T) { - m := NewManager() - m.ingestChatControl(conversationTestAgentID, "run-a", startedControl("run-a", "conv-1")) - m.ingestChatEvent(conversationTestAgentID, "run-a", tokenEvent("conv-1", "streaming")) - - sub := m.SubscribeConversationStream(conversationTestAgentID, "conv-1", 0, "") - defer sub.Cleanup() - - // Command accepted mid-run: nothing may reach the log yet — a seeded - // user_message here would flash a bubble until queued_in_gui compensates. - start := m.StartChatCommand(conversationTestAgentID, "run-b", "conv-1", "", "client-b", []map[string]any{ - {"type": "user_message", "message": "queued while busy"}, - }) - if start.AcceptedSeq != sub.LatestSeq { - t.Fatalf("deferred accept must not append events: acceptedSeq=%d latest=%d", start.AcceptedSeq, sub.LatestSeq) - } - - // The desktop parks it: still nothing in the log (no run_queued needed). - m.ingestChatControl(conversationTestAgentID, "run-b", &gatewayv2.ChatControlEvent{ - RequestId: "run-b", ConversationId: "conv-1", Type: "queued_in_gui", - }) - m.ingestChatEvent(conversationTestAgentID, "run-a", tokenEvent("conv-1", "more")) - live := drainEvents(t, sub.EventCh, 1) - if live[0].Type != "token" { - t.Fatalf("expected only run-a token after deferred accept + park, got %v", eventTypes(live)) - } - - // The queued item eventually auto-sends: the agent echo is authoritative. - m.ingestChatEvent(conversationTestAgentID, "run-a", doneEvent("conv-1")) - m.ingestChatControl(conversationTestAgentID, "run-b", startedControl("run-b", "conv-1")) - echo, _ := json.Marshal(map[string]any{"message": "queued while busy"}) - m.ingestChatEvent(conversationTestAgentID, "run-b", &gatewayv2.ChatEvent{ - Type: gatewayv2.ChatEvent_USER_MESSAGE, ConversationId: "conv-1", Data: string(echo), - }) - tail := drainEvents(t, sub.EventCh, 3) - types := eventTypes(tail) - if types[0] != StreamEventRunFinished || types[1] != StreamEventRunStarted || types[2] != "user_message" { - t.Fatalf("auto-send tail = %v, want [run_finished run_started user_message]", types) - } -} - -func TestDeferredSeedsFlushWhenRunStartsDirectly(t *testing.T) { - m := NewManager() - m.ingestChatControl(conversationTestAgentID, "run-a", startedControl("run-a", "conv-1")) - - m.StartChatCommand(conversationTestAgentID, "run-b", "conv-1", "", "client-b", []map[string]any{ - {"type": "user_message", "message": "interrupt prompt"}, - }) - - // The desktop runs the command immediately (interrupt policy): the - // deferred seeds surface right before run_started, and the agent's echo - // is swallowed as usual. - m.ingestChatControl(conversationTestAgentID, "run-b", startedControl("run-b", "conv-1")) - echo, _ := json.Marshal(map[string]any{"message": "interrupt prompt"}) - m.ingestChatEvent(conversationTestAgentID, "run-b", &gatewayv2.ChatEvent{ - Type: gatewayv2.ChatEvent_USER_MESSAGE, ConversationId: "conv-1", Data: string(echo), - }) - m.ingestChatEvent(conversationTestAgentID, "run-b", tokenEvent("conv-1", "reply")) - - sub := m.SubscribeConversationStream(conversationTestAgentID, "conv-1", 0, "") - defer sub.Cleanup() - types := eventTypes(sub.Events) - want := []string{"run_started", "run_finished", "user_message", "run_started", "token"} - if len(types) != len(want) { - t.Fatalf("replay = %v, want %v", types, want) - } - for i := range want { - if types[i] != want[i] { - t.Fatalf("replay = %v, want %v", types, want) - } - } - userMessages := 0 - for _, event := range sub.Events { - if event.Type == "user_message" { - userMessages++ - if event.Payload["client_request_id"] != "client-b" { - t.Fatalf("seeded user_message missing client_request_id: %#v", event.Payload) - } - } - } - if userMessages != 1 { - t.Fatalf("user_message count = %d, want 1 (echo swallowed)", userMessages) - } -} - -func editResendUserMessageEvent(conversationID string, message string, ref map[string]any) *gatewayv2.ChatEvent { - payload := map[string]any{"message": message} - if ref != nil { - payload["base_message_ref"] = ref - payload["reason"] = "edit_resend" - } - data, _ := json.Marshal(payload) - return &gatewayv2.ChatEvent{ - Type: gatewayv2.ChatEvent_USER_MESSAGE, - ConversationId: conversationID, - Data: string(data), - } -} - -func testBaseMessageRef() map[string]any { - return map[string]any{ - "segment_index": 0, - "message_index": 2, - "segment_id": "seg-1", - "message_id": "msg-2", - "role": "user", - "content_hash": "hash-2", - } -} - -func countEventType(events []*ConversationEvent, eventType string) int { - count := 0 - for _, event := range events { - if event.Type == eventType { - count++ - } - } - return count -} - -// GUI-local edit-resend, "started" control first (the usual desktop order): -// the ref-bearing user_message seeds one rebased event after run_started. -func TestGUIEditResendSeedsRebasedAfterRunStarted(t *testing.T) { - m := NewManager() - m.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-1")) - m.ingestChatEvent(conversationTestAgentID, "run-1", editResendUserMessageEvent("conv-1", "edited prompt", testBaseMessageRef())) - m.ingestChatEvent(conversationTestAgentID, "run-1", tokenEvent("conv-1", "reply")) - - sub := m.SubscribeConversationStream(conversationTestAgentID, "conv-1", 0, "") - defer sub.Cleanup() - types := eventTypes(sub.Events) - want := []string{StreamEventRunStarted, StreamEventRebased, "user_message", "token"} - if len(types) != len(want) { - t.Fatalf("replay = %v, want %v", types, want) - } - for i := range want { - if types[i] != want[i] { - t.Fatalf("replay = %v, want %v", types, want) - } - } - rebased := sub.Events[1] - ref, ok := rebased.Payload["base_message_ref"].(map[string]any) - if !ok || ref["message_id"] != "msg-2" || ref["content_hash"] != "hash-2" { - t.Fatalf("rebased base_message_ref = %#v", rebased.Payload["base_message_ref"]) - } - if rebased.Payload["reason"] != "edit_resend" { - t.Fatalf("rebased reason = %#v, want edit_resend", rebased.Payload["reason"]) - } -} - -// No prior control signal: the rebased seed still lands, before the -// synthesized run_started. -func TestGUIEditResendSeedsRebasedBeforeRunStarted(t *testing.T) { - m := NewManager() - m.ingestChatEvent(conversationTestAgentID, "run-1", editResendUserMessageEvent("conv-1", "edited prompt", testBaseMessageRef())) - - sub := m.SubscribeConversationStream(conversationTestAgentID, "conv-1", 0, "") - defer sub.Cleanup() - types := eventTypes(sub.Events) - want := []string{StreamEventRebased, StreamEventRunStarted, "user_message"} - if len(types) != len(want) { - t.Fatalf("replay = %v, want %v", types, want) - } - for i := range want { - if types[i] != want[i] { - t.Fatalf("replay = %v, want %v", types, want) - } - } -} - -// A reconnect replay redelivers the same ref-bearing user_message: exactly -// one rebased event is seeded for the run. -func TestGUIEditResendRebasedSeededOnce(t *testing.T) { - m := NewManager() - m.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-1")) - m.ingestChatEvent(conversationTestAgentID, "run-1", editResendUserMessageEvent("conv-1", "edited prompt", testBaseMessageRef())) - m.ingestChatEvent(conversationTestAgentID, "run-1", editResendUserMessageEvent("conv-1", "edited prompt", testBaseMessageRef())) - - sub := m.SubscribeConversationStream(conversationTestAgentID, "conv-1", 0, "") - defer sub.Cleanup() - if got := countEventType(sub.Events, StreamEventRebased); got != 1 { - t.Fatalf("rebased count = %d (types %v), want 1", got, eventTypes(sub.Events)) - } -} - -// Plain sends (no ref, a null ref — the desktop bridge always serializes the -// key, as null when unset — or an empty ref) must not seed a truncation. -func TestUserMessageWithoutRefSeedsNoRebased(t *testing.T) { - m := NewManager() - m.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-1")) - m.ingestChatEvent(conversationTestAgentID, "run-1", editResendUserMessageEvent("conv-1", "plain prompt", nil)) - - nullRef, _ := json.Marshal(map[string]any{ - "message": "null ref prompt", - "base_message_ref": nil, - }) - m.ingestChatControl(conversationTestAgentID, "run-1", completedControl("run-1", "conv-1")) - m.ingestChatControl(conversationTestAgentID, "run-2", startedControl("run-2", "conv-1")) - m.ingestChatEvent(conversationTestAgentID, "run-2", &gatewayv2.ChatEvent{ - Type: gatewayv2.ChatEvent_USER_MESSAGE, - ConversationId: "conv-1", - Data: string(nullRef), - }) - - emptyRef, _ := json.Marshal(map[string]any{ - "message": "empty ref prompt", - "base_message_ref": map[string]any{"message_id": "", "content_hash": " "}, - }) - m.ingestChatControl(conversationTestAgentID, "run-2", completedControl("run-2", "conv-1")) - m.ingestChatControl(conversationTestAgentID, "run-3", startedControl("run-3", "conv-1")) - m.ingestChatEvent(conversationTestAgentID, "run-3", &gatewayv2.ChatEvent{ - Type: gatewayv2.ChatEvent_USER_MESSAGE, - ConversationId: "conv-1", - Data: string(emptyRef), - }) - - sub := m.SubscribeConversationStream(conversationTestAgentID, "conv-1", 0, "") - defer sub.Cleanup() - if got := countEventType(sub.Events, StreamEventRebased); got != 0 { - t.Fatalf("rebased count = %d (types %v), want 0", got, eventTypes(sub.Events)) - } -} - -// A webui-initiated edit_resend already seeds its rebased at accept time; -// the agent's ref-bearing echo is swallowed and must not seed a second one. -func TestWebuiEditResendEchoSeedsNoSecondRebased(t *testing.T) { - m := NewManager() - ref := testBaseMessageRef() - m.StartChatCommand(conversationTestAgentID, "run-1", "conv-1", "/workspace", "client-1", []map[string]any{ - {"type": StreamEventRebased, "base_message_ref": ref, "reason": "edit_resend"}, - {"type": "user_message", "message": "edited prompt", "base_message_ref": ref, "reason": "edit_resend"}, - }) - m.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-1")) - m.ingestChatEvent(conversationTestAgentID, "run-1", editResendUserMessageEvent("conv-1", "edited prompt", ref)) - m.ingestChatEvent(conversationTestAgentID, "run-1", tokenEvent("conv-1", "reply")) - - sub := m.SubscribeConversationStream(conversationTestAgentID, "conv-1", 0, "") - defer sub.Cleanup() - if got := countEventType(sub.Events, StreamEventRebased); got != 1 { - t.Fatalf("rebased count = %d (types %v), want 1", got, eventTypes(sub.Events)) - } - if got := countEventType(sub.Events, "user_message"); got != 1 { - t.Fatalf("user_message count = %d (types %v), want 1 (echo swallowed)", got, eventTypes(sub.Events)) - } -} - -func userMessageEventWithRef(conversationID string, message string, ref map[string]any) *gatewayv2.ChatEvent { - payload := map[string]any{"message": message} - if ref != nil { - payload["message_ref"] = ref - } - data, _ := json.Marshal(payload) - return &gatewayv2.ChatEvent{ - Type: gatewayv2.ChatEvent_USER_MESSAGE, - ConversationId: conversationID, - Data: string(data), - } -} - -func testNewMessageRef() map[string]any { - return map[string]any{ - "segment_index": 0, - "message_index": 4, - "segment_id": "seg-1", - "message_id": "msg-9", - "role": "user", - "content_hash": "hash-9", - } -} - -// The gateway-seeded user_message cannot carry the message's persisted -// identity (ids are minted at desktop persist time). An echo whose identity -// arrives only through message_ref (no bare message_id) is forwarded exactly -// once, replay-safe, so subscribers can anchor a later edit-resend rebase. -func TestSeededUserMessageForwardsRefIdentityOnce(t *testing.T) { - m := NewManager() - m.StartChatCommand(conversationTestAgentID, "run-1", "conv-1", "", "client-1", []map[string]any{ - {"type": "user_message", "message": "prompt"}, - }) - m.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-1")) - m.ingestChatEvent(conversationTestAgentID, "run-1", userMessageEventWithRef("conv-1", "prompt", testNewMessageRef())) - // Reconnect replay redelivers the same echo: no third bubble. - m.ingestChatEvent(conversationTestAgentID, "run-1", userMessageEventWithRef("conv-1", "prompt", testNewMessageRef())) - - sub := m.SubscribeConversationStream(conversationTestAgentID, "conv-1", 0, "") - defer sub.Cleanup() - if got := countEventType(sub.Events, "user_message"); got != 2 { - t.Fatalf("user_message count = %d (types %v), want 2 (seed + one forwarded echo)", got, eventTypes(sub.Events)) - } - forwarded := sub.Events[len(sub.Events)-1] - if forwarded.Type != "user_message" { - t.Fatalf("last event = %s, want forwarded user_message", forwarded.Type) - } - ref, ok := forwarded.Payload["message_ref"].(map[string]any) - if !ok || ref["message_id"] != "msg-9" || ref["content_hash"] != "hash-9" { - t.Fatalf("forwarded message_ref = %#v", forwarded.Payload["message_ref"]) - } -} - -// Echoes without usable identity (absent, null, or blank-id message_ref — -// old desktop versions) swallow silently, exactly as before. -func TestSeededEchoWithoutIdentitySwallowed(t *testing.T) { - m := NewManager() - m.StartChatCommand(conversationTestAgentID, "run-1", "conv-1", "", "client-1", []map[string]any{ - {"type": "user_message", "message": "prompt"}, - }) - m.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-1")) - m.ingestChatEvent(conversationTestAgentID, "run-1", userMessageEventWithRef("conv-1", "prompt", nil)) - - nullRef, _ := json.Marshal(map[string]any{"message": "prompt", "message_ref": nil}) - m.ingestChatEvent(conversationTestAgentID, "run-1", &gatewayv2.ChatEvent{ - Type: gatewayv2.ChatEvent_USER_MESSAGE, ConversationId: "conv-1", Data: string(nullRef), - }) - blankRef, _ := json.Marshal(map[string]any{ - "message": "prompt", - "message_ref": map[string]any{"message_id": " ", "content_hash": "hash-9"}, - }) - m.ingestChatEvent(conversationTestAgentID, "run-1", &gatewayv2.ChatEvent{ - Type: gatewayv2.ChatEvent_USER_MESSAGE, ConversationId: "conv-1", Data: string(blankRef), - }) - - sub := m.SubscribeConversationStream(conversationTestAgentID, "conv-1", 0, "") - defer sub.Cleanup() - if got := countEventType(sub.Events, "user_message"); got != 1 { - t.Fatalf("user_message count = %d (types %v), want 1", got, eventTypes(sub.Events)) - } -} - -// A GUI-local send is never seeded, so the ref rides inside the single -// user_message itself. -func TestGUILocalUserMessageKeepsInlineRef(t *testing.T) { - m := NewManager() - m.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-1")) - m.ingestChatEvent(conversationTestAgentID, "run-1", userMessageEventWithRef("conv-1", "prompt", testNewMessageRef())) - - sub := m.SubscribeConversationStream(conversationTestAgentID, "conv-1", 0, "") - defer sub.Cleanup() - if got := countEventType(sub.Events, "user_message"); got != 1 { - t.Fatalf("user_message count = %d (types %v), want 1", got, eventTypes(sub.Events)) - } - for _, event := range sub.Events { - if event.Type != "user_message" { - continue - } - ref, ok := event.Payload["message_ref"].(map[string]any) - if !ok || ref["message_id"] != "msg-9" { - t.Fatalf("user_message message_ref = %#v, want inline ref", event.Payload["message_ref"]) - } - } -} - -// A webui edit_resend echo that carries identity is forwarded (so the ref -// binds), but its base_message_ref must not seed a second rebased — the -// truncation was already seeded from the command's accept-time payloads. -func TestWebuiEditResendIdentityEchoSeedsNoSecondRebased(t *testing.T) { - m := NewManager() - ref := testBaseMessageRef() - m.StartChatCommand(conversationTestAgentID, "run-1", "conv-1", "/workspace", "client-1", []map[string]any{ - {"type": StreamEventRebased, "base_message_ref": ref, "reason": "edit_resend"}, - {"type": "user_message", "message": "edited prompt", "base_message_ref": ref, "reason": "edit_resend"}, - }) - m.ingestChatControl(conversationTestAgentID, "run-1", startedControl("run-1", "conv-1")) - identityEcho, _ := json.Marshal(map[string]any{ - "message": "edited prompt", - "message_ref": testNewMessageRef(), - "base_message_ref": ref, - "reason": "edit_resend", - }) - m.ingestChatEvent(conversationTestAgentID, "run-1", &gatewayv2.ChatEvent{ - Type: gatewayv2.ChatEvent_USER_MESSAGE, ConversationId: "conv-1", Data: string(identityEcho), - }) - m.ingestChatEvent(conversationTestAgentID, "run-1", tokenEvent("conv-1", "reply")) - - sub := m.SubscribeConversationStream(conversationTestAgentID, "conv-1", 0, "") - defer sub.Cleanup() - if got := countEventType(sub.Events, StreamEventRebased); got != 1 { - t.Fatalf("rebased count = %d (types %v), want 1", got, eventTypes(sub.Events)) - } - if got := countEventType(sub.Events, "user_message"); got != 2 { - t.Fatalf("user_message count = %d (types %v), want 2 (seed + forwarded identity echo)", got, eventTypes(sub.Events)) - } -} diff --git a/crates/agent-gateway/internal/session/manager.go b/crates/agent-gateway/internal/session/manager.go deleted file mode 100644 index 533e22a1d..000000000 --- a/crates/agent-gateway/internal/session/manager.go +++ /dev/null @@ -1,93 +0,0 @@ -package session - -import ( - "errors" - "sync" - "time" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -var ErrAgentIDRequired = errors.New("agent_id is required") -var ErrAgentOffline = errors.New("agent offline") -var ErrChatProtocolIncompatible = errors.New("desktop chat protocol is incompatible; update LiveAgent desktop") -var ErrTunnelNotFound = errors.New("tunnel not found") -var ErrTunnelExpired = errors.New("tunnel expired") -var ErrTunnelOverLimit = errors.New("tunnel connection limit exceeded") - -const ( - chatRuntimeReadyTTL = 15 * time.Second - agentSessionHeartbeatTTL = 90 * time.Second - defaultRuntimeReadyState = "ready" -) - -type AuthSnapshot struct { - AgentID string - AgentVersion string - SessionID string -} - -type Manager struct { - registry *sessionRegistry - syncHub *syncHub - convStreams *conversationStreamStore - tunnels *tunnelRuntime - workspaceHub *workspaceActivityHub - managedProcesses *managedProcessHub - statusSubs *statusSubscriberHub -} - -type AgentSession struct { - AgentID string - AgentVersion string - SessionID string - ConnectedAt time.Time - LastPing time.Time - capabilities map[string]struct{} - - toAgent chan *OutboundEnvelope - pingCh chan *gatewayv2.GatewayEnvelope - done chan struct{} - - closeOnce sync.Once - closed bool - - streamsMu sync.Mutex - streams map[string]*agentStream -} - -type agentStream struct { - ch chan *gatewayv2.AgentEnvelope - done chan struct{} - closeOnce sync.Once -} - -type Status struct { - Online bool `json:"online"` - AgentReady bool `json:"agent_ready"` - ChatRuntimeReady bool `json:"chat_runtime_ready"` - AgentID string `json:"agent_id"` - AgentVersion string `json:"agent_version"` - SessionID string `json:"session_id,omitempty"` - ConnectedSince int64 `json:"connected_since"` - LastHeartbeat int64 `json:"last_heartbeat"` - RuntimeState string `json:"runtime_state,omitempty"` - RuntimeLastHeartbeat int64 `json:"runtime_last_heartbeat,omitempty"` - RuntimeWorkerID string `json:"runtime_worker_id,omitempty"` - RuntimeVisible bool `json:"runtime_visible,omitempty"` - RuntimeActiveRunCount uint32 `json:"runtime_active_run_count,omitempty"` -} - -func NewManager() *Manager { - m := &Manager{ - registry: newSessionRegistry(), - syncHub: newSyncHub(), - tunnels: newTunnelRuntime(), - workspaceHub: newWorkspaceActivityHub(), - managedProcesses: newManagedProcessHub(), - statusSubs: newStatusSubscriberHub(), - } - m.convStreams = newConversationStreamStore(m.IsOnline) - go m.tunnelExpirySweepLoop() - return m -} diff --git a/crates/agent-gateway/internal/session/manager_capabilities_test.go b/crates/agent-gateway/internal/session/manager_capabilities_test.go deleted file mode 100644 index 0fcd750f9..000000000 --- a/crates/agent-gateway/internal/session/manager_capabilities_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package session - -import ( - "testing" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -func TestChatRuntimeReadinessRequiresChatIngressV1(t *testing.T) { - manager := NewManager() - legacy := NewAgentSession(AuthSnapshot{AgentID: "agent-1", SessionID: "legacy-session"}) - manager.SetSession(legacy) - manager.UpdateRuntimeStatus(legacy, &gatewayv2.RuntimeStatusEvent{ - WorkerId: "worker-1", - State: "ready", - Visible: true, - }) - - legacyStatus := manager.Status("agent-1") - if legacyStatus.ChatRuntimeReady { - t.Fatal("legacy desktop without CHAT_INGRESS_V1 must not be chat-runtime ready") - } - if legacyStatus.RuntimeState != "protocol_incompatible" { - t.Fatalf("legacy runtime state = %q, want protocol_incompatible", legacyStatus.RuntimeState) - } - if manager.ChatIngressV1Ready("agent-1") { - t.Fatal("legacy desktop must not pass CHAT_INGRESS_V1 readiness") - } - - compatible := NewAgentSession(AuthSnapshot{AgentID: "agent-1", SessionID: "compatible-session"}) - compatible.SetCapabilities([]string{"ignored", gatewayv2.ChatIngressV1Capability}) - manager.SetSession(compatible) - t.Cleanup(func() { manager.ClearSession(compatible) }) - manager.UpdateRuntimeStatus(compatible, &gatewayv2.RuntimeStatusEvent{ - WorkerId: "worker-2", - State: "ready", - Visible: true, - }) - - compatibleStatus := manager.Status("agent-1") - if !compatibleStatus.ChatRuntimeReady { - t.Fatal("desktop and gateway with CHAT_INGRESS_V1 should be chat-runtime ready") - } - if compatibleStatus.RuntimeState != "ready" { - t.Fatalf("compatible runtime state = %q, want ready", compatibleStatus.RuntimeState) - } - if !manager.ChatIngressV1Ready("agent-1") { - t.Fatal("compatible desktop should pass CHAT_INGRESS_V1 readiness") - } -} diff --git a/crates/agent-gateway/internal/session/manager_chat_queue.go b/crates/agent-gateway/internal/session/manager_chat_queue.go deleted file mode 100644 index b07a76f86..000000000 --- a/crates/agent-gateway/internal/session/manager_chat_queue.go +++ /dev/null @@ -1,158 +0,0 @@ -package session - -import ( - "sort" - "strings" - "time" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -type chatQueueSnapshotRecord struct { - event *gatewayv2.ChatQueueEvent - sessionEpoch uint64 -} - -// SubscribeChatQueueEvents 订阅提示队列事件并回放全部 Agent 的现存快照 -// (快照按 Agent 存于各自 entry,回放帧携带来源标签)。 -func (m *Manager) SubscribeChatQueueEvents() (<-chan Tagged[*gatewayv2.ChatQueueEvent], func()) { - replay := make([]Tagged[*gatewayv2.ChatQueueEvent], 0) - for _, agentID := range m.knownAgentIDs() { - entry := m.entryFor(agentID) - if entry == nil { - continue - } - entry.chatQueueSnapshotsMu.Lock() - conversationIDs := make([]string, 0, len(entry.chatQueueSnapshots)) - for conversationID := range entry.chatQueueSnapshots { - conversationIDs = append(conversationIDs, conversationID) - } - sort.Strings(conversationIDs) - for _, conversationID := range conversationIDs { - replay = append(replay, Tagged[*gatewayv2.ChatQueueEvent]{ - AgentID: agentID, - Event: cloneChatQueueEvent(entry.chatQueueSnapshots[conversationID].event), - }) - } - entry.chatQueueSnapshotsMu.Unlock() - } - - m.syncHub.chatQueueMu.Lock() - ch := make(chan Tagged[*gatewayv2.ChatQueueEvent], 128+len(replay)) - subID := m.syncHub.nextChatQueueSubID - m.syncHub.nextChatQueueSubID += 1 - m.syncHub.chatQueueSubscribers[subID] = ch - m.syncHub.chatQueueMu.Unlock() - - for _, event := range replay { - ch <- event - } - - cleanup := func() { - m.syncHub.chatQueueMu.Lock() - delete(m.syncHub.chatQueueSubscribers, subID) - m.syncHub.chatQueueMu.Unlock() - } - - return ch, cleanup -} - -// knownAgentIDs 返回全部登记项 id(含离线),按字典序。 -func (m *Manager) knownAgentIDs() []string { - m.registry.mu.RLock() - ids := make([]string, 0, len(m.registry.agents)) - for id := range m.registry.agents { - ids = append(ids, id) - } - m.registry.mu.RUnlock() - sort.Strings(ids) - return ids -} - -func (m *Manager) ChatQueueSnapshot(agentID, conversationID string) (*gatewayv2.ChatQueueEvent, bool) { - key := strings.TrimSpace(conversationID) - if key == "" { - return nil, false - } - entry := m.entryFor(agentID) - if entry == nil { - return nil, false - } - - entry.chatQueueSnapshotsMu.Lock() - defer entry.chatQueueSnapshotsMu.Unlock() - - record, ok := entry.chatQueueSnapshots[key] - if !ok { - return nil, false - } - return cloneChatQueueEvent(record.event), true -} - -func (m *Manager) broadcastChatQueueEvent(agentID string, event *gatewayv2.ChatQueueEvent) { - if event == nil { - return - } - normalized := cloneChatQueueEvent(event) - conversationID := strings.TrimSpace(normalized.GetConversationId()) - if conversationID != "" { - normalized.ConversationId = conversationID - } - entry := m.entryOrCreate(agentID) - if entry == nil { - return - } - sessionEpoch := m.sessionEpochOf(agentID) - - if conversationID != "" { - entry.chatQueueSnapshotsMu.Lock() - if existing := entry.chatQueueSnapshots[conversationID]; existing.event != nil && existing.sessionEpoch == sessionEpoch { - existingRevision := existing.event.GetRevision() - incomingRevision := normalized.GetRevision() - if existingRevision > 0 && (incomingRevision == 0 || incomingRevision < existingRevision) { - entry.chatQueueSnapshotsMu.Unlock() - return - } - } - entry.chatQueueSnapshots[conversationID] = chatQueueSnapshotRecord{ - event: cloneChatQueueEvent(normalized), - sessionEpoch: sessionEpoch, - } - entry.chatQueueSnapshotsMu.Unlock() - } - - m.syncHub.chatQueueMu.Lock() - subscribers := make([]chan Tagged[*gatewayv2.ChatQueueEvent], 0, len(m.syncHub.chatQueueSubscribers)) - for _, ch := range m.syncHub.chatQueueSubscribers { - subscribers = append(subscribers, ch) - } - m.syncHub.chatQueueMu.Unlock() - - for _, ch := range subscribers { - select { - case ch <- Tagged[*gatewayv2.ChatQueueEvent]{AgentID: agentID, Event: cloneChatQueueEvent(normalized)}: - case <-time.After(50 * time.Millisecond): - } - } -} - -// sessionEpochOf 返回 agent_id 当前会话的 epoch;离线为 0。 -func (m *Manager) sessionEpochOf(agentID string) uint64 { - m.registry.mu.RLock() - defer m.registry.mu.RUnlock() - if entry := m.registry.agents[normalizeAgentKey(agentID)]; entry != nil && entry.session != nil { - return entry.sessionEpoch - } - return 0 -} - -func cloneChatQueueEvent(event *gatewayv2.ChatQueueEvent) *gatewayv2.ChatQueueEvent { - if event == nil { - return nil - } - return &gatewayv2.ChatQueueEvent{ - ConversationId: event.GetConversationId(), - SnapshotJson: event.GetSnapshotJson(), - Revision: event.GetRevision(), - } -} diff --git a/crates/agent-gateway/internal/session/manager_dispatch.go b/crates/agent-gateway/internal/session/manager_dispatch.go deleted file mode 100644 index 69c959e66..000000000 --- a/crates/agent-gateway/internal/session/manager_dispatch.go +++ /dev/null @@ -1,175 +0,0 @@ -package session - -import ( - "strings" - "time" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -// DispatchFromAgent 是显式 Agent 测试/嵌入入口;生产 WebSocket 链路使用 -// DispatchFromAgentForSession,把身份绑定到已认证连接。 -func (m *Manager) DispatchFromAgent(agentID string, env *gatewayv2.AgentEnvelope) { - session, err := m.resolveSession(agentID) - if err != nil { - return - } - m.dispatchFromAgent(session, env) -} - -func (m *Manager) DispatchFromAgentForSession(session *AgentSession, env *gatewayv2.AgentEnvelope) { - m.dispatchFromAgent(session, env) -} - -func (m *Manager) dispatchFromAgent(expected *AgentSession, env *gatewayv2.AgentEnvelope) { - // 严格校验 expected 仍是所属登记项的在线会话;被顶替连接的迟到事件直接丢弃。 - var session *AgentSession - m.registry.mu.RLock() - if entry := m.registry.entryForSessionLocked(expected); entry != nil { - session = entry.session - } - m.registry.mu.RUnlock() - if session == nil { - return - } - // 所有入站事件按已认证会话的 agent_id 打标入账——这是跨 Agent 隔离的唯一 - // 事实源:Agent 无法伪造他人身份的事件(身份来自握手,不来自载荷)。 - agentID := session.AgentID - reliableChatIngress := session.SupportsCapability(gatewayv2.ChatIngressV1Capability) - - if batch := env.GetChatIngressBatch(); batch != nil { - m.touchRuntimeActivity(session) - m.queueChatIngressAck(session, env.GetRequestId(), m.ingestChatIngressBatch(agentID, batch)) - return - } - if resume := env.GetChatIngressResume(); resume != nil { - m.touchRuntimeActivity(session) - for _, ack := range m.ingestChatIngressResume(agentID, resume) { - m.queueChatIngressAck(session, env.GetRequestId(), ack) - } - return - } - if fragment := env.GetChatIngressFragment(); fragment != nil { - m.touchRuntimeActivity(session) - m.queueChatIngressAck(session, env.GetRequestId(), m.ingestChatIngressFragment(agentID, fragment)) - return - } - - if runtimeStatus := env.GetRuntimeStatus(); runtimeStatus != nil { - m.UpdateRuntimeStatus(session, runtimeStatus) - m.convStreams.onRuntimeStatus(agentID, runtimeStatus, time.Now()) - return - } - - if env.GetChatEvent() != nil || env.GetChatControl() != nil || env.GetChatRuntimeSnapshot() != nil { - m.touchRuntimeActivity(session) - } - - if runtimeSnapshot := env.GetChatRuntimeSnapshot(); runtimeSnapshot != nil { - if reliableChatIngress { - return - } - m.ingestRuntimeSnapshot(agentID, runtimeSnapshot) - return - } - - if chatEvent := env.GetChatEvent(); chatEvent != nil { - if reliableChatIngress { - return - } - m.ingestChatEvent(agentID, env.GetRequestId(), chatEvent) - } - - if chatControl := env.GetChatControl(); chatControl != nil { - controlType := strings.TrimSpace(chatControl.GetType()) - if controlType == "" { - controlType = strings.TrimSpace(chatControl.GetState()) - } - if reliableChatIngress && (controlType == "completed" || controlType == "failed" || controlType == "cancelled") { - return - } - m.ingestChatControl(agentID, env.GetRequestId(), chatControl) - } - - if historySync := env.GetHistorySync(); historySync != nil { - // Agent-sent running/idle activity is dropped: conversation activity - // is derived from run lifecycle transitions in the stream store, which - // always carry run ids. - switch strings.TrimSpace(historySync.GetKind()) { - case "running", "idle": - return - } - m.broadcastHistorySync(agentID, historySync) - return - } - - if settingsSync := env.GetSettingsSync(); settingsSync != nil { - m.broadcastSettingsSync(agentID, settingsSync) - return - } - - if terminalEvent := env.GetTerminalEvent(); terminalEvent != nil { - m.broadcastTerminalEvent(agentID, terminalEvent) - return - } - - if sftpEvent := env.GetSftpEvent(); sftpEvent != nil { - m.broadcastSftpEvent(agentID, sftpEvent) - return - } - - if chatQueueEvent := env.GetChatQueueEvent(); chatQueueEvent != nil { - m.broadcastChatQueueEvent(agentID, chatQueueEvent) - return - } - - if tunnelFrame := env.GetTunnelFrame(); tunnelFrame != nil { - m.dispatchTunnelFrame(agentID, tunnelFrame) - return - } - - if workspaceActivity := env.GetWorkspaceActivity(); workspaceActivity != nil { - m.broadcastWorkspaceActivity(agentID, workspaceActivity) - return - } - - if managedProcessSnapshot := env.GetManagedProcessSnapshot(); managedProcessSnapshot != nil { - m.broadcastManagedProcessSnapshot(agentID, managedProcessSnapshot) - return - } - - // Desired-state and probe payloads fan out broadcasts and relay probes; - // run them off the agent stream read loop so tunnel frames keep flowing. - if tunnelDesired := env.GetTunnelDesired(); tunnelDesired != nil { - go m.ApplyDesiredState(agentID, tunnelDesired) - return - } - - if tunnelProbeReport := env.GetTunnelProbeReport(); tunnelProbeReport != nil { - go m.ApplyProbeReport(agentID, tunnelProbeReport) - return - } - - // TunnelMutationResult and ManagedProcessResponse intentionally fall - // through to session.dispatch: they answer gateway-issued requests and - // correlate by request id. - session.dispatch(env) -} - -func (m *Manager) queueChatIngressAck(session *AgentSession, requestID string, ack *gatewayv2.ChatIngressAck) { - if session == nil || ack == nil { - return - } - queued, err := session.TrySendToAgent(&gatewayv2.GatewayEnvelope{ - RequestId: requestID, - Timestamp: time.Now().Unix(), - Payload: &gatewayv2.GatewayEnvelope_ChatIngressAck{ - ChatIngressAck: ack, - }, - }) - if err != nil || !queued { - // An ACK that cannot be queued must force a reconnect. Silently losing - // it would leave the producer unsure whether the record committed. - session.Close() - } -} diff --git a/crates/agent-gateway/internal/session/manager_history_sync.go b/crates/agent-gateway/internal/session/manager_history_sync.go deleted file mode 100644 index 66a285b90..000000000 --- a/crates/agent-gateway/internal/session/manager_history_sync.go +++ /dev/null @@ -1,46 +0,0 @@ -package session - -import ( - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -func (m *Manager) SubscribeHistorySync() (<-chan Tagged[*gatewayv2.HistorySyncEvent], func()) { - ch := make(chan Tagged[*gatewayv2.HistorySyncEvent], 128) - - m.syncHub.historyMu.Lock() - subID := m.syncHub.nextHistorySubID - m.syncHub.nextHistorySubID += 1 - m.syncHub.historySubscribers[subID] = ch - m.syncHub.historyMu.Unlock() - - cleanup := func() { - m.syncHub.historyMu.Lock() - // Do not close the channel here: broadcastHistorySync sends after - // copying subscribers, so closing can race with an in-flight send. - delete(m.syncHub.historySubscribers, subID) - m.syncHub.historyMu.Unlock() - } - - return ch, cleanup -} - -func (m *Manager) broadcastHistorySync(agentID string, event *gatewayv2.HistorySyncEvent) { - if event == nil { - return - } - - m.syncHub.historyMu.Lock() - subscribers := make([]chan Tagged[*gatewayv2.HistorySyncEvent], 0, len(m.syncHub.historySubscribers)) - for _, ch := range m.syncHub.historySubscribers { - subscribers = append(subscribers, ch) - } - m.syncHub.historyMu.Unlock() - - tagged := Tagged[*gatewayv2.HistorySyncEvent]{AgentID: agentID, Event: event} - for _, ch := range subscribers { - select { - case ch <- tagged: - default: - } - } -} diff --git a/crates/agent-gateway/internal/session/manager_isolation_test.go b/crates/agent-gateway/internal/session/manager_isolation_test.go deleted file mode 100644 index 944be1125..000000000 --- a/crates/agent-gateway/internal/session/manager_isolation_test.go +++ /dev/null @@ -1,321 +0,0 @@ -package session - -import ( - "testing" - "time" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -// dispatchFor 以 agent 的已认证会话身份入账一条信封(模拟协议层的 -// DispatchFromAgentForSession 调用路径)。 -func dispatchFor(m *Manager, sess *AgentSession, env *gatewayv2.AgentEnvelope) { - m.DispatchFromAgentForSession(sess, env) -} - -func TestBroadcastEventsCarrySourceAgentTag(t *testing.T) { - m := NewManager() - a := newTestSession(m, "agent-a", "session-a") - m.SetSession(a) - b := newTestSession(m, "agent-b", "session-b") - m.SetSession(b) - t.Cleanup(func() { m.ClearSession(a); m.ClearSession(b) }) - - events, cleanup := m.SubscribeHistorySync() - defer cleanup() - - // A 与 B 各发一条历史事件:订阅端必须能凭标签区分来源。 - dispatchFor(m, a, &gatewayv2.AgentEnvelope{ - Payload: &gatewayv2.AgentEnvelope_HistorySync{ - HistorySync: &gatewayv2.HistorySyncEvent{Kind: "upsert", ConversationId: "conv-a"}, - }, - }) - dispatchFor(m, b, &gatewayv2.AgentEnvelope{ - Payload: &gatewayv2.AgentEnvelope_HistorySync{ - HistorySync: &gatewayv2.HistorySyncEvent{Kind: "upsert", ConversationId: "conv-b"}, - }, - }) - - for _, want := range []struct{ agentID, convID string }{ - {"agent-a", "conv-a"}, - {"agent-b", "conv-b"}, - } { - select { - case tagged := <-events: - if tagged.AgentID != want.agentID || tagged.Event.GetConversationId() != want.convID { - t.Fatalf("tagged event = %s/%s, want %s/%s", - tagged.AgentID, tagged.Event.GetConversationId(), want.agentID, want.convID) - } - case <-time.After(time.Second): - t.Fatalf("timed out waiting for %s event", want.agentID) - } - } -} - -func TestDisplacedSessionEventsAreDropped(t *testing.T) { - m := NewManager() - old := newTestSession(m, "agent-a", "session-old") - m.SetSession(old) - replacement := newTestSession(m, "agent-a", "session-new") - m.SetSession(replacement) - t.Cleanup(func() { m.ClearSession(replacement) }) - - events, cleanup := m.SubscribeHistorySync() - defer cleanup() - - // 被顶替连接的迟到事件必须被丢弃,不得冒充新会话入账。 - dispatchFor(m, old, &gatewayv2.AgentEnvelope{ - Payload: &gatewayv2.AgentEnvelope_HistorySync{ - HistorySync: &gatewayv2.HistorySyncEvent{Kind: "upsert", ConversationId: "stale"}, - }, - }) - select { - case tagged := <-events: - t.Fatalf("stale session event was broadcast: %#v", tagged) - case <-time.After(50 * time.Millisecond): - } -} - -func TestTunnelFrameFromWrongAgentIsRejected(t *testing.T) { - m := NewManager() - a := newTestSession(m, "agent-a", "session-a") - m.SetSession(a) - b := newTestSession(m, "agent-b", "session-b") - m.SetSession(b) - t.Cleanup(func() { m.ClearSession(a); m.ClearSession(b) }) - - // A 声明一条隧道并有访问者流。 - m.ApplyDesiredState("agent-a", &gatewayv2.TunnelDesiredState{ - Tunnels: []*gatewayv2.TunnelSpec{{Id: "tun-a", TargetUrl: "http://localhost:3000"}}, - }) - slug := m.TunnelStateSnapshot("agent-a").GetTunnels()[0].GetSlug() - lease, err := m.AcquireTunnel(slug, "s-1") - if err != nil { - t.Fatalf("acquire: %v", err) - } - defer lease.Release() - if lease.AgentID() != "agent-a" { - t.Fatalf("lease agent = %q, want agent-a", lease.AgentID()) - } - - // B 伪造 A 的 stream_id 注入数据:必须被丢弃。 - m.dispatchTunnelFrame("agent-b", &gatewayv2.TunnelFrame{ - StreamId: "s-1", - Kind: gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_RESPONSE_BODY, - Body: []byte("forged"), - }) - select { - case frame := <-lease.Frames(): - t.Fatalf("forged cross-agent frame was delivered: %#v", frame) - case <-time.After(50 * time.Millisecond): - } - - // A 自己的帧正常送达。 - m.dispatchTunnelFrame("agent-a", &gatewayv2.TunnelFrame{ - StreamId: "s-1", - Kind: gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_RESPONSE_BODY, - Body: []byte("legit"), - }) - select { - case frame := <-lease.Frames(): - if string(frame.GetBody()) != "legit" { - t.Fatalf("frame data = %q", frame.GetBody()) - } - case <-time.After(time.Second): - t.Fatal("legitimate frame was not delivered") - } -} - -func TestSettingsGatesAreScopedPerAgent(t *testing.T) { - m := NewManager() - a := newTestSession(m, "agent-a", "session-a") - m.SetSession(a) - b := newTestSession(m, "agent-b", "session-b") - m.SetSession(b) - t.Cleanup(func() { m.ClearSession(a); m.ClearSession(b) }) - - m.ApplySettingsJSON("agent-a", `{"remote":{"enableWebTerminal":true}}`) - m.ApplySettingsJSON("agent-b", `{"remote":{"enableWebTerminal":false}}`) - - if !m.WebTerminalEnabled("agent-a") { - t.Fatal("agent-a web terminal should be enabled") - } - if m.WebTerminalEnabled("agent-b") { - t.Fatal("agent-b web terminal must stay disabled — gates must not leak across agents") - } -} - -func TestTerminalSnapshotsAreScopedPerAgent(t *testing.T) { - m := NewManager() - a := newTestSession(m, "agent-a", "session-a") - m.SetSession(a) - b := newTestSession(m, "agent-b", "session-b") - m.SetSession(b) - t.Cleanup(func() { m.ClearSession(a); m.ClearSession(b) }) - - m.ApplyTerminalResponseSnapshot("agent-a", "create", "", &gatewayv2.TerminalResponse{ - Action: "create", - Session: &gatewayv2.TerminalSession{Id: "term-a", Running: true}, - }) - - if got := len(m.TerminalSessionSnapshot("agent-a", "")); got != 1 { - t.Fatalf("agent-a terminal snapshot = %d entries, want 1", got) - } - if got := len(m.TerminalSessionSnapshot("agent-b", "")); got != 0 { - t.Fatalf("agent-b terminal snapshot = %d entries, want 0 (isolation)", got) - } - - // A 的会话更替只清 A 的快照。 - m.SetSession(newTestSession(m, "agent-a", "session-a2")) - if got := len(m.TerminalSessionSnapshot("agent-a", "")); got != 0 { - t.Fatalf("agent-a snapshot after displacement = %d, want 0", got) - } -} - -func TestConversationEpochUsesSourceAgentSession(t *testing.T) { - m := NewManager() - a := newTestSession(m, "agent-a", "session-a") - m.SetSession(a) - b1 := newTestSession(m, "agent-b", "session-b1") - m.SetSession(b1) - b2 := newTestSession(m, "agent-b", "session-b2") - m.SetSession(b2) - t.Cleanup(func() { m.ClearSession(a); m.ClearSession(b2) }) - - wantEpoch := m.sessionEpochOf("agent-b") - if wantEpoch < 2 { - t.Fatalf("agent-b session epoch = %d, want replacement epoch", wantEpoch) - } - dispatchFor(m, b2, &gatewayv2.AgentEnvelope{ - RequestId: "run-b", - Payload: &gatewayv2.AgentEnvelope_ChatControl{ - ChatControl: startedControl("run-b", "conv-b"), - }, - }) - - m.convStreams.mu.Lock() - stream := m.convStreams.streams[conversationStreamKey("agent-b", "conv-b")] - gotEpoch := uint64(0) - if stream != nil { - gotEpoch = stream.agentEpoch - } - m.convStreams.mu.Unlock() - if gotEpoch != wantEpoch { - t.Fatalf("conversation agent epoch = %d, want source agent epoch %d", gotEpoch, wantEpoch) - } -} - -func TestRuntimeStatusReconcilesOnlySourceAgentRuns(t *testing.T) { - m := NewManager() - a := newTestSession(m, "agent-a", "session-a") - m.SetSession(a) - b := newTestSession(m, "agent-b", "session-b") - m.SetSession(b) - t.Cleanup(func() { m.ClearSession(a); m.ClearSession(b) }) - - dispatchFor(m, a, &gatewayv2.AgentEnvelope{ - RequestId: "run-a", - Payload: &gatewayv2.AgentEnvelope_ChatControl{ - ChatControl: startedControl("run-a", "conv-a"), - }, - }) - dispatchFor(m, b, &gatewayv2.AgentEnvelope{ - RequestId: "run-b", - Payload: &gatewayv2.AgentEnvelope_ChatControl{ - ChatControl: startedControl("run-b", "conv-b"), - }, - }) - - m.convStreams.onRuntimeStatus("agent-a", runsReport(nil, nil), time.Now().Add(20*time.Second)) - activities := m.ActiveConversationActivities() - if len(activities) != 1 || activities[0].AgentID != "agent-b" || activities[0].RunID != "run-b" { - t.Fatalf("activities after agent-a empty report = %#v, want only agent-b/run-b", activities) - } -} - -func TestConversationStreamsAreScopedByAgent(t *testing.T) { - m := NewManager() - subA := m.SubscribeConversationStream("agent-a", "conv-shared", 0, "") - defer subA.Cleanup() - subB := m.SubscribeConversationStream("agent-b", "conv-shared", 0, "") - defer subB.Cleanup() - - m.ingestChatControl("agent-a", "run-shared", startedControl("run-shared", "conv-shared")) - m.ingestChatEvent("agent-a", "run-shared", tokenEvent("conv-shared", "from-a")) - eventsA := drainEvents(t, subA.EventCh, 2) - if got := eventsA[1].Payload["text"]; got != "from-a" { - t.Fatalf("agent-a token = %#v, want from-a", got) - } - select { - case event := <-subB.EventCh: - t.Fatalf("agent-b received agent-a event: %#v", event) - case <-time.After(50 * time.Millisecond): - } - - m.ingestChatControl("agent-b", "run-shared", startedControl("run-shared", "conv-shared")) - m.ingestChatEvent("agent-b", "run-shared", tokenEvent("conv-shared", "from-b")) - eventsB := drainEvents(t, subB.EventCh, 2) - if got := eventsB[1].Payload["text"]; got != "from-b" { - t.Fatalf("agent-b token = %#v, want from-b", got) - } - select { - case event := <-subA.EventCh: - t.Fatalf("agent-a received agent-b event: %#v", event) - case <-time.After(50 * time.Millisecond): - } - - replayA := m.SubscribeConversationStream("agent-a", "conv-shared", 0, "") - defer replayA.Cleanup() - replayB := m.SubscribeConversationStream("agent-b", "conv-shared", 0, "") - defer replayB.Cleanup() - if len(replayA.Events) != 2 || replayA.Events[1].Payload["text"] != "from-a" { - t.Fatalf("agent-a replay leaked or lost events: %#v", replayA.Events) - } - if len(replayB.Events) != 2 || replayB.Events[1].Payload["text"] != "from-b" { - t.Fatalf("agent-b replay leaked or lost events: %#v", replayB.Events) - } -} - -func TestConversationCancelAndWatchdogAreScopedByAgent(t *testing.T) { - m := NewManager() - m.ingestChatControl("agent-a", "run-shared", startedControl("run-shared", "conv-shared")) - m.ingestChatControl("agent-b", "run-shared", startedControl("run-shared", "conv-shared")) - - runID, ok := m.MarkConversationCancelling("agent-a", "conv-shared", "run-shared") - if !ok || runID != "run-shared" { - t.Fatalf("agent-a cancel mark = %q/%v", runID, ok) - } - subA := m.SubscribeConversationStream("agent-a", "conv-shared", 0, "") - defer subA.Cleanup() - subB := m.SubscribeConversationStream("agent-b", "conv-shared", 0, "") - defer subB.Cleanup() - if subA.Activity == nil || subA.Activity.State != RunActivityCancelling { - t.Fatalf("agent-a activity = %#v, want cancelling", subA.Activity) - } - if subB.Activity == nil || subB.Activity.State != RunActivityRunning { - t.Fatalf("agent-b activity = %#v, want running", subB.Activity) - } - - m.ForceFinishRun("agent-a", "run-shared", "cancelled", "cancel_timeout", "watchdog") - activities := m.ActiveConversationActivities() - if len(activities) != 1 || activities[0].AgentID != "agent-b" || activities[0].RunID != "run-shared" { - t.Fatalf("cross-agent watchdog affected wrong run: %#v", activities) - } -} - -func TestChatCommandDedupeIsScopedByAgent(t *testing.T) { - m := NewManager() - startA := m.StartChatCommand("agent-a", "run-shared", "conv-shared", "", "client-shared", nil) - startB := m.StartChatCommand("agent-b", "run-shared", "conv-shared", "", "client-shared", nil) - if startA.Deduped || startB.Deduped { - t.Fatalf("cross-agent client_request_id was deduped: a=%#v b=%#v", startA, startB) - } - lookupA, okA := m.LookupChatCommand("agent-a", "client-shared") - lookupB, okB := m.LookupChatCommand("agent-b", "client-shared") - if !okA || lookupA.AgentID != "agent-a" || lookupA.RunID != "run-shared" { - t.Fatalf("agent-a lookup = %#v/%v", lookupA, okA) - } - if !okB || lookupB.AgentID != "agent-b" || lookupB.RunID != "run-shared" { - t.Fatalf("agent-b lookup = %#v/%v", lookupB, okB) - } -} diff --git a/crates/agent-gateway/internal/session/manager_managed_process.go b/crates/agent-gateway/internal/session/manager_managed_process.go deleted file mode 100644 index 0a61f1e8e..000000000 --- a/crates/agent-gateway/internal/session/manager_managed_process.go +++ /dev/null @@ -1,101 +0,0 @@ -package session - -import ( - "sync" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -// managedProcessHub caches the latest ManagedProcess snapshot published by -// each agent and fans it out to websocket subscribers. Delivery is -// latest-wins and non-blocking: a congested subscriber just skips ahead to -// the next snapshot. -type managedProcessHub struct { - mu sync.Mutex - latest map[string]*gatewayv2.ManagedProcessSnapshot - subscribers map[uint64]chan Tagged[*gatewayv2.ManagedProcessSnapshot] - nextSubID uint64 -} - -func newManagedProcessHub() *managedProcessHub { - return &managedProcessHub{ - latest: make(map[string]*gatewayv2.ManagedProcessSnapshot), - subscribers: make(map[uint64]chan Tagged[*gatewayv2.ManagedProcessSnapshot]), - } -} - -// ManagedProcessSnapshotCached returns the last snapshot seen from a named agentID -// (nil before the first publish), so webui clients can render the latest known state -// even while that Agent is offline. -func (m *Manager) ManagedProcessSnapshotCached(agentID string) *gatewayv2.ManagedProcessSnapshot { - agentID = normalizeAgentKey(agentID) - if agentID == "" { - return nil - } - m.managedProcesses.mu.Lock() - defer m.managedProcesses.mu.Unlock() - return m.managedProcesses.latest[agentID] -} - -func (m *Manager) SubscribeManagedProcessState() (<-chan Tagged[*gatewayv2.ManagedProcessSnapshot], func()) { - hub := m.managedProcesses - ch := make(chan Tagged[*gatewayv2.ManagedProcessSnapshot], 16) - - hub.mu.Lock() - subID := hub.nextSubID - hub.nextSubID += 1 - hub.subscribers[subID] = ch - hub.mu.Unlock() - - cleanup := func() { - hub.mu.Lock() - // Do not close the channel: the broadcast sends after copying the - // subscriber list, so closing can race with an in-flight send. - delete(hub.subscribers, subID) - hub.mu.Unlock() - } - return ch, cleanup -} - -func (m *Manager) broadcastManagedProcessSnapshot(agentID string, snapshot *gatewayv2.ManagedProcessSnapshot) { - if snapshot == nil { - return - } - key := normalizeAgentKey(agentID) - hub := m.managedProcesses - hub.mu.Lock() - // Agent-side publishes are spawned per change and can arrive reordered; - // revisions are agent-stamped and restart-safe, so drop strictly older - // snapshots (equal ones still flow for agent-online re-stamps). - if latest := hub.latest[key]; latest != nil && snapshot.GetRevision() < latest.GetRevision() { - hub.mu.Unlock() - return - } - hub.latest[key] = snapshot - subscribers := make([]chan Tagged[*gatewayv2.ManagedProcessSnapshot], 0, len(hub.subscribers)) - for _, ch := range hub.subscribers { - subscribers = append(subscribers, ch) - } - hub.mu.Unlock() - - tagged := Tagged[*gatewayv2.ManagedProcessSnapshot]{AgentID: agentID, Event: snapshot} - for _, ch := range subscribers { - select { - case ch <- tagged: - default: - } - } -} - -// rebroadcastManagedProcessState replays agentID's cached snapshot so -// subscribers re-render with the current agent-online flag (stamped at write -// time). -func (m *Manager) rebroadcastManagedProcessState(agentID string) { - m.managedProcesses.mu.Lock() - latest := m.managedProcesses.latest[normalizeAgentKey(agentID)] - m.managedProcesses.mu.Unlock() - if latest == nil { - return - } - m.broadcastManagedProcessSnapshot(agentID, latest) -} diff --git a/crates/agent-gateway/internal/session/manager_multiagent_test.go b/crates/agent-gateway/internal/session/manager_multiagent_test.go deleted file mode 100644 index e330fc14f..000000000 --- a/crates/agent-gateway/internal/session/manager_multiagent_test.go +++ /dev/null @@ -1,320 +0,0 @@ -package session - -import ( - "context" - "errors" - "testing" - "time" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -func newTestSession(m *Manager, agentID, sessionID string) *AgentSession { - auth := m.RecordAuthentication(agentID, "v-test", sessionID) - return NewAgentSession(auth) -} - -func TestMultiAgentSessionsCoexist(t *testing.T) { - m := NewManager() - a := newTestSession(m, "agent-a", "session-a") - m.SetSession(a) - b := newTestSession(m, "agent-b", "session-b") - m.SetSession(b) - t.Cleanup(func() { m.ClearSession(a); m.ClearSession(b) }) - - if !m.IsOnline("agent-a") || !m.IsOnline("agent-b") { - t.Fatalf("both agents should be online: a=%v b=%v", m.IsOnline("agent-a"), m.IsOnline("agent-b")) - } - if ids := m.ConnectedAgentIDs(); len(ids) != 2 || ids[0] != "agent-a" || ids[1] != "agent-b" { - t.Fatalf("connected ids = %v, want [agent-a agent-b]", ids) - } - - // 定向发送只命中目标 Agent 的出站队列。 - env := &gatewayv2.GatewayEnvelope{RequestId: "to-b"} - go func() { _ = m.SendToAgentContext(context.Background(), "agent-b", env) }() - select { - case outbound := <-b.Outbound(): - if outbound.GetRequestId() != "to-b" { - t.Fatalf("agent-b outbound = %q", outbound.GetRequestId()) - } - outbound.Ack(nil) - case <-a.Outbound(): - t.Fatal("request targeted at agent-b was delivered to agent-a") - case <-time.After(time.Second): - t.Fatal("timed out waiting for targeted delivery") - } -} - -func TestSetSessionDisplacesOnlySameAgentID(t *testing.T) { - m := NewManager() - a := newTestSession(m, "agent-a", "session-a1") - m.SetSession(a) - b := newTestSession(m, "agent-b", "session-b1") - m.SetSession(b) - - // 同 id 重连:顶掉 agent-a 的旧会话,agent-b 不受影响。 - a2 := newTestSession(m, "agent-a", "session-a2") - m.SetSession(a2) - t.Cleanup(func() { m.ClearSession(a2); m.ClearSession(b) }) - - select { - case <-a.Done(): - case <-time.After(time.Second): - t.Fatal("displaced agent-a session was not closed") - } - select { - case <-b.Done(): - t.Fatal("agent-b session must survive agent-a displacement") - default: - } - if status := m.Status("agent-a"); !status.Online || status.SessionID != "session-a2" { - t.Fatalf("agent-a status = %#v, want online session-a2", status) - } -} - -func TestHeartbeatEvictionIsScopedToSession(t *testing.T) { - m := NewManager() - a := newTestSession(m, "agent-a", "session-a") - m.SetSession(a) - b := newTestSession(m, "agent-b", "session-b") - b.LastPing = time.Now().Add(-time.Hour) - m.SetSession(b) - t.Cleanup(func() { m.ClearSession(a) }) - - if !m.ClearSessionIfHeartbeatStale(b, time.Minute) { - t.Fatal("stale agent-b session should be evicted") - } - if m.ClearSessionIfHeartbeatStale(a, time.Minute) { - t.Fatal("fresh agent-a session must not be evicted") - } - if !m.IsOnline("agent-a") || m.IsOnline("agent-b") { - t.Fatalf("after eviction: a=%v b=%v, want a online, b offline", m.IsOnline("agent-a"), m.IsOnline("agent-b")) - } -} - -func TestEmptyAgentIDIsAlwaysRejected(t *testing.T) { - m := NewManager() - assertRequired := func(stage string) { - t.Helper() - if err := m.SendToAgentContext(context.Background(), "", &gatewayv2.GatewayEnvelope{}); !errors.Is(err, ErrAgentIDRequired) { - t.Fatalf("%s: err = %v, want ErrAgentIDRequired", stage, err) - } - } - - assertRequired("no agents") - a := newTestSession(m, "agent-a", "session-a") - m.SetSession(a) - assertRequired("one agent") - b := newTestSession(m, "agent-b", "session-b") - m.SetSession(b) - t.Cleanup(func() { m.ClearSession(a); m.ClearSession(b) }) - assertRequired("two agents") - - go func() { - outbound := <-b.Outbound() - outbound.Ack(nil) - }() - if err := m.SendToAgentContext(context.Background(), "agent-b", &gatewayv2.GatewayEnvelope{RequestId: "explicit"}); err != nil { - t.Fatalf("explicit target: err = %v", err) - } -} - -func TestGlobalOnlineCheckDoesNotActAsAgentAddressing(t *testing.T) { - m := NewManager() - a := newTestSession(m, "agent-a", "session-a") - m.SetSession(a) - - if !m.AnyAgentOnline() { - t.Fatal("AnyAgentOnline should report the connected Agent") - } - if m.IsOnline("") { - t.Fatal("IsOnline with an empty id must not act as a global query") - } - if status := m.Status(""); status != (Status{}) { - t.Fatalf("empty-id status = %#v, want zero value", status) - } - - m.ClearSession(a) - if m.AnyAgentOnline() { - t.Fatal("AnyAgentOnline should be false after disconnect") - } -} - -func TestDisconnectAgentKicksLiveSession(t *testing.T) { - m := NewManager() - a := newTestSession(m, "agent-a", "session-a") - m.SetSession(a) - - if !m.DisconnectAgent("agent-a") { - t.Fatal("DisconnectAgent should report a kicked session") - } - select { - case <-a.Done(): - case <-time.After(time.Second): - t.Fatal("disconnected session was not closed") - } - if m.IsOnline("agent-a") { - t.Fatal("agent-a should be offline after DisconnectAgent") - } - if m.DisconnectAgent("agent-a") { - t.Fatal("second DisconnectAgent should be a no-op") - } -} - -func TestSetAuthenticatedSessionRejectsStaleCredentialWithoutDirectoryEntry(t *testing.T) { - m := NewManager() - sess := NewAgentSession(AuthSnapshot{ - AgentID: "agent-a", - AgentVersion: "v-test", - SessionID: "stale-session", - }) - - if m.SetAuthenticatedSessionIfCurrent(sess, func() bool { return false }) { - t.Fatal("stale authentication must not register a session") - } - select { - case <-sess.Done(): - default: - t.Fatal("rejected session must be closed") - } - if m.IsOnline("agent-a") || len(m.AgentStatuses()) != 0 { - t.Fatalf("stale authentication left a directory entry: %#v", m.AgentStatuses()) - } -} - -func TestRegisterTerminalStreamRejectsStaleCredentialWithoutDirectoryEntry(t *testing.T) { - m := NewManager() - toAgent := make(chan *gatewayv2.TerminalStreamFrame, 1) - revoked := make(chan struct{}) - cleanup, ok := m.RegisterTerminalStreamToAgentIfCurrent( - "agent-a", - toAgent, - func() { close(revoked) }, - func() bool { return false }, - ) - defer cleanup() - if ok { - t.Fatal("stale authentication must not register a terminal stream") - } - if len(m.AgentStatuses()) != 0 { - t.Fatalf("stale terminal authentication left a directory entry: %#v", m.AgentStatuses()) - } - select { - case <-revoked: - t.Fatal("a connection that was never registered must not be revoked by the manager") - default: - } -} - -func TestDisconnectAgentRevokesControlAndTerminalTransports(t *testing.T) { - m := NewManager() - sess := newTestSession(m, "agent-a", "session-a") - m.SetSession(sess) - terminalRevoked := make(chan struct{}) - cleanup, ok := m.RegisterTerminalStreamToAgentIfCurrent( - "agent-a", - make(chan *gatewayv2.TerminalStreamFrame, 1), - func() { close(terminalRevoked) }, - func() bool { return true }, - ) - defer cleanup() - if !ok { - t.Fatal("terminal stream registration failed") - } - - if !m.DisconnectAgent("agent-a") { - t.Fatal("disconnect must report the revoked transports") - } - select { - case <-sess.Done(): - case <-time.After(time.Second): - t.Fatal("control session was not closed") - } - select { - case <-terminalRevoked: - case <-time.After(time.Second): - t.Fatal("terminal transport was not revoked") - } - if m.DisconnectAgent("agent-a") { - t.Fatal("second disconnect must be a no-op") - } -} - -func TestTerminalReconnectCleanupDoesNotClearReplacement(t *testing.T) { - m := NewManager() - firstRevoked := make(chan struct{}) - first := make(chan *gatewayv2.TerminalStreamFrame, 1) - firstCleanup, ok := m.RegisterTerminalStreamToAgentIfCurrent( - "agent-a", first, func() { close(firstRevoked) }, func() bool { return true }, - ) - if !ok { - t.Fatal("first terminal registration failed") - } - second := make(chan *gatewayv2.TerminalStreamFrame, 1) - secondCleanup, ok := m.RegisterTerminalStreamToAgentIfCurrent( - "agent-a", second, func() {}, func() bool { return true }, - ) - defer secondCleanup() - if !ok { - t.Fatal("replacement terminal registration failed") - } - select { - case <-firstRevoked: - case <-time.After(time.Second): - t.Fatal("replacement did not revoke the old terminal transport") - } - - firstCleanup() - frame := &gatewayv2.TerminalStreamFrame{Kind: "attach"} - if err := m.SendTerminalFrameToAgent(context.Background(), "agent-a", frame); err != nil { - t.Fatalf("send to replacement terminal: %v", err) - } - select { - case got := <-second: - if got != frame { - t.Fatalf("replacement received %#v, want original frame", got) - } - case <-time.After(time.Second): - t.Fatal("old cleanup cleared the replacement terminal stream") - } -} - -func TestAgentStatusesIncludesOfflineEntries(t *testing.T) { - m := NewManager() - a := newTestSession(m, "agent-a", "session-a") - m.SetSession(a) - b := newTestSession(m, "agent-b", "session-b") - m.SetSession(b) - m.ClearSession(b) - t.Cleanup(func() { m.ClearSession(a) }) - - statuses := m.AgentStatuses() - if len(statuses) != 2 { - t.Fatalf("statuses = %d entries, want 2", len(statuses)) - } - // 断线 entry 保留(目录渲染离线 Agent),按 id 排序。 - if statuses[0].AgentID != "agent-a" || !statuses[0].Online { - t.Fatalf("statuses[0] = %#v, want online agent-a", statuses[0]) - } - if statuses[1].AgentID != "agent-b" || statuses[1].Online { - t.Fatalf("statuses[1] = %#v, want offline agent-b", statuses[1]) - } -} - -func TestAgentDirectoryStatusSnapshotIndexesWithoutDroppingOfflineAgents(t *testing.T) { - m := NewManager() - a := newTestSession(m, "agent-a", "session-a") - m.SetSession(a) - b := newTestSession(m, "agent-b", "session-b") - m.SetSession(b) - m.ClearSession(b) - t.Cleanup(func() { m.ClearSession(a) }) - - statuses, onlineAgentIDs := m.AgentDirectoryStatusSnapshot() - if len(statuses) != 2 || !statuses["agent-a"].Online || statuses["agent-b"].Online { - t.Fatalf("status snapshot = %#v", statuses) - } - if len(onlineAgentIDs) != 1 || onlineAgentIDs[0] != "agent-a" { - t.Fatalf("online ids = %#v", onlineAgentIDs) - } -} diff --git a/crates/agent-gateway/internal/session/manager_registry.go b/crates/agent-gateway/internal/session/manager_registry.go deleted file mode 100644 index ac1c927fe..000000000 --- a/crates/agent-gateway/internal/session/manager_registry.go +++ /dev/null @@ -1,572 +0,0 @@ -package session - -import ( - "context" - "sort" - "strings" - "time" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -// RecordAuthentication 登记一次具名 Agent 的鉴权结果;同一 agent_id 的后续连接 -// 复用该登记项(entry 跨断线存活)。空 id 不会创建会话登记项。 -func (m *Manager) RecordAuthentication(agentID, agentVersion, sessionID string) AuthSnapshot { - agentID = strings.TrimSpace(agentID) - if agentID == "" { - return AuthSnapshot{} - } - m.registry.mu.Lock() - defer m.registry.mu.Unlock() - entry := m.registry.entryLocked(agentID) - entry.lastAuth = AuthSnapshot{ - AgentID: agentID, - AgentVersion: strings.TrimSpace(agentVersion), - SessionID: strings.TrimSpace(sessionID), - } - entry.authValid = true - return entry.lastAuth -} - -// LatestAuthSnapshot 返回指定 agent_id 的最近鉴权快照;空或未知 id 返回空快照。 -func (m *Manager) LatestAuthSnapshot(agentID string) AuthSnapshot { - m.registry.mu.RLock() - defer m.registry.mu.RUnlock() - entry := m.registry.agents[strings.TrimSpace(agentID)] - if entry == nil { - return AuthSnapshot{} - } - return entry.lastAuth -} - -// IsOnline 报告具名 agent_id 是否在线;空 id 一律返回 false。 -func (m *Manager) IsOnline(agentID string) bool { - agentID = strings.TrimSpace(agentID) - if agentID == "" { - return false - } - m.registry.mu.RLock() - defer m.registry.mu.RUnlock() - entry := m.registry.agents[agentID] - return entry != nil && entry.session != nil -} - -// AnyAgentOnline 仅用于全局健康与后台存活判断,不承担 Agent 寻址。 -func (m *Manager) AnyAgentOnline() bool { - m.registry.mu.RLock() - defer m.registry.mu.RUnlock() - for _, entry := range m.registry.agents { - if entry.session != nil { - return true - } - } - return false -} - -// SetSession 把会话登记到其 agent_id 的登记项,只顶掉该 id 的旧会话; -// 不同 agent_id 的会话互不影响。 -func (m *Manager) SetSession(s *AgentSession) { - m.setSession(s, nil, false) -} - -// SetAuthenticatedSessionIfCurrent 把鉴权结果和会话作为一个注册动作提交。 -// isCurrent 在 registry 写锁内执行;返回 false 时不会创建离线登记项。 -func (m *Manager) SetAuthenticatedSessionIfCurrent( - s *AgentSession, - isCurrent func() bool, -) bool { - return m.setSession(s, isCurrent, true) -} - -func (m *Manager) setSession( - s *AgentSession, - isCurrent func() bool, - recordAuthentication bool, -) bool { - if s == nil || strings.TrimSpace(s.AgentID) == "" { - if s != nil { - s.Close() - } - return false - } - s.AgentID = strings.TrimSpace(s.AgentID) - m.registry.mu.Lock() - if isCurrent != nil && !isCurrent() { - m.registry.mu.Unlock() - s.Close() - return false - } - entry := m.registry.entryLocked(s.AgentID) - if recordAuthentication { - entry.lastAuth = AuthSnapshot{ - AgentID: s.AgentID, - AgentVersion: strings.TrimSpace(s.AgentVersion), - SessionID: strings.TrimSpace(s.SessionID), - } - entry.authValid = true - } - previous := entry.session - if entry.authValid { - s.AgentID = entry.lastAuth.AgentID - s.AgentVersion = entry.lastAuth.AgentVersion - s.SessionID = entry.lastAuth.SessionID - } - sessionChanged := previous != s - if sessionChanged { - entry.sessionEpoch += 1 - clearRuntimeStatusLocked(entry) - } - entry.session = s - agentID := entry.id - m.registry.mu.Unlock() - - if sessionChanged { - m.clearTerminalSessionSnapshot(agentID) - } - if previous != nil && previous != s { - previous.Close() - } - if s != nil && sessionChanged { - // Replay the watched-workdir set: a freshly connected agent starts - // with an empty watch set and only learns a non-empty one from this - // push. An empty set needs no replay. - if m.hasWorkspaceWatchInterest(agentID) { - go m.pushWorkspaceWatchSet(agentID) - } - } - if sessionChanged { - m.broadcastStatus(agentID) - } - return true -} - -// clearSessionEntry 摘除 session 所属登记项的在线会话;session 已被顶替时无操作。 -// 返回登记项 id 与是否实际摘除。 -func (m *Manager) clearSessionEntry(session *AgentSession) (string, bool) { - m.registry.mu.Lock() - entry := m.registry.entryForSessionLocked(session) - if entry == nil { - m.registry.mu.Unlock() - return "", false - } - entry.session = nil - clearRuntimeStatusLocked(entry) - agentID := entry.id - m.registry.mu.Unlock() - return agentID, true -} - -func (m *Manager) ClearSession(session *AgentSession) { - if session == nil { - return - } - agentID, cleared := m.clearSessionEntry(session) - if !cleared { - return - } - - session.Close() - m.clearTerminalSessionSnapshot(agentID) - go m.onAgentSessionCleared(agentID) -} - -func (m *Manager) ClearSessionIfHeartbeatStale(session *AgentSession, timeout time.Duration) bool { - if session == nil || timeout <= 0 { - return false - } - - now := time.Now() - m.registry.mu.Lock() - entry := m.registry.entryForSessionLocked(session) - if entry == nil { - m.registry.mu.Unlock() - return false - } - if lastPing := entry.session.LastPing; !lastPing.IsZero() && now.Sub(lastPing) <= timeout { - m.registry.mu.Unlock() - return false - } - entry.session = nil - clearRuntimeStatusLocked(entry) - agentID := entry.id - m.registry.mu.Unlock() - - session.Close() - m.clearTerminalSessionSnapshot(agentID) - go m.onAgentSessionCleared(agentID) - return true -} - -// DisconnectAgent 在同一注册表临界区摘除 agent_id 的控制会话与终端数据面; -// 返回是否有任一传输被断开。实际关闭在锁外执行,避免回调阻塞注册表。 -func (m *Manager) DisconnectAgent(agentID string) bool { - agentID = strings.TrimSpace(agentID) - if agentID == "" { - return false - } - m.registry.mu.Lock() - var session *AgentSession - var terminalRevoke func() - if entry := m.registry.agents[agentID]; entry != nil { - session = entry.session - if session != nil { - entry.session = nil - clearRuntimeStatusLocked(entry) - } - entry.terminalStreamMu.Lock() - terminalRevoke = entry.terminalStreamRevoke - entry.terminalStreamToAgent = nil - entry.terminalStreamRevoke = nil - entry.terminalStreamMu.Unlock() - } - m.registry.mu.Unlock() - - if session != nil { - session.Close() - go m.onAgentSessionCleared(agentID) - } - if terminalRevoke != nil { - terminalRevoke() - } - if session != nil || terminalRevoke != nil { - m.clearTerminalSessionSnapshot(agentID) - } - return session != nil || terminalRevoke != nil -} - -// ForgetAgent 从进程目录移除 agent_id,并关闭其当前会话。持久化目录删除后调用 -// 此方法,避免已删除客户端继续作为离线条目出现在 agent_list 中。 -func (m *Manager) ForgetAgent(agentID string) bool { - agentID = strings.TrimSpace(agentID) - if agentID == "" { - return false - } - m.registry.mu.Lock() - entry := m.registry.agents[agentID] - if entry == nil { - m.registry.mu.Unlock() - m.purgeAgentTunnels(agentID) - return false - } - delete(m.registry.agents, agentID) - session := entry.session - entry.terminalStreamMu.Lock() - terminalRevoke := entry.terminalStreamRevoke - entry.terminalStreamToAgent = nil - entry.terminalStreamRevoke = nil - entry.terminalStreamMu.Unlock() - m.registry.mu.Unlock() - - if session != nil { - session.Close() - go m.onAgentSessionCleared(agentID) - } - if terminalRevoke != nil { - terminalRevoke() - } - m.purgeAgentTunnels(agentID) - return session != nil || terminalRevoke != nil -} - -// Status 返回具名 agent_id 的状态;空或未知 id 返回零值。 -func (m *Manager) Status(agentID string) Status { - agentID = strings.TrimSpace(agentID) - if agentID == "" { - return Status{} - } - m.registry.mu.RLock() - defer m.registry.mu.RUnlock() - entry := m.registry.agents[agentID] - if entry == nil { - return Status{} - } - return statusLocked(entry, time.Now()) -} - -// AgentStatuses 返回全部登记项的状态(含离线项,供目录渲染),按 agent_id 排序。 -func (m *Manager) AgentStatuses() []Status { - m.registry.mu.RLock() - defer m.registry.mu.RUnlock() - - now := time.Now() - statuses := make([]Status, 0, len(m.registry.agents)) - for _, entry := range m.registry.agents { - statuses = append(statuses, statusLocked(entry, now)) - } - sort.Slice(statuses, func(i, j int) bool { return statuses[i].AgentID < statuses[j].AgentID }) - return statuses -} - -// AgentDirectoryStatusSnapshot 为管理目录查询生成同一时刻的状态索引和在线 ID。 -// 返回值无需排序,避免数据库分页请求额外执行全量 O(n log n) 排序。 -func (m *Manager) AgentDirectoryStatusSnapshot() (map[string]Status, []string) { - m.registry.mu.RLock() - defer m.registry.mu.RUnlock() - - now := time.Now() - statuses := make(map[string]Status, len(m.registry.agents)) - onlineAgentIDs := make([]string, 0, len(m.registry.agents)) - for _, entry := range m.registry.agents { - status := statusLocked(entry, now) - statuses[status.AgentID] = status - if status.Online { - onlineAgentIDs = append(onlineAgentIDs, status.AgentID) - } - } - return statuses, onlineAgentIDs -} - -// ConnectedAgentIDs 返回当前在线的 agent_id 列表,按字典序。 -func (m *Manager) ConnectedAgentIDs() []string { - m.registry.mu.RLock() - defer m.registry.mu.RUnlock() - - ids := make([]string, 0, len(m.registry.agents)) - for _, entry := range m.registry.agents { - if entry.session != nil { - ids = append(ids, entry.id) - } - } - sort.Strings(ids) - return ids -} - -func statusLocked(entry *agentEntry, now time.Time) Status { - status := Status{} - if entry.authValid { - status.AgentID = entry.lastAuth.AgentID - status.AgentVersion = entry.lastAuth.AgentVersion - status.SessionID = entry.lastAuth.SessionID - } - if entry.session == nil { - if status.AgentID == "" { - status.AgentID = entry.id - } - return status - } - status.Online = true - status.AgentReady = true - status.AgentID = entry.session.AgentID - status.AgentVersion = entry.session.AgentVersion - status.SessionID = entry.session.SessionID - status.ConnectedSince = entry.session.ConnectedAt.Unix() - status.LastHeartbeat = entry.session.LastPing.Unix() - if !entry.session.SupportsCapability(gatewayv2.ChatIngressV1Capability) { - status.RuntimeState = "protocol_incompatible" - } else { - status.RuntimeState = entry.runtimeState - } - status.RuntimeWorkerID = entry.runtimeWorkerID - status.RuntimeVisible = entry.runtimeVisible - status.RuntimeActiveRunCount = entry.runtimeActiveRunCount - if !entry.runtimeLastHeartbeat.IsZero() { - status.RuntimeLastHeartbeat = entry.runtimeLastHeartbeat.Unix() - } - status.ChatRuntimeReady = runtimeReadyLocked(entry, now) - return status -} - -func (m *Manager) ChatRuntimeReady(agentID string) bool { - m.registry.mu.RLock() - defer m.registry.mu.RUnlock() - entry, err := m.registry.resolveOnlineLocked(agentID) - if err != nil { - return false - } - return runtimeReadyLocked(entry, time.Now()) -} - -func (m *Manager) ChatIngressV1Ready(agentID string) bool { - m.registry.mu.RLock() - defer m.registry.mu.RUnlock() - entry, err := m.registry.resolveOnlineLocked(agentID) - return err == nil && entry.session.SupportsCapability(gatewayv2.ChatIngressV1Capability) -} - -// ChatRuntimeProbeEpoch 返回目标 Agent 的会话 epoch;探活完成后以同一 agent_id + -// epoch 调 RecordChatRuntimeProbe,把结果绑定到发起探活的那次连接。 -func (m *Manager) ChatRuntimeProbeEpoch(agentID string) (uint64, bool) { - m.registry.mu.RLock() - defer m.registry.mu.RUnlock() - entry, err := m.registry.resolveOnlineLocked(agentID) - if err != nil { - return 0, false - } - return entry.sessionEpoch, true -} - -func (m *Manager) RecordChatRuntimeProbe(agentID string, sessionEpoch uint64) bool { - m.registry.mu.Lock() - defer m.registry.mu.Unlock() - entry, err := m.registry.resolveOnlineLocked(agentID) - if err != nil || sessionEpoch == 0 || entry.sessionEpoch != sessionEpoch { - return false - } - entry.chatRuntimeProbeAt = time.Now() - return true -} - -func (m *Manager) ChatRuntimeProbeFresh(agentID string, maxAge time.Duration) bool { - if maxAge <= 0 { - return false - } - m.registry.mu.RLock() - defer m.registry.mu.RUnlock() - entry, err := m.registry.resolveOnlineLocked(agentID) - return err == nil && - !entry.chatRuntimeProbeAt.IsZero() && - time.Since(entry.chatRuntimeProbeAt) <= maxAge -} - -func (m *Manager) UpdateRuntimeStatus( - session *AgentSession, - event *gatewayv2.RuntimeStatusEvent, -) { - if event == nil { - return - } - workerID := strings.TrimSpace(event.GetWorkerId()) - state := normalizeRuntimeState(event.GetState()) - now := time.Now() - - m.registry.mu.Lock() - entry := m.registry.entryForSessionLocked(session) - if entry == nil { - m.registry.mu.Unlock() - return - } - previousReady := runtimeReadyLocked(entry, now) - changed := entry.runtimeState != state || - entry.runtimeWorkerID != workerID || - entry.runtimeVisible != event.GetVisible() || - entry.runtimeActiveRunCount != event.GetActiveRunCount() - entry.runtimeState = state - entry.runtimeWorkerID = workerID - entry.runtimeLastHeartbeat = now - entry.runtimeVisible = event.GetVisible() - entry.runtimeActiveRunCount = event.GetActiveRunCount() - changed = changed || previousReady != runtimeReadyLocked(entry, now) - agentID := entry.id - m.registry.mu.Unlock() - - // Runtime readiness is part of the public Status contract. Push semantic - // transitions, but do not fan out a status frame for every heartbeat tick; - // the low-frequency status poll reconciles timestamp-only changes. - if changed { - m.broadcastStatus(agentID) - } -} - -// touchRuntimeActivity refreshes the chat-runtime heartbeat when live chat -// traffic proves the desktop runtime is running, even while the webview's -// own status timer is throttled (hidden/occluded window). Only refreshes an -// already-reporting runtime: a zero heartbeat must not become readiness -// (normalizeRuntimeState("") defaults to "ready"). -func (m *Manager) touchRuntimeActivity(session *AgentSession) { - m.registry.mu.Lock() - defer m.registry.mu.Unlock() - entry := m.registry.entryForSessionLocked(session) - if entry == nil || entry.runtimeLastHeartbeat.IsZero() { - return - } - entry.runtimeLastHeartbeat = time.Now() -} - -func (m *Manager) TouchHeartbeat(session *AgentSession) { - m.registry.mu.Lock() - defer m.registry.mu.Unlock() - if entry := m.registry.entryForSessionLocked(session); entry != nil { - entry.session.LastPing = time.Now() - } -} - -func clearRuntimeStatusLocked(entry *agentEntry) { - entry.runtimeState = "" - entry.runtimeWorkerID = "" - entry.runtimeLastHeartbeat = time.Time{} - entry.runtimeVisible = false - entry.runtimeActiveRunCount = 0 - entry.chatRuntimeProbeAt = time.Time{} -} - -func runtimeReadyLocked(entry *agentEntry, now time.Time) bool { - if entry == nil || entry.session == nil { - return false - } - if !entry.session.SupportsCapability(gatewayv2.ChatIngressV1Capability) { - return false - } - if entry.session.LastPing.IsZero() || now.Sub(entry.session.LastPing) > agentSessionHeartbeatTTL { - return false - } - if entry.runtimeLastHeartbeat.IsZero() || - now.Sub(entry.runtimeLastHeartbeat) > chatRuntimeReadyTTL { - return false - } - switch normalizeRuntimeState(entry.runtimeState) { - case "ready", "draining", "busy": - return true - default: - return false - } -} - -func normalizeRuntimeState(state string) string { - switch strings.TrimSpace(state) { - case "ready", "draining", "busy", "suspended": - return strings.TrimSpace(state) - default: - return defaultRuntimeReadyState - } -} - -// resolveSession 按非空 agentID 精确解析在线会话。 -func (m *Manager) resolveSession(agentID string) (*AgentSession, error) { - m.registry.mu.RLock() - defer m.registry.mu.RUnlock() - entry, err := m.registry.resolveOnlineLocked(agentID) - if err != nil { - return nil, err - } - return entry.session, nil -} - -func (m *Manager) SendToAgentContext(ctx context.Context, agentID string, env *gatewayv2.GatewayEnvelope) error { - session, err := m.resolveSession(agentID) - if err != nil { - return err - } - return session.SendToAgentContext(ctx, env) -} - -// RegisterStreamAndSendContext binds response correlation and request delivery -// to the same AgentSession instance. Calling RegisterStream followed by -// Manager.SendToAgentContext performs two independent current-session lookups; -// a seamless session replacement between them can register the stream on the -// old session while sending the request to the new one, making every response -// unmatchable. Capturing the session once closes that TOCTOU window. -func (m *Manager) RegisterStreamAndSendContext( - ctx context.Context, - agentID string, - requestID string, - env *gatewayv2.GatewayEnvelope, -) (<-chan *gatewayv2.AgentEnvelope, <-chan struct{}, func(), error) { - session, err := m.resolveSession(agentID) - if err != nil { - return nil, nil, nil, err - } - - stream, err := session.registerStream(requestID) - if err != nil { - return nil, nil, nil, err - } - cleanup := func() { - session.unregisterStream(requestID, stream) - } - if err := session.SendToAgentContext(ctx, env); err != nil { - cleanup() - return nil, nil, nil, err - } - - return stream.ch, stream.done, cleanup, nil -} diff --git a/crates/agent-gateway/internal/session/manager_settings_sync.go b/crates/agent-gateway/internal/session/manager_settings_sync.go deleted file mode 100644 index 307c30dfb..000000000 --- a/crates/agent-gateway/internal/session/manager_settings_sync.go +++ /dev/null @@ -1,130 +0,0 @@ -package session - -import ( - "encoding/json" - "strings" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -func (m *Manager) SubscribeSettingsSync() (<-chan Tagged[*gatewayv2.SettingsSyncEvent], func()) { - ch := make(chan Tagged[*gatewayv2.SettingsSyncEvent], 64) - - m.syncHub.settingsMu.Lock() - subID := m.syncHub.nextSettingsSubID - m.syncHub.nextSettingsSubID += 1 - m.syncHub.settingsSubscribers[subID] = ch - m.syncHub.settingsMu.Unlock() - - cleanup := func() { - m.syncHub.settingsMu.Lock() - // Do not close the channel here: broadcastSettingsSync sends after - // copying subscribers, so closing can race with an in-flight send. - delete(m.syncHub.settingsSubscribers, subID) - m.syncHub.settingsMu.Unlock() - } - - return ch, cleanup -} - -// settingsRemoteBool 读取目标 Agent settings 快照里 remote. 的布尔门控; -// Agent 不存在或未同步过 settings 时一律 false(默认拒绝)。 -func (m *Manager) settingsRemoteBool(agentID, key string) bool { - entry := m.entryFor(agentID) - if entry == nil { - return false - } - entry.settingsSnapshotMu.RLock() - defer entry.settingsSnapshotMu.RUnlock() - - remote, ok := entry.settingsSnapshot["remote"].(map[string]any) - if !ok { - return false - } - enabled, ok := remote[key].(bool) - return ok && enabled -} - -func (m *Manager) WebTerminalEnabled(agentID string) bool { - return m.settingsRemoteBool(agentID, "enableWebTerminal") -} - -func (m *Manager) WebSshTerminalEnabled(agentID string) bool { - return m.settingsRemoteBool(agentID, "enableWebSshTerminal") -} - -func (m *Manager) WebGitEnabled(agentID string) bool { - return m.settingsRemoteBool(agentID, "enableWebGit") -} - -func parseSettingsJSON(settingsJSON string) (map[string]any, bool) { - raw := strings.TrimSpace(settingsJSON) - if raw == "" { - return nil, false - } - var payload map[string]any - if err := json.Unmarshal([]byte(raw), &payload); err != nil || payload == nil { - return nil, false - } - return payload, true -} - -func (m *Manager) ApplySettingsJSON(agentID, settingsJSON string) { - payload, ok := parseSettingsJSON(settingsJSON) - if !ok { - return - } - entry := m.entryOrCreate(agentID) - if entry == nil { - return - } - entry.settingsSnapshotMu.Lock() - if _, hasIncomingRemote := payload["remote"]; !hasIncomingRemote { - if existingRemote, hasExistingRemote := entry.settingsSnapshot["remote"]; hasExistingRemote { - payload["remote"] = existingRemote - } - } - entry.settingsSnapshot = payload - entry.settingsSnapshotMu.Unlock() -} - -func (m *Manager) ApplySettingsJSONPreservingRemote(agentID, settingsJSON string) { - payload, ok := parseSettingsJSON(settingsJSON) - if !ok { - return - } - entry := m.entryOrCreate(agentID) - if entry == nil { - return - } - entry.settingsSnapshotMu.Lock() - if existingRemote, ok := entry.settingsSnapshot["remote"]; ok { - payload["remote"] = existingRemote - } else { - delete(payload, "remote") - } - entry.settingsSnapshot = payload - entry.settingsSnapshotMu.Unlock() -} - -func (m *Manager) broadcastSettingsSync(agentID string, event *gatewayv2.SettingsSyncEvent) { - if event == nil { - return - } - m.ApplySettingsJSON(agentID, event.GetSettingsJson()) - - m.syncHub.settingsMu.Lock() - subscribers := make([]chan Tagged[*gatewayv2.SettingsSyncEvent], 0, len(m.syncHub.settingsSubscribers)) - for _, ch := range m.syncHub.settingsSubscribers { - subscribers = append(subscribers, ch) - } - m.syncHub.settingsMu.Unlock() - - tagged := Tagged[*gatewayv2.SettingsSyncEvent]{AgentID: agentID, Event: event} - for _, ch := range subscribers { - select { - case ch <- tagged: - default: - } - } -} diff --git a/crates/agent-gateway/internal/session/manager_sftp.go b/crates/agent-gateway/internal/session/manager_sftp.go deleted file mode 100644 index 0777b596a..000000000 --- a/crates/agent-gateway/internal/session/manager_sftp.go +++ /dev/null @@ -1,48 +0,0 @@ -package session - -import ( - "time" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -func (m *Manager) SubscribeSftpEvents() (<-chan Tagged[*gatewayv2.SftpEvent], func()) { - ch := make(chan Tagged[*gatewayv2.SftpEvent], 4096) - - m.syncHub.sftpMu.Lock() - subID := m.syncHub.nextSftpSubID - m.syncHub.nextSftpSubID += 1 - m.syncHub.sftpSubscribers[subID] = ch - m.syncHub.sftpMu.Unlock() - - cleanup := func() { - m.syncHub.sftpMu.Lock() - // Do not close the channel here: broadcastSftpEvent sends after - // copying subscribers, so closing can race with an in-flight send. - delete(m.syncHub.sftpSubscribers, subID) - m.syncHub.sftpMu.Unlock() - } - - return ch, cleanup -} - -func (m *Manager) broadcastSftpEvent(agentID string, event *gatewayv2.SftpEvent) { - if event == nil { - return - } - - m.syncHub.sftpMu.Lock() - subscribers := make([]chan Tagged[*gatewayv2.SftpEvent], 0, len(m.syncHub.sftpSubscribers)) - for _, ch := range m.syncHub.sftpSubscribers { - subscribers = append(subscribers, ch) - } - m.syncHub.sftpMu.Unlock() - - tagged := Tagged[*gatewayv2.SftpEvent]{AgentID: agentID, Event: event} - for _, ch := range subscribers { - select { - case ch <- tagged: - case <-time.After(50 * time.Millisecond): - } - } -} diff --git a/crates/agent-gateway/internal/session/manager_state.go b/crates/agent-gateway/internal/session/manager_state.go deleted file mode 100644 index c41e4eadf..000000000 --- a/crates/agent-gateway/internal/session/manager_state.go +++ /dev/null @@ -1,193 +0,0 @@ -package session - -import ( - "strings" - "sync" - "time" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -// sessionRegistry 按 agent_id 维护多个桌面 Agent 的登记项。entry 断线后保留 -// (auth/runtime 快照跨重连存活),同 id 重连只顶掉该 id 的旧会话。 -type sessionRegistry struct { - mu sync.RWMutex - agents map[string]*agentEntry -} - -// agentEntry 是单个 Agent 的登记项;session 为 nil 表示当前离线。 -// epoch 在每次会话更替时自增,用于把探活结果绑定到具体一次连接。 -// 各快照按 Agent 隔离并跨断线存活(重连后浏览器无需等待全量重推)。 -type agentEntry struct { - id string - session *AgentSession - sessionEpoch uint64 - lastAuth AuthSnapshot - authValid bool - - runtimeState string - runtimeWorkerID string - runtimeLastHeartbeat time.Time - runtimeVisible bool - runtimeActiveRunCount uint32 - chatRuntimeProbeAt time.Time - - // settingsSnapshot 缓存该 Agent 最近一次 settings 同步(功能门控依据)。 - settingsSnapshotMu sync.RWMutex - settingsSnapshot map[string]any - - // terminalSessions 缓存该 Agent 的终端会话快照(浏览器接入时回放)。 - terminalSessionsMu sync.Mutex - terminalSessions map[string]*gatewayv2.TerminalSession - - // chatQueueSnapshots 缓存该 Agent 各会话的提示队列快照。 - chatQueueSnapshotsMu sync.Mutex - chatQueueSnapshots map[string]chatQueueSnapshotRecord - - // terminalStreamToAgent 是该 Agent 终端数据面连接的入站通道;revoke - // 关闭通道所属连接,使凭证轮换和删除能同时撤销控制面与终端数据面。 - terminalStreamMu sync.Mutex - terminalStreamToAgent chan *gatewayv2.TerminalStreamFrame - terminalStreamRevoke func() -} - -func newAgentEntry(id string) *agentEntry { - return &agentEntry{ - id: id, - terminalSessions: make(map[string]*gatewayv2.TerminalSession), - chatQueueSnapshots: make(map[string]chatQueueSnapshotRecord), - } -} - -func newSessionRegistry() *sessionRegistry { - return &sessionRegistry{agents: make(map[string]*agentEntry)} -} - -// normalizeAgentKey 统一 agent_id 的 map 键形态(去空白)。 -func normalizeAgentKey(agentID string) string { - return strings.TrimSpace(agentID) -} - -// entryLocked 取或建 agent_id 的登记项;空 id 不创建登记项。调用方需持有写锁。 -func (r *sessionRegistry) entryLocked(agentID string) *agentEntry { - agentID = strings.TrimSpace(agentID) - if agentID == "" { - return nil - } - entry := r.agents[agentID] - if entry == nil { - entry = newAgentEntry(agentID) - r.agents[agentID] = entry - } - return entry -} - -// resolveOnlineLocked 按非空 agent_id 精确解析在线登记项。 -// 调用方需持锁(读锁即可)。 -func (r *sessionRegistry) resolveOnlineLocked(agentID string) (*agentEntry, error) { - agentID = strings.TrimSpace(agentID) - if agentID == "" { - return nil, ErrAgentIDRequired - } - if entry := r.agents[agentID]; entry != nil && entry.session != nil { - return entry, nil - } - return nil, ErrAgentOffline -} - -// entryForSessionLocked 反查 session 所属的登记项;session 已被顶替/清除时返回 nil, -// 使旧连接迟到的心跳与运行时上报不会污染新会话。 -func (r *sessionRegistry) entryForSessionLocked(session *AgentSession) *agentEntry { - if session == nil { - return nil - } - entry := r.agents[strings.TrimSpace(session.AgentID)] - if entry == nil || entry.session != session { - return nil - } - return entry -} - -// entryFor 返回非空 agent_id 的登记项(可能离线);不存在或 id 为空返回 nil。 -// 快照读写走 entry 自身的细粒度锁,注册表锁只保护 map 查找。 -func (m *Manager) entryFor(agentID string) *agentEntry { - agentID = strings.TrimSpace(agentID) - if agentID == "" { - return nil - } - m.registry.mu.RLock() - defer m.registry.mu.RUnlock() - return m.registry.agents[agentID] -} - -// entryOrCreate 按非空 agent_id 取或建登记项;空 id 返回 nil。 -func (m *Manager) entryOrCreate(agentID string) *agentEntry { - agentID = strings.TrimSpace(agentID) - if agentID == "" { - return nil - } - m.registry.mu.Lock() - defer m.registry.mu.Unlock() - return m.registry.entryLocked(agentID) -} - -// resolveEntry 按非空 agent_id 精确解析在线登记项。 -func (m *Manager) resolveEntry(agentID string) (*agentEntry, error) { - m.registry.mu.RLock() - defer m.registry.mu.RUnlock() - return m.registry.resolveOnlineLocked(agentID) -} - -// ResolveAgentID 返回请求明确指向的在线 agent_id;空 id 返回 ErrAgentIDRequired。 -func (m *Manager) ResolveAgentID(agentID string) (string, error) { - entry, err := m.resolveEntry(agentID) - if err != nil { - return "", err - } - return entry.id, nil -} - -// Tagged 给广播事件附加来源 agent_id。hub 订阅保持全局(每浏览器连接一份订阅), -// 事件按标签由消费端过滤/盖帧;标签一律取自已认证会话的 AgentID,是跨 Agent -// 隔离的唯一事实源。 -type Tagged[T any] struct { - AgentID string - Event T -} - -type syncHub struct { - historyMu sync.Mutex - nextHistorySubID int - historySubscribers map[int]chan Tagged[*gatewayv2.HistorySyncEvent] - - settingsMu sync.Mutex - nextSettingsSubID int - settingsSubscribers map[int]chan Tagged[*gatewayv2.SettingsSyncEvent] - - terminalMu sync.Mutex - nextTerminalSubID int - terminalSubscribers map[int]chan Tagged[*gatewayv2.TerminalEvent] - - terminalStreamMu sync.Mutex - nextTerminalStreamSubID int - terminalStreamSubscribers map[int]chan Tagged[*gatewayv2.TerminalStreamFrame] - - sftpMu sync.Mutex - nextSftpSubID int - sftpSubscribers map[int]chan Tagged[*gatewayv2.SftpEvent] - - chatQueueMu sync.Mutex - nextChatQueueSubID int - chatQueueSubscribers map[int]chan Tagged[*gatewayv2.ChatQueueEvent] -} - -func newSyncHub() *syncHub { - return &syncHub{ - historySubscribers: make(map[int]chan Tagged[*gatewayv2.HistorySyncEvent]), - settingsSubscribers: make(map[int]chan Tagged[*gatewayv2.SettingsSyncEvent]), - terminalSubscribers: make(map[int]chan Tagged[*gatewayv2.TerminalEvent]), - terminalStreamSubscribers: make(map[int]chan Tagged[*gatewayv2.TerminalStreamFrame]), - sftpSubscribers: make(map[int]chan Tagged[*gatewayv2.SftpEvent]), - chatQueueSubscribers: make(map[int]chan Tagged[*gatewayv2.ChatQueueEvent]), - } -} diff --git a/crates/agent-gateway/internal/session/manager_terminal.go b/crates/agent-gateway/internal/session/manager_terminal.go deleted file mode 100644 index 1332f4cdd..000000000 --- a/crates/agent-gateway/internal/session/manager_terminal.go +++ /dev/null @@ -1,372 +0,0 @@ -package session - -import ( - "context" - "sort" - "strings" - "time" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -func (m *Manager) SubscribeTerminalEvents() (<-chan Tagged[*gatewayv2.TerminalEvent], func()) { - ch := make(chan Tagged[*gatewayv2.TerminalEvent], 4096) - - m.syncHub.terminalMu.Lock() - subID := m.syncHub.nextTerminalSubID - m.syncHub.nextTerminalSubID += 1 - m.syncHub.terminalSubscribers[subID] = ch - m.syncHub.terminalMu.Unlock() - - cleanup := func() { - m.syncHub.terminalMu.Lock() - // Do not close the channel here: broadcastTerminalEvent sends after - // copying subscribers, so closing can race with an in-flight send. - delete(m.syncHub.terminalSubscribers, subID) - m.syncHub.terminalMu.Unlock() - } - - return ch, cleanup -} - -// RegisterTerminalStreamToAgentIfCurrent 把终端数据面鉴权结果与连接作为一个注册 -// 动作提交。isCurrent 在 registry 写锁内执行,凭证已轮换或删除时不会留下登记项。 -// 同 Agent 重连会撤销旧终端连接;不同 Agent 互不影响。 -func (m *Manager) RegisterTerminalStreamToAgentIfCurrent( - agentID string, - ch chan *gatewayv2.TerminalStreamFrame, - revoke func(), - isCurrent func() bool, -) (func(), bool) { - agentID = strings.TrimSpace(agentID) - if agentID == "" || ch == nil || revoke == nil { - return func() {}, false - } - - m.registry.mu.Lock() - if isCurrent != nil && !isCurrent() { - m.registry.mu.Unlock() - return func() {}, false - } - entry := m.registry.entryLocked(agentID) - entry.terminalStreamMu.Lock() - previousRevoke := entry.terminalStreamRevoke - entry.terminalStreamToAgent = ch - entry.terminalStreamRevoke = revoke - entry.terminalStreamMu.Unlock() - m.registry.mu.Unlock() - - if previousRevoke != nil { - previousRevoke() - } - - return func() { - entry.terminalStreamMu.Lock() - if entry.terminalStreamToAgent == ch { - entry.terminalStreamToAgent = nil - entry.terminalStreamRevoke = nil - } - entry.terminalStreamMu.Unlock() - }, true -} - -// SendTerminalFrameToAgent 把浏览器终端帧送往目标 Agent 的数据面连接。 -// 终端数据面独立于控制会话:只要求通道已登记,不要求控制会话在线 -// (终端数据面是独立连接,两者的建立顺序不定)。 -func (m *Manager) SendTerminalFrameToAgent(ctx context.Context, agentID string, frame *gatewayv2.TerminalStreamFrame) error { - if frame == nil { - return nil - } - if ctx == nil { - ctx = context.Background() - } - entry := m.entryFor(agentID) - if entry == nil { - return ErrAgentOffline - } - entry.terminalStreamMu.Lock() - ch := entry.terminalStreamToAgent - entry.terminalStreamMu.Unlock() - if ch == nil { - return ErrAgentOffline - } - select { - case <-ctx.Done(): - return ctx.Err() - case ch <- frame: - return nil - } -} - -func (m *Manager) SubscribeTerminalStreamFrames() (<-chan Tagged[*gatewayv2.TerminalStreamFrame], func()) { - ch := make(chan Tagged[*gatewayv2.TerminalStreamFrame], 4096) - - m.syncHub.terminalStreamMu.Lock() - subID := m.syncHub.nextTerminalStreamSubID - m.syncHub.nextTerminalStreamSubID += 1 - m.syncHub.terminalStreamSubscribers[subID] = ch - m.syncHub.terminalStreamMu.Unlock() - - cleanup := func() { - m.syncHub.terminalStreamMu.Lock() - delete(m.syncHub.terminalStreamSubscribers, subID) - m.syncHub.terminalStreamMu.Unlock() - } - - return ch, cleanup -} - -func (m *Manager) BroadcastTerminalStreamFrame(agentID string, frame *gatewayv2.TerminalStreamFrame) { - if frame == nil { - return - } - tagged := Tagged[*gatewayv2.TerminalStreamFrame]{AgentID: agentID, Event: frame} - m.syncHub.terminalStreamMu.Lock() - for id, ch := range m.syncHub.terminalStreamSubscribers { - select { - case ch <- tagged: - default: - delete(m.syncHub.terminalStreamSubscribers, id) - close(ch) - } - } - m.syncHub.terminalStreamMu.Unlock() -} - -func cloneTerminalSession(session *gatewayv2.TerminalSession) *gatewayv2.TerminalSession { - if session == nil { - return nil - } - return &gatewayv2.TerminalSession{ - Id: session.GetId(), - ProjectPathKey: session.GetProjectPathKey(), - Cwd: session.GetCwd(), - Shell: session.GetShell(), - Title: session.GetTitle(), - Pid: session.GetPid(), - Cols: session.GetCols(), - Rows: session.GetRows(), - CreatedAt: session.GetCreatedAt(), - UpdatedAt: session.GetUpdatedAt(), - FinishedAt: session.GetFinishedAt(), - ExitCode: session.GetExitCode(), - Running: session.GetRunning(), - Kind: session.GetKind(), - Ssh: cloneTerminalSshMetadata(session.GetSsh()), - } -} - -func cloneTerminalSshMetadata(ssh *gatewayv2.TerminalSshMetadata) *gatewayv2.TerminalSshMetadata { - if ssh == nil { - return nil - } - return &gatewayv2.TerminalSshMetadata{ - HostId: ssh.GetHostId(), - HostName: ssh.GetHostName(), - Username: ssh.GetUsername(), - Host: ssh.GetHost(), - Port: ssh.GetPort(), - AuthType: ssh.GetAuthType(), - Status: ssh.GetStatus(), - ReconnectAttempt: ssh.GetReconnectAttempt(), - ReconnectMaxAttempts: ssh.GetReconnectMaxAttempts(), - SftpEnabled: ssh.GetSftpEnabled(), - } -} - -func (m *Manager) TerminalSessionKind(agentID, sessionID string) string { - sessionID = strings.TrimSpace(sessionID) - if sessionID == "" { - return "" - } - entry := m.entryFor(agentID) - if entry == nil { - return "" - } - entry.terminalSessionsMu.Lock() - defer entry.terminalSessionsMu.Unlock() - session := entry.terminalSessions[sessionID] - if session == nil { - return "" - } - if strings.TrimSpace(session.GetKind()) == "ssh" { - return "ssh" - } - return "local" -} - -func terminalSessionSortKey(session *gatewayv2.TerminalSession) (string, uint64, string) { - if session == nil { - return "", 0, "" - } - return strings.TrimSpace(session.GetProjectPathKey()), session.GetCreatedAt(), strings.TrimSpace(session.GetId()) -} - -func sortTerminalSessions(sessions []*gatewayv2.TerminalSession) { - sort.Slice(sessions, func(i, j int) bool { - leftProject, leftCreatedAt, leftID := terminalSessionSortKey(sessions[i]) - rightProject, rightCreatedAt, rightID := terminalSessionSortKey(sessions[j]) - if leftProject != rightProject { - return leftProject < rightProject - } - if leftCreatedAt != rightCreatedAt { - return leftCreatedAt < rightCreatedAt - } - return leftID < rightID - }) -} - -func terminalSessionMatchesProject(session *gatewayv2.TerminalSession, projectPathKey string) bool { - projectPathKey = strings.TrimSpace(projectPathKey) - if projectPathKey == "" { - return true - } - if session == nil { - return false - } - return strings.TrimSpace(session.GetProjectPathKey()) == projectPathKey -} - -// clearTerminalSessionSnapshot 清空 agent_id 的终端会话快照(该 Agent 会话更替时, -// 旧连接的终端进程已随桌面端断开失效)。 -func (m *Manager) clearTerminalSessionSnapshot(agentID string) { - entry := m.entryFor(agentID) - if entry == nil { - return - } - entry.terminalSessionsMu.Lock() - entry.terminalSessions = make(map[string]*gatewayv2.TerminalSession) - entry.terminalSessionsMu.Unlock() -} - -func (m *Manager) TerminalSessionSnapshot(agentID, projectPathKey string) []*gatewayv2.TerminalSession { - projectPathKey = strings.TrimSpace(projectPathKey) - entry := m.entryFor(agentID) - if entry == nil { - return nil - } - entry.terminalSessionsMu.Lock() - sessions := make([]*gatewayv2.TerminalSession, 0, len(entry.terminalSessions)) - for _, session := range entry.terminalSessions { - if !terminalSessionMatchesProject(session, projectPathKey) { - continue - } - if cloned := cloneTerminalSession(session); cloned != nil { - sessions = append(sessions, cloned) - } - } - entry.terminalSessionsMu.Unlock() - sortTerminalSessions(sessions) - return sessions -} - -func (m *Manager) replaceTerminalSessionSnapshot( - entry *agentEntry, - projectPathKey string, - sessions []*gatewayv2.TerminalSession, -) { - projectPathKey = strings.TrimSpace(projectPathKey) - entry.terminalSessionsMu.Lock() - if projectPathKey == "" { - entry.terminalSessions = make(map[string]*gatewayv2.TerminalSession) - } else { - for id, session := range entry.terminalSessions { - if terminalSessionMatchesProject(session, projectPathKey) { - delete(entry.terminalSessions, id) - } - } - } - for _, session := range sessions { - id := strings.TrimSpace(session.GetId()) - if id == "" { - continue - } - entry.terminalSessions[id] = cloneTerminalSession(session) - } - entry.terminalSessionsMu.Unlock() -} - -func (m *Manager) ApplyTerminalResponseSnapshot( - agentID string, - action string, - projectPathKey string, - resp *gatewayv2.TerminalResponse, -) { - if resp == nil { - return - } - action = strings.TrimSpace(action) - projectPathKey = strings.TrimSpace(projectPathKey) - entry := m.entryOrCreate(agentID) - if entry == nil { - return - } - - switch action { - case "list": - m.replaceTerminalSessionSnapshot(entry, projectPathKey, resp.GetSessions()) - case "close_project": - m.replaceTerminalSessionSnapshot(entry, projectPathKey, nil) - case "close": - if sessionID := strings.TrimSpace(resp.GetSession().GetId()); sessionID != "" { - entry.terminalSessionsMu.Lock() - delete(entry.terminalSessions, sessionID) - entry.terminalSessionsMu.Unlock() - } - case "create", "create_ssh", "answer_ssh_prompt", "attach", "snapshot", "input", "resize", "rename": - session := resp.GetSession() - sessionID := strings.TrimSpace(session.GetId()) - if sessionID == "" { - return - } - entry.terminalSessionsMu.Lock() - entry.terminalSessions[sessionID] = cloneTerminalSession(session) - entry.terminalSessionsMu.Unlock() - } -} - -func (m *Manager) applyTerminalEventSnapshot(entry *agentEntry, event *gatewayv2.TerminalEvent) { - kind := strings.TrimSpace(event.GetKind()) - sessionID := strings.TrimSpace(event.GetSessionId()) - if sessionID == "" && event.GetSession() != nil { - sessionID = strings.TrimSpace(event.GetSession().GetId()) - } - if sessionID == "" { - return - } - - entry.terminalSessionsMu.Lock() - if kind == "closed" { - delete(entry.terminalSessions, sessionID) - } else if session := cloneTerminalSession(event.GetSession()); session != nil { - entry.terminalSessions[sessionID] = session - } - entry.terminalSessionsMu.Unlock() -} - -func (m *Manager) broadcastTerminalEvent(agentID string, event *gatewayv2.TerminalEvent) { - if event == nil { - return - } - entry := m.entryOrCreate(agentID) - if entry == nil { - return - } - - m.applyTerminalEventSnapshot(entry, event) - - m.syncHub.terminalMu.Lock() - subscribers := make([]chan Tagged[*gatewayv2.TerminalEvent], 0, len(m.syncHub.terminalSubscribers)) - for _, ch := range m.syncHub.terminalSubscribers { - subscribers = append(subscribers, ch) - } - m.syncHub.terminalMu.Unlock() - - tagged := Tagged[*gatewayv2.TerminalEvent]{AgentID: agentID, Event: event} - for _, ch := range subscribers { - select { - case ch <- tagged: - case <-time.After(50 * time.Millisecond): - } - } -} diff --git a/crates/agent-gateway/internal/session/manager_test.go b/crates/agent-gateway/internal/session/manager_test.go deleted file mode 100644 index 9f80ad419..000000000 --- a/crates/agent-gateway/internal/session/manager_test.go +++ /dev/null @@ -1,341 +0,0 @@ -package session - -import ( - "context" - "errors" - "testing" - "time" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -func TestStatusBroadcastIdentifiesOnlineSessionReplacement(t *testing.T) { - manager := NewManager() - statuses, unsubscribe := manager.SubscribeStatus() - defer unsubscribe() - - first := NewAgentSession(AuthSnapshot{AgentID: "test-agent", SessionID: "session-1"}) - manager.SetSession(first) - firstStatus := (<-statuses).Event - if !firstStatus.Online || firstStatus.SessionID != "session-1" { - t.Fatalf("first status = %#v, want online session-1", firstStatus) - } - - second := NewAgentSession(AuthSnapshot{AgentID: "test-agent", SessionID: "session-2"}) - manager.SetSession(second) - t.Cleanup(func() { manager.ClearSession(second) }) - secondStatus := (<-statuses).Event - if !secondStatus.Online || secondStatus.SessionID != "session-2" { - t.Fatalf("replacement status = %#v, want online session-2", secondStatus) - } -} - -func TestRegisterStreamAndSendContextCorrelatesOnCapturedSession(t *testing.T) { - manager := NewManager() - sess := NewAgentSession(AuthSnapshot{AgentID: "test-agent", SessionID: "session-1"}) - manager.SetSession(sess) - t.Cleanup(func() { manager.ClearSession(sess) }) - - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - type registeredRequest struct { - responses <-chan *gatewayv2.AgentEnvelope - done <-chan struct{} - cleanup func() - err error - } - registered := make(chan registeredRequest, 1) - go func() { - responses, done, cleanup, err := manager.RegisterStreamAndSendContext( - ctx, - "test-agent", - "history-1", - &gatewayv2.GatewayEnvelope{ - RequestId: "history-1", - Payload: &gatewayv2.GatewayEnvelope_HistoryList{ - HistoryList: &gatewayv2.HistoryListRequest{Page: 1, PageSize: 80}, - }, - }, - ) - registered <- registeredRequest{responses: responses, done: done, cleanup: cleanup, err: err} - }() - - var outbound *OutboundEnvelope - select { - case outbound = <-sess.Outbound(): - case <-time.After(time.Second): - t.Fatal("timed out waiting for captured-session request") - } - if outbound.GetRequestId() != "history-1" || outbound.GetHistoryList() == nil { - t.Fatalf("outbound request = %#v", outbound.GatewayEnvelope) - } - outbound.Ack(nil) - - result := <-registered - if result.err != nil { - t.Fatalf("RegisterStreamAndSendContext: %v", result.err) - } - defer result.cleanup() - - manager.DispatchFromAgentForSession(sess, &gatewayv2.AgentEnvelope{ - RequestId: "history-1", - Payload: &gatewayv2.AgentEnvelope_HistoryListResp{ - HistoryListResp: &gatewayv2.HistoryListResponse{TotalCount: 1}, - }, - }) - select { - case response := <-result.responses: - if response.GetHistoryListResp().GetTotalCount() != 1 { - t.Fatalf("history response = %#v", response.GetHistoryListResp()) - } - case <-result.done: - t.Fatal("captured response stream closed before dispatch") - case <-time.After(time.Second): - t.Fatal("timed out waiting for correlated response") - } -} - -func TestRegisterStreamAndSendContextDoesNotCrossSessionReplacement(t *testing.T) { - manager := NewManager() - first := NewAgentSession(AuthSnapshot{AgentID: "test-agent", SessionID: "session-1"}) - manager.SetSession(first) - - // Saturate the captured session's outbound lane so register-and-send pauses - // after correlation is installed but before delivery can complete. - for i := 0; i < cap(first.toAgent); i += 1 { - sent, err := first.TrySendToAgent(&gatewayv2.GatewayEnvelope{RequestId: "queued"}) - if err != nil || !sent { - t.Fatalf("fill first session outbound at %d: sent=%v err=%v", i, sent, err) - } - } - - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - result := make(chan error, 1) - go func() { - _, _, _, err := manager.RegisterStreamAndSendContext( - ctx, - "test-agent", - "history-replacement", - &gatewayv2.GatewayEnvelope{ - RequestId: "history-replacement", - Payload: &gatewayv2.GatewayEnvelope_HistoryList{ - HistoryList: &gatewayv2.HistoryListRequest{Page: 1, PageSize: 80}, - }, - }, - ) - result <- err - }() - - deadline := time.Now().Add(time.Second) - for { - first.streamsMu.Lock() - _, registered := first.streams["history-replacement"] - first.streamsMu.Unlock() - if registered { - break - } - if time.Now().After(deadline) { - t.Fatal("request stream was not registered on the captured session") - } - time.Sleep(time.Millisecond) - } - - second := NewAgentSession(AuthSnapshot{AgentID: "test-agent", SessionID: "session-2"}) - manager.SetSession(second) - t.Cleanup(func() { manager.ClearSession(second) }) - if err := <-result; !errors.Is(err, ErrAgentOffline) { - t.Fatalf("register-and-send across replacement = %v, want ErrAgentOffline", err) - } - - select { - case outbound := <-second.Outbound(): - t.Fatalf("request crossed into replacement session: %#v", outbound.GatewayEnvelope) - default: - } -} - -func TestChatRuntimeProbeFreshnessIsBoundToSessionEpoch(t *testing.T) { - manager := NewManager() - first := NewAgentSession(AuthSnapshot{AgentID: "test-agent", SessionID: "session-1"}) - manager.SetSession(first) - - firstEpoch, online := manager.ChatRuntimeProbeEpoch("test-agent") - if !online || firstEpoch == 0 { - t.Fatalf("first probe epoch = %d online=%v", firstEpoch, online) - } - if !manager.RecordChatRuntimeProbe("test-agent", firstEpoch) || - !manager.ChatRuntimeProbeFresh("test-agent", time.Second) { - t.Fatal("recorded probe should be fresh for the current session") - } - - second := NewAgentSession(AuthSnapshot{AgentID: "test-agent", SessionID: "session-2"}) - manager.SetSession(second) - t.Cleanup(func() { manager.ClearSession(second) }) - if manager.ChatRuntimeProbeFresh("test-agent", time.Second) { - t.Fatal("replacing the agent session must invalidate probe freshness") - } - if manager.RecordChatRuntimeProbe("test-agent", firstEpoch) { - t.Fatal("an old session epoch must not mark the replacement session fresh") - } - - secondEpoch, online := manager.ChatRuntimeProbeEpoch("test-agent") - if !online || secondEpoch == firstEpoch || !manager.RecordChatRuntimeProbe("test-agent", secondEpoch) { - t.Fatalf("replacement probe epoch = %d online=%v", secondEpoch, online) - } -} - -func TestApplySettingsJSONPreservingRemoteKeepsDesktopTerminalSetting(t *testing.T) { - manager := NewManager() - manager.ApplySettingsJSON("test-agent", `{"remote":{"enableWebTerminal":true,"enableWebSshTerminal":true},"theme":"dark"}`) - if !manager.WebTerminalEnabled("test-agent") { - t.Fatal("expected desktop settings sync to enable web terminal") - } - if !manager.WebSshTerminalEnabled("test-agent") { - t.Fatal("expected desktop settings sync to enable web SSH terminal") - } - - manager.ApplySettingsJSONPreservingRemote("test-agent", `{"remote":{"enableWebTerminal":false,"enableWebSshTerminal":false},"theme":"light"}`) - if !manager.WebTerminalEnabled("test-agent") { - t.Fatal("settings.update must not disable the desktop-owned web terminal setting") - } - if !manager.WebSshTerminalEnabled("test-agent") { - t.Fatal("settings.update must not disable the desktop-owned web SSH terminal setting") - } -} - -func TestApplySettingsJSONKeepsRemoteWhenPublicSettingsEventOmitsIt(t *testing.T) { - manager := NewManager() - manager.ApplySettingsJSON("test-agent", `{"remote":{"enableWebTerminal":true,"enableWebSshTerminal":true},"theme":"dark"}`) - if !manager.WebTerminalEnabled("test-agent") { - t.Fatal("expected desktop settings sync to enable web terminal") - } - if !manager.WebSshTerminalEnabled("test-agent") { - t.Fatal("expected desktop settings sync to enable web SSH terminal") - } - - manager.ApplySettingsJSON("test-agent", `{"theme":"light"}`) - if !manager.WebTerminalEnabled("test-agent") { - t.Fatal("public settings events without remote must not clear the desktop web terminal setting") - } - if !manager.WebSshTerminalEnabled("test-agent") { - t.Fatal("public settings events without remote must not clear the desktop web SSH terminal setting") - } -} - -func TestApplySettingsJSONPreservingRemoteDoesNotTrustIncomingRemote(t *testing.T) { - manager := NewManager() - manager.ApplySettingsJSONPreservingRemote("test-agent", `{"remote":{"enableWebTerminal":true,"enableWebSshTerminal":true}}`) - if manager.WebTerminalEnabled("test-agent") { - t.Fatal("settings.update must not enable web terminal without a desktop settings snapshot") - } - if manager.WebSshTerminalEnabled("test-agent") { - t.Fatal("settings.update must not enable web SSH terminal without a desktop settings snapshot") - } -} - -func TestTerminalSessionSnapshotPreservesSshMetadataAndSorts(t *testing.T) { - manager := NewManager() - manager.replaceTerminalSessionSnapshot(manager.entryOrCreate("test-agent"), "", []*gatewayv2.TerminalSession{ - { - Id: "ssh-2", - ProjectPathKey: "/workspace/b", - Cwd: "/workspace/b", - Shell: "ssh", - Title: "Production 2", - Kind: "ssh", - CreatedAt: 2, - UpdatedAt: 2, - Running: true, - Ssh: &gatewayv2.TerminalSshMetadata{ - HostId: "prod-2", - HostName: "Production 2", - Username: "deploy", - Host: "prod-2.example.com", - Port: 22, - AuthType: "privateKey", - }, - }, - { - Id: "local-1", - ProjectPathKey: "/workspace/a", - Cwd: "/workspace/a", - Shell: "zsh", - Title: "Local", - Kind: "local", - CreatedAt: 2, - UpdatedAt: 2, - Running: true, - }, - { - Id: "ssh-1", - ProjectPathKey: "/workspace/a", - Cwd: "/workspace/a", - Shell: "ssh", - Title: "Production", - Kind: "ssh", - CreatedAt: 1, - UpdatedAt: 1, - Running: true, - Ssh: &gatewayv2.TerminalSshMetadata{ - HostId: "prod", - HostName: "Production", - Username: "deploy", - Host: "prod.example.com", - Port: 22, - AuthType: "password", - }, - }, - }) - - sessions := manager.TerminalSessionSnapshot("test-agent", "") - if len(sessions) != 3 { - t.Fatalf("terminal sessions = %d, want 3", len(sessions)) - } - if got := []string{sessions[0].GetId(), sessions[1].GetId(), sessions[2].GetId()}; got[0] != "ssh-1" || got[1] != "local-1" || got[2] != "ssh-2" { - t.Fatalf("terminal session order = %#v", got) - } - if manager.TerminalSessionKind("test-agent", "ssh-1") != "ssh" { - t.Fatalf("TerminalSessionKind(ssh-1) = %q, want ssh", manager.TerminalSessionKind("test-agent", "ssh-1")) - } - if sessions[0].GetSsh().GetHostId() != "prod" || sessions[0].GetSsh().GetAuthType() != "password" { - t.Fatalf("ssh metadata = %#v", sessions[0].GetSsh()) - } - - sessions[0].Ssh.HostId = "mutated" - fresh := manager.TerminalSessionSnapshot("test-agent", "/workspace/a") - if len(fresh) != 2 { - t.Fatalf("filtered terminal sessions = %d, want 2", len(fresh)) - } - if fresh[0].GetSsh().GetHostId() != "prod" { - t.Fatalf("terminal snapshot should be immutable, got ssh host id %q", fresh[0].GetSsh().GetHostId()) - } -} - -func TestActiveConversationActivitiesTracksRunLifecycle(t *testing.T) { - manager := NewManager() - - manager.StartChatCommand("test-agent", "run-1", "conv-1", "/workspace", "client-1", nil) - manager.ingestChatControl("test-agent", "run-1", &gatewayv2.ChatControlEvent{ - RequestId: "run-1", - ConversationId: "conv-1", - Type: "started", - State: "running", - }) - - activities := manager.ActiveConversationActivities() - if len(activities) != 1 || activities[0].RunID != "run-1" || activities[0].State != RunActivityRunning { - t.Fatalf("activities = %#v, want running run-1", activities) - } - - manager.ingestChatControl("test-agent", "run-1", &gatewayv2.ChatControlEvent{ - RequestId: "run-1", - ConversationId: "conv-1", - Type: "completed", - State: "completed", - }) - - if activities := manager.ActiveConversationActivities(); len(activities) != 0 { - t.Fatalf("completed run should not appear in activities, got %#v", activities) - } -} diff --git a/crates/agent-gateway/internal/session/status_broadcast.go b/crates/agent-gateway/internal/session/status_broadcast.go deleted file mode 100644 index ad21d99eb..000000000 --- a/crates/agent-gateway/internal/session/status_broadcast.go +++ /dev/null @@ -1,61 +0,0 @@ -package session - -import "sync" - -// statusSubscriberHub fans out agent Status snapshots to /ws connections so -// clients learn about agent connect/disconnect by push instead of polling. -type statusSubscriberHub struct { - mu sync.Mutex - nextSubID uint64 - subscribers map[uint64]chan Tagged[Status] -} - -func newStatusSubscriberHub() *statusSubscriberHub { - return &statusSubscriberHub{ - subscribers: make(map[uint64]chan Tagged[Status]), - } -} - -func (m *Manager) SubscribeStatus() (<-chan Tagged[Status], func()) { - ch := make(chan Tagged[Status], 8) - - m.statusSubs.mu.Lock() - subID := m.statusSubs.nextSubID - m.statusSubs.nextSubID += 1 - m.statusSubs.subscribers[subID] = ch - m.statusSubs.mu.Unlock() - - cleanup := func() { - m.statusSubs.mu.Lock() - // Do not close the channel: broadcastStatus sends after copying - // subscribers, so closing can race with an in-flight send. - delete(m.statusSubs.subscribers, subID) - m.statusSubs.mu.Unlock() - } - return ch, cleanup -} - -// broadcastStatus pushes agentID's status snapshot to /ws subscribers. -// Sends are non-blocking: a stalled subscriber misses intermediate snapshots -// and reconciles from its fallback status poll. -func (m *Manager) broadcastStatus(agentID string) { - snapshot := m.Status(agentID) - if snapshot.AgentID == "" { - return - } - - m.statusSubs.mu.Lock() - subscribers := make([]chan Tagged[Status], 0, len(m.statusSubs.subscribers)) - for _, ch := range m.statusSubs.subscribers { - subscribers = append(subscribers, ch) - } - m.statusSubs.mu.Unlock() - - tagged := Tagged[Status]{AgentID: agentID, Event: snapshot} - for _, ch := range subscribers { - select { - case ch <- tagged: - default: - } - } -} diff --git a/crates/agent-gateway/internal/session/tunnel_state.go b/crates/agent-gateway/internal/session/tunnel_state.go deleted file mode 100644 index 214b96966..000000000 --- a/crates/agent-gateway/internal/session/tunnel_state.go +++ /dev/null @@ -1,734 +0,0 @@ -package session - -import ( - "context" - "crypto/rand" - "encoding/base64" - "fmt" - "regexp" - "sort" - "strings" - "sync" - "time" - - "github.com/google/uuid" - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -const ( - maxTunnelsPerAgent = 5 - maxTunnelConnections = 20 - tunnelSlugEntropyBytes = 24 - tunnelStreamChannelDepth = 256 - tunnelAgentSendTimeout = 10 * time.Second - tunnelRelayProbeTimeout = 5 * time.Second - tunnelExpirySweepPeriod = 30 * time.Second -) - -var tunnelSlugPattern = regexp.MustCompile(`^[A-Za-z0-9_-]{22,64}$`) - -// tunnelRuntime is the gateway-side runtime view of the agent's desired -// tunnel set: slug allocation, live streams, connection counts, and health. -// The desired specs themselves are owned and persisted by the agent. -type tunnelRuntime struct { - mu sync.Mutex - records map[string]*tunnelRecord - slugToID map[string]string - streams map[string]*tunnelStream - revisions map[string]uint64 - relays map[string]*gatewayv2.TunnelHealth - - subMu sync.Mutex - nextSubID int - subscribers map[int]chan Tagged[*gatewayv2.TunnelStateSnapshot] - - pingMu sync.Mutex - pendingPings map[string]pendingTunnelPing -} - -type tunnelRecord struct { - id string - agentID string - slug string - name string - targetURL string - projectPathKey string - createdAt time.Time - expiresAt time.Time - activeConnections int - local *gatewayv2.TunnelHealth -} - -type pendingTunnelPing struct { - agentID string - ch chan int64 -} - -type tunnelStream struct { - streamID string - tunnelID string - // agentID 是流所属 record 的归属 Agent;入站帧按它校验,防止 Agent A - // 伪造 stream_id 向 Agent B 的访问者注入数据。 - agentID string - ch chan *gatewayv2.TunnelFrame - done chan struct{} - once sync.Once -} - -// TunnelStreamLease is one visitor connection's claim on a tunnel. -type TunnelStreamLease struct { - manager *Manager - stream *tunnelStream - slug string - targetURL string - agentID string - once sync.Once -} - -func newTunnelRuntime() *tunnelRuntime { - return &tunnelRuntime{ - records: make(map[string]*tunnelRecord), - slugToID: make(map[string]string), - streams: make(map[string]*tunnelStream), - revisions: make(map[string]uint64), - relays: make(map[string]*gatewayv2.TunnelHealth), - subscribers: make(map[int]chan Tagged[*gatewayv2.TunnelStateSnapshot]), - pendingPings: make(map[string]pendingTunnelPing), - } -} - -func (s *tunnelStream) close() { - if s == nil { - return - } - s.once.Do(func() { - close(s.done) - }) -} - -func (l *TunnelStreamLease) TunnelID() string { - if l == nil || l.stream == nil { - return "" - } - return l.stream.tunnelID -} - -func (l *TunnelStreamLease) Slug() string { - if l == nil { - return "" - } - return l.slug -} - -func (l *TunnelStreamLease) TargetURL() string { - if l == nil { - return "" - } - return l.targetURL -} - -func (l *TunnelStreamLease) StreamID() string { - if l == nil || l.stream == nil { - return "" - } - return l.stream.streamID -} - -func (l *TunnelStreamLease) Frames() <-chan *gatewayv2.TunnelFrame { - if l == nil || l.stream == nil { - return nil - } - return l.stream.ch -} - -func (l *TunnelStreamLease) Done() <-chan struct{} { - if l == nil || l.stream == nil { - return nil - } - return l.stream.done -} - -// AgentID 返回租约所属隧道的归属 Agent(访问者帧的路由目标)。 -func (l *TunnelStreamLease) AgentID() string { - if l == nil { - return "" - } - return l.agentID -} - -func (l *TunnelStreamLease) Release() { - if l == nil { - return - } - l.once.Do(func() { - l.manager.releaseTunnelStream(l.stream) - }) -} - -func (m *Manager) WebTunnelsEnabled(agentID string) bool { - return m.settingsRemoteBool(agentID, "enableWebTunnels") -} - -// ApplyDesiredState reconciles agentID's runtime records against that agent's -// full desired tunnel set: allocates slugs for new tunnels (honoring valid -// unused hints), updates changed ones, and drops removed ones (canceling -// their streams). Records of other agents are untouched; the per-agent cap -// applies to each agent's own set. -func (m *Manager) ApplyDesiredState(agentID string, desired *gatewayv2.TunnelDesiredState) { - agentID = strings.TrimSpace(agentID) - if agentID == "" || desired == nil { - return - } - now := time.Now() - specs := desired.GetTunnels() - if len(specs) > maxTunnelsPerAgent { - specs = specs[:maxTunnelsPerAgent] - } - - var canceled []*tunnelStream - m.tunnels.mu.Lock() - seen := make(map[string]bool, len(specs)) - for _, spec := range specs { - id := strings.TrimSpace(spec.GetId()) - targetURL := strings.TrimSpace(spec.GetTargetUrl()) - if id == "" || targetURL == "" || seen[id] { - continue - } - expiresAt := time.Time{} - if spec.GetExpiresAt() > 0 { - expiresAt = time.Unix(spec.GetExpiresAt(), 0) - if !expiresAt.After(now) { - continue - } - } - seen[id] = true - record := m.tunnels.records[id] - if record != nil && record.agentID != agentID { - // 隧道 id 撞上他人的 record:拒绝接管(id 是 Agent 本地生成的, - // 跨 Agent 撞车只能来自伪造或配置复制),保持原归属不变。 - continue - } - if record == nil { - record = &tunnelRecord{ - id: id, - agentID: agentID, - slug: m.allocateTunnelSlugLocked(spec.GetSlugHint()), - createdAt: now, - } - m.tunnels.records[id] = record - m.tunnels.slugToID[record.slug] = id - } - record.name = strings.TrimSpace(spec.GetName()) - record.targetURL = targetURL - record.projectPathKey = strings.TrimSpace(spec.GetProjectPathKey()) - record.expiresAt = expiresAt - } - for id, record := range m.tunnels.records { - if seen[id] || record.agentID != agentID { - continue - } - canceled = append(canceled, m.dropTunnelRecordLocked(record)...) - } - m.tunnels.mu.Unlock() - - m.cancelTunnelStreams(canceled) - m.broadcastTunnelState(agentID) - go m.probeRelay(agentID) -} - -// ApplyProbeReport merges one authenticated Agent report into only that Agent records. -func (m *Manager) ApplyProbeReport(agentID string, report *gatewayv2.TunnelProbeReport) { - agentID = strings.TrimSpace(agentID) - if agentID == "" || report == nil || len(report.GetResults()) == 0 { - return - } - changed := false - m.tunnels.mu.Lock() - for _, result := range report.GetResults() { - record := m.tunnels.records[strings.TrimSpace(result.GetTunnelId())] - if record == nil || record.agentID != agentID || result.GetLocal() == nil { - continue - } - record.local = cloneTunnelHealth(result.GetLocal()) - changed = true - } - m.tunnels.mu.Unlock() - if changed { - m.broadcastTunnelState(agentID) - } -} - -// dropTunnelRecordLocked removes a record and returns its now-closed streams -// so CANCEL frames can be sent to the agent outside the lock. -func (m *Manager) dropTunnelRecordLocked(record *tunnelRecord) []*tunnelStream { - if record == nil { - return nil - } - delete(m.tunnels.records, record.id) - delete(m.tunnels.slugToID, record.slug) - var dropped []*tunnelStream - for streamID, stream := range m.tunnels.streams { - if stream == nil || stream.tunnelID != record.id { - continue - } - delete(m.tunnels.streams, streamID) - stream.close() - dropped = append(dropped, stream) - } - return dropped -} - -// cancelTunnelStreams 向各流的归属 Agent 发送 CANCEL(过期清扫可能跨多个 Agent)。 -func (m *Manager) cancelTunnelStreams(streams []*tunnelStream) { - for _, stream := range streams { - _ = m.SendTunnelFrameToAgent(stream.agentID, &gatewayv2.TunnelFrame{ - StreamId: stream.streamID, - Kind: gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_CANCEL, - }) - } -} - -func (m *Manager) allocateTunnelSlugLocked(hint string) string { - hint = strings.TrimSpace(hint) - if tunnelSlugPattern.MatchString(hint) { - if _, taken := m.tunnels.slugToID[hint]; !taken { - return hint - } - } - for { - slug := randomURLToken(tunnelSlugEntropyBytes) - if slug == "" { - // crypto/rand failure; fall back to a UUID-derived token. - slug = strings.ReplaceAll(uuid.NewString(), "-", "") - } - if _, taken := m.tunnels.slugToID[slug]; !taken { - return slug - } - } -} - -func randomURLToken(byteCount int) string { - if byteCount <= 0 { - return "" - } - buf := make([]byte, byteCount) - if _, err := rand.Read(buf); err != nil { - return "" - } - return base64.RawURLEncoding.EncodeToString(buf) -} - -// TunnelStateSnapshot builds the authoritative state for one named Agent. -func (m *Manager) TunnelStateSnapshot(agentID string) *gatewayv2.TunnelStateSnapshot { - agentID = strings.TrimSpace(agentID) - if agentID == "" { - return &gatewayv2.TunnelStateSnapshot{} - } - online := m.IsOnline(agentID) - m.tunnels.mu.Lock() - defer m.tunnels.mu.Unlock() - return m.tunnelStateSnapshotLocked(agentID, online) -} - -func (m *Manager) tunnelStateSnapshotLocked(agentID string, online bool) *gatewayv2.TunnelStateSnapshot { - tunnels := make([]*gatewayv2.TunnelStatus, 0, len(m.tunnels.records)) - for _, record := range m.tunnels.records { - if record.agentID != agentID { - continue - } - tunnels = append(tunnels, &gatewayv2.TunnelStatus{ - Id: record.id, - Slug: record.slug, - Name: record.name, - TargetUrl: record.targetURL, - PublicPath: "/t/" + record.slug + "/", - CreatedAt: record.createdAt.Unix(), - ExpiresAt: unixOrZero(record.expiresAt), - ActiveConnections: uint32(max(record.activeConnections, 0)), - ProjectPathKey: record.projectPathKey, - Local: cloneTunnelHealth(record.local), - }) - } - sort.Slice(tunnels, func(i, j int) bool { - if tunnels[i].GetCreatedAt() != tunnels[j].GetCreatedAt() { - return tunnels[i].GetCreatedAt() < tunnels[j].GetCreatedAt() - } - return tunnels[i].GetId() < tunnels[j].GetId() - }) - m.tunnels.revisions[agentID] += 1 - return &gatewayv2.TunnelStateSnapshot{ - Tunnels: tunnels, - Revision: m.tunnels.revisions[agentID], - AgentOnline: online, - Relay: cloneTunnelHealth(m.tunnels.relays[agentID]), - } -} - -func (m *Manager) SubscribeTunnelState() (<-chan Tagged[*gatewayv2.TunnelStateSnapshot], func()) { - ch := make(chan Tagged[*gatewayv2.TunnelStateSnapshot], 16) - - m.tunnels.subMu.Lock() - subID := m.tunnels.nextSubID - m.tunnels.nextSubID += 1 - m.tunnels.subscribers[subID] = ch - m.tunnels.subMu.Unlock() - - cleanup := func() { - m.tunnels.subMu.Lock() - // Do not close the channel: broadcastTunnelState sends after copying - // subscribers, so closing can race with an in-flight send. - delete(m.tunnels.subscribers, subID) - m.tunnels.subMu.Unlock() - } - return ch, cleanup -} - -// broadcastTunnelState pushes one Agent snapshot to /ws subscribers and back -// to the same Agent, which persists allocated slugs and re-emits it to the GUI. -func (m *Manager) broadcastTunnelState(agentID string) { - agentID = strings.TrimSpace(agentID) - if agentID == "" { - return - } - snapshot := m.TunnelStateSnapshot(agentID) - tagged := Tagged[*gatewayv2.TunnelStateSnapshot]{AgentID: agentID, Event: snapshot} - - m.tunnels.subMu.Lock() - subscribers := make([]chan Tagged[*gatewayv2.TunnelStateSnapshot], 0, len(m.tunnels.subscribers)) - for _, ch := range m.tunnels.subscribers { - subscribers = append(subscribers, ch) - } - m.tunnels.subMu.Unlock() - - for _, ch := range subscribers { - select { - case ch <- tagged: - default: - } - } - - // Best-effort and non-blocking. A fresher snapshot follows every state change. - if session, err := m.resolveSession(agentID); err == nil { - _, _ = session.TrySendToAgent(&gatewayv2.GatewayEnvelope{ - RequestId: "tunnel-state-" + uuid.NewString(), - Timestamp: time.Now().Unix(), - Payload: &gatewayv2.GatewayEnvelope_TunnelState{ - TunnelState: snapshot, - }, - }) - } -} - -// AcquireTunnel claims a visitor stream slot on the tunnel behind slug. -// The lease is bound to the tunnel's owning agent; visitor frames route there. -func (m *Manager) AcquireTunnel(slug string, streamID string) (*TunnelStreamLease, error) { - slug = strings.TrimSpace(slug) - streamID = strings.TrimSpace(streamID) - if slug == "" || streamID == "" { - return nil, ErrTunnelNotFound - } - now := time.Now() - - m.tunnels.mu.Lock() - defer m.tunnels.mu.Unlock() - - record := m.tunnels.records[m.tunnels.slugToID[slug]] - if record == nil { - return nil, ErrTunnelNotFound - } - // 在线判定按 record 归属 Agent,与其他 Agent 的状态无关。 - if !m.IsOnline(record.agentID) { - return nil, ErrAgentOffline - } - if !record.expiresAt.IsZero() && !record.expiresAt.After(now) { - return nil, ErrTunnelExpired - } - if record.activeConnections >= maxTunnelConnections { - return nil, ErrTunnelOverLimit - } - stream := &tunnelStream{ - streamID: streamID, - tunnelID: record.id, - agentID: record.agentID, - ch: make(chan *gatewayv2.TunnelFrame, tunnelStreamChannelDepth), - done: make(chan struct{}), - } - if existing := m.tunnels.streams[streamID]; existing != nil { - existing.close() - } - m.tunnels.streams[streamID] = stream - record.activeConnections += 1 - - return &TunnelStreamLease{ - manager: m, - stream: stream, - slug: record.slug, - targetURL: record.targetURL, - agentID: record.agentID, - }, nil -} - -func (m *Manager) releaseTunnelStream(stream *tunnelStream) { - if stream == nil { - return - } - m.tunnels.mu.Lock() - if existing := m.tunnels.streams[stream.streamID]; existing == stream { - delete(m.tunnels.streams, stream.streamID) - } - if record := m.tunnels.records[stream.tunnelID]; record != nil && record.activeConnections > 0 { - record.activeConnections -= 1 - } - stream.close() - m.tunnels.mu.Unlock() -} - -// SendTunnelFrameToAgent 把访问者帧送往目标 Agent。 -func (m *Manager) SendTunnelFrameToAgent(agentID string, frame *gatewayv2.TunnelFrame) error { - if frame == nil { - return fmt.Errorf("tunnel frame is required") - } - ctx, cancel := context.WithTimeout(context.Background(), tunnelAgentSendTimeout) - defer cancel() - return m.SendToAgentContext(ctx, agentID, &gatewayv2.GatewayEnvelope{ - RequestId: "tunnel-frame-" + uuid.NewString(), - Timestamp: time.Now().Unix(), - Payload: &gatewayv2.GatewayEnvelope_TunnelFrame{ - TunnelFrame: frame, - }, - }) -} - -// dispatchTunnelFrame routes an agent frame to its visitor stream. It runs on -// the agent read loop, so it must never block: a full stream channel closes -// the stream (the visitor handler cancels) instead of waiting. Frames whose -// stream belongs to a different agent are rejected — an agent can only feed -// its own visitors. -func (m *Manager) dispatchTunnelFrame(agentID string, frame *gatewayv2.TunnelFrame) { - agentID = strings.TrimSpace(agentID) - if agentID == "" || frame == nil { - return - } - if frame.GetKind() == gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_PONG { - m.resolveRelayPong(agentID, frame.GetStreamId()) - return - } - streamID := strings.TrimSpace(frame.GetStreamId()) - if streamID == "" { - return - } - m.tunnels.mu.Lock() - stream := m.tunnels.streams[streamID] - m.tunnels.mu.Unlock() - if stream == nil { - return - } - if stream.agentID != agentID { - // 跨 Agent 伪造 stream_id:直接丢弃,不给探测反馈。 - return - } - select { - case <-stream.done: - case stream.ch <- frame: - default: - m.releaseTunnelStream(stream) - go func() { - _ = m.SendTunnelFrameToAgent(agentID, &gatewayv2.TunnelFrame{ - StreamId: streamID, - Kind: gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_CANCEL, - Error: "tunnel stream backlog exceeded", - }) - }() - } -} - -// probeRelay measures the gateway<->agent frame path with a PING/PONG round -// trip and folds the result into the broadcast snapshot. -func (m *Manager) probeRelay(agentID string) { - checkedAt := time.Now() - health := &gatewayv2.TunnelHealth{Status: "failed", CheckedAt: checkedAt.Unix()} - - if !m.IsOnline(agentID) { - health.Error = "agent offline" - m.setRelayHealth(agentID, health) - return - } - - pingID := "ping-" + uuid.NewString() - pongCh := make(chan int64, 1) - m.tunnels.pingMu.Lock() - m.tunnels.pendingPings[pingID] = pendingTunnelPing{agentID: agentID, ch: pongCh} - m.tunnels.pingMu.Unlock() - defer func() { - m.tunnels.pingMu.Lock() - delete(m.tunnels.pendingPings, pingID) - m.tunnels.pingMu.Unlock() - }() - - if err := m.SendTunnelFrameToAgent(agentID, &gatewayv2.TunnelFrame{ - StreamId: pingID, - Kind: gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_PING, - }); err != nil { - health.Error = err.Error() - m.setRelayHealth(agentID, health) - return - } - - timer := time.NewTimer(tunnelRelayProbeTimeout) - defer timer.Stop() - select { - case <-pongCh: - health.Status = "ok" - health.RttMs = uint32(min(time.Since(checkedAt).Milliseconds(), int64(^uint32(0)))) - case <-timer.C: - health.Error = "relay probe timed out" - } - m.setRelayHealth(agentID, health) -} - -func (m *Manager) resolveRelayPong(agentID, streamID string) { - streamID = strings.TrimSpace(streamID) - m.tunnels.pingMu.Lock() - pending, ok := m.tunnels.pendingPings[streamID] - if ok && pending.agentID == strings.TrimSpace(agentID) { - delete(m.tunnels.pendingPings, streamID) - } else { - ok = false - } - m.tunnels.pingMu.Unlock() - if ok { - select { - case pending.ch <- time.Now().UnixMilli(): - default: - } - } -} - -func (m *Manager) setRelayHealth(agentID string, health *gatewayv2.TunnelHealth) { - agentID = strings.TrimSpace(agentID) - if agentID == "" { - return - } - // 永久删除后,删除前已发出的异步探测可能迟到;不存在的登记项不得重新 - // 写回 relay 状态。普通离线仍保留 registry entry,因此不受影响。 - m.registry.mu.RLock() - _, registered := m.registry.agents[agentID] - m.registry.mu.RUnlock() - if !registered { - return - } - m.tunnels.mu.Lock() - m.tunnels.relays[agentID] = health - m.tunnels.mu.Unlock() - m.broadcastTunnelState(agentID) -} - -// onAgentSessionCleared drops agentID's live visitor streams (their frames can -// no longer be relayed) and pushes an offline snapshot; the specs stay so -// `/t/*` answers 503 instead of 404 and clients keep rendering the tunnels as -// offline. Streams and tunnels of other agents are untouched. -func (m *Manager) onAgentSessionCleared(agentID string) { - agentID = strings.TrimSpace(agentID) - m.tunnels.mu.Lock() - for streamID, stream := range m.tunnels.streams { - if stream.agentID != agentID { - continue - } - delete(m.tunnels.streams, streamID) - if record := m.tunnels.records[stream.tunnelID]; record != nil && record.activeConnections > 0 { - record.activeConnections -= 1 - } - stream.close() - } - delete(m.tunnels.relays, agentID) - m.tunnels.mu.Unlock() - m.broadcastTunnelState(agentID) - // Managed-process subscribers re-render with agent_online=false. - m.rebroadcastManagedProcessState(agentID) - // /ws clients learn the agent went offline by push, not by poll. - m.broadcastStatus(agentID) -} - -// purgeAgentTunnels 仅用于永久删除 Agent:普通断线继续保留 specs,删除则同步 -// 移除公开 slug、记录、访问流、探测状态和 relay 状态,使旧 /t/* 立即变为 404。 -func (m *Manager) purgeAgentTunnels(agentID string) { - agentID = strings.TrimSpace(agentID) - if agentID == "" { - return - } - var canceled []*tunnelStream - m.tunnels.mu.Lock() - for _, record := range m.tunnels.records { - if record.agentID == agentID { - canceled = append(canceled, m.dropTunnelRecordLocked(record)...) - } - } - delete(m.tunnels.relays, agentID) - m.tunnels.mu.Unlock() - - m.tunnels.pingMu.Lock() - for pingID, pending := range m.tunnels.pendingPings { - if pending.agentID != agentID { - continue - } - delete(m.tunnels.pendingPings, pingID) - select { - case pending.ch <- 0: - default: - } - } - m.tunnels.pingMu.Unlock() - - m.cancelTunnelStreams(canceled) - m.broadcastTunnelState(agentID) -} - -func (m *Manager) tunnelExpirySweepLoop() { - ticker := time.NewTicker(tunnelExpirySweepPeriod) - defer ticker.Stop() - for range ticker.C { - m.sweepExpiredTunnels(time.Now()) - } -} - -func (m *Manager) sweepExpiredTunnels(now time.Time) { - var canceled []*tunnelStream - affectedAgentIDs := make(map[string]struct{}) - m.tunnels.mu.Lock() - for _, record := range m.tunnels.records { - if record.expiresAt.IsZero() || record.expiresAt.After(now) { - continue - } - affectedAgentIDs[record.agentID] = struct{}{} - canceled = append(canceled, m.dropTunnelRecordLocked(record)...) - } - m.tunnels.mu.Unlock() - - if len(affectedAgentIDs) == 0 { - return - } - m.cancelTunnelStreams(canceled) - for agentID := range affectedAgentIDs { - m.broadcastTunnelState(agentID) - } -} - -func cloneTunnelHealth(health *gatewayv2.TunnelHealth) *gatewayv2.TunnelHealth { - if health == nil { - return nil - } - return &gatewayv2.TunnelHealth{ - Status: health.GetStatus(), - HttpStatus: health.GetHttpStatus(), - Error: health.GetError(), - CheckedAt: health.GetCheckedAt(), - RttMs: health.GetRttMs(), - } -} - -func unixOrZero(value time.Time) int64 { - if value.IsZero() { - return 0 - } - return value.Unix() -} diff --git a/crates/agent-gateway/internal/session/tunnel_state_test.go b/crates/agent-gateway/internal/session/tunnel_state_test.go deleted file mode 100644 index 67166128e..000000000 --- a/crates/agent-gateway/internal/session/tunnel_state_test.go +++ /dev/null @@ -1,304 +0,0 @@ -package session - -import ( - "strings" - "testing" - "time" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -func newTunnelTestManager(t *testing.T) *Manager { - t.Helper() - m := NewManager() - m.SetSession(NewAgentSession(AuthSnapshot{AgentID: "test-agent"})) - return m -} - -func desiredState(specs ...*gatewayv2.TunnelSpec) *gatewayv2.TunnelDesiredState { - return &gatewayv2.TunnelDesiredState{Tunnels: specs} -} - -func findTunnelStatus(snapshot *gatewayv2.TunnelStateSnapshot, id string) *gatewayv2.TunnelStatus { - for _, tunnel := range snapshot.GetTunnels() { - if tunnel.GetId() == id { - return tunnel - } - } - return nil -} - -func TestApplyDesiredStateAddUpdateRemove(t *testing.T) { - m := newTunnelTestManager(t) - - m.ApplyDesiredState("test-agent", desiredState( - &gatewayv2.TunnelSpec{Id: "tun-a", TargetUrl: "http://localhost:3000", Name: "a"}, - &gatewayv2.TunnelSpec{Id: "tun-b", TargetUrl: "http://localhost:4000"}, - )) - snapshot := m.TunnelStateSnapshot("test-agent") - if len(snapshot.GetTunnels()) != 2 { - t.Fatalf("tunnels = %d, want 2", len(snapshot.GetTunnels())) - } - statusA := findTunnelStatus(snapshot, "tun-a") - if statusA == nil || statusA.GetSlug() == "" { - t.Fatalf("tun-a missing or has no slug: %#v", statusA) - } - if statusA.GetPublicPath() != "/t/"+statusA.GetSlug()+"/" { - t.Fatalf("public path = %q", statusA.GetPublicPath()) - } - slugA := statusA.GetSlug() - - // Update keeps the allocated slug; removal drops the record. - m.ApplyDesiredState("test-agent", desiredState( - &gatewayv2.TunnelSpec{Id: "tun-a", TargetUrl: "http://localhost:3001", Name: "renamed"}, - )) - snapshot = m.TunnelStateSnapshot("test-agent") - if len(snapshot.GetTunnels()) != 1 { - t.Fatalf("tunnels after removal = %d, want 1", len(snapshot.GetTunnels())) - } - statusA = findTunnelStatus(snapshot, "tun-a") - if statusA.GetSlug() != slugA { - t.Fatalf("slug changed across update: %q -> %q", slugA, statusA.GetSlug()) - } - if statusA.GetTargetUrl() != "http://localhost:3001" || statusA.GetName() != "renamed" { - t.Fatalf("update not applied: %#v", statusA) - } -} - -func TestApplyDesiredStateHonorsSlugHintAndCollision(t *testing.T) { - m := newTunnelTestManager(t) - hint := strings.Repeat("a", 32) - - m.ApplyDesiredState("test-agent", desiredState( - &gatewayv2.TunnelSpec{Id: "tun-a", TargetUrl: "http://localhost:3000", SlugHint: hint}, - &gatewayv2.TunnelSpec{Id: "tun-b", TargetUrl: "http://localhost:4000", SlugHint: hint}, - )) - snapshot := m.TunnelStateSnapshot("test-agent") - statusA := findTunnelStatus(snapshot, "tun-a") - statusB := findTunnelStatus(snapshot, "tun-b") - if statusA.GetSlug() != hint { - t.Fatalf("tun-a slug = %q, want hint %q", statusA.GetSlug(), hint) - } - if statusB.GetSlug() == hint || statusB.GetSlug() == "" { - t.Fatalf("tun-b slug should be freshly allocated, got %q", statusB.GetSlug()) - } - - // Invalid hints are ignored. - m.ApplyDesiredState("test-agent", desiredState( - &gatewayv2.TunnelSpec{Id: "tun-c", TargetUrl: "http://localhost:5000", SlugHint: "short"}, - )) - statusC := findTunnelStatus(m.TunnelStateSnapshot("test-agent"), "tun-c") - if statusC.GetSlug() == "short" { - t.Fatal("invalid slug hint must not be honored") - } -} - -func TestApplyDesiredStateEnforcesTunnelCap(t *testing.T) { - m := newTunnelTestManager(t) - specs := make([]*gatewayv2.TunnelSpec, 0, maxTunnelsPerAgent+2) - for i := 0; i < maxTunnelsPerAgent+2; i++ { - specs = append(specs, &gatewayv2.TunnelSpec{ - Id: "tun-" + string(rune('a'+i)), - TargetUrl: "http://localhost:3000", - }) - } - m.ApplyDesiredState("test-agent", desiredState(specs...)) - if got := len(m.TunnelStateSnapshot("test-agent").GetTunnels()); got != maxTunnelsPerAgent { - t.Fatalf("tunnels = %d, want cap %d", got, maxTunnelsPerAgent) - } -} - -func TestApplyDesiredStateSkipsExpiredAndInvalidSpecs(t *testing.T) { - m := newTunnelTestManager(t) - m.ApplyDesiredState("test-agent", desiredState( - &gatewayv2.TunnelSpec{Id: "expired", TargetUrl: "http://localhost:3000", ExpiresAt: time.Now().Add(-time.Minute).Unix()}, - &gatewayv2.TunnelSpec{Id: "", TargetUrl: "http://localhost:3000"}, - &gatewayv2.TunnelSpec{Id: "no-target"}, - &gatewayv2.TunnelSpec{Id: "ok", TargetUrl: "http://localhost:3000"}, - )) - snapshot := m.TunnelStateSnapshot("test-agent") - if len(snapshot.GetTunnels()) != 1 || findTunnelStatus(snapshot, "ok") == nil { - t.Fatalf("snapshot = %#v, want only \"ok\"", snapshot.GetTunnels()) - } -} - -func TestSnapshotRevisionIsMonotonic(t *testing.T) { - m := newTunnelTestManager(t) - first := m.TunnelStateSnapshot("test-agent").GetRevision() - second := m.TunnelStateSnapshot("test-agent").GetRevision() - if second <= first { - t.Fatalf("revision not monotonic: %d then %d", first, second) - } -} - -func TestAcquireTunnelLifecycleAndLimits(t *testing.T) { - m := newTunnelTestManager(t) - m.ApplyDesiredState("test-agent", desiredState( - &gatewayv2.TunnelSpec{Id: "tun-a", TargetUrl: "http://localhost:3000"}, - )) - slug := m.TunnelStateSnapshot("test-agent").GetTunnels()[0].GetSlug() - - if _, err := m.AcquireTunnel("missing", "s-1"); err != ErrTunnelNotFound { - t.Fatalf("acquire missing = %v, want ErrTunnelNotFound", err) - } - - leases := make([]*TunnelStreamLease, 0, maxTunnelConnections) - for i := 0; i < maxTunnelConnections; i++ { - lease, err := m.AcquireTunnel(slug, "s-"+string(rune('a'+i))) - if err != nil { - t.Fatalf("acquire %d: %v", i, err) - } - leases = append(leases, lease) - } - if _, err := m.AcquireTunnel(slug, "s-over"); err != ErrTunnelOverLimit { - t.Fatalf("over-limit acquire = %v, want ErrTunnelOverLimit", err) - } - if got := m.TunnelStateSnapshot("test-agent").GetTunnels()[0].GetActiveConnections(); got != maxTunnelConnections { - t.Fatalf("active connections = %d, want %d", got, maxTunnelConnections) - } - for _, lease := range leases { - lease.Release() - } - if got := m.TunnelStateSnapshot("test-agent").GetTunnels()[0].GetActiveConnections(); got != 0 { - t.Fatalf("active connections after release = %d, want 0", got) - } - - if lease, err := m.AcquireTunnel(slug, "s-again"); err != nil { - t.Fatalf("re-acquire after release: %v", err) - } else { - if lease.TargetURL() != "http://localhost:3000" { - t.Fatalf("lease target = %q", lease.TargetURL()) - } - lease.Release() - } - - m.ClearSession(mustCurrentSession(t, m)) - if _, err := m.AcquireTunnel(slug, "s-offline"); err != ErrAgentOffline { - t.Fatalf("offline acquire = %v, want ErrAgentOffline", err) - } -} - -func mustCurrentSession(t *testing.T, m *Manager) *AgentSession { - t.Helper() - session, err := m.resolveSession("test-agent") - if err != nil { - t.Fatalf("resolve current agent session: %v", err) - } - return session -} - -func TestDispatchTunnelFrameDropsStreamWhenBacklogged(t *testing.T) { - m := newTunnelTestManager(t) - m.ApplyDesiredState("test-agent", desiredState( - &gatewayv2.TunnelSpec{Id: "tun-a", TargetUrl: "http://localhost:3000"}, - )) - slug := m.TunnelStateSnapshot("test-agent").GetTunnels()[0].GetSlug() - lease, err := m.AcquireTunnel(slug, "s-backlog") - if err != nil { - t.Fatalf("acquire: %v", err) - } - defer lease.Release() - - for i := 0; i < tunnelStreamChannelDepth+1; i++ { - m.dispatchTunnelFrame("test-agent", &gatewayv2.TunnelFrame{ - StreamId: "s-backlog", - Kind: gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_RESPONSE_BODY, - }) - } - select { - case <-lease.Done(): - case <-time.After(time.Second): - t.Fatal("backlogged stream was not closed") - } -} - -func TestSweepExpiredTunnelsRemovesRecords(t *testing.T) { - m := newTunnelTestManager(t) - m.ApplyDesiredState("test-agent", desiredState( - &gatewayv2.TunnelSpec{Id: "short", TargetUrl: "http://localhost:3000", ExpiresAt: time.Now().Add(30 * time.Second).Unix()}, - &gatewayv2.TunnelSpec{Id: "forever", TargetUrl: "http://localhost:4000"}, - )) - if got := len(m.TunnelStateSnapshot("test-agent").GetTunnels()); got != 2 { - t.Fatalf("tunnels = %d, want 2", got) - } - m.sweepExpiredTunnels(time.Now().Add(2 * time.Minute)) - snapshot := m.TunnelStateSnapshot("test-agent") - if len(snapshot.GetTunnels()) != 1 || findTunnelStatus(snapshot, "forever") == nil { - t.Fatalf("after sweep = %#v, want only \"forever\"", snapshot.GetTunnels()) - } -} - -func TestOnAgentSessionClearedClosesStreamsAndMarksOffline(t *testing.T) { - m := newTunnelTestManager(t) - m.ApplyDesiredState("test-agent", desiredState( - &gatewayv2.TunnelSpec{Id: "tun-a", TargetUrl: "http://localhost:3000"}, - )) - slug := m.TunnelStateSnapshot("test-agent").GetTunnels()[0].GetSlug() - lease, err := m.AcquireTunnel(slug, "s-1") - if err != nil { - t.Fatalf("acquire: %v", err) - } - - m.ClearSession(mustCurrentSession(t, m)) - select { - case <-lease.Done(): - case <-time.After(time.Second): - t.Fatal("stream not closed after agent session cleared") - } - snapshot := m.TunnelStateSnapshot("test-agent") - if snapshot.GetAgentOnline() { - t.Fatal("snapshot still reports agent online") - } - if len(snapshot.GetTunnels()) != 1 { - t.Fatalf("specs must survive agent disconnect, got %d", len(snapshot.GetTunnels())) - } -} - -func TestForgetAgentPurgesOfflineTunnelRecordsAndRoutes(t *testing.T) { - m := newTunnelTestManager(t) - m.ApplyDesiredState("test-agent", desiredState( - &gatewayv2.TunnelSpec{Id: "tun-a", TargetUrl: "http://localhost:3000"}, - )) - slug := m.TunnelStateSnapshot("test-agent").GetTunnels()[0].GetSlug() - - // 普通断线保留 spec;随后永久删除即使 Agent 已离线,也必须删除公开路由。 - m.ClearSession(mustCurrentSession(t, m)) - if got := len(m.TunnelStateSnapshot("test-agent").GetTunnels()); got != 1 { - t.Fatalf("offline tunnel count = %d, want preserved spec", got) - } - if disconnected := m.ForgetAgent("test-agent"); disconnected { - t.Fatal("offline agent deletion must not report a disconnected live session") - } - if got := len(m.TunnelStateSnapshot("test-agent").GetTunnels()); got != 0 { - t.Fatalf("deleted agent tunnel count = %d, want 0", got) - } - if _, err := m.AcquireTunnel(slug, "after-delete"); err != ErrTunnelNotFound { - t.Fatalf("acquire deleted route = %v, want ErrTunnelNotFound", err) - } - m.setRelayHealth("test-agent", &gatewayv2.TunnelHealth{Status: "late"}) - m.tunnels.mu.Lock() - _, relayRestored := m.tunnels.relays["test-agent"] - m.tunnels.mu.Unlock() - if relayRestored { - t.Fatal("late relay probe must not restore deleted agent state") - } -} - -func TestSubscribeTunnelStateReceivesBroadcasts(t *testing.T) { - m := newTunnelTestManager(t) - ch, cleanup := m.SubscribeTunnelState() - defer cleanup() - - m.ApplyDesiredState("test-agent", desiredState( - &gatewayv2.TunnelSpec{Id: "tun-a", TargetUrl: "http://localhost:3000"}, - )) - - select { - case tagged := <-ch: - if tagged.AgentID != "test-agent" || findTunnelStatus(tagged.Event, "tun-a") == nil { - t.Fatalf("broadcast snapshot = %#v, want test-agent/tun-a", tagged) - } - case <-time.After(time.Second): - t.Fatal("no tunnel.state broadcast received") - } -} diff --git a/crates/agent-gateway/internal/session/unary.go b/crates/agent-gateway/internal/session/unary.go deleted file mode 100644 index 6a343e7b7..000000000 --- a/crates/agent-gateway/internal/session/unary.go +++ /dev/null @@ -1,34 +0,0 @@ -package session - -import ( - "context" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -// AwaitUnaryResponse 以单次请求-响应语义向目标 Agent 发送信封并等待首条关联响应; -// 取消/超时由调用方 ctx 控制;agentID 必须明确且非空。 -func (m *Manager) AwaitUnaryResponse( - ctx context.Context, - agentID string, - requestID string, - envelope *gatewayv2.GatewayEnvelope, -) (*gatewayv2.AgentEnvelope, error) { - ch, done, cleanup, err := m.RegisterStreamAndSendContext(ctx, agentID, requestID, envelope) - if err != nil { - return nil, err - } - defer cleanup() - - select { - case <-ctx.Done(): - return nil, ctx.Err() - case <-done: - return nil, ErrAgentOffline - case env, ok := <-ch: - if !ok { - return nil, ErrAgentOffline - } - return env, nil - } -} diff --git a/crates/agent-gateway/internal/session/workspace_activity.go b/crates/agent-gateway/internal/session/workspace_activity.go deleted file mode 100644 index 3653ad2ae..000000000 --- a/crates/agent-gateway/internal/session/workspace_activity.go +++ /dev/null @@ -1,173 +0,0 @@ -package session - -import ( - "sort" - "strings" - "sync" - "time" - - "github.com/google/uuid" - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -const workspaceActivityChannelDepth = 16 - -// workspaceActivityHub tracks which (agent, workdir) pairs /ws clients are -// interested in and fans agent-reported activity events out to them. Per -// agent, the union of watched workdirs is pushed as a declarative full set -// whenever it changes (and on that agent's reconnect), so the agent owns zero -// subscription state. -type workspaceActivityHub struct { - mu sync.Mutex - watchCounts map[workspaceWatchKey]int - nextSubID int - subscribers map[int]*workspaceActivitySubscriber -} - -// workspaceWatchKey 按明确的 agentID + workdir 标识一个订阅目标。 -type workspaceWatchKey struct { - agentID string - workdir string -} - -type workspaceActivitySubscriber struct { - key workspaceWatchKey - ch chan *gatewayv2.WorkspaceActivityEvent -} - -func newWorkspaceActivityHub() *workspaceActivityHub { - return &workspaceActivityHub{ - watchCounts: make(map[workspaceWatchKey]int), - subscribers: make(map[int]*workspaceActivitySubscriber), - } -} - -// SubscribeWorkspaceActivity registers interest in one workdir on one agent. -// The returned cleanup drops the subscription; when the pair's refcount -// reaches zero it leaves that agent's watch set on the next push. -func (m *Manager) SubscribeWorkspaceActivity( - agentID string, - workdir string, -) (<-chan *gatewayv2.WorkspaceActivityEvent, func()) { - key := workspaceWatchKey{ - agentID: strings.TrimSpace(agentID), - workdir: strings.TrimSpace(workdir), - } - sub := &workspaceActivitySubscriber{ - key: key, - ch: make(chan *gatewayv2.WorkspaceActivityEvent, workspaceActivityChannelDepth), - } - - m.workspaceHub.mu.Lock() - subID := m.workspaceHub.nextSubID - m.workspaceHub.nextSubID += 1 - m.workspaceHub.subscribers[subID] = sub - m.workspaceHub.watchCounts[key] += 1 - watchSetChanged := m.workspaceHub.watchCounts[key] == 1 - m.workspaceHub.mu.Unlock() - - if watchSetChanged { - m.pushWorkspaceWatchSet(key.agentID) - } - - var once sync.Once - cleanup := func() { - once.Do(func() { - m.workspaceHub.mu.Lock() - // Do not close the channel: broadcastWorkspaceActivity sends after - // copying subscribers, so closing can race with an in-flight send. - delete(m.workspaceHub.subscribers, subID) - changed := false - if count := m.workspaceHub.watchCounts[key]; count > 1 { - m.workspaceHub.watchCounts[key] = count - 1 - } else { - delete(m.workspaceHub.watchCounts, key) - changed = true - } - m.workspaceHub.mu.Unlock() - if changed { - m.pushWorkspaceWatchSet(key.agentID) - } - }) - } - return sub.ch, cleanup -} - -// broadcastWorkspaceActivity fans one agent event out to the subscribers of -// its workdir on that agent. Runs on the agent read loop, so it must never -// block: a full subscriber channel drops the event (consumers converge on the -// next one, and revision gaps are already tolerated client-side). -func (m *Manager) broadcastWorkspaceActivity(agentID string, event *gatewayv2.WorkspaceActivityEvent) { - if event == nil { - return - } - workdir := strings.TrimSpace(event.GetWorkdir()) - if workdir == "" { - return - } - agentID = strings.TrimSpace(agentID) - m.workspaceHub.mu.Lock() - targets := make([]chan *gatewayv2.WorkspaceActivityEvent, 0, len(m.workspaceHub.subscribers)) - for _, sub := range m.workspaceHub.subscribers { - if sub.key.workdir != workdir { - continue - } - if sub.key.agentID == agentID { - targets = append(targets, sub.ch) - } - } - m.workspaceHub.mu.Unlock() - - for _, ch := range targets { - select { - case ch <- event: - default: - } - } -} - -// hasWorkspaceWatchInterest 报告 agent_id 是否有工作区订阅。 -func (m *Manager) hasWorkspaceWatchInterest(agentID string) bool { - agentID = strings.TrimSpace(agentID) - m.workspaceHub.mu.Lock() - defer m.workspaceHub.mu.Unlock() - for key := range m.workspaceHub.watchCounts { - if key.agentID == agentID { - return true - } - } - return false -} - -// pushWorkspaceWatchSet 把 agentID 的完整工作区订阅集合推送给该 Agent。 -// 此操作为 best-effort 且不阻塞: -// the set is re-pushed on every change and on agent reconnect, so a dropped -// push heals itself. -func (m *Manager) pushWorkspaceWatchSet(agentID string) { - agentID = strings.TrimSpace(agentID) - m.workspaceHub.mu.Lock() - workdirSet := make(map[string]bool) - for key := range m.workspaceHub.watchCounts { - if key.agentID == agentID { - workdirSet[key.workdir] = true - } - } - m.workspaceHub.mu.Unlock() - workdirs := make([]string, 0, len(workdirSet)) - for workdir := range workdirSet { - workdirs = append(workdirs, workdir) - } - sort.Strings(workdirs) - - session, err := m.resolveSession(agentID) - if err != nil { - return - } - _, _ = session.TrySendToAgent(&gatewayv2.GatewayEnvelope{ - RequestId: "workspace-watch-" + uuid.NewString(), - Timestamp: time.Now().Unix(), - Payload: &gatewayv2.GatewayEnvelope_WorkspaceWatch{ - WorkspaceWatch: &gatewayv2.WorkspaceWatchRequest{Workdirs: workdirs}, - }, - }) -} diff --git a/crates/agent-gateway/internal/session/workspace_activity_test.go b/crates/agent-gateway/internal/session/workspace_activity_test.go deleted file mode 100644 index 8a0fc2da6..000000000 --- a/crates/agent-gateway/internal/session/workspace_activity_test.go +++ /dev/null @@ -1,187 +0,0 @@ -package session - -import ( - "testing" - "time" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" -) - -// newWorkspaceTestManager builds a manager with a live session. SetSession -// replays the watch set only when it is non-empty, so no async push races the -// assertions below: every push comes from a synchronous subscribe/unsubscribe -// call. -func newWorkspaceTestManager(t *testing.T) (*Manager, *AgentSession) { - t.Helper() - m := NewManager() - session := NewAgentSession(AuthSnapshot{AgentID: "test-agent"}) - m.SetSession(session) - assertNoWorkspaceWatchPush(t, session) - return m, session -} - -// awaitWorkspaceWatchSet blocks until the next WorkspaceWatchRequest reaches -// the agent outbound queue and returns its workdir set. -func awaitWorkspaceWatchSet(t *testing.T, session *AgentSession) []string { - t.Helper() - deadline := time.After(2 * time.Second) - for { - select { - case env := <-session.Outbound(): - if watch := env.GetWorkspaceWatch(); watch != nil { - return watch.GetWorkdirs() - } - case <-deadline: - t.Fatal("timed out waiting for a workspace watch push") - return nil - } - } -} - -// assertNoWorkspaceWatchPush fails when a WorkspaceWatchRequest is already -// queued on the agent outbound channel. -func assertNoWorkspaceWatchPush(t *testing.T, session *AgentSession) { - t.Helper() - for { - select { - case env := <-session.Outbound(): - if watch := env.GetWorkspaceWatch(); watch != nil { - t.Fatalf("unexpected workspace watch push: %v", watch.GetWorkdirs()) - } - default: - return - } - } -} - -func workspaceActivityEvent(workdir string, revision uint64) *gatewayv2.WorkspaceActivityEvent { - return &gatewayv2.WorkspaceActivityEvent{ - Workdir: workdir, - Revision: revision, - Fs: true, - Git: true, - } -} - -func TestSubscribeWorkspaceActivityPushesWatchSetWithRefcount(t *testing.T) { - m, session := newWorkspaceTestManager(t) - - _, cleanupA1 := m.SubscribeWorkspaceActivity("test-agent", "/repo/a") - if set := awaitWorkspaceWatchSet(t, session); len(set) != 1 || set[0] != "/repo/a" { - t.Fatalf("watch set after first subscribe = %v, want [/repo/a]", set) - } - - // Second subscriber on the same workdir must not re-push the set. - _, cleanupA2 := m.SubscribeWorkspaceActivity("test-agent", "/repo/a") - assertNoWorkspaceWatchPush(t, session) - - _, cleanupB := m.SubscribeWorkspaceActivity("test-agent", "/repo/b") - set := awaitWorkspaceWatchSet(t, session) - if len(set) != 2 || set[0] != "/repo/a" || set[1] != "/repo/b" { - t.Fatalf("watch set after second workdir = %v, want [/repo/a /repo/b]", set) - } - - // Dropping one of two /repo/a subscribers keeps the workdir watched. - cleanupA1() - assertNoWorkspaceWatchPush(t, session) - - // Cleanup is idempotent: replaying it must not decrement again. - cleanupA1() - assertNoWorkspaceWatchPush(t, session) - - // The last /repo/a subscriber leaving removes the key. - cleanupA2() - if set := awaitWorkspaceWatchSet(t, session); len(set) != 1 || set[0] != "/repo/b" { - t.Fatalf("watch set after refcount reached zero = %v, want [/repo/b]", set) - } - - cleanupB() - if set := awaitWorkspaceWatchSet(t, session); len(set) != 0 { - t.Fatalf("watch set after last unsubscribe = %v, want []", set) - } -} - -func TestSetSessionReplaysNonEmptyWorkspaceActivityWatchSet(t *testing.T) { - m, session := newWorkspaceTestManager(t) - - _, cleanup := m.SubscribeWorkspaceActivity("test-agent", "/repo/a") - defer cleanup() - if set := awaitWorkspaceWatchSet(t, session); len(set) != 1 || set[0] != "/repo/a" { - t.Fatalf("watch set after subscribe = %v, want [/repo/a]", set) - } - - // A reconnected agent starts blank and must learn the watch set again. - replacement := NewAgentSession(AuthSnapshot{AgentID: "test-agent"}) - m.SetSession(replacement) - if set := awaitWorkspaceWatchSet(t, replacement); len(set) != 1 || set[0] != "/repo/a" { - t.Fatalf("replayed watch set = %v, want [/repo/a]", set) - } -} - -func TestBroadcastWorkspaceActivityFiltersByWorkdir(t *testing.T) { - m, _ := newWorkspaceTestManager(t) - - eventsA, cleanupA := m.SubscribeWorkspaceActivity("test-agent", "/repo/a") - defer cleanupA() - eventsB, cleanupB := m.SubscribeWorkspaceActivity("test-agent", "/repo/b") - defer cleanupB() - - m.broadcastWorkspaceActivity("test-agent", workspaceActivityEvent("/repo/a", 1)) - m.broadcastWorkspaceActivity("test-agent", workspaceActivityEvent("/repo/missing", 2)) - - select { - case event := <-eventsA: - if event.GetWorkdir() != "/repo/a" || event.GetRevision() != 1 { - t.Fatalf("unexpected event on /repo/a subscriber: %#v", event) - } - case <-time.After(time.Second): - t.Fatal("subscriber for /repo/a did not receive its event") - } - - select { - case event := <-eventsA: - t.Fatalf("subscriber for /repo/a received foreign event: %#v", event) - case event := <-eventsB: - t.Fatalf("subscriber for /repo/b received foreign event: %#v", event) - default: - } -} - -func TestBroadcastWorkspaceActivityDoesNotBlockOnSlowSubscriber(t *testing.T) { - m, _ := newWorkspaceTestManager(t) - - // Never read from the channel: once its buffer is full, broadcasts must - // drop instead of blocking. - _, cleanup := m.SubscribeWorkspaceActivity("test-agent", "/repo/a") - defer cleanup() - - done := make(chan struct{}) - go func() { - defer close(done) - for i := 0; i < workspaceActivityChannelDepth*3; i++ { - m.broadcastWorkspaceActivity("test-agent", workspaceActivityEvent("/repo/a", uint64(i+1))) - } - }() - - select { - case <-done: - case <-time.After(2 * time.Second): - t.Fatal("broadcastWorkspaceActivity blocked on a slow subscriber") - } -} - -func TestBroadcastWorkspaceActivityIgnoresNilAndEmptyWorkdir(t *testing.T) { - m, _ := newWorkspaceTestManager(t) - - events, cleanup := m.SubscribeWorkspaceActivity("test-agent", "/repo/a") - defer cleanup() - - m.broadcastWorkspaceActivity("test-agent", nil) - m.broadcastWorkspaceActivity("test-agent", workspaceActivityEvent(" ", 1)) - - select { - case event := <-events: - t.Fatalf("unexpected event delivered: %#v", event) - default: - } -} diff --git a/crates/agent-gateway/internal/transport/wscore/conn.go b/crates/agent-gateway/internal/transport/wscore/conn.go deleted file mode 100644 index 0ed3ea794..000000000 --- a/crates/agent-gateway/internal/transport/wscore/conn.go +++ /dev/null @@ -1,443 +0,0 @@ -package wscore - -import ( - "errors" - "log/slog" - "sync" - "sync/atomic" - "time" - - "github.com/gorilla/websocket" - - "github.com/liveagent/agent-gateway/internal/observability" -) - -// 写泵行为常量,保持既有稳定取值。 -const ( - // DefaultQueueSize 是数据队列默认容量。 - DefaultQueueSize = 512 - // DefaultCtrlQueueSize 是控制队列默认容量。 - DefaultCtrlQueueSize = 64 - // DefaultQueueBytes / DefaultCtrlQueueBytes 让队列同时受帧数与字节数约束。 - DefaultQueueBytes = 8 * 1024 * 1024 - DefaultCtrlQueueBytes = 256 * 1024 - - defaultHeartbeatPeriod = 15 * time.Second - heartbeatGraceFloor = 5 * time.Second - defaultControlWriteWait = 10 * time.Second - // writeLoopBatchSize 是一次唤醒最多连续写出的帧数(控制帧优先穿插)。 - writeLoopBatchSize = 64 -) - -// Config 是连接运行时的行为参数;零值字段取默认。 -type Config struct { - // WriteTimeout 同时用作单帧写超时与入队等待上限。 - WriteTimeout time.Duration - // QueueSize / CtrlQueueSize 为两条队列的容量。 - QueueSize int - CtrlQueueSize int - // QueueBytes / CtrlQueueBytes 是两条队列的内存上限。 - QueueBytes int64 - CtrlQueueBytes int64 - // HeartbeatPeriod / HeartbeatGrace 决定心跳周期与空闲驱逐窗口(IdleTimeout = 3*period + grace)。 - HeartbeatPeriod time.Duration - HeartbeatGrace time.Duration - // Remote 是掉帧日志中的对端标识(通常为 RemoteAddr)。 - Remote string - // OnClose 在连接关闭时恰好回调一次(done 已关闭、底层 ws 尚未关闭),供协议层清理订阅等资源。 - OnClose func() -} - -// Conn 是单条 WebSocket 连接的传输运行时。Outbox/CtrlOutbox 由任意 goroutine 经 Enqueue -// 生产、由唯一写泵 goroutine 消费;两通道导出仅为白盒测试,业务代码一律走 Enqueue。 -type Conn struct { - // Outbox 是数据队列(写泵独占消费;除测试外勿直接读写)。 - Outbox chan Frame - // CtrlOutbox 是控制队列,写泵优先消费它,使拥塞无法饿死心跳与流恢复信号。 - CtrlOutbox chan Frame - - ws *websocket.Conn - cfg Config - - writeMu sync.Mutex - droppedFrames atomic.Int64 - writerCloses atomic.Int64 - queueByteOverflows atomic.Int64 - dataBytes atomic.Int64 - controlBytes atomic.Int64 - dataFreed chan struct{} - controlFreed chan struct{} - writeOverride func(Frame) error - - closeOnce sync.Once - done chan struct{} - - // authorized 只在鉴权成功后置位;置位前入站活动不刷新读超时——客户端只有一个 - // IdleTimeout 窗口完成鉴权。 - authorized atomic.Bool - - lastInboundMu sync.Mutex - lastInboundAt time.Time - - writeLoopOnce sync.Once - heartbeatOnce sync.Once -} - -// NewConn 构造连接运行时。ws 允许为 nil(仅入队语义的单元测试)。 -func NewConn(ws *websocket.Conn, cfg Config) *Conn { - if cfg.QueueSize <= 0 { - cfg.QueueSize = DefaultQueueSize - } - if cfg.CtrlQueueSize <= 0 { - cfg.CtrlQueueSize = DefaultCtrlQueueSize - } - if cfg.QueueBytes <= 0 { - cfg.QueueBytes = DefaultQueueBytes - } - if cfg.CtrlQueueBytes <= 0 { - cfg.CtrlQueueBytes = DefaultCtrlQueueBytes - } - return &Conn{ - Outbox: make(chan Frame, cfg.QueueSize), - CtrlOutbox: make(chan Frame, cfg.CtrlQueueSize), - ws: ws, - cfg: cfg, - done: make(chan struct{}), - dataFreed: make(chan struct{}, 1), - controlFreed: make(chan struct{}, 1), - } -} - -// Done 返回连接关闭信号通道。 -func (c *Conn) Done() <-chan struct{} { - return c.done -} - -// Close 幂等关闭连接:先发布 done、回调 OnClose 清理,最后关底层 ws。 -func (c *Conn) Close() { - c.closeOnce.Do(func() { - close(c.done) - if c.cfg.OnClose != nil { - c.cfg.OnClose() - } - if c.ws != nil { - _ = c.ws.Close() - } - }) -} - -// SetAuthorized 标记鉴权完成;此后入站活动开始刷新读超时。 -func (c *Conn) SetAuthorized() { - c.authorized.Store(true) -} - -// TouchInboundActivity 记录入站活动并(鉴权后)后推读超时;读循环收到任何帧及 WS pong 回调须调用。 -func (c *Conn) TouchInboundActivity() { - c.lastInboundMu.Lock() - c.lastInboundAt = time.Now() - c.lastInboundMu.Unlock() - if !c.authorized.Load() || c.ws == nil { - return - } - _ = c.ws.SetReadDeadline(time.Now().Add(c.IdleTimeout())) -} - -// IdleTimeout 是空闲驱逐窗口:3 个心跳周期加宽限。 -func (c *Conn) IdleTimeout() time.Duration { - period := c.cfg.HeartbeatPeriod - if period <= 0 { - period = defaultHeartbeatPeriod - } - grace := c.cfg.HeartbeatGrace - if grace <= 0 { - grace = heartbeatGraceFloor - } - return period*3 + grace -} - -// ControlWriteTimeout 是入队等待与控制帧写出的时间上限。 -func (c *Conn) ControlWriteTimeout() time.Duration { - if c.cfg.WriteTimeout > 0 { - return c.cfg.WriteTimeout - } - return defaultControlWriteWait -} - -// DroppedFrames 返回累计掉帧数(观测与测试用)。 -func (c *Conn) DroppedFrames() int64 { - return c.droppedFrames.Load() -} - -// WriterCloses 返回写泵因底层写错误而关闭连接的累计次数。 -func (c *Conn) WriterCloses() int64 { - return c.writerCloses.Load() -} - -// QueueByteOverflows 返回帧或聚合队列超过字节预算的累计次数。 -func (c *Conn) QueueByteOverflows() int64 { - return c.queueByteOverflows.Load() -} - -// Enqueue 将帧交给写泵:控制/心跳帧走优先队列;数据帧持续拥塞时丢弃并返回 -// ErrWriteQueueFull;FrameResponse 掉帧则关连接,让客户端重连重试而非挂到超时。 -func (c *Conn) Enqueue(frame Frame) error { - if frame.Class == FrameControl || frame.Class == FramePing { - err := c.enqueueControl(frame) - if errors.Is(err, ErrWriteQueueFull) || errors.Is(err, ErrWriteFrameTooLarge) { - c.noteDroppedFrame(frame, "control", writeQueueDropReason(err)) - } - return err - } - err := c.enqueueData(frame) - if errors.Is(err, ErrWriteQueueFull) || errors.Is(err, ErrWriteFrameTooLarge) { - c.noteDroppedFrame(frame, "data", writeQueueDropReason(err)) - if frame.Class == FrameResponse { - c.Close() - } - } - return err -} - -// enqueueData 在数据队列瞬时满载时最多等 ControlWriteTimeout,持续积压才报 ErrWriteQueueFull;快速路径零分配。 -func (c *Conn) enqueueData(frame Frame) error { - return c.enqueueBounded(frame, "data", c.Outbox, &c.dataBytes, c.cfg.QueueBytes, c.dataFreed) -} - -func (c *Conn) enqueueControl(frame Frame) error { - if frame.Class == FramePing { - if c.tryEnqueueBounded(frame, "control", c.CtrlOutbox, &c.controlBytes, c.cfg.CtrlQueueBytes) { - return nil - } - select { - case <-c.done: - return errors.New("connection closed") - default: - // 心跳是周期性的:控制队列满时静默丢弃,下个周期自然取代。 - c.noteDroppedFrame(frame, "control", "queue_full") - return nil - } - } - return c.enqueueBounded(frame, "control", c.CtrlOutbox, &c.controlBytes, c.cfg.CtrlQueueBytes, c.controlFreed) -} - -func (c *Conn) enqueueBounded( - frame Frame, - lane string, - queue chan<- Frame, - queuedBytes *atomic.Int64, - byteLimit int64, - freed <-chan struct{}, -) error { - frameBytes := int64(len(frame.Data)) - if frameBytes > byteLimit { - c.noteQueueByteOverflow(frameBytes, lane, "frame_too_large") - return ErrWriteFrameTooLarge - } - timer := time.NewTimer(c.ControlWriteTimeout()) - defer timer.Stop() - for { - if reserveQueueBytes(queuedBytes, frameBytes, byteLimit) { - select { - case <-c.done: - releaseQueueBytes(queuedBytes, frameBytes, nil) - return errors.New("connection closed") - case queue <- frame: - return nil - case <-timer.C: - releaseQueueBytes(queuedBytes, frameBytes, nil) - return ErrWriteQueueFull - } - } - select { - case <-c.done: - return errors.New("connection closed") - case <-freed: - case <-timer.C: - c.noteQueueByteOverflow(frameBytes, lane, "byte_limit") - return ErrWriteQueueFull - } - } -} - -func (c *Conn) tryEnqueueBounded( - frame Frame, - lane string, - queue chan<- Frame, - queuedBytes *atomic.Int64, - byteLimit int64, -) bool { - frameBytes := int64(len(frame.Data)) - if frameBytes > byteLimit { - c.noteQueueByteOverflow(frameBytes, lane, "frame_too_large") - return false - } - if !reserveQueueBytes(queuedBytes, frameBytes, byteLimit) { - c.noteQueueByteOverflow(frameBytes, lane, "byte_limit") - return false - } - select { - case <-c.done: - releaseQueueBytes(queuedBytes, frameBytes, nil) - return false - case queue <- frame: - return true - default: - releaseQueueBytes(queuedBytes, frameBytes, nil) - return false - } -} - -func reserveQueueBytes(queuedBytes *atomic.Int64, frameBytes, byteLimit int64) bool { - for { - current := queuedBytes.Load() - if frameBytes > byteLimit-current { - return false - } - if queuedBytes.CompareAndSwap(current, current+frameBytes) { - return true - } - } -} - -func releaseQueueBytes(queuedBytes *atomic.Int64, frameBytes int64, freed chan<- struct{}) { - for { - current := queuedBytes.Load() - next := current - frameBytes - if next < 0 { - next = 0 - } - if queuedBytes.CompareAndSwap(current, next) { - break - } - } - if freed != nil { - select { - case freed <- struct{}{}: - default: - } - } -} - -func (c *Conn) noteDroppedFrame(frame Frame, lane string, reason string) { - dropped := c.droppedFrames.Add(1) - // 只记第一次与每第 100 次:生产可见掉帧,突发期不刷屏。 - if dropped == 1 || dropped%100 == 0 { - slog.Warn("websocket: shed frame for slow client", - "lane", lane, - "kind", frame.Kind, - "request_id", frame.RequestID, - "remote", c.cfg.Remote, - "size", len(frame.Data), - "reason", reason, - ) - } -} - -func (c *Conn) noteQueueByteOverflow(size int64, lane string, reason string) { - overflows := c.queueByteOverflows.Add(1) - observability.Usage.WebSocketQueueByteOverflowsTotal.Add(1) - if overflows == 1 || overflows%100 == 0 { - slog.Warn("websocket_queue_byte_overflow", - "lane", lane, - "remote", c.cfg.Remote, - "size", size, - "reason", reason, - ) - } -} - -func writeQueueDropReason(err error) string { - if errors.Is(err, ErrWriteFrameTooLarge) { - return "frame_too_large" - } - return "queue_full" -} - -// StartWriteLoop 启动写泵(幂等),协议层在鉴权成功后调用——鉴权前队列无人消费。 -func (c *Conn) StartWriteLoop() { - c.writeLoopOnce.Do(func() { - go c.writeLoop() - }) -} - -// writeLoop 优先清空控制队列再消费数据队列,使拥塞无法饿死心跳与流恢复帧。 -func (c *Conn) writeLoop() { - for { - select { - case <-c.done: - return - case frame := <-c.CtrlOutbox: - if !c.deliverQueuedFrame(frame, true) { - return - } - case frame := <-c.Outbox: - if !c.deliverQueuedFrame(frame, false) { - return - } - for drained := 0; drained < writeLoopBatchSize; drained++ { - select { - case extra := <-c.CtrlOutbox: - if !c.deliverQueuedFrame(extra, true) { - return - } - continue - default: - } - select { - case extra := <-c.Outbox: - if !c.deliverQueuedFrame(extra, false) { - return - } - default: - goto batchDone - } - } - batchDone: - } - } -} - -// deliverQueuedFrame 在第一次写错误时立即关闭连接;可靠恢复必须发生在新连接上。 -func (c *Conn) deliverQueuedFrame(frame Frame, control bool) bool { - if control { - defer releaseQueueBytes(&c.controlBytes, int64(len(frame.Data)), c.controlFreed) - } else { - defer releaseQueueBytes(&c.dataBytes, int64(len(frame.Data)), c.dataFreed) - } - if err := c.writeFrameDirect(frame); err != nil { - lane := "data" - if control { - lane = "control" - } - c.writerCloses.Add(1) - observability.Usage.WebSocketWriterClosesTotal.Add(1) - slog.Error("websocket_writer_closed", - "lane", lane, - "size", len(frame.Data), - "reason", "write_failed", - ) - c.Close() - return false - } - return true -} - -func (c *Conn) writeFrameDirect(frame Frame) error { - c.writeMu.Lock() - defer c.writeMu.Unlock() - if c.writeOverride != nil { - return c.writeOverride(frame) - } - if c.ws == nil { - return errors.New("websocket connection is nil") - } - if c.cfg.WriteTimeout > 0 { - if err := c.ws.SetWriteDeadline(time.Now().Add(c.cfg.WriteTimeout)); err != nil { - return err - } - defer func() { - _ = c.ws.SetWriteDeadline(time.Time{}) - }() - } - return c.ws.WriteMessage(frame.MessageType, frame.Data) -} diff --git a/crates/agent-gateway/internal/transport/wscore/conn_test.go b/crates/agent-gateway/internal/transport/wscore/conn_test.go deleted file mode 100644 index caef2d00e..000000000 --- a/crates/agent-gateway/internal/transport/wscore/conn_test.go +++ /dev/null @@ -1,232 +0,0 @@ -package wscore - -import ( - "errors" - "sync/atomic" - "testing" - "time" -) - -// 写泵语义回归测试(原 internal/server/websocket_write_test.go):队列路由、 -// 拥塞掉帧、关联响应关连接、心跳静默丢弃等行为必须逐一保持。 - -func newTestConn(queueSize int, writeTimeout time.Duration) *Conn { - return NewConn(nil, Config{ - QueueSize: queueSize, - WriteTimeout: writeTimeout, - }) -} - -func TestEnqueueDataWaitsForDrainedSlot(t *testing.T) { - t.Parallel() - - c := newTestConn(1, 500*time.Millisecond) - c.Outbox <- Frame{Kind: "ping"} - - go func() { - time.Sleep(10 * time.Millisecond) - <-c.Outbox - }() - - if err := c.Enqueue(Frame{Class: FrameData, Kind: "chat.event"}); err != nil { - t.Fatalf("Enqueue with draining outbox = %v, want nil", err) - } -} - -func TestEnqueueDataFailsAfterPersistentBacklog(t *testing.T) { - t.Parallel() - - c := newTestConn(1, 50*time.Millisecond) - c.Outbox <- Frame{Kind: "ping"} - - started := time.Now() - err := c.Enqueue(Frame{Class: FrameData, Kind: "chat.event"}) - if !errors.Is(err, ErrWriteQueueFull) { - t.Fatalf("Enqueue with stuck outbox = %v, want ErrWriteQueueFull", err) - } - if waited := time.Since(started); waited < 50*time.Millisecond { - t.Fatalf("Enqueue gave up after %s, want at least the 50ms write timeout", waited) - } -} - -func TestEnqueueReturnsWhenConnectionCloses(t *testing.T) { - t.Parallel() - - c := newTestConn(1, time.Second) - c.Outbox <- Frame{Kind: "ping"} - - go func() { - time.Sleep(10 * time.Millisecond) - c.Close() - }() - - err := c.Enqueue(Frame{Class: FrameData, Kind: "chat.event"}) - if err == nil || err.Error() != "connection closed" { - t.Fatalf("Enqueue on closed connection = %v, want connection closed", err) - } -} - -func TestControlFramesRouteToControlQueue(t *testing.T) { - t.Parallel() - - c := newTestConn(1, 50*time.Millisecond) - - if err := c.Enqueue(Frame{Class: FramePing, Kind: "ping"}); err != nil { - t.Fatalf("Enqueue(ping) = %v, want nil", err) - } - for _, kind := range []string{"error", "chat.subscription_reset", "chat.command_update"} { - if err := c.Enqueue(Frame{Class: FrameControl, Kind: kind}); err != nil { - t.Fatalf("Enqueue(%q) = %v, want nil", kind, err) - } - } - if got := len(c.CtrlOutbox); got != 4 { - t.Fatalf("control queue depth = %d, want 4", got) - } - if got := len(c.Outbox); got != 0 { - t.Fatalf("data queue depth = %d, want 0", got) - } - - if err := c.Enqueue(Frame{Class: FrameData, Kind: "chat.event"}); err != nil { - t.Fatalf("Enqueue(chat.event) = %v, want nil", err) - } - if got := len(c.Outbox); got != 1 { - t.Fatalf("data queue depth after chat.event = %d, want 1", got) - } -} - -func TestDataQueueFullDoesNotCloseConnection(t *testing.T) { - t.Parallel() - - c := newTestConn(1, 20*time.Millisecond) - c.Outbox <- Frame{Kind: "chat.event"} - - err := c.Enqueue(Frame{Class: FrameData, Kind: "chat.event"}) - if !errors.Is(err, ErrWriteQueueFull) { - t.Fatalf("Enqueue with stuck outbox = %v, want ErrWriteQueueFull", err) - } - select { - case <-c.Done(): - t.Fatal("Enqueue closed the connection on a full data queue") - default: - } - if got := c.DroppedFrames(); got != 1 { - t.Fatalf("DroppedFrames = %d, want 1", got) - } -} - -func TestResponseQueueFullClosesConnectionForRecovery(t *testing.T) { - t.Parallel() - - c := newTestConn(1, 20*time.Millisecond) - c.Outbox <- Frame{Kind: "chat.event"} - - err := c.Enqueue(Frame{Class: FrameResponse, RequestID: "history-1", Kind: "response"}) - if !errors.Is(err, ErrWriteQueueFull) { - t.Fatalf("Enqueue(response) with stuck outbox = %v, want ErrWriteQueueFull", err) - } - select { - case <-c.Done(): - // 预期:客户端观察到断连即可恢复该关联请求,而非等一个被静默丢弃的响应到超时。 - default: - t.Fatal("dropping a correlated response left the connection open") - } -} - -func TestPingDroppedSilentlyWhenControlQueueFull(t *testing.T) { - t.Parallel() - - c := newTestConn(1, time.Second) - for range DefaultCtrlQueueSize { - c.CtrlOutbox <- Frame{Kind: "error"} - } - - started := time.Now() - if err := c.Enqueue(Frame{Class: FramePing, Kind: "ping"}); err != nil { - t.Fatalf("Enqueue(ping) with full control queue = %v, want nil (dropped)", err) - } - if waited := time.Since(started); waited > 100*time.Millisecond { - t.Fatalf("ping enqueue blocked for %s, want immediate drop", waited) - } - if got := c.DroppedFrames(); got != 1 { - t.Fatalf("DroppedFrames = %d, want 1", got) - } - select { - case <-c.Done(): - t.Fatal("dropped ping closed the connection") - default: - } -} - -func TestOnCloseRunsExactlyOnce(t *testing.T) { - t.Parallel() - - calls := 0 - c := NewConn(nil, Config{OnClose: func() { calls++ }}) - c.Close() - c.Close() - if calls != 1 { - t.Fatalf("OnClose calls = %d, want 1", calls) - } -} - -func TestDataQueueEnforcesByteLimit(t *testing.T) { - t.Parallel() - - c := NewConn(nil, Config{ - QueueSize: 4, - QueueBytes: 4, - WriteTimeout: 20 * time.Millisecond, - }) - if err := c.Enqueue(Frame{Class: FrameData, Kind: "chat.event", Data: []byte("1234")}); err != nil { - t.Fatalf("Enqueue within byte limit = %v, want nil", err) - } - if err := c.Enqueue(Frame{Class: FrameData, Kind: "chat.event", Data: []byte("5")}); !errors.Is(err, ErrWriteQueueFull) { - t.Fatalf("Enqueue beyond aggregate byte limit = %v, want ErrWriteQueueFull", err) - } - if got := c.QueueByteOverflows(); got != 1 { - t.Fatalf("QueueByteOverflows = %d, want 1", got) - } -} - -func TestFrameLargerThanByteLimitFailsImmediately(t *testing.T) { - t.Parallel() - - c := NewConn(nil, Config{QueueSize: 4, QueueBytes: 4, WriteTimeout: time.Second}) - started := time.Now() - err := c.Enqueue(Frame{Class: FrameData, Kind: "chat.event", Data: []byte("12345")}) - if !errors.Is(err, ErrWriteFrameTooLarge) { - t.Fatalf("oversized Enqueue = %v, want ErrWriteFrameTooLarge", err) - } - if waited := time.Since(started); waited > 100*time.Millisecond { - t.Fatalf("oversized Enqueue waited %s, want immediate rejection", waited) - } - if got := c.QueueByteOverflows(); got != 1 { - t.Fatalf("QueueByteOverflows = %d, want 1", got) - } -} - -func TestFirstWriteErrorClosesWithoutRetry(t *testing.T) { - t.Parallel() - - c := NewConn(nil, Config{QueueSize: 1, QueueBytes: 16, WriteTimeout: time.Second}) - var calls atomic.Int64 - c.writeOverride = func(Frame) error { - calls.Add(1) - return errors.New("write failed") - } - c.StartWriteLoop() - if err := c.Enqueue(Frame{Class: FrameData, Kind: "chat.event", Data: []byte("payload")}); err != nil { - t.Fatalf("Enqueue = %v, want nil", err) - } - select { - case <-c.Done(): - case <-time.After(time.Second): - t.Fatal("write error did not close connection") - } - if got := calls.Load(); got != 1 { - t.Fatalf("write attempts = %d, want exactly 1", got) - } - if got := c.WriterCloses(); got != 1 { - t.Fatalf("WriterCloses = %d, want 1", got) - } -} diff --git a/crates/agent-gateway/internal/transport/wscore/frame.go b/crates/agent-gateway/internal/transport/wscore/frame.go deleted file mode 100644 index c3e0a6bcf..000000000 --- a/crates/agent-gateway/internal/transport/wscore/frame.go +++ /dev/null @@ -1,39 +0,0 @@ -// Package wscore 提供 v2 WebSocket 协议共用的连接运行时(优先级双队列写泵、帧数/字节 -// 双限、空闲驱逐与心跳)。对帧格式无感:帧以已编码字节入队,协议层负责编码并声明 -// 拥塞策略(Frame.Class);第一次底层写错误会立即关闭连接并交给重连恢复。 -package wscore - -import "errors" - -// ErrWriteQueueFull 表示帧因持续拥塞被丢弃;协议层可据此对单个流降级恢复而不牺牲整条连接。 -var ErrWriteQueueFull = errors.New("write queue full") - -// ErrWriteFrameTooLarge 表示单帧本身已经超过所属队列的字节预算,继续等待不会恢复。 -var ErrWriteFrameTooLarge = errors.New("write frame exceeds queue byte limit") - -// FrameClass 决定帧的入队队列与拥塞策略。 -type FrameClass uint8 - -const ( - // FrameData 是可掉帧的事件/广播数据:走数据队列,持续拥塞时丢弃并返回 ErrWriteQueueFull。 - FrameData FrameClass = iota - // FrameControl 是尽力送达的控制帧:走优先队列越过数据积压;持续拥塞时丢弃报错但不关连接。 - FrameControl - // FramePing 是周期心跳:走优先队列,队列满时静默丢弃(下个周期取代)。 - FramePing - // FrameResponse 是请求关联响应:静默丢弃会让客户端挂到超时,故持续拥塞时关连接促使重连重试。 - FrameResponse -) - -// Frame 是写泵承载的单帧描述,载荷为已编码字节。 -type Frame struct { - Class FrameClass - // RequestID 为关联响应的请求 id,仅用于诊断。 - RequestID string - // Kind 是帧类型标签(v2 oneof 臂名 / v2 oneof 臂名),仅用于掉帧日志与测试断言。 - Kind string - // MessageType 为 websocket.TextMessage 或 websocket.BinaryMessage。 - MessageType int - // Data 为完整的已编码帧载荷。 - Data []byte -} diff --git a/crates/agent-gateway/internal/transport/wscore/heartbeat.go b/crates/agent-gateway/internal/transport/wscore/heartbeat.go deleted file mode 100644 index 43d918343..000000000 --- a/crates/agent-gateway/internal/transport/wscore/heartbeat.go +++ /dev/null @@ -1,44 +0,0 @@ -package wscore - -import ( - "time" - - "github.com/gorilla/websocket" -) - -// StartHeartbeat 启动心跳与空闲驱逐循环(幂等),每周期依次:空闲检查(超过 IdleTimeout -// 即关连接,唯一驱逐裁决点);WS 控制帧 ping(浏览器网络进程应答,冻结/节流标签页也能证明存活); -// buildPing 构造的应用层 ping(页面 JS 唯一可观测入站活动,尽力而为,掉帧不影响驱逐裁决)。 -func (c *Conn) StartHeartbeat(buildPing func() (Frame, bool)) { - c.heartbeatOnce.Do(func() { - period := c.cfg.HeartbeatPeriod - if period <= 0 { - period = defaultHeartbeatPeriod - } - go func() { - ticker := time.NewTicker(period) - defer ticker.Stop() - for { - select { - case <-c.done: - return - case <-ticker.C: - c.lastInboundMu.Lock() - lastInbound := c.lastInboundAt - c.lastInboundMu.Unlock() - if time.Since(lastInbound) > c.IdleTimeout() { - c.Close() - return - } - deadline := time.Now().Add(c.ControlWriteTimeout()) - _ = c.ws.WriteControl(websocket.PingMessage, nil, deadline) - if buildPing != nil { - if frame, ok := buildPing(); ok { - _ = c.Enqueue(frame) - } - } - } - } - }() - }) -} diff --git a/crates/agent-gateway/internal/transport/wscore/limits.go b/crates/agent-gateway/internal/transport/wscore/limits.go deleted file mode 100644 index 4bedf9dc1..000000000 --- a/crates/agent-gateway/internal/transport/wscore/limits.go +++ /dev/null @@ -1,90 +0,0 @@ -package wscore - -import ( - "sync" - "time" -) - -// DispatchLimiter 限制单连接的在途派发数:读循环 TryAcquire 失败即拒绝该请求 -// (绝不阻塞读循环——阻塞会拖死 pong/存活检测),处理 goroutine 结束时 Release。 -// 没有它,慢速直通请求(可阻塞至 requestTimeout)会随请求速率无界累积 goroutine。 -type DispatchLimiter struct { - slots chan struct{} -} - -func NewDispatchLimiter(limit int) *DispatchLimiter { - if limit <= 0 { - limit = 16 - } - return &DispatchLimiter{slots: make(chan struct{}, limit)} -} - -func (l *DispatchLimiter) TryAcquire() bool { - select { - case l.slots <- struct{}{}: - return true - default: - return false - } -} - -func (l *DispatchLimiter) Release() { - select { - case <-l.slots: - default: - } -} - -// InboundRateLimiter 是单连接入站帧的令牌桶:快帧(本地应答、解析即弃)不受 -// 派发信号量约束,仍可打满 CPU(每帧一次 Unmarshal + 分发),令牌桶补上这一段。 -// 连续违规超过阈值判定为失控客户端,调用方应关闭连接。 -type InboundRateLimiter struct { - mu sync.Mutex - tokens float64 - burst float64 - perSecond float64 - lastRefill time.Time - - violations int - maxViolations int -} - -func NewInboundRateLimiter(perSecond, burst float64, maxViolations int) *InboundRateLimiter { - if perSecond <= 0 { - perSecond = 100 - } - if burst <= 0 { - burst = perSecond * 2 - } - if maxViolations <= 0 { - maxViolations = 3 - } - return &InboundRateLimiter{ - tokens: burst, - burst: burst, - perSecond: perSecond, - lastRefill: time.Now(), - maxViolations: maxViolations, - } -} - -// Allow 消费一个令牌。第二返回值为 true 表示连续违规已超阈值,连接应被关闭。 -func (l *InboundRateLimiter) Allow() (ok bool, exceeded bool) { - l.mu.Lock() - defer l.mu.Unlock() - - now := time.Now() - l.tokens += now.Sub(l.lastRefill).Seconds() * l.perSecond - if l.tokens > l.burst { - l.tokens = l.burst - } - l.lastRefill = now - - if l.tokens >= 1 { - l.tokens -= 1 - l.violations = 0 - return true, false - } - l.violations += 1 - return false, l.violations >= l.maxViolations -} diff --git a/crates/agent-gateway/proto/v2/gateway.proto b/crates/agent-gateway/proto/v2/gateway.proto deleted file mode 100644 index 5706cab87..000000000 --- a/crates/agent-gateway/proto/v2/gateway.proto +++ /dev/null @@ -1,1282 +0,0 @@ -syntax = "proto3"; - -package liveagent.gateway.v2; - -option go_package = "github.com/liveagent/agent-gateway/internal/proto/v2;gatewayv2"; - -// 三端共享的 v2 业务消息单一事实源。WebSocket 帧壳见 gateway_ws.proto; -// 已发布消息的字段编号只增不改。 - -message GatewayEnvelope { - string request_id = 1; - int64 timestamp = 2; - - oneof payload { - ChatCommandRequest chat_command = 10; - CronManageRequest cron_manage = 20; - HistoryListRequest history_list = 30; - HistoryGetRequest history_get = 31; - HistoryRenameRequest history_rename = 32; - HistoryDeleteRequest history_delete = 33; - HistoryPrefixRequest history_prefix = 34; - HistoryPinRequest history_pin = 35; - HistoryShareGetRequest history_share_get = 36; - HistoryShareSetRequest history_share_set = 37; - HistoryShareResolveRequest history_share_resolve = 38; - HistoryWorkdirsRequest history_workdirs = 39; - ProviderListRequest provider_list = 40; - SettingsGetRequest settings_get = 41; - SettingsUpdateRequest settings_update = 42; - SkillFilesListRequest skill_files_list = 43; - SkillMetadataReadRequest skill_metadata_read = 44; - SkillTextReadRequest skill_text_read = 45; - FileMentionListRequest file_mention_list = 46; - UploadReadableFilesRequest upload_readable_files = 47; - FsRootsRequest fs_roots = 48; - FsListDirsRequest fs_list_dirs = 49; - PingRequest ping = 50; - UploadedImagePreviewRequest uploaded_image_preview = 51; - MemoryManageRequest memory_manage = 52; - SkillManageRequest skill_manage = 53; - FsCreateProjectFolderRequest fs_create_project_folder = 54; - TerminalRequest terminal_request = 55; - FsListRequest fs_list = 56; - FsWriteTextRequest fs_write_text = 57; - FsCreateDirRequest fs_create_dir = 58; - FsRenameRequest fs_rename = 59; - FsDeleteRequest fs_delete = 60; - GitRequest git_request = 61; - FsReadEditableTextRequest fs_read_editable_text = 62; - FsReadWorkspaceImageRequest fs_read_workspace_image = 63; - SftpRequest sftp_request = 64; - ProviderModelsRequest provider_models = 65; - SettingsResetSshKnownHostRequest settings_reset_ssh_known_host = 72; - ChatQueueRequest chat_queue = 73; - ChatIngressAck chat_ingress_ack = 75; - TunnelStateSnapshot tunnel_state = 80; - TunnelMutation tunnel_mutation = 81; - TunnelFrame tunnel_frame = 82; - WorkspaceWatchRequest workspace_watch = 90; - ManagedProcessRequest managed_process_request = 91; - HistoryBranchRequest history_branch = 92; - ProviderUsageRequest provider_usage = 93; - ChatFileOpenRequest chat_file_open = 94; - } - - // Legacy tunnel control/frame payloads (pre-rewrite protocol) and the - // never-wired chat event replay request. - reserved 67, 68, 69, 74; -} - -message AgentEnvelope { - string request_id = 1; - int64 timestamp = 2; - - oneof payload { - ChatEvent chat_event = 10; - CronManageResponse cron_manage_resp = 20; - HistoryListResponse history_list_resp = 30; - HistoryGetResponse history_get_resp = 31; - HistoryRenameResponse history_rename_resp = 32; - HistoryDeleteResponse history_delete_resp = 33; - HistorySyncEvent history_sync = 34; - HistoryPrefixResponse history_prefix_resp = 35; - HistoryPinResponse history_pin_resp = 36; - HistoryShareGetResponse history_share_get_resp = 37; - HistoryShareSetResponse history_share_set_resp = 38; - HistoryShareResolveResponse history_share_resolve_resp = 39; - HistoryWorkdirsResponse history_workdirs_resp = 56; - ProviderListResponse provider_list_resp = 40; - SettingsGetResponse settings_get_resp = 41; - SettingsUpdateResponse settings_update_resp = 42; - SettingsSyncEvent settings_sync = 43; - SkillFilesListResponse skill_files_list_resp = 44; - SkillMetadataReadResponse skill_metadata_read_resp = 45; - SkillTextReadResponse skill_text_read_resp = 46; - FileMentionListResponse file_mention_list_resp = 47; - UploadReadableFilesResponse upload_readable_files_resp = 48; - FsRootsResponse fs_roots_resp = 49; - PongResponse pong = 50; - FsListDirsResponse fs_list_dirs_resp = 51; - UploadedImagePreviewResponse uploaded_image_preview_resp = 52; - MemoryManageResponse memory_manage_resp = 53; - SkillManageResponse skill_manage_resp = 54; - FsCreateProjectFolderResponse fs_create_project_folder_resp = 55; - TerminalResponse terminal_response = 57; - TerminalEvent terminal_event = 58; - FsListResponse fs_list_resp = 59; - FsWriteTextResponse fs_write_text_resp = 60; - FsCreateDirResponse fs_create_dir_resp = 61; - FsRenameResponse fs_rename_resp = 62; - FsDeleteResponse fs_delete_resp = 63; - GitResponse git_response = 64; - FsReadEditableTextResponse fs_read_editable_text_resp = 65; - FsReadWorkspaceImageResponse fs_read_workspace_image_resp = 66; - SftpResponse sftp_response = 73; - SftpEvent sftp_event = 74; - ChatQueueResponse chat_queue_resp = 75; - ChatQueueEvent chat_queue_event = 76; - ChatControlEvent chat_control = 70; - RuntimeStatusEvent runtime_status = 71; - SettingsResetSshKnownHostResponse settings_reset_ssh_known_host_resp = 72; - ChatRuntimeSnapshot chat_runtime_snapshot = 77; - ProviderModelsResponse provider_models_resp = 79; - TunnelDesiredState tunnel_desired = 80; - TunnelMutationResult tunnel_mutation_result = 81; - TunnelFrame tunnel_frame = 82; - TunnelProbeReport tunnel_probe_report = 83; - WorkspaceActivityEvent workspace_activity = 90; - ManagedProcessResponse managed_process_response = 91; - ManagedProcessSnapshot managed_process_snapshot = 92; - HistoryBranchResponse history_branch_resp = 93; - ProviderUsageResponse provider_usage_resp = 94; - ChatIngressBatch chat_ingress_batch = 95; - ChatIngressResume chat_ingress_resume = 96; - ChatIngressFragment chat_ingress_fragment = 97; - ChatFileOpenResponse chat_file_open_resp = 98; - ErrorResponse error = 99; - } - - // Legacy tunnel control/frame payloads (pre-rewrite protocol) and the - // never-wired chat event replay response. - reserved 67, 68, 69, 78; -} - -message ChatSelectedModel { - string custom_provider_id = 1; - string model = 2; - string provider_type = 3; -} - -message ChatRuntimeControls { - bool thinking_enabled = 1; - bool native_web_search_enabled = 2; - string reasoning = 3; -} - -message ChatUploadedFile { - string relative_path = 1; - string absolute_path = 2; - string file_name = 3; - string kind = 4; - int64 size_bytes = 5; -} - -message UploadReadableFile { - string file_name = 1; - string mime_type = 2; - bytes content = 3; -} - -message UploadReadableFilesRequest { - string workdir = 1; - repeated UploadReadableFile files = 2; -} - -message UploadReadableFilesResponse { - repeated ChatUploadedFile files = 1; - repeated string skipped = 2; -} - -message UploadedImagePreviewRequest { - string workdir = 1; - string absolute_path = 2; -} - -message UploadedImagePreviewResponse { - string mime_type = 1; - string data = 2; -} - -// ---- Tunnel desired state (agent -> gateway) ---- - -message TunnelSpec { - string id = 1; // agent-generated, stable across restarts - string slug_hint = 2; // last allocated slug; gateway honors when valid and unused - string name = 3; - string target_url = 4; // http://localhost:PORT[/base] - int64 expires_at = 5; // unix seconds, 0 = never - string project_path_key = 6; -} - -message TunnelDesiredState { - repeated TunnelSpec tunnels = 1; - uint64 revision = 2; // agent-side monotonic -} - -// ---- Tunnel runtime snapshot (gateway -> agent, mirrored to /ws JSON) ---- - -message TunnelHealth { - string status = 1; // "ok" | "failed" | "unknown" - uint32 http_status = 2; // local layer only - string error = 3; - int64 checked_at = 4; - uint32 rtt_ms = 5; // relay layer only -} - -message TunnelStatus { - string id = 1; - string slug = 2; - string name = 3; - string target_url = 4; - string public_path = 5; // "/t/{slug}/"; clients compose the full URL - int64 created_at = 6; - int64 expires_at = 7; - uint32 active_connections = 8; - string project_path_key = 9; - TunnelHealth local = 10; // agent -> local service reachability -} - -message TunnelStateSnapshot { - repeated TunnelStatus tunnels = 1; - uint64 revision = 2; // gateway-side monotonic - bool agent_online = 3; - TunnelHealth relay = 4; // gateway <-> agent frame path -} - -// ---- Tunnel mutations forwarded gateway -> agent (webui-originated) ---- - -message TunnelMutation { - string action = 1; // "create" | "update" | "close" | "check" - string tunnel_id = 2; // update/close/check - string target_url = 3; - string name = 4; - optional uint32 ttl_seconds = 5; // absent on update = keep current expiry - string project_path_key = 6; -} - -message TunnelMutationResult { - string tunnel_id = 1; - string error_code = 2; // "" = ok; invalid_target|limit_exceeded|not_found|invalid_ttl - string error_message = 3; -} - -// ---- Tunnel local-service probe results (agent -> gateway) ---- - -message TunnelProbeResult { - string tunnel_id = 1; - TunnelHealth local = 2; -} - -message TunnelProbeReport { - repeated TunnelProbeResult results = 1; -} - -// ---- Tunnel data plane ---- - -message TunnelHeader { - string name = 1; - string value = 2; -} - -enum TunnelFrameKind { - TUNNEL_FRAME_KIND_UNSPECIFIED = 0; - TUNNEL_FRAME_KIND_HTTP_REQUEST_START = 1; - TUNNEL_FRAME_KIND_HTTP_REQUEST_BODY = 2; - TUNNEL_FRAME_KIND_HTTP_REQUEST_END = 3; - TUNNEL_FRAME_KIND_HTTP_RESPONSE_START = 4; - TUNNEL_FRAME_KIND_HTTP_RESPONSE_BODY = 5; - TUNNEL_FRAME_KIND_HTTP_RESPONSE_END = 6; - TUNNEL_FRAME_KIND_WS_DIAL = 7; - TUNNEL_FRAME_KIND_WS_DIAL_OK = 8; - TUNNEL_FRAME_KIND_WS_DIAL_ERROR = 9; - TUNNEL_FRAME_KIND_WS_FRAME = 10; - TUNNEL_FRAME_KIND_WS_CLOSE = 11; - TUNNEL_FRAME_KIND_ERROR = 12; - TUNNEL_FRAME_KIND_CANCEL = 13; - TUNNEL_FRAME_KIND_PING = 14; - TUNNEL_FRAME_KIND_PONG = 15; -} - -enum TunnelWsMessageType { - TUNNEL_WS_MESSAGE_TYPE_UNSPECIFIED = 0; - TUNNEL_WS_MESSAGE_TYPE_TEXT = 1; - TUNNEL_WS_MESSAGE_TYPE_BINARY = 2; -} - -message TunnelFrame { - string stream_id = 1; - TunnelFrameKind kind = 2; - string target_url = 3; // set on HTTP_REQUEST_START and WS_DIAL only - string method = 4; - string path = 5; // path+query relative to the target base - repeated TunnelHeader headers = 6; - uint32 status = 7; - bytes body = 8; - string error = 9; - TunnelWsMessageType ws_message_type = 10; - string ws_subprotocol = 11; - uint32 ws_close_code = 12; - string ws_close_reason = 13; -} - -// ---- Workspace activity watch (gateway -> agent declarative full set, -// ---- agent -> gateway debounced change events) ---- - -message WorkspaceWatchRequest { - repeated string workdirs = 1; // full desired set; replaces the previous one -} - -message WorkspaceActivityEvent { - string workdir = 1; - uint64 revision = 2; // per-workdir monotonic within one agent process - bool fs = 3; // working-tree content changed - bool git = 4; // git state (HEAD/refs/index/...) changed - repeated string changed_paths = 5; // relative to workdir, deduped, capped - bool truncated = 6; // changed_paths hit the cap or is unknown -} - -// ---- Managed processes (ManagedProcess tool children on the agent host). -// ---- Agent broadcasts full-table snapshots; the gateway caches the latest -// ---- and fans it out to webui subscribers. Panel operations are forwarded -// ---- gateway -> agent and answered with the same request_id. ---- - -message ManagedProcessRecord { - string id = 1; - string label = 2; - string command = 3; - string cwd = 4; - string shell = 5; - uint32 pid = 6; - string log_path = 7; - int64 started_at = 8; // unix ms - optional int64 finished_at = 9; // unix ms - optional int32 exit_code = 10; // absent for restored/pid-managed entries - bool running = 11; - bool isolated = 12; // survives LiveAgent exit - bool restored = 13; // recovered after restart; managed by pid -} - -message ManagedProcessSnapshot { - repeated ManagedProcessRecord processes = 1; - uint64 revision = 2; // agent-side monotonic, restart-safe -} - -message ManagedProcessRequest { - string action = 1; // "snapshot" | "stop" | "read_log" | "clear" - string process_id = 2; // stop/read_log; optional for clear - uint32 max_bytes = 3; // read_log only; 0 = default -} - -message ManagedProcessResponse { - string action = 1; - ManagedProcessSnapshot snapshot = 2; // set for snapshot/stop/clear - string log_content = 3; // read_log only - string log_path = 4; // read_log only - bool log_truncated = 5; // read_log only - bool stopped = 6; // stop only -} - -message MemoryManageRequest { - string command = 1; - string args_json = 2; -} - -message MemoryManageResponse { - string result_json = 1; -} - -message TerminalRequest { - string action = 1; - string session_id = 2; - string project_path_key = 3; - string cwd = 4; - string shell = 5; - string title = 6; - string data = 7; - uint32 cols = 8; - uint32 rows = 9; - uint32 max_bytes = 10; - string ssh_host_id = 11; - string prompt_id = 12; - string prompt_answer = 13; - bool trust_host_key = 14; - bool sftp_enabled = 15; - string tab_id = 16; - string tab_kind = 17; - string remote_host = 18; - uint32 remote_port = 19; - uint32 local_port = 20; - string forward_id = 21; -} - -message TerminalSession { - string id = 1; - string project_path_key = 2; - string cwd = 3; - string shell = 4; - string title = 5; - uint32 pid = 6; - uint32 cols = 7; - uint32 rows = 8; - uint64 created_at = 9; - uint64 updated_at = 10; - uint64 finished_at = 11; - int32 exit_code = 12; - bool running = 13; - string kind = 14; - TerminalSshMetadata ssh = 15; -} - -message TerminalSshMetadata { - string host_id = 1; - string host_name = 2; - string username = 3; - string host = 4; - uint32 port = 5; - string auth_type = 6; - string status = 7; - uint32 reconnect_attempt = 8; - uint32 reconnect_max_attempts = 9; - bool sftp_enabled = 10; -} - -message SftpRequest { - string action = 1; - string session_id = 2; - string project_path_key = 3; - string workdir = 4; - string local_path = 5; - string remote_path = 6; - string from_path = 7; - string to_path = 8; - string direction = 9; - string target_path = 10; - bool recursive = 11; - bool overwrite = 12; -} - -message SftpEntry { - string path = 1; - string name = 2; - string kind = 3; - uint64 size_bytes = 4; - uint64 mtime = 5; -} - -message SftpTransfer { - string id = 1; - string session_id = 2; - string direction = 3; - string status = 4; - string source_path = 5; - string target_path = 6; - string current_path = 7; - uint64 bytes_done = 8; - uint64 bytes_total = 9; - uint32 files_done = 10; - uint32 files_total = 11; - string error = 12; -} - -message SftpResponse { - string action = 1; - string path = 2; - repeated SftpEntry entries = 3; - SftpEntry entry = 4; - bool exists = 5; - SftpTransfer transfer = 6; -} - -message SftpEvent { - string kind = 1; - SftpTransfer transfer = 2; -} - -message TerminalSshPrompt { - string id = 1; - string kind = 2; - string host_id = 3; - string host_name = 4; - string host = 5; - uint32 port = 6; - string message = 7; - string fingerprint_sha256 = 8; - string key_type = 9; - bool answer_echo = 10; -} - -message TerminalShellOption { - string id = 1; - string label = 2; - string command = 3; -} - -message TerminalSshTab { - string id = 1; - string session_id = 2; - string project_path_key = 3; - string kind = 4; - uint64 created_at = 5; - uint64 updated_at = 6; -} - -message TerminalSshTabsSnapshot { - reserved 3; - string project_path_key = 1; - repeated TerminalSshTab tabs = 2; - uint64 revision = 4; -} - -message TerminalSshLocalForward { - string id = 1; - string session_id = 2; - string project_path_key = 3; - string local_host = 4; - uint32 local_port = 5; - string address = 6; - string remote_host = 7; - uint32 remote_port = 8; - string status = 9; - uint64 created_at = 10; - uint64 updated_at = 11; - string error = 12; -} - -message TerminalSshLocalForwardsSnapshot { - repeated TerminalSshLocalForward forwards = 1; - uint64 revision = 2; -} - -message TerminalSshLocalForwardAction { - TerminalSshLocalForward forward = 1; - uint64 revision = 2; - // Only set on events: started | stopped | failed. - string kind = 3; -} - -message TerminalResponse { - string action = 1; - repeated TerminalSession sessions = 2; - TerminalSession session = 3; - bytes output = 4; - bool truncated = 5; - repeated TerminalShellOption shell_options = 6; - string default_shell = 7; - uint64 output_start_offset = 8; - uint64 output_end_offset = 9; - TerminalSshPrompt ssh_prompt = 10; - uint32 latency_ms = 11; - TerminalSshTabsSnapshot ssh_tabs = 12; - TerminalSshLocalForwardsSnapshot ssh_local_forwards = 13; - TerminalSshLocalForwardAction ssh_local_forward = 14; - bool ssh_local_forward_port_available = 15; -} - -message TerminalEvent { - string kind = 1; - string session_id = 2; - string project_path_key = 3; - TerminalSession session = 4; - bytes data = 5; - uint64 output_start_offset = 6; - uint64 output_end_offset = 7; - TerminalSshTabsSnapshot ssh_tabs = 8; - TerminalSshLocalForwardAction ssh_local_forward = 9; -} - -message TerminalStreamFrame { - string kind = 1; - string stream_id = 2; - string session_id = 3; - string project_path_key = 4; - uint64 seq = 5; - uint64 start_offset = 6; - uint64 end_offset = 7; - uint32 cols = 8; - uint32 rows = 9; - uint32 max_bytes = 10; - bool truncated = 11; - string error = 12; - TerminalSession session = 13; - bytes data = 14; -} - -message GitRequest { - string action = 1; - string workdir = 2; - string args_json = 3; -} - -message GitResponse { - string action = 1; - string result_json = 2; -} - -message ChatRequest { - string conversation_id = 1; - string message = 2; - ChatSelectedModel selected_model = 3; - string execution_mode = 4; - string workdir = 5; - reserved 6; - reserved "selected_system_tools"; - repeated ChatUploadedFile uploaded_files = 7; - string client_request_id = 8; - ChatRuntimeControls runtime_controls = 9; - string queue_policy = 10; -} - -message ChatMessageRef { - int32 segment_index = 1; - int32 message_index = 2; - string segment_id = 3; - string message_id = 4; - string role = 5; - string content_hash = 6; -} - -message CancelChatRequest { - string conversation_id = 1; - // 可选运行 id 提示:v2 浏览器链路用它消除同会话并发运行的歧义;桌面端可忽略。 - string run_id = 2; -} - -message ChatCommandRequest { - string type = 1; - ChatRequest request = 2; - ChatMessageRef base_message_ref = 3; - CancelChatRequest cancel = 4; -} - -message ChatQueueRequest { - string action = 1; - string conversation_id = 2; - string item_id = 3; - string direction = 4; - uint64 revision = 5; - string draft_json = 6; - string uploaded_files_json = 7; - string request_json = 8; -} - -message ChatQueueResponse { - bool accepted = 1; - string message = 2; - string snapshot_json = 3; - string item_json = 4; - string error_code = 5; - uint64 revision = 6; -} - -message ChatQueueEvent { - string conversation_id = 1; - string snapshot_json = 2; - uint64 revision = 3; -} - -message ChatEvent { - ChatEventType type = 1; - string conversation_id = 2; - string data = 3; - - enum ChatEventType { - TOKEN = 0; - THINKING = 1; - TOOL_CALL = 2; - TOOL_RESULT = 3; - DONE = 4; - ERROR = 5; - TOOL_STATUS = 6; - HOSTED_SEARCH = 7; - USER_MESSAGE = 8; - } -} - -message ChatControlEvent { - string request_id = 1; - string client_request_id = 2; - string conversation_id = 3; - int64 run_epoch = 4; - string type = 5; - string state = 6; - string error_code = 7; - string message = 8; - int64 seq = 9; -} - -message ChatRuntimeSnapshot { - string conversation_id = 1; - string run_id = 2; - string client_request_id = 3; - string worker_id = 4; - string state = 5; - string cwd = 6; - int64 updated_at = 7; - int64 revision = 8; - string entries_json = 9; - string tool_status = 10; - bool tool_status_is_compaction = 11; -} - -message RuntimeStatusEvent { - string worker_id = 1; - string state = 2; - bool visible = 3; - uint32 active_run_count = 4; - int64 timestamp = 5; - repeated ChatRunReport active_runs = 6; - repeated ChatRunReport finished_runs = 7; -} - -message ChatRunReport { - string run_id = 1; - string conversation_id = 2; - string state = 3; - string error_code = 4; - string message = 5; - int64 updated_at = 6; -} - -message CronManageRequest { - string action = 1; - string task_id = 2; - string task_json = 3; -} - -message CronManageResponse { - string action = 1; - string result_json = 2; -} - -message HistoryListRequest { - int32 page = 1; - int32 page_size = 2; - string cwd = 3; - bool cwd_empty = 4; -} - -message HistoryListResponse { - repeated ConversationSummary conversations = 1; - int32 total_count = 2; -} - -message ConversationSummary { - string id = 1; - string title = 2; - int64 created_at = 3; - int64 updated_at = 4; - int32 message_count = 5; - string provider_id = 6; - string model = 7; - string session_id = 8; - string cwd = 9; - bool is_pinned = 10; - int64 pinned_at = 11; - bool is_shared = 12; - string selected_model_json = 13; -} - -message HistoryGetRequest { - string conversation_id = 1; - int32 max_messages = 2; -} - -message HistoryGetResponse { - string conversation_id = 1; - string messages_json = 2; - int32 total_message_count = 3; - int32 returned_message_count = 4; - bool has_more = 5; - ConversationSummary conversation = 6; -} - -message HistoryPrefixRequest { - string conversation_id = 1; - int32 max_messages = 2; - ChatMessageRef base_message_ref = 3; -} - -message HistoryPrefixResponse { - string conversation_id = 1; - string messages_json = 2; - int32 total_message_count = 3; - int32 returned_message_count = 4; - bool has_more = 5; - ConversationSummary conversation = 6; -} - -message HistoryRenameRequest { - string conversation_id = 1; - string title = 2; -} - -message HistoryRenameResponse { - ConversationSummary conversation = 1; -} - -// Copies the conversation prefix up to and including the assistant response -// that answers the anchored user message (base_message_ref) into a brand-new -// conversation titled "新分支". -message HistoryBranchRequest { - string conversation_id = 1; - ChatMessageRef base_message_ref = 2; -} - -message HistoryBranchResponse { - ConversationSummary conversation = 1; -} - -message HistoryPinRequest { - string conversation_id = 1; - bool is_pinned = 2; -} - -message HistoryPinResponse { - ConversationSummary conversation = 1; -} - -message HistoryShareStatus { - string conversation_id = 1; - bool enabled = 2; - string token = 3; - int64 created_at = 4; - int64 updated_at = 5; - bool redact_tool_content = 6; -} - -message HistoryShareGetRequest { - string conversation_id = 1; -} - -message HistoryShareGetResponse { - HistoryShareStatus share = 1; -} - -message HistoryShareSetRequest { - string conversation_id = 1; - bool enabled = 2; - optional bool redact_tool_content = 3; -} - -message HistoryShareSetResponse { - HistoryShareStatus share = 1; -} - -message HistoryShareResolveRequest { - string token = 1; -} - -message HistoryShareResolveResponse { - string conversation_id = 1; - string messages_json = 2; - int32 total_message_count = 3; - ConversationSummary conversation = 4; - bool redact_tool_content = 5; -} - -message HistoryWorkdirsRequest {} - -message HistoryWorkdirSummary { - string path = 1; - int32 conversation_count = 2; - int64 updated_at = 3; -} - -message HistoryWorkdirsResponse { - repeated HistoryWorkdirSummary workdirs = 1; -} - -message HistoryDeleteRequest { - string conversation_id = 1; -} - -message HistoryDeleteResponse {} - -message HistorySyncEvent { - string kind = 1; - ConversationSummary conversation = 2; - string conversation_id = 3; -} - -message ProviderListRequest {} - -message ProviderListResponse { - string providers_json = 1; -} - -message SettingsGetRequest {} - -message SettingsGetResponse { - string settings_json = 1; -} - -message SettingsUpdateRequest { - string settings_json = 1; -} - -message SettingsUpdateResponse { - bool accepted = 1; - string message = 2; -} - -message SettingsResetSshKnownHostRequest { - string host = 1; - uint32 port = 2; -} - -message SettingsResetSshKnownHostResponse { - uint32 deleted = 1; -} - -message SettingsSyncEvent { - string settings_json = 1; -} - -message SkillFilesListRequest {} - -message SkillFilesListResponse { - string root_dir = 1; - repeated string paths = 2; - bool truncated = 3; -} - -message SkillMetadataReadRequest { - string path = 1; -} - -message SkillMetadataReadResponse { - string name = 1; - string description = 2; -} - -message SkillTextReadRequest { - string path = 1; - uint32 offset = 2; - uint32 length = 3; -} - -message SkillTextReadResponse { - string content = 1; - bool truncated = 2; -} - -message SkillManageRequest { - string payload_json = 1; -} - -message SkillManageResponse { - string result_json = 1; -} - -message FileMentionListRequest { - string workdir = 1; - uint32 max_results = 2; - string query = 3; - optional bool show_hidden = 4; -} - -message FileMentionEntry { - string path = 1; - string kind = 2; - bool hidden = 3; -} - -message FileMentionListResponse { - repeated FileMentionEntry entries = 1; - bool truncated = 2; -} - -message FsRoot { - string id = 1; - string path = 2; - string kind = 3; - string label = 4; -} - -message FsRootsRequest {} - -message FsRootsResponse { repeated FsRoot roots = 1; } - -message FsListDirsRequest { - string path = 1; - uint32 max_results = 2; -} - -message FsDirEntry { - string path = 1; - string name = 2; -} - -message FsListDirsResponse { - string path = 1; - repeated FsDirEntry entries = 2; - bool truncated = 3; -} - -message FsCreateProjectFolderRequest { - string parent = 1; - string name = 2; -} - -message FsCreateProjectFolderResponse { - string path = 1; -} - -message FsListRequest { - string workdir = 1; - string path = 2; - uint32 depth = 3; - uint32 offset = 4; - uint32 max_results = 5; - optional bool show_hidden = 6; -} - -message FsListEntry { - string path = 1; - string kind = 2; - bool hidden = 3; -} - -message FsListResponse { - string path = 1; - bool has_path = 2; - uint32 depth = 3; - uint32 offset = 4; - uint32 max_results = 5; - uint32 total = 6; - bool has_more = 7; - repeated FsListEntry entries = 8; -} - -message FsReadEditableTextRequest { - string workdir = 1; - string path = 2; -} - -message FsReadEditableTextResponse { - string path = 1; - string content = 2; - uint64 mtime_ms = 3; - string content_hash = 4; - uint64 size_bytes = 5; - uint64 total_lines = 6; -} - -message FsReadWorkspaceImageRequest { - string workdir = 1; - string path = 2; -} - -message FsReadWorkspaceImageResponse { - string path = 1; - string mime_type = 2; - string data = 3; - uint64 size_bytes = 4; - uint64 mtime_ms = 5; - string content_hash = 6; -} - -message ChatFileOpenRequest { - string conversation_id = 1; - string workdir = 2; - string path = 3; - string source = 4; - optional uint32 line = 5; - optional uint32 end_line = 6; - optional uint32 column = 7; - bool open_in_file_manager = 8; -} - -message ChatFileOpenResponse { - string action = 1; - string kind = 2; - string workdir = 3; - string path = 4; - optional uint32 line = 5; - optional uint32 end_line = 6; - optional uint32 column = 7; - bool outside_workspace = 8; -} - -message FsWriteTextRequest { - string workdir = 1; - string path = 2; - string content = 3; - string mode = 4; - uint64 expected_mtime_ms = 5; - string expected_content_hash = 6; - bool has_expected_mtime_ms = 7; - bool has_expected_content_hash = 8; -} - -message FsWriteTextResponse { - string path = 1; - string mode = 2; - bool existed_before = 3; - uint64 bytes_written = 4; - uint64 mtime_ms = 5; - string content_hash = 6; - uint64 total_lines = 7; -} - -message FsCreateDirRequest { - string workdir = 1; - string path = 2; -} - -message FsCreateDirResponse { - string path = 1; - string kind = 2; -} - -message FsRenameRequest { - string workdir = 1; - string from_path = 2; - string to_path = 3; -} - -message FsRenameResponse { - string from_path = 1; - string path = 2; - string kind = 3; -} - -message FsDeleteRequest { - string workdir = 1; - string path = 2; -} - -message FsDeleteResponse { - string path = 1; - string kind = 2; -} - -message PingRequest { - int64 timestamp = 1; -} - -message PongResponse { - int64 timestamp = 1; -} - -message ErrorResponse { - int32 code = 1; - string message = 2; -} - -message ProviderModelsRequest { - string provider_type = 1; - string base_url = 2; - string api_key = 3; - bool use_system_proxy = 4; -} - -message ProviderModelsResponse { - string models_json = 1; -} - -message ProviderUsageRequest { - string provider_id = 1; - bool refresh = 2; - // 非空时为「按草稿测试」:桌面端按此 JSON 配置(UsageQueryConfig 形状)执行 - // 一次查询——忽略启用开关、不落库、不读写缓存;空串为常规查询。 - string config_json = 3; -} - -message ProviderUsageResponse { - string result_json = 1; -} - -// ---- Reliable chat ingress (desktop -> gateway) ---- - -// ChatIngressBatch carries contiguous logical records for one run. Each record -// occupies exactly one sequence number beginning at first_seq. -message ChatIngressBatch { - string run_id = 1; - string conversation_id = 2; - uint64 first_seq = 3; - repeated ChatIngressRecord records = 4; -} - -message ChatIngressRecord { - oneof payload { - ChatIngressDelta delta = 1; - ChatIngressCheckpoint checkpoint = 2; - ChatIngressTerminal terminal = 3; - ChatIngressHeartbeat heartbeat = 4; - } -} - -message ChatIngressDelta { - string event_json = 1; - string worker_id = 2; -} - -message ChatIngressHeartbeat { - // Unix milliseconds. - int64 updated_at = 1; -} - -message ChatIngressCheckpoint { - uint64 covers_through_seq = 1; - uint64 revision = 2; - bytes compressed_projection = 3; - uint64 uncompressed_bytes = 4; - string sha256 = 5; - bool content_complete = 6; - bool history_required = 7; -} - -message ChatIngressTerminal { - uint64 covers_through_seq = 1; - uint64 revision = 2; - bytes compressed_projection = 3; - uint64 uncompressed_bytes = 4; - string sha256 = 5; - bool content_complete = 6; - bool history_required = 7; - string state = 8; - string error_code = 9; - string error_message = 10; -} - -// ChatIngressResume declares the replay window retained by the desktop after a -// reconnect. The gateway answers each run with ChatIngressAck. -message ChatIngressResume { - repeated ChatIngressRunResume runs = 1; -} - -message ChatIngressRunResume { - string run_id = 1; - string conversation_id = 2; - uint64 replay_from_seq = 3; - uint64 replay_through_seq = 4; - uint64 next_seq = 5; - uint64 latest_checkpoint_seq = 6; - uint64 terminal_seq = 7; - bool terminal_pending = 8; -} - -// ChatIngressFragment transports one encoded ChatIngressRecord that exceeds a -// normal batch frame. Fragments do not consume additional logical sequence -// numbers; source_seq is the sequence of the reconstructed record. -message ChatIngressFragment { - string run_id = 1; - string conversation_id = 2; - uint64 source_seq = 3; - uint32 fragment_index = 4; - uint32 fragment_count = 5; - bytes encoded_record_chunk = 6; - uint64 encoded_record_bytes = 7; - string sha256 = 8; -} - -message ChatIngressAck { - string run_id = 1; - string conversation_id = 2; - uint64 committed_through = 3; - uint64 expected_next = 4; - Action action = 5; - bool terminal_committed = 6; - string error_code = 7; - string error_message = 8; - - enum Action { - ACTION_UNSPECIFIED = 0; - CONTINUE = 1; - REPLAY_FROM_EXPECTED = 2; - SEND_CHECKPOINT = 3; - REJECTED = 4; - } -} diff --git a/crates/agent-gateway/proto/v2/gateway_ws.proto b/crates/agent-gateway/proto/v2/gateway_ws.proto deleted file mode 100644 index 06c06fa34..000000000 --- a/crates/agent-gateway/proto/v2/gateway_ws.proto +++ /dev/null @@ -1,342 +0,0 @@ -// v2 统一线协议(WebSocket+Protobuf):/ws/v2(浏览器)、/ws/v2/agent(桌面端)、 -// /ws/v2/terminal(终端数据面)三链路的帧壳。一条 WS 二进制消息承载一条帧,文本帧即协议错误; -// 首帧必须为 hello,鉴权失败以 close code 4401 关闭。业务载荷复用 gateway.proto 的 v2 消息(三端唯一事实源), -// 本文件仅定义帧壳与网关本地载荷。 -syntax = "proto3"; - -package liveagent.gateway.v2; - -import "proto/v2/gateway.proto"; - -option go_package = "github.com/liveagent/agent-gateway/internal/proto/v2;gatewayv2"; - -// --------------------------------------------------------------------------- -// 握手 -// --------------------------------------------------------------------------- - -// ClientRole 区分 /ws/v2/terminal 上连接的所属端(该链路两端共用一条路径,靠 hello.role 区分)。 -enum ClientRole { - CLIENT_ROLE_UNSPECIFIED = 0; - CLIENT_ROLE_BROWSER = 1; - CLIENT_ROLE_AGENT = 2; -} - -// ClientHello 是所有 v2 连接的第一帧。 -message ClientHello { - // 协议版本,当前恒为 2;未知版本被服务端拒绝。 - uint32 protocol_version = 1; - ClientRole role = 2; - // 网关访问令牌,服务端做常量时间比较。 - string token = 3; - // CLIENT_ROLE_AGENT 必须以 agent_id 声明自身身份;浏览器角色在 - // /ws/v2/terminal 上也必须以 agent_id 显式绑定数据面目标。 - string agent_id = 4; - string agent_version = 5; - // 客户端标识(如 "webui" / "desktop"),仅用于观测与日志。 - string client_name = 6; - string client_version = 7; - // Optional feature identifiers supported by this client. Reliable desktop - // chat ingress is negotiated with "CHAT_INGRESS_V1". - repeated string capabilities = 8; -} - -// ServerHello 是服务端对 ClientHello 的应答;ok=false 时随即关闭连接。 -message ServerHello { - bool ok = 1; - string message = 2; - // 仅 agent 角色返回。 - string session_id = 3; - // 服务端 Unix 秒时间戳,供客户端校准。 - int64 server_time = 4; - // 服务端心跳周期与消息大小上限,客户端应据此配置本地看门狗与分片。 - uint32 heartbeat_period_seconds = 5; - uint64 max_message_bytes = 6; - // Feature identifiers supported by the gateway. - repeated string capabilities = 7; -} - -// PingFrame / PongFrame 是应用层心跳:WS 控制帧 ping 探测网络栈,本帧探测页面 JS/事件循环存活。 -message PingFrame { - int64 timestamp = 1; -} - -message PongFrame { - int64 timestamp = 1; -} - -// AckResult 是本地操作的通用确认应答。 -message AckResult { - bool ok = 1; -} - -// --------------------------------------------------------------------------- -// 浏览器链路(/ws/v2) -// --------------------------------------------------------------------------- - -// WebClientFrame 为浏览器 → 网关方向的帧。除 agent_request 直通臂外,其余臂均为 -// 网关本地操作(由网关自身状态应答,不经桌面端往返)。 -message WebClientFrame { - // 请求关联 id,客户端生成、响应帧回携;广播事件与心跳帧为空。 - string request_id = 1; - // 目标 Agent id。agent_request / status_get / chat_command / chat_prepare / - // workspace_* 必填;hello / pong / agent_list 与全局会话查询臂忽略本字段。 - string agent_id = 13; - - oneof payload { - ClientHello hello = 2; - // 直通请求:网关校验白名单与限额后转发桌面端(request_id 按连接命名空间化防冲突)。 - GatewayEnvelope agent_request = 3; - StatusGetRequest status_get = 4; - ChatCommandRequest chat_command = 5; - ChatPrepareRequest chat_prepare = 6; - ChatSubscribeRequest chat_subscribe = 7; - ChatUnsubscribeRequest chat_unsubscribe = 8; - ChatActivitiesRequest chat_activities = 9; - WorkspaceSubscribeRequest workspace_subscribe = 10; - WorkspaceUnsubscribeRequest workspace_unsubscribe = 11; - PongFrame pong = 12; - // Agent 目录查询:全部已登记 Agent(含离线)的状态列表。 - AgentListRequest agent_list = 14; - } -} - -// WebServerFrame 为网关 → 浏览器方向的帧。 -message WebServerFrame { - // 关联响应回填请求 id;服务端主动推送(广播/心跳)时为空。 - string request_id = 1; - // 来源/目标 Agent id:目标型响应回填请求声明的目标,Agent 事件广播标注来源; - // hello / ping / ack / agent_list 与全局聚合帧为空。客户端按此字段过滤 - // 非活跃 Agent 的事件。 - string agent_id = 16; - - oneof payload { - ServerHello hello = 2; - // 直通响应:桌面端返回的业务信封(含 error=99 错误臂)。 - AgentEnvelope agent_response = 3; - // 网关本地错误(鉴权、校验、离线等)。 - ErrorResponse local_error = 4; - PingFrame ping = 5; - // status_get / chat_prepare 的响应与 status 广播共用一个状态归一化器。 - StatusEvent status = 6; - ChatSubscribeResult chat_subscribed = 7; - ChatCommandAccepted chat_accepted = 8; - ChatActivitiesResult chat_activities = 9; - ChatStreamEvent chat_event = 10; - ChatCommandUpdate chat_command_update = 11; - ChatSubscriptionReset chat_subscription_reset = 12; - ChatActivityEvent chat_activity = 13; - AckResult ack = 14; - ChatCancelResult chat_cancelled = 15; - AgentListResult agent_list = 17; - - // 广播事件:session 层吐出的业务消息零塑形直转。 - HistorySyncEvent history_event = 20; - SettingsSyncEvent settings_event = 21; - TerminalEvent terminal_event = 22; - SftpEvent sftp_event = 23; - ChatQueueEvent chat_queue_event = 24; - TunnelStateSnapshot tunnel_state = 25; - ManagedProcessSnapshot process_state = 26; - WorkspaceActivityEvent workspace_activity = 27; - } -} - -// AgentListRequest 查询 Agent 目录;响应为 AgentListResult。 -message AgentListRequest {} - -// AgentListResult 返回全部已登记 Agent 的状态(含离线项,供目录渲染), -// 复用 StatusEvent 归一化器,按 agent_id 排序。 -message AgentListResult { - repeated StatusEvent agents = 1; -} - -// --------------------------------------------------------------------------- -// 桌面端链路(/ws/v2/agent) -// --------------------------------------------------------------------------- - -// AgentClientFrame 为桌面端 → 网关方向的帧。 -message AgentClientFrame { - oneof payload { - ClientHello hello = 1; - AgentEnvelope envelope = 2; - } -} - -// AgentServerFrame 为网关 → 桌面端方向的帧。 -message AgentServerFrame { - oneof payload { - ServerHello hello = 1; - GatewayEnvelope envelope = 2; - } -} - -// --------------------------------------------------------------------------- -// 终端链路(/ws/v2/terminal,两端共用) -// --------------------------------------------------------------------------- - -// TerminalClientFrame 为客户端(浏览器或桌面端)→ 网关方向的帧。 -message TerminalClientFrame { - oneof payload { - ClientHello hello = 1; - TerminalStreamFrame frame = 2; - } -} - -// TerminalServerFrame 为网关 → 客户端方向的帧。 -message TerminalServerFrame { - oneof payload { - ServerHello hello = 1; - TerminalStreamFrame frame = 2; - } -} - -// --------------------------------------------------------------------------- -// 网关本地载荷(由网关直接处理并以 protobuf 建模) -// --------------------------------------------------------------------------- - -// StatusGetRequest 请求网关侧运行状态快照(操作类型:"status.get")。 -message StatusGetRequest {} - -// StatusEvent 镜像 session.Status 的 JSON 形状(字段一一对应)。 -message StatusEvent { - bool online = 1; - bool agent_ready = 2; - bool chat_runtime_ready = 3; - string agent_id = 4; - string agent_version = 5; - string session_id = 6; - int64 connected_since = 7; - int64 last_heartbeat = 8; - string runtime_state = 9; - int64 runtime_last_heartbeat = 10; - string runtime_worker_id = 11; - bool runtime_visible = 12; - uint32 runtime_active_run_count = 13; - // 仅 agent_list 目录响应填充;实时状态响应与广播保持为空,避免状态热路径访问数据库。 - string name = 14; -} - -// ChatPrepareRequest 唤醒/探活桌面端 chat 运行时(操作类型:"chat.prepare");响应为 StatusEvent。 -message ChatPrepareRequest { - string reason = 1; -} - -// ChatSubscribeRequest 订阅会话事件流(操作类型:"chat.subscribe");外层 WebClientFrame.agent_id 必须非空, -// 会话按 (agent_id, conversation_id) 隔离。after_seq + stream_epoch -// 支持断线重放:epoch 不匹配或序号超界时服务端置 reset 并从头回放。 -message ChatSubscribeRequest { - string conversation_id = 1; - int64 after_seq = 2; - string stream_epoch = 3; -} - -// ChatRunActivity 镜像 session.RunActivity 的 JSON 形状。 -message ChatRunActivity { - string run_id = 1; - string state = 2; - int64 started_seq = 3; - // Unix 毫秒时间戳。 - int64 updated_at_ms = 4; - string tool_status = 5; - bool tool_status_is_compaction = 6; - string client_request_id = 7; - // 以下字段仅在 chat_activities 列表中填充(agent_id 标注运行所在 Agent)。 - string conversation_id = 8; - string workdir = 9; - string agent_id = 10; -} - -// ChatRunSnapshot 镜像 session.RunSnapshot 的 JSON 形状。 -message ChatRunSnapshot { - string run_id = 1; - int64 revision = 2; - // 桌面端渲染快照,内容为动态 JSON(按字符串携带,不建模)。 - string entries_json = 3; - string tool_status = 4; - bool tool_status_is_compaction = 5; - int64 as_of_seq = 6; -} - -// ChatSubscribeResult 是 chat_subscribe 的响应。 -message ChatSubscribeResult { - string conversation_id = 1; - string stream_epoch = 2; - int64 latest_seq = 3; - bool reset = 4; - ChatRunActivity activity = 5; - ChatRunSnapshot snapshot = 6; - // 回放的事件序列。载荷由 chatwire 塑形为深度动态 JSON,按字节携带、不额外 proto 化 - // (重建模会分叉 chatwire 且对压缩后的二进制帧无实际收益)。 - repeated bytes events_json = 7; -} - -// ChatUnsubscribeRequest 取消订阅(操作类型:"chat.unsubscribe");响应 AckResult。 -message ChatUnsubscribeRequest { - string conversation_id = 1; -} - -// ChatActivitiesRequest 查询运行中会话(操作类型:"chat.activities");仅由网关状态应答,桌面端离线时亦可用。 -message ChatActivitiesRequest {} - -message ChatActivitiesResult { - repeated ChatRunActivity running_conversations = 1; -} - -// ChatStreamEvent 是订阅后推送的单条会话事件。 -message ChatStreamEvent { - string conversation_id = 1; - // 事件序号(与 payload_json 内的 seq 一致,便于不解析载荷即可去重)。 - int64 seq = 2; - bytes payload_json = 3; -} - -// ChatCommandAccepted 是 chat_command 提交被接受的响应(chat_command 的接受应答)。 -message ChatCommandAccepted { - string run_id = 1; - string conversation_id = 2; - int64 accepted_seq = 3; - bool deduped = 4; -} - -// ChatCommandUpdate 推送命令的前置阶段结果(bound / queued_in_gui / failed),镜像 session.ChatCommandUpdate。 -message ChatCommandUpdate { - string run_id = 1; - string client_request_id = 2; - string conversation_id = 3; - string phase = 4; - string error_code = 5; - string message = 6; -} - -// ChatSubscriptionReset 通知客户端某会话流已被限流丢弃,需重新订阅(after_seq 断点续传)。 -message ChatSubscriptionReset { - string conversation_id = 1; -} - -// ChatCancelResult 是 chat.cancel 的响应。 -message ChatCancelResult { - bool ok = 1; - string run_id = 2; - string conversation_id = 3; -} - -// ChatActivityEvent 广播会话活动状态变化,镜像 session.ConversationActivityEvent。 -message ChatActivityEvent { - string conversation_id = 1; - string run_id = 2; - string client_request_id = 3; - bool running = 4; - string state = 5; - string workdir = 6; - int64 updated_at_ms = 7; -} - -// WorkspaceSubscribeRequest 订阅工作区活动(操作类型:"workspace.subscribe");响应 AckResult,事件经 workspace_activity 臂广播。 -message WorkspaceSubscribeRequest { - string workdir = 1; -} - -// WorkspaceUnsubscribeRequest 取消订阅;响应 AckResult。 -message WorkspaceUnsubscribeRequest { - string workdir = 1; -} diff --git a/crates/agent-gateway/test/README.md b/crates/agent-gateway/test/README.md deleted file mode 100644 index 218a9b413..000000000 --- a/crates/agent-gateway/test/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# agent-gateway tests - -All project-level gateway tests live under `crates/agent-gateway/test` and are split by boundary: - -| Directory | Coverage | -| --- | --- | -| `auth/` | HTTP bearer parsing and WebSocket token auth behavior | -| `http/` | Gateway HTTP route auth, `/api/status`, and SPA fallback | -| `upload/` | `/api/files/import` validation, multipart parsing, and agent forwarding | -| `websocket/` | WebSocket auth, request forwarding, chat streaming, and cancellation-facing events | -| `webui/` | Browser-side WebUI helpers, auth, upload normalization, history state, live stream state, and WebSocket client behavior | -| `../web/test/` | WebUI source-adjacent module tests for chat transcript, history scope, and live conversation commit helpers | -| `helpers/` | Shared Node test module loader for WebUI TypeScript modules | - -Run Go-side tests from `crates/agent-gateway`: - -```sh -go test ./... -``` - -Run WebUI Node tests from `crates/agent-gateway`: - -```sh -node --test test/webui/*.test.mjs web/test/*.test.mjs -``` - -Run the WebUI type/build gate separately from `crates/agent-gateway/web`: - -```sh -pnpm build -``` diff --git a/crates/agent-gateway/test/auth/auth_test.go b/crates/agent-gateway/test/auth/auth_test.go deleted file mode 100644 index 3cd55e26b..000000000 --- a/crates/agent-gateway/test/auth/auth_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package auth_test - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/liveagent/agent-gateway/internal/auth" -) - -func TestHTTPMiddlewareRequiresValidBearerToken(t *testing.T) { - t.Parallel() - - var called bool - handler := auth.HTTPMiddleware(" secret-token\r\n", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - called = true - w.WriteHeader(http.StatusNoContent) - })) - - cases := []struct { - name string - authorization string - wantStatus int - wantCalled bool - }{ - { - name: "missing header", - wantStatus: http.StatusUnauthorized, - }, - { - name: "wrong scheme", - authorization: "Token secret-token", - wantStatus: http.StatusUnauthorized, - }, - { - name: "wrong token", - authorization: "Bearer wrong", - wantStatus: http.StatusUnauthorized, - }, - { - name: "valid bearer token with whitespace", - authorization: " bearer secret-token ", - wantStatus: http.StatusNoContent, - wantCalled: true, - }, - } - - for _, tc := range cases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - called = false - req := httptest.NewRequest(http.MethodGet, "/api/status", nil) - if tc.authorization != "" { - req.Header.Set("Authorization", tc.authorization) - } - rec := httptest.NewRecorder() - - handler.ServeHTTP(rec, req) - - if rec.Code != tc.wantStatus { - t.Fatalf("status = %d, want %d", rec.Code, tc.wantStatus) - } - if called != tc.wantCalled { - t.Fatalf("handler called = %v, want %v", called, tc.wantCalled) - } - }) - } -} - -func TestValidateTokenTrimsAndRejectsEmptyValues(t *testing.T) { - t.Parallel() - - if !auth.ValidateToken(" secret-token ", "\nsecret-token\r\n") { - t.Fatal("ValidateToken should accept matching trimmed tokens") - } - if auth.ValidateToken("", "secret-token") { - t.Fatal("ValidateToken should reject empty input token") - } - if auth.ValidateToken("secret-token", "") { - t.Fatal("ValidateToken should reject empty expected token") - } - if auth.ValidateToken("wrong-token", "secret-token") { - t.Fatal("ValidateToken should reject mismatched tokens") - } -} diff --git a/crates/agent-gateway/test/helpers/gateway-v2.mjs b/crates/agent-gateway/test/helpers/gateway-v2.mjs deleted file mode 100644 index d25f3dae7..000000000 --- a/crates/agent-gateway/test/helpers/gateway-v2.mjs +++ /dev/null @@ -1,68 +0,0 @@ -// v2 线协议测试编解码器:用与被测代码同一份生成 schema + protobuf 运行时 -// 编解码二进制帧,让 FakeWebSocket 以 v2 服务端的身份说话。 -// 服务端帧用 protojson 形状(proto 字段名 + oneof 臂即普通字段)构造, -// bytes 字段传 base64 字符串。 -export function createGatewayV2Codec(loader) { - const pb = loader.loadModule("@bufbuild/protobuf"); - const v2 = loader.loadModule("src/lib/proto/gen/proto/v2/gateway_ws_pb.ts"); - - const toBytes = (data) => { - if (data instanceof Uint8Array) return data; - if (data instanceof ArrayBuffer) return new Uint8Array(data); - if (ArrayBuffer.isView(data)) { - return new Uint8Array(data.buffer, data.byteOffset, data.byteLength); - } - throw new Error("expected binary frame data"); - }; - - // 编码为 ArrayBuffer(浏览器 binaryType="arraybuffer" 时 event.data 的形状)。 - const toArrayBuffer = (u8) => u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength); - - function decodeClientFrame(data) { - const frame = pb.fromBinary(v2.WebClientFrameSchema, toBytes(data)); - const json = pb.toJson(v2.WebClientFrameSchema, frame, { useProtoFieldName: true }); - return { - requestId: frame.requestId ?? "", - case: frame.payload?.case, - json, - frame, - }; - } - - // init 为 protojson 形状,如 { request_id: "r1", status: { online: true } }。 - function encodeServerFrame(init) { - const frame = pb.fromJson(v2.WebServerFrameSchema, init); - return toArrayBuffer(pb.toBinary(v2.WebServerFrameSchema, frame)); - } - - function decodeTerminalClientFrame(data) { - const frame = pb.fromBinary(v2.TerminalClientFrameSchema, toBytes(data)); - const json = pb.toJson(v2.TerminalClientFrameSchema, frame, { useProtoFieldName: true }); - return { case: frame.payload?.case, json, frame }; - } - - function encodeTerminalServerFrame(init) { - const frame = pb.fromJson(v2.TerminalServerFrameSchema, init); - return toArrayBuffer(pb.toBinary(v2.TerminalServerFrameSchema, frame)); - } - - // bytes 字段的 protojson 形式:字符串按 UTF-8、二进制原样、其余 JSON 序列化。 - const base64 = (value) => { - if (value instanceof Uint8Array || Array.isArray(value)) { - return Buffer.from(value).toString("base64"); - } - return Buffer.from(typeof value === "string" ? value : JSON.stringify(value)).toString( - "base64", - ); - }; - - return { - pb, - v2, - decodeClientFrame, - encodeServerFrame, - decodeTerminalClientFrame, - encodeTerminalServerFrame, - base64, - }; -} diff --git a/crates/agent-gateway/test/helpers/load-web-module.mjs b/crates/agent-gateway/test/helpers/load-web-module.mjs deleted file mode 100644 index 00b82692a..000000000 --- a/crates/agent-gateway/test/helpers/load-web-module.mjs +++ /dev/null @@ -1,249 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; -import vm from "node:vm"; -import { createRequire } from "node:module"; -import { fileURLToPath, pathToFileURL } from "node:url"; - -const DEFAULT_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json", ".css"]; - -// pi-ai ships an import-only exports map, so the loader's CJS require() path -// cannot reach it. Import the real model catalog by file URL (bypassing the -// exports map) so settings code sees the exact runtime data. -const piAiModels = await import( - new URL( - "../../web/node_modules/@earendil-works/pi-ai/dist/models.js", - import.meta.url, - ).href -); -const piAiProvidersAll = await import( - new URL( - "../../web/node_modules/@earendil-works/pi-ai/dist/providers/all.js", - import.meta.url, - ).href -); - -function createDefaultMocks() { - return { - "@earendil-works/pi-ai": { - getSupportedThinkingLevels: piAiModels.getSupportedThinkingLevels, - clampThinkingLevel: piAiModels.clampThinkingLevel, - }, - "@earendil-works/pi-ai/providers/all": { - getBuiltinModels: piAiProvidersAll.getBuiltinModels, - getBuiltinModel: piAiProvidersAll.getBuiltinModel, - }, - "@sinclair/typebox": { - Type: { - Object(properties = {}) { - return { type: "object", properties }; - }, - String(options = {}) { - return { type: "string", ...options }; - }, - Number(options = {}) { - return { type: "number", ...options }; - }, - Integer(options = {}) { - return { type: "integer", ...options }; - }, - Null(options = {}) { - return { type: "null", ...options }; - }, - Boolean(options = {}) { - return { type: "boolean", ...options }; - }, - Optional(schema) { - return { ...schema, optional: true }; - }, - Array(items, options = {}) { - return { type: "array", items, ...options }; - }, - }, - }, - "@tauri-apps/api/core": { - invoke() { - throw new Error("tauri invoke mock was not expected to be called"); - }, - }, - "@tauri-apps/api/event": { - listen() { - throw new Error("tauri listen mock was not expected to be called"); - }, - }, - "@tauri-apps/plugin-opener": { - openUrl() { - throw new Error("tauri openUrl mock was not expected to be called"); - }, - }, - "react/jsx-runtime": { - jsx(type, props, key) { - return { type, props: props ?? {}, key: key ?? null }; - }, - jsxs(type, props, key) { - return { type, props: props ?? {}, key: key ?? null }; - }, - Fragment: Symbol.for("react.fragment"), - }, - "lucide-react": new Proxy({}, { - get(_target, prop) { - return function Icon(props) { - return { type: String(prop), props: props ?? {} }; - }; - }, - }), - }; -} - -function createIconMock(specifier) { - if (specifier.endsWith("?raw")) { - return ""; - } - return function Icon(props) { - return { type: specifier, props: props ?? {} }; - }; -} - -function hasExtension(filePath) { - return path.extname(filePath).length > 0; -} - -function resolveAsFileOrDirectory(candidate) { - if (hasExtension(candidate) && fs.existsSync(candidate)) { - return candidate; - } - - for (const ext of DEFAULT_EXTENSIONS) { - const withExt = `${candidate}${ext}`; - if (fs.existsSync(withExt)) return withExt; - } - - if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) { - for (const ext of DEFAULT_EXTENSIONS) { - const indexPath = path.join(candidate, `index${ext}`); - if (fs.existsSync(indexPath)) return indexPath; - } - } - - throw new Error(`Cannot resolve module path: ${candidate}`); -} - -export function createWebModuleLoader(options = {}) { - const rootDir = options.rootDir - ? path.resolve(options.rootDir) - : path.resolve(fileURLToPath(new URL("../../web", import.meta.url))); - const requireFromRoot = createRequire(path.join(rootDir, "package.json")); - const ts = requireFromRoot("typescript"); - const cache = new Map(); - const mocks = new Map([ - ...Object.entries(createDefaultMocks()), - ...Object.entries(options.mocks ?? {}), - ]); - - function resolveLocal(specifier, parentDir = rootDir) { - if (specifier.startsWith("@/")) { - return resolveAsFileOrDirectory( - path.join(rootDir, "src", specifier.slice("@/".length)), - ); - } - if (specifier === "@") { - return resolveAsFileOrDirectory(path.join(rootDir, "src")); - } - - const candidate = path.isAbsolute(specifier) - ? specifier - : path.resolve(parentDir, specifier); - return resolveAsFileOrDirectory(candidate); - } - - function resolveMock(specifier, parentDir) { - if (mocks.has(specifier)) return mocks.get(specifier); - if (specifier.startsWith("~icons/")) return createIconMock(specifier); - if (specifier.startsWith(".") || path.isAbsolute(specifier) || specifier.startsWith("@/")) { - const resolved = resolveLocal(specifier, parentDir); - if (mocks.has(resolved)) return mocks.get(resolved); - } - return undefined; - } - - function loadModule(specifier, parentDir = rootDir) { - const mock = resolveMock(specifier, parentDir); - if (mock !== undefined) return mock; - - const isRootRelative = - specifier.startsWith("src/") || - specifier.startsWith("test/") || - specifier.startsWith("@/"); - - if (!isRootRelative && !specifier.startsWith(".") && !path.isAbsolute(specifier)) { - return requireFromRoot(specifier); - } - - const filePath = resolveLocal(specifier, isRootRelative ? rootDir : parentDir); - if (cache.has(filePath)) return cache.get(filePath).exports; - - if (filePath.endsWith(".json")) { - const jsonModule = { exports: JSON.parse(fs.readFileSync(filePath, "utf8")) }; - cache.set(filePath, jsonModule); - return jsonModule.exports; - } - - if (filePath.endsWith(".css")) { - const cssModule = { exports: {} }; - cache.set(filePath, cssModule); - return cssModule.exports; - } - - const source = fs.readFileSync(filePath, "utf8"); - const transpiled = ts.transpileModule(source, { - fileName: filePath, - compilerOptions: { - module: ts.ModuleKind.CommonJS, - target: ts.ScriptTarget.ES2022, - jsx: ts.JsxEmit.ReactJSX, - esModuleInterop: true, - allowSyntheticDefaultImports: true, - moduleResolution: ts.ModuleResolutionKind.Node10 ?? ts.ModuleResolutionKind.NodeJs, - resolveJsonModule: true, - ignoreDeprecations: "6.0", - }, - reportDiagnostics: true, - }); - - const diagnostics = transpiled.diagnostics ?? []; - const fatalDiagnostics = diagnostics.filter((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error); - if (fatalDiagnostics.length > 0) { - const message = ts.formatDiagnosticsWithColorAndContext(fatalDiagnostics, { - getCanonicalFileName: (name) => name, - getCurrentDirectory: () => rootDir, - getNewLine: () => "\n", - }); - throw new Error(message); - } - - const module = { exports: {} }; - cache.set(filePath, module); - - const dirname = path.dirname(filePath); - const localRequire = (nextSpecifier) => loadModule(nextSpecifier, dirname); - localRequire.resolve = (nextSpecifier) => - nextSpecifier.startsWith(".") || path.isAbsolute(nextSpecifier) || nextSpecifier.startsWith("@/") - ? resolveLocal(nextSpecifier, dirname) - : requireFromRoot.resolve(nextSpecifier); - - const outputText = transpiled.outputText.replaceAll( - "import.meta.url", - JSON.stringify(pathToFileURL(filePath).href), - ); - const wrapped = `(function (exports, require, module, __filename, __dirname) {\n${outputText}\n})`; - const script = new vm.Script(wrapped, { filename: filePath }); - const compiled = script.runInThisContext(); - compiled(module.exports, localRequire, module, filePath, dirname); - return module.exports; - } - - return { - rootDir, - loadModule, - resolveLocal, - }; -} diff --git a/crates/agent-gateway/test/http/agents_api_test.go b/crates/agent-gateway/test/http/agents_api_test.go deleted file mode 100644 index 086847d83..000000000 --- a/crates/agent-gateway/test/http/agents_api_test.go +++ /dev/null @@ -1,402 +0,0 @@ -package httproutes - -// Agent 目录与凭证管理 API 测试:签发→轮换/删除→踢线的闭环,及管理 token 门禁。 - -import ( - "encoding/json" - "errors" - "fmt" - "net/http" - "net/http/httptest" - "path/filepath" - "strconv" - "strings" - "testing" - "time" - - "github.com/liveagent/agent-gateway/internal/auth/agenttoken" - "github.com/liveagent/agent-gateway/internal/config" - "github.com/liveagent/agent-gateway/internal/db" - "github.com/liveagent/agent-gateway/internal/server" - "github.com/liveagent/agent-gateway/internal/session" -) - -func newAgentsAPIServer(t *testing.T) (http.Handler, *session.Manager, *agenttoken.Store) { - t.Helper() - database, err := db.Open(filepath.Join(t.TempDir(), "agent-tokens.db")) - if err != nil { - t.Fatalf("open gateway db: %v", err) - } - t.Cleanup(func() { _ = database.Close() }) - store, err := agenttoken.NewStore(database) - if err != nil { - t.Fatalf("init agent token store: %v", err) - } - sm := session.NewManager() - handler := server.NewHTTPServer(&config.Config{ - Token: "admin-token", - RequestTimeout: time.Second, - }, sm, store) - return handler, sm, store -} - -func agentTokenAuthenticates( - t *testing.T, - store *agenttoken.Store, - agentID string, - token string, -) bool { - t.Helper() - _, err := store.AuthenticateAndRegister(agentID, token, false) - if err == nil { - return true - } - if errors.Is(err, agenttoken.ErrUnauthorized) { - return false - } - t.Fatalf("authenticate agent token: %v", err) - return false -} - -func doAgentsRequest(t *testing.T, handler http.Handler, method, path, token string) *httptest.ResponseRecorder { - t.Helper() - req := httptest.NewRequest(method, "http://gateway.test"+path, nil) - if token != "" { - req.Header.Set("Authorization", "Bearer "+token) - } - rec := httptest.NewRecorder() - handler.ServeHTTP(rec, req) - return rec -} - -func doAgentsJSONRequest( - t *testing.T, - handler http.Handler, - method, path, token, body string, -) *httptest.ResponseRecorder { - t.Helper() - req := httptest.NewRequest(method, "http://gateway.test"+path, strings.NewReader(body)) - req.Header.Set("Content-Type", "application/json") - if token != "" { - req.Header.Set("Authorization", "Bearer "+token) - } - rec := httptest.NewRecorder() - handler.ServeHTTP(rec, req) - return rec -} - -func TestAgentsAPIIssueRevokeKicksSession(t *testing.T) { - t.Parallel() - - handler, sm, store := newAgentsAPIServer(t) - agentID := testAgentID(1) - - // 签发:明文只出现在响应里。 - rec := doAgentsRequest(t, handler, http.MethodPost, "/api/agents/"+agentID+"/token", "admin-token") - if rec.Code != http.StatusOK { - t.Fatalf("issue status = %d body=%s", rec.Code, rec.Body.String()) - } - var issued struct { - AgentID string `json:"agent_id"` - Token string `json:"token"` - } - if err := json.Unmarshal(rec.Body.Bytes(), &issued); err != nil { - t.Fatalf("decode issue response: %v", err) - } - if issued.AgentID != agentID || !strings.HasPrefix(issued.Token, "agt_") { - t.Fatalf("issued = %#v", issued) - } - if !agentTokenAuthenticates(t, store, agentID, issued.Token) { - t.Fatal("issued token must validate") - } - - // 模拟该 Agent 在线。 - sm.RecordAuthentication(agentID, "1.0.0", "session-a") - sess := session.NewAgentSession(sm.LatestAuthSnapshot(agentID)) - sm.SetSession(sess) - if !sm.IsOnline(agentID) { - t.Fatal("agent should be online") - } - - // 目录能看到在线 + 已签发,并带分页元信息。 - rec = doAgentsRequest(t, handler, http.MethodGet, "/api/agents", "admin-token") - if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), `"agent_id":"`+agentID+`"`) { - t.Fatalf("list status = %d body=%s", rec.Code, rec.Body.String()) - } - var listResp struct { - Agents []map[string]any `json:"agents"` - Total int `json:"total"` - Page int `json:"page"` - PageSize int `json:"page_size"` - HasMore bool `json:"has_more"` - } - if err := json.Unmarshal(rec.Body.Bytes(), &listResp); err != nil { - t.Fatalf("decode list: %v", err) - } - if listResp.Total != 1 || listResp.Page != 1 || len(listResp.Agents) != 1 || listResp.HasMore { - t.Fatalf("list pagination = %#v", listResp) - } - if online, _ := listResp.Agents[0]["online"].(bool); !online { - t.Fatalf("agent should show online in directory: %#v", listResp.Agents[0]) - } - - // 删除:整条记录消失、凭证失效且活跃会话被踢。 - rec = doAgentsRequest(t, handler, http.MethodDelete, "/api/agents/"+agentID, "admin-token") - if rec.Code != http.StatusOK { - t.Fatalf("revoke status = %d body=%s", rec.Code, rec.Body.String()) - } - if agentTokenAuthenticates(t, store, agentID, issued.Token) { - t.Fatal("deleted token must be invalid") - } - select { - case <-sess.Done(): - case <-time.After(time.Second): - t.Fatal("delete must disconnect the live session") - } - if sm.IsOnline(agentID) { - t.Fatal("agent must be absent after delete") - } -} - -func TestAgentsAPIRotationKicksLiveSession(t *testing.T) { - t.Parallel() - - handler, sm, store := newAgentsAPIServer(t) - agentID := testAgentID(2) - - first := doAgentsRequest(t, handler, http.MethodPost, "/api/agents/"+agentID+"/token", "admin-token") - if first.Code != http.StatusOK { - t.Fatalf("initial issue status = %d body=%s", first.Code, first.Body.String()) - } - var firstIssued struct { - Token string `json:"token"` - } - if err := json.Unmarshal(first.Body.Bytes(), &firstIssued); err != nil { - t.Fatalf("decode initial issue response: %v", err) - } - - sm.RecordAuthentication(agentID, "1.0.0", "session-rotation") - sess := session.NewAgentSession(sm.LatestAuthSnapshot(agentID)) - sm.SetSession(sess) - if !sm.IsOnline(agentID) { - t.Fatal("agent should be online before rotation") - } - - rotated := doAgentsRequest(t, handler, http.MethodPost, "/api/agents/"+agentID+"/token", "admin-token") - if rotated.Code != http.StatusOK { - t.Fatalf("rotation status = %d body=%s", rotated.Code, rotated.Body.String()) - } - var rotatedResponse struct { - Token string `json:"token"` - Disconnected bool `json:"disconnected"` - } - if err := json.Unmarshal(rotated.Body.Bytes(), &rotatedResponse); err != nil { - t.Fatalf("decode rotation response: %v", err) - } - if !rotatedResponse.Disconnected { - t.Fatal("rotation must report and perform live-session disconnect") - } - if agentTokenAuthenticates(t, store, agentID, firstIssued.Token) { - t.Fatal("rotated-out token must be invalid") - } - if !agentTokenAuthenticates(t, store, agentID, rotatedResponse.Token) { - t.Fatal("rotated-in token must be valid") - } - select { - case <-sess.Done(): - case <-time.After(time.Second): - t.Fatal("rotation must disconnect the live session") - } - if sm.IsOnline(agentID) { - t.Fatal("agent must be offline after rotation") - } -} - -func TestAgentsAPIRequiresManagementToken(t *testing.T) { - t.Parallel() - - handler, _, store := newAgentsAPIServer(t) - agentToken, err := store.Issue("agent-a", "") - if err != nil { - t.Fatalf("issue: %v", err) - } - - // 无 token 与 Agent 凭证都不能访问管理 API(Agent 凭证不授权管理面)。 - if rec := doAgentsRequest(t, handler, http.MethodGet, "/api/agents", ""); rec.Code != http.StatusUnauthorized { - t.Fatalf("no-token list status = %d", rec.Code) - } - if rec := doAgentsRequest(t, handler, http.MethodPost, "/api/agents/agent-b/token", agentToken); rec.Code != http.StatusUnauthorized { - t.Fatalf("agent-token issue status = %d", rec.Code) - } -} - -func TestAgentsAPIValidatesIDAndUpdatesOptionalName(t *testing.T) { - t.Parallel() - - handler, _, _ := newAgentsAPIServer(t) - invalid := doAgentsJSONRequest(t, handler, http.MethodPost, "/api/agents/hhhh/token", "admin-token", `{}`) - if invalid.Code != http.StatusBadRequest { - t.Fatalf("invalid id status = %d body=%s", invalid.Code, invalid.Body.String()) - } - - agentID := testAgentID(20) - issued := doAgentsJSONRequest(t, handler, http.MethodPost, "/api/agents/"+agentID+"/token", "admin-token", `{"name":" Office desktop "}`) - if issued.Code != http.StatusOK { - t.Fatalf("issue named agent status = %d body=%s", issued.Code, issued.Body.String()) - } - - updated := doAgentsJSONRequest(t, handler, http.MethodPatch, "/api/agents/"+agentID, "admin-token", `{"name":""}`) - if updated.Code != http.StatusOK { - t.Fatalf("clear name status = %d body=%s", updated.Code, updated.Body.String()) - } - - tooLongName, _ := json.Marshal(map[string]string{"name": strings.Repeat("名", 65)}) - tooLong := doAgentsJSONRequest(t, handler, http.MethodPatch, "/api/agents/"+agentID, "admin-token", string(tooLongName)) - if tooLong.Code != http.StatusBadRequest { - t.Fatalf("long name status = %d body=%s", tooLong.Code, tooLong.Body.String()) - } - - listed := doAgentsRequest(t, handler, http.MethodGet, "/api/agents", "admin-token") - if listed.Code != http.StatusOK || !strings.Contains(listed.Body.String(), `"name":""`) { - t.Fatalf("cleared name list status = %d body=%s", listed.Code, listed.Body.String()) - } -} - -func TestAgentsAPIListsRegisteredAgentWithoutIndependentToken(t *testing.T) { - t.Parallel() - - handler, sm, store := newAgentsAPIServer(t) - if err := store.Register("shared-token-agent"); err != nil { - t.Fatalf("register: %v", err) - } - sm.RecordAuthentication("shared-token-agent", "1.0.0", "session-shared") - sess := session.NewAgentSession(sm.LatestAuthSnapshot("shared-token-agent")) - sm.SetSession(sess) - t.Cleanup(func() { sm.ClearSession(sess) }) - - rec := doAgentsRequest(t, handler, http.MethodGet, - "/api/agents?status=online&page=1&page_size=50", "admin-token") - if rec.Code != http.StatusOK { - t.Fatalf("list status = %d body=%s", rec.Code, rec.Body.String()) - } - var resp struct { - Agents []struct { - AgentID string `json:"agent_id"` - Online bool `json:"online"` - HasToken bool `json:"has_token"` - RegisteredAt string `json:"registered_at"` - } `json:"agents"` - Total int `json:"total"` - } - if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { - t.Fatalf("decode: %v", err) - } - if resp.Total != 1 || len(resp.Agents) != 1 || resp.Agents[0].AgentID != "shared-token-agent" || - !resp.Agents[0].Online || resp.Agents[0].HasToken || resp.Agents[0].RegisteredAt == "" { - t.Fatalf("registered agent response = %#v", resp) - } -} - -func TestAgentsAPIPaginatesDirectory(t *testing.T) { - t.Parallel() - - handler, _, _ := newAgentsAPIServer(t) - // 签发凭证会同时登记 120 个 Agent。 - for i := 0; i < 120; i++ { - rec := doAgentsRequest(t, handler, http.MethodPost, - "/api/agents/"+testAgentID(i)+"/token", "admin-token") - if rec.Code != http.StatusOK { - t.Fatalf("issue %d: %d", i, rec.Code) - } - } - - rec := doAgentsRequest(t, handler, http.MethodGet, "/api/agents?page=2&page_size=50", "admin-token") - if rec.Code != http.StatusOK { - t.Fatalf("list page 2: %d", rec.Code) - } - var resp struct { - Agents []map[string]any `json:"agents"` - Total int `json:"total"` - Page int `json:"page"` - PageSize int `json:"page_size"` - HasMore bool `json:"has_more"` - } - if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { - t.Fatalf("decode: %v", err) - } - if resp.Total != 120 || resp.Page != 2 || resp.PageSize != 50 || - len(resp.Agents) != 50 || !resp.HasMore { - t.Fatalf("page 2 = %#v", resp) - } - - // 末页余 20 条、无更多。 - rec = doAgentsRequest(t, handler, http.MethodGet, "/api/agents?page=3&page_size=50", "admin-token") - _ = json.Unmarshal(rec.Body.Bytes(), &resp) - if len(resp.Agents) != 20 || resp.HasMore { - t.Fatalf("page 3 = len %d hasMore %v", len(resp.Agents), resp.HasMore) - } -} - -func TestAgentsAPIFiltersBeforeDatabasePaging(t *testing.T) { - t.Parallel() - - handler, sm, store := newAgentsAPIServer(t) - for i := 0; i < 6; i++ { - agentID := "agent-" + leftPad(i) - if _, err := store.Issue(agentID, ""); err != nil { - t.Fatalf("issue %s: %v", agentID, err) - } - if i%2 == 0 { - sm.RecordAuthentication(agentID, "1.0.0", "session-"+leftPad(i)) - sess := session.NewAgentSession(sm.LatestAuthSnapshot(agentID)) - sm.SetSession(sess) - t.Cleanup(func() { sm.ClearSession(sess) }) - } - } - - testCases := []struct { - name string - status string - wantFirst string - }{ - {name: "online", status: "online", wantFirst: "agent-004"}, - {name: "offline", status: "offline", wantFirst: "agent-005"}, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - rec := doAgentsRequest(t, handler, http.MethodGet, - "/api/agents?status="+tc.status+"&page=2&page_size=2", "admin-token") - if rec.Code != http.StatusOK { - t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String()) - } - var resp struct { - Agents []map[string]any `json:"agents"` - Total int `json:"total"` - HasMore bool `json:"has_more"` - } - if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { - t.Fatalf("decode: %v", err) - } - if resp.Total != 3 || len(resp.Agents) != 1 || resp.HasMore || resp.Agents[0]["agent_id"] != tc.wantFirst { - t.Fatalf("filtered page = %#v", resp) - } - }) - } - - invalid := doAgentsRequest(t, handler, http.MethodGet, "/api/agents?status=busy", "admin-token") - if invalid.Code != http.StatusBadRequest { - t.Fatalf("invalid filter status = %d body=%s", invalid.Code, invalid.Body.String()) - } -} - -func leftPad(i int) string { - s := strconv.Itoa(i) - for len(s) < 3 { - s = "0" + s - } - return s -} - -func testAgentID(i int) string { - return fmt.Sprintf("agent-00000000-0000-4000-8000-%012d", i) -} diff --git a/crates/agent-gateway/test/http/http_routes_test.go b/crates/agent-gateway/test/http/http_routes_test.go deleted file mode 100644 index 245d31f92..000000000 --- a/crates/agent-gateway/test/http/http_routes_test.go +++ /dev/null @@ -1,115 +0,0 @@ -package httproutes_test - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" - - "github.com/liveagent/agent-gateway/internal/config" - "github.com/liveagent/agent-gateway/internal/server" - "github.com/liveagent/agent-gateway/internal/session" -) - -func newHTTPTestHandler(sm *session.Manager) http.Handler { - return server.NewHTTPServer(&config.Config{ - Token: "dev-token", - RequestTimeout: 500 * time.Millisecond, - }, sm, nil) -} - -func TestAPIRoutesRequireBearerToken(t *testing.T) { - t.Parallel() - - handler := newHTTPTestHandler(session.NewManager()) - - req := httptest.NewRequest(http.MethodGet, "http://gateway.test/api/status", nil) - rec := httptest.NewRecorder() - handler.ServeHTTP(rec, req) - - if rec.Code != http.StatusUnauthorized { - t.Fatalf("status = %d, want %d", rec.Code, http.StatusUnauthorized) - } - if contentType := rec.Header().Get("Content-Type"); !strings.Contains(contentType, "application/json") { - t.Fatalf("content-type = %q, want JSON", contentType) - } - if !strings.Contains(rec.Body.String(), "unauthorized") { - t.Fatalf("body = %q, want unauthorized error", rec.Body.String()) - } -} - -func TestHealthRouteIsPublic(t *testing.T) { - t.Parallel() - - handler := newHTTPTestHandler(session.NewManager()) - - req := httptest.NewRequest(http.MethodGet, "http://gateway.test/healthz", nil) - rec := httptest.NewRecorder() - handler.ServeHTTP(rec, req) - - if rec.Code != http.StatusOK { - t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String()) - } - if !strings.Contains(rec.Body.String(), `"ok":true`) { - t.Fatalf("body = %q, want health payload", rec.Body.String()) - } -} - -func TestStatusRouteReturnsAgentDirectory(t *testing.T) { - t.Parallel() - - sm := session.NewManager() - sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1") - sm.SetSession(session.NewAgentSession(sm.LatestAuthSnapshot("desktop-agent"))) - handler := newHTTPTestHandler(sm) - - req := httptest.NewRequest(http.MethodGet, "http://gateway.test/api/status", nil) - req.Header.Set("Authorization", " bearer dev-token ") - rec := httptest.NewRecorder() - handler.ServeHTTP(rec, req) - - if rec.Code != http.StatusOK { - t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String()) - } - - var payload struct { - Agents []struct { - Online bool `json:"online"` - AgentID string `json:"agent_id"` - AgentVersion string `json:"agent_version"` - SessionID string `json:"session_id"` - } `json:"agents"` - } - if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil { - t.Fatalf("decode status payload: %v", err) - } - if len(payload.Agents) != 1 { - t.Fatalf("agents = %#v, want one entry", payload.Agents) - } - agent := payload.Agents[0] - if !agent.Online || agent.AgentID != "desktop-agent" || agent.AgentVersion != "0.9.0" || agent.SessionID != "session-1" { - t.Fatalf("agent = %#v, want authenticated session identity", agent) - } -} - -func TestSPAFallbackServesIndexWithoutAuthorization(t *testing.T) { - t.Parallel() - - handler := newHTTPTestHandler(session.NewManager()) - - req := httptest.NewRequest(http.MethodGet, "http://gateway.test/conversations/session-1", nil) - rec := httptest.NewRecorder() - handler.ServeHTTP(rec, req) - - if rec.Code != http.StatusOK { - t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) - } - if location := rec.Header().Get("Location"); location != "" { - t.Fatalf("expected no redirect, got Location=%q", location) - } - if !strings.Contains(rec.Body.String(), "LiveAgent Gateway") { - t.Fatalf("expected embedded WebUI index.html, got %q", rec.Body.String()) - } -} diff --git a/crates/agent-gateway/test/session/manager_test.go b/crates/agent-gateway/test/session/manager_test.go deleted file mode 100644 index c780b3852..000000000 --- a/crates/agent-gateway/test/session/manager_test.go +++ /dev/null @@ -1,648 +0,0 @@ -package session_test - -import ( - "context" - "errors" - "fmt" - "strings" - "testing" - "time" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" - "github.com/liveagent/agent-gateway/internal/session" -) - -func newTestSessionManager() *session.Manager { - sm := session.NewManager() - sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1") - return sm -} - -func dispatchChatControl( - sm *session.Manager, - requestID string, - conversationID string, - controlType string, - state string, -) { - sm.DispatchFromAgent("desktop-agent", &gatewayv2.AgentEnvelope{ - RequestId: requestID, - Payload: &gatewayv2.AgentEnvelope_ChatControl{ - ChatControl: &gatewayv2.ChatControlEvent{ - RequestId: requestID, - ConversationId: conversationID, - Type: controlType, - State: state, - }, - }, - }) -} - -func assertDoneClosed(t *testing.T, done <-chan struct{}) { - t.Helper() - select { - case <-done: - case <-time.After(time.Second): - t.Fatalf("timed out waiting for session done to close") - } -} - -func assertDoneOpen(t *testing.T, done <-chan struct{}) { - t.Helper() - select { - case <-done: - t.Fatalf("session done is closed") - default: - } -} - -func TestClearSessionDoesNotCloseReplacement(t *testing.T) { - t.Parallel() - - sm := newTestSessionManager() - first := session.NewAgentSession(sm.LatestAuthSnapshot("desktop-agent")) - sm.SetSession(first) - second := session.NewAgentSession(sm.LatestAuthSnapshot("desktop-agent")) - sm.SetSession(second) - - assertDoneClosed(t, first.Done()) - assertDoneOpen(t, second.Done()) - - sm.ClearSession(first) - if status := sm.Status("desktop-agent"); !status.Online { - t.Fatalf("status online = false after clearing stale session") - } - assertDoneOpen(t, second.Done()) - - env := &gatewayv2.GatewayEnvelope{RequestId: "still-current"} - // SendToAgentContext 等待送达 Ack;在旁路读取并 Ack 出站信封后再收敛。 - sendErr := make(chan error, 1) - go func() { - sendErr <- sm.SendToAgentContext(context.Background(), "desktop-agent", env) - }() - select { - case got := <-second.Outbound(): - got.Ack(nil) - if got.GetRequestId() != "still-current" { - t.Fatalf("request id = %q, want still-current", got.GetRequestId()) - } - case <-time.After(time.Second): - t.Fatalf("timed out waiting for current session outbound message") - } - if err := <-sendErr; err != nil { - t.Fatalf("SendToAgentContext after stale ClearSession: %v", err) - } - - sm.ClearSession(second) - assertDoneClosed(t, second.Done()) - if status := sm.Status("desktop-agent"); status.Online { - t.Fatalf("status online = true after clearing current session") - } - if err := sm.SendToAgentContext(context.Background(), "desktop-agent", env); !errors.Is(err, session.ErrAgentOffline) { - t.Fatalf("SendToAgent after clearing current session = %v, want ErrAgentOffline", err) - } -} - -func TestClearSessionIfHeartbeatStaleClosesOnlyCurrentSession(t *testing.T) { - t.Parallel() - - sm := newTestSessionManager() - first := session.NewAgentSession(sm.LatestAuthSnapshot("desktop-agent")) - sm.SetSession(first) - second := session.NewAgentSession(sm.LatestAuthSnapshot("desktop-agent")) - sm.SetSession(second) - - time.Sleep(time.Millisecond) - if sm.ClearSessionIfHeartbeatStale(first, time.Nanosecond) { - t.Fatalf("stale first session should not close replacement session") - } - assertDoneOpen(t, second.Done()) - if status := sm.Status("desktop-agent"); !status.Online { - t.Fatalf("status online = false after stale old-session heartbeat timeout") - } - - time.Sleep(time.Millisecond) - if !sm.ClearSessionIfHeartbeatStale(second, time.Nanosecond) { - t.Fatalf("current stale session was not cleared") - } - assertDoneClosed(t, second.Done()) - if status := sm.Status("desktop-agent"); status.Online { - t.Fatalf("status online = true after current session heartbeat timeout") - } - if err := sm.SendToAgentContext(context.Background(), "desktop-agent", &gatewayv2.GatewayEnvelope{RequestId: "after-timeout"}); !errors.Is(err, session.ErrAgentOffline) { - t.Fatalf("SendToAgent after heartbeat timeout = %v, want ErrAgentOffline", err) - } -} - -func TestChatRuntimeReadyRequiresFreshRuntimeHeartbeat(t *testing.T) { - t.Parallel() - - sm := newTestSessionManager() - sess := session.NewAgentSession(sm.LatestAuthSnapshot("desktop-agent")) - sess.SetCapabilities([]string{gatewayv2.ChatIngressV1Capability}) - sm.SetSession(sess) - - if status := sm.Status("desktop-agent"); !status.Online || status.ChatRuntimeReady { - t.Fatalf("initial status = %#v, want online without chat runtime readiness", status) - } - - sm.UpdateRuntimeStatus(sess, &gatewayv2.RuntimeStatusEvent{ - WorkerId: "runtime-1", - State: "ready", - Visible: true, - ActiveRunCount: 0, - Timestamp: time.Now().Unix(), - }) - if status := sm.Status("desktop-agent"); !status.ChatRuntimeReady || - status.RuntimeState != "ready" || - status.RuntimeWorkerID != "runtime-1" || - status.RuntimeLastHeartbeat == 0 { - t.Fatalf("ready runtime status = %#v", status) - } - - sm.UpdateRuntimeStatus(sess, &gatewayv2.RuntimeStatusEvent{ - WorkerId: "runtime-1", - State: "suspended", - Timestamp: time.Now().Unix(), - }) - if status := sm.Status("desktop-agent"); status.ChatRuntimeReady || status.RuntimeState != "suspended" { - t.Fatalf("suspended runtime status = %#v, want not ready", status) - } - - sm.UpdateRuntimeStatus(sess, &gatewayv2.RuntimeStatusEvent{ - WorkerId: "runtime-1", - State: "busy", - Timestamp: time.Now().Unix(), - }) - if !sm.ChatRuntimeReady("desktop-agent") { - t.Fatalf("busy runtime should be ready to manage chat runs") - } - - sm.ClearSession(sess) - if status := sm.Status("desktop-agent"); status.ChatRuntimeReady || status.RuntimeState != "" { - t.Fatalf("cleared session status = %#v, want runtime readiness reset", status) - } -} - -func TestRuntimeStatusUpdateBroadcastsStatus(t *testing.T) { - t.Parallel() - - sm := newTestSessionManager() - sess := session.NewAgentSession(sm.LatestAuthSnapshot("desktop-agent")) - sess.SetCapabilities([]string{gatewayv2.ChatIngressV1Capability}) - sm.SetSession(sess) - updates, cleanup := sm.SubscribeStatus() - defer cleanup() - - sm.UpdateRuntimeStatus(sess, &gatewayv2.RuntimeStatusEvent{ - WorkerId: "runtime-broadcast", - State: "ready", - Visible: true, - ActiveRunCount: 2, - Timestamp: time.Now().Unix(), - }) - - select { - case tagged := <-updates: - status := tagged.Event - if !status.Online || !status.ChatRuntimeReady || - status.RuntimeWorkerID != "runtime-broadcast" || - status.RuntimeActiveRunCount != 2 { - t.Fatalf("runtime status broadcast = %#v", status) - } - case <-time.After(time.Second): - t.Fatal("timed out waiting for runtime status broadcast") - } - - // A heartbeat with identical semantic state only refreshes the internal TTL - // and must not spam every subscribed WebSocket. - sm.UpdateRuntimeStatus(sess, &gatewayv2.RuntimeStatusEvent{ - WorkerId: "runtime-broadcast", - State: "ready", - Visible: true, - ActiveRunCount: 2, - Timestamp: time.Now().Unix(), - }) - select { - case duplicate := <-updates: - t.Fatalf("timestamp-only runtime heartbeat was broadcast: %#v", duplicate) - case <-time.After(50 * time.Millisecond): - } -} - -func TestDispatchFromStaleSessionIsIgnored(t *testing.T) { - t.Parallel() - - sm := newTestSessionManager() - first := session.NewAgentSession(sm.LatestAuthSnapshot("desktop-agent")) - sm.SetSession(first) - second := session.NewAgentSession(sm.LatestAuthSnapshot("desktop-agent")) - sm.SetSession(second) - - // RegisterStreamAndSendContext 等待送达 Ack;没有服务泵,用一次性 drain 代替。 - go func() { - outbound := <-second.Outbound() - outbound.Ack(nil) - }() - ch, done, cleanup, err := sm.RegisterStreamAndSendContext(context.Background(), "desktop-agent", "request-1", &gatewayv2.GatewayEnvelope{RequestId: "request-1"}) - if err != nil { - t.Fatalf("RegisterStreamAndSendContext: %v", err) - } - defer cleanup() - - staleEnv := &gatewayv2.AgentEnvelope{ - RequestId: "request-1", - Payload: &gatewayv2.AgentEnvelope_Error{ - Error: &gatewayv2.ErrorResponse{Code: 500, Message: "stale"}, - }, - } - sm.DispatchFromAgentForSession(first, staleEnv) - select { - case got := <-ch: - t.Fatalf("received stale session envelope: %#v", got) - case <-done: - t.Fatalf("stream closed while current session is still active") - case <-time.After(50 * time.Millisecond): - } - - currentEnv := &gatewayv2.AgentEnvelope{ - RequestId: "request-1", - Payload: &gatewayv2.AgentEnvelope_Error{ - Error: &gatewayv2.ErrorResponse{Code: 500, Message: "current"}, - }, - } - sm.DispatchFromAgentForSession(second, currentEnv) - select { - case got := <-ch: - if got.GetError().GetMessage() != "current" { - t.Fatalf("error message = %q, want current", got.GetError().GetMessage()) - } - case <-done: - t.Fatalf("stream closed before current session dispatch") - case <-time.After(time.Second): - t.Fatalf("timed out waiting for current session envelope") - } -} - -func TestSendToAgentUnblocksWhenSessionCloses(t *testing.T) { - t.Parallel() - - sm := newTestSessionManager() - sess := session.NewAgentSession(sm.LatestAuthSnapshot("desktop-agent")) - sm.SetSession(sess) - - errCh := make(chan error, 1) - go func() { - defer func() { - if recovered := recover(); recovered != nil { - errCh <- fmt.Errorf("panic: %v", recovered) - } - }() - for i := 0; i < 128; i += 1 { - _ = sm.SendToAgentContext(context.Background(), "desktop-agent", &gatewayv2.GatewayEnvelope{RequestId: fmt.Sprintf("request-%d", i)}) - } - errCh <- nil - }() - - time.Sleep(10 * time.Millisecond) - sm.ClearSession(sess) - - select { - case err := <-errCh: - if err != nil { - t.Fatal(err) - } - case <-time.After(time.Second): - t.Fatalf("SendToAgent did not unblock after session close") - } -} - -func TestSendToAgentContextTimeoutKeepsSessionAlive(t *testing.T) { - t.Parallel() - - sm := newTestSessionManager() - sess := session.NewAgentSession(sm.LatestAuthSnapshot("desktop-agent")) - sm.SetSession(sess) - - for i := 0; i < cap(sess.Outbound()); i += 1 { - if err := sess.SendToAgent(&gatewayv2.GatewayEnvelope{RequestId: fmt.Sprintf("queued-%d", i)}); err != nil { - t.Fatalf("prime outbound queue: %v", err) - } - } - - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) - defer cancel() - - err := sm.SendToAgentContext(ctx, "desktop-agent", &gatewayv2.GatewayEnvelope{RequestId: "blocked"}) - if !errors.Is(err, context.DeadlineExceeded) { - t.Fatalf("SendToAgentContext with full queue = %v, want context deadline exceeded", err) - } - if status := sm.Status("desktop-agent"); !status.Online { - t.Fatalf("status online = false after SendToAgentContext timeout; congestion must not kill the session") - } - select { - case <-sess.Done(): - t.Fatalf("session closed after SendToAgentContext timeout") - default: - } -} - -func TestSendPingBypassesFullOutboundQueue(t *testing.T) { - t.Parallel() - - sm := newTestSessionManager() - sess := session.NewAgentSession(sm.LatestAuthSnapshot("desktop-agent")) - sm.SetSession(sess) - - for i := 0; i < cap(sess.Outbound()); i += 1 { - if err := sess.SendToAgent(&gatewayv2.GatewayEnvelope{RequestId: fmt.Sprintf("queued-%d", i)}); err != nil { - t.Fatalf("prime outbound queue: %v", err) - } - } - - if err := sess.SendPing(&gatewayv2.GatewayEnvelope{RequestId: "ping-1"}); err != nil { - t.Fatalf("SendPing with full outbound queue: %v", err) - } - if err := sess.SendPing(&gatewayv2.GatewayEnvelope{RequestId: "ping-2"}); err != nil { - t.Fatalf("SendPing replacing queued ping: %v", err) - } - - select { - case ping := <-sess.Pings(): - if ping.GetRequestId() != "ping-2" { - t.Fatalf("queued ping = %q, want latest ping-2", ping.GetRequestId()) - } - default: - t.Fatalf("no ping queued on the dedicated lane") - } - - sess.Close() - if err := sess.SendPing(&gatewayv2.GatewayEnvelope{RequestId: "ping-3"}); !errors.Is(err, session.ErrAgentOffline) { - t.Fatalf("SendPing after close = %v, want ErrAgentOffline", err) - } -} - -func TestChatQueueEventsReplayLatestSnapshotToNewSubscribers(t *testing.T) { - t.Parallel() - - sm := newTestSessionManager() - sm.SetSession(session.NewAgentSession(sm.LatestAuthSnapshot("desktop-agent"))) - sm.DispatchFromAgent("desktop-agent", &gatewayv2.AgentEnvelope{ - RequestId: "queue-event-1", - Payload: &gatewayv2.AgentEnvelope_ChatQueueEvent{ - ChatQueueEvent: &gatewayv2.ChatQueueEvent{ - ConversationId: " conversation-1 ", - SnapshotJson: `{"conversationId":"conversation-1","revision":2,"items":[{"id":"queue-1"}]}`, - Revision: 2, - }, - }, - }) - sm.DispatchFromAgent("desktop-agent", &gatewayv2.AgentEnvelope{ - RequestId: "queue-event-stale", - Payload: &gatewayv2.AgentEnvelope_ChatQueueEvent{ - ChatQueueEvent: &gatewayv2.ChatQueueEvent{ - ConversationId: "conversation-1", - SnapshotJson: `{"conversationId":"conversation-1","revision":1,"items":[]}`, - Revision: 1, - }, - }, - }) - sm.DispatchFromAgent("desktop-agent", &gatewayv2.AgentEnvelope{ - RequestId: "queue-event-zero", - Payload: &gatewayv2.AgentEnvelope_ChatQueueEvent{ - ChatQueueEvent: &gatewayv2.ChatQueueEvent{ - ConversationId: "conversation-1", - SnapshotJson: `{"conversationId":"conversation-1","revision":0,"items":[]}`, - Revision: 0, - }, - }, - }) - - cached, ok := sm.ChatQueueSnapshot("desktop-agent", "conversation-1") - if !ok || cached.GetRevision() != 2 || !strings.Contains(cached.GetSnapshotJson(), "queue-1") { - t.Fatalf("cached queue snapshot = %#v ok=%v, want revision 2 with queue-1", cached, ok) - } - - events, cleanup := sm.SubscribeChatQueueEvents() - defer cleanup() - select { - case tagged := <-events: - event := tagged.Event - if event.GetConversationId() != "conversation-1" || - event.GetRevision() != 2 || - !strings.Contains(event.GetSnapshotJson(), "queue-1") { - t.Fatalf("replayed queue snapshot = %#v, want latest revision 2", event) - } - case <-time.After(time.Second): - t.Fatal("timed out waiting for replayed queue snapshot") - } -} - -func TestChatQueueSnapshotAllowsNewSessionToResetRevision(t *testing.T) { - t.Parallel() - - sm := newTestSessionManager() - sm.SetSession(session.NewAgentSession(sm.LatestAuthSnapshot("desktop-agent"))) - sm.DispatchFromAgent("desktop-agent", &gatewayv2.AgentEnvelope{ - RequestId: "queue-event-1", - Payload: &gatewayv2.AgentEnvelope_ChatQueueEvent{ - ChatQueueEvent: &gatewayv2.ChatQueueEvent{ - ConversationId: "conversation-1", - SnapshotJson: `{"conversationId":"conversation-1","revision":5,"items":[{"id":"queue-1"}]}`, - Revision: 5, - }, - }, - }) - - sm.SetSession(session.NewAgentSession(sm.LatestAuthSnapshot("desktop-agent"))) - sm.DispatchFromAgent("desktop-agent", &gatewayv2.AgentEnvelope{ - RequestId: "queue-event-reset", - Payload: &gatewayv2.AgentEnvelope_ChatQueueEvent{ - ChatQueueEvent: &gatewayv2.ChatQueueEvent{ - ConversationId: "conversation-1", - SnapshotJson: `{"conversationId":"conversation-1","revision":0,"items":[]}`, - Revision: 0, - }, - }, - }) - - cached, ok := sm.ChatQueueSnapshot("desktop-agent", "conversation-1") - if !ok || cached.GetRevision() != 0 || strings.Contains(cached.GetSnapshotJson(), "queue-1") { - t.Fatalf("cached queue snapshot after new session = %#v ok=%v, want empty revision 0", cached, ok) - } -} - -func dispatchChatToken(sm *session.Manager, requestID string, conversationID string, text string) { - sm.DispatchFromAgent("desktop-agent", &gatewayv2.AgentEnvelope{ - RequestId: requestID, - Payload: &gatewayv2.AgentEnvelope_ChatEvent{ - ChatEvent: &gatewayv2.ChatEvent{ - Type: gatewayv2.ChatEvent_TOKEN, - ConversationId: conversationID, - Data: fmt.Sprintf(`{"text":%q}`, text), - }, - }, - }) -} - -func dispatchChatDone(sm *session.Manager, requestID string, conversationID string) { - sm.DispatchFromAgent("desktop-agent", &gatewayv2.AgentEnvelope{ - RequestId: requestID, - Payload: &gatewayv2.AgentEnvelope_ChatEvent{ - ChatEvent: &gatewayv2.ChatEvent{ - Type: gatewayv2.ChatEvent_DONE, - ConversationId: conversationID, - Data: "{}", - }, - }, - }) -} - -func TestConversationStreamSeqContinuesAcrossDispatchedRuns(t *testing.T) { - t.Parallel() - - sm := newTestSessionManager() - sm.SetSession(session.NewAgentSession(sm.LatestAuthSnapshot("desktop-agent"))) - - dispatchChatControl(sm, "run-1", "conversation-1", "started", "running") - dispatchChatToken(sm, "run-1", "conversation-1", "first") - dispatchChatDone(sm, "run-1", "conversation-1") - dispatchChatControl(sm, "run-2", "conversation-1", "started", "running") - dispatchChatToken(sm, "run-2", "conversation-1", "second") - - sub := sm.SubscribeConversationStream("desktop-agent", "conversation-1", 0, "") - defer sub.Cleanup() - - var lastSeq int64 - runFinished := 0 - for _, event := range sub.Events { - if event.Seq <= lastSeq { - t.Fatalf("seq regressed: %d after %d", event.Seq, lastSeq) - } - lastSeq = event.Seq - if event.Type == "run_finished" { - runFinished++ - } - } - if runFinished != 1 { - t.Fatalf("run_finished events = %d, want 1", runFinished) - } - if sub.Activity == nil || sub.Activity.RunID != "run-2" { - t.Fatalf("activity = %#v, want run-2", sub.Activity) - } -} - -func TestDispatchedHistoryRunningIdleAreDropped(t *testing.T) { - t.Parallel() - - sm := newTestSessionManager() - sm.SetSession(session.NewAgentSession(sm.LatestAuthSnapshot("desktop-agent"))) - - historyEvents, cleanup := sm.SubscribeHistorySync() - defer cleanup() - - for _, kind := range []string{"running", "idle"} { - sm.DispatchFromAgent("desktop-agent", &gatewayv2.AgentEnvelope{ - RequestId: "history-sync", - Payload: &gatewayv2.AgentEnvelope_HistorySync{ - HistorySync: &gatewayv2.HistorySyncEvent{ - Kind: kind, - ConversationId: "conversation-1", - }, - }, - }) - } - - select { - case event := <-historyEvents: - t.Fatalf("agent running/idle history event should be dropped, got %#v", event) - case <-time.After(50 * time.Millisecond): - } - if activities := sm.ActiveConversationActivities(); len(activities) != 0 { - t.Fatalf("history running must not create activity, got %#v", activities) - } - - sm.DispatchFromAgent("desktop-agent", &gatewayv2.AgentEnvelope{ - RequestId: "history-sync", - Payload: &gatewayv2.AgentEnvelope_HistorySync{ - HistorySync: &gatewayv2.HistorySyncEvent{ - Kind: "upsert", - ConversationId: "conversation-1", - Conversation: &gatewayv2.ConversationSummary{Id: "conversation-1"}, - }, - }, - }) - select { - case tagged := <-historyEvents: - if tagged.Event.GetKind() != "upsert" { - t.Fatalf("history event kind = %q, want upsert", tagged.Event.GetKind()) - } - case <-time.After(time.Second): - t.Fatalf("upsert history event was not forwarded") - } -} - -func TestAgentDisconnectPreservesActiveConversationActivity(t *testing.T) { - t.Parallel() - - sm := newTestSessionManager() - sess := session.NewAgentSession(sm.LatestAuthSnapshot("desktop-agent")) - sm.SetSession(sess) - - dispatchChatControl(sm, "run-1", "conversation-1", "started", "running") - sm.ClearSession(sess) - - activities := sm.ActiveConversationActivities() - if len(activities) != 1 || activities[0].RunID != "run-1" { - t.Fatalf("activities after disconnect = %#v, want run-1 preserved", activities) - } -} - -func TestTerminalSnapshotFinishesRunAndStaleRunningIsIgnored(t *testing.T) { - t.Parallel() - - sm := newTestSessionManager() - sm.SetSession(session.NewAgentSession(sm.LatestAuthSnapshot("desktop-agent"))) - - dispatchChatControl(sm, "run-1", "conversation-1", "started", "running") - sm.DispatchFromAgent("desktop-agent", &gatewayv2.AgentEnvelope{ - RequestId: "run-1", - Payload: &gatewayv2.AgentEnvelope_ChatRuntimeSnapshot{ - ChatRuntimeSnapshot: &gatewayv2.ChatRuntimeSnapshot{ - RunId: "run-1", - ConversationId: "conversation-1", - State: "completed", - }, - }, - }) - if activities := sm.ActiveConversationActivities(); len(activities) != 0 { - t.Fatalf("terminal snapshot should clear activity, got %#v", activities) - } - - // A stale "running" snapshot after the terminal must not resurrect the run. - sm.DispatchFromAgent("desktop-agent", &gatewayv2.AgentEnvelope{ - RequestId: "run-1", - Payload: &gatewayv2.AgentEnvelope_ChatRuntimeSnapshot{ - ChatRuntimeSnapshot: &gatewayv2.ChatRuntimeSnapshot{ - RunId: "run-1", - ConversationId: "conversation-1", - State: "running", - }, - }, - }) - if activities := sm.ActiveConversationActivities(); len(activities) != 0 { - t.Fatalf("stale running snapshot resurrected the run: %#v", activities) - } - - sub := sm.SubscribeConversationStream("desktop-agent", "conversation-1", 0, "") - defer sub.Cleanup() - finished := 0 - for _, event := range sub.Events { - if event.Type == "run_finished" { - finished++ - } - } - if finished != 1 { - t.Fatalf("run_finished events = %d, want exactly 1", finished) - } -} diff --git a/crates/agent-gateway/test/tunnel/tunnel_e2e_test.go b/crates/agent-gateway/test/tunnel/tunnel_e2e_test.go deleted file mode 100644 index 9dd0eac85..000000000 --- a/crates/agent-gateway/test/tunnel/tunnel_e2e_test.go +++ /dev/null @@ -1,373 +0,0 @@ -package tunnel_test - -import ( - "io" - "net/http" - "net/http/httptest" - "strings" - "sync" - "testing" - "time" - - "github.com/gorilla/websocket" - "github.com/liveagent/agent-gateway/internal/config" - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" - "github.com/liveagent/agent-gateway/internal/server" - "github.com/liveagent/agent-gateway/internal/session" -) - -// fakeAgent emulates the desktop agent's data-plane behavior in-process: it -// drains the session outbound queue and answers tunnel frames the way the -// Rust proxy does (HTTP echo, SSE stream, WS echo, PONG). -type fakeAgent struct { - sm *session.Manager - sess *session.AgentSession - done chan struct{} - once sync.Once -} - -func startFakeAgent(t *testing.T) (*session.Manager, *fakeAgent) { - t.Helper() - sm := session.NewManager() - sess := session.NewAgentSession(session.AuthSnapshot{AgentID: "fake-agent"}) - sm.SetSession(sess) - agent := &fakeAgent{sm: sm, sess: sess, done: make(chan struct{})} - go agent.run() - t.Cleanup(agent.stop) - return sm, agent -} - -func (a *fakeAgent) stop() { - a.once.Do(func() { close(a.done) }) -} - -func (a *fakeAgent) run() { - for { - select { - case <-a.done: - return - case outbound := <-a.sess.Outbound(): - if outbound == nil || outbound.GatewayEnvelope == nil { - continue - } - outbound.Ack(nil) - frame := outbound.GetTunnelFrame() - if frame == nil { - continue - } - a.handleFrame(frame) - } - } -} - -func (a *fakeAgent) reply(frame *gatewayv2.TunnelFrame) { - a.sm.DispatchFromAgentForSession(a.sess, &gatewayv2.AgentEnvelope{ - RequestId: "fake-agent-frame", - Timestamp: time.Now().Unix(), - Payload: &gatewayv2.AgentEnvelope_TunnelFrame{TunnelFrame: frame}, - }) -} - -func (a *fakeAgent) handleFrame(frame *gatewayv2.TunnelFrame) { - streamID := frame.GetStreamId() - switch frame.GetKind() { - case gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_PING: - a.reply(&gatewayv2.TunnelFrame{ - StreamId: streamID, - Kind: gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_PONG, - }) - case gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_REQUEST_START: - if strings.HasPrefix(frame.GetPath(), "/sse") { - a.reply(&gatewayv2.TunnelFrame{ - StreamId: streamID, - Kind: gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_RESPONSE_START, - Status: 200, - Headers: []*gatewayv2.TunnelHeader{ - {Name: "Content-Type", Value: "text/event-stream; charset=utf-8"}, - }, - }) - for _, chunk := range []string{"event: tick\ndata: 1\n\n", "event: tick\ndata: 2\n\n"} { - a.reply(&gatewayv2.TunnelFrame{ - StreamId: streamID, - Kind: gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_RESPONSE_BODY, - Body: []byte(chunk), - }) - } - a.reply(&gatewayv2.TunnelFrame{ - StreamId: streamID, - Kind: gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_RESPONSE_END, - }) - return - } - a.reply(&gatewayv2.TunnelFrame{ - StreamId: streamID, - Kind: gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_RESPONSE_START, - Status: 200, - Headers: []*gatewayv2.TunnelHeader{ - {Name: "Content-Type", Value: "text/plain; charset=utf-8"}, - }, - }) - a.reply(&gatewayv2.TunnelFrame{ - StreamId: streamID, - Kind: gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_RESPONSE_BODY, - Body: []byte("hello " + frame.GetMethod() + " " + frame.GetPath()), - }) - a.reply(&gatewayv2.TunnelFrame{ - StreamId: streamID, - Kind: gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_RESPONSE_END, - }) - case gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_WS_DIAL: - a.reply(&gatewayv2.TunnelFrame{ - StreamId: streamID, - Kind: gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_WS_DIAL_OK, - }) - case gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_WS_FRAME: - if string(frame.GetBody()) == "close-me" { - // Emulate the local service closing the socket with its own code. - a.reply(&gatewayv2.TunnelFrame{ - StreamId: streamID, - Kind: gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_WS_CLOSE, - WsCloseCode: 4321, - WsCloseReason: "goodbye", - }) - return - } - a.reply(&gatewayv2.TunnelFrame{ - StreamId: streamID, - Kind: gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_WS_FRAME, - Body: append([]byte("echo:"), frame.GetBody()...), - WsMessageType: frame.GetWsMessageType(), - }) - } -} - -func startTunnelTestServer(t *testing.T, sm *session.Manager) *httptest.Server { - t.Helper() - handler := server.NewHTTPServer(&config.Config{ - Token: "dev-token", - RequestTimeout: 2 * time.Second, - }, sm, nil) - ts := httptest.NewServer(handler) - t.Cleanup(ts.Close) - return ts -} - -func applyOneTunnel(t *testing.T, sm *session.Manager) string { - t.Helper() - sm.ApplyDesiredState("fake-agent", &gatewayv2.TunnelDesiredState{ - Tunnels: []*gatewayv2.TunnelSpec{ - {Id: "tun-e2e", TargetUrl: "http://localhost:3999", Name: "e2e"}, - }, - }) - snapshot := sm.TunnelStateSnapshot("fake-agent") - if len(snapshot.GetTunnels()) != 1 { - t.Fatalf("tunnels = %d, want 1", len(snapshot.GetTunnels())) - } - slug := snapshot.GetTunnels()[0].GetSlug() - if slug == "" { - t.Fatal("no slug allocated") - } - return slug -} - -func TestTunnelEndToEndHTTP(t *testing.T) { - sm, _ := startFakeAgent(t) - ts := startTunnelTestServer(t, sm) - slug := applyOneTunnel(t, sm) - - resp, err := http.Get(ts.URL + "/t/" + slug + "/app/page?x=1") - if err != nil { - t.Fatalf("GET through tunnel: %v", err) - } - defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) - if resp.StatusCode != http.StatusOK { - t.Fatalf("status = %d body=%s", resp.StatusCode, body) - } - if got := string(body); got != "hello GET /app/page?x=1" { - t.Fatalf("body = %q", got) - } -} - -func TestTunnelEndToEndSSEStreams(t *testing.T) { - sm, _ := startFakeAgent(t) - ts := startTunnelTestServer(t, sm) - slug := applyOneTunnel(t, sm) - - resp, err := http.Get(ts.URL + "/t/" + slug + "/sse") - if err != nil { - t.Fatalf("GET sse through tunnel: %v", err) - } - defer resp.Body.Close() - if contentType := resp.Header.Get("Content-Type"); !strings.Contains(contentType, "text/event-stream") { - t.Fatalf("content-type = %q", contentType) - } - body, _ := io.ReadAll(resp.Body) - if !strings.Contains(string(body), "data: 1") || !strings.Contains(string(body), "data: 2") { - t.Fatalf("sse body = %q", body) - } -} - -func TestTunnelEndToEndWebSocketEchoAndClose(t *testing.T) { - sm, _ := startFakeAgent(t) - ts := startTunnelTestServer(t, sm) - slug := applyOneTunnel(t, sm) - - wsURL := "ws" + strings.TrimPrefix(ts.URL, "http") + "/t/" + slug + "/socket" - conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil) - if err != nil { - t.Fatalf("dial tunnel websocket: %v", err) - } - defer conn.Close() - - if err := conn.WriteMessage(websocket.TextMessage, []byte("ping")); err != nil { - t.Fatalf("write: %v", err) - } - _ = conn.SetReadDeadline(time.Now().Add(2 * time.Second)) - messageType, body, err := conn.ReadMessage() - if err != nil { - t.Fatalf("read echo: %v", err) - } - if messageType != websocket.TextMessage || string(body) != "echo:ping" { - t.Fatalf("echo = (%d, %q)", messageType, body) - } - - // Upstream-initiated close: the local service's close code/reason must - // reach the visitor verbatim through the frame relay. - if err := conn.WriteMessage(websocket.TextMessage, []byte("close-me")); err != nil { - t.Fatalf("write close-me: %v", err) - } - _ = conn.SetReadDeadline(time.Now().Add(2 * time.Second)) - _, _, err = conn.ReadMessage() - closeErr, ok := err.(*websocket.CloseError) - if !ok { - t.Fatalf("expected close error, got %v", err) - } - if closeErr.Code != 4321 || closeErr.Text != "goodbye" { - t.Fatalf("close = (%d, %q), want (4321, goodbye)", closeErr.Code, closeErr.Text) - } -} - -// TestTunnelTrafficWhileControlPlaneBusy is the deadlock regression: the old -// design executed probes synchronously on the agent read loop, so any control -// activity starved the data plane. The new design must serve traffic while -// desired-state applies and relay probes run concurrently. -func TestTunnelTrafficWhileControlPlaneBusy(t *testing.T) { - sm, _ := startFakeAgent(t) - ts := startTunnelTestServer(t, sm) - slug := applyOneTunnel(t, sm) - - stop := make(chan struct{}) - var controlWG sync.WaitGroup - controlWG.Add(1) - go func() { - defer controlWG.Done() - for { - select { - case <-stop: - return - default: - sm.ApplyDesiredState("fake-agent", &gatewayv2.TunnelDesiredState{ - Tunnels: []*gatewayv2.TunnelSpec{ - {Id: "tun-e2e", TargetUrl: "http://localhost:3999", Name: "e2e", SlugHint: slug}, - }, - }) - } - } - }() - - deadline := time.Now().Add(2 * time.Second) - requests := 0 - for time.Now().Before(deadline) { - client := http.Client{Timeout: time.Second} - resp, err := client.Get(ts.URL + "/t/" + slug + "/") - if err != nil { - close(stop) - controlWG.Wait() - t.Fatalf("request %d during control churn: %v", requests, err) - } - _, _ = io.Copy(io.Discard, resp.Body) - _ = resp.Body.Close() - if resp.StatusCode != http.StatusOK { - close(stop) - controlWG.Wait() - t.Fatalf("request %d status = %d", requests, resp.StatusCode) - } - requests += 1 - } - close(stop) - controlWG.Wait() - if requests < 10 { - t.Fatalf("only %d requests completed during control churn", requests) - } -} - -func TestTunnelHTMLRewriteInjectsShimAndDropsContentLength(t *testing.T) { - sm := session.NewManager() - sess := session.NewAgentSession(session.AuthSnapshot{AgentID: "fake-agent"}) - sm.SetSession(sess) - agent := &fakeAgent{sm: sm, sess: sess, done: make(chan struct{})} - t.Cleanup(agent.stop) - - html := "x" - go func() { - for { - select { - case <-agent.done: - return - case outbound := <-sess.Outbound(): - if outbound == nil || outbound.GatewayEnvelope == nil { - continue - } - outbound.Ack(nil) - frame := outbound.GetTunnelFrame() - if frame == nil || - frame.GetKind() != gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_REQUEST_START { - continue - } - agent.reply(&gatewayv2.TunnelFrame{ - StreamId: frame.GetStreamId(), - Kind: gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_RESPONSE_START, - Status: 200, - Headers: []*gatewayv2.TunnelHeader{ - {Name: "Content-Type", Value: "text/html; charset=utf-8"}, - {Name: "Content-Length", Value: "999"}, - {Name: "Content-Security-Policy", Value: "script-src 'self'"}, - }, - }) - agent.reply(&gatewayv2.TunnelFrame{ - StreamId: frame.GetStreamId(), - Kind: gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_RESPONSE_BODY, - Body: []byte(html), - }) - agent.reply(&gatewayv2.TunnelFrame{ - StreamId: frame.GetStreamId(), - Kind: gatewayv2.TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_RESPONSE_END, - }) - } - } - }() - - ts := startTunnelTestServer(t, sm) - slug := applyOneTunnel(t, sm) - - resp, err := http.Get(ts.URL + "/t/" + slug + "/") - if err != nil { - t.Fatalf("GET html through tunnel: %v", err) - } - defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) - - if resp.Header.Get("Content-Length") == "999" { - t.Fatal("stale Content-Length must be dropped for rewritten responses") - } - if !strings.Contains(string(body), "data-liveagent-tunnel-shim") { - t.Fatalf("shim not injected: %q", body) - } - if !strings.Contains(string(body), "/t/"+slug+"/about") { - t.Fatalf("href not rewritten: %q", body) - } - if policy := resp.Header.Get("Content-Security-Policy"); !strings.Contains(policy, "'sha256-") { - t.Fatalf("CSP not hash-amended: %q", policy) - } -} diff --git a/crates/agent-gateway/test/upload/import_readable_files_test.go b/crates/agent-gateway/test/upload/import_readable_files_test.go deleted file mode 100644 index 79cfede5c..000000000 --- a/crates/agent-gateway/test/upload/import_readable_files_test.go +++ /dev/null @@ -1,199 +0,0 @@ -package upload_test - -import ( - "bytes" - "encoding/json" - "io" - "mime/multipart" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/liveagent/agent-gateway/internal/config" - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" - "github.com/liveagent/agent-gateway/internal/server" - "github.com/liveagent/agent-gateway/internal/session" -) - -func TestImportReadableFilesForwardsMultipartToAgent(t *testing.T) { - t.Parallel() - - sm := session.NewManager() - sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1") - agentSession := session.NewAgentSession(sm.LatestAuthSnapshot("desktop-agent")) - sm.SetSession(agentSession) - - handler := server.NewHTTPServer(&config.Config{ - Token: "upload-token", - RequestTimeout: time.Second, - }, sm, nil) - - var body bytes.Buffer - writer := multipart.NewWriter(&body) - if err := writer.WriteField("workdir", " /workspace/project "); err != nil { - t.Fatalf("write workdir field: %v", err) - } - part, err := writer.CreateFormFile("files", "notes.txt") - if err != nil { - t.Fatalf("create file part: %v", err) - } - if _, err := io.WriteString(part, "hello from upload"); err != nil { - t.Fatalf("write file part: %v", err) - } - part, err = writer.CreateFormFile("files", "tasks.md") - if err != nil { - t.Fatalf("create second file part: %v", err) - } - if _, err := io.WriteString(part, "# tasks"); err != nil { - t.Fatalf("write second file part: %v", err) - } - if err := writer.Close(); err != nil { - t.Fatalf("close multipart writer: %v", err) - } - - req := httptest.NewRequest(http.MethodPost, "http://gateway.test/api/files/import?agent_id=desktop-agent", &body) - req.Header.Set("Authorization", "Bearer upload-token") - req.Header.Set("Content-Type", writer.FormDataContentType()) - rec := httptest.NewRecorder() - - done := make(chan struct{}) - go func() { - defer close(done) - handler.ServeHTTP(rec, req) - }() - - var outbound *gatewayv2.GatewayEnvelope - select { - case delivered := <-agentSession.Outbound(): - delivered.Ack(nil) - outbound = delivered.GatewayEnvelope - case <-time.After(time.Second): - t.Fatalf("timed out waiting for upload request to reach agent") - } - - uploadReq := outbound.GetUploadReadableFiles() - if uploadReq == nil { - t.Fatalf("outbound payload = %T, want UploadReadableFilesRequest", outbound.GetPayload()) - } - if uploadReq.GetWorkdir() != "/workspace/project" { - t.Fatalf("workdir = %q, want trimmed workdir", uploadReq.GetWorkdir()) - } - if len(uploadReq.GetFiles()) != 2 { - t.Fatalf("files len = %d, want 2", len(uploadReq.GetFiles())) - } - file := uploadReq.GetFiles()[0] - if file.GetFileName() != "notes.txt" { - t.Fatalf("file name = %q, want notes.txt", file.GetFileName()) - } - if string(file.GetContent()) != "hello from upload" { - t.Fatalf("file content = %q", string(file.GetContent())) - } - secondFile := uploadReq.GetFiles()[1] - if secondFile.GetFileName() != "tasks.md" { - t.Fatalf("second file name = %q, want tasks.md", secondFile.GetFileName()) - } - if string(secondFile.GetContent()) != "# tasks" { - t.Fatalf("second file content = %q", string(secondFile.GetContent())) - } - - sm.DispatchFromAgentForSession(agentSession, &gatewayv2.AgentEnvelope{ - RequestId: outbound.GetRequestId(), - Timestamp: time.Now().Unix(), - Payload: &gatewayv2.AgentEnvelope_UploadReadableFilesResp{ - UploadReadableFilesResp: &gatewayv2.UploadReadableFilesResponse{ - Files: []*gatewayv2.ChatUploadedFile{ - { - RelativePath: "uploads/notes.txt", - AbsolutePath: "/workspace/project/uploads/notes.txt", - FileName: "notes.txt", - Kind: "text", - SizeBytes: int64(len("hello from upload")), - }, - { - RelativePath: "uploads/tasks.md", - AbsolutePath: "/workspace/project/uploads/tasks.md", - FileName: "tasks.md", - Kind: "text", - SizeBytes: int64(len("# tasks")), - }, - }, - Skipped: []string{"ignored.bin"}, - }, - }, - }) - - select { - case <-done: - case <-time.After(time.Second): - t.Fatalf("timed out waiting for HTTP response") - } - - if rec.Code != http.StatusOK { - t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String()) - } - - var payload struct { - Files []struct { - RelativePath string `json:"relativePath"` - AbsolutePath string `json:"absolutePath"` - FileName string `json:"fileName"` - Kind string `json:"kind"` - SizeBytes int64 `json:"sizeBytes"` - } `json:"files"` - Skipped []string `json:"skipped"` - } - if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil { - t.Fatalf("decode response: %v", err) - } - if len(payload.Files) != 2 || - payload.Files[0].RelativePath != "uploads/notes.txt" || - payload.Files[1].RelativePath != "uploads/tasks.md" { - t.Fatalf("files payload = %#v", payload.Files) - } - if len(payload.Skipped) != 1 || payload.Skipped[0] != "ignored.bin" { - t.Fatalf("skipped payload = %#v", payload.Skipped) - } -} - -func TestImportReadableFilesRequiresAgentID(t *testing.T) { - t.Parallel() - - handler := server.NewHTTPServer(&config.Config{ - Token: "upload-token", - RequestTimeout: time.Second, - }, session.NewManager(), nil) - - req := httptest.NewRequest(http.MethodPost, "http://gateway.test/api/files/import", nil) - req.Header.Set("Authorization", "Bearer upload-token") - rec := httptest.NewRecorder() - handler.ServeHTTP(rec, req) - - if rec.Code != http.StatusBadRequest { - t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest) - } - if !bytes.Contains(rec.Body.Bytes(), []byte("agent_id is required")) { - t.Fatalf("body = %q, want agent_id validation error", rec.Body.String()) - } -} - -func TestImportReadableFilesRejectsOfflineAgentBeforeParsing(t *testing.T) { - t.Parallel() - - handler := server.NewHTTPServer(&config.Config{ - Token: "upload-token", - RequestTimeout: time.Second, - }, session.NewManager(), nil) - - req := httptest.NewRequest(http.MethodPost, "http://gateway.test/api/files/import?agent_id=desktop-agent", nil) - req.Header.Set("Authorization", "Bearer upload-token") - rec := httptest.NewRecorder() - handler.ServeHTTP(rec, req) - - if rec.Code != http.StatusServiceUnavailable { - t.Fatalf("status = %d, want %d", rec.Code, http.StatusServiceUnavailable) - } - if rec.Body.String() == "" { - t.Fatalf("expected JSON error body") - } -} diff --git a/crates/agent-gateway/test/websocket/agent_token_test.go b/crates/agent-gateway/test/websocket/agent_token_test.go deleted file mode 100644 index ed581853a..000000000 --- a/crates/agent-gateway/test/websocket/agent_token_test.go +++ /dev/null @@ -1,356 +0,0 @@ -package websocket_test - -// 每 Agent 凭证(agenttoken)集成测试:角色-凭证绑定、签发/轮换/撤销、撤销踢线。 - -import ( - "errors" - "net/http" - "path/filepath" - "testing" - "time" - - "github.com/gorilla/websocket" - "google.golang.org/protobuf/proto" - - "github.com/liveagent/agent-gateway/internal/auth/agenttoken" - "github.com/liveagent/agent-gateway/internal/db" - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" - "github.com/liveagent/agent-gateway/internal/protocol/pbws" - "github.com/liveagent/agent-gateway/internal/session" -) - -func newAgentTokenStore(t *testing.T) *agenttoken.Store { - t.Helper() - _, store := openAgentTokenDB(t, filepath.Join(t.TempDir(), "agent-tokens.db")) - return store -} - -func agentTokenAuthenticates( - t *testing.T, - store *agenttoken.Store, - agentID string, - token string, -) bool { - t.Helper() - _, err := store.AuthenticateAndRegister(agentID, token, false) - if err == nil { - return true - } - if errors.Is(err, agenttoken.ErrUnauthorized) { - return false - } - t.Fatalf("authenticate agent token: %v", err) - return false -} - -// openAgentTokenDB 打开共享池并初始化凭证表(测试清理时关闭池)。 -func openAgentTokenDB(t *testing.T, path string) (*db.DB, *agenttoken.Store) { - t.Helper() - database, err := db.Open(path) - if err != nil { - t.Fatalf("open gateway db: %v", err) - } - t.Cleanup(func() { _ = database.Close() }) - store, err := agenttoken.NewStore(database) - if err != nil { - t.Fatalf("init agent token store: %v", err) - } - return database, store -} - -// dialV2AgentHello 拨号 agent 链路并发送 hello,返回服务端的 hello 判定。 -func dialV2AgentHello(t *testing.T, handler http.Handler, agentID, token string) *gatewayv2.ServerHello { - t.Helper() - conn, cleanup := dialV2(t, handler) - defer cleanup() - - sendProtoFrame(t, conn, &gatewayv2.AgentClientFrame{ - Payload: &gatewayv2.AgentClientFrame_Hello{ - Hello: &gatewayv2.ClientHello{ - ProtocolVersion: pbws.ProtocolVersion, - Role: gatewayv2.ClientRole_CLIENT_ROLE_AGENT, - AgentId: agentID, - Token: token, - }, - }, - }) - _ = conn.SetReadDeadline(time.Now().Add(time.Second)) - messageType, data, err := conn.ReadMessage() - if err != nil { - t.Fatalf("read agent hello reply: %v", err) - } - if messageType != websocket.BinaryMessage { - t.Fatalf("hello reply message type = %d", messageType) - } - var frame gatewayv2.AgentServerFrame - if err := proto.Unmarshal(data, &frame); err != nil { - t.Fatalf("unmarshal agent hello reply: %v", err) - } - hello := frame.GetHello() - if hello == nil { - t.Fatalf("agent hello reply = %#v, want hello", &frame) - } - return hello -} - -func dialV2TerminalAgent( - t *testing.T, - handler http.Handler, - agentID string, - token string, -) (*websocket.Conn, *gatewayv2.ServerHello, func()) { - t.Helper() - conn, cleanup := dialV2(t, handler) - sendProtoFrame(t, conn, &gatewayv2.TerminalClientFrame{ - Payload: &gatewayv2.TerminalClientFrame_Hello{ - Hello: &gatewayv2.ClientHello{ - ProtocolVersion: pbws.ProtocolVersion, - Role: gatewayv2.ClientRole_CLIENT_ROLE_AGENT, - AgentId: agentID, - Token: token, - }, - }, - }) - hello := receiveTerminalServerFrame(t, conn).GetHello() - if hello == nil { - cleanup() - t.Fatal("terminal agent hello reply is missing") - } - return conn, hello, cleanup -} - -func TestAgentCredentialsRequireIssuedToken(t *testing.T) { - t.Parallel() - - store := newAgentTokenStore(t) - tokenA, err := store.Issue("agent-a", "") - if err != nil { - t.Fatalf("issue agent-a token: %v", err) - } - sm := session.NewManager() - cfg := newV2TestConfig() - srv := pbws.NewServer(cfg, sm, store) - - // 正确凭证 + 正确 id:通过。 - if hello := dialV2AgentHello(t, srv.AgentHandler(), "agent-a", tokenA); !hello.GetOk() { - t.Fatalf("agent-a with own token rejected: %q", hello.GetMessage()) - } - // A 的凭证声明 B 的身份:拒绝(凭证按 id 绑定)。 - if hello := dialV2AgentHello(t, srv.AgentHandler(), "agent-b", tokenA); hello.GetOk() { - t.Fatal("agent-a token must not authenticate agent-b") - } - // agent_id 必填。 - if hello := dialV2AgentHello(t, srv.AgentHandler(), "", tokenA); hello.GetOk() { - t.Fatal("empty agent_id must be rejected") - } -} - -func TestAgentAuthAcceptsGatewayAndPerAgentTokens(t *testing.T) { - t.Parallel() - - store := newAgentTokenStore(t) - agentToken, err := store.Issue("agent-a", "") - if err != nil { - t.Fatalf("issue agent token: %v", err) - } - srv := pbws.NewServer(newV2TestConfig(), session.NewManager(), store) - if hello := dialV2AgentHello(t, srv.AgentHandler(), "gateway-token-agent", "ws-token"); !hello.GetOk() { - t.Fatalf("gateway token rejected: %q", hello.GetMessage()) - } - if hello := dialV2AgentHello(t, srv.AgentHandler(), "agent-a", agentToken); !hello.GetOk() { - t.Fatalf("agent token rejected: %q", hello.GetMessage()) - } - registered, err := store.Registered() - if err != nil { - t.Fatalf("list registered agents: %v", err) - } - if len(registered) != 2 || registered[1].AgentID != "gateway-token-agent" { - t.Fatalf("gateway-token agent was not persisted in directory: %#v", registered) - } -} - -func TestTerminalAgentAuthAcceptsGatewayAndPerAgentTokens(t *testing.T) { - t.Parallel() - - store := newAgentTokenStore(t) - agentToken, err := store.Issue("agent-a", "") - if err != nil { - t.Fatalf("issue agent token: %v", err) - } - srv := pbws.NewServer(newV2TestConfig(), session.NewManager(), store) - - for _, test := range []struct { - name string - agentID string - token string - }{ - {name: "gateway token", agentID: "gateway-token-agent", token: "ws-token"}, - {name: "per-agent token", agentID: "agent-a", token: agentToken}, - } { - t.Run(test.name, func(t *testing.T) { - _, hello, cleanup := dialV2TerminalAgent(t, srv.TerminalHandler(), test.agentID, test.token) - defer cleanup() - if !hello.GetOk() { - t.Fatalf("terminal agent token rejected: %q", hello.GetMessage()) - } - }) - } - - _, hello, cleanup := dialV2TerminalAgent(t, srv.TerminalHandler(), "agent-a", "wrong-token") - defer cleanup() - if hello.GetOk() || hello.GetMessage() != "unauthorized" { - t.Fatalf("wrong terminal agent token = %#v, want unauthorized", hello) - } -} - -func TestTerminalAgentCredentialChangesRevokeLiveConnection(t *testing.T) { - t.Parallel() - - for _, test := range []struct { - name string - revoke func(*testing.T, *agenttoken.Store, *session.Manager) - }{ - { - name: "rotation", - revoke: func(t *testing.T, store *agenttoken.Store, sm *session.Manager) { - t.Helper() - if _, err := store.Issue("agent-a", ""); err != nil { - t.Fatalf("rotate token: %v", err) - } - if !sm.DisconnectAgent("agent-a") { - t.Fatal("rotation did not revoke terminal connection") - } - }, - }, - { - name: "delete", - revoke: func(t *testing.T, store *agenttoken.Store, sm *session.Manager) { - t.Helper() - if deleted, err := store.Delete("agent-a"); err != nil || !deleted { - t.Fatalf("delete agent = %v, %v", deleted, err) - } - if !sm.ForgetAgent("agent-a") { - t.Fatal("delete did not revoke terminal connection") - } - }, - }, - } { - t.Run(test.name, func(t *testing.T) { - t.Parallel() - store := newAgentTokenStore(t) - token, err := store.Issue("agent-a", "") - if err != nil { - t.Fatalf("issue token: %v", err) - } - sm := session.NewManager() - srv := pbws.NewServer(newV2TestConfig(), sm, store) - conn, hello, cleanup := dialV2TerminalAgent(t, srv.TerminalHandler(), "agent-a", token) - defer cleanup() - if !hello.GetOk() { - t.Fatalf("terminal agent hello rejected: %q", hello.GetMessage()) - } - - test.revoke(t, store, sm) - if err := conn.SetReadDeadline(time.Now().Add(time.Second)); err != nil { - t.Fatalf("set read deadline: %v", err) - } - if _, _, err := conn.ReadMessage(); err == nil { - t.Fatal("revoked terminal connection remained open") - } - }) - } -} - -func TestAgentIDRequired(t *testing.T) { - t.Parallel() - - srv := pbws.NewServer(newV2TestConfig(), session.NewManager(), nil) - hello := dialV2AgentHello(t, srv.AgentHandler(), "", "ws-token") - if hello.GetOk() || hello.GetMessage() != "agent_id is required" { - t.Fatalf("agent hello without id = %v, want rejection", hello) - } -} - -func TestAgentTokenRejectedOnBrowserLink(t *testing.T) { - t.Parallel() - - store := newAgentTokenStore(t) - tokenA, err := store.Issue("agent-a", "") - if err != nil { - t.Fatalf("issue token: %v", err) - } - sm := session.NewManager() - handler := pbws.NewServer(newV2TestConfig(), sm, store).BrowserHandler() - conn, cleanup := dialV2(t, handler) - defer cleanup() - - // Agent 凭证冒充浏览器(控制端):必须拒绝——角色-凭证绑定的另一半。 - sendProtoFrame(t, conn, &gatewayv2.WebClientFrame{ - RequestId: "hello-agent-token", - Payload: &gatewayv2.WebClientFrame_Hello{ - Hello: &gatewayv2.ClientHello{ - ProtocolVersion: pbws.ProtocolVersion, - Role: gatewayv2.ClientRole_CLIENT_ROLE_BROWSER, - Token: tokenA, - }, - }, - }) - frame := receiveWebFrameRaw(t, conn) - if hello := frame.GetHello(); hello == nil || hello.GetOk() { - t.Fatalf("browser hello with agent token = %#v, want rejection", frame) - } -} - -func TestTokenRotationInvalidatesOldCredential(t *testing.T) { - t.Parallel() - - store := newAgentTokenStore(t) - oldToken, err := store.Issue("agent-a", "") - if err != nil { - t.Fatalf("issue: %v", err) - } - newToken, err := store.Issue("agent-a", "") - if err != nil { - t.Fatalf("rotate: %v", err) - } - if agentTokenAuthenticates(t, store, "agent-a", oldToken) { - t.Fatal("rotated-out token must be invalid") - } - if !agentTokenAuthenticates(t, store, "agent-a", newToken) { - t.Fatal("rotated-in token must be valid") - } -} - -func TestDeleteInvalidatesAndSurvivesReload(t *testing.T) { - t.Parallel() - - path := filepath.Join(t.TempDir(), "agent-tokens.db") - database, store := openAgentTokenDB(t, path) - token, err := store.Issue("agent-a", "prod laptop") - if err != nil { - t.Fatalf("issue: %v", err) - } - if err := database.Close(); err != nil { - t.Fatalf("close: %v", err) - } - - // 落盘后重开(模拟网关重启):凭证仍有效,删除也持久化。 - reloadedDB, reloaded := openAgentTokenDB(t, path) - if !agentTokenAuthenticates(t, reloaded, "agent-a", token) { - t.Fatal("token must survive store reload") - } - if deleted, err := reloaded.Delete("agent-a"); err != nil || !deleted { - t.Fatalf("delete = %v, %v", deleted, err) - } - if agentTokenAuthenticates(t, reloaded, "agent-a", token) { - t.Fatal("deleted token must be invalid") - } - if err := reloadedDB.Close(); err != nil { - t.Fatalf("close reloaded: %v", err) - } - - _, again := openAgentTokenDB(t, path) - if agentTokenAuthenticates(t, again, "agent-a", token) { - t.Fatal("deletion must survive reload") - } -} diff --git a/crates/agent-gateway/test/websocket/multiagent_benchmark_test.go b/crates/agent-gateway/test/websocket/multiagent_benchmark_test.go deleted file mode 100644 index 7038059ad..000000000 --- a/crates/agent-gateway/test/websocket/multiagent_benchmark_test.go +++ /dev/null @@ -1,196 +0,0 @@ -package websocket_test - -import ( - "fmt" - "net/http" - "net/http/httptest" - "path/filepath" - "strings" - "sync" - "testing" - "time" - - "github.com/gorilla/websocket" - "google.golang.org/protobuf/proto" - - "github.com/liveagent/agent-gateway/internal/auth/agenttoken" - "github.com/liveagent/agent-gateway/internal/config" - "github.com/liveagent/agent-gateway/internal/db" - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" - "github.com/liveagent/agent-gateway/internal/protocol/pbws" - "github.com/liveagent/agent-gateway/internal/session" -) - -const concurrentAgentConnections = 1000 - -type agentHandshakeResult struct { - conn *websocket.Conn - err error -} - -func benchmarkAgentHello(agentID, token string) ([]byte, error) { - return proto.Marshal(&gatewayv2.AgentClientFrame{ - Payload: &gatewayv2.AgentClientFrame_Hello{ - Hello: &gatewayv2.ClientHello{ - ProtocolVersion: pbws.ProtocolVersion, - Role: gatewayv2.ClientRole_CLIENT_ROLE_AGENT, - AgentId: agentID, - Token: token, - AgentVersion: "benchmark", - }, - }, - }) -} - -func dialAndAuthenticateBenchmarkAgent( - wsURL, origin string, - helloFrame []byte, -) (*websocket.Conn, error) { - dialer := websocket.Dialer{Subprotocols: []string{pbws.Subprotocol}} - conn, response, err := dialer.Dial(wsURL, http.Header{"Origin": []string{origin}}) - if err != nil { - if response != nil && response.Body != nil { - _ = response.Body.Close() - } - return nil, fmt.Errorf("dial websocket: %w", err) - } - deadline := time.Now().Add(15 * time.Second) - if err := conn.SetWriteDeadline(deadline); err != nil { - _ = conn.Close() - return nil, fmt.Errorf("set write deadline: %w", err) - } - if err := conn.WriteMessage(websocket.BinaryMessage, helloFrame); err != nil { - _ = conn.Close() - return nil, fmt.Errorf("write hello: %w", err) - } - if err := conn.SetReadDeadline(deadline); err != nil { - _ = conn.Close() - return nil, fmt.Errorf("set read deadline: %w", err) - } - messageType, payload, err := conn.ReadMessage() - if err != nil { - _ = conn.Close() - return nil, fmt.Errorf("read hello: %w", err) - } - if messageType != websocket.BinaryMessage { - _ = conn.Close() - return nil, fmt.Errorf("hello message type = %d, want binary", messageType) - } - var frame gatewayv2.AgentServerFrame - if err := proto.Unmarshal(payload, &frame); err != nil { - _ = conn.Close() - return nil, fmt.Errorf("unmarshal hello: %w", err) - } - if hello := frame.GetHello(); hello == nil || !hello.GetOk() { - _ = conn.Close() - return nil, fmt.Errorf("gateway rejected hello: %v", frame.GetHello()) - } - return conn, nil -} - -// Benchmark1000AgentWebSocketHandshakes 测量 1000 个已签发独立凭证的 Agent -// 同时完成 HTTP Upgrade、WebSocket+Protobuf hello、SQLite 鉴权和会话登记的整批耗时。 -// Worker 创建、凭证签发和测试服务器初始化不计入计时。 -func Benchmark1000AgentWebSocketHandshakes(b *testing.B) { - tempDir := b.TempDir() - b.ReportAllocs() - b.ResetTimer() - - for iteration := range b.N { - b.StopTimer() - database, err := db.Open(filepath.Join(tempDir, fmt.Sprintf("gateway-%d.db", iteration))) - if err != nil { - b.Fatalf("open benchmark database: %v", err) - } - store, err := agenttoken.NewStore(database) - if err != nil { - _ = database.Close() - b.Fatalf("open agent token store: %v", err) - } - - helloFrames := make([][]byte, concurrentAgentConnections) - for index := range helloFrames { - agentID := fmt.Sprintf("agent-00000000-0000-4000-8000-%012x", index) - token, issueErr := store.Issue(agentID, "") - if issueErr != nil { - _ = database.Close() - b.Fatalf("issue token %d: %v", index, issueErr) - } - helloFrames[index], err = benchmarkAgentHello(agentID, token) - if err != nil { - _ = database.Close() - b.Fatalf("marshal hello %d: %v", index, err) - } - } - - cfg := &config.Config{ - Token: "benchmark-gateway-token", - MaxAgentConnections: concurrentAgentConnections + 100, - RequestTimeout: 15 * time.Second, - HeartbeatPeriod: time.Hour, - WebSocketHeartbeatPeriod: time.Hour, - WebSocketWriteTimeout: 15 * time.Second, - } - manager := session.NewManager() - server := pbws.NewServer(cfg, manager, store) - mux := http.NewServeMux() - mux.Handle("/ws/v2/agent", server.AgentHandler()) - testServer := httptest.NewServer(mux) - wsURL := "ws" + strings.TrimPrefix(testServer.URL, "http") + "/ws/v2/agent" - - results := make([]agentHandshakeResult, len(helloFrames)) - start := make(chan struct{}) - var ready sync.WaitGroup - var done sync.WaitGroup - ready.Add(len(helloFrames)) - done.Add(len(helloFrames)) - for index := range helloFrames { - go func() { - defer done.Done() - ready.Done() - <-start - results[index].conn, results[index].err = dialAndAuthenticateBenchmarkAgent( - wsURL, testServer.URL, helloFrames[index], - ) - }() - } - ready.Wait() - b.StartTimer() - close(start) - done.Wait() - b.StopTimer() - - for index := range results { - if results[index].err != nil { - for cleanupIndex := range results { - if results[cleanupIndex].conn != nil { - _ = results[cleanupIndex].conn.Close() - } - } - testServer.Close() - _ = database.Close() - b.Fatalf("agent handshake %d: %v", index, results[index].err) - } - } - if online := manager.ConnectedAgentIDs(); len(online) != concurrentAgentConnections { - b.Fatalf("online agents = %d, want %d", len(online), concurrentAgentConnections) - } - for index := range results { - _ = results[index].conn.Close() - } - testServer.Close() - if err := database.Close(); err != nil { - b.Fatalf("close benchmark database: %v", err) - } - } - - elapsed := b.Elapsed() - b.ReportMetric( - float64(b.N*concurrentAgentConnections)/elapsed.Seconds(), - "connections/s", - ) - b.ReportMetric( - float64(elapsed.Nanoseconds())/float64(b.N*concurrentAgentConnections), - "ns/connection", - ) -} diff --git a/crates/agent-gateway/test/websocket/v2_chat_terminal_test.go b/crates/agent-gateway/test/websocket/v2_chat_terminal_test.go deleted file mode 100644 index 967bab806..000000000 --- a/crates/agent-gateway/test/websocket/v2_chat_terminal_test.go +++ /dev/null @@ -1,182 +0,0 @@ -package websocket_test - -// v2 chat 命令编排与终端链路的集成测试。 - -import ( - "net/http" - "testing" - "time" - - "github.com/gorilla/websocket" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" - "github.com/liveagent/agent-gateway/internal/protocol/pbws" - "github.com/liveagent/agent-gateway/internal/session" - "google.golang.org/protobuf/proto" -) - -// TestV2ChatCommandAcceptedFlow 覆盖 submit 编排:运行时探活(网关发 Ping、假 agent 回 Pong) -// → 接受回执 → 命令信封投递到 agent。 -func TestV2ChatCommandAcceptedFlow(t *testing.T) { - t.Parallel() - - sm, agentSession, conn, cleanup := newV2BrowserTest(t) - defer cleanup() - - sendProtoFrame(t, conn, &gatewayv2.WebClientFrame{ - RequestId: "cmd-1", - AgentId: "desktop-agent", - Payload: &gatewayv2.WebClientFrame_ChatCommand{ - ChatCommand: &gatewayv2.ChatCommandRequest{ - Type: "chat.submit", - Request: &gatewayv2.ChatRequest{ - ConversationId: "conv-cmd", - ClientRequestId: "client-cmd-1", - Message: "hello v2", - }, - }, - }, - }) - - // 网关先发运行时探活;假 agent 应答 Pong。 - answerChatRuntimeProbe(t, sm, agentSession) - - frame := receiveWebFrameWithID(t, conn, "cmd-1") - accepted := frame.GetChatAccepted() - if accepted == nil || accepted.GetConversationId() != "conv-cmd" || accepted.GetRunId() == "" { - t.Fatalf("chat command reply = %#v, want chat_accepted", frame) - } - - // 命令信封随后投递到 agent。 - outbound := readOutboundEnvelope(t, agentSession) - command := outbound.GetChatCommand() - if command.GetType() != "chat.submit" || command.GetRequest().GetMessage() != "hello v2" { - t.Fatalf("agent chat command = %#v, want chat.submit hello v2", command) - } -} - -func TestV2TerminalBrowserRequiresAgentID(t *testing.T) { - t.Parallel() - - handler := pbws.NewServer(newV2TestConfig(), session.NewManager(), nil).TerminalHandler() - conn, cleanup := dialV2(t, handler) - defer cleanup() - sendProtoFrame(t, conn, &gatewayv2.TerminalClientFrame{ - Payload: &gatewayv2.TerminalClientFrame_Hello{ - Hello: &gatewayv2.ClientHello{ - ProtocolVersion: pbws.ProtocolVersion, - Role: gatewayv2.ClientRole_CLIENT_ROLE_BROWSER, - Token: "ws-token", - }, - }, - }) - hello := receiveTerminalServerFrame(t, conn).GetHello() - if hello == nil || hello.GetOk() || hello.GetMessage() != "agent_id is required" { - t.Fatalf("terminal hello without agent_id = %v, want rejection", hello) - } -} - -// TestV2TerminalBrowserGating 覆盖终端链路浏览器角色:默认设置下 attach 被权限门控拒绝; -// 开启 Web 终端后 attach 转发失败(agent 离线)也以 error 帧回报。 -func TestV2TerminalBrowserGating(t *testing.T) { - t.Parallel() - - sm := session.NewManager() - handler := pbws.NewServer(newV2TestConfig(), sm, nil).TerminalHandler() - - dialTerminal := func() (*websocket.Conn, func()) { - conn, cleanup := dialV2(t, handler) - sendProtoFrame(t, conn, &gatewayv2.TerminalClientFrame{ - Payload: &gatewayv2.TerminalClientFrame_Hello{ - Hello: &gatewayv2.ClientHello{ - ProtocolVersion: pbws.ProtocolVersion, - Role: gatewayv2.ClientRole_CLIENT_ROLE_BROWSER, - Token: "ws-token", - AgentId: "desktop-agent", - }, - }, - }) - hello := receiveTerminalServerFrame(t, conn).GetHello() - if hello == nil || !hello.GetOk() { - t.Fatalf("terminal hello reply = %#v, want ok", hello) - } - return conn, cleanup - } - - attach := func(conn *websocket.Conn) { - sendProtoFrame(t, conn, &gatewayv2.TerminalClientFrame{ - Payload: &gatewayv2.TerminalClientFrame_Frame{ - Frame: &gatewayv2.TerminalStreamFrame{ - Kind: "attach", - SessionId: "sess-1", - StreamId: "stream-1", - }, - }, - }) - } - - // 默认设置:Web 终端关闭 → 权限错误。 - conn, cleanup := dialTerminal() - attach(conn) - frame := receiveTerminalServerFrame(t, conn).GetFrame() - if frame.GetKind() != "error" || frame.GetError() == "" { - t.Fatalf("gated attach reply = %#v, want error frame", frame) - } - cleanup() - - // 开启 Web 终端:attach 通过门控,但 agent 离线 → 离线错误。 - sm.ApplySettingsJSON("desktop-agent", `{"remote":{"enableWebTerminal":true}}`) - conn, cleanup = dialTerminal() - defer cleanup() - attach(conn) - frame = receiveTerminalServerFrame(t, conn).GetFrame() - if frame.GetKind() != "error" || frame.GetError() != "desktop agent is offline" { - t.Fatalf("offline attach reply = %#v, want agent offline error", frame) - } -} - -// TestV2AgentHelloRejectsBrowserRole 确认角色错配被拒绝。 -func TestV2AgentHelloRejectsBrowserRole(t *testing.T) { - t.Parallel() - - sm := session.NewManager() - srv := pbws.NewServer(newV2TestConfig(), sm, nil) - mux := http.NewServeMux() - mux.Handle("/ws/v2/agent", srv.AgentHandler()) - - conn, cleanup := dialV2Path(t, mux, "/ws/v2/agent") - defer cleanup() - - sendProtoFrame(t, conn, &gatewayv2.AgentClientFrame{ - Payload: &gatewayv2.AgentClientFrame_Hello{ - Hello: &gatewayv2.ClientHello{ - ProtocolVersion: pbws.ProtocolVersion, - Role: gatewayv2.ClientRole_CLIENT_ROLE_BROWSER, - Token: "ws-token", - }, - }, - }) - hello := receiveAgentServerFrame(t, conn).GetHello() - if hello == nil || hello.GetOk() { - t.Fatalf("agent hello with browser role = %#v, want ok=false", hello) - } -} - -func receiveTerminalServerFrame(t *testing.T, conn *websocket.Conn) *gatewayv2.TerminalServerFrame { - t.Helper() - if err := conn.SetReadDeadline(time.Now().Add(time.Second)); err != nil { - t.Fatalf("set terminal read deadline: %v", err) - } - messageType, data, err := conn.ReadMessage() - if err != nil { - t.Fatalf("receive terminal frame: %v", err) - } - if messageType != websocket.BinaryMessage { - t.Fatalf("terminal frame message type = %d, want binary", messageType) - } - var frame gatewayv2.TerminalServerFrame - if err := proto.Unmarshal(data, &frame); err != nil { - t.Fatalf("unmarshal terminal frame: %v", err) - } - return &frame -} diff --git a/crates/agent-gateway/test/websocket/v2_git_gating_test.go b/crates/agent-gateway/test/websocket/v2_git_gating_test.go deleted file mode 100644 index 87e548cc7..000000000 --- a/crates/agent-gateway/test/websocket/v2_git_gating_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package websocket_test - -// v2 直通 git 门控:写操作受桌面端 Remote 设置 enable_web_git 门控, -// 读操作始终放行。 - -import ( - "strings" - "testing" - - "github.com/gorilla/websocket" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" - "github.com/liveagent/agent-gateway/internal/protocol/pbws" - "github.com/liveagent/agent-gateway/internal/session" -) - -func newV2GitBrowserTest( - t *testing.T, - webGitEnabled bool, -) (*session.Manager, *session.AgentSession, *websocket.Conn, func()) { - t.Helper() - - sm := session.NewManager() - webGitSetting := "false" - if webGitEnabled { - webGitSetting = "true" - } - sm.ApplySettingsJSON("desktop-agent", `{"remote":{"enableWebGit":`+webGitSetting+`}}`) - sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1") - agentSession := session.NewAgentSession(sm.LatestAuthSnapshot("desktop-agent")) - sm.SetSession(agentSession) - - handler := pbws.NewServer(newV2TestConfig(), sm, nil).BrowserHandler() - conn, cleanup := dialV2(t, handler) - helloV2(t, conn, "ws-token") - return sm, agentSession, conn, cleanup -} - -func sendGitAgentRequest(t *testing.T, conn *websocket.Conn, id string, action string) { - t.Helper() - sendProtoFrame(t, conn, &gatewayv2.WebClientFrame{ - RequestId: id, - AgentId: "desktop-agent", - Payload: &gatewayv2.WebClientFrame_AgentRequest{ - AgentRequest: &gatewayv2.GatewayEnvelope{ - RequestId: id, - Payload: &gatewayv2.GatewayEnvelope_GitRequest{ - GitRequest: &gatewayv2.GitRequest{ - Action: action, - Workdir: "/workspace/project", - ArgsJson: "{}", - }, - }, - }, - }, - }) -} - -func TestV2GitRejectsWriteRequestsWhenDisabled(t *testing.T) { - t.Parallel() - - _, _, conn, cleanup := newV2GitBrowserTest(t, false) - defer cleanup() - - for _, action := range []string{"clone", "stage", "init", "stage_all", "unstage_all", "discard_all", "push", "commit"} { - id := "git-disabled-" + action - sendGitAgentRequest(t, conn, id, action) - - frame := receiveWebFrameWithID(t, conn, id) - localError := frame.GetLocalError() - if localError == nil { - t.Fatalf("git %s reply = %#v, want local_error", action, frame) - } - if !strings.Contains(localError.GetMessage(), "web git is disabled") { - t.Fatalf("git %s error = %q, want web git disabled message", action, localError.GetMessage()) - } - } -} - -func TestV2GitAllowsReadRequestsWhenDisabled(t *testing.T) { - t.Parallel() - - _, agentSession, conn, cleanup := newV2GitBrowserTest(t, false) - defer cleanup() - - for _, action := range []string{"status", "list_remote_branches"} { - sendGitAgentRequest(t, conn, "git-read-"+action, action) - - outbound := readOutboundEnvelope(t, agentSession) - if outbound.GetGitRequest().GetAction() != action { - t.Fatalf("outbound = %#v, want forwarded git %s request", outbound, action) - } - } -} - -func TestV2GitAllowsWriteRequestsWhenEnabled(t *testing.T) { - t.Parallel() - - _, agentSession, conn, cleanup := newV2GitBrowserTest(t, true) - defer cleanup() - - sendGitAgentRequest(t, conn, "git-stage-1", "stage") - - outbound := readOutboundEnvelope(t, agentSession) - if outbound.GetGitRequest().GetAction() != "stage" { - t.Fatalf("outbound = %#v, want forwarded git stage request", outbound) - } -} diff --git a/crates/agent-gateway/test/websocket/v2_hardening_test.go b/crates/agent-gateway/test/websocket/v2_hardening_test.go deleted file mode 100644 index c5aa78739..000000000 --- a/crates/agent-gateway/test/websocket/v2_hardening_test.go +++ /dev/null @@ -1,198 +0,0 @@ -package websocket_test - -// v2 加固集成测试:连接上限、派发信号量、按链路读限额、入站限速。 - -import ( - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" - - "github.com/gorilla/websocket" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" - "github.com/liveagent/agent-gateway/internal/protocol/pbws" - "github.com/liveagent/agent-gateway/internal/session" - "google.golang.org/protobuf/proto" -) - -// writeProtoFrameRaw 直接写出帧、错误返回而非 t.Fatal(限速测试里服务端断开是预期)。 -func writeProtoFrameRaw(conn *websocket.Conn, frame proto.Message) error { - data, err := proto.Marshal(frame) - if err != nil { - return err - } - return conn.WriteMessage(websocket.BinaryMessage, data) -} - -func TestV2BrowserConnectionCapRejectsExcess(t *testing.T) { - t.Parallel() - - // 上限已是配置项:用小值验证行为,避免测试随默认值调整而失效。 - cfg := newV2TestConfig() - cfg.MaxBrowserConnections = 4 - sm := session.NewManager() - handler := pbws.NewServer(cfg, sm, nil).BrowserHandler() - ts := httptest.NewServer(handler) - defer ts.Close() - wsURL := "ws" + strings.TrimPrefix(ts.URL, "http") - dialer := websocket.Dialer{Subprotocols: []string{pbws.Subprotocol}} - - conns := make([]*websocket.Conn, 0, cfg.MaxBrowserConnections) - defer func() { - for _, conn := range conns { - _ = conn.Close() - } - }() - for i := 0; i < cfg.MaxBrowserConnections; i++ { - conn, _, err := dialer.Dial(wsURL, http.Header{"Origin": []string{ts.URL}}) - if err != nil { - t.Fatalf("dial %d: %v", i, err) - } - conns = append(conns, conn) - } - - // 超限的下一个连接:升级前即 503。 - _, resp, err := dialer.Dial(wsURL, http.Header{"Origin": []string{ts.URL}}) - if err == nil { - t.Fatal("connection beyond the cap should be rejected") - } - if resp == nil || resp.StatusCode != http.StatusServiceUnavailable { - t.Fatalf("over-cap connection status = %v, want 503", resp) - } - - // 释放一个槽位后可再连(计数正确回收)。 - _ = conns[0].Close() - conns = conns[1:] - deadline := time.Now().Add(2 * time.Second) - for { - conn, _, err := dialer.Dial(wsURL, http.Header{"Origin": []string{ts.URL}}) - if err == nil { - conns = append(conns, conn) - break - } - if time.Now().After(deadline) { - t.Fatalf("slot was not released after close: %v", err) - } - time.Sleep(20 * time.Millisecond) - } -} - -func TestV2DispatchSemaphoreRejectsAndRecovers(t *testing.T) { - t.Parallel() - - // 两个 Agent 在线且不应答:agent_request 挂在 AwaitUnaryResponse 上直到 - // requestTimeout(1s),期间占满 16 个在途槽位。 - sm, agentA, _, conn, cleanup := newV2MultiAgentTest(t) - defer cleanup() - - for i := 0; i < 17; i++ { - sendProtoFrame(t, conn, &gatewayv2.WebClientFrame{ - RequestId: "slow-" + string(rune('a'+i)), - AgentId: "agent-a", - Payload: &gatewayv2.WebClientFrame_AgentRequest{ - AgentRequest: &gatewayv2.GatewayEnvelope{ - Payload: &gatewayv2.GatewayEnvelope_HistoryWorkdirs{ - HistoryWorkdirs: &gatewayv2.HistoryWorkdirsRequest{}, - }, - }, - }, - }) - } - - // 第 17 个在途请求必须很快得到信号量本地错误(其余 16 个等到超时才有响应; - // 期间会先收到快照回放等广播帧,跳过)。 - deadlineReject := time.Now().Add(time.Second) - for { - if time.Now().After(deadlineReject) { - t.Fatal("timed out waiting for semaphore local_error") - } - frame := receiveWebFrameRaw(t, conn) - if localError := frame.GetLocalError(); localError != nil { - if !strings.Contains(localError.GetMessage(), "too many concurrent requests") { - t.Fatalf("local_error = %q, want semaphore rejection", localError.GetMessage()) - } - break - } - } - - // 槽位随超时释放:之后的请求恢复正常处理。此时才启动应答泵(前 16 个请求 - // 必须无应答才能占满槽位),恢复后的请求应立即得到真实响应。 - time.Sleep(1200 * time.Millisecond) - go answerAgentRequests(sm, agentA, "/recovered") - sendProtoFrame(t, conn, &gatewayv2.WebClientFrame{ - RequestId: "after-recovery", - AgentId: "agent-a", - Payload: &gatewayv2.WebClientFrame_AgentRequest{ - AgentRequest: &gatewayv2.GatewayEnvelope{ - Payload: &gatewayv2.GatewayEnvelope_HistoryWorkdirs{ - HistoryWorkdirs: &gatewayv2.HistoryWorkdirsRequest{}, - }, - }, - }, - }) - deadline := time.Now().Add(3 * time.Second) - for time.Now().Before(deadline) { - frame := receiveWebFrameRaw(t, conn) - if frame.GetRequestId() != "after-recovery" { - continue - } - if message := frame.GetLocalError().GetMessage(); strings.Contains(message, "too many concurrent requests") { - t.Fatalf("semaphore did not recover: %q", message) - } - return - } - t.Fatal("timed out waiting for post-recovery response") -} - -func TestV2BrowserOversizedFrameClosesConnection(t *testing.T) { - t.Parallel() - - sm := session.NewManager() - handler := pbws.NewServer(newV2TestConfig(), sm, nil).BrowserHandler() - conn, cleanup := dialV2(t, handler) - defer cleanup() - helloV2(t, conn, "ws-token") - - // 超过浏览器链路 4 MiB 读限额的帧:服务端立即断开(写侧收到 reset 或读侧 - // 收到关闭都算命中)。 - oversized := make([]byte, 5<<20) - if err := conn.WriteMessage(websocket.BinaryMessage, oversized); err != nil { - return - } - _ = conn.SetReadDeadline(time.Now().Add(2 * time.Second)) - for { - if _, _, err := conn.ReadMessage(); err != nil { - return - } - } -} - -func TestV2InboundRateLimitClosesRunawayConnection(t *testing.T) { - t.Parallel() - - sm := session.NewManager() - handler := pbws.NewServer(newV2TestConfig(), sm, nil).BrowserHandler() - conn, cleanup := dialV2(t, handler) - defer cleanup() - helloV2(t, conn, "ws-token") - - // 突发远超 burst(200):先收 local_error,连续违规后连接被关闭。 - for i := 0; i < 400; i++ { - frame := &gatewayv2.WebClientFrame{RequestId: "flood"} - if err := conn.SetWriteDeadline(time.Now().Add(time.Second)); err != nil { - t.Fatalf("set write deadline: %v", err) - } - if err := writeProtoFrameRaw(conn, frame); err != nil { - // 服务端已断开——达到预期。 - return - } - } - _ = conn.SetReadDeadline(time.Now().Add(3 * time.Second)) - for { - if _, _, err := conn.ReadMessage(); err != nil { - return - } - } -} diff --git a/crates/agent-gateway/test/websocket/v2_helpers_test.go b/crates/agent-gateway/test/websocket/v2_helpers_test.go deleted file mode 100644 index 9df7ee33c..000000000 --- a/crates/agent-gateway/test/websocket/v2_helpers_test.go +++ /dev/null @@ -1,206 +0,0 @@ -package websocket_test - -// v2(WebSocket+Protobuf)二进制帧测试 harness:起真实 httptest 服务器、以子协议拨号、 -// 按 proto 帧收发。 - -import ( - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" - - "github.com/gorilla/websocket" - "google.golang.org/protobuf/proto" - - "github.com/liveagent/agent-gateway/internal/config" - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" - "github.com/liveagent/agent-gateway/internal/protocol/pbws" - "github.com/liveagent/agent-gateway/internal/session" -) - -func newV2TestConfig() *config.Config { - return &config.Config{ - Token: "ws-token", - RequestTimeout: time.Second, - } -} - -// dialV2 起服务并以 v2 子协议拨号。 -func dialV2(t *testing.T, handler http.Handler) (*websocket.Conn, func()) { - t.Helper() - ts := httptest.NewServer(handler) - wsURL := "ws" + strings.TrimPrefix(ts.URL, "http") - dialer := websocket.Dialer{Subprotocols: []string{pbws.Subprotocol}} - conn, resp, err := dialer.Dial(wsURL, http.Header{ - "Origin": []string{ts.URL}, - }) - if err != nil { - ts.Close() - t.Fatalf("dial v2 websocket: %v", err) - } - if got := resp.Header.Get("Sec-Websocket-Protocol"); got != pbws.Subprotocol { - _ = conn.Close() - ts.Close() - t.Fatalf("subprotocol = %q, want %q", got, pbws.Subprotocol) - } - return conn, func() { - _ = conn.Close() - ts.Close() - } -} - -func sendProtoFrame(t *testing.T, conn *websocket.Conn, frame proto.Message) { - t.Helper() - data, err := proto.Marshal(frame) - if err != nil { - t.Fatalf("marshal v2 frame: %v", err) - } - if err := conn.SetWriteDeadline(time.Now().Add(time.Second)); err != nil { - t.Fatalf("set v2 write deadline: %v", err) - } - if err := conn.WriteMessage(websocket.BinaryMessage, data); err != nil { - t.Fatalf("send v2 frame: %v", err) - } -} - -// receiveWebFrame 读取一条 WebServerFrame,跳过与断言无关的周期/广播帧(测试 helper -// 过滤集);带关联 id 的 status 是 status_get / chat_prepare 的响应,不过滤。 -func receiveWebFrame(t *testing.T, conn *websocket.Conn) *gatewayv2.WebServerFrame { - t.Helper() - for { - frame := receiveWebFrameRaw(t, conn) - switch frame.GetPayload().(type) { - case *gatewayv2.WebServerFrame_Ping, - *gatewayv2.WebServerFrame_TunnelState, - *gatewayv2.WebServerFrame_ProcessState: - continue - case *gatewayv2.WebServerFrame_Status: - if frame.GetRequestId() == "" { - continue - } - return frame - default: - return frame - } - } -} - -func receiveWebFrameRaw(t *testing.T, conn *websocket.Conn) *gatewayv2.WebServerFrame { - t.Helper() - if err := conn.SetReadDeadline(time.Now().Add(time.Second)); err != nil { - t.Fatalf("set v2 read deadline: %v", err) - } - messageType, data, err := conn.ReadMessage() - if err != nil { - t.Fatalf("receive v2 frame: %v", err) - } - if messageType != websocket.BinaryMessage { - t.Fatalf("v2 frame message type = %d, want binary", messageType) - } - var frame gatewayv2.WebServerFrame - if err := proto.Unmarshal(data, &frame); err != nil { - t.Fatalf("unmarshal v2 frame: %v", err) - } - return &frame -} - -// receiveWebFrameWithID 等待携带指定关联 id 的帧。 -func receiveWebFrameWithID(t *testing.T, conn *websocket.Conn, id string) *gatewayv2.WebServerFrame { - t.Helper() - for attempt := 0; attempt < 8; attempt++ { - frame := receiveWebFrame(t, conn) - if frame.GetRequestId() == id { - return frame - } - } - t.Fatalf("timed out waiting for v2 frame id %q", id) - return nil -} - -// helloV2 完成浏览器链路握手并断言成功。 -func helloV2(t *testing.T, conn *websocket.Conn, token string) { - t.Helper() - sendProtoFrame(t, conn, &gatewayv2.WebClientFrame{ - RequestId: "hello-1", - Payload: &gatewayv2.WebClientFrame_Hello{ - Hello: &gatewayv2.ClientHello{ - ProtocolVersion: pbws.ProtocolVersion, - Role: gatewayv2.ClientRole_CLIENT_ROLE_BROWSER, - Token: token, - ClientName: "webui-test", - }, - }, - }) - frame := receiveWebFrameRaw(t, conn) - hello := frame.GetHello() - if hello == nil || !hello.GetOk() { - t.Fatalf("v2 hello reply = %#v, want ok hello", frame) - } -} - -// newV2BrowserTest 建好 manager + 假 agent 会话 + 已握手的浏览器连接。 -func newV2BrowserTest(t *testing.T) (*session.Manager, *session.AgentSession, *websocket.Conn, func()) { - t.Helper() - - sm := session.NewManager() - sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1") - agentSession := session.NewAgentSession(sm.LatestAuthSnapshot("desktop-agent")) - agentSession.SetCapabilities([]string{gatewayv2.ChatIngressV1Capability}) - sm.SetSession(agentSession) - - handler := pbws.NewServer(newV2TestConfig(), sm, nil).BrowserHandler() - conn, cleanup := dialV2(t, handler) - helloV2(t, conn, "ws-token") - return sm, agentSession, conn, cleanup -} - -// readOutboundEnvelope 取出网关发往桌面端的下一条信封并 Ack。 -func readOutboundEnvelope(t *testing.T, agentSession *session.AgentSession) *gatewayv2.GatewayEnvelope { - t.Helper() - select { - case outbound := <-agentSession.Outbound(): - outbound.Ack(nil) - return outbound.GatewayEnvelope - case <-time.After(time.Second): - t.Fatalf("timed out waiting for gateway request to reach agent") - return nil - } -} - -// answerChatRuntimeProbe 以假桌面端身份应答 chat.prepare 的关联 Ping 探测。 -func answerChatRuntimeProbe( - t *testing.T, - sm *session.Manager, - agentSession *session.AgentSession, -) string { - t.Helper() - envelope := readOutboundEnvelope(t, agentSession) - requestID := envelope.GetRequestId() - if !strings.HasPrefix(requestID, "chat-runtime-wake-") || envelope.GetPing() == nil { - t.Fatalf("chat runtime probe = %#v, want chat-runtime-wake-* Ping", envelope) - } - sm.DispatchFromAgent("desktop-agent", &gatewayv2.AgentEnvelope{ - RequestId: requestID, - Timestamp: time.Now().Unix(), - Payload: &gatewayv2.AgentEnvelope_Pong{ - Pong: &gatewayv2.PongResponse{Timestamp: envelope.GetPing().GetTimestamp()}, - }, - }) - return requestID -} - -// dispatchStarted 以假桌面端身份上报 run 的 started 控制事件。 -func dispatchStarted(sm *session.Manager, runID string, conversationID string) { - sm.DispatchFromAgent("desktop-agent", &gatewayv2.AgentEnvelope{ - RequestId: runID, - Payload: &gatewayv2.AgentEnvelope_ChatControl{ - ChatControl: &gatewayv2.ChatControlEvent{ - RequestId: runID, - ConversationId: conversationID, - Type: "started", - State: "running", - }, - }, - }) -} diff --git a/crates/agent-gateway/test/websocket/v2_multiagent_test.go b/crates/agent-gateway/test/websocket/v2_multiagent_test.go deleted file mode 100644 index f1b8972ca..000000000 --- a/crates/agent-gateway/test/websocket/v2_multiagent_test.go +++ /dev/null @@ -1,279 +0,0 @@ -package websocket_test - -// v2 多 Agent 寻址集成测试:定向直通、歧义错误、agent_list 目录、广播打标隔离。 - -import ( - "path/filepath" - "testing" - "time" - - "github.com/gorilla/websocket" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" - "github.com/liveagent/agent-gateway/internal/protocol/pbws" - "github.com/liveagent/agent-gateway/internal/session" -) - -// newV2MultiAgentTest 建两个假 Agent 会话 + 已握手的浏览器连接。 -func newV2MultiAgentTest(t *testing.T) (*session.Manager, *session.AgentSession, *session.AgentSession, *websocket.Conn, func()) { - t.Helper() - - sm := session.NewManager() - sm.RecordAuthentication("agent-a", "1.0.0", "session-a") - agentA := session.NewAgentSession(sm.LatestAuthSnapshot("agent-a")) - sm.SetSession(agentA) - sm.RecordAuthentication("agent-b", "1.0.0", "session-b") - agentB := session.NewAgentSession(sm.LatestAuthSnapshot("agent-b")) - sm.SetSession(agentB) - - handler := pbws.NewServer(newV2TestConfig(), sm, newAgentTokenStore(t)).BrowserHandler() - conn, cleanup := dialV2(t, handler) - helloV2(t, conn, "ws-token") - return sm, agentA, agentB, conn, cleanup -} - -// answerAgentRequests 消费假 Agent 出站队列并按固定应答回填(history_workdirs 臂)。 -func answerAgentRequests(sm *session.Manager, sess *session.AgentSession, marker string) { - for outbound := range sess.Outbound() { - outbound.Ack(nil) - if outbound.GetHistoryWorkdirs() == nil { - continue - } - sm.DispatchFromAgentForSession(sess, &gatewayv2.AgentEnvelope{ - RequestId: outbound.GetRequestId(), - Payload: &gatewayv2.AgentEnvelope_HistoryWorkdirsResp{ - HistoryWorkdirsResp: &gatewayv2.HistoryWorkdirsResponse{ - Workdirs: []*gatewayv2.HistoryWorkdirSummary{{Path: marker}}, - }, - }, - }) - } -} - -func TestV2AgentRequestRoutesToTargetAgent(t *testing.T) { - t.Parallel() - - sm, agentA, agentB, conn, cleanup := newV2MultiAgentTest(t) - defer cleanup() - go answerAgentRequests(sm, agentA, "/from-agent-a") - go answerAgentRequests(sm, agentB, "/from-agent-b") - - // 指定 agent-b:响应必须来自 B 且帧回填 agent_id=b。 - sendProtoFrame(t, conn, &gatewayv2.WebClientFrame{ - RequestId: "route-b", - AgentId: "agent-b", - Payload: &gatewayv2.WebClientFrame_AgentRequest{ - AgentRequest: &gatewayv2.GatewayEnvelope{ - Payload: &gatewayv2.GatewayEnvelope_HistoryWorkdirs{ - HistoryWorkdirs: &gatewayv2.HistoryWorkdirsRequest{}, - }, - }, - }, - }) - frame := receiveWebFrameWithID(t, conn, "route-b") - resp := frame.GetAgentResponse() - if resp == nil || len(resp.GetHistoryWorkdirsResp().GetWorkdirs()) != 1 || - resp.GetHistoryWorkdirsResp().GetWorkdirs()[0].GetPath() != "/from-agent-b" { - t.Fatalf("agent-b response = %#v", frame) - } - if frame.GetAgentId() != "agent-b" { - t.Fatalf("response agent_id = %q, want agent-b", frame.GetAgentId()) - } - - // 指定 agent-a:同一连接可交替定向。 - sendProtoFrame(t, conn, &gatewayv2.WebClientFrame{ - RequestId: "route-a", - AgentId: "agent-a", - Payload: &gatewayv2.WebClientFrame_AgentRequest{ - AgentRequest: &gatewayv2.GatewayEnvelope{ - Payload: &gatewayv2.GatewayEnvelope_HistoryWorkdirs{ - HistoryWorkdirs: &gatewayv2.HistoryWorkdirsRequest{}, - }, - }, - }, - }) - frame = receiveWebFrameWithID(t, conn, "route-a") - if workdirs := frame.GetAgentResponse().GetHistoryWorkdirsResp().GetWorkdirs(); len(workdirs) != 1 || workdirs[0].GetPath() != "/from-agent-a" { - t.Fatalf("agent-a response = %#v", frame) - } -} - -func TestV2AgentRequestRequiresExplicitAgentID(t *testing.T) { - t.Parallel() - - _, _, _, conn, cleanup := newV2MultiAgentTest(t) - defer cleanup() - - sendProtoFrame(t, conn, &gatewayv2.WebClientFrame{ - RequestId: "ambiguous", - Payload: &gatewayv2.WebClientFrame_AgentRequest{ - AgentRequest: &gatewayv2.GatewayEnvelope{ - Payload: &gatewayv2.GatewayEnvelope_HistoryWorkdirs{ - HistoryWorkdirs: &gatewayv2.HistoryWorkdirsRequest{}, - }, - }, - }, - }) - frame := receiveWebFrameWithID(t, conn, "ambiguous") - localError := frame.GetLocalError() - if localError == nil || localError.GetMessage() != "agent_id is required" { - t.Fatalf("agent request without agent_id = %#v, want required-id local_error", frame) - } -} - -func TestV2AgentListReturnsDirectory(t *testing.T) { - t.Parallel() - - sm, _, agentB, conn, cleanup := newV2MultiAgentTest(t) - defer cleanup() - - // B 断线:目录仍应包含离线条目。 - sm.ClearSession(agentB) - - sendProtoFrame(t, conn, &gatewayv2.WebClientFrame{ - RequestId: "list", - Payload: &gatewayv2.WebClientFrame_AgentList{AgentList: &gatewayv2.AgentListRequest{}}, - }) - frame := receiveWebFrameWithID(t, conn, "list") - list := frame.GetAgentList() - if list == nil || len(list.GetAgents()) != 2 { - t.Fatalf("agent_list = %#v, want 2 entries", frame) - } - byID := map[string]*gatewayv2.StatusEvent{} - for _, entry := range list.GetAgents() { - byID[entry.GetAgentId()] = entry - } - if a := byID["agent-a"]; a == nil || !a.GetOnline() { - t.Fatalf("agent-a entry = %#v, want online", byID["agent-a"]) - } - if b := byID["agent-b"]; b == nil || b.GetOnline() { - t.Fatalf("agent-b entry = %#v, want offline", byID["agent-b"]) - } -} - -func TestV2AgentListIncludesRegistryNotesAndGlobalOrder(t *testing.T) { - t.Parallel() - - store := newAgentTokenStore(t) - if _, err := store.Issue("agent-a", "Office desktop"); err != nil { - t.Fatalf("issue agent-a token: %v", err) - } - if _, err := store.Issue("agent-c", "Spare laptop"); err != nil { - t.Fatalf("issue agent-c token: %v", err) - } - - sm := session.NewManager() - sm.RecordAuthentication("agent-a", "1.0.0", "session-a") - sm.SetSession(session.NewAgentSession(sm.LatestAuthSnapshot("agent-a"))) - sm.RecordAuthentication("agent-b", "1.0.0", "session-b") - sm.SetSession(session.NewAgentSession(sm.LatestAuthSnapshot("agent-b"))) - - handler := pbws.NewServer(newV2TestConfig(), sm, store).BrowserHandler() - conn, cleanup := dialV2(t, handler) - defer cleanup() - helloV2(t, conn, "ws-token") - - sendProtoFrame(t, conn, &gatewayv2.WebClientFrame{ - RequestId: "list-with-notes", - Payload: &gatewayv2.WebClientFrame_AgentList{ - AgentList: &gatewayv2.AgentListRequest{}, - }, - }) - list := receiveWebFrameWithID(t, conn, "list-with-notes").GetAgentList() - if list == nil || len(list.GetAgents()) != 3 { - t.Fatalf("agent_list = %#v, want 3 entries", list) - } - if got := []string{ - list.GetAgents()[0].GetAgentId(), - list.GetAgents()[1].GetAgentId(), - list.GetAgents()[2].GetAgentId(), - }; got[0] != "agent-a" || got[1] != "agent-b" || got[2] != "agent-c" { - t.Fatalf("agent order = %v, want [agent-a agent-b agent-c]", got) - } - if got := list.GetAgents()[0].GetName(); got != "Office desktop" { - t.Fatalf("agent-a name = %q, want Office desktop", got) - } - if got := list.GetAgents()[1].GetName(); got != "" { - t.Fatalf("agent-b name = %q, want empty", got) - } - if got := list.GetAgents()[2].GetName(); got != "Spare laptop" { - t.Fatalf("agent-c name = %q, want Spare laptop", got) - } -} - -func TestV2AgentListReturnsDatabaseError(t *testing.T) { - t.Parallel() - - database, store := openAgentTokenDB(t, filepath.Join(t.TempDir(), "agent-list.db")) - handler := pbws.NewServer(newV2TestConfig(), session.NewManager(), store).BrowserHandler() - conn, cleanup := dialV2(t, handler) - defer cleanup() - helloV2(t, conn, "ws-token") - if err := database.Close(); err != nil { - t.Fatalf("close agent database: %v", err) - } - - sendProtoFrame(t, conn, &gatewayv2.WebClientFrame{ - RequestId: "list-db-error", - Payload: &gatewayv2.WebClientFrame_AgentList{ - AgentList: &gatewayv2.AgentListRequest{}, - }, - }) - frame := receiveWebFrameWithID(t, conn, "list-db-error") - if frame.GetAgentList() != nil { - t.Fatalf("database failure was hidden as agent_list: %#v", frame) - } - if got := frame.GetLocalError().GetMessage(); got != "agent directory unavailable" { - t.Fatalf("agent directory error = %q, want agent directory unavailable", got) - } -} - -func TestV2BroadcastFramesCarrySourceAgentID(t *testing.T) { - t.Parallel() - - sm, agentA, agentB, conn, cleanup := newV2MultiAgentTest(t) - defer cleanup() - - sm.DispatchFromAgentForSession(agentA, &gatewayv2.AgentEnvelope{ - Payload: &gatewayv2.AgentEnvelope_HistorySync{ - HistorySync: &gatewayv2.HistorySyncEvent{Kind: "upsert", ConversationId: "conv-a"}, - }, - }) - sm.DispatchFromAgentForSession(agentB, &gatewayv2.AgentEnvelope{ - Payload: &gatewayv2.AgentEnvelope_HistorySync{ - HistorySync: &gatewayv2.HistorySyncEvent{Kind: "upsert", ConversationId: "conv-b"}, - }, - }) - - // 两条广播帧各自携带来源 agent_id(顺序不定,按 conversation 对账)。 - seen := map[string]string{} - deadline := time.Now().Add(2 * time.Second) - for len(seen) < 2 && time.Now().Before(deadline) { - frame := receiveWebFrameRaw(t, conn) - if history := frame.GetHistoryEvent(); history != nil { - seen[history.GetConversationId()] = frame.GetAgentId() - } - } - if seen["conv-a"] != "agent-a" || seen["conv-b"] != "agent-b" { - t.Fatalf("broadcast tags = %#v, want conv-a→agent-a conv-b→agent-b", seen) - } -} - -func TestV2WorkspaceSubscribeRequiresExplicitAgentID(t *testing.T) { - t.Parallel() - - _, _, _, conn, cleanup := newV2MultiAgentTest(t) - defer cleanup() - - sendProtoFrame(t, conn, &gatewayv2.WebClientFrame{ - RequestId: "workspace-ambiguous", - Payload: &gatewayv2.WebClientFrame_WorkspaceSubscribe{ - WorkspaceSubscribe: &gatewayv2.WorkspaceSubscribeRequest{Workdir: "/repo"}, - }, - }) - frame := receiveWebFrameWithID(t, conn, "workspace-ambiguous") - localError := frame.GetLocalError() - if localError == nil || localError.GetMessage() != "agent_id is required" { - t.Fatalf("workspace subscribe without agent_id = %#v, want required-id local_error", frame) - } -} diff --git a/crates/agent-gateway/test/websocket/v2_test.go b/crates/agent-gateway/test/websocket/v2_test.go deleted file mode 100644 index 60cce11b4..000000000 --- a/crates/agent-gateway/test/websocket/v2_test.go +++ /dev/null @@ -1,356 +0,0 @@ -package websocket_test - -// v2 浏览器链路集成测试:真实 httptest 服务器 + 二进制 proto 帧,覆盖握手鉴权、本地操作、 -// 直通转发(白名单/限额/关联 id 命名空间化)、chat 订阅与事件推送。 - -import ( - "encoding/json" - "net/http" - "strings" - "testing" - "time" - - "github.com/gorilla/websocket" - "google.golang.org/protobuf/proto" - - gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" - "github.com/liveagent/agent-gateway/internal/protocol/pbws" - "github.com/liveagent/agent-gateway/internal/session" -) - -func TestV2HelloRejectsBadToken(t *testing.T) { - t.Parallel() - - sm := session.NewManager() - handler := pbws.NewServer(newV2TestConfig(), sm, nil).BrowserHandler() - conn, cleanup := dialV2(t, handler) - defer cleanup() - - sendProtoFrame(t, conn, &gatewayv2.WebClientFrame{ - RequestId: "hello-bad", - Payload: &gatewayv2.WebClientFrame_Hello{ - Hello: &gatewayv2.ClientHello{ - ProtocolVersion: pbws.ProtocolVersion, - Token: "wrong-token", - }, - }, - }) - frame := receiveWebFrameRaw(t, conn) - hello := frame.GetHello() - if hello == nil || hello.GetOk() { - t.Fatalf("hello reply = %#v, want ok=false", frame) - } - // 其后连接应被服务端关闭。 - _ = conn.SetReadDeadline(time.Now().Add(time.Second)) - if _, _, err := conn.ReadMessage(); err == nil { - t.Fatal("connection stayed open after rejected hello") - } -} - -func TestV2HelloRejectsWrongVersion(t *testing.T) { - t.Parallel() - - sm := session.NewManager() - handler := pbws.NewServer(newV2TestConfig(), sm, nil).BrowserHandler() - conn, cleanup := dialV2(t, handler) - defer cleanup() - - sendProtoFrame(t, conn, &gatewayv2.WebClientFrame{ - Payload: &gatewayv2.WebClientFrame_Hello{ - Hello: &gatewayv2.ClientHello{ProtocolVersion: 99, Token: "ws-token"}, - }, - }) - frame := receiveWebFrameRaw(t, conn) - if hello := frame.GetHello(); hello == nil || hello.GetOk() { - t.Fatalf("hello reply = %#v, want ok=false for wrong version", frame) - } -} - -func TestV2StatusGet(t *testing.T) { - t.Parallel() - - _, _, conn, cleanup := newV2BrowserTest(t) - defer cleanup() - - sendProtoFrame(t, conn, &gatewayv2.WebClientFrame{ - RequestId: "status-1", - AgentId: "desktop-agent", - Payload: &gatewayv2.WebClientFrame_StatusGet{StatusGet: &gatewayv2.StatusGetRequest{}}, - }) - frame := receiveWebFrameWithID(t, conn, "status-1") - status := frame.GetStatus() - if status == nil { - t.Fatalf("status.get reply = %#v, want status payload", frame) - } - if !status.GetOnline() || status.GetAgentId() != "desktop-agent" { - t.Fatalf("status = %#v, want online desktop-agent", status) - } -} - -func TestV2AgentRequestPassthroughRoundtrip(t *testing.T) { - t.Parallel() - - sm, agentSession, conn, cleanup := newV2BrowserTest(t) - defer cleanup() - - // page_size 越界应被网关钳制到协议上限(200)。 - sendProtoFrame(t, conn, &gatewayv2.WebClientFrame{ - RequestId: "hist-1", - AgentId: "desktop-agent", - Payload: &gatewayv2.WebClientFrame_AgentRequest{ - AgentRequest: &gatewayv2.GatewayEnvelope{ - RequestId: "hist-1", - Payload: &gatewayv2.GatewayEnvelope_HistoryList{ - HistoryList: &gatewayv2.HistoryListRequest{PageSize: 999}, - }, - }, - }, - }) - - outbound := readOutboundEnvelope(t, agentSession) - if !strings.HasSuffix(outbound.GetRequestId(), ":hist-1") || - outbound.GetRequestId() == "hist-1" { - t.Fatalf("agent request id = %q, want per-connection namespaced hist-1", outbound.GetRequestId()) - } - if got := outbound.GetHistoryList().GetPageSize(); got != 200 { - t.Fatalf("page_size = %d, want clamped to 200", got) - } - if got := outbound.GetHistoryList().GetPage(); got != 1 { - t.Fatalf("page = %d, want defaulted to 1", got) - } - - sm.DispatchFromAgent("desktop-agent", &gatewayv2.AgentEnvelope{ - RequestId: outbound.GetRequestId(), - Payload: &gatewayv2.AgentEnvelope_HistoryListResp{ - HistoryListResp: &gatewayv2.HistoryListResponse{TotalCount: 3}, - }, - }) - - frame := receiveWebFrameWithID(t, conn, "hist-1") - response := frame.GetAgentResponse() - if response == nil { - t.Fatalf("passthrough reply = %#v, want agent_response", frame) - } - // 回程信封的关联 id 已剥离命名空间前缀。 - if response.GetRequestId() != "hist-1" { - t.Fatalf("agent_response request_id = %q, want hist-1", response.GetRequestId()) - } - if response.GetHistoryListResp().GetTotalCount() != 3 { - t.Fatalf("history list resp = %#v, want total_count 3", response) - } -} - -func TestV2GuardRejectsNonWhitelistedArms(t *testing.T) { - t.Parallel() - - _, _, conn, cleanup := newV2BrowserTest(t) - defer cleanup() - - // chat_command 必须走网关编排帧,不允许直通。 - sendProtoFrame(t, conn, &gatewayv2.WebClientFrame{ - RequestId: "bad-1", - AgentId: "desktop-agent", - Payload: &gatewayv2.WebClientFrame_AgentRequest{ - AgentRequest: &gatewayv2.GatewayEnvelope{ - Payload: &gatewayv2.GatewayEnvelope_ChatCommand{ - ChatCommand: &gatewayv2.ChatCommandRequest{Type: "chat.submit"}, - }, - }, - }, - }) - frame := receiveWebFrameWithID(t, conn, "bad-1") - if frame.GetLocalError() == nil { - t.Fatalf("chat_command passthrough reply = %#v, want local_error", frame) - } - - // 内部推送臂同理。 - sendProtoFrame(t, conn, &gatewayv2.WebClientFrame{ - RequestId: "bad-2", - AgentId: "desktop-agent", - Payload: &gatewayv2.WebClientFrame_AgentRequest{ - AgentRequest: &gatewayv2.GatewayEnvelope{ - Payload: &gatewayv2.GatewayEnvelope_Ping{ - Ping: &gatewayv2.PingRequest{}, - }, - }, - }, - }) - frame = receiveWebFrameWithID(t, conn, "bad-2") - if frame.GetLocalError() == nil { - t.Fatalf("ping passthrough reply = %#v, want local_error", frame) - } -} - -func TestV2ChatSubscribeAndStreamEvents(t *testing.T) { - t.Parallel() - - sm, _, conn, cleanup := newV2BrowserTest(t) - defer cleanup() - - sendProtoFrame(t, conn, &gatewayv2.WebClientFrame{ - RequestId: "sub-1", - AgentId: "desktop-agent", - Payload: &gatewayv2.WebClientFrame_ChatSubscribe{ - ChatSubscribe: &gatewayv2.ChatSubscribeRequest{ConversationId: "conv-1"}, - }, - }) - frame := receiveWebFrameWithID(t, conn, "sub-1") - subscribed := frame.GetChatSubscribed() - if subscribed == nil || subscribed.GetConversationId() != "conv-1" { - t.Fatalf("chat_subscribe reply = %#v, want chat_subscribed conv-1", frame) - } - - dispatchStarted(sm, "run-1", "conv-1") - tokenData, _ := json.Marshal(map[string]any{"type": "token", "text": "hello"}) - sm.DispatchFromAgent("desktop-agent", &gatewayv2.AgentEnvelope{ - RequestId: "ingress-1", - Payload: &gatewayv2.AgentEnvelope_ChatIngressBatch{ - ChatIngressBatch: &gatewayv2.ChatIngressBatch{ - RunId: "run-1", - ConversationId: "conv-1", - FirstSeq: 1, - Records: []*gatewayv2.ChatIngressRecord{{ - Payload: &gatewayv2.ChatIngressRecord_Delta{ - Delta: &gatewayv2.ChatIngressDelta{EventJson: string(tokenData)}, - }, - }}, - }, - }, - }) - - // 依次应收到 started 与 token 两条流事件。 - sawToken := false - for attempt := 0; attempt < 8 && !sawToken; attempt++ { - frame := receiveWebFrame(t, conn) - event := frame.GetChatEvent() - if event == nil { - continue - } - if event.GetConversationId() != "conv-1" { - t.Fatalf("chat_event conversation = %q, want conv-1", event.GetConversationId()) - } - var payload map[string]any - if err := json.Unmarshal(event.GetPayloadJson(), &payload); err != nil { - t.Fatalf("chat_event payload_json invalid: %v", err) - } - if payload["type"] == "token" { - sawToken = true - } - } - if !sawToken { - t.Fatal("timed out waiting for token chat_event") - } -} - -// TestV2EndToEndBinaryPath 打通首条全二进制路径:假 agent 经 /ws/v2/agent 接入, -// 浏览器经 /ws/v2 直通请求,全程使用 Protobuf 二进制帧。 -func TestV2EndToEndBinaryPath(t *testing.T) { - t.Parallel() - - sm := session.NewManager() - store := newAgentTokenStore(t) - agentToken, err := store.Issue("desktop-agent", "") - if err != nil { - t.Fatalf("issue desktop agent token: %v", err) - } - srv := pbws.NewServer(newV2TestConfig(), sm, store) - - mux := http.NewServeMux() - mux.Handle("/ws/v2", srv.BrowserHandler()) - mux.Handle("/ws/v2/agent", srv.AgentHandler()) - - // ---- 假 agent 上线 ---- - agentConn, agentCleanup := dialV2Path(t, mux, "/ws/v2/agent") - defer agentCleanup() - sendProtoFrame(t, agentConn, &gatewayv2.AgentClientFrame{ - Payload: &gatewayv2.AgentClientFrame_Hello{ - Hello: &gatewayv2.ClientHello{ - ProtocolVersion: pbws.ProtocolVersion, - Role: gatewayv2.ClientRole_CLIENT_ROLE_AGENT, - Token: agentToken, - AgentId: "desktop-agent", - AgentVersion: "1.0.0", - }, - }, - }) - agentHello := receiveAgentServerFrame(t, agentConn).GetHello() - if agentHello == nil || !agentHello.GetOk() || agentHello.GetSessionId() == "" { - t.Fatalf("agent hello reply = %#v, want ok with session id", agentHello) - } - - // ---- 浏览器接入并发起直通请求 ---- - browserConn, browserCleanup := dialV2Path(t, mux, "/ws/v2") - defer browserCleanup() - helloV2(t, browserConn, "ws-token") - - sendProtoFrame(t, browserConn, &gatewayv2.WebClientFrame{ - RequestId: "e2e-1", - AgentId: "desktop-agent", - Payload: &gatewayv2.WebClientFrame_AgentRequest{ - AgentRequest: &gatewayv2.GatewayEnvelope{ - Payload: &gatewayv2.GatewayEnvelope_SettingsGet{ - SettingsGet: &gatewayv2.SettingsGetRequest{}, - }, - }, - }, - }) - - // agent 侧应收到直通信封(跳过心跳 Ping)。 - var inbound *gatewayv2.GatewayEnvelope - for attempt := 0; attempt < 8; attempt++ { - envelope := receiveAgentServerFrame(t, agentConn).GetEnvelope() - if envelope == nil || envelope.GetPing() != nil { - continue - } - inbound = envelope - break - } - if inbound == nil || inbound.GetSettingsGet() == nil { - t.Fatalf("agent inbound = %#v, want settings_get", inbound) - } - - sendProtoFrame(t, agentConn, &gatewayv2.AgentClientFrame{ - Payload: &gatewayv2.AgentClientFrame_Envelope{ - Envelope: &gatewayv2.AgentEnvelope{ - RequestId: inbound.GetRequestId(), - Payload: &gatewayv2.AgentEnvelope_SettingsGetResp{ - SettingsGetResp: &gatewayv2.SettingsGetResponse{SettingsJson: `{"ok":true}`}, - }, - }, - }, - }) - - frame := receiveWebFrameWithID(t, browserConn, "e2e-1") - response := frame.GetAgentResponse() - if response == nil || response.GetSettingsGetResp().GetSettingsJson() != `{"ok":true}` { - t.Fatalf("e2e reply = %#v, want settings_get_resp", frame) - } -} - -// dialV2Path 对多路由 mux 的指定路径拨号。 -func dialV2Path(t *testing.T, handler http.Handler, path string) (*websocket.Conn, func()) { - t.Helper() - return dialV2(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - r.URL.Path = path - handler.ServeHTTP(w, r) - })) -} - -func receiveAgentServerFrame(t *testing.T, conn *websocket.Conn) *gatewayv2.AgentServerFrame { - t.Helper() - if err := conn.SetReadDeadline(time.Now().Add(time.Second)); err != nil { - t.Fatalf("set agent read deadline: %v", err) - } - messageType, data, err := conn.ReadMessage() - if err != nil { - t.Fatalf("receive agent frame: %v", err) - } - if messageType != websocket.BinaryMessage { - t.Fatalf("agent frame message type = %d, want binary", messageType) - } - var frame gatewayv2.AgentServerFrame - if err := proto.Unmarshal(data, &frame); err != nil { - t.Fatalf("unmarshal agent frame: %v", err) - } - return &frame -} diff --git a/crates/agent-gateway/test/webui/auth-storage.test.mjs b/crates/agent-gateway/test/webui/auth-storage.test.mjs deleted file mode 100644 index a66e89fe7..000000000 --- a/crates/agent-gateway/test/webui/auth-storage.test.mjs +++ /dev/null @@ -1,81 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { createWebModuleLoader } from "../helpers/load-web-module.mjs"; - -const loader = createWebModuleLoader(); -const auth = loader.loadModule("src/lib/gatewayAuth.ts"); -const storage = loader.loadModule("src/lib/storage.ts"); - -function installWindow(overrides = {}) { - const store = new Map(); - globalThis.window = { - location: { origin: "https://gateway.example" }, - localStorage: { - getItem(key) { - return store.has(key) ? store.get(key) : null; - }, - setItem(key, value) { - store.set(key, String(value)); - }, - removeItem(key) { - store.delete(key); - }, - }, - ...overrides, - }; - return store; -} - -test("normalizeGatewayAccessToken trims plain and Bearer-prefixed tokens", () => { - assert.equal(auth.normalizeGatewayAccessToken(" plain-token "), "plain-token"); - assert.equal(auth.normalizeGatewayAccessToken("Bearer secret-token"), "secret-token"); - assert.equal(auth.normalizeGatewayAccessToken(" bearer secret-token "), "secret-token"); - assert.equal(auth.normalizeGatewayAccessToken(" "), ""); -}); - -test("verifyGatewayAccessToken sends normalized bearer header and maps unauthorized errors", async () => { - installWindow(); - const requests = []; - globalThis.fetch = async (url, init) => { - requests.push({ url, init }); - return { - ok: false, - async text() { - return JSON.stringify({ error: "unauthorized" }); - }, - }; - }; - - await assert.rejects( - () => auth.verifyGatewayAccessToken("Bearer bad-token"), - /Access Token 错误,请检查后重试。/, - ); - - assert.equal(requests.length, 1); - assert.equal(requests[0].url, "https://gateway.example/api/status"); - assert.equal(requests[0].init.method, "GET"); - assert.equal(requests[0].init.headers.Authorization, "Bearer bad-token"); -}); - -test("verifyGatewayAccessToken returns normalized token after successful status check", async () => { - installWindow(); - globalThis.fetch = async () => ({ - ok: true, - async text() { - return ""; - }, - }); - - const token = await auth.verifyGatewayAccessToken(" bearer good-token "); - assert.equal(token, "good-token"); -}); - -test("gateway token storage persists and clears the single WebUI token key", () => { - installWindow(); - - assert.equal(storage.loadToken(), ""); - storage.saveToken("abc123"); - assert.equal(storage.loadToken(), "abc123"); - storage.clearToken(); - assert.equal(storage.loadToken(), ""); -}); diff --git a/crates/agent-gateway/test/webui/browser-uuid.test.mjs b/crates/agent-gateway/test/webui/browser-uuid.test.mjs deleted file mode 100644 index 7d7494385..000000000 --- a/crates/agent-gateway/test/webui/browser-uuid.test.mjs +++ /dev/null @@ -1,106 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { createWebModuleLoader } from "../helpers/load-web-module.mjs"; - -const loader = createWebModuleLoader(); -const { createUuid } = loader.loadModule("@/lib/shared/id.ts"); -const { createEmptyRequestDraft } = loader.loadModule("@/pages/settings/httpRequestEditor.tsx"); -const { normalizeAgentPromptTemplate, normalizeCustomProvider, normalizeSshSettings } = - loader.loadModule("@/lib/settings/index.ts"); - -const UUID_V4_PATTERN = - /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; - -function withCrypto(value, run) { - const descriptor = Object.getOwnPropertyDescriptor(globalThis, "crypto"); - if (value === undefined) { - delete globalThis.crypto; - } else { - Object.defineProperty(globalThis, "crypto", { - configurable: true, - value, - }); - } - try { - return run(); - } finally { - if (descriptor) { - Object.defineProperty(globalThis, "crypto", descriptor); - } else { - delete globalThis.crypto; - } - } -} - -test("createUuid uses crypto.randomUUID when available", () => { - withCrypto({ randomUUID: () => "native-uuid" }, () => { - assert.equal(createUuid(), "native-uuid"); - }); -}); - -test("createUuid falls back to an RFC 4122 v4 UUID without randomUUID", () => { - withCrypto({}, () => { - assert.match(createUuid(), UUID_V4_PATTERN); - }); -}); - -test("createUuid works when global crypto is unavailable", () => { - withCrypto(undefined, () => { - assert.match(createUuid(), UUID_V4_PATTERN); - }); -}); - -test("createUuid falls back when browser crypto methods throw", () => { - withCrypto( - { - randomUUID() { - throw new Error("randomUUID unavailable"); - }, - getRandomValues() { - throw new Error("getRandomValues unavailable"); - }, - }, - () => { - assert.match(createUuid(), UUID_V4_PATTERN); - }, - ); -}); - -test("createUuid fallback remains unique when time and randomness repeat", () => { - const originalNow = Date.now; - const originalRandom = Math.random; - Date.now = () => 123; - Math.random = () => 0; - try { - withCrypto({}, () => { - assert.notEqual(createUuid(), createUuid()); - }); - } finally { - Date.now = originalNow; - Math.random = originalRandom; - } -}); - -test("settings normalize generated IDs without crypto.randomUUID", () => { - withCrypto({}, () => { - const provider = normalizeCustomProvider({ name: "Provider", type: "codex" }); - const agent = normalizeAgentPromptTemplate({ name: "Agent" }); - const ssh = normalizeSshSettings({ - hosts: [ - { id: "duplicate", host: "first.example" }, - { id: "duplicate", host: "second.example" }, - { host: "third.example" }, - ], - }); - - assert.match(provider.id, UUID_V4_PATTERN); - assert.match(agent.id, UUID_V4_PATTERN); - assert.equal(new Set(ssh.hosts.map((host) => host.id)).size, 3); - }); -}); - -test("Hook/Cron HTTP request drafts work without crypto.randomUUID", () => { - withCrypto({}, () => { - assert.match(createEmptyRequestDraft().id, UUID_V4_PATTERN); - }); -}); diff --git a/crates/agent-gateway/test/webui/chat-command-pipeline.test.mjs b/crates/agent-gateway/test/webui/chat-command-pipeline.test.mjs deleted file mode 100644 index 7022ff148..000000000 --- a/crates/agent-gateway/test/webui/chat-command-pipeline.test.mjs +++ /dev/null @@ -1,456 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { createWebModuleLoader } from "../helpers/load-web-module.mjs"; - -const loader = createWebModuleLoader(); -const { ChatCommandPipeline } = loader.loadModule("src/lib/chat/stream/chatCommandPipeline.ts"); -const { createTranscriptStore } = loader.loadModule("src/lib/chat/transcript/transcriptStore.ts"); - -function createHarness() { - const stores = new Map(); - const outcomes = { bound: [], queued: [], failed: [] }; - const pipeline = new ChatCommandPipeline({ - getTranscriptStore(conversationId) { - let store = stores.get(conversationId); - if (!store) { - store = createTranscriptStore(); - stores.set(conversationId, store); - } - return store; - }, - onBound(update, pending) { - outcomes.bound.push({ update, pending }); - }, - onQueuedInGui(update, pending) { - outcomes.queued.push({ update, pending }); - }, - onFailed(pending, errorCode, message) { - outcomes.failed.push({ pending, errorCode, message }); - }, - }); - return { pipeline, stores, outcomes }; -} - -function rowText(row) { - if (row.kind === "assistant") { - return row.rounds - .map((round) => - round.blocks.flatMap((block) => (block.kind === "text" ? [block.text] : [])).join(""), - ) - .join("\n"); - } - return row.text ?? ""; -} - -function liveRows(snapshot) { - return snapshot.liveStartIndex >= 0 ? snapshot.rows.slice(snapshot.liveStartIndex) : []; -} - -// Live-flow texts (the region the old snapshot exposed as `tail`). -function tailTexts(store) { - store.flush(); - return liveRows(store.getSnapshot()).map((row) => rowText(row)); -} - -function transcriptTexts(store) { - store.flush(); - return store.getSnapshot().rows.map((row) => rowText(row)); -} - -test("submit inserts the optimistic bubble and resolves the accepted run", async () => { - const { pipeline, stores } = createHarness(); - const outcome = await pipeline.submit({ - conversationId: "conv-1", - clientRequestId: "client-1", - message: "hello", - submit: async () => ({ runId: "run-1", conversationId: "conv-1", acceptedSeq: 2 }), - }); - - assert.equal(outcome.kind, "accepted"); - assert.equal(outcome.accepted.runId, "run-1"); - assert.deepEqual(tailTexts(stores.get("conv-1")), ["hello"]); - assert.equal(pipeline.hasPending("conv-1"), true); - - // The stream's run signal settles the pending spinner. - pipeline.handleRunSignal("conv-1", "run-1"); - assert.equal(pipeline.hasPending("conv-1"), false); -}); - -test("edit-resend truncates at the edited message before command acknowledgement", async () => { - const { pipeline, stores } = createHarness(); - const store = createTranscriptStore(); - stores.set("conv-1", store); - const baseMessageRef = { - segmentIndex: 0, - messageIndex: 2, - segmentId: "segment-0", - messageId: "message-2", - role: "user", - contentHash: "hash-2", - }; - store.applyHistorySnapshot( - [ - { - id: "user-1", - kind: "user", - text: "first question", - attachments: [], - messageRef: { - segmentIndex: 0, - messageIndex: 0, - segmentId: "segment-0", - messageId: "message-0", - role: "user", - contentHash: "hash-0", - }, - }, - { id: "assistant-1", kind: "assistant", text: "first answer", round: 1 }, - { - id: "user-2", - kind: "user", - text: "old second question", - attachments: [], - messageRef: baseMessageRef, - }, - { id: "assistant-2", kind: "assistant", text: "old second answer", round: 1 }, - { - id: "user-3", - kind: "user", - text: "later question", - attachments: [], - messageRef: { - segmentIndex: 0, - messageIndex: 4, - segmentId: "segment-0", - messageId: "message-4", - role: "user", - contentHash: "hash-4", - }, - }, - { id: "assistant-3", kind: "assistant", text: "later answer", round: 1 }, - ], - { mode: "replace" }, - ); - - let acceptCommand; - const acceptGate = new Promise((resolve) => { - acceptCommand = resolve; - }); - const submitPromise = pipeline.submit({ - conversationId: "conv-1", - clientRequestId: "client-edit", - message: "edited second question", - isEditResend: true, - baseMessageRef, - submit: () => acceptGate, - }); - - assert.deepEqual( - transcriptTexts(store), - ["first question", "first answer", "edited second question"], - "the stale suffix is gone in the same optimistic update as the replacement bubble", - ); - - // The gateway's authoritative rebase arrives later and must be a no-op, - // while user_message binds the existing optimistic bubble instead of - // appending another one. - store.applyEvent({ - type: "rebased", - conversation_id: "conv-1", - run_id: "run-edit", - seq: 1, - base_message_ref: { - segment_index: 0, - message_index: 2, - segment_id: "segment-0", - message_id: "message-2", - role: "user", - content_hash: "hash-2", - }, - }); - store.applyEvent({ - type: "user_message", - conversation_id: "conv-1", - run_id: "run-edit", - client_request_id: "client-edit", - seq: 2, - message: "edited second question", - }); - assert.deepEqual(transcriptTexts(store), [ - "first question", - "first answer", - "edited second question", - ]); - - acceptCommand({ runId: "run-edit", conversationId: "conv-1", acceptedSeq: 2 }); - const outcome = await submitPromise; - assert.equal(outcome.kind, "accepted"); - pipeline.handleRunSignal("conv-1", "run-edit"); - assert.equal(pipeline.hasPending("conv-1"), false); -}); - -test("submit failure removes the bubble and surfaces an error entry", async () => { - const { pipeline, stores, outcomes } = createHarness(); - const outcome = await pipeline.submit({ - conversationId: "conv-1", - clientRequestId: "client-1", - message: "hello", - submit: async () => { - throw new Error("agent offline"); - }, - }); - - assert.equal(outcome.kind, "failed"); - assert.equal(pipeline.hasPending("conv-1"), false); - const texts = tailTexts(stores.get("conv-1")); - assert.equal(texts.some((text) => text === "hello"), false, "optimistic bubble removed"); - assert.equal(texts.some((text) => /agent offline/.test(text)), true); - assert.equal(outcomes.failed.length, 1); -}); - -test("bound update re-keys a draft conversation", async () => { - const { pipeline, outcomes } = createHarness(); - await pipeline.submit({ - conversationId: "draft-1", - clientRequestId: "client-1", - message: "first message", - submit: async () => ({ runId: "run-1", conversationId: "", acceptedSeq: 0 }), - }); - - pipeline.handleCommandUpdate({ - runId: "run-1", - clientRequestId: "client-1", - conversationId: "conv-real", - phase: "bound", - errorCode: null, - message: null, - }); - - assert.equal(outcomes.bound.length, 1); - assert.equal(outcomes.bound[0].pending.conversationId, "conv-real"); - assert.equal(pipeline.hasPending("draft-1"), false); - assert.equal(pipeline.hasPending("conv-real"), true); - pipeline.handleRunSignal("conv-real", "run-1"); - assert.equal(pipeline.hasPending("conv-real"), false); -}); - -test("queued_in_gui clears pending and removes the optimistic bubble", async () => { - const { pipeline, stores, outcomes } = createHarness(); - await pipeline.submit({ - conversationId: "conv-1", - clientRequestId: "client-1", - message: "park me", - submit: async () => ({ runId: "run-1", conversationId: "conv-1", acceptedSeq: 1 }), - }); - - pipeline.handleCommandUpdate({ - runId: "run-1", - clientRequestId: "client-1", - conversationId: "conv-1", - phase: "queued_in_gui", - errorCode: null, - message: null, - }); - - assert.equal(pipeline.hasPending("conv-1"), false); - assert.equal(outcomes.queued.length, 1); - assert.deepEqual(tailTexts(stores.get("conv-1")), [], "bubble removed; queue panel owns it"); -}); - -test("failed update surfaces the gateway error", async () => { - const { pipeline, stores, outcomes } = createHarness(); - await pipeline.submit({ - conversationId: "conv-1", - clientRequestId: "client-1", - message: "doomed", - submit: async () => ({ runId: "run-1", conversationId: "conv-1", acceptedSeq: 1 }), - }); - - pipeline.handleCommandUpdate({ - runId: "run-1", - clientRequestId: "client-1", - conversationId: "conv-1", - phase: "failed", - errorCode: "startup_timeout", - message: "did not start", - }); - - assert.equal(pipeline.hasPending("conv-1"), false); - assert.equal(outcomes.failed.length, 1); - assert.equal(outcomes.failed[0].errorCode, "startup_timeout"); - const texts = tailTexts(stores.get("conv-1")); - assert.equal(texts.some((text) => /did not start/.test(text)), true); -}); - -test("run signals settle only on strict identity (runId or own clientRequestId)", async () => { - const { pipeline } = createHarness(); - let releaseAccept; - const acceptGate = new Promise((resolve) => { - releaseAccept = resolve; - }); - const submitPromise = pipeline.submit({ - conversationId: "conv-1", - clientRequestId: "client-1", - message: "hello", - submit: async () => { - await acceptGate; - return { runId: "run-1", conversationId: "conv-1", acceptedSeq: 2 }; - }, - }); - - // Accept response still in flight (pending.runId === null): a foreign run - // signal without our client_request_id must NOT settle the pending — - // otherwise a GUI queue auto-send would disarm the startup watchdog. - pipeline.handleRunSignal("conv-1", "run-foreign"); - assert.equal(pipeline.hasPending("conv-1"), true, "foreign signal ignored"); - pipeline.handleRunSignal("conv-1", "run-foreign", "client-other"); - assert.equal(pipeline.hasPending("conv-1"), true, "foreign clientRequestId ignored"); - - // Our own run signal, matched by client_request_id, settles before the - // accept response lands. - pipeline.handleRunSignal("conv-1", "run-1", "client-1"); - assert.equal(pipeline.hasPending("conv-1"), false, "own clientRequestId settles"); - - releaseAccept(); - const outcome = await submitPromise; - assert.equal(outcome.kind, "settled"); - // The late accept response must not resurrect a byRunId registration for - // the already-settled pending: a later command_update for that run id is a - // no-op instead of firing hooks against a dead pending. - pipeline.handleCommandUpdate({ - runId: "run-1", - clientRequestId: "client-1", - conversationId: "conv-other", - phase: "bound", - errorCode: null, - message: null, - }); - assert.equal(pipeline.hasPending("conv-other"), false); -}); - -test("a run signal that beats a lost acknowledgement prevents a false local failure", async () => { - const { pipeline, stores, outcomes } = createHarness(); - let rejectAccept; - const acceptGate = new Promise((_, reject) => { - rejectAccept = reject; - }); - const submitPromise = pipeline.submit({ - conversationId: "conv-1", - clientRequestId: "client-1", - message: "hello", - submit: () => acceptGate, - }); - - pipeline.handleRunSignal("conv-1", "run-1", "client-1"); - rejectAccept(new Error("chat command acknowledgement lost")); - const outcome = await submitPromise; - - assert.equal(outcome.kind, "settled"); - assert.equal(pipeline.hasPending("conv-1"), false); - assert.equal(outcomes.failed.length, 0, "no duplicate local failure hook"); - assert.deepEqual(tailTexts(stores.get("conv-1")), ["hello"]); -}); - -test("a queued update that beats a lost acknowledgement remains queued", async () => { - const { pipeline, stores, outcomes } = createHarness(); - let rejectAccept; - const acceptGate = new Promise((_, reject) => { - rejectAccept = reject; - }); - const submitPromise = pipeline.submit({ - conversationId: "conv-1", - clientRequestId: "client-1", - message: "queue me", - submit: () => acceptGate, - }); - - const update = { - runId: "run-1", - clientRequestId: "client-1", - conversationId: "conv-1", - phase: "queued_in_gui", - errorCode: null, - message: null, - }; - pipeline.handleCommandUpdate(update); - rejectAccept(new Error("chat command acknowledgement lost")); - const outcome = await submitPromise; - - assert.equal(outcome.kind, "queued_in_gui"); - assert.equal(outcomes.queued.length, 1); - assert.equal(outcomes.failed.length, 0); - assert.deepEqual(tailTexts(stores.get("conv-1")), []); -}); - -test("run signals with a known runId settle regardless of clientRequestId", async () => { - const { pipeline } = createHarness(); - await pipeline.submit({ - conversationId: "conv-1", - clientRequestId: "client-1", - message: "hello", - submit: async () => ({ runId: "run-1", conversationId: "conv-1", acceptedSeq: 2 }), - }); - assert.equal(pipeline.hasPending("conv-1"), true); - - // Foreign run id: ignored even though the conversation matches. - pipeline.handleRunSignal("conv-1", "run-9"); - assert.equal(pipeline.hasPending("conv-1"), true); - - // Matching run id (e.g. an activity event while the conversation is not - // displayed) settles without any clientRequestId. - pipeline.handleRunSignal("conv-1", "run-1"); - assert.equal(pipeline.hasPending("conv-1"), false); -}); - -test("reset clears pending commands and ignores their late acknowledgements", async () => { - const { pipeline, stores, outcomes } = createHarness(); - let rejectAccept; - const acceptGate = new Promise((_, reject) => { - rejectAccept = reject; - }); - const submitPromise = pipeline.submit({ - conversationId: "conv-1", - clientRequestId: "client-1", - message: "old agent request", - submit: () => acceptGate, - }); - - assert.equal(pipeline.hasPending("conv-1"), true); - pipeline.reset(); - assert.equal(pipeline.hasPending("conv-1"), false); - assert.deepEqual(tailTexts(stores.get("conv-1")), []); - - rejectAccept(new Error("old agent disconnected")); - assert.equal((await submitPromise).kind, "settled"); - assert.equal(outcomes.failed.length, 0); -}); - - -test("optimistic:false suppresses the transcript echo for queue-destined sends", async () => { - const { pipeline, stores } = createHarness(); - await pipeline.submit({ - conversationId: "conv-1", - clientRequestId: "client-1", - message: "park me quietly", - optimistic: false, - submit: async () => ({ runId: "run-1", conversationId: "conv-1", acceptedSeq: 0 }), - }); - // No store is touched at submit time — the transcript never sees the prompt. - const storeAfterSubmit = stores.get("conv-1"); - if (storeAfterSubmit) { - assert.deepEqual(tailTexts(storeAfterSubmit), [], "no bubble flash"); - } - assert.equal(pipeline.hasPending("conv-1"), true, "watchdog still armed"); - - // queued_in_gui settles it without ever having shown a bubble. - pipeline.handleCommandUpdate({ - runId: "run-1", - clientRequestId: "client-1", - conversationId: "conv-1", - phase: "queued_in_gui", - errorCode: null, - message: null, - }); - assert.equal(pipeline.hasPending("conv-1"), false); - assert.deepEqual(tailTexts(stores.get("conv-1")), []); -}); diff --git a/crates/agent-gateway/test/webui/chat-turn-queue.test.mjs b/crates/agent-gateway/test/webui/chat-turn-queue.test.mjs deleted file mode 100644 index b1498ab38..000000000 --- a/crates/agent-gateway/test/webui/chat-turn-queue.test.mjs +++ /dev/null @@ -1,50 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { createWebModuleLoader } from "../helpers/load-web-module.mjs"; - -// The chat turn queue itself lives in the desktop GUI (the gateway relays -// snapshots); the web module keeps only the composer-side content check. -const loader = createWebModuleLoader(); -const { queuedChatTurnHasContent } = loader.loadModule("src/pages/chat/queue/chatTurnQueue.ts"); - -function draft(overrides = {}) { - return { - isEmpty: false, - text: "hello", - textWithoutLargePastes: "hello", - largePastes: [], - segments: [{ type: "text", text: "hello" }], - ...overrides, - }; -} - -test("queuedChatTurnHasContent accepts drafts with text", () => { - assert.equal(queuedChatTurnHasContent(draft(), []), true); -}); - -test("queuedChatTurnHasContent accepts empty drafts with uploads", () => { - const uploads = [ - { - relativePath: "notes.md", - absolutePath: "/workspace/notes.md", - fileName: "notes.md", - kind: "text", - sizeBytes: 12, - }, - ]; - assert.equal(queuedChatTurnHasContent(draft({ isEmpty: true, text: "" }), uploads), true); -}); - -test("queuedChatTurnHasContent rejects missing or empty drafts", () => { - assert.equal(queuedChatTurnHasContent(null, []), false); - assert.equal(queuedChatTurnHasContent(undefined, []), false); - assert.equal(queuedChatTurnHasContent(draft({ isEmpty: true, text: " " }), []), false); -}); - -test("queuedChatTurnHasContent treats structured-only drafts as content", () => { - assert.equal( - queuedChatTurnHasContent(draft({ isEmpty: false, text: "" }), []), - true, - "non-empty draft flag wins even without plain text", - ); -}); diff --git a/crates/agent-gateway/test/webui/clipboard-files.test.mjs b/crates/agent-gateway/test/webui/clipboard-files.test.mjs deleted file mode 100644 index 4dcaeaa26..000000000 --- a/crates/agent-gateway/test/webui/clipboard-files.test.mjs +++ /dev/null @@ -1,46 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { createWebModuleLoader } from "../helpers/load-web-module.mjs"; - -const loader = createWebModuleLoader(); -const clipboard = loader.loadModule("src/lib/clipboardFiles.ts"); - -function createClipboardFile(name, content, type, lastModified) { - return new File([content], name, { type, lastModified }); -} - -test("extractClipboardFiles does not double-read files exposed through clipboard items", () => { - const directFile = createClipboardFile("", "image-bytes", "image/png", 100); - const itemFile = createClipboardFile("", "image-bytes", "image/png", 101); - - const files = clipboard.extractClipboardFiles({ - files: [directFile], - items: [ - { - kind: "file", - getAsFile: () => itemFile, - }, - ], - }); - - assert.equal(files.length, 1); - assert.equal(files[0].name, "clipboard-file-1.png"); - assert.equal(files[0].type, "image/png"); -}); - -test("extractClipboardFiles falls back to clipboard items when files list is empty", () => { - const itemFile = createClipboardFile("", "image-bytes", "image/png", 101); - - const files = clipboard.extractClipboardFiles({ - files: [], - items: [ - { - kind: "file", - getAsFile: () => itemFile, - }, - ], - }); - - assert.equal(files.length, 1); - assert.equal(files[0].name, "clipboard-file-1.png"); -}); diff --git a/crates/agent-gateway/test/webui/conversation-stream-client.test.mjs b/crates/agent-gateway/test/webui/conversation-stream-client.test.mjs deleted file mode 100644 index 6fa8f16d8..000000000 --- a/crates/agent-gateway/test/webui/conversation-stream-client.test.mjs +++ /dev/null @@ -1,311 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { createWebModuleLoader } from "../helpers/load-web-module.mjs"; - -const loader = createWebModuleLoader(); -const { ConversationStreamClient } = loader.loadModule( - "src/lib/chat/stream/conversationStreamClient.ts", -); - -function createTransport() { - const calls = []; - let responder = () => ({}); - return { - calls, - setResponder(fn) { - responder = fn; - }, - request(type, payload, options) { - calls.push({ type, payload, options }); - return Promise.resolve(responder(type, payload)); - }, - }; -} - -function subscribeResponse(overrides = {}) { - return { - conversation_id: "conv-1", - stream_epoch: "epoch-1", - latest_seq: 0, - reset: false, - activity: null, - snapshot: null, - events: [], - ...overrides, - }; -} - -function collectHandlers() { - const seen = { syncs: [], events: [] }; - return { - seen, - handlers: { - onSync(result) { - seen.syncs.push(result); - }, - onEvent(event) { - seen.events.push(event); - }, - }, - }; -} - -async function flushMicrotasks() { - for (let i = 0; i < 8; i += 1) { - await Promise.resolve(); - } -} - -function waitFor(predicate, label, timeoutMs = 1_000) { - return new Promise((resolve, reject) => { - const startedAt = Date.now(); - const tick = () => { - if (predicate()) { - resolve(); - return; - } - if (Date.now() - startedAt > timeoutMs) { - reject(new Error(`timed out waiting for ${label}`)); - return; - } - setTimeout(tick, 5); - }; - tick(); - }); -} - -test("subscribes with resume cursor and re-subscribes after reconnect", async () => { - const transport = createTransport(); - const client = new ConversationStreamClient(transport); - const { seen, handlers } = collectHandlers(); - - transport.setResponder(() => subscribeResponse({ latest_seq: 4 })); - client.subscribe("conv-1", handlers); - client.handleConnected(); - await flushMicrotasks(); - - assert.equal(transport.calls.length, 1); - assert.equal(transport.calls[0].type, "chat.subscribe"); - assert.equal(transport.calls[0].payload.after_seq, 0); - assert.equal(transport.calls[0].options.timeoutMs, 30_000); - assert.equal(seen.syncs.length, 1); - - // Live events advance the cursor. - client.handleChatEvent({ type: "token", conversation_id: "conv-1", run_id: "run-1", seq: 5, text: "a" }); - client.handleChatEvent({ type: "token", conversation_id: "conv-1", run_id: "run-1", seq: 6, text: "b" }); - assert.equal(seen.events.length, 2); - - // Disconnect + reconnect: the registration survives and resumes from seq 6 - // with the stream epoch. - client.handleDisconnected(); - transport.setResponder(() => subscribeResponse({ latest_seq: 8, events: [ - { type: "token", conversation_id: "conv-1", run_id: "run-1", seq: 7, text: "c" }, - { type: "token", conversation_id: "conv-1", run_id: "run-1", seq: 8, text: "d" }, - ] })); - client.handleConnected(); - await flushMicrotasks(); - - assert.equal(transport.calls.length, 2); - assert.equal(transport.calls[1].payload.after_seq, 6); - assert.equal(transport.calls[1].payload.stream_epoch, "epoch-1"); - assert.equal(seen.syncs.length, 2); - assert.equal(seen.syncs[1].events.length, 2); -}); - -test("duplicate and stale seqs are dropped; gaps trigger a resync", async () => { - const transport = createTransport(); - const client = new ConversationStreamClient(transport); - const { seen, handlers } = collectHandlers(); - - transport.setResponder(() => subscribeResponse({ latest_seq: 2 })); - client.subscribe("conv-1", handlers); - client.handleConnected(); - await flushMicrotasks(); - - client.handleChatEvent({ type: "token", conversation_id: "conv-1", seq: 2, text: "dup" }); - assert.equal(seen.events.length, 0, "stale seq dropped"); - - client.handleChatEvent({ type: "token", conversation_id: "conv-1", seq: 3, text: "ok" }); - assert.equal(seen.events.length, 1); - - transport.setResponder(() => subscribeResponse({ latest_seq: 9, events: [] })); - client.handleChatEvent({ type: "token", conversation_id: "conv-1", seq: 9, text: "gap" }); - await flushMicrotasks(); - assert.equal(seen.events.length, 1, "gapped event not delivered directly"); - assert.equal( - transport.calls.filter((call) => call.type === "chat.subscribe").length, - 2, - "gap triggered a resync", - ); -}); - -test("events racing ahead of the subscribe response are buffered, then drained", async () => { - const transport = createTransport(); - const client = new ConversationStreamClient(transport); - const { seen, handlers } = collectHandlers(); - - let release; - const gate = new Promise((resolve) => { - release = resolve; - }); - transport.setResponder(() => gate.then(() => subscribeResponse({ latest_seq: 1 }))); - - client.subscribe("conv-1", handlers); - client.handleConnected(); - - // Pushes arrive while the subscribe response is still in flight. - client.handleChatEvent({ type: "token", conversation_id: "conv-1", seq: 2, text: "early" }); - client.handleChatEvent({ type: "token", conversation_id: "conv-1", seq: 3, text: "birds" }); - assert.equal(seen.events.length, 0); - - release(); - await flushMicrotasks(); - assert.equal(seen.syncs.length, 1); - assert.deepEqual( - seen.events.map((event) => event.text), - ["early", "birds"], - ); -}); - -test("seq-less events (snapshot pushes) pass through without cursor changes", async () => { - const transport = createTransport(); - const client = new ConversationStreamClient(transport); - const { seen, handlers } = collectHandlers(); - - transport.setResponder(() => subscribeResponse({ latest_seq: 5 })); - client.subscribe("conv-1", handlers); - client.handleConnected(); - await flushMicrotasks(); - - client.handleChatEvent({ type: "snapshot", conversation_id: "conv-1", run_id: "run-1", entries_json: "[]" }); - client.handleChatEvent({ type: "token", conversation_id: "conv-1", seq: 6, text: "next" }); - assert.deepEqual( - seen.events.map((event) => event.type), - ["snapshot", "token"], - ); -}); - -test("subscription_reset resumes from the cursor; cleanup unsubscribes", async () => { - const transport = createTransport(); - const client = new ConversationStreamClient(transport); - const { handlers } = collectHandlers(); - - transport.setResponder(() => subscribeResponse({ latest_seq: 3 })); - const cleanup = client.subscribe("conv-1", handlers); - client.handleConnected(); - await flushMicrotasks(); - - client.handleSubscriptionReset({ conversation_id: "conv-1" }); - await flushMicrotasks(); - const subscribes = transport.calls.filter((call) => call.type === "chat.subscribe"); - assert.equal(subscribes.length, 2); - assert.equal(subscribes[1].payload.after_seq, 3); - - assert.equal(client.size, 1); - cleanup(); - await flushMicrotasks(); - assert.equal(client.size, 0); - assert.equal( - transport.calls.filter((call) => call.type === "chat.unsubscribe").length, - 1, - ); -}); - -test("disconnect clears events buffered before the subscribe response", async () => { - const transport = createTransport(); - const client = new ConversationStreamClient(transport); - const { seen, handlers } = collectHandlers(); - - let release; - const gate = new Promise((resolve) => { - release = resolve; - }); - transport.setResponder(() => gate.then(() => subscribeResponse({ latest_seq: 1 }))); - client.subscribe("conv-1", handlers); - client.handleConnected(); - - // Events buffered while the subscribe response is in flight belong to the - // dying connection; after a disconnect the resume protocol re-fetches - // everything, so draining them later would corrupt the transcript. - client.handleChatEvent({ type: "token", conversation_id: "conv-1", seq: 2, text: "stale" }); - client.handleDisconnected(); - - transport.setResponder(() => subscribeResponse({ latest_seq: 3 })); - client.handleConnected(); - release(); - await flushMicrotasks(); - - assert.equal(seen.events.length, 0, "stale buffered events were dropped"); - assert.equal(seen.syncs.length, 1, "the stale pre-disconnect response was ignored"); - assert.equal(seen.syncs[0].latestSeq, 3); -}); - -test("handleConnected is idempotent for the same authenticated connection", async () => { - const transport = createTransport(); - const client = new ConversationStreamClient(transport); - const { handlers } = collectHandlers(); - - transport.setResponder(() => subscribeResponse({ latest_seq: 1 })); - client.subscribe("conv-1", handlers); - client.handleConnected(); - client.handleConnected(); - await flushMicrotasks(); - - assert.equal( - transport.calls.filter((call) => call.type === "chat.subscribe").length, - 1, - ); -}); - -test("failed subscribe retries on the current connection and drains buffered pushes", async () => { - const transport = createTransport(); - const client = new ConversationStreamClient(transport); - const { seen, handlers } = collectHandlers(); - let attempt = 0; - - transport.setResponder(() => { - attempt += 1; - if (attempt === 1) { - throw new Error("temporary subscribe failure"); - } - return subscribeResponse({ latest_seq: 1 }); - }); - client.subscribe("conv-1", handlers); - client.handleConnected(); - await flushMicrotasks(); - - client.handleChatEvent({ - type: "token", - conversation_id: "conv-1", - run_id: "run-1", - seq: 2, - text: "recovered", - }); - await waitFor( - () => transport.calls.filter((call) => call.type === "chat.subscribe").length === 2, - "subscribe retry", - ); - await flushMicrotasks(); - - assert.equal(seen.syncs.length, 1); - assert.deepEqual(seen.events.map((event) => event.text), ["recovered"]); -}); - -test("disconnect and cleanup cancel scheduled subscribe retries", async () => { - const transport = createTransport(); - const client = new ConversationStreamClient(transport); - const { handlers } = collectHandlers(); - - transport.setResponder(() => { - throw new Error("temporary subscribe failure"); - }); - const cleanup = client.subscribe("conv-1", handlers); - client.handleConnected(); - await flushMicrotasks(); - assert.equal(transport.calls.length, 1); - - client.handleDisconnected(); - cleanup(); - await new Promise((resolve) => setTimeout(resolve, 450)); - assert.equal(transport.calls.length, 1, "no retry fires after disconnect/cleanup"); -}); diff --git a/crates/agent-gateway/test/webui/custom-headers.test.mjs b/crates/agent-gateway/test/webui/custom-headers.test.mjs deleted file mode 100644 index 81c8a61bf..000000000 --- a/crates/agent-gateway/test/webui/custom-headers.test.mjs +++ /dev/null @@ -1,237 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { createWebModuleLoader } from "../helpers/load-web-module.mjs"; - -const loader = createWebModuleLoader(); -const customHeaders = loader.loadModule("src/lib/providers/customHeaders.ts"); - -function errorHasCode(code) { - return (error) => error?.code === code; -} - -test("parses JSON objects and arrays", () => { - assert.deepEqual( - customHeaders.parseCustomHeadersImport( - '{"X-Title":"LiveAgent","X-Environment":"production"}', - ), - { - headers: [ - { key: "X-Title", value: "LiveAgent" }, - { key: "X-Environment", value: "production" }, - ], - issues: [], - }, - ); - - assert.deepEqual( - customHeaders.parseCustomHeadersImport( - '[{"key":"X-Title","value":"LiveAgent"}]', - ), - { - headers: [{ key: "X-Title", value: "LiveAgent" }], - issues: [], - }, - ); -}); - -test("converts JSON number and boolean values and skips nested values", () => { - const result = customHeaders.parseCustomHeadersImport( - JSON.stringify({ - "X-String": "literal", - "X-Number": 42, - "X-Boolean": false, - "X-Null": null, - "X-Object": { nested: true }, - "X-Array": ["nested"], - }), - ); - - assert.deepEqual(result.headers, [ - { key: "X-String", value: "literal" }, - { key: "X-Number", value: "42" }, - { key: "X-Boolean", value: "false" }, - ]); - assert.deepEqual( - result.issues.map(({ key, reason }) => ({ key, reason })), - [ - { key: "X-Null", reason: "unsupported-value" }, - { key: "X-Object", reason: "unsupported-value" }, - { key: "X-Array", reason: "unsupported-value" }, - ], - ); - assert.ok(result.issues.every((issue) => !("value" in issue))); -}); - -test("extracts quoted cURL headers across Bash, PowerShell, and CMD continuations", () => { - const bash = [ - 'curl "https://example.test" \\', - ' -H "X-Double: one" \\', - " --header 'X-Single: two'", - ].join("\n"); - const powershell = [ - 'curl "https://example.test" ' + String.fromCharCode(96), - ' --header "X-PowerShell: three"', - ].join("\n"); - const cmd = [ - 'curl "https://example.test" ^', - ' -H "X-Cmd: four"', - ].join("\r\n"); - - assert.deepEqual(customHeaders.parseCustomHeadersImport(bash).headers, [ - { key: "X-Double", value: "one" }, - { key: "X-Single", value: "two" }, - ]); - assert.deepEqual(customHeaders.parseCustomHeadersImport(powershell).headers, [ - { key: "X-PowerShell", value: "three" }, - ]); - assert.deepEqual(customHeaders.parseCustomHeadersImport(cmd).headers, [ - { key: "X-Cmd", value: "four" }, - ]); -}); - -test("supports --header=value, preserves value colons, and ignores non-header cURL options", () => { - const result = customHeaders.parseCustomHeadersImport( - 'curl "https://example.test" -X POST --data "secret-body" --cookie "secret-cookie" ' + - '--header="X-Endpoint: https://api.example.test:8443/v1"', - ); - - assert.deepEqual(result, { - headers: [{ key: "X-Endpoint", value: "https://api.example.test:8443/v1" }], - issues: [], - }); -}); - -test("uses the last case-insensitive duplicate from imported content", () => { - const result = customHeaders.parseCustomHeadersImport( - '[{"key":"X-Title","value":"first"},{"key":"x-title","value":"last"}]', - ); - - assert.deepEqual(result.headers, [{ key: "x-title", value: "last" }]); -}); - -test("overwrites existing names in place and appends new headers without mutating inputs", () => { - const current = [ - { key: "X-First", value: "one" }, - { key: "X-Title", value: "old" }, - { key: "X-Last", value: "three" }, - { key: "x-title", value: "duplicate" }, - ]; - const imported = [ - { key: "x-TITLE", value: "new" }, - { key: "X-New", value: "four" }, - ]; - - const merged = customHeaders.mergeImportedCustomHeaders(current, imported); - - assert.deepEqual(merged, { - headers: [ - { key: "X-First", value: "one" }, - { key: "x-TITLE", value: "new" }, - { key: "X-Last", value: "three" }, - { key: "X-New", value: "four" }, - ], - importedCount: 2, - overwrittenCount: 1, - }); - assert.deepEqual(current, [ - { key: "X-First", value: "one" }, - { key: "X-Title", value: "old" }, - { key: "X-Last", value: "three" }, - { key: "x-title", value: "duplicate" }, - ]); - assert.deepEqual(imported, [ - { key: "x-TITLE", value: "new" }, - { key: "X-New", value: "four" }, - ]); -}); - -test("skips protected names, invalid names, and CR/LF values without exposing values", () => { - const result = customHeaders.parseCustomHeadersImport( - JSON.stringify({ - Authorization: "secret-auth", - "x-api-key": "secret-api", - "x-goog-api-key": "secret-google", - "anthropic-beta": "secret-beta", - Host: "secret-host", - "Content-Length": 10, - "Bad Header": "secret-invalid", - "X-Line": "secret\r\ninjected", - "X-Okay": "kept", - }), - ); - - assert.deepEqual(result.headers, [{ key: "X-Okay", value: "kept" }]); - assert.deepEqual( - result.issues.map(({ key, reason }) => ({ key, reason })), - [ - { key: "Authorization", reason: "reserved" }, - { key: "x-api-key", reason: "reserved" }, - { key: "x-goog-api-key", reason: "reserved" }, - { key: "anthropic-beta", reason: "reserved" }, - { key: "Host", reason: "reserved" }, - { key: "Content-Length", reason: "reserved" }, - { key: "Bad Header", reason: "invalid-key" }, - { key: "X-Line", reason: "invalid-value" }, - ], - ); - assert.ok(result.issues.every((issue) => !("value" in issue))); -}); - -test("reports empty input, malformed JSON, and unterminated cURL quotes", () => { - assert.throws( - () => customHeaders.parseCustomHeadersImport(" "), - errorHasCode("empty"), - ); - assert.throws( - () => customHeaders.parseCustomHeadersImport('{"X-Title":'), - errorHasCode("invalid-json"), - ); - assert.throws( - () => customHeaders.parseCustomHeadersImport('curl -H "X-Title: open'), - errorHasCode("unterminated-quote"), - ); -}); - -test("does not read @files and leaves current headers unchanged when nothing is valid", () => { - const current = [{ key: "X-Existing", value: "unchanged" }]; - const fileResult = customHeaders.parseCustomHeadersImport("curl -H @headers.txt"); - assert.deepEqual(fileResult, { - headers: [], - issues: [{ reason: "malformed-header" }], - }); - - const protectedResult = customHeaders.parseCustomHeadersImport( - '{"Authorization":"secret"}', - ); - const merged = customHeaders.mergeImportedCustomHeaders( - current, - protectedResult.headers, - ); - assert.deepEqual(merged, { - headers: [{ key: "X-Existing", value: "unchanged" }], - importedCount: 0, - overwrittenCount: 0, - }); - assert.deepEqual(current, [{ key: "X-Existing", value: "unchanged" }]); -}); - -test("parsed and saved headers reach runtime merge while CR/LF values are rejected", () => { - const parsed = customHeaders.parseCustomHeadersImport( - '{"X-Imported":"sentinel"}', - ); - const saved = customHeaders.mergeImportedCustomHeaders([], parsed.headers); - - assert.deepEqual( - customHeaders.mergeCustomHeaders( - { Accept: "application/json" }, - [ - ...saved.headers, - { key: "X-Line", value: "bad\nvalue" }, - ], - ), - { - Accept: "application/json", - "X-Imported": "sentinel", - }, - ); -}); diff --git a/crates/agent-gateway/test/webui/font-family.test.mjs b/crates/agent-gateway/test/webui/font-family.test.mjs deleted file mode 100644 index e54b02ccd..000000000 --- a/crates/agent-gateway/test/webui/font-family.test.mjs +++ /dev/null @@ -1,203 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { createWebModuleLoader } from "../helpers/load-web-module.mjs"; - -const loader = createWebModuleLoader(); -const fontFamily = loader.loadModule("src/lib/shared/fontFamily.ts"); - -async function withNavigator(value, task) { - const previous = Object.getOwnPropertyDescriptor(globalThis, "navigator"); - Object.defineProperty(globalThis, "navigator", { - configurable: true, - enumerable: true, - value, - }); - try { - return await task(); - } finally { - if (previous) { - Object.defineProperty(globalThis, "navigator", previous); - } else { - delete globalThis.navigator; - } - } -} - -test("font family normalizer keeps freeform stacks and rejects unsafe values", () => { - assert.equal(fontFamily.normalizeFontFamily(""), ""); - assert.equal(fontFamily.normalizeFontFamily("system"), "system"); - assert.equal(fontFamily.normalizeFontFamily(" Inter "), "Inter"); - assert.equal( - fontFamily.normalizeFontFamily('Inter, "PingFang SC", sans-serif'), - 'Inter, "PingFang SC", sans-serif', - ); - assert.equal(fontFamily.normalizeFontFamily("rounded"), "rounded"); - assert.equal(fontFamily.normalizeFontFamily("serif"), "serif"); - assert.equal(fontFamily.normalizeFontFamily('Inter; background: red'), ""); - assert.equal(fontFamily.normalizeFontFamily("url(https://evil.example/font.woff2)"), ""); - assert.equal(fontFamily.normalizeFontFamily("x".repeat(201)), ""); -}); - -test("font family resolvers preserve the established defaults", () => { - assert.equal( - fontFamily.resolveFontFamily("", fontFamily.DEFAULT_INTERFACE_FONT_FAMILY), - fontFamily.DEFAULT_INTERFACE_FONT_FAMILY, - ); - assert.equal(fontFamily.resolveCodeFontFamily(""), fontFamily.DEFAULT_CODE_FONT_FAMILY); - assert.equal(fontFamily.resolveCodeFontFamily("Menlo"), "Menlo"); - assert.equal(fontFamily.quoteFontFamilyName("PingFang SC"), '"PingFang SC"'); - assert.equal(fontFamily.quoteFontFamilyName("Inter"), "Inter"); -}); - -test("font family select helpers map default/custom sentinels and build options", () => { - const options = fontFamily.buildFontFamilySelectOptions(["PingFang SC", "Inter"]); - assert.equal( - fontFamily.toFontFamilySelectValue("", options), - fontFamily.FONT_FAMILY_DEFAULT_SELECT_VALUE, - ); - assert.equal(fontFamily.toFontFamilySelectValue("Inter", options), "Inter"); - assert.equal( - fontFamily.toFontFamilySelectValue("Maple Mono", options), - fontFamily.FONT_FAMILY_CUSTOM_SELECT_VALUE, - ); - assert.equal( - fontFamily.toFontFamilySelectValue("", options, true), - fontFamily.FONT_FAMILY_CUSTOM_SELECT_VALUE, - ); - assert.equal( - fontFamily.fromFontFamilySelectValue(fontFamily.FONT_FAMILY_DEFAULT_SELECT_VALUE), - "", - ); - assert.equal( - fontFamily.fromFontFamilySelectValue(fontFamily.FONT_FAMILY_CUSTOM_SELECT_VALUE), - "", - ); - assert.equal(fontFamily.fromFontFamilySelectValue("Menlo"), "Menlo"); - assert.equal(fontFamily.isKnownFontFamilySelectValue("Inter", options), true); - assert.equal(fontFamily.isKnownFontFamilySelectValue("Maple Mono", options), false); - - assert.deepEqual( - options.map((option) => option.value), - [ - "Arial", - '"Cascadia Code"', - "Consolas", - '"Fira Code"', - "Georgia", - '"Helvetica Neue"', - '"Hiragino Sans GB"', - '"IBM Plex Mono"', - "Inter", - '"JetBrains Mono"', - "Menlo", - '"Microsoft YaHei"', - "Monaco", - '"Noto Sans SC"', - '"PingFang SC"', - '"SF Mono"', - '"SF Pro Text"', - '"Songti SC"', - '"Source Code Pro"', - '"Source Han Sans SC"', - "STSong", - '"Times New Roman"', - ], - ); - assert.equal(options.find((option) => option.value === '"PingFang SC"')?.label, "PingFang SC"); -}); - -test("applying font families updates CSS variables and only emits code changes", () => { - const previousWindow = globalThis.window; - const windowTarget = new EventTarget(); - globalThis.window = windowTarget; - const values = new Map(); - const root = { - style: { - getPropertyValue: (name) => values.get(name) ?? "", - setProperty: (name, value) => values.set(name, value), - }, - }; - const codeFonts = []; - windowTarget.addEventListener(fontFamily.CODE_FONT_FAMILY_CHANGE_EVENT, (event) => { - codeFonts.push(event.detail); - }); - try { - fontFamily.applyFontFamilies( - { interfaceFontFamily: "Inter", chatFontFamily: "Charter", codeFontFamily: "Menlo" }, - root, - ); - fontFamily.applyFontFamilies( - { interfaceFontFamily: "Inter", chatFontFamily: "Charter", codeFontFamily: "Menlo" }, - root, - ); - fontFamily.applyFontFamilies( - { interfaceFontFamily: "Inter", chatFontFamily: "Charter", codeFontFamily: "Monaco" }, - root, - ); - assert.equal(values.get("--app-font-family"), "Inter"); - assert.equal(values.get("--chat-font-family"), "Charter"); - assert.equal(values.get("--code-font-family"), "Monaco"); - assert.deepEqual(codeFonts, ["Menlo", "Monaco"]); - } finally { - globalThis.window = previousWindow; - } -}); - -test("listLocalFontFamilies does not trigger a local-fonts permission prompt", async () => { - const previous = globalThis.queryLocalFonts; - let queryCount = 0; - globalThis.queryLocalFonts = async () => { - queryCount += 1; - return [{ family: "Inter" }]; - }; - try { - await withNavigator( - { - permissions: { - query: async (descriptor) => { - assert.deepEqual(descriptor, { name: "local-fonts" }); - return { state: "prompt" }; - }, - }, - }, - async () => { - assert.deepEqual(await fontFamily.listLocalFontFamilies(), []); - }, - ); - assert.equal(queryCount, 0); - } finally { - if (previous === undefined) { - delete globalThis.queryLocalFonts; - } else { - globalThis.queryLocalFonts = previous; - } - } -}); - -test("listLocalFontFamilies uses queryLocalFonts when permission is already granted", async () => { - const previous = globalThis.queryLocalFonts; - globalThis.queryLocalFonts = async () => [ - { family: "Inter" }, - { family: "PingFang SC" }, - { family: "Inter" }, - { family: " " }, - ]; - try { - await withNavigator( - { - permissions: { - query: async () => ({ state: "granted" }), - }, - }, - async () => { - assert.deepEqual(await fontFamily.listLocalFontFamilies(), ["Inter", "PingFang SC"]); - }, - ); - } finally { - if (previous === undefined) { - delete globalThis.queryLocalFonts; - } else { - globalThis.queryLocalFonts = previous; - } - } -}); diff --git a/crates/agent-gateway/test/webui/gateway-socket-client.test.mjs b/crates/agent-gateway/test/webui/gateway-socket-client.test.mjs deleted file mode 100644 index e37d00d75..000000000 --- a/crates/agent-gateway/test/webui/gateway-socket-client.test.mjs +++ /dev/null @@ -1,1842 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { createGatewayV2Codec } from "../helpers/gateway-v2.mjs"; -import { createWebModuleLoader } from "../helpers/load-web-module.mjs"; - -// FakeWebSocket 以 v2 服务端身份说话:收发全部为二进制 protobuf 帧。 -class FakeWebSocket { - static CONNECTING = 0; - static OPEN = 1; - static CLOSING = 2; - static CLOSED = 3; - static instances = []; - - readyState = FakeWebSocket.CONNECTING; - sent = []; - binaryType = "blob"; - onopen = null; - onmessage = null; - onerror = null; - onclose = null; - - constructor(url, protocols) { - this.url = url; - this.protocols = protocols; - FakeWebSocket.instances.push(this); - } - - send(raw) { - this.sent.push(raw); - } - - open() { - this.readyState = FakeWebSocket.OPEN; - this.onopen?.(); - } - - receiveBinary(data) { - this.onmessage?.({ data }); - } - - close(event = {}) { - if (this.readyState === FakeWebSocket.CLOSED) return; - this.readyState = FakeWebSocket.CLOSED; - this.onclose?.({ - code: event.code ?? 1006, - reason: event.reason ?? "", - wasClean: event.wasClean ?? false, - }); - } -} - -function installBrowser(options = {}) { - FakeWebSocket.instances = []; - globalThis.WebSocket = FakeWebSocket; - delete globalThis.SharedWorker; - const windowListeners = new Map(); - const documentListeners = new Map(); - const addListener = (listeners, type, listener) => { - const items = listeners.get(type) ?? new Set(); - items.add(listener); - listeners.set(type, items); - }; - const removeListener = (listeners, type, listener) => { - listeners.get(type)?.delete(listener); - }; - const dispatch = (listeners, event) => { - const type = event?.type; - if (typeof type !== "string") return; - for (const listener of listeners.get(type) ?? []) { - listener(event); - } - }; - globalThis.window = { - location: { origin: "https://gateway.example" }, - setTimeout: options.setTimeout ?? setTimeout, - clearTimeout: options.clearTimeout ?? clearTimeout, - setInterval: options.setInterval ?? setInterval, - clearInterval: options.clearInterval ?? clearInterval, - addEventListener: (type, listener) => addListener(windowListeners, type, listener), - removeEventListener: (type, listener) => removeListener(windowListeners, type, listener), - dispatchEvent: (event) => { - dispatch(windowListeners, event); - return true; - }, - }; - globalThis.document = { - visibilityState: options.visibilityState ?? "visible", - addEventListener: (type, listener) => addListener(documentListeners, type, listener), - removeEventListener: (type, listener) => removeListener(documentListeners, type, listener), - dispatchEvent: (event) => { - dispatch(documentListeners, event); - return true; - }, - }; -} - -function waitFor(predicate, label) { - return new Promise((resolve, reject) => { - const startedAt = Date.now(); - const tick = () => { - if (predicate()) { - resolve(); - return; - } - if (Date.now() - startedAt > 500) { - reject(new Error(`timed out waiting for ${label}`)); - return; - } - setTimeout(tick, 0); - }; - tick(); - }); -} - -function loadGatewaySocket() { - const loader = createWebModuleLoader(); - const codec = createGatewayV2Codec(loader); - const { getGatewayWebSocketClient, resetGatewayWebSocketClient } = loader.loadModule( - "src/lib/gatewaySocket.ts", - ); - return { loader, codec, getGatewayWebSocketClient, resetGatewayWebSocketClient }; -} - -function frames(codec, socket) { - return socket.sent.map((raw) => codec.decodeClientFrame(raw)); -} - -// 查找第一条命中指定直通臂的 agent_request 帧。 -function findAgentRequest(codec, socket, arm) { - return frames(codec, socket).find( - (frame) => frame.case === "agentRequest" && frame.json.agent_request?.[arm] !== undefined, - ); -} - -function findFrame(codec, socket, frameCase) { - return frames(codec, socket).find((frame) => frame.case === frameCase); -} - -async function connectAndAuth(codec, index = 0) { - await waitFor(() => FakeWebSocket.instances.length > index, "websocket construction"); - const socket = FakeWebSocket.instances[index]; - socket.open(); - await waitFor(() => socket.sent.length >= 1, "hello frame"); - assert.equal(socket.url, "wss://gateway.example/ws/v2"); - assert.equal(socket.protocols, "liveagent.v2.pb"); - assert.equal(socket.binaryType, "arraybuffer"); - const hello = codec.decodeClientFrame(socket.sent[0]); - assert.equal(hello.case, "hello"); - assert.equal(hello.json.hello.protocol_version, 2); - assert.equal(hello.json.hello.token, "token"); - assert.equal(hello.json.hello.client_name, "webui"); - socket.receiveBinary(codec.encodeServerFrame({ request_id: hello.requestId, hello: { ok: true } })); - if (index === 0) { - await waitFor(() => findFrame(codec, socket, "agentList"), "agent_list frame"); - const listRequest = findFrame(codec, socket, "agentList"); - socket.receiveBinary( - codec.encodeServerFrame({ - request_id: listRequest.requestId, - agent_list: { agents: [{ agent_id: "desktop-agent", online: true }] }, - }), - ); - } - return socket; -} - -// history.list 现在并行发出 chat_activities 帧;用网关状态应答它。 -function answerChatActivities(codec, socket, running = [], answered = new Set()) { - for (const frame of frames(codec, socket)) { - if (frame.case !== "chatActivities" || answered.has(frame.requestId)) continue; - answered.add(frame.requestId); - socket.receiveBinary( - codec.encodeServerFrame({ - request_id: frame.requestId, - chat_activities: { running_conversations: running }, - }), - ); - } -} - -test("GatewayWebSocketClient authenticates via hello and sends status_get over /ws/v2", async () => { - installBrowser(); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient(" token "); - const statusPromise = client.getStatus(); - const socket = await connectAndAuth(codec); - await waitFor(() => findFrame(codec, socket, "statusGet"), "status_get frame"); - const statusRequest = findFrame(codec, socket, "statusGet"); - socket.receiveBinary( - codec.encodeServerFrame({ - request_id: statusRequest.requestId, - status: { online: true, agent_id: "desktop-agent" }, - }), - ); - - const status = await statusPromise; - assert.equal(status.online, true); - assert.equal(status.agent_id, "desktop-agent"); - resetGatewayWebSocketClient(); -}); - -test("GatewayWebSocketClient surfaces hello rejection as an auth error without reconnect loops", async () => { - installBrowser(); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient("token"); - const statusPromise = assert.rejects(client.getStatus(), /unauthorized/); - await waitFor(() => FakeWebSocket.instances.length === 1, "websocket construction"); - const socket = FakeWebSocket.instances[0]; - socket.open(); - await waitFor(() => socket.sent.length >= 1, "hello frame"); - const hello = codec.decodeClientFrame(socket.sent[0]); - socket.receiveBinary( - codec.encodeServerFrame({ - request_id: hello.requestId, - hello: { ok: false, message: "unauthorized" }, - }), - ); - await statusPromise; - socket.close({ code: 4401, reason: "unauthorized" }); - await new Promise((resolve) => setTimeout(resolve, 20)); - assert.equal(FakeWebSocket.instances.length, 1, "bad token must not trigger a reconnect loop"); - resetGatewayWebSocketClient(); -}); - -test("GatewayWebSocketClient bounds the complete WebSocket connection attempt", async () => { - const timers = createManualTimers(); - installBrowser({ ...timers }); - const { getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient("token"); - const statusResult = assert.rejects(client.getStatus(), /Gateway WebSocket connection timed out/); - await waitFor(() => FakeWebSocket.instances.length === 1, "connecting websocket"); - const socket = FakeWebSocket.instances[0]; - assert.equal(socket.readyState, FakeWebSocket.CONNECTING); - - timers.fire((timer) => timer.ms === 10_000); - await statusResult; - assert.equal(socket.readyState, FakeWebSocket.CLOSED, "timed-out attempt was abandoned"); - resetGatewayWebSocketClient(); -}); - -test("GatewayWebSocketClient sends chat_prepare with the caller reason", async () => { - installBrowser(); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient("token"); - const preparePromise = client.prepareChatRuntime("composer-focus"); - const socket = await connectAndAuth(codec); - await waitFor(() => findFrame(codec, socket, "chatPrepare"), "chat_prepare frame"); - const prepareRequest = findFrame(codec, socket, "chatPrepare"); - assert.deepEqual(prepareRequest.json.chat_prepare, { reason: "composer-focus" }); - socket.receiveBinary( - codec.encodeServerFrame({ - request_id: prepareRequest.requestId, - status: { online: true, chat_runtime_ready: true, runtime_state: "ready" }, - }), - ); - - const prepared = await preparePromise; - assert.equal(prepared.online, true); - assert.equal(prepared.chat_runtime_ready, true); - assert.equal(prepared.runtime_state, "ready"); - resetGatewayWebSocketClient(); -}); - -test("GatewayWebSocketClient falls back to status_get when chat_prepare is unsupported", async () => { - installBrowser(); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient("token"); - const preparePromise = client.prepareChatRuntime("foreground"); - const socket = await connectAndAuth(codec); - await waitFor(() => findFrame(codec, socket, "chatPrepare"), "chat_prepare frame"); - const prepareRequest = findFrame(codec, socket, "chatPrepare"); - socket.receiveBinary( - codec.encodeServerFrame({ - request_id: prepareRequest.requestId, - local_error: { message: "unsupported request type" }, - }), - ); - await waitFor(() => findFrame(codec, socket, "statusGet"), "fallback status_get frame"); - const statusRequest = findFrame(codec, socket, "statusGet"); - socket.receiveBinary( - codec.encodeServerFrame({ request_id: statusRequest.requestId, status: { online: true } }), - ); - - assert.equal((await preparePromise).online, true); - resetGatewayWebSocketClient(); -}); - -test("GatewayWebSocketClient clears a timed-out prepare so the next wake can retry", async () => { - const timers = createManualTimers(); - installBrowser({ ...timers }); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient("token"); - const firstResult = assert.rejects(client.prepareChatRuntime("foreground"), /Gateway/); - const firstSocket = await connectAndAuth(codec, 0); - await waitFor(() => findFrame(codec, firstSocket, "chatPrepare"), "first chat_prepare frame"); - timers.fire((timer) => timer.ms === 2_500); - await firstResult; - - const secondPrepare = client.prepareChatRuntime("send"); - const secondSocket = await connectAndAuth(codec, 1); - await waitFor(() => findFrame(codec, secondSocket, "chatPrepare"), "second chat_prepare frame"); - const secondRequest = findFrame(codec, secondSocket, "chatPrepare"); - assert.deepEqual(secondRequest.json.chat_prepare, { reason: "send" }); - secondSocket.receiveBinary( - codec.encodeServerFrame({ - request_id: secondRequest.requestId, - status: { online: true, chat_runtime_ready: true }, - }), - ); - assert.equal((await secondPrepare).chat_runtime_ready, true); - resetGatewayWebSocketClient(); -}); - -const terminalTestSession = { - id: "terminal-1", - projectPathKey: "/workspace/project", - cwd: "/workspace/project", - shell: "zsh", - title: "Terminal 1", - kind: "local", - cols: 80, - rows: 24, - createdAt: 1, - updatedAt: 2, - running: true, -}; - -const terminalProtoSession = { - id: "terminal-1", - project_path_key: "/workspace/project", - cwd: "/workspace/project", - shell: "zsh", - title: "Terminal 1", - kind: "local", - cols: 80, - rows: 24, - created_at: 1, - updated_at: 2, - running: true, -}; - -async function connectTerminalStream(codec, index = 0) { - await waitFor(() => FakeWebSocket.instances.length > index, "terminal stream socket"); - const socket = FakeWebSocket.instances[index]; - socket.open(); - await waitFor(() => socket.sent.length >= 1, "terminal stream hello"); - assert.equal(socket.url, "wss://gateway.example/ws/v2/terminal"); - assert.equal(socket.protocols, "liveagent.v2.pb"); - const hello = codec.decodeTerminalClientFrame(socket.sent[0]); - assert.equal(hello.case, "hello"); - assert.equal(hello.json.hello.token, "token"); - assert.equal(hello.json.hello.role, "CLIENT_ROLE_BROWSER"); - assert.equal(hello.json.hello.agent_id, "desktop-agent"); - socket.receiveBinary(codec.encodeTerminalServerFrame({ hello: { ok: true } })); - return socket; -} - -test("BrowserGatewayTerminalStreamClient connects to /ws/v2/terminal and attaches with protobuf frames", async () => { - installBrowser(); - const loader = createWebModuleLoader(); - const codec = createGatewayV2Codec(loader); - const { BrowserGatewayTerminalStreamClient } = loader.loadModule( - "src/lib/terminal/gatewayTerminalStreamClient.ts", - ); - - const client = new BrowserGatewayTerminalStreamClient("token", () => "desktop-agent"); - const attachPromise = client.attach(terminalTestSession, { maxBytes: 8192 }); - const socket = await connectTerminalStream(codec); - await waitFor(() => socket.sent.length >= 2, "terminal stream attach frame"); - const attach = codec.decodeTerminalClientFrame(socket.sent[1]); - assert.equal(attach.case, "frame"); - assert.equal(attach.json.frame.kind, "attach"); - assert.equal(attach.json.frame.session_id, "terminal-1"); - assert.equal(attach.json.frame.project_path_key, "/workspace/project"); - assert.equal(attach.json.frame.max_bytes, 8192); - - socket.receiveBinary( - codec.encodeTerminalServerFrame({ - frame: { - kind: "snapshot", - stream_id: attach.json.frame.stream_id, - session_id: "terminal-1", - project_path_key: "/workspace/project", - session: terminalProtoSession, - start_offset: 10, - end_offset: 13, - data: codec.base64(new Uint8Array([112, 119, 100])), - }, - }), - ); - const handle = await attachPromise; - assert.equal(handle.snapshot.session.id, "terminal-1"); - assert.deepEqual([...handle.snapshot.bytes], [112, 119, 100]); - assert.equal(handle.snapshot.outputStartOffset, 10); - handle.dispose(); - client.dispose(); -}); - -test("BrowserGatewayTerminalStreamClient retries attach while desktop stream is offline", async () => { - installBrowser(); - const loader = createWebModuleLoader(); - const codec = createGatewayV2Codec(loader); - const { BrowserGatewayTerminalStreamClient } = loader.loadModule( - "src/lib/terminal/gatewayTerminalStreamClient.ts", - ); - - const client = new BrowserGatewayTerminalStreamClient("token", () => "desktop-agent"); - const attachPromise = client.attach(terminalTestSession); - const socket = await connectTerminalStream(codec); - await waitFor(() => socket.sent.length >= 2, "terminal stream attach frame"); - const firstAttach = codec.decodeTerminalClientFrame(socket.sent[1]); - - socket.receiveBinary( - codec.encodeTerminalServerFrame({ - frame: { - kind: "error", - stream_id: firstAttach.json.frame.stream_id, - session_id: "terminal-1", - error: "desktop agent is offline", - }, - }), - ); - - await waitFor(() => socket.sent.length >= 3, "retry terminal stream attach frame"); - const retryAttach = codec.decodeTerminalClientFrame(socket.sent[2]); - assert.equal(retryAttach.json.frame.kind, "attach"); - assert.equal(retryAttach.json.frame.stream_id, firstAttach.json.frame.stream_id); - assert.equal(retryAttach.json.frame.session_id, "terminal-1"); - - socket.receiveBinary( - codec.encodeTerminalServerFrame({ - frame: { - kind: "snapshot", - stream_id: retryAttach.json.frame.stream_id, - session_id: "terminal-1", - session: terminalProtoSession, - end_offset: 2, - data: codec.base64(new Uint8Array([111, 107])), - }, - }), - ); - const handle = await attachPromise; - assert.deepEqual([...handle.snapshot.bytes], [111, 107]); - handle.dispose(); - client.dispose(); -}); - -test("BrowserGatewayTerminalStreamClient rejects the attach when the hello is refused", async () => { - installBrowser(); - const loader = createWebModuleLoader(); - const codec = createGatewayV2Codec(loader); - const { BrowserGatewayTerminalStreamClient } = loader.loadModule( - "src/lib/terminal/gatewayTerminalStreamClient.ts", - ); - - const client = new BrowserGatewayTerminalStreamClient("token", () => "desktop-agent"); - const attachPromise = assert.rejects(client.attach(terminalTestSession), /unauthorized/); - await waitFor(() => FakeWebSocket.instances.length >= 1, "terminal stream socket"); - const socket = FakeWebSocket.instances[0]; - socket.open(); - await waitFor(() => socket.sent.length >= 1, "terminal stream hello"); - socket.receiveBinary( - codec.encodeTerminalServerFrame({ hello: { ok: false, message: "unauthorized" } }), - ); - await attachPromise; - client.dispose(); -}); - -test("GatewayWebSocketClient sends git requests with workdir and args", async () => { - installBrowser(); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient(" token "); - const gitPromise = client.gitRequest("diff", "/workspace/project", { mode: "branch" }); - const socket = await connectAndAuth(codec); - await waitFor(() => findAgentRequest(codec, socket, "git_request"), "git_request frame"); - const request = findAgentRequest(codec, socket, "git_request"); - assert.equal(request.json.agent_request.git_request.action, "diff"); - assert.equal(request.json.agent_request.git_request.workdir, "/workspace/project"); - assert.deepEqual(JSON.parse(request.json.agent_request.git_request.args_json), { - mode: "branch", - }); - socket.receiveBinary( - codec.encodeServerFrame({ - request_id: request.requestId, - agent_response: { - git_response: { result_json: JSON.stringify({ patch: "diff --git a/file b/file" }) }, - }, - }), - ); - - assert.deepEqual(await gitPromise, { patch: "diff --git a/file b/file" }); - resetGatewayWebSocketClient(); -}); - -test("GatewayWebSocketClient does not recover mutating git requests", async () => { - installBrowser(); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient(" token "); - const stagePromise = client.gitRequest("stage", "/workspace/project", { path: "src/main.rs" }); - const socket = await connectAndAuth(codec); - await waitFor(() => findAgentRequest(codec, socket, "git_request"), "git_request frame"); - assert.equal(findAgentRequest(codec, socket, "git_request").json.agent_request.git_request.action, "stage"); - socket.close({ code: 1006, wasClean: false }); - - await assert.rejects(stagePromise, /Gateway WebSocket disconnected/); - assert.equal(FakeWebSocket.instances.length, 1); - resetGatewayWebSocketClient(); -}); - -test("GatewayWebSocketClient sends mention query payloads", async () => { - installBrowser(); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient("token"); - const mentionPromise = client.listMentionFiles("/workspace", 200, "src"); - const socket = await connectAndAuth(codec); - await waitFor(() => findAgentRequest(codec, socket, "file_mention_list"), "mentions frame"); - const request = findAgentRequest(codec, socket, "file_mention_list"); - assert.deepEqual(request.json.agent_request.file_mention_list, { - workdir: "/workspace", - max_results: 200, - query: "src", - }); - socket.receiveBinary( - codec.encodeServerFrame({ - request_id: request.requestId, - agent_response: { - file_mention_list_resp: { - entries: [{ path: "src/main.ts", kind: "file" }], - truncated: false, - }, - }, - }), - ); - - assert.deepEqual(await mentionPromise, { - entries: [{ path: "src/main.ts", kind: "file", hidden: false }], - truncated: false, - }); - resetGatewayWebSocketClient(); -}); - -test("GatewayWebSocketClient sends memory manage payloads", async () => { - installBrowser(); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient("token"); - const memoryPromise = client.memoryManage({ - command: "memory_search", - args: { query: "Kevin", limit: 3 }, - }); - const socket = await connectAndAuth(codec); - await waitFor(() => findAgentRequest(codec, socket, "memory_manage"), "memory frame"); - const request = findAgentRequest(codec, socket, "memory_manage"); - assert.equal(request.json.agent_request.memory_manage.command, "memory_search"); - assert.deepEqual(JSON.parse(request.json.agent_request.memory_manage.args_json), { - query: "Kevin", - limit: 3, - }); - socket.receiveBinary( - codec.encodeServerFrame({ - request_id: request.requestId, - agent_response: { - memory_manage_resp: { - result_json: JSON.stringify({ matches: [], usedFallback: false }), - }, - }, - }), - ); - - assert.deepEqual(await memoryPromise, { matches: [], usedFallback: false }); - resetGatewayWebSocketClient(); -}); - -test("GatewayWebSocketClient retries recoverable memory manage commands after a clean disconnect", async () => { - installBrowser(); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient("token"); - const updatePromise = client.memoryManage({ - command: "memory_organize_run_update", - args: { - runId: "run-1", - safeApplied: 2, - trimmedProtocol: { - manualApplyState: { status: "applied" }, - }, - }, - }); - - const firstSocket = await connectAndAuth(codec, 0); - await waitFor( - () => findAgentRequest(codec, firstSocket, "memory_manage"), - "initial memory update frame", - ); - const firstRequest = findAgentRequest(codec, firstSocket, "memory_manage"); - assert.equal(firstRequest.json.agent_request.memory_manage.command, "memory_organize_run_update"); - - firstSocket.close({ code: 1000, wasClean: true }); - await waitFor(() => FakeWebSocket.instances.length === 2, "memory update recovery websocket"); - const reconnectSocket = await connectAndAuth(codec, 1); - await waitFor( - () => findAgentRequest(codec, reconnectSocket, "memory_manage"), - "retried memory update frame", - ); - - const retriedRequest = findAgentRequest(codec, reconnectSocket, "memory_manage"); - assert.deepEqual( - retriedRequest.json.agent_request.memory_manage, - firstRequest.json.agent_request.memory_manage, - ); - const payload = { - runId: "run-1", - status: "succeeded", - trimmedProtocol: { - manualApplyState: { status: "applied" }, - }, - }; - reconnectSocket.receiveBinary( - codec.encodeServerFrame({ - request_id: retriedRequest.requestId, - agent_response: { memory_manage_resp: { result_json: JSON.stringify(payload) } }, - }), - ); - - assert.deepEqual(await updatePromise, payload); - resetGatewayWebSocketClient(); -}); - -test("GatewayWebSocketClient does not replay memory apply batch after a disconnect", async () => { - installBrowser(); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient("token"); - const applyPromise = client.memoryManage({ - command: "memory_apply_batch", - args: { - trigger: "memory-organize", - decisions: [ - { - op: "delete", - slug: "stale-memory", - scope: "project", - }, - ], - }, - }); - - const socket = await connectAndAuth(codec, 0); - await waitFor(() => findAgentRequest(codec, socket, "memory_manage"), "memory apply frame"); - socket.close({ code: 1000, wasClean: true }); - - await assert.rejects(applyPromise, /Gateway WebSocket disconnected \(code=1000 clean=true\)/); - assert.equal(FakeWebSocket.instances.length, 1); - resetGatewayWebSocketClient(); -}); - -test("GatewayWebSocketClient sends skill manage payloads", async () => { - installBrowser(); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient("token"); - const skillPromise = client.manageSkill({ action: "list" }); - const socket = await connectAndAuth(codec); - await waitFor(() => findAgentRequest(codec, socket, "skill_manage"), "skill manage frame"); - const request = findAgentRequest(codec, socket, "skill_manage"); - assert.deepEqual(JSON.parse(request.json.agent_request.skill_manage.payload_json), { - action: "list", - }); - socket.receiveBinary( - codec.encodeServerFrame({ - request_id: request.requestId, - agent_response: { - skill_manage_resp: { - result_json: JSON.stringify({ - action: "list", - rootDir: "/Users/me/.liveagent/skills", - skills: [], - }), - }, - }, - }), - ); - - assert.deepEqual(await skillPromise, { - action: "list", - rootDir: "/Users/me/.liveagent/skills", - skills: [], - }); - resetGatewayWebSocketClient(); -}); - -test("GatewayWebSocketClient sends history list requests and merges running conversations", async () => { - installBrowser(); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient("token"); - const listPromise = client.listHistory(2, 50); - const socket = await connectAndAuth(codec); - await waitFor(() => findAgentRequest(codec, socket, "history_list"), "history list frame"); - const listRequest = findAgentRequest(codec, socket, "history_list"); - assert.deepEqual(listRequest.json.agent_request.history_list, { page: 2, page_size: 50 }); - await waitFor(() => findFrame(codec, socket, "chatActivities"), "chat_activities frame"); - answerChatActivities(codec, socket, [ - { - conversation_id: "conversation-running", - run_id: "run-running", - state: "running", - workdir: "/tmp/project-a", - updated_at_ms: 123, - }, - ]); - socket.receiveBinary( - codec.encodeServerFrame({ - request_id: listRequest.requestId, - agent_response: { history_list_resp: { conversations: [], total_count: 0 } }, - }), - ); - assert.deepEqual(await listPromise, { - conversations: [], - total_count: 0, - running_conversations: [ - { - conversation_id: "conversation-running", - run_id: "run-running", - state: "running", - cwd: "/tmp/project-a", - updated_at: 123, - }, - ], - }); - - const sharedListPromise = client.listSharedHistory(1, 25); - await waitFor(() => findAgentRequest(codec, socket, "memory_manage"), "shared history frame"); - const sharedRequest = findAgentRequest(codec, socket, "memory_manage"); - assert.equal(sharedRequest.json.agent_request.memory_manage.command, "history_shared_list"); - assert.deepEqual(JSON.parse(sharedRequest.json.agent_request.memory_manage.args_json), { - page: 1, - page_size: 25, - }); - socket.receiveBinary( - codec.encodeServerFrame({ - request_id: sharedRequest.requestId, - agent_response: { - memory_manage_resp: { - result_json: JSON.stringify({ conversations: [], total_count: 0 }), - }, - }, - }), - ); - assert.deepEqual(await sharedListPromise, { conversations: [], total_count: 0 }); - - resetGatewayWebSocketClient(); -}); - -test("GatewayWebSocketClient sends project-aware history and fs requests", async () => { - installBrowser(); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const answeredActivities = new Set(); - const client = getGatewayWebSocketClient("token"); - const filteredListPromise = client.listHistory(3, 25, { cwd: "/tmp/project-a" }); - const socket = await connectAndAuth(codec); - await waitFor(() => findAgentRequest(codec, socket, "history_list"), "filtered history frame"); - const filteredRequest = findAgentRequest(codec, socket, "history_list"); - assert.deepEqual(filteredRequest.json.agent_request.history_list, { - page: 3, - page_size: 25, - cwd: "/tmp/project-a", - }); - answerChatActivities(codec, socket, [], answeredActivities); - socket.receiveBinary( - codec.encodeServerFrame({ - request_id: filteredRequest.requestId, - agent_response: { history_list_resp: { conversations: [], total_count: 0 } }, - }), - ); - assert.deepEqual(await filteredListPromise, { - conversations: [], - total_count: 0, - running_conversations: [], - }); - - const chatModeListPromise = client.listHistory(1, 80, { cwdEmpty: true }); - await waitFor( - () => - frames(codec, socket).filter( - (frame) => frame.case === "agentRequest" && frame.json.agent_request?.history_list, - ).length >= 2, - "cwd empty history frame", - ); - const chatModeRequest = frames(codec, socket) - .filter((frame) => frame.case === "agentRequest" && frame.json.agent_request?.history_list) - .at(-1); - assert.deepEqual(chatModeRequest.json.agent_request.history_list, { - page: 1, - page_size: 80, - cwd_empty: true, - }); - answerChatActivities(codec, socket, [], answeredActivities); - socket.receiveBinary( - codec.encodeServerFrame({ - request_id: chatModeRequest.requestId, - agent_response: { history_list_resp: { conversations: [], total_count: 0 } }, - }), - ); - assert.deepEqual(await chatModeListPromise, { - conversations: [], - total_count: 0, - running_conversations: [], - }); - - const workdirsPromise = client.listHistoryWorkdirs(); - await waitFor(() => findAgentRequest(codec, socket, "history_workdirs"), "history workdirs frame"); - const workdirsRequest = findAgentRequest(codec, socket, "history_workdirs"); - socket.receiveBinary( - codec.encodeServerFrame({ - request_id: workdirsRequest.requestId, - agent_response: { - history_workdirs_resp: { - workdirs: [ - { path: "/tmp/project-a", conversation_count: 2, updated_at: 1700000000300 }, - ], - }, - }, - }), - ); - assert.deepEqual(await workdirsPromise, { - workdirs: [{ path: "/tmp/project-a", conversationCount: 2, updatedAt: 1700000000300 }], - }); - - const createPromise = client.createProjectFolder("/tmp", "Project A"); - await waitFor( - () => findAgentRequest(codec, socket, "fs_create_project_folder"), - "create project folder frame", - ); - const createRequest = findAgentRequest(codec, socket, "fs_create_project_folder"); - assert.deepEqual(createRequest.json.agent_request.fs_create_project_folder, { - parent: "/tmp", - name: "Project A", - }); - socket.receiveBinary( - codec.encodeServerFrame({ - request_id: createRequest.requestId, - agent_response: { fs_create_project_folder_resp: { path: "/tmp/Project A" } }, - }), - ); - assert.deepEqual(await createPromise, { path: "/tmp/Project A" }); - - resetGatewayWebSocketClient(); -}); - -test("GatewayWebSocketClient defaults invalid history pagination", async () => { - installBrowser(); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient("token"); - const listPromise = client.listHistory(0, 0); - const socket = await connectAndAuth(codec); - await waitFor(() => findAgentRequest(codec, socket, "history_list"), "history list frame"); - const listRequest = findAgentRequest(codec, socket, "history_list"); - assert.deepEqual(listRequest.json.agent_request.history_list, { page: 1, page_size: 80 }); - answerChatActivities(codec, socket); - socket.receiveBinary( - codec.encodeServerFrame({ - request_id: listRequest.requestId, - agent_response: { history_list_resp: { conversations: [], total_count: 0 } }, - }), - ); - assert.deepEqual(await listPromise, { - conversations: [], - total_count: 0, - running_conversations: [], - }); - - const sharedListPromise = client.listSharedHistory(Number.NaN, 500); - await waitFor(() => findAgentRequest(codec, socket, "memory_manage"), "shared list frame"); - const sharedRequest = findAgentRequest(codec, socket, "memory_manage"); - assert.deepEqual(JSON.parse(sharedRequest.json.agent_request.memory_manage.args_json), { - page: 1, - page_size: 200, - }); - socket.receiveBinary( - codec.encodeServerFrame({ - request_id: sharedRequest.requestId, - agent_response: { - memory_manage_resp: { - result_json: JSON.stringify({ conversations: [], total_count: 0 }), - }, - }, - }), - ); - assert.deepEqual(await sharedListPromise, { conversations: [], total_count: 0 }); - - resetGatewayWebSocketClient(); -}); - -test("GatewayWebSocketClient sends history share requests", async () => { - installBrowser(); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient("token"); - const getPromise = client.getHistoryShare("conversation-1"); - const socket = await connectAndAuth(codec); - await waitFor(() => findAgentRequest(codec, socket, "history_share_get"), "share get frame"); - const getRequest = findAgentRequest(codec, socket, "history_share_get"); - assert.deepEqual(getRequest.json.agent_request.history_share_get, { - conversation_id: "conversation-1", - }); - socket.receiveBinary( - codec.encodeServerFrame({ - request_id: getRequest.requestId, - agent_response: { - history_share_get_resp: { - share: { conversation_id: "conversation-1", enabled: false }, - }, - }, - }), - ); - assert.deepEqual(await getPromise, { - conversation_id: "conversation-1", - enabled: false, - token: "", - created_at: 0, - updated_at: 0, - redact_tool_content: false, - }); - - const setPromise = client.setHistoryShare("conversation-1", true, { - redactToolContent: true, - }); - await waitFor(() => findAgentRequest(codec, socket, "history_share_set"), "share set frame"); - const setRequest = findAgentRequest(codec, socket, "history_share_set"); - assert.deepEqual(setRequest.json.agent_request.history_share_set, { - conversation_id: "conversation-1", - enabled: true, - redact_tool_content: true, - }); - socket.receiveBinary( - codec.encodeServerFrame({ - request_id: setRequest.requestId, - agent_response: { - history_share_set_resp: { - share: { - conversation_id: "conversation-1", - enabled: true, - token: "share-token", - created_at: 10, - updated_at: 20, - redact_tool_content: true, - }, - }, - }, - }), - ); - assert.deepEqual(await setPromise, { - conversation_id: "conversation-1", - enabled: true, - token: "share-token", - created_at: 10, - updated_at: 20, - redact_tool_content: true, - }); - - resetGatewayWebSocketClient(); -}); - -test("GatewayWebSocketClient sends history branch requests with the base message ref", async () => { - installBrowser(); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient("token"); - const branchPromise = client.branchHistory("conversation-1", { - segmentIndex: 2, - messageIndex: 5, - segmentId: "segment-2", - messageId: "message-5", - role: "user", - contentHash: "hash-abc", - }); - const socket = await connectAndAuth(codec); - await waitFor(() => findAgentRequest(codec, socket, "history_branch"), "history branch frame"); - const request = findAgentRequest(codec, socket, "history_branch"); - assert.deepEqual(request.json.agent_request.history_branch, { - conversation_id: "conversation-1", - base_message_ref: { - segment_index: 2, - message_index: 5, - segment_id: "segment-2", - message_id: "message-5", - role: "user", - content_hash: "hash-abc", - }, - }); - socket.receiveBinary( - codec.encodeServerFrame({ - request_id: request.requestId, - agent_response: { - history_branch_resp: { - conversation: { - id: "conversation-branch", - title: "新分支", - message_count: 6, - created_at: 1700000000100, - updated_at: 1700000000200, - }, - }, - }, - }), - ); - const branched = await branchPromise; - assert.equal(branched.id, "conversation-branch"); - assert.equal(branched.title, "新分支"); - assert.equal(branched.message_count, 6); - assert.equal(branched.created_at, 1700000000100); - assert.equal(branched.updated_at, 1700000000200); - - resetGatewayWebSocketClient(); -}); - -test("GatewayWebSocketClient reconnects before read requests when an authenticated socket goes stale", async () => { - installBrowser(); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const realDateNow = Date.now; - try { - const client = getGatewayWebSocketClient("token"); - const statusPromise = client.getStatus(); - const firstSocket = await connectAndAuth(codec); - await waitFor(() => findFrame(codec, firstSocket, "statusGet"), "initial status_get"); - const statusRequest = findFrame(codec, firstSocket, "statusGet"); - firstSocket.receiveBinary( - codec.encodeServerFrame({ - request_id: statusRequest.requestId, - status: { online: true, agent_id: "desktop-agent" }, - }), - ); - await statusPromise; - - let mockNow = realDateNow(); - Date.now = () => mockNow; - mockNow += 46_000; - - const historyPromise = client.getHistory("conversation-1"); - assert.equal(FakeWebSocket.instances.length, 2); - - Date.now = realDateNow; - - const reconnectSocket = await connectAndAuth(codec, 1); - await waitFor( - () => findAgentRequest(codec, reconnectSocket, "history_get"), - "history request after stale reconnect", - ); - - const historyRequest = findAgentRequest(codec, reconnectSocket, "history_get"); - assert.deepEqual(historyRequest.json.agent_request.history_get, { - conversation_id: "conversation-1", - }); - - reconnectSocket.receiveBinary( - codec.encodeServerFrame({ - request_id: historyRequest.requestId, - agent_response: { - history_get_resp: { conversation_id: "conversation-1", messages_json: "[]" }, - }, - }), - ); - - assert.deepEqual(await historyPromise, { - conversation_id: "conversation-1", - messages_json: "[]", - total_message_count: 0, - returned_message_count: 0, - has_more: false, - conversation: null, - }); - } finally { - Date.now = realDateNow; - resetGatewayWebSocketClient(); - } -}); - -test("GatewayWebSocketClient retries history.get after a recoverable transport stall timeout", async () => { - const realSetTimeout = setTimeout; - installBrowser({ - setTimeout: (fn, delay, ...args) => realSetTimeout(fn, delay >= 30_000 ? 0 : delay, ...args), - }); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient("token"); - const historyPromise = client.getHistory("conversation-1"); - const firstSocket = await connectAndAuth(codec); - await waitFor( - () => findAgentRequest(codec, firstSocket, "history_get"), - "initial history_get frame", - ); - - await waitFor(() => FakeWebSocket.instances.length === 2, "timeout recovery websocket"); - const reconnectSocket = await connectAndAuth(codec, 1); - await waitFor( - () => findAgentRequest(codec, reconnectSocket, "history_get"), - "retried history_get frame", - ); - - const historyRequest = findAgentRequest(codec, reconnectSocket, "history_get"); - reconnectSocket.receiveBinary( - codec.encodeServerFrame({ - request_id: historyRequest.requestId, - agent_response: { - history_get_resp: { conversation_id: "conversation-1", messages_json: "[]" }, - }, - }), - ); - - assert.equal((await historyPromise).conversation_id, "conversation-1"); - resetGatewayWebSocketClient(); -}); - -test("GatewayWebSocketClient suppresses transient recoverable disconnect status errors", async () => { - installBrowser(); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient("token"); - const statusEvents = []; - const unsubscribe = client.subscribeStatus((status, error) => { - statusEvents.push({ status, error }); - }); - const socket = await connectAndAuth(codec); - await waitFor(() => findFrame(codec, socket, "statusGet"), "status frame"); - const statusRequest = findFrame(codec, socket, "statusGet"); - socket.receiveBinary( - codec.encodeServerFrame({ - request_id: statusRequest.requestId, - status: { online: true, agent_id: "desktop-agent" }, - }), - ); - await waitFor( - () => statusEvents.some((event) => event.status?.online === true), - "online status event", - ); - - socket.close(); - await new Promise((resolve) => setTimeout(resolve, 0)); - - assert.equal( - statusEvents.some((event) => - String(event.error ?? "").includes("Gateway WebSocket disconnected"), - ), - false, - ); - - unsubscribe(); - resetGatewayWebSocketClient(); -}); - -test("GatewayWebSocketClient preserves client names across live status updates", async () => { - installBrowser(); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient("token"); - const snapshots = []; - const unsubscribe = client.subscribeAgents((agents) => snapshots.push(agents)); - const initialList = client.listAgents(); - const socket = await connectAndAuth(codec); - await initialList; - - const refreshedList = client.listAgents(); - await waitFor( - () => frames(codec, socket).filter((frame) => frame.case === "agentList").length >= 2, - "second agent_list frame", - ); - const listRequests = frames(codec, socket).filter((frame) => frame.case === "agentList"); - const listRequest = listRequests[listRequests.length - 1]; - socket.receiveBinary( - codec.encodeServerFrame({ - request_id: listRequest.requestId, - agent_list: { - agents: [{ agent_id: "desktop-agent", online: true, name: "Office desktop" }], - }, - }), - ); - await refreshedList; - - socket.receiveBinary( - codec.encodeServerFrame({ - agent_id: "desktop-agent", - status: { agent_id: "desktop-agent", online: false }, - }), - ); - await waitFor( - () => - snapshots.some( - (agents) => agents[0]?.online === false && agents[0]?.name === "Office desktop", - ), - "status update with preserved name", - ); - - unsubscribe(); - resetGatewayWebSocketClient(); -}); - -test("GatewayWebSocketClient replies to app-level pings with pong frames", async () => { - installBrowser(); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient("token"); - const statusPromise = client.getStatus(); - const socket = await connectAndAuth(codec); - socket.receiveBinary(codec.encodeServerFrame({ ping: { timestamp: 123 } })); - await waitFor(() => findFrame(codec, socket, "pong"), "pong frame"); - const pong = findFrame(codec, socket, "pong"); - assert.equal(Number(pong.json.pong.timestamp), 123); - - await waitFor(() => findFrame(codec, socket, "statusGet"), "status frame"); - const statusRequest = findFrame(codec, socket, "statusGet"); - socket.receiveBinary( - codec.encodeServerFrame({ request_id: statusRequest.requestId, status: { online: true } }), - ); - await statusPromise; - resetGatewayWebSocketClient(); -}); - -test("GatewayWebSocketClient chatCommand sends the command frame and parses the accept response", async () => { - installBrowser(); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient("token"); - const commandPromise = client.chatCommand({ - type: "chat.submit", - message: "hello", - conversationId: "conversation-1", - clientRequestId: "req-1", - queuePolicy: "append", - systemSettings: { - executionMode: "agent", - workdir: "/workspace/project", - }, - }); - const socket = await connectAndAuth(codec); - await waitFor(() => findFrame(codec, socket, "chatCommand"), "chat command frame"); - const command = findFrame(codec, socket, "chatCommand"); - assert.equal(command.json.chat_command.type, "chat.submit"); - assert.equal(command.json.chat_command.request.message, "hello"); - assert.equal(command.json.chat_command.request.conversation_id, "conversation-1"); - assert.equal(command.json.chat_command.request.client_request_id, "req-1"); - assert.equal(command.json.chat_command.request.queue_policy, "append"); - assert.equal(command.json.chat_command.request.workdir, "/workspace/project"); - - socket.receiveBinary( - codec.encodeServerFrame({ - request_id: command.requestId, - chat_accepted: { run_id: " run-1 ", conversation_id: "conversation-1", accepted_seq: 7 }, - }), - ); - assert.deepEqual(await commandPromise, { - runId: "run-1", - conversationId: "conversation-1", - acceptedSeq: 7, - }); - resetGatewayWebSocketClient(); -}); - -test("GatewayWebSocketClient reconnects once and retries chatCommand with the same payload", async () => { - installBrowser(); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient("token"); - const commandPromise = client.chatCommand({ - type: "chat.submit", - message: "retry me", - conversationId: "conversation-1", - clientRequestId: "client-retry-1", - }); - const firstSocket = await connectAndAuth(codec, 0); - await waitFor(() => findFrame(codec, firstSocket, "chatCommand"), "first chat command frame"); - const firstCommand = findFrame(codec, firstSocket, "chatCommand"); - firstSocket.close({ code: 1006, wasClean: false }); - - const secondSocket = await connectAndAuth(codec, 1); - await waitFor(() => findFrame(codec, secondSocket, "chatCommand"), "retried chat command frame"); - const retriedCommand = findFrame(codec, secondSocket, "chatCommand"); - assert.deepEqual(retriedCommand.json.chat_command, firstCommand.json.chat_command); - assert.equal( - retriedCommand.json.chat_command.request.client_request_id, - "client-retry-1", - "retry preserves the idempotency key", - ); - secondSocket.receiveBinary( - codec.encodeServerFrame({ - request_id: retriedCommand.requestId, - chat_accepted: { - run_id: "run-canonical", - conversation_id: "conversation-1", - accepted_seq: 3, - }, - }), - ); - - assert.equal((await commandPromise).runId, "run-canonical"); - assert.equal(FakeWebSocket.instances.length, 2, "only one transparent retry is attempted"); - resetGatewayWebSocketClient(); -}); - -test("GatewayWebSocketClient uses a short ACK timeout and preserves a generated client id", async () => { - const timers = createManualTimers(); - installBrowser({ ...timers }); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient("token"); - const commandPromise = client.chatCommand({ - type: "chat.submit", - message: "timeout retry", - conversationId: "conversation-1", - }); - const firstSocket = await connectAndAuth(codec, 0); - await waitFor(() => findFrame(codec, firstSocket, "chatCommand"), "first chat command frame"); - const firstCommand = findFrame(codec, firstSocket, "chatCommand"); - const generatedClientRequestId = firstCommand.json.chat_command.request.client_request_id; - assert.match(generatedClientRequestId, /^webui-chat\.submit-/); - - timers.fire((timer) => timer.ms === 4_000); - const secondSocket = await connectAndAuth(codec, 1); - await waitFor(() => findFrame(codec, secondSocket, "chatCommand"), "ACK-timeout retry frame"); - const retriedCommand = findFrame(codec, secondSocket, "chatCommand"); - assert.equal( - retriedCommand.json.chat_command.request.client_request_id, - generatedClientRequestId, - ); - assert.deepEqual(retriedCommand.json.chat_command, firstCommand.json.chat_command); - secondSocket.receiveBinary( - codec.encodeServerFrame({ - request_id: retriedCommand.requestId, - chat_accepted: { run_id: "run-timeout", conversation_id: "conversation-1", accepted_seq: 1 }, - }), - ); - - assert.equal((await commandPromise).runId, "run-timeout"); - resetGatewayWebSocketClient(); -}); - -test("a detached socket's late close cannot tear down its replacement", async () => { - installBrowser(); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient("token"); - const firstStatus = client.getStatus(); - const firstSocket = await connectAndAuth(codec, 0); - await waitFor(() => findFrame(codec, firstSocket, "statusGet"), "first status request"); - const firstStatusRequest = findFrame(codec, firstSocket, "statusGet"); - firstSocket.receiveBinary( - codec.encodeServerFrame({ request_id: firstStatusRequest.requestId, status: { online: true } }), - ); - await firstStatus; - - const lateClose = firstSocket.onclose; - firstSocket.close({ code: 1006, wasClean: false }); - const secondStatus = client.getStatus(); - const secondSocket = await connectAndAuth(codec, 1); - await waitFor(() => findFrame(codec, secondSocket, "statusGet"), "replacement status request"); - lateClose?.({ code: 1006, reason: "late old close", wasClean: false }); - const secondStatusRequest = findFrame(codec, secondSocket, "statusGet"); - secondSocket.receiveBinary( - codec.encodeServerFrame({ - request_id: secondStatusRequest.requestId, - status: { online: true, session_id: "replacement" }, - }), - ); - - assert.equal((await secondStatus).session_id, "replacement"); - assert.equal(FakeWebSocket.instances.length, 2); - resetGatewayWebSocketClient(); -}); - -test("GatewayWebSocketClient cancelChat sends a cancel chat command with conversation and run ids", async () => { - installBrowser(); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient("token"); - const cancelPromise = client.cancelChat(" conversation-1 ", " run-9 "); - const socket = await connectAndAuth(codec); - await waitFor(() => findFrame(codec, socket, "chatCommand"), "chat cancel frame"); - const cancelFrame = findFrame(codec, socket, "chatCommand"); - assert.equal(cancelFrame.json.chat_command.type, "chat.cancel"); - assert.deepEqual(cancelFrame.json.chat_command.cancel, { - conversation_id: "conversation-1", - run_id: "run-9", - }); - socket.receiveBinary( - codec.encodeServerFrame({ - request_id: cancelFrame.requestId, - chat_cancelled: { ok: true, run_id: "run-9", conversation_id: "conversation-1" }, - }), - ); - await cancelPromise; - resetGatewayWebSocketClient(); -}); - -test("GatewayWebSocketClient conversation subscriptions subscribe after auth, route pushes, and survive reconnects", async () => { - installBrowser(); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient("token"); - const seen = { syncs: [], events: [] }; - const cleanup = client.subscribeConversationStream("conversation-1", { - onSync: (result) => seen.syncs.push(result), - onEvent: (event) => seen.events.push(event), - }); - - // 鉴权是唯一的连接通知点;对每条 chat_subscribe 帧按调用方游标 + 暂存的 - // 重放事件应答。 - const answeredSubscribes = new Set(); - let replayEvents = []; - const subscribeCalls = []; - const answerSubscribes = (socket) => { - for (const frame of frames(codec, socket)) { - if (frame.case !== "chatSubscribe" || answeredSubscribes.has(frame.requestId)) { - continue; - } - answeredSubscribes.add(frame.requestId); - const payload = { - conversation_id: frame.json.chat_subscribe.conversation_id ?? "", - after_seq: Number(frame.json.chat_subscribe.after_seq ?? 0), - stream_epoch: frame.json.chat_subscribe.stream_epoch ?? "", - }; - subscribeCalls.push(payload); - const events = replayEvents; - replayEvents = []; - const latestSeq = events.length - ? events[events.length - 1].seq - : Math.max(payload.after_seq, 2); - socket.receiveBinary( - codec.encodeServerFrame({ - request_id: frame.requestId, - chat_subscribed: { - conversation_id: "conversation-1", - stream_epoch: "epoch-1", - latest_seq: latestSeq, - events_json: events.map((event) => codec.base64(event)), - }, - }), - ); - } - }; - const settle = async (socket) => { - for (let i = 0; i < 20; i += 1) { - answerSubscribes(socket); - await new Promise((resolve) => setTimeout(resolve, 5)); - } - }; - - // 鉴权完成 → 持久订阅发出 chat_subscribe。 - const socket = await connectAndAuth(codec); - await waitFor(() => findFrame(codec, socket, "chatSubscribe"), "chat_subscribe"); - assert.equal(subscribeCalls.length, 0); - await settle(socket); - assert.ok(seen.syncs.length >= 1, "subscribe sync delivered"); - assert.equal(subscribeCalls.length, 1, "initial auth issues exactly one subscribe"); - assert.equal(subscribeCalls[0].conversation_id, "conversation-1"); - assert.equal(subscribeCalls[0].after_seq, 0); - - // chat_event 推送按会话 id 路由。 - const pushChatEvent = (target, payload) => { - target.receiveBinary( - codec.encodeServerFrame({ - chat_event: { - conversation_id: payload.conversation_id, - seq: payload.seq, - payload_json: codec.base64(payload), - }, - }), - ); - }; - pushChatEvent(socket, { - type: "run_started", - conversation_id: "conversation-1", - run_id: "run-1", - seq: 3, - }); - pushChatEvent(socket, { - type: "token", - conversation_id: "conversation-1", - run_id: "run-1", - seq: 4, - text: "hi", - }); - pushChatEvent(socket, { - type: "token", - conversation_id: "conversation-other", - run_id: "run-x", - seq: 9, - text: "ignored", - }); - await settle(socket); - assert.deepEqual( - seen.events.map((event) => event.type), - ["run_started", "token"], - ); - - // 断线保留登记;重连按 resume 游标与 epoch 重新订阅。 - const syncsBeforeReconnect = seen.syncs.length; - replayEvents = [ - { type: "token", conversation_id: "conversation-1", run_id: "run-1", seq: 5, text: "re" }, - { - type: "run_finished", - conversation_id: "conversation-1", - run_id: "run-1", - seq: 6, - status: "completed", - }, - ]; - const subscribesBeforeReconnect = subscribeCalls.length; - socket.close(); - await new Promise((resolve, reject) => { - const startedAt = Date.now(); - const tick = () => { - if (FakeWebSocket.instances.length >= 2) { - resolve(); - return; - } - if (Date.now() - startedAt > 3_000) { - reject(new Error("timed out waiting for reconnect socket")); - return; - } - setTimeout(tick, 10); - }; - tick(); - }); - const reconnectSocket = await connectAndAuth(codec, 1); - await settle(reconnectSocket); - assert.ok(seen.syncs.length > syncsBeforeReconnect, "resume sync delivered"); - assert.equal( - subscribeCalls.length, - subscribesBeforeReconnect + 1, - "each reconnect issues exactly one resume subscribe", - ); - const resumePayload = subscribeCalls[subscribesBeforeReconnect]; - assert.equal(resumePayload.after_seq, 4, "resume cursor from last delivered seq"); - assert.equal(resumePayload.stream_epoch, "epoch-1"); - const resumeSync = seen.syncs[seen.syncs.length - 1]; - assert.deepEqual( - resumeSync.events.map((event) => event.type), - ["token", "run_finished"], - "replayed events delivered with the resume sync", - ); - - // chat_subscription_reset 触发从游标再同步。 - const subscribesBeforeReset = subscribeCalls.length; - reconnectSocket.receiveBinary( - codec.encodeServerFrame({ - chat_subscription_reset: { conversation_id: "conversation-1" }, - }), - ); - await settle(reconnectSocket); - assert.ok(subscribeCalls.length > subscribesBeforeReset, "reset re-subscribed"); - assert.equal(subscribeCalls[subscribesBeforeReset].after_seq, 6); - - // 清理时在线退订。 - cleanup(); - await waitFor(() => findFrame(codec, reconnectSocket, "chatUnsubscribe"), "chat_unsubscribe"); - resetGatewayWebSocketClient(); -}); - -test("GatewayWebSocketClient fans chat_activity and chat_command_update out to listeners", async () => { - installBrowser(); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient("token"); - const activityEvents = []; - const commandUpdates = []; - client.subscribeChatActivity((event) => activityEvents.push(event)); - client.subscribeChatCommandUpdates((update) => commandUpdates.push(update)); - - const statusPromise = client.getStatus(); - const socket = await connectAndAuth(codec); - await waitFor(() => findFrame(codec, socket, "statusGet"), "status frame"); - const statusRequest = findFrame(codec, socket, "statusGet"); - socket.receiveBinary( - codec.encodeServerFrame({ request_id: statusRequest.requestId, status: { online: true } }), - ); - await statusPromise; - - socket.receiveBinary( - codec.encodeServerFrame({ - chat_activity: { - conversation_id: "conversation-1", - run_id: "run-1", - running: true, - state: "running", - workdir: "/workspace/project", - client_request_id: "req-42", - updated_at_ms: 1234, - }, - }), - ); - socket.receiveBinary( - codec.encodeServerFrame({ - chat_activity: { conversation_id: "", running: true }, - }), - ); - assert.equal(activityEvents.length, 1, "malformed activity payloads are dropped"); - assert.deepEqual(activityEvents[0], { - conversationId: "conversation-1", - runId: "run-1", - running: true, - state: "running", - workdir: "/workspace/project", - clientRequestId: "req-42", - updatedAt: 1234, - }); - - socket.receiveBinary( - codec.encodeServerFrame({ - chat_command_update: { - run_id: "run-1", - client_request_id: "req-1", - conversation_id: "conversation-9", - phase: "bound", - }, - }), - ); - socket.receiveBinary( - codec.encodeServerFrame({ - chat_command_update: { run_id: "run-1", phase: "unknown-phase" }, - }), - ); - assert.equal(commandUpdates.length, 1, "unknown phases are dropped"); - assert.deepEqual(commandUpdates[0], { - runId: "run-1", - clientRequestId: "req-1", - conversationId: "conversation-9", - phase: "bound", - errorCode: null, - message: null, - }); - resetGatewayWebSocketClient(); -}); - -function createManualTimers() { - const timers = new Map(); - let nextId = 1; - return { - setTimeout: (fn, ms = 0) => { - const id = nextId++; - timers.set(id, { fn, ms, kind: "timeout" }); - return id; - }, - clearTimeout: (id) => { - timers.delete(id); - }, - setInterval: (fn, ms = 0) => { - const id = nextId++; - timers.set(id, { fn, ms, kind: "interval" }); - return id; - }, - clearInterval: (id) => { - timers.delete(id); - }, - fire: (predicate) => { - for (const [id, timer] of [...timers]) { - if (!predicate(timer)) continue; - if (timer.kind === "timeout") timers.delete(id); - timer.fn(); - } - }, - delays: () => [...timers.values()].map((timer) => timer.ms), - }; -} - -test("GatewayWebSocketClient applies pushed status frames and polls slowly as fallback", async () => { - const timers = createManualTimers(); - installBrowser({ ...timers }); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient("token"); - const statuses = []; - client.subscribeStatus((status, error) => { - statuses.push({ status, error }); - }); - assert.ok( - timers.delays().includes(30_000), - `status poll interval registered at 30s, got delays ${JSON.stringify(timers.delays())}`, - ); - - const socket = await connectAndAuth(codec); - // subscribeStatus 触发的首轮轮询搭乘新连接。 - await waitFor(() => findFrame(codec, socket, "statusGet"), "initial status_get"); - - socket.receiveBinary( - codec.encodeServerFrame({ status: { online: true, agent_id: "desktop-agent" } }), - ); - assert.equal(statuses.at(-1)?.status?.online, true, "pushed status frame reaches listeners"); - assert.equal(statuses.at(-1)?.error, null); - - socket.receiveBinary(codec.encodeServerFrame({ status: { online: false } })); - assert.equal(statuses.at(-1)?.status?.online, false, "offline push reaches listeners"); - resetGatewayWebSocketClient(); -}); - -test("GatewayWebSocketClient defers offline verdicts while hidden and reconciles on wake", async () => { - const timers = createManualTimers(); - installBrowser({ ...timers }); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient("token"); - const statuses = []; - client.subscribeStatus((status, error) => { - statuses.push({ status, error }); - }); - const socket = await connectAndAuth(codec); - socket.receiveBinary(codec.encodeServerFrame({ status: { online: true } })); - assert.equal(statuses.at(-1)?.status?.online, true); - - // 标签页转后台后连接断开(如冻结期间代理掐断链路)。 - globalThis.document.visibilityState = "hidden"; - const offlineCountBefore = statuses.filter((s) => s.status?.online === false).length; - socket.close(); - - // 15s 重连提示在后台触发:不得涂画离线态。 - timers.fire((timer) => timer.ms === 15_000); - assert.equal( - statuses.filter((s) => s.status?.online === false).length, - offlineCountBefore, - "hidden tab must not paint offline from throttled timers", - ); - - // 回前台:唤醒处理器重连;离线判定推迟到重连 + 状态刷新落定。 - globalThis.document.visibilityState = "visible"; - globalThis.document.dispatchEvent({ type: "visibilitychange" }); - timers.fire((timer) => timer.ms === 0); // armed reconnect timer - const socket2 = await connectAndAuth(codec, 1); - await waitFor(() => findFrame(codec, socket2, "statusGet"), "post-wake status refresh"); - const statusReq = findFrame(codec, socket2, "statusGet"); - socket2.receiveBinary( - codec.encodeServerFrame({ request_id: statusReq.requestId, status: { online: true } }), - ); - await waitFor(() => statuses.at(-1)?.status?.online === true, "post-wake online status"); - assert.equal( - statuses.filter((s) => s.status?.online === false).length, - offlineCountBefore, - "successful wake reconcile never flashed offline", - ); - resetGatewayWebSocketClient(); -}); - -test("GatewayWebSocketClient paints offline when the post-wake reconnect notice expires visible", async () => { - const timers = createManualTimers(); - installBrowser({ ...timers }); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient("token"); - const statuses = []; - client.subscribeStatus((status, error) => { - statuses.push({ status, error }); - }); - const socket = await connectAndAuth(codec); - socket.receiveBinary(codec.encodeServerFrame({ status: { online: true } })); - - globalThis.document.visibilityState = "hidden"; - socket.close(); - timers.fire((timer) => timer.ms === 15_000); - assert.ok(!statuses.some((s) => s.status?.online === false), "no offline while hidden"); - - globalThis.document.visibilityState = "visible"; - globalThis.document.dispatchEvent({ type: "visibilitychange" }); - // 重连始终不成功;重新武装的提示在前台超时。 - timers.fire((timer) => timer.ms === 15_000); - const last = statuses.at(-1); - assert.equal(last?.status?.online, false, "failed wake reconcile paints offline"); - assert.equal(last?.error, "Gateway 正在重新连接..."); - resetGatewayWebSocketClient(); -}); - -test("GatewayWebSocketClient refreshes status immediately on wake with a healthy socket", async () => { - const timers = createManualTimers(); - installBrowser({ ...timers }); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient("token"); - client.subscribeStatus(() => {}); - const socket = await connectAndAuth(codec); - await waitFor(() => findFrame(codec, socket, "statusGet"), "initial status_get"); - const initialStatusRequests = frames(codec, socket).filter( - (frame) => frame.case === "statusGet", - ).length; - const statusReq = findFrame(codec, socket, "statusGet"); - socket.receiveBinary( - codec.encodeServerFrame({ request_id: statusReq.requestId, status: { online: true } }), - ); - // 等在途 refreshStatus 落定(finally 在微任务里跑),避免唤醒触发的刷新 - // 被 in-flight 守卫吞掉。 - await new Promise((resolve) => setTimeout(resolve, 0)); - - globalThis.window.dispatchEvent({ type: "focus" }); - await waitFor( - () => - frames(codec, socket).filter((frame) => frame.case === "statusGet").length > - initialStatusRequests, - "wake-triggered status_get", - ); - resetGatewayWebSocketClient(); -}); diff --git a/crates/agent-gateway/test/webui/history-chat-ui.test.mjs b/crates/agent-gateway/test/webui/history-chat-ui.test.mjs deleted file mode 100644 index c36043e1c..000000000 --- a/crates/agent-gateway/test/webui/history-chat-ui.test.mjs +++ /dev/null @@ -1,961 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { createWebModuleLoader } from "../helpers/load-web-module.mjs"; - -const loader = createWebModuleLoader(); -const chatUi = loader.loadModule("src/lib/chatUi.ts"); -const transcriptStoreModule = loader.loadModule("src/lib/chat/transcript/transcriptStore.ts"); -const { createTurn, applyEventToTurn } = loader.loadModule( - "src/lib/chat/transcript/turnReducer.ts", -); -const { buildRowsFromEntries } = loader.loadModule("src/lib/chat/transcript/rows.ts"); -const historyShare = loader.loadModule("src/lib/historyShare.ts"); -const conversationState = loader.loadModule("src/lib/chat/conversationState.ts"); - -// Live-stream reducer harness: the createTurn/applyEventToTurn pair replaces -// the old flat pushChatEvent pipeline — one Turn holds a single run's entries. -function reduceTurnEvents(events) { - let turn = createTurn({ key: "req:test", runId: "run-test" }); - for (const event of events) { - turn = applyEventToTurn(turn, event); - } - return turn; -} - -function findAssistantRow(rows) { - return rows.find((row) => row.kind === "assistant"); -} - -test("history share helpers parse and build share URLs", () => { - assert.equal(historyShare.parseHistoryShareToken("/share/abc123"), "abc123"); - assert.equal(historyShare.parseHistoryShareToken("/share/abc%20123"), "abc 123"); - assert.equal(historyShare.parseHistoryShareToken("/chat/abc123"), null); - assert.equal(historyShare.parseHistoryShareToken("/share/abc/extra"), null); - assert.equal( - historyShare.buildHistoryShareUrl("abc123", "https://gateway.example/"), - "https://gateway.example/share/abc123", - ); -}); - -test("history share timestamps accept seconds milliseconds and microseconds", () => { - const timestampMs = Date.UTC(2026, 4, 13, 12, 34, 0); - - assert.equal( - historyShare.normalizeHistoryTimestampMs(Math.floor(timestampMs / 1000)), - timestampMs, - ); - assert.equal(historyShare.normalizeHistoryTimestampMs(timestampMs), timestampMs); - assert.equal(historyShare.normalizeHistoryTimestampMs(timestampMs * 1000), timestampMs); - assert.equal(historyShare.normalizeHistoryTimestampMs(0), null); - - const formatted = historyShare.formatSharedHistoryTimestamp(timestampMs); - assert.match(formatted, /2026/); - assert.doesNotMatch(formatted, /58331/); -}); - -test("fetchSharedHistory reads public share details that parse into transcript entries", async () => { - const previousFetch = globalThis.fetch; - globalThis.fetch = async (url, options) => { - assert.equal(url, "/api/public/history-shares/share-token"); - assert.equal(options.credentials, "omit"); - return { - ok: true, - async json() { - return { - conversation_id: "conversation-1", - messages_json: JSON.stringify([{ role: "user", content: "hello shared" }]), - total_message_count: 1, - redact_tool_content: true, - conversation: { - id: "conversation-1", - title: "Shared", - created_at: 1, - updated_at: 2, - message_count: 1, - }, - }; - }, - }; - }; - - try { - const detail = await historyShare.fetchSharedHistory("share-token"); - const entries = chatUi.parseHistoryMessagesJson(detail.messages_json); - assert.equal(detail.conversation_id, "conversation-1"); - assert.equal(detail.redact_tool_content, true); - assert.equal(entries.length, 1); - assert.equal(entries[0].kind, "user"); - assert.equal(entries[0].text, "hello shared"); - } finally { - globalThis.fetch = previousFetch; - } -}); - -test("parseHistoryMessagesJson preserves upload display text and checkpoint metadata", () => { - const entries = chatUi.parseHistoryMessagesJson(JSON.stringify([ - { - role: "user", - content: "internal content with upload instruction", - liveAgentDisplayContent: "please inspect notes", - liveAgentAttachments: [ - { - relativePath: "uploads/notes.txt", - fileName: "notes.txt", - kind: "text", - sizeBytes: 42, - }, - ], - liveAgentHistoryRef: { - segmentIndex: 1, - messageIndex: 2, - segmentId: "segment-1", - messageId: "message-2", - role: "user", - contentHash: "hash-2", - }, - }, - { - role: "summary", - id: "summary-1", - content: "compressed facts", - summaryMeta: { - coveredMessageCount: 8, - generatedBy: { - providerId: "liveagent", - model: "summary", - promptVersion: "summary-v2", - }, - }, - }, - ])); - - assert.equal(entries.length, 2); - assert.equal(entries[0].kind, "user"); - assert.equal(entries[0].text, "please inspect notes"); - assert.equal(entries[0].attachments[0].relativePath, "uploads/notes.txt"); - assert.deepEqual(entries[0].messageRef, { - segmentIndex: 1, - messageIndex: 2, - segmentId: "segment-1", - messageId: "message-2", - role: "user", - contentHash: "hash-2", - }); - assert.equal(entries[1].kind, "checkpoint"); - assert.equal(entries[1].summaryId, "summary-1"); - assert.equal(entries[1].coveredMessageCount, 8); -}); - -test("parseHistoryMessagesJson preserves Image tool result image content", () => { - const entries = chatUi.parseHistoryMessagesJson(JSON.stringify([ - { - role: "assistant", - content: [ - { - type: "toolCall", - id: "image-call", - name: "Image", - arguments: { paths: ["uploads/001.jpg", "uploads/002.png"] }, - }, - ], - provider: "codex", - model: "gpt-test", - api: "openai-responses", - stopReason: "toolUse", - timestamp: 1, - }, - { - role: "toolResult", - toolCallId: "image-call", - toolName: "Image", - content: [ - { type: "text", text: "Display images: 2" }, - { type: "image", mimeType: "image/jpeg", data: "abc123" }, - { type: "image", mimeType: "image/png", data: "def456" }, - ], - details: { - kind: "display_image", - images: [ - { - path: "uploads/001.jpg", - mimeType: "image/jpeg", - sizeBytes: 12, - mtimeMs: 10, - contentHash: "hash-1", - }, - { - path: "uploads/002.png", - mimeType: "image/png", - sizeBytes: 34, - mtimeMs: 11, - contentHash: "hash-2", - }, - ], - path: "uploads/001.jpg", - mimeType: "image/jpeg", - sizeBytes: 12, - mtimeMs: 10, - contentHash: "hash-1", - loadMode: "inline", - }, - isError: false, - timestamp: 2, - }, - ])); - - const toolCallEntry = entries.find((entry) => entry.kind === "tool_call"); - const toolResultEntry = entries.find((entry) => entry.kind === "tool_result"); - - assert.ok(toolCallEntry); - assert.equal(toolCallEntry.toolCall.name, "Image"); - assert.equal(toolCallEntry.summary, "Image paths=2 first=uploads/001.jpg"); - assert.ok(toolResultEntry); - assert.equal(toolResultEntry.toolResult.details.kind, "display_image"); - assert.equal(toolResultEntry.toolResult.content[1].type, "image"); - assert.equal(toolResultEntry.toolResult.content[1].mimeType, "image/jpeg"); - assert.equal(toolResultEntry.toolResult.content[2].type, "image"); - assert.equal(toolResultEntry.toolResult.content[2].mimeType, "image/png"); -}); - -test("parseHistoryMessagesJson preserves provider tool_use input arguments", () => { - const entries = chatUi.parseHistoryMessagesJson(JSON.stringify([ - { - role: "assistant", - content: [ - { - type: "tool_use", - id: "bash-call", - name: "Bash", - input: { - command: "pnpm -C crates/agent-gateway/web build", - cwd: "crates/agent-gateway/web", - root: "workspace", - }, - }, - ], - }, - ])); - - const assistant = findAssistantRow(buildRowsFromEntries(entries, "history")); - const toolBlock = assistant.rounds[0].blocks.find((block) => block.kind === "tool"); - - assert.ok(toolBlock); - assert.equal(toolBlock.item.toolCall.name, "Bash"); - assert.deepEqual(toolBlock.item.toolCall.arguments, { - command: "pnpm -C crates/agent-gateway/web build", - cwd: "crates/agent-gateway/web", - root: "workspace", - }); -}); - -test("WebUI transcript strips leaked DSML tool call markup from text and thinking", () => { - const dsml = [ - "<||DSML|| tool_calls>", - '<||DSML|| invoke name="builtin_web_search">', - '<||DSML|| parameter name="query">LiveAgent DSML markup', - "", - "", - ].join("\n"); - const entries = chatUi.parseHistoryMessagesJson(JSON.stringify([ - { - role: "assistant", - content: [ - { type: "text", text: `before\n${dsml}\nafter` }, - { type: "thinking", thinking: `thinking\n${dsml}` }, - ], - }, - ])); - const assistant = findAssistantRow(buildRowsFromEntries(entries, "history")); - const round = assistant.rounds[0]; - const allText = JSON.stringify(round.blocks); - - assert.match(allText, /before/); - assert.match(allText, /after/); - assert.match(allText, /thinking/); - assert.doesNotMatch(allText, /DSML/); - assert.doesNotMatch(allText, /builtin_web_search/); -}); - -test("WebUI transcript hides provider-native web_search tool traces when hosted search exists", () => { - const webSearchCall = { - type: "toolCall", - id: "dsml-tool-call-webui-search", - name: "web_search", - arguments: { query: "LiveAgent DeepSeek webui search" }, - }; - const entries = chatUi.parseHistoryMessagesJson(JSON.stringify([ - { role: "user", content: "search" }, - { - role: "assistant", - content: [ - { type: "text", text: "searching" }, - { - type: "hostedSearch", - id: "hosted-search-1", - provider: "claude_code", - status: "completed", - queries: ["LiveAgent DeepSeek webui search"], - sources: [{ url: "https://example.com/result", title: "Result" }], - }, - webSearchCall, - ], - stopReason: "toolUse", - }, - { - role: "toolResult", - toolCallId: webSearchCall.id, - toolName: webSearchCall.name, - content: [{ type: "text", text: "Tool web_search not found" }], - details: { recoveredProviderNativeWebSearch: true }, - isError: true, - }, - ])); - - const assistant = findAssistantRow(buildRowsFromEntries(entries, "history")); - const round = assistant.rounds[0]; - - assert.equal(round.blocks.some((block) => block.kind === "tool"), false); - assert.equal(round.blocks.some((block) => block.kind === "hostedSearch"), true); -}); - -test("WebUI live transcript removes provider-native web_search when hosted search arrives later", () => { - let turn = createTurn({ key: "req:test", runId: "run-test" }); - turn = applyEventToTurn(turn, { - type: "tool_call", - id: "call_00_webui_search", - name: "web_search", - arguments: { query: "LiveAgent DeepSeek live search" }, - round: 1, - }); - - let assistant = findAssistantRow(buildRowsFromEntries(turn.entries, "stream")); - assert.equal(assistant.rounds[0].blocks.some((block) => block.kind === "tool"), true); - - turn = applyEventToTurn(turn, { - type: "hosted_search", - id: "hosted-search-live", - provider: "claude_code", - status: "completed", - queries: ["LiveAgent DeepSeek live search"], - sources: [{ url: "https://example.com/live", title: "Live Result" }], - round: 1, - }); - - assistant = findAssistantRow(buildRowsFromEntries(turn.entries, "stream")); - const round = assistant.rounds[0]; - - assert.equal(round.blocks.some((block) => block.kind === "tool"), false); - assert.equal(round.blocks.some((block) => block.kind === "hostedSearch"), true); - assert.deepEqual(round.runningToolCallIds, []); -}); - -test("WebUI live transcript hides recovered provider-native web_search results without hosted search", () => { - const turn = reduceTurnEvents([ - { - type: "tool_call", - id: "call_00_webui_recovered_search", - name: "WebSearch", - arguments: { query: "LiveAgent recovered search" }, - round: 1, - }, - { - type: "tool_result", - id: "call_00_webui_recovered_search", - name: "WebSearch", - content: [{ type: "text", text: "Recovered provider-native web search." }], - details: { recoveredProviderNativeWebSearch: true }, - isError: false, - round: 1, - }, - ]); - - // The recovered result hides the whole trace; with nothing else in the - // round the content gate drops it entirely — no avatar-only assistant row - // (stronger than the old "row without tool blocks" rendering). - const rows = buildRowsFromEntries(turn.entries, "stream"); - assert.equal(findAssistantRow(rows), undefined, "fully hidden trace renders no assistant row"); - assert.equal(rows.length, 0); -}); - -test("WebUI live transcript hides recovered DSML provider-native web_search calls immediately", () => { - const turn = reduceTurnEvents([ - { - type: "tool_call", - id: "dsml-tool-call-webui-live-search", - name: "builtin_web_search", - arguments: { query: "LiveAgent DSML hidden search" }, - round: 1, - }, - ]); - - // The DSML-recovered call is hidden immediately; the content-less round is - // dropped so no assistant row (avatar) can appear for it. - const rows = buildRowsFromEntries(turn.entries, "stream"); - assert.equal(findAssistantRow(rows), undefined, "fully hidden call renders no assistant row"); - assert.equal(rows.length, 0); -}); - -test("turn reducer appends streaming text, dedupes tool cards, and dedupes compaction checkpoints", () => { - let turn = createTurn({ key: "req:test", runId: "run-test" }); - turn = applyEventToTurn(turn, { - type: "token", - text: "hello ", - round: 1, - provider: "codex", - model: "gpt-test", - usage: { totalTokens: 12 }, - }); - turn = applyEventToTurn(turn, { type: "token", text: "world", round: 1 }); - assert.equal(turn.entries.length, 1); - assert.equal(turn.entries[0].kind, "assistant"); - assert.equal(turn.entries[0].text, "hello world"); - assert.equal(turn.entries[0].meta.usageTotalTokens, 12); - - const toolCall = { type: "tool_call", id: "call-1", name: "Read", arguments: { path: "README.md" }, round: 1 }; - turn = applyEventToTurn(turn, toolCall); - turn = applyEventToTurn(turn, toolCall); - assert.equal(turn.entries.filter((entry) => entry.kind === "tool_call").length, 1); - - const checkpoint = { - type: "token", - text: "compressed facts", - checkpoint: { - summaryId: "summary-1", - coveredMessageCount: 5, - generatedBy: { providerId: "liveagent", model: "summary" }, - }, - }; - turn = applyEventToTurn(turn, checkpoint); - turn = applyEventToTurn(turn, checkpoint); - assert.equal(turn.entries.filter((entry) => entry.kind === "checkpoint").length, 1); -}); - -test("turn reducer preserves tool call arguments from JSON string and input aliases", () => { - const turn = reduceTurnEvents([ - { - type: "tool_call", - id: "bash-call", - name: "Bash", - arguments: JSON.stringify({ - command: "echo gateway", - cwd: "crates/agent-gateway", - root: "workspace", - }), - round: 1, - }, - { - type: "tool_call", - id: "read-call", - name: "Read", - input: { - path: "README.md", - root: "workspace", - }, - round: 1, - }, - { - type: "tool_call", - data: JSON.stringify({ - id: "glob-call", - name: "Glob", - args: { - pattern: "**/*.ts", - path: "src", - root: "workspace", - }, - }), - round: 1, - }, - ]); - const entries = turn.entries; - - const bashCall = entries.find((entry) => entry.kind === "tool_call" && entry.toolCall.id === "bash-call"); - const readCall = entries.find((entry) => entry.kind === "tool_call" && entry.toolCall.id === "read-call"); - const globCall = entries.find((entry) => entry.kind === "tool_call" && entry.toolCall.id === "glob-call"); - const assistant = findAssistantRow(buildRowsFromEntries(entries, "stream")); - const toolBlocks = assistant.rounds[0].blocks.filter((block) => block.kind === "tool"); - - assert.ok(bashCall); - assert.equal(bashCall.toolCall.arguments.command, "echo gateway"); - assert.match(bashCall.summary, /command=echo gateway/); - assert.ok(readCall); - assert.equal(readCall.toolCall.arguments.path, "README.md"); - assert.ok(globCall); - assert.equal(globCall.toolCall.arguments.pattern, "**/*.ts"); - assert.equal(toolBlocks[0].item.toolCall.arguments.command, "echo gateway"); - assert.equal(toolBlocks[1].item.toolCall.arguments.path, "README.md"); - assert.equal(toolBlocks[2].item.toolCall.arguments.pattern, "**/*.ts"); -}); - -test("turn reducer reconstructs a parameterized tool card from tool_result arguments", () => { - const turn = reduceTurnEvents([ - { - type: "tool_result", - id: "bash-result-only", - name: "Bash", - arguments: { - command: "printf live", - cwd: "crates/agent-gateway", - root: "workspace", - }, - content: [{ type: "text", text: "live" }], - isError: false, - round: 1, - }, - ]); - const entries = turn.entries; - - assert.equal(entries.length, 2); - assert.equal(entries[0].kind, "tool_call"); - assert.equal(entries[0].toolCall.arguments.command, "printf live"); - assert.equal(entries[1].kind, "tool_result"); - - const assistant = findAssistantRow(buildRowsFromEntries(entries, "stream")); - const toolBlock = assistant.rounds[0].blocks.find((block) => block.kind === "tool"); - - assert.ok(toolBlock); - assert.equal(toolBlock.item.toolCall.name, "Bash"); - assert.equal(toolBlock.item.toolCall.arguments.command, "printf live"); - assert.equal(toolBlock.item.toolResult.content[0].text, "live"); -}); - -test("turn reducer does not duplicate tool cards when tool_call precedes parameterized tool_result", () => { - const toolArguments = { - command: "printf once", - cwd: "crates/agent-gateway", - root: "workspace", - }; - const turn = reduceTurnEvents([ - { - type: "tool_call", - id: "bash-no-duplicate", - name: "Bash", - arguments: toolArguments, - round: 1, - }, - { - type: "tool_result", - id: "bash-no-duplicate", - name: "Bash", - arguments: toolArguments, - content: [{ type: "text", text: "once" }], - isError: false, - round: 1, - }, - ]); - const entries = turn.entries; - - assert.equal(entries.filter((entry) => entry.kind === "tool_call").length, 1); - assert.equal(entries.filter((entry) => entry.kind === "tool_result").length, 1); - - const assistant = findAssistantRow(buildRowsFromEntries(entries, "stream")); - const toolBlocks = assistant.rounds[0].blocks.filter((block) => block.kind === "tool"); - - assert.equal(toolBlocks.length, 1); - assert.equal(toolBlocks[0].item.toolCall.arguments.command, "printf once"); - assert.equal(toolBlocks[0].item.toolResult.content[0].text, "once"); -}); - -test("turn reducer upgrades an existing live tool card when execution start carries arguments", () => { - const turn = reduceTurnEvents([ - { - type: "tool_call", - id: "bash-late-args", - name: "Bash", - round: 1, - }, - { - type: "tool_call", - id: "bash-late-args", - name: "Bash", - arguments: { - command: "printf from-start", - cwd: "crates/agent-gateway", - root: "workspace", - }, - round: 1, - }, - ]); - const entries = turn.entries; - - assert.equal(entries.filter((entry) => entry.kind === "tool_call").length, 1); - assert.equal(entries[0].toolCall.arguments.command, "printf from-start"); - assert.match(entries[0].summary, /command=printf from-start/); -}); - -test("turn reducer upgrades an existing live tool card when tool_result carries arguments", () => { - const turn = reduceTurnEvents([ - { - type: "tool_call", - id: "bash-result-args", - name: "Bash", - round: 1, - }, - { - type: "tool_result", - id: "bash-result-args", - name: "Bash", - arguments: { - command: "printf from-result", - cwd: "crates/agent-gateway", - root: "workspace", - }, - content: [{ type: "text", text: "from-result" }], - isError: false, - round: 1, - }, - ]); - const entries = turn.entries; - - assert.equal(entries.filter((entry) => entry.kind === "tool_call").length, 1); - assert.equal(entries.filter((entry) => entry.kind === "tool_result").length, 1); - assert.equal(entries[0].toolCall.arguments.command, "printf from-result"); - - const assistant = findAssistantRow(buildRowsFromEntries(entries, "stream")); - const toolBlock = assistant.rounds[0].blocks.find((block) => block.kind === "tool"); - - assert.ok(toolBlock); - assert.equal(toolBlock.item.toolCall.arguments.command, "printf from-result"); - assert.equal(toolBlock.item.toolResult.content[0].text, "from-result"); -}); - -test("turn reducer keeps a deduped result while applying late result arguments", () => { - const turn = reduceTurnEvents([ - { - type: "tool_call", - id: "bash-duplicate-result", - name: "Bash", - round: 1, - }, - { - type: "tool_result", - id: "bash-duplicate-result", - name: "Bash", - content: [{ type: "text", text: "duplicate" }], - isError: false, - round: 1, - }, - { - type: "tool_result", - id: "bash-duplicate-result", - name: "Bash", - arguments: { - command: "printf duplicate", - cwd: "crates/agent-gateway", - root: "workspace", - }, - content: [{ type: "text", text: "duplicate" }], - isError: false, - round: 1, - }, - ]); - const entries = turn.entries; - - assert.equal(entries.filter((entry) => entry.kind === "tool_call").length, 1); - assert.equal(entries.filter((entry) => entry.kind === "tool_result").length, 1); - assert.equal(entries[0].toolCall.arguments.command, "printf duplicate"); -}); - -function findTreeNode(node, predicate) { - if (Array.isArray(node)) { - for (const child of node) { - const match = findTreeNode(child, predicate); - if (match) { - return match; - } - } - return null; - } - if (node == null || typeof node !== "object") { - return null; - } - if (predicate(node)) { - return node; - } - const children = node.props?.children; - const childList = Array.isArray(children) ? children : [children]; - for (const child of childList) { - const match = findTreeNode(child, predicate); - if (match) { - return match; - } - } - return null; -} - -test("formatConversationTitle falls back to stable labels", () => { - assert.equal(chatUi.formatConversationTitle({ id: "abc", title: " Named " }), "Named"); - assert.equal(chatUi.formatConversationTitle(null, "conversation-abcdef"), "会话 conversa"); - assert.equal(chatUi.formatConversationTitle(null, ""), "新对话"); -}); - -test("resolveConversationBrowserTitle uses project title for project-level empty selection", () => { - assert.equal( - chatUi.resolveConversationBrowserTitle({ - conversation: null, - conversationId: "conversation-abcdef", - projectName: " Project Alpha ", - newConversationTitle: "LiveAgent", - }), - "Project Alpha", - ); - assert.equal( - chatUi.resolveConversationBrowserTitle({ - conversation: { id: "conversation-abcdef", title: " Named " }, - conversationId: "conversation-abcdef", - projectName: "Project Alpha", - newConversationTitle: "LiveAgent", - }), - "Named", - ); - assert.equal( - chatUi.resolveConversationBrowserTitle({ - conversation: null, - conversationId: "__local_draft__:abc", - projectName: "Project Alpha", - isLocalDraftConversation: true, - newConversationTitle: "LiveAgent", - }), - "LiveAgent", - ); -}); - -test("buildOptimisticConversationTitle uses the first ten characters of the first prompt paragraph", () => { - assert.equal( - chatUi.buildOptimisticConversationTitle(" 12345 67890 abc\nstill first paragraph\n\nsecond"), - "12345 6789", - ); - assert.equal( - chatUi.buildOptimisticConversationTitle("这是第一段提示词超过十个字\n\n第二段"), - "这是第一段提示词超过", - ); - assert.equal(chatUi.buildOptimisticConversationTitle(" \n\n "), "新对话"); -}); - -test("GatewayTranscript renders folded and live rows in one virtualized list", () => { - const fakeReact = { - createContext(defaultValue) { - return { defaultValue }; - }, - // Module-load shim: ui/button.tsx calls React.forwardRef at top level - // (pulled in via the retry ConfirmActionPopover import chain). - forwardRef(render) { - return render; - }, - memo(component) { - return component; - }, - useCallback(callback) { - return callback; - }, - useContext(context) { - return context.defaultValue; - }, - useEffect() {}, - useLayoutEffect() {}, - useMemo(factory) { - return factory(); - }, - useRef(value) { - return { current: value }; - }, - useState(initialValue) { - const value = typeof initialValue === "function" ? initialValue() : initialValue; - return [value, () => {}]; - }, - useSyncExternalStore(_subscribe, getSnapshot) { - return getSnapshot(); - }, - }; - const transcriptLoader = createWebModuleLoader({ - mocks: { - react: fakeReact, - "@tanstack/react-virtual": { - useVirtualizer({ count, getItemKey }) { - return { - getTotalSize: () => count * 100, - getVirtualItems: () => - Array.from({ length: count }, (_, index) => ({ - index, - key: getItemKey(index), - start: index * 100, - })), - measureElement: () => {}, - }; - }, - }, - "@/components/Markdown": { - Markdown(props) { - return { type: "Markdown", props }; - }, - }, - "@/components/chat/ImagePreview": { - ImagePreview(props) { - return { type: "ImagePreview", props }; - }, - }, - "@/pages/chat/AssistantBubble": { - AssistantAvatar() { - return { type: "AssistantAvatar", props: {} }; - }, - AssistantBubble(props) { - return { type: "AssistantBubble", props }; - }, - CompactingText(props) { - return { type: "CompactingText", props }; - }, - VibingText(props) { - return { type: "VibingText", props }; - }, - }, - }, - }); - const { GatewayTranscript } = transcriptLoader.loadModule("src/components/GatewayTranscript.tsx"); - - globalThis.window = undefined; - globalThis.document = { visibilityState: "visible" }; - - // Folded rows come from parsed history; live rows are born from the - // stream (a seeded prompt plus its streaming reply). Both regions share - // one row list, separated only by liveStartIndex. - const store = transcriptStoreModule.createTranscriptStore(); - store.applyHistorySnapshot( - [ - { id: "hu:m1", kind: "user", text: "earlier question", attachments: [] }, - { id: "ht:hu:m1>0", kind: "assistant", text: "earlier answer", round: 1 }, - ], - { mode: "replace" }, - ); - store.applyEvent({ - type: "user_message", - conversation_id: "conversation-1", - run_id: "run-1", - seq: 1, - message: "queued from gui", - }); - store.applyEvent({ - type: "run_started", - conversation_id: "conversation-1", - run_id: "run-1", - seq: 2, - }); - store.applyEvent({ - type: "token", - conversation_id: "conversation-1", - run_id: "run-1", - seq: 3, - text: "reply", - }); - store.flush(); - const snapshot = store.getSnapshot(); - assert.equal(snapshot.liveStartIndex, 2, "history rows fold; the live exchange stays below"); - assert.deepEqual( - snapshot.rows.map((row) => row.kind), - ["user", "assistant", "user", "assistant"], - ); - - const transcriptTree = GatewayTranscript({ - conversationId: "conversation-1", - rows: snapshot.rows, - liveStartIndex: snapshot.liveStartIndex, - activeTurnKey: snapshot.activeTurnKey, - isStreaming: true, - }); - - const listRegionNode = findTreeNode( - transcriptTree, - (node) => - typeof node.type === "function" && - Array.isArray(node.props?.rows) && - node.props?.conversationId === "conversation-1", - ); - assert.ok(listRegionNode, "the virtualized region receives the unified row list"); - assert.deepEqual( - listRegionNode.props.rows.map((row) => row.key), - snapshot.rows.map((row) => row.key), - ); - assert.equal(listRegionNode.props.liveStartIndex, snapshot.liveStartIndex); - - const listTree = listRegionNode.type(listRegionNode.props); - assert.ok( - findTreeNode( - listTree, - (node) => - typeof node.props?.className === "string" && - node.props.className.includes("gateway-transcript-row-user"), - ), - "live user bubble renders before the live assistant output", - ); - assert.ok( - findTreeNode( - listTree, - // User rows render through GatewayUserMessageRowBody, which receives - // the whole row (row.text) rather than a bare text prop. - (node) => - typeof node.type === "function" && - (node.props?.text === "queued from gui" || node.props?.row?.text === "queued from gui"), - ), - ); - const assistantBubble = findTreeNode( - listTree, - (node) => - typeof node.type === "function" && - node.props?.renderMode !== undefined && - node.props?.isLive === true, - ); - assert.ok(assistantBubble, "live assistant bubble rendered"); - assert.equal( - assistantBubble.props.renderMode, - "streaming", - "live-born rows keep the streaming render mode", - ); -}); - -test("transcript store history refresh stays quiet for identical content", () => { - // The old pipeline kept a quiet refresh stable via text-hash dedup keys - // (renamed incoming ids were re-mapped onto the rendered ones). The new - // parser makes ids deterministic instead: reparsing the same persisted - // JSON yields identical ids, so an idle "enrich" refresh with unchanged - // content is a structural no-op — same snapshot, same row keys, and the - // exchange still renders exactly once. - globalThis.window = undefined; - globalThis.document = { visibilityState: "visible" }; - const store = transcriptStoreModule.createTranscriptStore(); - - const messagesJson = JSON.stringify([ - { role: "user", content: "hello" }, - { role: "assistant", content: "world" }, - ]); - const firstParse = chatUi.parseHistoryMessagesJson(messagesJson); - store.applyHistorySnapshot(firstParse, { mode: "replace" }); - store.flush(); - const first = store.getSnapshot(); - assert.deepEqual( - first.rows.map((row) => row.kind), - ["user", "assistant"], - ); - assert.equal(first.liveStartIndex, -1, "history renders before the live boundary"); - - const secondParse = chatUi.parseHistoryMessagesJson(messagesJson); - assert.deepEqual( - secondParse.map((entry) => entry.id), - firstParse.map((entry) => entry.id), - "reparsing the same JSON yields identical deterministic ids", - ); - - // Idle quiet refresh with identical content: nothing re-renders. - store.applyHistorySnapshot(secondParse, { mode: "enrich" }); - store.flush(); - const second = store.getSnapshot(); - assert.equal(second, first, "identical content leaves the snapshot untouched"); - assert.deepEqual( - second.rows.map((row) => row.key), - first.rows.map((row) => row.key), - "row keys are stable across the refresh", - ); - assert.equal( - second.rows.filter((row) => row.kind === "user").length, - 1, - "the exchange renders exactly once (no duplicate prompt)", - ); -}); diff --git a/crates/agent-gateway/test/webui/markdown-image-policy.test.mjs b/crates/agent-gateway/test/webui/markdown-image-policy.test.mjs deleted file mode 100644 index 4d5ebae27..000000000 --- a/crates/agent-gateway/test/webui/markdown-image-policy.test.mjs +++ /dev/null @@ -1,59 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; - -import { createWebModuleLoader } from "../helpers/load-web-module.mjs"; - -const loader = createWebModuleLoader({ - mocks: { - "@streamdown/cjk": { - cjk: {}, - }, - "@streamdown/code": { - code: {}, - }, - "@streamdown/math": { - math: {}, - }, - "@streamdown/mermaid": { - mermaid: {}, - }, - streamdown: { - Streamdown(props) { - return { type: "Streamdown", props }; - }, - defaultRemarkPlugins: {}, - defaultRehypePlugins: {}, - }, - "./ui/button": { - Button(props) { - return { type: "Button", props }; - }, - }, - "../lib/shared/utils": { - cn: (...parts) => parts.filter(Boolean).join(" "), - }, - "../lib/shared/modalMotion": { - useModalMotion(onClose) { - return { modalState: "open", requestClose: onClose }; - }, - }, - }, -}); - -const markdownModule = loader.loadModule("src/components/Markdown.tsx"); - -test("webui markdown image syntax also falls back to alt text", () => { - const node = markdownModule.markdownComponents.img({ - alt: "东门老街", - title: "深圳夜景", - }); - - assert.ok(node); - assert.equal(node.type, "span"); - assert.equal(node.props["data-liveagent-markdown-image"], "text-fallback"); - assert.equal(node.props.title, "东门老街"); - assert.equal(node.props.children, "东门老街"); - - const empty = markdownModule.markdownComponents.img({}); - assert.equal(empty, null); -}); diff --git a/crates/agent-gateway/test/webui/markdown-latex.test.mjs b/crates/agent-gateway/test/webui/markdown-latex.test.mjs deleted file mode 100644 index 7ff3c85f9..000000000 --- a/crates/agent-gateway/test/webui/markdown-latex.test.mjs +++ /dev/null @@ -1,42 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; - -import { createWebModuleLoader } from "../helpers/load-web-module.mjs"; - -const loader = createWebModuleLoader(); -const { normalizeLatexDelimiters } = loader.loadModule( - "src/lib/normalizeLatexDelimiters.ts", -); - -test("webui normalizes LaTeX delimiters with the mirrored parser", () => { - const content = String.raw`\[ -p_0 = p \cdot 10^{\frac{H}{18400(1+t/273)}} -\] - -其中 \(p_0\) 是海平面气压。`; - - assert.equal( - normalizeLatexDelimiters(content), - String.raw`$$ -p_0 = p \cdot 10^{\frac{H}{18400(1+t/273)}} -$$ - -其中 $$p_0$$ 是海平面气压。`, - ); -}); - -test("webui preserves code and supports an incomplete streaming formula", () => { - const fenced = ["```latex", "\\[", "x", "\\]", "```"].join("\n"); - assert.equal(normalizeLatexDelimiters(fenced, true), fenced); - assert.equal(normalizeLatexDelimiters(String.raw`\(x`, true), "$$x"); -}); - -test("webui converts single-dollar math and keeps currency literal", () => { - assert.equal(normalizeLatexDelimiters("质能方程 $E = mc^2$。"), "质能方程 $$E = mc^2$$。"); - - const currency = "价格 $5,成本 $10。"; - assert.equal(normalizeLatexDelimiters(currency), currency); - - const streamingInline = "计算 $E = mc^"; - assert.equal(normalizeLatexDelimiters(streamingInline, true), streamingInline); -}); diff --git a/crates/agent-gateway/test/webui/provider-usage-query-form.test.mjs b/crates/agent-gateway/test/webui/provider-usage-query-form.test.mjs deleted file mode 100644 index 6da585b13..000000000 --- a/crates/agent-gateway/test/webui/provider-usage-query-form.test.mjs +++ /dev/null @@ -1,77 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { createWebModuleLoader } from "../helpers/load-web-module.mjs"; - -const loader = createWebModuleLoader(); -const forms = loader.loadModule("src/pages/settings/providerUtils.ts"); - -const usageQuery = { - enabled: true, - mode: "newapi", - script: "", - scripts: {}, - baseUrl: "https://usage.example.test", - apiKey: "", - apiKeyConfigured: true, - accessToken: "", - accessTokenConfigured: true, - userId: "user-1", - accessKeyId: "key-1", - secretAccessKey: "", - secretAccessKeyConfigured: true, - codingPlanProvider: "", - teamOrganizationId: "", - teamProjectId: "", - timeoutSecs: 10, -}; - -test("WebUI usage query draft preserves configured redacted secrets when saved", () => { - const draft = forms.createUsageQueryDraft(usageQuery, true); - - assert.notEqual(draft.apiKey, ""); - assert.notEqual(draft.accessToken, ""); - assert.notEqual(draft.secretAccessKey, ""); - assert.deepEqual(forms.serializeUsageQueryDraft(draft, true), usageQuery); -}); - -test("WebUI usage query serialization clamps the timeout", () => { - const serialized = forms.serializeUsageQueryDraft({ ...usageQuery, timeoutSecs: 500 }, false); - assert.equal(serialized.timeoutSecs, 30); -}); - -test("WebUI mode switch keeps per-mode scripts independent", () => { - const generalPreset = forms.USAGE_QUERY_PRESET_SCRIPTS.general; - const filled = forms.applyUsageQueryModePreset({ ...usageQuery, script: "" }, "general"); - assert.equal(filled.script, generalPreset); - - // newapi 里的编辑切到 general 再切回后原样恢复。 - const edited = forms.setUsageQueryScript( - { ...usageQuery, mode: "newapi", script: "" }, - "(my newapi script)", - ); - const onGeneral = forms.applyUsageQueryModePreset(edited, "general"); - assert.equal(onGeneral.script, generalPreset); - const back = forms.applyUsageQueryModePreset(onGeneral, "newapi"); - assert.equal(back.script, "(my newapi script)"); -}); - -test("WebUI usage test action accepts only a persisted provider id", () => { - assert.equal(forms.getPersistedUsageQueryProviderId(undefined), null); - assert.equal(forms.getPersistedUsageQueryProviderId({ id: "" }), null); - assert.equal(forms.getPersistedUsageQueryProviderId({ id: "provider-a" }), "provider-a"); -}); - -test("WebUI custom usage query needs confirmation before its first enabled save", () => { - assert.equal( - forms.requiresCustomUsageQueryConfirmation({ ...usageQuery, mode: "custom" }, false), - true, - ); - assert.equal( - forms.requiresCustomUsageQueryConfirmation({ ...usageQuery, mode: "custom" }, true), - false, - ); - assert.equal( - forms.requiresCustomUsageQueryConfirmation({ ...usageQuery, mode: "custom", enabled: true }, true), - false, - ); -}); diff --git a/crates/agent-gateway/test/webui/provider-usage-query.test.mjs b/crates/agent-gateway/test/webui/provider-usage-query.test.mjs deleted file mode 100644 index 9d8c9e77e..000000000 --- a/crates/agent-gateway/test/webui/provider-usage-query.test.mjs +++ /dev/null @@ -1,141 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { createGatewayV2Codec } from "../helpers/gateway-v2.mjs"; -import { createWebModuleLoader } from "../helpers/load-web-module.mjs"; - -const requestCalls = []; -const testCalls = []; -const loader = createWebModuleLoader({ - mocks: { - "@/lib/gatewaySocket": { - getGatewayWebSocketClient() { - return { - providerUsageQuery(providerId, refresh) { - requestCalls.push({ providerId, refresh }); - return Promise.resolve({ - data: [{ planName: "Credits", remaining: 11, unit: "USD" }], - queriedAt: 123, - error: null, - isStale: false, - }); - }, - providerUsageTest(providerId, configJson) { - testCalls.push({ providerId, configJson }); - return Promise.resolve({ - data: [{ planName: "Draft", remaining: 3, unit: "USD" }], - queriedAt: 456, - error: null, - isStale: false, - }); - }, - }; - }, - }, - "@/lib/storage": { loadToken: () => "gateway-token" }, - }, -}); -const usage = loader.loadModule("src/lib/providers/usageQuery.ts"); -const adapters = loader.loadModule("src/lib/gatewaySocketV2/adapters.ts"); -const codec = createGatewayV2Codec(loader); - -test("WebUI query client refreshes one provider through the Gateway", async () => { - requestCalls.length = 0; - - const result = await usage.queryProviderUsage("provider-a", true); - - assert.equal(result.data[0].remaining, 11); - assert.deepEqual(requestCalls, [{ providerId: "provider-a", refresh: true }]); -}); - -test("WebUI draft test forwards the editor config JSON to the desktop", async () => { - testCalls.length = 0; - - const result = await usage.testProviderUsage("provider-a", { enabled: false, mode: "custom" }); - - assert.equal(result.data[0].planName, "Draft"); - assert.deepEqual(testCalls, [ - { providerId: "provider-a", configJson: '{"enabled":false,"mode":"custom"}' }, - ]); -}); - -test("WebUI protobuf encodes usage request and decodes JSON response", () => { - const request = codec.decodeClientFrame( - adapters.encodeRequestFrame( - "request-1", - "provider.usage.query", - { provider_id: "provider-a", refresh: true }, - "desktop-agent", - ), - ); - - assert.equal(request.case, "agentRequest"); - assert.deepEqual(request.json.agent_request.provider_usage, { - provider_id: "provider-a", - refresh: true, - }); - - // 按草稿测试:config_json 随请求透传。 - const draftRequest = codec.decodeClientFrame( - adapters.encodeRequestFrame( - "request-9", - "provider.usage.query", - { provider_id: "provider-a", refresh: true, config_json: '{"mode":"custom"}' }, - "desktop-agent", - ), - ); - assert.equal( - draftRequest.json.agent_request.provider_usage.config_json, - '{"mode":"custom"}', - ); - - const frame = codec.encodeServerFrame({ - request_id: "request-1", - agent_id: "desktop-agent", - agent_response: { - provider_usage_resp: { - result_json: JSON.stringify({ - data: [{ planName: "Credits", remaining: 11, unit: "USD" }], - queriedAt: 123, - error: null, - isStale: false, - }), - }, - }, - }); - const decoded = adapters.decodeServerFrame(adapters.decodeServerFrameBinary(frame), { - agentOnline: true, - }); - - assert.deepEqual(decoded, { - kind: "response", - requestId: "request-1", - agentId: "desktop-agent", - payload: { - data: [{ planName: "Credits", remaining: 11, unit: "USD" }], - queriedAt: 123, - error: null, - isStale: false, - }, - }); -}); - -test("WebUI protobuf rejects malformed usage response JSON", () => { - const frame = codec.encodeServerFrame({ - request_id: "request-2", - agent_id: "desktop-agent", - agent_response: { - provider_usage_resp: { result_json: "{not-json" }, - }, - }); - - const decoded = adapters.decodeServerFrame(adapters.decodeServerFrameBinary(frame), { - agentOnline: true, - }); - - assert.deepEqual(decoded, { - kind: "error", - requestId: "request-2", - agentId: "desktop-agent", - message: "provider usage response is not valid JSON", - }); -}); diff --git a/crates/agent-gateway/test/webui/ssh-scan-paths.test.mjs b/crates/agent-gateway/test/webui/ssh-scan-paths.test.mjs deleted file mode 100644 index 388283f06..000000000 --- a/crates/agent-gateway/test/webui/ssh-scan-paths.test.mjs +++ /dev/null @@ -1,29 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { createWebModuleLoader } from "../helpers/load-web-module.mjs"; - -const loader = createWebModuleLoader(); -const scan = loader.loadModule("@/lib/ssh/scan.ts"); - -test("expandIdentityPath supports Windows SSH identity paths", () => { - const home = "C:\\Users\\Alice"; - - assert.equal(scan.expandIdentityPath(home, "~\\keys\\id_ed25519"), "C:\\Users\\Alice\\keys\\id_ed25519"); - assert.equal(scan.expandIdentityPath(home, "%USERPROFILE%\\.ssh\\id_rsa"), "C:\\Users\\Alice\\.ssh\\id_rsa"); - assert.equal(scan.expandIdentityPath(home, "%HOMEDRIVE%%HOMEPATH%\\.ssh\\id_rsa"), "C:\\Users\\Alice\\.ssh\\id_rsa"); - assert.equal(scan.expandIdentityPath(home, "C:\\Keys\\prod key"), "C:\\Keys\\prod key"); - assert.equal(scan.expandIdentityPath(home, "C:Keys\\id_rsa"), "C:\\Users\\Alice\\C:Keys\\id_rsa"); - assert.equal(scan.expandIdentityPath(home, "\\\\server\\share\\id_rsa"), "\\\\server\\share\\id_rsa"); - assert.equal(scan.expandIdentityPath(home, "\\\\?\\C:\\Keys\\id_rsa"), "\\\\?\\C:\\Keys\\id_rsa"); -}); - -test("expandIdentityPath preserves POSIX path semantics", () => { - const home = "/Users/alice"; - - assert.equal(scan.expandIdentityPath(home, "~/keys/id_ed25519"), "/Users/alice/keys/id_ed25519"); - assert.equal(scan.expandIdentityPath(home, "$HOME/.ssh/id_rsa"), "/Users/alice/.ssh/id_rsa"); - assert.equal(scan.expandIdentityPath(home, "${HOME}/.ssh/id_rsa"), "/Users/alice/.ssh/id_rsa"); - assert.equal(scan.expandIdentityPath(home, "/opt/keys/id_rsa"), "/opt/keys/id_rsa"); - assert.equal(scan.expandIdentityPath(home, "dir\\key"), "/Users/alice/dir\\key"); - assert.equal(scan.expandIdentityPath(home, "C:\\Keys\\id_rsa"), "/Users/alice/C:\\Keys\\id_rsa"); -}); diff --git a/crates/agent-gateway/test/webui/ssh-tunnel-panel.test.mjs b/crates/agent-gateway/test/webui/ssh-tunnel-panel.test.mjs deleted file mode 100644 index 682e81587..000000000 --- a/crates/agent-gateway/test/webui/ssh-tunnel-panel.test.mjs +++ /dev/null @@ -1,268 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { createWebModuleLoader } from "../helpers/load-web-module.mjs"; - -const loader = createWebModuleLoader(); -const panel = loader.loadModule("@/components/project-tools/SshTunnelPanel.tsx"); - -function host(overrides = {}) { - return { - id: "host-1", - name: "Production", - description: "", - host: "prod.example.com", - port: 22, - username: "deploy", - authType: "password", - password: "", - passwordConfigured: false, - privateKey: "", - privateKeyPath: "", - privateKeyConfigured: false, - privateKeyPassphrase: "", - privateKeyPassphraseConfigured: false, - proxy: { - type: "socks5", - url: "", - port: 0, - username: "", - password: "", - passwordConfigured: false, - }, - ...overrides, - }; -} - -test("SSH tunnel panel treats keyboard-interactive hosts as credential ready", () => { - const keyboardInteractiveHost = host({ - authType: "keyboardInteractive", - passwordConfigured: false, - privateKeyConfigured: false, - }); - - assert.equal(panel.hostSecretReady(keyboardInteractiveHost), true); - assert.equal(panel.hostStatusMessage(keyboardInteractiveHost, (key) => key), ""); -}); - -test("SSH tunnel panel does not disable hosts only because proxy is configured", () => { - const proxyHost = host({ - passwordConfigured: true, - proxy: { - type: "http", - url: "http://127.0.0.1", - port: 8080, - username: "proxy-user", - password: "", - passwordConfigured: true, - }, - }); - - assert.equal(panel.hostStatusMessage(proxyHost, (key) => key), ""); -}); - -const forwarding = loader.loadModule("@/lib/terminal/sshLocalForwardTypes.ts"); - -test("SSH local forwarding validates remote host and ports", () => { - assert.deepEqual(forwarding.validateSshLocalForwardTarget(" db.internal ", "5432"), { - remoteHost: "db.internal", - remotePort: 5432, - localPort: 0, - }); - assert.deepEqual(forwarding.validateSshLocalForwardTarget("", "5432"), { - remoteHost: "127.0.0.1", - remotePort: 5432, - localPort: 0, - }); - assert.deepEqual(forwarding.validateSshLocalForwardTarget(" ", "5432", "15432"), { - remoteHost: "127.0.0.1", - remotePort: 5432, - localPort: 15432, - }); - assert.equal(forwarding.validateSshLocalForwardTarget("db\ninternal", "5432"), null); - assert.equal(forwarding.validateSshLocalForwardTarget("db.internal", "0"), null); - assert.equal(forwarding.validateSshLocalForwardTarget("db.internal", ""), null); - assert.equal(forwarding.validateSshLocalForwardTarget("db.internal", "65536"), null); - assert.equal(forwarding.validateSshLocalForwardTarget("db.internal", "not-a-port"), null); -}); - -test("SSH local forwarding treats empty or zero local port as auto", () => { - assert.deepEqual(forwarding.validateSshLocalForwardTarget("db.internal", "5432", ""), { - remoteHost: "db.internal", - remotePort: 5432, - localPort: 0, - }); - assert.deepEqual(forwarding.validateSshLocalForwardTarget("db.internal", "5432", "0"), { - remoteHost: "db.internal", - remotePort: 5432, - localPort: 0, - }); - assert.equal(forwarding.validateSshLocalForwardTarget("db.internal", "5432", "65536"), null); - assert.equal(forwarding.validateSshLocalForwardTarget("db.internal", "5432", "-1"), null); - assert.equal(forwarding.validateSshLocalForwardTarget("db.internal", "5432", "abc"), null); -}); - -test("SSH local forwarding port drafts stay editable while typing", () => { - assert.equal(forwarding.isSshLocalForwardPortDraft(""), true); - assert.equal(forwarding.isSshLocalForwardPortDraft("0"), true); - assert.equal(forwarding.isSshLocalForwardPortDraft("65535"), true); - assert.equal(forwarding.isSshLocalForwardPortDraft("655356"), false); - assert.equal(forwarding.isSshLocalForwardPortDraft("12a"), false); - assert.equal(forwarding.isSshLocalForwardPortDraft("-1"), false); - assert.equal(forwarding.isSshLocalForwardPortDraft("1.5"), false); -}); - -test("SSH local forwarding ignores stale revisions and applies stop once", () => { - const forward = { - id: "forward-1", - sessionId: "ssh-1", - projectPathKey: "/project", - localHost: "127.0.0.1", - localPort: 49152, - address: "127.0.0.1:49152", - remoteHost: "127.0.0.1", - remotePort: 5432, - status: "active", - createdAt: 1, - updatedAt: 1, - }; - const started = forwarding.reduceSshLocalForwardState( - { forwards: [], revision: 0 }, - { kind: "started", forward, revision: 1 }, - ); - assert.deepEqual(started.forwards, [forward]); - assert.equal( - forwarding.reduceSshLocalForwardState(started, { forwards: [], revision: 0 }), - started, - ); - const stopped = forwarding.reduceSshLocalForwardState(started, { - kind: "stopped", - forward: { ...forward, status: "stopped" }, - revision: 2, - }); - assert.deepEqual(stopped, { forwards: [], revision: 2 }); - assert.equal( - forwarding.reduceSshLocalForwardState(stopped, { - kind: "stopped", - forward: { ...forward, status: "stopped" }, - revision: 2, - }), - stopped, - ); -}); - -test("SSH local forwarding relays gateway websocket operations", async () => { - const calls = []; - const fakeSocket = { - listSshLocalForwards(params) { - calls.push(["list", params]); - return Promise.resolve({ forwards: [], revision: 3 }); - }, - startSshLocalForward(params) { - calls.push(["start", params]); - return Promise.resolve({ - forward: { - id: "forward-1", - sessionId: params.sessionId, - projectPathKey: params.projectPathKey ?? "", - localHost: "127.0.0.1", - localPort: params.localPort ?? 0, - address: `127.0.0.1:${params.localPort ?? 0}`, - remoteHost: params.remoteHost, - remotePort: params.remotePort, - status: "active", - createdAt: 1, - updatedAt: 1, - }, - revision: 1, - }); - }, - stopSshLocalForward(params) { - calls.push(["stop", params]); - return Promise.resolve({ - forward: { - id: params.forwardId, - sessionId: params.sessionId ?? "", - projectPathKey: "", - localHost: "127.0.0.1", - localPort: 49152, - address: "127.0.0.1:49152", - remoteHost: "127.0.0.1", - remotePort: 5432, - status: "stopped", - createdAt: 1, - updatedAt: 2, - }, - revision: 2, - }); - }, - checkSshLocalForwardPort(port) { - calls.push(["check", port]); - return Promise.resolve(port !== 15432); - }, - subscribeSshLocalForward(listener) { - calls.push(["subscribe"]); - listener({ - kind: "started", - forward: { - id: "forward-1", - sessionId: "ssh-1", - projectPathKey: "/project", - localHost: "127.0.0.1", - localPort: 49152, - address: "127.0.0.1:49152", - remoteHost: "127.0.0.1", - remotePort: 5432, - status: "active", - createdAt: 1, - updatedAt: 1, - }, - revision: 1, - }); - return () => calls.push(["detach"]); - }, - }; - const clientLoader = createWebModuleLoader({ - mocks: { - "@/lib/gatewaySocket": { - getGatewayWebSocketClient: () => fakeSocket, - onGatewayWebSocketClientReplaced: () => () => {}, - }, - "@/lib/storage": { loadToken: () => "token" }, - }, - }); - const { gatewaySshLocalForwardClient } = clientLoader.loadModule( - "@/lib/terminal/gatewaySshLocalForwardClient.ts", - ); - - const snapshot = await gatewaySshLocalForwardClient.list({ sessionId: "ssh-1" }); - assert.deepEqual(snapshot, { forwards: [], revision: 3 }); - - const started = await gatewaySshLocalForwardClient.start({ - sessionId: "ssh-1", - projectPathKey: "/project", - remoteHost: "db.internal", - remotePort: 5432, - localPort: 15432, - }); - assert.equal(started.forward.remoteHost, "db.internal"); - - const occupied = await gatewaySshLocalForwardClient.checkLocalPort(15432); - const free = await gatewaySshLocalForwardClient.checkLocalPort(15433); - assert.equal(occupied, false); - assert.equal(free, true); - - const events = []; - const unsubscribe = await gatewaySshLocalForwardClient.subscribe((event) => { - events.push(event); - }); - unsubscribe(); - - assert.equal(events.length, 1); - assert.equal(events[0].kind, "started"); - assert.deepEqual(calls[0], ["list", { sessionId: "ssh-1" }]); - assert.equal(calls[1][0], "start"); - assert.deepEqual(calls[2], ["check", 15432]); - assert.deepEqual(calls[3], ["check", 15433]); - assert.deepEqual(calls[4], ["subscribe"]); - assert.deepEqual(calls[5], ["detach"]); -}); diff --git a/crates/agent-gateway/test/webui/terminal-session-store.test.mjs b/crates/agent-gateway/test/webui/terminal-session-store.test.mjs deleted file mode 100644 index 9f34048d0..000000000 --- a/crates/agent-gateway/test/webui/terminal-session-store.test.mjs +++ /dev/null @@ -1,203 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { createWebModuleLoader } from "../helpers/load-web-module.mjs"; - -const loader = createWebModuleLoader(); -const { - applyTerminalEventToSessions, - replaceTerminalSessionsForProject, - terminalSessionBelongsToProject, -} = loader.loadModule("src/lib/terminal/sessionStore.ts"); - -function terminal(id, projectPathKey, createdAt, title = id) { - return { - id, - projectPathKey, - cwd: projectPathKey, - shell: "zsh", - title, - cols: 80, - rows: 24, - createdAt, - updatedAt: createdAt, - running: true, - }; -} - -test("terminal project replacement only touches the requested project", () => { - const current = [ - terminal("terminal-a-1", "/workspace/a", 1), - terminal("terminal-b-1", "/workspace/b", 2), - ]; - - const next = replaceTerminalSessionsForProject(current, " /workspace/a ", [ - terminal("terminal-a-2", "/workspace/a", 3), - terminal("terminal-b-2", "/workspace/b", 4), - ]); - - assert.deepEqual( - next.map((session) => session.id), - ["terminal-a-2", "terminal-b-1"], - ); -}); - -test("terminal event merge preserves refreshed sessions and adds created terminals", () => { - const bootstrapped = replaceTerminalSessionsForProject([], "/workspace/project", [ - terminal("terminal-1", "/workspace/project", 1, "Terminal 1"), - terminal("terminal-2", "/workspace/project", 2, "Terminal 2"), - terminal("terminal-3", "/workspace/project", 3, "Terminal 3"), - ]); - - const withCreated = applyTerminalEventToSessions(bootstrapped, { - kind: "created", - sessionId: "terminal-4", - projectPathKey: "/workspace/project", - session: terminal("terminal-4", "/workspace/project", 4, "Terminal 4"), - }); - - assert.deepEqual( - withCreated.map((session) => session.title), - ["Terminal 1", "Terminal 2", "Terminal 3", "Terminal 4"], - ); -}); - -test("terminal project matching falls back to cwd when project key is missing", () => { - assert.equal( - terminalSessionBelongsToProject( - { - ...terminal("terminal-1", "", 1), - cwd: "/workspace/project", - }, - "/workspace/project", - ), - true, - ); -}); - -test("terminal project matching normalizes Windows-shaped project keys", () => { - assert.equal( - terminalSessionBelongsToProject(terminal("terminal-1", "C:\\Repo", 1), "c:/repo/"), - true, - ); - assert.deepEqual( - replaceTerminalSessionsForProject( - [terminal("old", "C:\\Repo", 1), terminal("other", "/tmp/Foo", 2)], - "c:/repo", - [terminal("new", "c:/repo", 3)], - ).map((session) => session.id), - ["other", "new"], - ); - assert.equal( - terminalSessionBelongsToProject(terminal("terminal-2", "/tmp/Foo", 1), "/tmp/foo"), - false, - ); -}); - -// --- XTermViewport chunk bookkeeping (gap / reset handling) --- -// These cases exercise the viewport's writeTerminalChunk rather than the -// session store above: the reconnect-gap "reset & replay" contract lives in -// the viewport, and this is the terminal-focused suite that loads web modules. - -const viewportLoader = createWebModuleLoader({ - mocks: { - "@xterm/xterm/css/xterm.css": {}, - "@xterm/xterm": { Terminal: class Terminal {} }, - "@xterm/addon-fit": { FitAddon: class FitAddon {} }, - }, -}); -const { writeTerminalChunk } = viewportLoader.loadModule( - "@/components/project-tools/XTermViewport.tsx", -); - -function fakeTerm() { - const calls = []; - return { - calls, - write(data) { - calls.push(["write", Uint8Array.from(data)]); - }, - reset() { - calls.push(["reset"]); - }, - }; -} - -function chunk(bytes, startOffset, endOffset) { - return { - sessionId: "terminal-1", - projectPathKey: "/workspace/project", - bytes: Uint8Array.from(bytes), - startOffset, - endOffset, - }; -} - -test("terminal chunk overlapping the rendered offset is trimmed before writing", () => { - const term = fakeTerm(); - let offset = 10; - const result = writeTerminalChunk( - term, - chunk([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 5, 15), - (next) => { - offset = next; - }, - offset, - ); - assert.equal(result, "written"); - assert.equal(offset, 15); - assert.deepEqual(term.calls, [["write", Uint8Array.from([6, 7, 8, 9, 10])]]); -}); - -test("terminal chunk entirely behind the rendered offset is skipped", () => { - const term = fakeTerm(); - let offset = 20; - const result = writeTerminalChunk( - term, - chunk([1, 2, 3], 10, 13), - (next) => { - offset = next; - }, - offset, - ); - assert.equal(result, "skipped"); - assert.equal(offset, 20); - assert.deepEqual(term.calls, []); -}); - -test("terminal chunk after a gap resets the terminal and replays the chunk", () => { - const term = fakeTerm(); - let offset = 10; - const result = writeTerminalChunk( - term, - chunk([7, 8, 9], 20, 23), - (next) => { - offset = next; - }, - offset, - ); - assert.equal(result, "reset"); - assert.equal(offset, 23); - assert.deepEqual(term.calls, [["reset"], ["write", Uint8Array.from([7, 8, 9])]]); -}); - -test("terminal chunk without offsets appends and advances by byte length", () => { - const term = fakeTerm(); - let offset = 4; - const result = writeTerminalChunk( - term, - { - sessionId: "terminal-1", - projectPathKey: "/workspace/project", - bytes: Uint8Array.from([1, 2]), - startOffset: undefined, - endOffset: undefined, - }, - (next) => { - offset = next; - }, - offset, - ); - assert.equal(result, "written"); - assert.equal(offset, 6); - assert.deepEqual(term.calls, [["write", Uint8Array.from([1, 2])]]); -}); diff --git a/crates/agent-gateway/test/webui/upload-readable-files.test.mjs b/crates/agent-gateway/test/webui/upload-readable-files.test.mjs deleted file mode 100644 index 99ce6cf02..000000000 --- a/crates/agent-gateway/test/webui/upload-readable-files.test.mjs +++ /dev/null @@ -1,197 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { createWebModuleLoader } from "../helpers/load-web-module.mjs"; - -const loader = createWebModuleLoader(); -const upload = loader.loadModule("src/lib/uploadReadableFiles.ts"); - -function installWindow() { - globalThis.window = { - location: { origin: "https://gateway.example" }, - }; -} - -function createNamedBlob(name, content, type = "text/plain") { - const blob = new Blob([content], { type }); - Object.defineProperty(blob, "name", { - value: name, - configurable: true, - }); - return blob; -} - -test("importReadableFiles validates token, agent id, and workdir before network calls", async () => { - installWindow(); - let fetchCalled = false; - globalThis.fetch = async () => { - fetchCalled = true; - throw new Error("unexpected fetch"); - }; - - await assert.rejects( - () => - upload.importReadableFiles(" ", "desktop-agent", "/workspace", [ - createNamedBlob("a.txt", "a"), - ]), - /Gateway token is required/, - ); - await assert.rejects( - () => upload.importReadableFiles("token", " ", "/workspace", [createNamedBlob("a.txt", "a")]), - /agent_id is required/, - ); - await assert.rejects( - () => - upload.importReadableFiles("token", "desktop-agent", " ", [createNamedBlob("a.txt", "a")]), - /项目目录未选择,无法导入文件。/, - ); - assert.deepEqual(await upload.importReadableFiles("token", "desktop-agent", "/workspace", []), { - files: [], - skipped: [], - }); - assert.equal(fetchCalled, false); -}); - -test("importReadableFiles posts multipart form and normalizes response files", async () => { - installWindow(); - const requests = []; - globalThis.fetch = async (url, init) => { - requests.push({ url, init }); - return { - ok: true, - async json() { - return { - files: [ - { - relativePath: "uploads/a.txt", - absolutePath: " /workspace/uploads/a.txt ", - fileName: "a.txt", - kind: "text", - sizeBytes: 12, - }, - { - relativePath: "", - fileName: "bad.bin", - kind: "binary", - sizeBytes: 9, - }, - { - relativePath: "uploads/report.docx", - fileName: "report.docx", - kind: "word", - sizeBytes: 34, - }, - { - relativePath: "uploads/screenshot.webp", - fileName: "screenshot.webp", - kind: "image", - sizeBytes: 45, - }, - { - relativePath: "uploads/report.pdf", - fileName: "report.pdf", - kind: "pdf", - sizeBytes: 67, - }, - { - relativePath: "uploads/workbook.xlsx", - fileName: "workbook.xlsx", - kind: "spreadsheet", - sizeBytes: 56, - }, - { - relativePath: "uploads/assets.zip", - fileName: "assets.zip", - kind: "archive", - sizeBytes: 78, - }, - ], - skipped: ["ignored.bin", 42], - }; - }, - }; - }; - - const result = await upload.importReadableFiles(" token ", " desktop-agent ", " /workspace ", [ - createNamedBlob("a.txt", "hello"), - createNamedBlob("b.txt", "world"), - ]); - - assert.equal(requests.length, 1); - assert.equal( - requests[0].url, - "https://gateway.example/api/files/import?agent_id=desktop-agent", - ); - assert.equal(requests[0].init.method, "POST"); - assert.equal(requests[0].init.headers.Authorization, "Bearer token"); - assert.ok(requests[0].init.body instanceof FormData); - assert.equal(requests[0].init.body.get("workdir"), "/workspace"); - const uploadedParts = requests[0].init.body.getAll("files"); - assert.equal(uploadedParts.length, 2); - assert.equal(uploadedParts[0].name, "a.txt"); - assert.equal(uploadedParts[1].name, "b.txt"); - assert.deepEqual(result, { - files: [ - { - relativePath: "uploads/a.txt", - absolutePath: "/workspace/uploads/a.txt", - fileName: "a.txt", - kind: "text", - sizeBytes: 12, - }, - { - relativePath: "uploads/report.docx", - absolutePath: undefined, - fileName: "report.docx", - kind: "word", - sizeBytes: 34, - }, - { - relativePath: "uploads/screenshot.webp", - absolutePath: undefined, - fileName: "screenshot.webp", - kind: "image", - sizeBytes: 45, - }, - { - relativePath: "uploads/report.pdf", - absolutePath: undefined, - fileName: "report.pdf", - kind: "pdf", - sizeBytes: 67, - }, - { - relativePath: "uploads/workbook.xlsx", - absolutePath: undefined, - fileName: "workbook.xlsx", - kind: "spreadsheet", - sizeBytes: 56, - }, - { - relativePath: "uploads/assets.zip", - absolutePath: undefined, - fileName: "assets.zip", - kind: "archive", - sizeBytes: 78, - }, - ], - skipped: ["ignored.bin"], - }); -}); - -test("importReadableFiles surfaces gateway error payloads", async () => { - installWindow(); - globalThis.fetch = async () => ({ - ok: false, - async text() { - return JSON.stringify({ error: "agent offline" }); - }, - }); - - await assert.rejects( - () => - upload.importReadableFiles("token", "desktop-agent", "/workspace", [ - createNamedBlob("a.txt", "a"), - ]), - /agent offline/, - ); -}); diff --git a/crates/agent-gateway/test/webui/web-model-catalog.test.mjs b/crates/agent-gateway/test/webui/web-model-catalog.test.mjs deleted file mode 100644 index 80afb7132..000000000 --- a/crates/agent-gateway/test/webui/web-model-catalog.test.mjs +++ /dev/null @@ -1,25 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { createWebModuleLoader } from "../helpers/load-web-module.mjs"; - -const loader = createWebModuleLoader(); -const catalog = loader.loadModule("@/lib/models/modelCatalog.ts"); - -// 目录模块两端逐字节镜像(scripts/mirror-manifest.json),数据不变量由 -// agent-gui/test/models/model-catalog.test.mjs 全量覆盖;这里只冒烟验证 -// web 构建树里的 seam 可用且关键数值一致。 -test("web mirror of the model catalog resolves limits and fallbacks", () => { - assert.deepEqual(catalog.resolveModelLimits("xai", "grok-4.5"), { - contextWindow: 500_000, - maxOutputToken: 32_000, - }); - assert.equal(catalog.findCatalogModel("claude_code", "claude-sonnet-4-6[1m]")?.id, "claude-sonnet-4-6"); - assert.deepEqual(catalog.getProviderFallbackLimits("xai"), { - contextWindow: 258_000, - maxOutputToken: 142_000, - }); - assert.deepEqual( - catalog.normalizeModelLimits({ contextWindow: 128_000, maxOutputToken: 128_000 }), - { contextWindow: 128_000, maxOutputToken: 32_000 }, - ); -}); diff --git a/crates/agent-gateway/test/webui/web-remote-input.test.mjs b/crates/agent-gateway/test/webui/web-remote-input.test.mjs deleted file mode 100644 index 46def815e..000000000 --- a/crates/agent-gateway/test/webui/web-remote-input.test.mjs +++ /dev/null @@ -1,16 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { createWebModuleLoader } from "../helpers/load-web-module.mjs"; - -const loader = createWebModuleLoader(); -const remoteInput = loader.loadModule("src/pages/settings/remoteInput.ts"); - -test("web remote integer drafts stay editable while preserving valid values", () => { - assert.equal(remoteInput.normalizeIntegerDraftInput(":50051"), "50051"); - assert.equal(remoteInput.normalizeIntegerDraftInput(" 12abc34 "), "1234"); - - assert.equal(remoteInput.parseIntegerDraftValue("", { min: 1, max: 65_535 }), null); - assert.equal(remoteInput.parseIntegerDraftValue("0", { min: 1, max: 65_535 }), null); - assert.equal(remoteInput.parseIntegerDraftValue("443", { min: 1, max: 65_535 }), 443); - assert.equal(remoteInput.parseIntegerDraftValue("65536", { min: 1, max: 65_535 }), 65_535); -}); diff --git a/crates/agent-gateway/test/webui/web-settings.test.mjs b/crates/agent-gateway/test/webui/web-settings.test.mjs deleted file mode 100644 index a6c19ec4d..000000000 --- a/crates/agent-gateway/test/webui/web-settings.test.mjs +++ /dev/null @@ -1,1665 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { createWebModuleLoader } from "../helpers/load-web-module.mjs"; - -const loader = createWebModuleLoader(); -const webSettings = loader.loadModule("src/lib/webSettings.ts"); -const settings = loader.loadModule("@/lib/settings/index.ts"); -const settingsSync = loader.loadModule("@/lib/settings/sync.ts"); -const chatHelpers = loader.loadModule("@/lib/chat/chatPageHelpers.ts"); -const adminApi = loader.loadModule("@/lib/adminApi.ts"); -const RIGHT_DOCK_TAB_IDS = settings.RIGHT_DOCK_SINGLETON_TAB_IDS; - -test("custom provider normalization defaults and filters ordered custom headers", () => { - assert.deepEqual(settings.normalizeCustomProvider({}).customHeaders, []); - - const provider = settings.normalizeCustomProvider({ - customHeaders: [ - { key: " X-Request-ID ", value: " request-123 " }, - { key: "", value: "ignored" }, - { key: " ", value: "ignored" }, - { key: "anthropic-beta", value: "feature-flag" }, - null, - ], - }); - - assert.deepEqual(provider.customHeaders, [ - { key: "X-Request-ID", value: " request-123 " }, - { key: "anthropic-beta", value: "feature-flag" }, - ]); -}); - -test("gateway model picker keeps same-name provider instances in separate groups", () => { - const modelOptions = chatHelpers.buildModelOptions({ - customProviders: [ - { - id: "same-api-a", - name: "Shared", - type: "codex", - activeModels: ["shared-model", "model-a"], - }, - { id: "same-api-b", name: "Shared", type: "codex", activeModels: ["shared-model"] }, - { id: "different-api", name: "Shared", type: "claude_code", activeModels: ["model-c"] }, - ], - selectedModel: { customProviderId: "same-api-b", model: "shared-model" }, - }); - - const groups = chatHelpers.groupModelOptionsByProvider(modelOptions); - - assert.deepEqual( - groups.map((group) => ({ - id: group.id, - name: group.name, - type: group.providerType, - options: group.opts.map((option) => ({ value: option.value, model: option.model })), - })), - [ - { - id: "same-api-b", - name: "Shared", - type: "codex", - options: [{ value: "same-api-b::shared-model", model: "shared-model" }], - }, - { - id: "same-api-a", - name: "Shared", - type: "codex", - options: [ - { value: "same-api-a::shared-model", model: "shared-model" }, - { value: "same-api-a::model-a", model: "model-a" }, - ], - }, - { - id: "different-api", - name: "Shared", - type: "claude_code", - options: [{ value: "different-api::model-c", model: "model-c" }], - }, - ], - ); -}); - -function installWindow(origin = "https://gateway.example") { - const store = new Map(); - globalThis.window = { - location: { origin }, - localStorage: { - getItem(key) { - return store.has(key) ? store.get(key) : null; - }, - setItem(key, value) { - store.set(key, String(value)); - }, - removeItem(key) { - store.delete(key); - }, - }, - }; - return store; -} - -test("agent directory requests database-paged status filters", async () => { - installWindow("https://gateway.example"); - const originalFetch = globalThis.fetch; - let requestUrl; - let authorization; - globalThis.fetch = async (input, init) => { - requestUrl = new URL(String(input)); - authorization = init?.headers?.Authorization; - return new Response( - JSON.stringify({ - agents: [ - { - agent_id: "shared-token-agent", - name: "", - online: false, - has_token: false, - registered_at: "2026-07-22T00:00:00Z", - }, - ], - page: 3, - page_size: 50, - total: 1, - has_more: false, - }), - { status: 200, headers: { "content-type": "application/json" } }, - ); - }; - - try { - const result = await adminApi.listAdminAgents(" gateway-token ", 3, 50, "offline"); - assert.equal(requestUrl.pathname, "/api/agents"); - assert.equal(requestUrl.searchParams.get("page"), "3"); - assert.equal(requestUrl.searchParams.get("page_size"), "50"); - assert.equal(requestUrl.searchParams.get("status"), "offline"); - assert.equal(authorization, "Bearer gateway-token"); - assert.equal(result.page, 3); - assert.equal(result.agents[0].has_token, false); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("agent management validates generated IDs and uses the single-record API", async () => { - installWindow("https://gateway.example"); - const agentId = "agent-550e8400-e29b-41d4-a716-446655440000"; - assert.equal(adminApi.isGeneratedAgentID(` ${agentId} `), true); - assert.equal(adminApi.isGeneratedAgentID("agent-550e8400-e29b-11d4-a716-446655440000"), false); - assert.equal(adminApi.isGeneratedAgentID("agent-550E8400-E29B-41D4-A716-446655440000"), false); - assert.equal(adminApi.isGeneratedAgentID("manual-agent"), false); - - const originalFetch = globalThis.fetch; - const requests = []; - globalThis.fetch = async (input, init = {}) => { - requests.push({ url: new URL(String(input)), init }); - if (init.method === "POST") { - return new Response(JSON.stringify({ token: "agt_plaintext" }), { - status: 200, - headers: { "content-type": "application/json" }, - }); - } - return new Response("{}", { status: 200, headers: { "content-type": "application/json" } }); - }; - - try { - const issued = await adminApi.issueAdminToken(" gateway-token ", agentId, " 办公室电脑 "); - await adminApi.updateAdminAgentName("gateway-token", agentId, " "); - await adminApi.deleteAdminAgent("gateway-token", agentId); - - assert.equal(issued, "agt_plaintext"); - assert.equal(requests.length, 3); - assert.equal(requests[0].url.pathname, `/api/agents/${agentId}/token`); - assert.equal(requests[0].init.method, "POST"); - assert.equal(requests[0].init.headers.Authorization, "Bearer gateway-token"); - assert.equal(requests[0].init.headers["Content-Type"], "application/json"); - assert.equal(requests[0].init.body, JSON.stringify({ name: "办公室电脑" })); - assert.equal(requests[1].url.pathname, `/api/agents/${agentId}`); - assert.equal(requests[1].init.method, "PATCH"); - assert.equal(requests[1].init.body, JSON.stringify({ name: "" })); - assert.equal(requests[2].url.pathname, `/api/agents/${agentId}`); - assert.equal(requests[2].init.method, "DELETE"); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("getWebDefaultSettings enables remote settings from the gateway token", () => { - installWindow("https://gateway.example"); - - const settings = webSettings.getWebDefaultSettings(" token "); - assert.equal(settings.system.executionMode, "tools"); - assert.equal(settings.system.workdir, ""); - assert.equal(settings.remote.enabled, true); - assert.equal(settings.remote.gatewayUrl, "https://gateway.example"); - assert.equal(settings.remote.token, "token"); -}); - -test("web settings normalize independent font families and migrate the retired interface field", () => { - const migrated = settings.normalizeSettings({ customSettings: { fontFamily: "Inter" } }); - assert.equal(migrated.customSettings.interfaceFontFamily, "Inter"); - assert.equal(Object.hasOwn(migrated.customSettings, "fontFamily"), false); - - const normalized = settings.normalizeSettings({ - customSettings: { - interfaceFontFamily: 'Inter, "PingFang SC", sans-serif', - chatFontFamily: "rounded", - codeFontFamily: "Menlo", - }, - }); - assert.equal(normalized.customSettings.interfaceFontFamily, 'Inter, "PingFang SC", sans-serif'); - assert.equal(normalized.customSettings.chatFontFamily, "rounded"); - assert.equal(normalized.customSettings.codeFontFamily, "Menlo"); - assert.equal( - settings.normalizeSettings({ customSettings: { codeFontFamily: "invalid;}" } }).customSettings - .codeFontFamily, - "", - ); -}); - -test("web settings normalization canonicalizes project keyed maps with Windows path compatibility", () => { - const normalized = settings.normalizeSettings({ - ssh: { - hosts: [ - { id: "host-a", host: "example.com", username: "me" }, - { id: "host-b", host: "example.org", username: "me" }, - ], - projectHostAssociations: { - "c:/repo": ["host-b"], - "C:\\Repo\\": ["host-a"], - }, - }, - customSettings: { - rightDock: { - projects: { - "C:\\Repo\\": { - activeTabId: RIGHT_DOCK_TAB_IDS.fileTree, - tabOrder: [ - RIGHT_DOCK_TAB_IDS.gitReview, - "", - RIGHT_DOCK_TAB_IDS.fileTree, - RIGHT_DOCK_TAB_IDS.fileTree, - "x".repeat(200), - ], - tabs: { - [RIGHT_DOCK_TAB_IDS.fileTree]: { - id: RIGHT_DOCK_TAB_IDS.fileTree, - kind: "fileTree", - projectPathKey: "C:\\Repo\\", - createdAt: 1, - uiState: { - query: "legacy", - selectedPath: "src\\main.ts", - expandedPaths: ["", "src", "src\\components", "src"], - showHidden: true, - revision: 2, - }, - }, - [RIGHT_DOCK_TAB_IDS.gitReview]: { - id: RIGHT_DOCK_TAB_IDS.gitReview, - kind: "gitReview", - projectPathKey: "C:\\Repo\\", - createdAt: 2, - }, - invalid: { - id: "invalid", - kind: "unknown", - projectPathKey: "C:\\Repo\\", - createdAt: 3, - }, - }, - }, - }, - }, - }, - }); - - assert.deepEqual(normalized.ssh.projectHostAssociations, { - "c:/repo": ["host-b"], - }); - assert.deepEqual(Object.keys(normalized.customSettings.rightDock.projects), ["c:/repo"]); - assert.deepEqual(normalized.customSettings.rightDock.projects["c:/repo"], { - activeTabId: RIGHT_DOCK_TAB_IDS.fileTree, - tabOrder: [RIGHT_DOCK_TAB_IDS.gitReview, RIGHT_DOCK_TAB_IDS.fileTree], - tools: { - fileTree: { - openedAt: 1, - uiState: { - query: "legacy", - selectedPath: "src/main.ts", - expandedPaths: ["", "src", "src/components"], - showHidden: true, - revision: 2, - }, - }, - gitReview: { - openedAt: 2, - }, - }, - openVersion: 0, - stateVersion: 0, - writerId: "", - lastUsedAt: 0, - }); -}); - -test("web chat runtime controls default and follow model-aware reasoning support", () => { - installWindow("https://gateway.example"); - - const defaults = webSettings.getWebDefaultSettings(" token "); - assert.deepEqual(defaults.chatRuntimeControls, { - thinkingEnabled: true, - nativeWebSearchEnabled: true, - reasoning: "high", - reasoningByProvider: { - claude_code: "high", - codex_openai_responses: "high", - codex_openai_completions: "high", - gemini: "high", - xai: "high", - }, - }); - - assert.deepEqual(settings.getChatRuntimeReasoningLevelsForProvider({}), []); - // 档位全部来自生成目录(models.dev):adaptive 世代无 minimal 档。 - assert.deepEqual( - settings.getChatRuntimeReasoningLevelsForProvider({ - providerId: "claude_code", - modelId: "claude-opus-4-8", - }), - ["low", "medium", "high", "xhigh", "max"], - ); - assert.deepEqual( - settings.getChatRuntimeReasoningLevelsForProvider({ - providerId: "claude_code", - modelId: "claude-sonnet-4-6", - }), - ["low", "medium", "high", "max"], - ); - assert.deepEqual( - settings.getChatRuntimeReasoningLevelsForProvider({ - providerId: "codex", - requestFormat: "openai-responses", - modelId: "gpt-5.2", - }), - ["low", "medium", "high", "xhigh"], - ); - assert.deepEqual( - settings.getChatRuntimeReasoningLevelsForProvider({ - providerId: "codex", - requestFormat: "openai-completions", - modelId: "gpt-5", - }), - ["minimal", "low", "medium", "high"], - ); - assert.deepEqual( - settings.getChatRuntimeReasoningLevelsForProvider({ - providerId: "gemini", - modelId: "gemini-2.5-pro", - }), - ["minimal", "low", "medium", "high"], - ); - // 中转挂载的国产厂商模型走跨供应商回查命中真实形态:glm-4.7 纯 toggle - //(单 "high" 档),deepseek-reasoner 恒开不可调(无档位),deepseek-chat - // 非思考模型(思考控件整组隐藏)。 - assert.deepEqual( - settings.getChatRuntimeReasoningLevelsForProvider({ - providerId: "codex", - requestFormat: "openai-completions", - modelId: "glm-4.7", - }), - ["high"], - ); - assert.deepEqual( - settings.getChatRuntimeReasoningLevelsForProvider({ - providerId: "claude_code", - modelId: "deepseek-reasoner", - }), - [], - ); - assert.equal(settings.isThinkingAlwaysOnForModel("claude_code", "deepseek-reasoner"), true); - assert.deepEqual( - settings.getChatRuntimeReasoningLevelsForProvider({ - providerId: "codex", - modelId: "deepseek-chat", - }), - [], - ); - - assert.equal(settings.isThinkingAlwaysOnForModel("claude_code", "claude-fable-5"), true); - assert.equal(settings.isThinkingAlwaysOnForModel("claude_code", "claude-opus-4-8"), false); - assert.equal(settings.isThinkingAlwaysOnForModel("claude_code", undefined), false); - - // 中转装饰过的 Anthropic id(日期后缀/大小写/@版本)按规范化后的目录条目解析, - // xhigh/max 档位与"思考不可关"语义不丢失;与桌面端 modelFactory 同步。 - assert.deepEqual( - settings.getChatRuntimeReasoningLevelsForProvider({ - providerId: "claude_code", - modelId: "claude-opus-4-8-20260213", - }), - ["low", "medium", "high", "xhigh", "max"], - ); - assert.deepEqual( - settings.getChatRuntimeReasoningLevelsForProvider({ - providerId: "claude_code", - modelId: "claude-sonnet-4-6-20251114", - }), - ["low", "medium", "high", "max"], - ); - assert.equal(settings.isThinkingAlwaysOnForModel("claude_code", "Claude-Fable-5"), true); - // 目录彻底未命中的三方改名 id 走 id 启发式补 xhigh/max(adaptive 世代无 minimal)。 - assert.deepEqual( - settings.getChatRuntimeReasoningLevelsForProvider({ - providerId: "claude_code", - modelId: "claude-4.6-sonnet", - }), - ["low", "medium", "high", "max"], - ); - assert.deepEqual( - settings.getChatRuntimeReasoningLevelsForProvider({ - providerId: "claude_code", - modelId: "claude-5-sonnet", - }), - ["low", "medium", "high", "xhigh", "max"], - ); - // 旧世代 id 不误判。 - assert.deepEqual( - settings.getChatRuntimeReasoningLevelsForProvider({ - providerId: "claude_code", - modelId: "claude-3-5-sonnet-20241022", - }), - ["minimal", "low", "medium", "high"], - ); - - assert.deepEqual( - settings.normalizeChatRuntimeControlsForProvider( - { - thinkingEnabled: false, - nativeWebSearchEnabled: false, - reasoning: "xhigh", - reasoningByProvider: { - gemini: "xhigh", - }, - }, - { providerId: "gemini", modelId: "gemini-2.5-pro" }, - ), - { - thinkingEnabled: false, - nativeWebSearchEnabled: false, - reasoning: "high", - reasoningByProvider: { - claude_code: "xhigh", - codex_openai_responses: "xhigh", - codex_openai_completions: "xhigh", - gemini: "high", - xai: "xhigh", - }, - }, - ); - assert.deepEqual( - settings.normalizeChatRuntimeControlsForProvider( - { - thinkingEnabled: true, - nativeWebSearchEnabled: true, - reasoning: "xhigh", - reasoningByProvider: { - codex_openai_completions: "xhigh", - }, - }, - { providerId: "codex", requestFormat: "openai-completions", modelId: "gpt-5.2" }, - ), - { - thinkingEnabled: true, - nativeWebSearchEnabled: true, - reasoning: "xhigh", - reasoningByProvider: { - claude_code: "xhigh", - codex_openai_responses: "xhigh", - codex_openai_completions: "xhigh", - // gemini / xai 未在 reasoningByProvider 输入里显式给出,也未参与本次调用 - // 的当前 provider key,因此只继承顶层 reasoning 原值,不做钳制。 - gemini: "xhigh", - xai: "xhigh", - }, - }, - ); - - assert.deepEqual( - settings.updateChatRuntimeControlsForProvider( - defaults.chatRuntimeControls, - { reasoning: "xhigh" }, - { providerId: "codex", requestFormat: "openai-responses", modelId: "gpt-5.2" }, - ), - { - thinkingEnabled: true, - nativeWebSearchEnabled: true, - reasoning: "xhigh", - reasoningByProvider: { - claude_code: "high", - codex_openai_responses: "xhigh", - codex_openai_completions: "high", - gemini: "high", - xai: "high", - }, - }, - ); - assert.equal( - settings.normalizeChatRuntimeControlsForProvider( - { - ...defaults.chatRuntimeControls, - reasoningByProvider: { - ...defaults.chatRuntimeControls.reasoningByProvider, - claude_code: "xhigh", - gemini: "low", - }, - }, - { providerId: "claude_code", modelId: "claude-opus-4-8" }, - ).reasoning, - "xhigh", - ); - assert.equal( - settings.normalizeChatRuntimeControlsForProvider( - { - ...defaults.chatRuntimeControls, - reasoningByProvider: { - ...defaults.chatRuntimeControls.reasoningByProvider, - claude_code: "xhigh", - gemini: "low", - }, - }, - { providerId: "gemini", modelId: "gemini-2.5-pro" }, - ).reasoning, - "low", - ); - assert.equal( - settings.normalizeChatRuntimeControlsForProvider(defaults.chatRuntimeControls, { - providerId: "claude_code", - modelId: "not-a-real-model", - }).reasoning, - "high", - ); -}); - -test("Anthropic settings keep 1M context parity for adaptive and explicit relay suffix models", () => { - assert.equal( - settings.getProviderModelDefaults("claude_code", "claude-sonnet-4-6").contextWindow, - 1_000_000, - ); - assert.equal( - settings.getProviderModelDefaults("claude_code", "claude-sonnet-4-5").contextWindow, - 200_000, - ); - assert.equal( - settings.getProviderModelDefaults("claude_code", "claude-sonnet-4-5[1m]").contextWindow, - 1_000_000, - ); - assert.equal( - settings.getProviderModelDefaults("claude_code", "claude-4.6-sonnet").contextWindow, - 1_000_000, - ); - assert.equal( - settings.findProviderModelConfig( - { models: [], type: "claude_code", baseUrl: "https://relay.example.com/v1" }, - "claude-sonnet-4-5[1m]", - ).contextWindow, - 1_000_000, - ); - assert.equal( - settings.findProviderModelConfig( - { models: [], type: "claude_code", baseUrl: "https://api.anthropic.com/v1" }, - "claude-sonnet-4-5[1m]", - ).contextWindow, - 200_000, - ); -}); - -test("loadWebSettings forces current gateway URL/token over stale persisted remote settings", () => { - const store = installWindow("https://new.example"); - const stale = webSettings.getWebDefaultSettings("old-token"); - stale.remote.gatewayUrl = "https://old.example"; - stale.remote.token = "old-token"; - stale.system.workdir = "/workspace"; - stale.customSettings.rightDock = { - width: 612, - projects: { - "/stale/project": { - activeTabId: RIGHT_DOCK_TAB_IDS.fileTree, - tabOrder: [RIGHT_DOCK_TAB_IDS.fileTree], - tabs: { - [RIGHT_DOCK_TAB_IDS.fileTree]: { - id: RIGHT_DOCK_TAB_IDS.fileTree, - kind: "fileTree", - projectPathKey: "/stale/project", - createdAt: 1, - }, - }, - openVersion: 1, - stateVersion: 1, - }, - }, - }; - store.set("liveagent.gateway.webui.settings.v1", JSON.stringify(stale)); - - const loaded = webSettings.loadWebSettings(" new-token "); - assert.equal(loaded.system.workdir, "/workspace"); - assert.equal(loaded.remote.gatewayUrl, "https://new.example"); - assert.equal(loaded.remote.token, "new-token"); - assert.equal(loaded.remote.enabled, true); - assert.equal(loaded.customSettings.rightDock.width, 612); - assert.deepEqual(Object.keys(loaded.customSettings.rightDock.projects), ["/stale/project"]); -}); - -test("gateway settings sync keeps remote connection local and syncs web terminal setting", () => { - installWindow(); - const current = webSettings.getWebDefaultSettings("token"); - const synced = settingsSync.applyGatewaySettingsSyncPayload(current, { - system: { - executionMode: "tools", - workdir: "/remote-workdir", - }, - chatRuntimeControls: { - thinkingEnabled: false, - nativeWebSearchEnabled: false, - reasoning: "minimal", - reasoningByProvider: { - claude_code: "minimal", - codex_openai_responses: "minimal", - codex_openai_completions: "high", - gemini: "xhigh", - }, - }, - selectedModel: null, - }); - - assert.equal(synced.system.executionMode, "tools"); - assert.equal(synced.system.workdir, "/remote-workdir"); - assert.equal(synced.chatRuntimeControls.thinkingEnabled, false); - assert.equal(synced.chatRuntimeControls.nativeWebSearchEnabled, false); - assert.equal(synced.chatRuntimeControls.reasoning, "minimal"); - assert.equal(synced.chatRuntimeControls.reasoningByProvider.claude_code, "minimal"); - assert.equal(synced.chatRuntimeControls.reasoningByProvider.codex_openai_responses, "minimal"); - assert.equal(synced.chatRuntimeControls.reasoningByProvider.gemini, "xhigh"); - assert.equal(synced.selectedModel, undefined); - assert.equal(synced.remote.gatewayUrl, "https://gateway.example"); - assert.equal(synced.remote.token, "token"); - - const payload = settingsSync.buildGatewaySettingsSyncPayload(synced); - assert.deepEqual(payload.remote, { - enableWebTerminal: synced.remote.enableWebTerminal, - enableWebSshTerminal: synced.remote.enableWebSshTerminal, - enableWebGit: synced.remote.enableWebGit, - enableWebTunnels: synced.remote.enableWebTunnels, - }); - assert.deepEqual(payload.chatRuntimeControls, synced.chatRuntimeControls); -}); - -test("ssh settings sync redacts stored secrets and carries one-shot secret updates", () => { - installWindow(); - const source = settings.normalizeSettings({ - ssh: { - hosts: [ - { - id: "ssh-prod", - name: "Prod", - description: "Production jump host", - host: "prod.example.com", - port: 2222, - username: "deploy", - authType: "privateKey", - password: "ssh-password", - privateKey: "-----BEGIN OPENSSH PRIVATE KEY-----", - privateKeyPath: "~/.ssh/prod", - proxy: { - type: "http", - url: "http://127.0.0.1", - port: 1080, - username: "proxy-user", - password: "proxy-password", - }, - }, - ], - projectHostAssociations: { - "/project-a": ["ssh-prod", "missing-host", "ssh-prod"], - " ": ["ssh-prod"], - }, - }, - }); - assert.deepEqual(source.ssh.projectHostAssociations, { - "/project-a": ["ssh-prod"], - }); - - const redacted = settingsSync.redactSettingsForWebStorage(source); - assert.deepEqual(redacted.ssh.projectHostAssociations, { - "/project-a": ["ssh-prod"], - }); - assert.equal(redacted.ssh.hosts[0].password, ""); - assert.equal(redacted.ssh.hosts[0].privateKey, ""); - assert.equal(redacted.ssh.hosts[0].proxy.type, "http"); - assert.equal(redacted.ssh.hosts[0].proxy.password, ""); - assert.equal(redacted.ssh.hosts[0].passwordConfigured, true); - assert.equal(redacted.ssh.hosts[0].privateKeyConfigured, true); - assert.equal(redacted.ssh.hosts[0].proxy.passwordConfigured, true); - - const publicPayload = settingsSync.buildGatewaySettingsSyncPayload(source); - assert.deepEqual(publicPayload.ssh.projectHostAssociations, { - "/project-a": ["ssh-prod"], - }); - assert.equal(publicPayload.ssh.hosts[0].password, ""); - assert.equal(publicPayload.ssh.hosts[0].privateKey, ""); - assert.equal(publicPayload.ssh.hosts[0].proxy.type, "http"); - assert.equal(publicPayload.ssh.hosts[0].proxy.password, ""); - assert.equal(publicPayload.ssh.hosts[0].passwordConfigured, true); - assert.equal(publicPayload.ssh.hosts[0].privateKeyConfigured, true); - assert.equal(publicPayload.ssh.hosts[0].proxy.passwordConfigured, true); - assert.equal(Object.hasOwn(publicPayload, "sshSecretUpdates"), false); - - const privatePayload = settingsSync.buildGatewaySettingsSyncPayload(source, { - includeProviderApiKeyUpdates: true, - }); - assert.deepEqual(privatePayload.ssh.projectHostAssociations, { - "/project-a": ["ssh-prod"], - }); - assert.equal(privatePayload.ssh.hosts[0].password, ""); - assert.equal(privatePayload.ssh.hosts[0].privateKey, ""); - assert.equal(privatePayload.ssh.hosts[0].proxy.type, "http"); - assert.equal(privatePayload.ssh.hosts[0].proxy.password, ""); - assert.deepEqual(privatePayload.sshSecretUpdates, { - "ssh-prod": { - password: "ssh-password", - privateKey: "-----BEGIN OPENSSH PRIVATE KEY-----", - proxyPassword: "proxy-password", - }, - }); -}); - -test("ssh keyboard-interactive hosts normalize without credential secrets or secret updates", () => { - installWindow(); - const appSettings = settings.normalizeSettings({ - ssh: { - hosts: [ - { - id: "kbi-prod", - name: "Keyboard Interactive Production", - host: "prod.example.com", - username: "deploy", - authType: "keyboardInteractive", - password: "old-password", - passwordConfigured: true, - privateKey: "old-key", - privateKeyPath: "~/.ssh/id_rsa", - privateKeyConfigured: true, - privateKeyPassphrase: "old-passphrase", - privateKeyPassphraseConfigured: true, - proxy: { - type: "http", - url: "http://127.0.0.1", - port: 8080, - username: "proxy-user", - password: "proxy-password", - }, - }, - ], - }, - }); - - const host = appSettings.ssh.hosts[0]; - assert.equal(host.authType, "keyboardInteractive"); - assert.equal(host.password, ""); - assert.equal(host.passwordConfigured, false); - assert.equal(host.privateKey, ""); - assert.equal(host.privateKeyPath, ""); - assert.equal(host.privateKeyConfigured, false); - assert.equal(host.privateKeyPassphrase, ""); - assert.equal(host.privateKeyPassphraseConfigured, false); - - const payload = settingsSync.buildGatewaySettingsSyncPayload(appSettings, { - includeProviderApiKeyUpdates: true, - }); - assert.deepEqual(payload.sshSecretUpdates, { - "kbi-prod": { proxyPassword: "proxy-password" }, - }); - assert.equal(payload.ssh.hosts[0].passwordConfigured, false); - assert.equal(payload.ssh.hosts[0].privateKeyConfigured, false); - assert.equal(payload.ssh.hosts[0].privateKeyPassphraseConfigured, false); - assert.equal(payload.ssh.hosts[0].proxy.password, ""); - assert.equal(payload.ssh.hosts[0].proxy.passwordConfigured, true); -}); - -test("legacy ssh agent hosts fall back to password auth", () => { - installWindow(); - const appSettings = settings.normalizeSettings({ - ssh: { - hosts: [ - { - id: "legacy-agent", - name: "Legacy Agent", - host: "legacy.example.com", - username: "deploy", - authType: "agent", - }, - ], - }, - }); - - const host = appSettings.ssh.hosts[0]; - assert.equal(host.authType, "password"); - assert.equal(host.password, ""); - assert.equal(host.passwordConfigured, false); -}); - -test("ssh settings sync merges one-shot secret updates into existing hosts", () => { - installWindow(); - const current = settings.normalizeSettings({ - ssh: { - hosts: [ - { - id: "ssh-prod", - name: "Prod", - host: "prod.example.com", - username: "deploy", - authType: "password", - password: "old-password", - privateKey: "old-key", - proxy: { - type: "socks5", - url: "socks5://127.0.0.1", - port: 1080, - username: "proxy-user", - password: "old-proxy-password", - }, - }, - ], - }, - }); - - const synced = settingsSync.applyGatewaySettingsSyncPayload(current, { - ssh: { - hosts: [ - { - id: "ssh-prod", - name: "Prod", - host: "prod.example.com", - username: "deploy", - authType: "privateKey", - password: "", - passwordConfigured: true, - privateKey: "", - privateKeyPath: "~/.ssh/prod", - privateKeyConfigured: true, - proxy: { - type: "http", - url: "http://127.0.0.1", - port: 1080, - username: "proxy-user", - password: "", - passwordConfigured: true, - }, - }, - ], - projectHostAssociations: { - "/project-a": ["ssh-prod"], - }, - }, - sshSecretUpdates: { - "ssh-prod": { - password: "new-password", - privateKey: "new-key", - proxyPassword: "new-proxy-password", - }, - }, - }); - - assert.equal(synced.ssh.hosts[0].authType, "privateKey"); - assert.equal(synced.ssh.hosts[0].password, "new-password"); - assert.equal(synced.ssh.hosts[0].privateKey, "new-key"); - assert.equal(synced.ssh.hosts[0].proxy.type, "http"); - assert.equal(synced.ssh.hosts[0].proxy.password, "new-proxy-password"); - assert.equal(synced.ssh.hosts[0].passwordConfigured, true); - assert.equal(synced.ssh.hosts[0].privateKeyConfigured, true); - assert.equal(synced.ssh.hosts[0].proxy.passwordConfigured, true); - assert.deepEqual(synced.ssh.projectHostAssociations, { - "/project-a": ["ssh-prod"], - }); -}); - -test("ssh settings sync preserves project host associations when older payload omits them", () => { - installWindow(); - const current = settings.normalizeSettings({ - ssh: { - hosts: [ - { - id: "ssh-prod", - name: "Prod", - host: "prod.example.com", - username: "deploy", - authType: "password", - }, - ], - projectHostAssociations: { - "/project-a": ["ssh-prod"], - }, - }, - }); - - const preserved = settingsSync.applyGatewaySettingsSyncPayload(current, { - ssh: { - hosts: [ - { - id: "ssh-prod", - name: "Prod", - host: "prod.example.com", - username: "deploy", - authType: "password", - }, - ], - }, - }); - assert.deepEqual(preserved.ssh.projectHostAssociations, { - "/project-a": ["ssh-prod"], - }); - - const cleared = settingsSync.applyGatewaySettingsSyncPayload(current, { - ssh: { - hosts: [ - { - id: "ssh-prod", - name: "Prod", - host: "prod.example.com", - username: "deploy", - authType: "password", - }, - ], - projectHostAssociations: {}, - }, - }); - assert.deepEqual(cleared.ssh.projectHostAssociations, {}); -}); - -test("settings update payload omits unchanged empty ssh hosts for non-ssh updates", () => { - installWindow(); - const desktop = settings.normalizeSettings({ - ssh: { - hosts: [ - { - id: "ssh-prod", - name: "Prod", - host: "prod.example.com", - username: "deploy", - authType: "password", - }, - ], - projectHostAssociations: { - "/project-a": ["ssh-prod"], - }, - }, - }); - const staleWeb = settings.normalizeSettings({ - ssh: { - hosts: [], - projectHostAssociations: {}, - }, - }); - const nextWeb = settings.openRightDockSingletonTab(staleWeb, "/project-a", "sshTunnel"); - - const update = settingsSync.buildGatewaySettingsSyncUpdatePayload(staleWeb, nextWeb, { - includeProviderApiKeyUpdates: true, - }); - - assert.equal(Object.hasOwn(update, "ssh"), false); - assert.equal(Object.hasOwn(update, "customSettings"), true); - - const merged = settingsSync.applyGatewaySettingsSyncPayload(desktop, update); - assert.deepEqual( - merged.ssh.hosts.map((host) => host.id), - ["ssh-prod"], - ); - assert.deepEqual(merged.ssh.projectHostAssociations, { - "/project-a": ["ssh-prod"], - }); - assert.equal( - settings.isRightDockSingletonTabOpen(merged.customSettings, "/project-a", "sshTunnel"), - true, - ); -}); - -test("settings update payload uses sshPatch when hosts are explicitly deleted", () => { - installWindow(); - const current = settings.normalizeSettings({ - ssh: { - hosts: [ - { - id: "ssh-prod", - name: "Prod", - host: "prod.example.com", - username: "deploy", - authType: "password", - }, - ], - projectHostAssociations: { - "/project-a": ["ssh-prod"], - }, - }, - }); - const deleted = settings.updateSsh(current, { - hosts: [], - projectHostAssociations: {}, - }); - - const update = settingsSync.buildGatewaySettingsSyncUpdatePayload(current, deleted, { - includeProviderApiKeyUpdates: true, - }); - - assert.equal(Object.hasOwn(update, "ssh"), false); - assert.deepEqual(update.sshPatch.hostChanges, [ - { - id: "ssh-prod", - before: { - ...current.ssh.hosts[0], - password: "", - passwordConfigured: false, - privateKey: "", - privateKeyConfigured: false, - privateKeyPassphrase: "", - privateKeyPassphraseConfigured: false, - proxy: { - type: "socks5", - url: "", - port: 0, - username: "", - password: "", - passwordConfigured: false, - }, - }, - after: null, - }, - ]); - assert.deepEqual(update.sshPatch.projectAssociationChanges, [ - { - pathKey: "/project-a", - before: ["ssh-prod"], - after: [], - }, - ]); -}); - -test("settings update payload uses sshSecretUpdates for secret-only ssh updates", () => { - installWindow(); - const current = settings.normalizeSettings({ - ssh: { - hosts: [ - { - id: "ssh-prod", - name: "Prod", - host: "prod.example.com", - username: "deploy", - authType: "password", - password: "old-password", - }, - ], - }, - }); - const next = settings.normalizeSettings({ - ...current, - ssh: { - ...current.ssh, - hosts: [ - { - ...current.ssh.hosts[0], - password: "new-password", - }, - ], - }, - }); - - const update = settingsSync.buildGatewaySettingsSyncUpdatePayload(current, next, { - includeProviderApiKeyUpdates: true, - }); - - assert.equal(Object.hasOwn(update, "ssh"), false); - assert.deepEqual(update.sshPatch, {}); - assert.deepEqual(update.sshSecretUpdates, { - "ssh-prod": { - password: "new-password", - }, - }); - - const merged = settingsSync.applyGatewaySettingsSyncPayload(current, update); - assert.equal(merged.ssh.hosts[0].password, "new-password"); -}); - -test("settings update payload omits unchanged ssh secrets", () => { - installWindow(); - const current = settings.normalizeSettings({ - ssh: { - hosts: [ - { - id: "ssh-prod", - name: "Prod", - host: "prod.example.com", - username: "deploy", - authType: "password", - password: "prod-password", - }, - { - id: "ssh-staging", - name: "Staging", - host: "staging.example.com", - username: "deploy", - authType: "password", - password: "staging-password", - }, - ], - }, - }); - const next = settings.normalizeSettings({ - ...current, - ssh: { - ...current.ssh, - hosts: [ - { - ...current.ssh.hosts[0], - host: "prod.internal", - }, - current.ssh.hosts[1], - ], - }, - }); - - const update = settingsSync.buildGatewaySettingsSyncUpdatePayload(current, next, { - includeProviderApiKeyUpdates: true, - }); - - assert.equal(Object.hasOwn(update, "ssh"), false); - assert.equal(update.sshSecretUpdates, undefined); - assert.equal(update.sshPatch.hostChanges.length, 1); - assert.equal(update.sshPatch.hostChanges[0].id, "ssh-prod"); -}); - -test("settings update payload sends empty ssh secret updates when secrets are cleared", () => { - installWindow(); - const current = settings.normalizeSettings({ - ssh: { - hosts: [ - { - id: "ssh-prod", - name: "Prod", - host: "prod.example.com", - username: "deploy", - authType: "password", - password: "old-password", - }, - ], - }, - }); - const next = settings.normalizeSettings({ - ...current, - ssh: { - ...current.ssh, - hosts: [ - { - ...current.ssh.hosts[0], - password: "", - passwordConfigured: false, - }, - ], - }, - }); - - const update = settingsSync.buildGatewaySettingsSyncUpdatePayload(current, next, { - includeProviderApiKeyUpdates: true, - }); - - assert.equal(Object.hasOwn(update, "ssh"), false); - assert.deepEqual(update.sshSecretUpdates, { - "ssh-prod": { - password: "", - }, - }); - - const merged = settingsSync.applyGatewaySettingsSyncPayload(current, update); - assert.equal(merged.ssh.hosts[0].password, ""); - assert.equal(merged.ssh.hosts[0].passwordConfigured, false); -}); - -test("settings update payload clears redacted configured ssh secrets", () => { - installWindow(); - const current = settings.normalizeSettings({ - ssh: { - hosts: [ - { - id: "ssh-prod", - name: "Prod", - host: "prod.example.com", - username: "deploy", - authType: "password", - password: "", - passwordConfigured: true, - }, - ], - }, - }); - const next = settings.normalizeSettings({ - ...current, - ssh: { - ...current.ssh, - hosts: [ - { - ...current.ssh.hosts[0], - passwordConfigured: false, - }, - ], - }, - }); - - const update = settingsSync.buildGatewaySettingsSyncUpdatePayload(current, next, { - includeProviderApiKeyUpdates: true, - }); - - assert.deepEqual(update.sshSecretUpdates, { - "ssh-prod": { - password: "", - }, - }); -}); - -test("workspace project selection stays out of synced system workdir", () => { - installWindow(); - const resolvedSystem = settings.resolveWorkspaceProjects( - { - ...settings.getDefaultSettings().system, - executionMode: "tools", - workdir: "/default-workdir", - workspaceProjects: [ - { - id: "project-a", - name: "Project A", - path: "/project-a", - kind: "folder", - createdAt: 1, - updatedAt: 1, - }, - ], - activeWorkspaceProjectId: "project-a", - }, - "/default-workdir", - ); - - assert.equal(resolvedSystem.workdir, "/default-workdir"); - assert.equal(resolvedSystem.activeWorkspaceProjectId, "project-a"); - - const payload = settingsSync.buildGatewaySettingsSyncPayload( - settings.normalizeSettings({ - system: resolvedSystem, - }), - ); - assert.equal(Object.hasOwn(payload.system, "activeWorkspaceProjectId"), false); - assert.equal(payload.system.workdir, "/default-workdir"); -}); - -test("gateway settings sync preserves active workspace project by path when ids differ", () => { - installWindow(); - const current = settings.normalizeSettings({ - system: settings.resolveWorkspaceProjects( - { - ...settings.getDefaultSettings().system, - executionMode: "tools", - workdir: "/default-workdir", - workspaceProjects: [ - { - id: "web-project-a", - name: "Project A", - path: "/project-a", - kind: "folder", - createdAt: 1, - updatedAt: 1, - }, - ], - activeWorkspaceProjectId: "web-project-a", - }, - "/default-workdir", - ), - }); - const incoming = settingsSync.buildGatewaySettingsSyncPayload( - settings.normalizeSettings({ - system: settings.resolveWorkspaceProjects( - { - ...settings.getDefaultSettings().system, - executionMode: "tools", - workdir: "/default-workdir", - workspaceProjects: [ - { - id: "desktop-project-a", - name: "Project A", - path: "/project-a", - kind: "folder", - createdAt: 2, - updatedAt: 2, - }, - ], - }, - "/default-workdir", - ), - }), - ); - - const synced = settingsSync.applyGatewaySettingsSyncPayload(current, incoming); - - assert.equal(synced.system.activeWorkspaceProjectId, "desktop-project-a"); -}); - -test("gateway settings sync keeps right dock width local and syncs project state", () => { - installWindow(); - const current = settings.normalizeSettings({ - customSettings: { - rightDock: { - width: 612, - projects: { - "/desktop/project": { - activeTabId: "desktop-terminal", - tabOrder: ["desktop-terminal"], - tabs: { - "desktop-terminal": { - id: "desktop-terminal", - kind: "terminal", - projectPathKey: "/desktop/project", - createdAt: 1, - }, - }, - openVersion: 1, - stateVersion: 1, - }, - "/shared/project": { - activeTabId: RIGHT_DOCK_TAB_IDS.fileTree, - tabOrder: [RIGHT_DOCK_TAB_IDS.fileTree], - tabs: { - [RIGHT_DOCK_TAB_IDS.fileTree]: { - id: RIGHT_DOCK_TAB_IDS.fileTree, - kind: "fileTree", - projectPathKey: "/shared/project", - createdAt: 2, - uiState: { - query: "desktop", - selectedPath: "desktop.ts", - expandedPaths: ["", "src"], - showHidden: true, - revision: 1, - stateVersion: 3, - }, - }, - }, - openVersion: 2, - stateVersion: 3, - }, - }, - }, - }, - }); - const incoming = settings.normalizeSettings({ - customSettings: { - rightDock: { - width: 360, - projects: { - "/web/project": { - activeTabId: "web-terminal", - tabOrder: ["web-terminal"], - tabs: { - "web-terminal": { - id: "web-terminal", - kind: "terminal", - projectPathKey: "/web/project", - createdAt: 3, - }, - }, - openVersion: 2, - stateVersion: 2, - }, - "/shared/project": { - activeTabId: RIGHT_DOCK_TAB_IDS.fileTree, - tabOrder: [RIGHT_DOCK_TAB_IDS.fileTree], - tabs: { - [RIGHT_DOCK_TAB_IDS.fileTree]: { - id: RIGHT_DOCK_TAB_IDS.fileTree, - kind: "fileTree", - projectPathKey: "/shared/project", - createdAt: 4, - uiState: { - query: "web", - selectedPath: "web.ts", - expandedPaths: ["", "packages"], - revision: 2, - stateVersion: 2, - }, - }, - }, - openVersion: 5, - stateVersion: 2, - }, - }, - }, - }, - }); - - const payload = settingsSync.buildGatewaySettingsSyncPayload(incoming); - const synced = settingsSync.applyGatewaySettingsSyncPayload(current, payload); - - assert.equal(synced.customSettings.rightDock.width, 612); - assert.deepEqual(Object.keys(synced.customSettings.rightDock.projects).sort(), [ - "/desktop/project", - "/shared/project", - "/web/project", - ]); - assert.deepEqual( - settings.getRightDockFileTreeState(synced.customSettings, "/shared/project"), - { - query: "desktop", - selectedPath: "desktop.ts", - expandedPaths: ["", "src"], - showHidden: true, - revision: 1, - }, - ); - assert.equal(synced.customSettings.rightDock.projects["/shared/project"].openVersion, 5); - assert.equal(synced.customSettings.rightDock.projects["/shared/project"].stateVersion, 3); -}); - -test("gateway settings sync keeps newer project conversation activity", () => { - installWindow(); - const current = settings.normalizeSettings({ - system: { - ...settings.getDefaultSettings().system, - workdir: "/default-workdir", - workspaceProjects: [ - { - id: "project-a", - name: "Project A", - path: "/project-a", - kind: "folder", - createdAt: 1, - updatedAt: 1, - lastConversationAt: 1_700_000_000_900, - }, - ], - }, - }); - const incoming = settingsSync.buildGatewaySettingsSyncPayload( - settings.normalizeSettings({ - system: { - ...settings.getDefaultSettings().system, - workdir: "/default-workdir", - workspaceProjects: [ - { - id: "project-a", - name: "Project A", - path: "/project-a", - kind: "folder", - createdAt: 1, - updatedAt: 1, - lastConversationAt: 1_700_000_000_100, - }, - ], - }, - }), - ); - - const synced = settingsSync.applyGatewaySettingsSyncPayload(current, incoming); - - assert.equal( - synced.system.workspaceProjects.find((item) => item.id === "project-a")?.lastConversationAt, - 1_700_000_000_900, - ); -}); - -test("web remote settings normalize single-slash http gateway URLs", () => { - const remote = settings.normalizeRemoteSettings({ - enabled: true, - gatewayUrl: " https:/gateway.example/ ", - token: " token ", - }); - - assert.equal(remote.gatewayUrl, "https://gateway.example"); - assert.equal(remote.token, "token"); - - const remoteWithOversizedPort = settings.normalizeRemoteSettings({ - gatewayPort: "70000", - }); - assert.equal(remoteWithOversizedPort.gatewayPort, 65_535); -}); - -test("web provider normalization keeps native web search toggle", () => { - const enabledByDefault = settings.normalizeCustomProvider({ - id: "provider-enabled", - type: "codex", - baseUrl: "https://api.openai.com/v1", - }); - assert.equal(enabledByDefault.nativeWebSearchEnabled, true); - - const disabled = settings.normalizeCustomProvider({ - id: "provider-disabled", - type: "gemini", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - nativeWebSearchEnabled: false, - }); - assert.equal(disabled.nativeWebSearchEnabled, false); -}); - -test("web right dock normalize keeps unknown session ids and unresolved active tab", () => { - const project = settings.normalizeRightDockProjectState({ - activeTabId: "sess-active", - tabOrder: ["sess-a", RIGHT_DOCK_TAB_IDS.gitReview, "sess-b"], - tools: { gitReview: { openedAt: 4 } }, - openVersion: 1, - stateVersion: 2, - writerId: "peer", - lastUsedAt: 9, - }); - assert.deepEqual(project.tabOrder, ["sess-a", RIGHT_DOCK_TAB_IDS.gitReview, "sess-b"]); - assert.equal(project.activeTabId, "sess-active"); - assert.deepEqual(Object.keys(project.tools), ["gitReview"]); -}); - -test("web right dock merge converges symmetrically on writerId ties", () => { - const bucket = (writerId, activeTabId) => ({ - activeTabId, - tabOrder: [activeTabId], - tools: { gitReview: { openedAt: 1 } }, - openVersion: 1, - stateVersion: 3, - writerId, - lastUsedAt: 100, - }); - const stateA = settings.normalizeSettings({ - customSettings: { rightDock: { projects: { "/w/app": bucket("bbb", RIGHT_DOCK_TAB_IDS.gitReview) } } }, - }); - const stateB = settings.normalizeSettings({ - customSettings: { rightDock: { projects: { "/w/app": bucket("aaa", "sess-2") } } }, - }); - const aGotB = settingsSync.applyGatewaySettingsSyncPayload(stateA, { - customSettings: { rightDock: stateB.customSettings.rightDock }, - }); - const bGotA = settingsSync.applyGatewaySettingsSyncPayload(stateB, { - customSettings: { rightDock: stateA.customSettings.rightDock }, - }); - const mergedA = aGotB.customSettings.rightDock.projects["/w/app"]; - const mergedB = bGotA.customSettings.rightDock.projects["/w/app"]; - assert.equal(mergedA.activeTabId, RIGHT_DOCK_TAB_IDS.gitReview); - assert.deepEqual(mergedA, mergedB); - assert.equal(mergedA.stateVersion, 3); - assert.equal(mergedA.lastUsedAt, 100); -}); - -test("web right dock buckets are kept by recency and tombstones expire", () => { - const now = Date.now(); - const projects = {}; - for (let index = 0; index <= 100; index += 1) { - projects[`/p/n${String(index).padStart(3, "0")}`] = { - tabOrder: [], - tools: { gitReview: { openedAt: 1 } }, - openVersion: 1, - stateVersion: 1, - writerId: "w", - lastUsedAt: now - index * 1000, - }; - } - const capped = settings.normalizeRightDockSettings({ projects }); - assert.equal(Object.keys(capped.projects).length, 100); - assert.equal(capped.projects["/p/n100"], undefined); - assert.ok(capped.projects["/p/n000"]); - - const tombstones = settings.normalizeRightDockSettings({ - projects: { - "/t/expired": { tools: {}, openVersion: 1, stateVersion: 2, lastUsedAt: now - 91 * 24 * 3600 * 1000 }, - "/t/fresh": { tools: {}, openVersion: 1, stateVersion: 2, lastUsedAt: now - 1000 }, - "/t/legacy": { tools: {}, openVersion: 1, stateVersion: 2 }, - }, - }); - assert.deepEqual(Object.keys(tombstones.projects).sort(), ["/t/fresh", "/t/legacy"]); - assert.ok(tombstones.projects["/t/legacy"].lastUsedAt >= now - 1000); -}); - -test("web right dock migrates the legacy tabs shape", () => { - const project = settings.normalizeRightDockProjectState({ - activeTabId: "sess-1", - tabOrder: ["sess-1", RIGHT_DOCK_TAB_IDS.fileTree], - tabs: { - "sess-1": { id: "sess-1", kind: "terminal", projectPathKey: "/w/app", createdAt: 1 }, - [RIGHT_DOCK_TAB_IDS.fileTree]: { - id: RIGHT_DOCK_TAB_IDS.fileTree, - kind: "fileTree", - projectPathKey: "/w/app", - createdAt: 7, - uiState: { query: "q", expandedPaths: ["", "src"] }, - }, - }, - openVersion: 2, - stateVersion: 5, - }); - assert.deepEqual(Object.keys(project.tools), ["fileTree"]); - assert.equal(project.tools.fileTree.openedAt, 7); - assert.equal(project.tools.fileTree.uiState.query, "q"); - assert.deepEqual(project.tabOrder, ["sess-1", RIGHT_DOCK_TAB_IDS.fileTree]); - assert.equal(project.activeTabId, "sess-1"); -}); - -test("xai model limits use the generated catalog without changing thinking detection", () => { - const grok45 = settings.getProviderModelDefaults("xai", "grok-4.5"); - assert.equal(grok45.contextWindow, 500_000); - // 上游"输出=窗口"的退化条目在生成期统一钳到 32K。 - assert.equal(grok45.maxOutputToken, 32_000); - // 上游(models.dev)已下架的旧模型与未收录模型一样吃供应商兜底值。 - assert.equal(settings.getProviderModelDefaults("xai", "grok-3").contextWindow, 258_000); - assert.equal(settings.getProviderModelDefaults("xai", "grok-unknown").contextWindow, 258_000); - // 思考档位与限额同吃生成目录(见下一个用例)。 - assert.ok(settings.getKnownModelThinkingLevels("xai", "grok-4.5").includes("high")); -}); - -test("xai thinking levels come from the catalog per model, thinking always on", () => { - // 档位按型号差异化(目录真值):grok-4.5 只有 low/medium/high; - // grok-4.20-multi-agent-0309 声明到 xhigh。xai 思考一律恒开 - //(wire 无法表达 off),目录的 off 声明对 xai 供应商不生效。 - assert.deepEqual(settings.getKnownModelThinkingLevels("xai", "grok-4.5"), [ - "low", - "medium", - "high", - ]); - assert.equal(settings.isThinkingAlwaysOnForModel("xai", "grok-4.5"), true); - const multiAgent = settings.getKnownModelThinkingLevels("xai", "grok-4.20-multi-agent-0309"); - assert.ok(multiAgent.includes("xhigh")); - assert.equal(settings.isThinkingAlwaysOnForModel("xai", "grok-4.3"), true); - // 钳制路径:xhigh 超出 grok-4.5 档位表时压回默认 high。 - const clamped = settings.normalizeChatRuntimeControlsForProvider( - { reasoning: "xhigh", reasoningByProvider: { xai: "xhigh" } }, - { providerId: "xai", modelId: "grok-4.5" }, - ); - assert.equal(clamped.reasoning, "high"); -}); - -test("gateway sync keeps all web font families local", () => { - const current = settings.normalizeSettings({ - customSettings: { - interfaceFontFamily: "Inter", - chatFontFamily: "Noto Sans", - codeFontFamily: "Menlo", - }, - }); - const incoming = settingsSync.buildGatewaySettingsSyncPayload( - settings.normalizeSettings({ - customSettings: { - interfaceFontFamily: "Arial", - chatFontFamily: "Open Sans", - codeFontFamily: "Monaco", - }, - }), - ); - - assert.deepEqual( - { - interfaceFontFamily: incoming.customSettings.interfaceFontFamily, - chatFontFamily: incoming.customSettings.chatFontFamily, - codeFontFamily: incoming.customSettings.codeFontFamily, - }, - { interfaceFontFamily: "", chatFontFamily: "", codeFontFamily: "" }, - ); - assert.deepEqual( - settingsSync.applyGatewaySettingsSyncPayload(current, incoming).customSettings, - current.customSettings, - ); -}); diff --git a/crates/agent-gateway/test/webui/workspace-projects.test.mjs b/crates/agent-gateway/test/webui/workspace-projects.test.mjs deleted file mode 100644 index 2a6d07ab0..000000000 --- a/crates/agent-gateway/test/webui/workspace-projects.test.mjs +++ /dev/null @@ -1,347 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { createWebModuleLoader } from "../helpers/load-web-module.mjs"; - -const loader = createWebModuleLoader(); -const settings = loader.loadModule("src/lib/settings/index.ts"); -const workspaceProjects = loader.loadModule("src/lib/workspaceProjects.ts"); - -function project(id, path, index) { - return { - id, - name: id, - path, - kind: id === settings.DEFAULT_WORKSPACE_PROJECT_ID ? "managed" : "manual", - createdAt: index, - updatedAt: index, - }; -} - -function withLastConversationAt(item, lastConversationAt) { - return { - ...item, - lastConversationAt, - }; -} - -test("workspace project path key normalizes windows-shaped paths and preserves POSIX semantics", () => { - assert.equal( - settings.workspaceProjectPathKey(" C:\\Users\\Me\\Repo\\ "), - "c:/users/me/repo", - ); - assert.equal(settings.workspaceProjectPathKey("c:/USERS/me/REPO"), "c:/users/me/repo"); - assert.equal( - settings.workspaceProjectPathKey("\\\\Server\\Share\\Repo\\"), - "//server/share/repo", - ); - assert.equal( - settings.workspaceProjectPathKey("\\\\?\\C:\\Users\\Me\\Repo\\"), - "c:/users/me/repo", - ); - assert.equal( - settings.workspaceProjectPathKey("\\\\?\\UNC\\Server\\Share\\Repo\\"), - "//server/share/repo", - ); - assert.equal(settings.workspaceProjectPathKey(" /Users/A/App/ "), "/Users/A/App"); - assert.equal(settings.workspaceProjectPathKey("/tmp/Foo"), "/tmp/Foo"); - assert.equal(settings.workspaceProjectPathKey("/tmp/Foo\\"), "/tmp/Foo\\"); - assert.notEqual( - settings.workspaceProjectPathKey("/tmp/Foo"), - settings.workspaceProjectPathKey("/tmp/foo"), - ); -}); - -test("workspace project ordering follows latest activity instead of pinning default first", () => { - const projects = [ - project(settings.DEFAULT_WORKSPACE_PROJECT_ID, "/tmp/default-project", 1), - project("project-a", "/tmp/project-a", 2), - project("project-b", "/tmp/project-b", 3), - ]; - const activity = workspaceProjects.buildWorkspaceProjectActivityUpdatedAts([ - { path: "/tmp/default-project", updatedAt: 1_700_000_000_100 }, - { path: "/tmp/project-a", updatedAt: 1_700_000_000_300 }, - { path: "/tmp/project-b", updatedAt: 1_700_000_000_200 }, - ]); - - const ordered = workspaceProjects.sortWorkspaceProjectsByActivity(projects, { - projectActivityUpdatedAts: activity, - }); - - assert.deepEqual( - ordered.map((item) => item.id), - ["project-a", "project-b", settings.DEFAULT_WORKSPACE_PROJECT_ID], - ); -}); - -test("workspace project keeps its active position after the running marker is cleared", () => { - const projects = [ - project(settings.DEFAULT_WORKSPACE_PROJECT_ID, "/tmp/default-project", 1), - project("project-a", "/tmp/project-a", 2), - ]; - const projectAKey = settings.workspaceProjectPathKey("/tmp/project-a"); - const activity = workspaceProjects.buildWorkspaceProjectActivityUpdatedAts([ - { path: "/tmp/default-project", updatedAt: 1_700_000_000_100 }, - { path: "/tmp/project-a", updatedAt: 1_700_000_000_300 }, - ]); - - const duringRun = workspaceProjects.sortWorkspaceProjectsByActivity(projects, { - projectActivityUpdatedAts: activity, - runningProjectPathKeys: new Set([projectAKey]), - }); - const afterRun = workspaceProjects.sortWorkspaceProjectsByActivity(projects, { - projectActivityUpdatedAts: activity, - runningProjectPathKeys: new Set(), - }); - - assert.deepEqual( - duringRun.map((item) => item.id), - ["project-a", settings.DEFAULT_WORKSPACE_PROJECT_ID], - ); - assert.deepEqual( - afterRun.map((item) => item.id), - ["project-a", settings.DEFAULT_WORKSPACE_PROJECT_ID], - ); -}); - -test("running workspace project outranks a newer idle project", () => { - const projects = [ - project(settings.DEFAULT_WORKSPACE_PROJECT_ID, "/tmp/default-project", 1), - project("project-running", "/tmp/project-running", 2), - ]; - const activity = workspaceProjects.buildWorkspaceProjectActivityUpdatedAts([ - { path: "/tmp/default-project", updatedAt: 1_700_000_000_900 }, - { path: "/tmp/project-running", updatedAt: 1_700_000_000_100 }, - ]); - - const ordered = workspaceProjects.sortWorkspaceProjectsByActivity(projects, { - projectActivityUpdatedAts: activity, - runningProjectPathKeys: new Set([ - settings.workspaceProjectPathKey("/tmp/project-running"), - ]), - }); - - assert.deepEqual( - ordered.map((item) => item.id), - ["project-running", settings.DEFAULT_WORKSPACE_PROJECT_ID], - ); -}); - -test("workspace project selection metadata does not change activity ordering", () => { - const projects = [ - project(settings.DEFAULT_WORKSPACE_PROJECT_ID, "/tmp/default-project", 1), - { - ...project("project-a", "/tmp/project-a", Date.now()), - kind: "history", - }, - ]; - - const ordered = workspaceProjects.sortWorkspaceProjectsByActivity(projects); - - assert.deepEqual( - ordered.map((item) => item.id), - [settings.DEFAULT_WORKSPACE_PROJECT_ID, "project-a"], - ); -}); - -test("history workdir activity restores ordering after page refresh", () => { - const projects = [ - project(settings.DEFAULT_WORKSPACE_PROJECT_ID, "/tmp/default-project", 1), - project("project-a", "/tmp/project-a", 2), - ]; - const hydrated = workspaceProjects.buildWorkspaceProjectActivityUpdatedAts([ - { path: "/tmp/default-project", updatedAt: 1_700_000_000_100 }, - { path: "/tmp/project-a", updatedAt: 1_700_000_000_500 }, - ]); - - const ordered = workspaceProjects.sortWorkspaceProjectsByActivity(projects, { - projectActivityUpdatedAts: hydrated, - }); - - assert.deepEqual( - ordered.map((item) => item.id), - ["project-a", settings.DEFAULT_WORKSPACE_PROJECT_ID], - ); -}); - -test("persisted last conversation activity restores ordering before history workdirs hydrate", () => { - const projects = [ - withLastConversationAt( - project(settings.DEFAULT_WORKSPACE_PROJECT_ID, "/tmp/default-project", 1), - 1_700_000_000_100, - ), - withLastConversationAt(project("project-a", "/tmp/project-a", 2), 1_700_000_000_500), - ]; - - const ordered = workspaceProjects.sortWorkspaceProjectsByActivity(projects); - - assert.deepEqual( - ordered.map((item) => item.id), - ["project-a", settings.DEFAULT_WORKSPACE_PROJECT_ID], - ); -}); - -test("history merge stores conversation activity on configured and discovered projects", () => { - const system = { - ...settings.getDefaultSettings().system, - workspaceProjects: [ - project(settings.DEFAULT_WORKSPACE_PROJECT_ID, "/tmp/default-project", 1), - project("project-a", "/tmp/project-a", 2), - ], - }; - - const merged = workspaceProjects.mergeWorkspaceProjectsWithHistory(system, [ - { path: "/tmp/project-a", conversationCount: 2, updatedAt: 1_700_000_000_500 }, - { path: "/tmp/project-b", conversationCount: 1, updatedAt: 1_700_000_000_600 }, - ]); - - assert.equal( - merged.find((item) => item.id === "project-a")?.lastConversationAt, - 1_700_000_000_500, - ); - assert.equal( - merged.find((item) => item.path === "/tmp/project-b")?.lastConversationAt, - 1_700_000_000_600, - ); -}); - -test("archived paths survive resolveWorkspaceProjects normalization", () => { - const resolved = settings.resolveWorkspaceProjects( - { - ...settings.getDefaultSettings().system, - workspaceProjects: [ - project(settings.DEFAULT_WORKSPACE_PROJECT_ID, "/tmp/default-project", 1), - project("project-a", "/tmp/project-a", 2), - project("project-b", "/tmp/project-b", 3), - ], - archivedWorkspaceProjectPaths: [ - "/tmp/project-a", - "/tmp/project-a/", - " /tmp/default-project ", - ], - }, - "/tmp/default-project", - ); - - assert.deepEqual(resolved.archivedWorkspaceProjectPaths, [ - "/tmp/project-a", - "/tmp/default-project", - ]); - assert.equal(resolved.activeWorkspaceProjectId, "project-b"); -}); - -test("resolveWorkspaceProjects keeps one workspace selectable when every path is archived", () => { - const resolved = settings.resolveWorkspaceProjects( - { - ...settings.getDefaultSettings().system, - workspaceProjects: [ - project(settings.DEFAULT_WORKSPACE_PROJECT_ID, "/tmp/default-project", 1), - project("project-a", "/tmp/project-a", 2), - ], - activeWorkspaceProjectId: "project-a", - archivedWorkspaceProjectPaths: ["/tmp/default-project", "/tmp/project-a"], - }, - "/tmp/default-project", - ); - - assert.equal(resolved.activeWorkspaceProjectId, settings.DEFAULT_WORKSPACE_PROJECT_ID); - assert.deepEqual(resolved.archivedWorkspaceProjectPaths, ["/tmp/project-a"]); -}); - -test("removed (hidden) paths are dropped from the archived list", () => { - const resolved = settings.resolveWorkspaceProjects( - { - ...settings.getDefaultSettings().system, - workspaceProjects: [ - project(settings.DEFAULT_WORKSPACE_PROJECT_ID, "/tmp/default-project", 1), - ], - hiddenWorkspaceProjectPaths: ["/tmp/project-a"], - archivedWorkspaceProjectPaths: ["/tmp/project-a", "/tmp/project-b"], - }, - "/tmp/default-project", - ); - - assert.deepEqual(resolved.archivedWorkspaceProjectPaths, ["/tmp/project-b"]); -}); - -test("conversation activity persistence does not rewrite project metadata ordering", () => { - const projects = [ - project(settings.DEFAULT_WORKSPACE_PROJECT_ID, "/tmp/default-project", 1), - project("project-a", "/tmp/project-a", 2), - ]; - const activity = workspaceProjects.buildWorkspaceProjectActivityUpdatedAts([ - { path: "/tmp/project-a", updatedAt: 1_700_000_000_900 }, - ]); - - const next = workspaceProjects.applyWorkspaceProjectConversationActivityMap( - projects, - activity, - ); - - assert.deepEqual( - next.map((item) => item.id), - [settings.DEFAULT_WORKSPACE_PROJECT_ID, "project-a"], - ); - assert.equal(next[1].updatedAt, 2); - assert.equal(next[1].lastConversationAt, 1_700_000_000_900); -}); - -test("live activity overrides stale persisted last conversation activity", () => { - const projects = [ - withLastConversationAt( - project(settings.DEFAULT_WORKSPACE_PROJECT_ID, "/tmp/default-project", 1), - 1_700_000_000_500, - ), - withLastConversationAt(project("project-a", "/tmp/project-a", 2), 1_700_000_000_100), - ]; - const activity = workspaceProjects.buildWorkspaceProjectActivityUpdatedAts([ - { path: "/tmp/project-a", updatedAt: 1_700_000_000_900 }, - ]); - - const ordered = workspaceProjects.sortWorkspaceProjectsByActivity(projects, { - projectActivityUpdatedAts: activity, - }); - - assert.deepEqual( - ordered.map((item) => item.id), - ["project-a", settings.DEFAULT_WORKSPACE_PROJECT_ID], - ); -}); - -test("workspace project activity merge keeps newer timestamps", () => { - const newerActivity = workspaceProjects.buildWorkspaceProjectActivityUpdatedAts([ - { path: "/tmp/project-a", updatedAt: 1_700_000_000_900 }, - ]); - const olderActivity = workspaceProjects.buildWorkspaceProjectActivityUpdatedAts([ - { path: "/tmp/project-a", updatedAt: 1_700_000_000_100 }, - { path: "/tmp/project-b", updatedAt: 1_700_000_000_200 }, - ]); - - const merged = workspaceProjects.mergeWorkspaceProjectActivityUpdatedAts( - newerActivity, - olderActivity, - ); - - assert.equal( - merged.get(settings.workspaceProjectPathKey("/tmp/project-a")), - 1_700_000_000_900, - ); - assert.equal( - merged.get(settings.workspaceProjectPathKey("/tmp/project-b")), - 1_700_000_000_200, - ); -}); - -test("workspace project ordering uses deterministic path tie breaker", () => { - const projects = [ - project("project-b", "/tmp/project-b", 1), - project(settings.DEFAULT_WORKSPACE_PROJECT_ID, "/tmp/default-project", 2), - project("project-a", "/tmp/project-a", 3), - ]; - - const ordered = workspaceProjects.sortWorkspaceProjectsByActivity(projects); - - assert.deepEqual( - ordered.map((item) => item.id), - [settings.DEFAULT_WORKSPACE_PROJECT_ID, "project-a", "project-b"], - ); -}); diff --git a/crates/agent-gateway/web/biome.json b/crates/agent-gateway/web/biome.json deleted file mode 100644 index b95520ff3..000000000 --- a/crates/agent-gateway/web/biome.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "$schema": "https://biomejs.dev/schemas/2.4.15/schema.json", - "vcs": { - "enabled": true, - "clientKind": "git", - "useIgnoreFile": false - }, - "files": { - "includes": [ - "src/**", - "!!**/dist", - "!!**/node_modules", - "!src/lib/proto/gen/**", - "!src/lib/models/catalog.generated.ts" - ] - }, - "formatter": { - "enabled": true, - "indentStyle": "space", - "indentWidth": 2, - "lineWidth": 100 - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true, - "a11y": { - "useKeyWithClickEvents": "warn", - "noStaticElementInteractions": "warn", - "noLabelWithoutControl": "warn", - "useSemanticElements": "warn", - "useAriaPropsSupportedByRole": "warn", - "useAriaPropsForRole": "warn", - "useFocusableInteractive": "warn", - "noAutofocus": "off", - "noSvgWithoutTitle": "warn" - }, - "correctness": { - "useExhaustiveDependencies": "warn", - "noUnusedImports": "error" - }, - "suspicious": { - "noArrayIndexKey": "warn" - } - } - }, - "javascript": { - "formatter": { - "quoteStyle": "double", - "trailingCommas": "all", - "semicolons": "always" - } - }, - "css": { - "parser": { - "tailwindDirectives": true - } - }, - "assist": { - "enabled": true, - "actions": { - "source": { - "organizeImports": "on" - } - } - }, - "overrides": [ - { - "includes": [ - "**/*.css" - ], - "linter": { - "rules": { - "suspicious": { - "noDuplicateProperties": "off" - } - } - } - } - ] -} diff --git a/crates/agent-gateway/web/index.html b/crates/agent-gateway/web/index.html deleted file mode 100644 index 821df41a6..000000000 --- a/crates/agent-gateway/web/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - LiveAgent Gateway - - -
- - - diff --git a/crates/agent-gateway/web/package.json b/crates/agent-gateway/web/package.json deleted file mode 100644 index 6f90a755b..000000000 --- a/crates/agent-gateway/web/package.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "name": "@liveagent/gateway-webui", - "private": true, - "version": "0.1.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "preview": "vite preview", - "test": "node --test ../test/webui/*.test.mjs test/*.test.mjs", - "lint": "biome check src/", - "format": "biome format --write src/", - "lint:fix": "biome check --write src/" - }, - "dependencies": { - "@base-ui/react": "^1.6.0", - "@bufbuild/protobuf": "^2.12.1", - "@earendil-works/pi-ai": "^0.80.6", - "@git-diff-view/file": "^0.1.3", - "@git-diff-view/react": "^0.1.3", - "@iconify-json/gravity-ui": "^1.2.12", - "@sinclair/typebox": "^0.34.49", - "@streamdown/cjk": "^1.0.3", - "@streamdown/code": "^1.1.1", - "@streamdown/math": "^1.0.2", - "@streamdown/mermaid": "^1.0.2", - "@tanstack/react-virtual": "^3.14.6", - "@xterm/addon-fit": "^0.11.0", - "@xterm/xterm": "^6.0.0", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "docx-preview": "^0.4.0", - "katex": "^0.17.0", - "monaco-editor": "^0.55.1", - "react": "^19.2.4", - "react-complex-tree": "^2.6.1", - "react-dom": "^19.2.4", - "remark-breaks": "^4.0.0", - "streamdown": "^2.5.0", - "tailwind-merge": "^3.5.0", - "xlsx": "^0.18.5", - "yet-another-react-lightbox": "^3.31.0" - }, - "devDependencies": { - "@biomejs/biome": "^2.4.15", - "@bufbuild/protoc-gen-es": "2.12.1", - "@iconify-json/logos": "^1.2.11", - "@iconify-json/lucide": "^1.2.108", - "@iconify-json/material-icon-theme": "1.2.67", - "@svgr/core": "^8.1.0", - "@svgr/plugin-jsx": "^8.1.0", - "@tailwindcss/postcss": "4.2.2", - "@tailwindcss/typography": "^0.5.19", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.1", - "postcss": "^8.5.8", - "tailwindcss": "4.2.2", - "typescript": "~6.0.2", - "unplugin-icons": "^23.0.1", - "vite": "^8.0.5" - } -} diff --git a/crates/agent-gateway/web/pnpm-lock.yaml b/crates/agent-gateway/web/pnpm-lock.yaml deleted file mode 100644 index 3dfdee44d..000000000 --- a/crates/agent-gateway/web/pnpm-lock.yaml +++ /dev/null @@ -1,5479 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - '@base-ui/react': - specifier: ^1.6.0 - version: 1.6.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@bufbuild/protobuf': - specifier: ^2.12.1 - version: 2.12.1 - '@earendil-works/pi-ai': - specifier: ^0.80.6 - version: 0.80.6(ws@8.21.0)(zod@4.4.3) - '@git-diff-view/file': - specifier: ^0.1.3 - version: 0.1.3 - '@git-diff-view/react': - specifier: ^0.1.3 - version: 0.1.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@iconify-json/gravity-ui': - specifier: ^1.2.12 - version: 1.2.12 - '@sinclair/typebox': - specifier: ^0.34.49 - version: 0.34.49 - '@streamdown/cjk': - specifier: ^1.0.3 - version: 1.0.3(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(react@19.2.5)(unified@11.0.5) - '@streamdown/code': - specifier: ^1.1.1 - version: 1.1.1(react@19.2.5) - '@streamdown/math': - specifier: ^1.0.2 - version: 1.0.2(react@19.2.5) - '@streamdown/mermaid': - specifier: ^1.0.2 - version: 1.0.2(react@19.2.5) - '@tanstack/react-virtual': - specifier: ^3.14.6 - version: 3.14.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@xterm/addon-fit': - specifier: ^0.11.0 - version: 0.11.0 - '@xterm/xterm': - specifier: ^6.0.0 - version: 6.0.0 - class-variance-authority: - specifier: ^0.7.1 - version: 0.7.1 - clsx: - specifier: ^2.1.1 - version: 2.1.1 - docx-preview: - specifier: ^0.4.0 - version: 0.4.0 - katex: - specifier: ^0.17.0 - version: 0.17.0 - monaco-editor: - specifier: ^0.55.1 - version: 0.55.1 - react: - specifier: ^19.2.4 - version: 19.2.5 - react-complex-tree: - specifier: ^2.6.1 - version: 2.6.2(react@19.2.5) - react-dom: - specifier: ^19.2.4 - version: 19.2.5(react@19.2.5) - remark-breaks: - specifier: ^4.0.0 - version: 4.0.0 - streamdown: - specifier: ^2.5.0 - version: 2.5.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - tailwind-merge: - specifier: ^3.5.0 - version: 3.5.0 - xlsx: - specifier: ^0.18.5 - version: 0.18.5 - yet-another-react-lightbox: - specifier: ^3.31.0 - version: 3.31.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - devDependencies: - '@biomejs/biome': - specifier: ^2.4.15 - version: 2.5.2 - '@bufbuild/protoc-gen-es': - specifier: 2.12.1 - version: 2.12.1(@bufbuild/protobuf@2.12.1) - '@iconify-json/logos': - specifier: ^1.2.11 - version: 1.2.11 - '@iconify-json/lucide': - specifier: ^1.2.108 - version: 1.2.108 - '@iconify-json/material-icon-theme': - specifier: 1.2.67 - version: 1.2.67 - '@svgr/core': - specifier: ^8.1.0 - version: 8.1.0(typescript@6.0.2) - '@svgr/plugin-jsx': - specifier: ^8.1.0 - version: 8.1.0(@svgr/core@8.1.0(typescript@6.0.2)) - '@tailwindcss/postcss': - specifier: 4.2.2 - version: 4.2.2 - '@tailwindcss/typography': - specifier: ^0.5.19 - version: 0.5.20(tailwindcss@4.2.2) - '@types/react': - specifier: ^19.2.14 - version: 19.2.14 - '@types/react-dom': - specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.14) - '@vitejs/plugin-react': - specifier: ^6.0.1 - version: 6.0.1(vite@8.0.8(@types/node@26.1.1)(jiti@2.6.1)) - postcss: - specifier: ^8.5.8 - version: 8.5.9 - tailwindcss: - specifier: 4.2.2 - version: 4.2.2 - typescript: - specifier: ~6.0.2 - version: 6.0.2 - unplugin-icons: - specifier: ^23.0.1 - version: 23.0.1(@svgr/core@8.1.0(typescript@6.0.2)) - vite: - specifier: ^8.0.5 - version: 8.0.8(@types/node@26.1.1)(jiti@2.6.1) - -packages: - - '@alloc/quick-lru@5.2.0': - resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} - engines: {node: '>=10'} - - '@antfu/install-pkg@1.1.0': - resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} - - '@anthropic-ai/sdk@0.91.1': - resolution: {integrity: sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==} - hasBin: true - peerDependencies: - zod: ^3.25.0 || ^4.0.0 - peerDependenciesMeta: - zod: - optional: true - - '@aws-crypto/sha256-browser@5.2.0': - resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} - - '@aws-crypto/sha256-js@5.2.0': - resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} - engines: {node: '>=16.0.0'} - - '@aws-crypto/supports-web-crypto@5.2.0': - resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} - - '@aws-crypto/util@5.2.0': - resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} - - '@aws-sdk/client-bedrock-runtime@3.1048.0': - resolution: {integrity: sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/core@3.975.1': - resolution: {integrity: sha512-8qh/6EYb7hl/ZwVfQufhbMEZs1gQIc7GbdrIf4eprQJ7cv042+74nE6l3YDfyWNzb9iPXb8fRyYSHkNIk5eE6Q==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-env@3.972.57': - resolution: {integrity: sha512-1RfJaF7SW1TOnvNGU7kaYjwUf5H3sfm+synGH1bHhRlqcnxCt3szebH3dmKEyY4tuGcbQ6ffzUT89cRitBV8OQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-http@3.972.59': - resolution: {integrity: sha512-sRCkpTiFnCdQvuaRVjQ6SVoHu6i7RUpurVo1c4F81HWhPvUJ7Wdp5MNtSdX1O29CNXc8em3O5m52hCjVtAD9SA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-ini@3.973.1': - resolution: {integrity: sha512-6d8H6ZAh3ZPKZ6fe1nG2OWeZEZPtt9ravoD1dezPdPtsSkJRoxGAnFSHwKT3E/Te6fHE30zRzjV6TD12rvF6yQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-login@3.972.63': - resolution: {integrity: sha512-GREWRrMj0XnNKMaVa/Mauoaui26qBEHu71WWqXbwZOu/jFQOnPZjTf7u0KtGKC8VGa6VUs9kDWGgocrKNLS9vw==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-node@3.972.66': - resolution: {integrity: sha512-f+qjRXZpz7sgzbc4QB+6nLKfyKFgRRXzWdXbsKPv/VhVRyHsDyq4yBWC/B75BAJpFIcUeI2XR/3gdWJ677zB4A==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-process@3.972.57': - resolution: {integrity: sha512-TiVQhuU0pbhIZAUZacbPHMyzrIdiH+lnx+PMY/Pu/b93dJrq3wdZwzUJ0TPpvNxaqbHsxJvQZW3/h/beLiKq7Q==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-sso@3.973.1': - resolution: {integrity: sha512-3foTZUJ4821Ij60X7K3NJroygiZLnbBmarN+T//O2cjkISan90zElN3NBmgSlDrTQ7Gs6z/yO8V7h60QNcDZHQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-web-identity@3.972.63': - resolution: {integrity: sha512-8qZLFhM69eKcS37m459ctPR05Qimycm/74OPVioe6wNZabMT54GYhwBju0+J656RkMasNSawWQu+c8CmBe3TUQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/eventstream-handler-node@3.972.26': - resolution: {integrity: sha512-RE1fu7Nn05vG0EUJM+8Sde2GFecC658WGaC/asPzLF6K4x3H5ZaDBcQtHRE67Gdgb1VZpyUUliYejHFK1qt0Uw==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/middleware-eventstream@3.972.22': - resolution: {integrity: sha512-jtkgmhevnpzC1WeS+Y/sgymYbaQ6qg7pVOUl5cUT/8MiLptqrtnXQlNV80m+j2WIx5MIL7kVHIZNxxcK2tfUEQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/middleware-websocket@3.972.39': - resolution: {integrity: sha512-CS1spxRSezmTmI3PD+3Xrnp6KryTSEz0EefA8u6uGd0s2I0uXseWHALDI/03Wi0IUczXNWo2QrZEaHDuJNby/Q==} - engines: {node: '>= 14.0.0'} - - '@aws-sdk/nested-clients@3.997.31': - resolution: {integrity: sha512-BDHTpwcsZHEBNEJzOg/B1BkFYJxAXY50dau/NyVWs3d51F0WgIUGSWZot/Os+N3KpDhXeaXnz37mWffAvduREw==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/signature-v4-multi-region@3.996.39': - resolution: {integrity: sha512-8+srXqYIF8KYMLC4FxMLEM5Ek7kUNibJu1R4m8/fUhhNYIZZz26oGtKkCr8I/HiG2fFQxBvaGgQZT4/mqRCSnA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/token-providers@3.1048.0': - resolution: {integrity: sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/token-providers@3.1083.0': - resolution: {integrity: sha512-s0woKnxuHrExLc5L2ArIH5BMkbonHPtt+5hSBM8oknp9M6QTuUmmAmJ2E0EdzCGONrO+8+ADPqvv6UX0nNcc7A==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/types@3.974.0': - resolution: {integrity: sha512-QIBrw90CDm4O0UaIIzkU6DrFdeJzEb2Va5EPEVpyldj6sHJxB6cshhStJuhZxk3wR3PmjJlYsjPmY1kNb+KGBg==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/util-locate-window@3.965.8': - resolution: {integrity: sha512-uUbMs1cBZPafD0ohUj6EwNf0fPZ534NvBxHox4hjX+0Rxq5paSYUem7+hi833pYrzrcnBATKIYpR02MDXT5M9g==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/xml-builder@3.972.34': - resolution: {integrity: sha512-wHhWL1y7sN3enBA8POrPpQM5jCcmu2ozyhbRei4c8OjVcEaEs6yLucLa/pla457ggS/ysuy7bosagz3HaJkZXA==} - engines: {node: '>=20.0.0'} - - '@aws/lambda-invoke-store@0.3.0': - resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==} - engines: {node: '>=18.0.0'} - - '@babel/code-frame@7.29.0': - resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} - engines: {node: '>=6.9.0'} - - '@babel/compat-data@7.29.3': - resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.29.0': - resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.29.1': - resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-compilation-targets@7.28.6': - resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-globals@7.29.7': - resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-imports@7.28.6': - resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-transforms@7.28.6': - resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-string-parser@7.29.7': - resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.29.7': - resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-option@7.29.7': - resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} - engines: {node: '>=6.9.0'} - - '@babel/helpers@7.29.2': - resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.29.3': - resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/runtime@7.29.7': - resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} - engines: {node: '>=6.9.0'} - - '@babel/template@7.28.6': - resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.29.0': - resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.29.0': - resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} - engines: {node: '>=6.9.0'} - - '@base-ui/react@1.6.0': - resolution: {integrity: sha512-/jzjTWJYXhRFO45Bev9lc3cHbmjzCMpUqbMZ2AgKy/z25mY9B6shGSNcXcjQar9n5doM0KYW1W8fcFv2jZBuMw==} - engines: {node: '>=14.0.0'} - peerDependencies: - '@date-fns/tz': ^1.2.0 - '@types/react': ^17 || ^18 || ^19 - date-fns: ^4.0.0 - react: ^17 || ^18 || ^19 - react-dom: ^17 || ^18 || ^19 - peerDependenciesMeta: - '@date-fns/tz': - optional: true - '@types/react': - optional: true - date-fns: - optional: true - - '@base-ui/utils@0.3.1': - resolution: {integrity: sha512-gFFiltORVmW/N6IILTGxizP3PBpVpysqML1ALY5Vk0mH+7faVkCknOU31goYHN5Aoek2dkjxva1XOD2Ce9WuIg==} - peerDependencies: - '@types/react': ^17 || ^18 || ^19 - react: ^17 || ^18 || ^19 - react-dom: ^17 || ^18 || ^19 - peerDependenciesMeta: - '@types/react': - optional: true - - '@biomejs/biome@2.5.2': - resolution: {integrity: sha512-VQ3RCqr7JmDIX+w6stWYl+g/3bYofN3q2wDBHUKKc/c7i5QWrFKFBZYCYPWTE6agsUPMIZZe6/CMmVUfUAhkKA==} - engines: {node: '>=14.21.3'} - hasBin: true - - '@biomejs/cli-darwin-arm64@2.5.2': - resolution: {integrity: sha512-e7P3P7EkwFc/KiX2AHw4YDLIBOMfG9CPCAwy52k5Bp0dfhkozx9hf6wCmIr2QeXy2XeccJ3V/Sg+hDmzYEqxSg==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [darwin] - - '@biomejs/cli-darwin-x64@2.5.2': - resolution: {integrity: sha512-ymzMvjC1Jg0b9K0D26ZdARqFQXs7MocfLC5FOCGfkC0Ss+ACUJkX5364ZM5nT4NLZanHRZNVrZEy+Ibwcvux/g==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [darwin] - - '@biomejs/cli-linux-arm64-musl@2.5.2': - resolution: {integrity: sha512-w+ANG0ZvTu9IeEg9QnstoOnk6L0fpwJifW6aHR18+cb5Z39bkANItYjAfMrnvce5tmMK+IQ6nPX7/kQFdam5iw==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@biomejs/cli-linux-arm64@2.5.2': - resolution: {integrity: sha512-t7sseOmqND57uUWTwlawU6BYj+J06T/9EkydzBhkrgw/FK3QVhjU2wsJR0frljrKZ0/I8A/rYw7284QgqjQfIQ==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@biomejs/cli-linux-x64-musl@2.5.2': - resolution: {integrity: sha512-VArNLAzND063tF+XY0yPyM+DyahpzOMzOAvb7qs259nhjJWRjvjZdssuA+Rfl+l07+NOesKZ0Xu2yFrXyBMtzw==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@biomejs/cli-linux-x64@2.5.2': - resolution: {integrity: sha512-M/lOZrewzTCRDINbjhQ1gYYru37KlD3kJBQwwKCG0ckz5E9IZwIoJ3X0wBwRXA+yBDIwWUuPBHS67HzJY4dTfA==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@biomejs/cli-win32-arm64@2.5.2': - resolution: {integrity: sha512-kbjFFKyZlzYnAuw7sRy5qDoFG6zrP40UK08oPQsWK0ct3NMnGSt+Bs1iviEEyEIP57N5MrykGXdO/wRiaR4lww==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [win32] - - '@biomejs/cli-win32-x64@2.5.2': - resolution: {integrity: sha512-4InchVpdVmdkkkgjQqKpgvyu+VPnoF/7RPSw5YATgEVpt2j72wcCAeV5TwaE9ZGJUZWZn7v2CwSAj6CrMJEx8A==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [win32] - - '@braintree/sanitize-url@7.1.2': - resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} - - '@bufbuild/protobuf@2.12.1': - resolution: {integrity: sha512-BvAMfS6LrgZiryOAZ4pBYucu4wG/Ei/9o9DZ9akbREnMLbPJiom2i8b9C8IsKErQoiKqVhrerzt3kOT/RrzLHg==} - - '@bufbuild/protoc-gen-es@2.12.1': - resolution: {integrity: sha512-SWa7XvRYRouMo+vBQmpNFZ+ZEqQ8AC0LpL4QWAo1gvstLhFh/Y7Nf/a+MK7ZxDq5LZSThwfk974L1sFxO3OaGw==} - engines: {node: '>=20'} - hasBin: true - peerDependencies: - '@bufbuild/protobuf': 2.12.1 - peerDependenciesMeta: - '@bufbuild/protobuf': - optional: true - - '@bufbuild/protoplugin@2.12.1': - resolution: {integrity: sha512-PY58KxQVAD1BnnKtStOctsMoegEVGfBnY5AOqVQOIu711nA13oYtTqJM8df5lUQg2J1DR3XxUXptE+fWX5oLdA==} - - '@chevrotain/cst-dts-gen@12.0.0': - resolution: {integrity: sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg==} - - '@chevrotain/gast@12.0.0': - resolution: {integrity: sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ==} - - '@chevrotain/regexp-to-ast@12.0.0': - resolution: {integrity: sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA==} - - '@chevrotain/types@12.0.0': - resolution: {integrity: sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA==} - - '@chevrotain/utils@12.0.0': - resolution: {integrity: sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA==} - - '@earendil-works/pi-ai@0.80.6': - resolution: {integrity: sha512-7xfLk8sANBp+bpPEbjoOZTbPxsa+++b1JXAoSJsNa3vbs9AHHEclmvg54XLQcxH+fuwaeti/g2jeIfJ+mVYLpA==} - engines: {node: '>=22.19.0'} - hasBin: true - - '@emnapi/core@1.9.2': - resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} - - '@emnapi/runtime@1.9.2': - resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} - - '@emnapi/wasi-threads@1.2.1': - resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - - '@floating-ui/core@1.7.5': - resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} - - '@floating-ui/dom@1.7.6': - resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} - - '@floating-ui/react-dom@2.1.8': - resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' - - '@floating-ui/utils@0.2.11': - resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} - - '@git-diff-view/core@0.1.5': - resolution: {integrity: sha512-xvMZnD0k8BlmP3RqSocQrPVVzgUxr8KEl0wT5TTIm9c2B5tODHat69zidUvFWIoaYvHwDu4PfVcdGj4c+PrL6g==} - - '@git-diff-view/file@0.1.3': - resolution: {integrity: sha512-ECafXpjH543lLTWtv7CaPrLlQEld3bBplPCUlly4uzO8Usbk4qqdtVJLmOiHIHMGXYMOIO4Ic5YSwseZnLgWUg==} - - '@git-diff-view/lowlight@0.1.5': - resolution: {integrity: sha512-6sxEJcGIUHzJc6Rx1kQ1TqAi0g6ABiGG8z5xhlLHWcQwDmGzB2wTISbyRxNXTwQTwJIMX5u5Cj+2xl+a84APxQ==} - - '@git-diff-view/react@0.1.3': - resolution: {integrity: sha512-AKfkGGx0ulXsNE8ITfsC7fDRFT3pPlQJjwGlEq7+yC1D95E5mSGP/9GHLCEpuTXmcNv6D6h+4j/Lq2/DtgaQvg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - '@google/genai@1.52.0': - resolution: {integrity: sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==} - engines: {node: '>=20.0.0'} - peerDependencies: - '@modelcontextprotocol/sdk': ^1.25.2 - peerDependenciesMeta: - '@modelcontextprotocol/sdk': - optional: true - - '@iconify-json/gravity-ui@1.2.12': - resolution: {integrity: sha512-/66CorNnUoFs66nb8FJ+ZPq6Zog9NwR13ml90qijI0Hj3bVqQIdiWZ9npSyNWRgJe452LFmWZrdWQONYBbJ7XA==} - - '@iconify-json/logos@1.2.11': - resolution: {integrity: sha512-fOo4pGEatuyuCFNL+cwquYMa2Im0oJHRHV7lt/Qqs5Ode/lPImHCQcfTtPzZj7qYMPb/h8YHN3TG54uEowrjNQ==} - - '@iconify-json/lucide@1.2.108': - resolution: {integrity: sha512-jnmMx7xxShfsKeNNJhn47IKj3gD/AbRz+poKLIPn4rSIXw+yVbXCfUBXza/Jo9YIEEFajBk6Zayet8DqGCvX6w==} - - '@iconify-json/material-icon-theme@1.2.67': - resolution: {integrity: sha512-SouqLxahwVOuIVqED8Spl1wSy3DM7sYwNOPxFW7Eh4hVNqR4L4zTkEXFP4V6bQegkhl9kFWbNGlYKDFjXh92iQ==} - - '@iconify/types@2.0.0': - resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} - - '@iconify/utils@3.1.0': - resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==} - - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - - '@mermaid-js/parser@1.1.0': - resolution: {integrity: sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw==} - - '@mistralai/mistralai@2.2.6': - resolution: {integrity: sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==} - peerDependencies: - '@opentelemetry/api': ^1.9.0 - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - - '@napi-rs/wasm-runtime@1.1.3': - resolution: {integrity: sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==} - peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 - - '@opentelemetry/api@1.9.0': - resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} - engines: {node: '>=8.0.0'} - - '@opentelemetry/semantic-conventions@1.42.0': - resolution: {integrity: sha512-icc5xCzndZfhuJMy5oqk5AvloWquR7jtae74qzpkKkhGp8BivK+oCcEXgGnjCdTfp8hA44l+w8gE8yYJbocJJw==} - engines: {node: '>=14'} - - '@oxc-project/types@0.124.0': - resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} - - '@protobufjs/aspromise@1.1.2': - resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} - - '@protobufjs/base64@1.1.2': - resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} - - '@protobufjs/codegen@2.0.5': - resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} - - '@protobufjs/eventemitter@1.1.1': - resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} - - '@protobufjs/fetch@1.1.1': - resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} - - '@protobufjs/float@1.0.2': - resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} - - '@protobufjs/path@1.1.2': - resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} - - '@protobufjs/pool@1.1.0': - resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} - - '@protobufjs/utf8@1.1.2': - resolution: {integrity: sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==} - - '@rolldown/binding-android-arm64@1.0.0-rc.15': - resolution: {integrity: sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - - '@rolldown/binding-darwin-arm64@1.0.0-rc.15': - resolution: {integrity: sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@rolldown/binding-darwin-x64@1.0.0-rc.15': - resolution: {integrity: sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@rolldown/binding-freebsd-x64@1.0.0-rc.15': - resolution: {integrity: sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': - resolution: {integrity: sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': - resolution: {integrity: sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': - resolution: {integrity: sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': - resolution: {integrity: sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - - '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': - resolution: {integrity: sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': - resolution: {integrity: sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': - resolution: {integrity: sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - - '@rolldown/pluginutils@1.0.0-rc.15': - resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==} - - '@rolldown/pluginutils@1.0.0-rc.7': - resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} - - '@shikijs/core@3.23.0': - resolution: {integrity: sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==} - - '@shikijs/engine-javascript@3.23.0': - resolution: {integrity: sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==} - - '@shikijs/engine-oniguruma@3.23.0': - resolution: {integrity: sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==} - - '@shikijs/langs@3.23.0': - resolution: {integrity: sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==} - - '@shikijs/themes@3.23.0': - resolution: {integrity: sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==} - - '@shikijs/types@3.23.0': - resolution: {integrity: sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==} - - '@shikijs/vscode-textmate@10.0.2': - resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} - - '@sinclair/typebox@0.34.49': - resolution: {integrity: sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==} - - '@smithy/core@3.29.2': - resolution: {integrity: sha512-DXUk6yU0C1Q1tYvJh1VCtl8QOBcSoZpKwjTPkxT6A4MUQYHvgeKGByL8mrEdxnvhdf9nq5GyzmRb5n/vPgu3Lw==} - engines: {node: '>=18.0.0'} - - '@smithy/credential-provider-imds@4.4.7': - resolution: {integrity: sha512-UEMLOoA0Fl4uYBxh6l0uN0H6EJe/A89OGeDNTteQeXpJ20BcpfIr4wlCY9pel1jEAUHAxaYwuqrYlrKdXE1GKQ==} - engines: {node: '>=18.0.0'} - - '@smithy/fetch-http-handler@5.6.4': - resolution: {integrity: sha512-psnst7NZWdAEvJvyW8YZEE7xNVMyLrQFfHtyrVFrxNyy+dKWkQ+rqC6oI5ZhxThpUy9RSfEshgm34zqbOxzsRw==} - engines: {node: '>=18.0.0'} - - '@smithy/is-array-buffer@2.2.0': - resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} - engines: {node: '>=14.0.0'} - - '@smithy/node-http-handler@4.7.3': - resolution: {integrity: sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==} - engines: {node: '>=18.0.0'} - - '@smithy/node-http-handler@4.9.4': - resolution: {integrity: sha512-BNTop/fSOptmoVk8g+efwHCofFh37g70OWGAFES1TeAAJja1K5aAI8rTE26ETSc5k8IQuWY2kAIoPla01NgYrA==} - engines: {node: '>=18.0.0'} - - '@smithy/signature-v4@5.6.3': - resolution: {integrity: sha512-8qVKKzqh7naF27ePmx0SkUfnGP/wBI9dyaeAmhHvopnbIlItUAmB/e6PkPCU3rRb2v9BY8D4EZXSoydSibatvw==} - engines: {node: '>=18.0.0'} - - '@smithy/types@4.16.0': - resolution: {integrity: sha512-aVUabzlBBmY0PfvVgLKQSOGFIL5/7R54JE3uD9a5Ay/jSED61SkuAcCYENNXJzYUvJ1NPrWO0P+rAXHCkbBUKw==} - engines: {node: '>=18.0.0'} - - '@smithy/util-buffer-from@2.2.0': - resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} - engines: {node: '>=14.0.0'} - - '@smithy/util-utf8@2.3.0': - resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} - engines: {node: '>=14.0.0'} - - '@streamdown/cjk@1.0.3': - resolution: {integrity: sha512-WRg8HR/gHbBoTgsMd91OKFUClIoDcEFVofJvluvEAyjx3KpU0aGgD9tGDqHkHj14ShoMSkX0IYetWGegTcwIJw==} - peerDependencies: - react: ^18.0.0 || ^19.0.0 - - '@streamdown/code@1.1.1': - resolution: {integrity: sha512-i7HTNuDgZWb+VdrNVOam9gQhIc5MSSDXKWXgbUrn/4vSRaSMM+Rtl10MQj4wLWPNpF7p80waJsAqFP8HZfb0Jg==} - peerDependencies: - react: ^18.0.0 || ^19.0.0 - - '@streamdown/math@1.0.2': - resolution: {integrity: sha512-r8Ur9/lBuFnzZAFdEWrLUF2s/gRwRRRwruqltdZibyjbCBnuW7SJbFm26nXqvpJPW/gzpBUMrBVBzd88z05D5g==} - peerDependencies: - react: ^18.0.0 || ^19.0.0 - - '@streamdown/mermaid@1.0.2': - resolution: {integrity: sha512-Fr/4sBWnAeSnxM3PcrV/+DiZe5oPMq9gOkUIAH7ZauJeuwrZ/DVzD4g0zlav6AH0axh2m/sOfrfLtY5aLT7niw==} - peerDependencies: - react: ^18.0.0 || ^19.0.0 - - '@svgr/babel-plugin-add-jsx-attribute@8.0.0': - resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==} - engines: {node: '>=14'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@svgr/babel-plugin-remove-jsx-attribute@8.0.0': - resolution: {integrity: sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==} - engines: {node: '>=14'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0': - resolution: {integrity: sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==} - engines: {node: '>=14'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0': - resolution: {integrity: sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==} - engines: {node: '>=14'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@svgr/babel-plugin-svg-dynamic-title@8.0.0': - resolution: {integrity: sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==} - engines: {node: '>=14'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@svgr/babel-plugin-svg-em-dimensions@8.0.0': - resolution: {integrity: sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==} - engines: {node: '>=14'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@svgr/babel-plugin-transform-react-native-svg@8.1.0': - resolution: {integrity: sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==} - engines: {node: '>=14'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@svgr/babel-plugin-transform-svg-component@8.0.0': - resolution: {integrity: sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==} - engines: {node: '>=12'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@svgr/babel-preset@8.1.0': - resolution: {integrity: sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==} - engines: {node: '>=14'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@svgr/core@8.1.0': - resolution: {integrity: sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==} - engines: {node: '>=14'} - - '@svgr/hast-util-to-babel-ast@8.0.0': - resolution: {integrity: sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==} - engines: {node: '>=14'} - - '@svgr/plugin-jsx@8.1.0': - resolution: {integrity: sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==} - engines: {node: '>=14'} - peerDependencies: - '@svgr/core': '*' - - '@tailwindcss/node@4.2.2': - resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==} - - '@tailwindcss/oxide-android-arm64@4.2.2': - resolution: {integrity: sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [android] - - '@tailwindcss/oxide-darwin-arm64@4.2.2': - resolution: {integrity: sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [darwin] - - '@tailwindcss/oxide-darwin-x64@4.2.2': - resolution: {integrity: sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==} - engines: {node: '>= 20'} - cpu: [x64] - os: [darwin] - - '@tailwindcss/oxide-freebsd-x64@4.2.2': - resolution: {integrity: sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==} - engines: {node: '>= 20'} - cpu: [x64] - os: [freebsd] - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': - resolution: {integrity: sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==} - engines: {node: '>= 20'} - cpu: [arm] - os: [linux] - - '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': - resolution: {integrity: sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@tailwindcss/oxide-linux-arm64-musl@4.2.2': - resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@tailwindcss/oxide-linux-x64-gnu@4.2.2': - resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==} - engines: {node: '>= 20'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@tailwindcss/oxide-linux-x64-musl@4.2.2': - resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==} - engines: {node: '>= 20'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@tailwindcss/oxide-wasm32-wasi@4.2.2': - resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - bundledDependencies: - - '@napi-rs/wasm-runtime' - - '@emnapi/core' - - '@emnapi/runtime' - - '@tybys/wasm-util' - - '@emnapi/wasi-threads' - - tslib - - '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': - resolution: {integrity: sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [win32] - - '@tailwindcss/oxide-win32-x64-msvc@4.2.2': - resolution: {integrity: sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==} - engines: {node: '>= 20'} - cpu: [x64] - os: [win32] - - '@tailwindcss/oxide@4.2.2': - resolution: {integrity: sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==} - engines: {node: '>= 20'} - - '@tailwindcss/postcss@4.2.2': - resolution: {integrity: sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==} - - '@tailwindcss/typography@0.5.20': - resolution: {integrity: sha512-hwbzQuNUfcPvbegQFatVPl/MY/tcM9KLl963hQ5laJKPh81TEZ1+dNG9PirGvcaDBkp+BCshExAyKVPW91dozw==} - peerDependencies: - tailwindcss: '>=3.0.0 || >=4.0.0 || insiders' - - '@tanstack/react-virtual@3.14.6': - resolution: {integrity: sha512-4+Uq8m0/gzO4kMCHUEpTtGX1RnONK0C+g88b2ltwPMWUBiaVarBuWKoPJaz7gj1cKCVRAdyu+U8GcKhwCc2beA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - '@tanstack/virtual-core@3.17.4': - resolution: {integrity: sha512-nGm5KteqxasUdThLc2izl6dHUqLv0LQj7Nuyo5gYalTPf/U8a9ermvsl7reT+6ioBW1l8WfpP/mcU338nLXpqw==} - - '@tybys/wasm-util@0.10.1': - resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} - - '@types/d3-array@3.2.2': - resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} - - '@types/d3-axis@3.0.6': - resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} - - '@types/d3-brush@3.0.6': - resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} - - '@types/d3-chord@3.0.6': - resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} - - '@types/d3-color@3.1.3': - resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} - - '@types/d3-contour@3.0.6': - resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} - - '@types/d3-delaunay@6.0.4': - resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} - - '@types/d3-dispatch@3.0.7': - resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} - - '@types/d3-drag@3.0.7': - resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} - - '@types/d3-dsv@3.0.7': - resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} - - '@types/d3-ease@3.0.2': - resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} - - '@types/d3-fetch@3.0.7': - resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} - - '@types/d3-force@3.0.10': - resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} - - '@types/d3-format@3.0.4': - resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} - - '@types/d3-geo@3.1.0': - resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} - - '@types/d3-hierarchy@3.1.7': - resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} - - '@types/d3-interpolate@3.0.4': - resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} - - '@types/d3-path@3.1.1': - resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} - - '@types/d3-polygon@3.0.2': - resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} - - '@types/d3-quadtree@3.0.6': - resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} - - '@types/d3-random@3.0.3': - resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} - - '@types/d3-scale-chromatic@3.1.0': - resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} - - '@types/d3-scale@4.0.9': - resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} - - '@types/d3-selection@3.0.11': - resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} - - '@types/d3-shape@3.1.8': - resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} - - '@types/d3-time-format@4.0.3': - resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} - - '@types/d3-time@3.0.4': - resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} - - '@types/d3-timer@3.0.2': - resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} - - '@types/d3-transition@3.0.9': - resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} - - '@types/d3-zoom@3.0.8': - resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} - - '@types/d3@7.4.3': - resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} - - '@types/debug@4.1.13': - resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} - - '@types/estree-jsx@1.0.5': - resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} - - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - - '@types/geojson@7946.0.16': - resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} - - '@types/hast@3.0.4': - resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} - - '@types/katex@0.16.8': - resolution: {integrity: sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==} - - '@types/mdast@4.0.4': - resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} - - '@types/ms@2.1.0': - resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - - '@types/node@26.1.1': - resolution: {integrity: sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==} - - '@types/react-dom@19.2.3': - resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} - peerDependencies: - '@types/react': ^19.2.0 - - '@types/react@19.2.14': - resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} - - '@types/retry@0.12.0': - resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} - - '@types/trusted-types@2.0.7': - resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} - - '@types/unist@2.0.11': - resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} - - '@types/unist@3.0.3': - resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - - '@typescript/vfs@1.6.4': - resolution: {integrity: sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ==} - peerDependencies: - typescript: '*' - - '@ungap/structured-clone@1.3.0': - resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - deprecated: Potential CWE-502 - Update to 1.3.1 or higher - - '@upsetjs/venn.js@2.0.0': - resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==} - - '@vitejs/plugin-react@6.0.1': - resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} - engines: {node: ^20.19.0 || >=22.12.0} - peerDependencies: - '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 - babel-plugin-react-compiler: ^1.0.0 - vite: ^8.0.0 - peerDependenciesMeta: - '@rolldown/plugin-babel': - optional: true - babel-plugin-react-compiler: - optional: true - - '@vue/reactivity@3.5.35': - resolution: {integrity: sha512-tVc+SsHConvh/Lz64qq1pP3rYArBmK42xonovEcxY74SQtvctZodG/zhq54P5dr38cVuw25d27cPNRdlMidpGQ==} - - '@vue/shared@3.5.35': - resolution: {integrity: sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA==} - - '@xterm/addon-fit@0.11.0': - resolution: {integrity: sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==} - - '@xterm/xterm@6.0.0': - resolution: {integrity: sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==} - - acorn@8.16.0: - resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} - engines: {node: '>=0.4.0'} - hasBin: true - - adler-32@1.3.1: - resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==} - engines: {node: '>=0.8'} - - agent-base@7.1.4: - resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} - engines: {node: '>= 14'} - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - - bail@2.0.2: - resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} - - base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - - baseline-browser-mapping@2.10.30: - resolution: {integrity: sha512-xjOFN16Ha1+Rz4nFYKqHU/LSB+gx/Vi3yQLX7r7sAW+Wa+8hhF2h4pvqTrTMc8+WcDBEunnUurr46Jvv0jk3Vg==} - engines: {node: '>=6.0.0'} - hasBin: true - - bignumber.js@9.3.1: - resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} - - bowser@2.14.1: - resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} - - browserslist@4.28.2: - resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - buffer-equal-constant-time@1.0.1: - resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} - - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - camelcase@6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} - engines: {node: '>=10'} - - caniuse-lite@1.0.30001793: - resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} - - ccount@2.0.1: - resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} - - cfb@1.2.2: - resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==} - engines: {node: '>=0.8'} - - character-entities-html4@2.1.0: - resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} - - character-entities-legacy@3.0.0: - resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} - - character-entities@2.0.2: - resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} - - character-reference-invalid@2.0.1: - resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} - - chevrotain-allstar@0.4.1: - resolution: {integrity: sha512-PvVJm3oGqrveUVW2Vt/eZGeiAIsJszYweUcYwcskg9e+IubNYKKD+rHHem7A6XVO22eDAL+inxNIGAzZ/VIWlA==} - peerDependencies: - chevrotain: ^12.0.0 - - chevrotain@12.0.0: - resolution: {integrity: sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ==} - engines: {node: '>=22.0.0'} - - class-variance-authority@0.7.1: - resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} - - clsx@2.1.1: - resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} - engines: {node: '>=6'} - - codepage@1.15.0: - resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==} - engines: {node: '>=0.8'} - - comma-separated-tokens@2.0.3: - resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} - - commander@7.2.0: - resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} - engines: {node: '>= 10'} - - commander@8.3.0: - resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} - engines: {node: '>= 12'} - - confbox@0.1.8: - resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - - confbox@0.2.4: - resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} - - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - - core-util-is@1.0.3: - resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - - cose-base@1.0.3: - resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} - - cose-base@2.2.0: - resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} - - cosmiconfig@8.3.6: - resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} - engines: {node: '>=14'} - peerDependencies: - typescript: '>=4.9.5' - peerDependenciesMeta: - typescript: - optional: true - - crc-32@1.2.2: - resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} - engines: {node: '>=0.8'} - hasBin: true - - cssesc@3.0.0: - resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} - engines: {node: '>=4'} - hasBin: true - - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - - cytoscape-cose-bilkent@4.1.0: - resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} - peerDependencies: - cytoscape: ^3.2.0 - - cytoscape-fcose@2.2.0: - resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} - peerDependencies: - cytoscape: ^3.2.0 - - cytoscape@3.33.2: - resolution: {integrity: sha512-sj4HXd3DokGhzZAdjDejGvTPLqlt84vNFN8m7bGsOzDY5DyVcxIb2ejIXat2Iy7HxWhdT/N1oKyheJ5YdpsGuw==} - engines: {node: '>=0.10'} - - d3-array@2.12.1: - resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} - - d3-array@3.2.4: - resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} - engines: {node: '>=12'} - - d3-axis@3.0.0: - resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} - engines: {node: '>=12'} - - d3-brush@3.0.0: - resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} - engines: {node: '>=12'} - - d3-chord@3.0.1: - resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} - engines: {node: '>=12'} - - d3-color@3.1.0: - resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} - engines: {node: '>=12'} - - d3-contour@4.0.2: - resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} - engines: {node: '>=12'} - - d3-delaunay@6.0.4: - resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} - engines: {node: '>=12'} - - d3-dispatch@3.0.1: - resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} - engines: {node: '>=12'} - - d3-drag@3.0.0: - resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} - engines: {node: '>=12'} - - d3-dsv@3.0.1: - resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} - engines: {node: '>=12'} - hasBin: true - - d3-ease@3.0.1: - resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} - engines: {node: '>=12'} - - d3-fetch@3.0.1: - resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} - engines: {node: '>=12'} - - d3-force@3.0.0: - resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} - engines: {node: '>=12'} - - d3-format@3.1.2: - resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} - engines: {node: '>=12'} - - d3-geo@3.1.1: - resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} - engines: {node: '>=12'} - - d3-hierarchy@3.1.2: - resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} - engines: {node: '>=12'} - - d3-interpolate@3.0.1: - resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} - engines: {node: '>=12'} - - d3-path@1.0.9: - resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} - - d3-path@3.1.0: - resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} - engines: {node: '>=12'} - - d3-polygon@3.0.1: - resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} - engines: {node: '>=12'} - - d3-quadtree@3.0.1: - resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} - engines: {node: '>=12'} - - d3-random@3.0.1: - resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} - engines: {node: '>=12'} - - d3-sankey@0.12.3: - resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} - - d3-scale-chromatic@3.1.0: - resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} - engines: {node: '>=12'} - - d3-scale@4.0.2: - resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} - engines: {node: '>=12'} - - d3-selection@3.0.0: - resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} - engines: {node: '>=12'} - - d3-shape@1.3.7: - resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} - - d3-shape@3.2.0: - resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} - engines: {node: '>=12'} - - d3-time-format@4.1.0: - resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} - engines: {node: '>=12'} - - d3-time@3.1.0: - resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} - engines: {node: '>=12'} - - d3-timer@3.0.1: - resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} - engines: {node: '>=12'} - - d3-transition@3.0.1: - resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} - engines: {node: '>=12'} - peerDependencies: - d3-selection: 2 - 3 - - d3-zoom@3.0.0: - resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} - engines: {node: '>=12'} - - d3@7.9.0: - resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} - engines: {node: '>=12'} - - dagre-d3-es@7.0.14: - resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==} - - data-uri-to-buffer@4.0.1: - resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} - engines: {node: '>= 12'} - - dayjs@1.11.20: - resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - decode-named-character-reference@1.3.0: - resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} - - delaunator@5.1.0: - resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} - - dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} - - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - - devlop@1.1.0: - resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} - - diff@8.0.4: - resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} - engines: {node: '>=0.3.1'} - - docx-preview@0.4.0: - resolution: {integrity: sha512-OdKtE/uj3M4RfGarLkGjahUzRg8/kBp0Sraj1r1NAY1tp/sTpHOBqDrzVf9onMBt9vxP6SdQ6bpLCUCsFwjgcA==} - - dompurify@3.2.7: - resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==} - - dompurify@3.3.3: - resolution: {integrity: sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==} - - dot-case@3.0.4: - resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} - - ecdsa-sig-formatter@1.0.11: - resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} - - electron-to-chromium@1.5.357: - resolution: {integrity: sha512-NHlTIQDK8fmVwHwuIzmXYEJ1Ewq3D9wDNc0cWXxDGysP6Pb21giwGNkxiTifyKy/4SoPuN5l6GLP1W9Sv7zB2g==} - - enhanced-resolve@5.20.1: - resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} - engines: {node: '>=10.13.0'} - - entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} - - entities@6.0.1: - resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} - engines: {node: '>=0.12'} - - error-ex@1.3.4: - resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} - - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - - escape-string-regexp@5.0.0: - resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} - engines: {node: '>=12'} - - estree-util-is-identifier-name@3.0.0: - resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} - - exsolve@1.0.8: - resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} - - extend@3.0.2: - resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - - fast-diff@1.3.0: - resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - fetch-blob@3.2.0: - resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} - engines: {node: ^12.20 || >= 14.13} - - formdata-polyfill@4.0.10: - resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} - engines: {node: '>=12.20.0'} - - frac@1.1.2: - resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==} - engines: {node: '>=0.8'} - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - gaxios@7.2.0: - resolution: {integrity: sha512-CUVb4wcYe+771XevyH6HtGmXFAGGKkIC3kswAP8Z1JCe0j80JMaTPZH930DWFrvo0atjh18Arc0pEyUCWa5bfg==} - engines: {node: '>=18'} - - gcp-metadata@8.1.2: - resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} - engines: {node: '>=18'} - - gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - - get-east-asian-width@1.5.0: - resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} - engines: {node: '>=18'} - - google-auth-library@10.9.0: - resolution: {integrity: sha512-xtvUqvINPhTaBm7nXqlYPcrMHJPm1lCNdSovxnKKhTm+4JsvQ+KGVYJViLoH9Yxu8w+T0Qv5HubzYT9BLrppJg==} - engines: {node: '>=18'} - - google-logging-utils@1.1.3: - resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} - engines: {node: '>=14'} - - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - - hachure-fill@0.5.2: - resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} - - hast-util-from-dom@5.0.1: - resolution: {integrity: sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==} - - hast-util-from-html-isomorphic@2.0.0: - resolution: {integrity: sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==} - - hast-util-from-html@2.0.3: - resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} - - hast-util-from-parse5@8.0.3: - resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} - - hast-util-is-element@3.0.0: - resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} - - hast-util-parse-selector@4.0.0: - resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} - - hast-util-raw@9.1.0: - resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} - - hast-util-sanitize@5.0.2: - resolution: {integrity: sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==} - - hast-util-to-html@9.0.5: - resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} - - hast-util-to-jsx-runtime@2.3.6: - resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} - - hast-util-to-parse5@8.0.1: - resolution: {integrity: sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==} - - hast-util-to-text@4.0.2: - resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} - - hast-util-whitespace@3.0.0: - resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} - - hastscript@9.0.1: - resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} - - highlight.js@11.11.1: - resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} - engines: {node: '>=12.0.0'} - - html-url-attributes@3.0.1: - resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} - - html-void-elements@3.0.0: - resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} - - http-proxy-agent@7.0.2: - resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} - engines: {node: '>= 14'} - - https-proxy-agent@7.0.6: - resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} - engines: {node: '>= 14'} - - iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - - immediate@3.0.6: - resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} - - import-fresh@3.3.1: - resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} - engines: {node: '>=6'} - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - inline-style-parser@0.2.7: - resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} - - internmap@1.0.1: - resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} - - internmap@2.0.3: - resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} - engines: {node: '>=12'} - - is-alphabetical@2.0.1: - resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} - - is-alphanumerical@2.0.1: - resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} - - is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - - is-decimal@2.0.1: - resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} - - is-hexadecimal@2.0.1: - resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} - - is-plain-obj@4.1.0: - resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} - engines: {node: '>=12'} - - isarray@1.0.0: - resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - - jiti@2.6.1: - resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} - hasBin: true - - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - js-yaml@4.1.1: - resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} - hasBin: true - - jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} - hasBin: true - - json-bigint@1.0.0: - resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} - - json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - - json-schema-to-ts@3.1.1: - resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} - engines: {node: '>=16'} - - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - - jszip@3.10.1: - resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} - - jwa@2.0.1: - resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} - - jws@4.0.1: - resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} - - katex@0.16.45: - resolution: {integrity: sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==} - hasBin: true - - katex@0.17.0: - resolution: {integrity: sha512-Vdw0ATsQ9V+LuegM/BTwQqV/6cTl5lbGcIrU+BCgLxyf6bo38ybOr372tuSIxir3CN720flu1meYR6XzNMwQnw==} - hasBin: true - - khroma@2.1.0: - resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} - - langium@4.2.2: - resolution: {integrity: sha512-JUshTRAfHI4/MF9dH2WupvjSXyn8JBuUEWazB8ZVJUtXutT0doDlAv1XKbZ1Pb5sMexa8FF4CFBc0iiul7gbUQ==} - engines: {node: '>=20.10.0', npm: '>=10.2.3'} - - layout-base@1.0.2: - resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} - - layout-base@2.0.1: - resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} - - lie@3.3.0: - resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} - - lightningcss-android-arm64@1.32.0: - resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [android] - - lightningcss-darwin-arm64@1.32.0: - resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [darwin] - - lightningcss-darwin-x64@1.32.0: - resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [darwin] - - lightningcss-freebsd-x64@1.32.0: - resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [freebsd] - - lightningcss-linux-arm-gnueabihf@1.32.0: - resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} - engines: {node: '>= 12.0.0'} - cpu: [arm] - os: [linux] - - lightningcss-linux-arm64-gnu@1.32.0: - resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - lightningcss-linux-arm64-musl@1.32.0: - resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [musl] - - lightningcss-linux-x64-gnu@1.32.0: - resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [glibc] - - lightningcss-linux-x64-musl@1.32.0: - resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [musl] - - lightningcss-win32-arm64-msvc@1.32.0: - resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [win32] - - lightningcss-win32-x64-msvc@1.32.0: - resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [win32] - - lightningcss@1.32.0: - resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} - engines: {node: '>= 12.0.0'} - - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - - local-pkg@1.1.2: - resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} - engines: {node: '>=14'} - - lodash-es@4.18.1: - resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} - - long@5.3.2: - resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} - - longest-streak@3.1.0: - resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} - - lower-case@2.0.2: - resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} - - lowlight@3.3.0: - resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==} - - lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - - markdown-table@3.0.4: - resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} - - marked@14.0.0: - resolution: {integrity: sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==} - engines: {node: '>= 18'} - hasBin: true - - marked@16.4.2: - resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} - engines: {node: '>= 20'} - hasBin: true - - marked@17.0.6: - resolution: {integrity: sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA==} - engines: {node: '>= 20'} - hasBin: true - - mdast-util-find-and-replace@3.0.2: - resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} - - mdast-util-from-markdown@2.0.3: - resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} - - mdast-util-gfm-autolink-literal@2.0.1: - resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} - - mdast-util-gfm-footnote@2.1.0: - resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} - - mdast-util-gfm-strikethrough@2.0.0: - resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} - - mdast-util-gfm-table@2.0.0: - resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} - - mdast-util-gfm-task-list-item@2.0.0: - resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} - - mdast-util-gfm@3.1.0: - resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} - - mdast-util-math@3.0.0: - resolution: {integrity: sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==} - - mdast-util-mdx-expression@2.0.1: - resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} - - mdast-util-mdx-jsx@3.2.0: - resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} - - mdast-util-mdxjs-esm@2.0.1: - resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} - - mdast-util-newline-to-break@2.0.0: - resolution: {integrity: sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==} - - mdast-util-phrasing@4.1.0: - resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} - - mdast-util-to-hast@13.2.1: - resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} - - mdast-util-to-markdown@2.1.2: - resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} - - mdast-util-to-string@4.0.0: - resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} - - mermaid@11.14.0: - resolution: {integrity: sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g==} - - micromark-core-commonmark@2.0.3: - resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} - - micromark-extension-cjk-friendly-gfm-strikethrough@2.0.1: - resolution: {integrity: sha512-wVC0zwjJNqQeX+bb07YTPu/CvSAyCTafyYb7sMhX1r62/Lw5M/df3JyYaANyp8g15c1ypJRFSsookTqA1IDsUg==} - engines: {node: '>=18'} - peerDependencies: - micromark: ^4.0.0 - micromark-util-types: ^2.0.0 - peerDependenciesMeta: - micromark-util-types: - optional: true - - micromark-extension-cjk-friendly-util@3.0.1: - resolution: {integrity: sha512-GcbXqTTHOsiZHyF753oIddP/J2eH8j9zpyQPhkof6B2JNxfEJabnQqxbCgzJNuNes0Y2jTNJ3LiYPSXr6eJA8w==} - engines: {node: '>=18'} - peerDependencies: - micromark-util-types: '*' - peerDependenciesMeta: - micromark-util-types: - optional: true - - micromark-extension-cjk-friendly@2.0.1: - resolution: {integrity: sha512-OkzoYVTL1ChbvQ8Cc1ayTIz7paFQz8iS9oIYmewncweUSwmWR+hkJF9spJ1lxB90XldJl26A1F4IkPOKS3bDXw==} - engines: {node: '>=18'} - peerDependencies: - micromark: ^4.0.0 - micromark-util-types: ^2.0.0 - peerDependenciesMeta: - micromark-util-types: - optional: true - - micromark-extension-gfm-autolink-literal@2.1.0: - resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} - - micromark-extension-gfm-footnote@2.1.0: - resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} - - micromark-extension-gfm-strikethrough@2.1.0: - resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} - - micromark-extension-gfm-table@2.1.1: - resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} - - micromark-extension-gfm-tagfilter@2.0.0: - resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} - - micromark-extension-gfm-task-list-item@2.1.0: - resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} - - micromark-extension-gfm@3.0.0: - resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} - - micromark-extension-math@3.1.0: - resolution: {integrity: sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==} - - micromark-factory-destination@2.0.1: - resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} - - micromark-factory-label@2.0.1: - resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} - - micromark-factory-space@2.0.1: - resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} - - micromark-factory-title@2.0.1: - resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} - - micromark-factory-whitespace@2.0.1: - resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} - - micromark-util-character@2.1.1: - resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} - - micromark-util-chunked@2.0.1: - resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} - - micromark-util-classify-character@2.0.1: - resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} - - micromark-util-combine-extensions@2.0.1: - resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} - - micromark-util-decode-numeric-character-reference@2.0.2: - resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} - - micromark-util-decode-string@2.0.1: - resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} - - micromark-util-encode@2.0.1: - resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} - - micromark-util-html-tag-name@2.0.1: - resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} - - micromark-util-normalize-identifier@2.0.1: - resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} - - micromark-util-resolve-all@2.0.1: - resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} - - micromark-util-sanitize-uri@2.0.1: - resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} - - micromark-util-subtokenize@2.1.0: - resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} - - micromark-util-symbol@2.0.1: - resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} - - micromark-util-types@2.0.2: - resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} - - micromark@4.0.2: - resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} - - mlly@1.8.2: - resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} - - monaco-editor@0.55.1: - resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - no-case@3.0.4: - resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} - - node-domexception@1.0.0: - resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} - engines: {node: '>=10.5.0'} - deprecated: Use your platform's native DOMException instead - - node-fetch@3.3.2: - resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - node-releases@2.0.44: - resolution: {integrity: sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==} - - obug@2.1.1: - resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} - - oniguruma-parser@0.12.1: - resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} - - oniguruma-to-es@4.3.5: - resolution: {integrity: sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==} - - openai@6.26.0: - resolution: {integrity: sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==} - hasBin: true - peerDependencies: - ws: ^8.18.0 - zod: ^3.25 || ^4.0 - peerDependenciesMeta: - ws: - optional: true - zod: - optional: true - - p-retry@4.6.2: - resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} - engines: {node: '>=8'} - - package-manager-detector@1.6.0: - resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} - - pako@1.0.11: - resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} - - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - - parse-entities@4.0.2: - resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} - - parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} - - parse5@7.3.0: - resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} - - partial-json@0.1.7: - resolution: {integrity: sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==} - - path-data-parser@0.1.0: - resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} - - path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@4.0.4: - resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} - engines: {node: '>=12'} - - pkg-types@1.3.1: - resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - - pkg-types@2.3.1: - resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} - - points-on-curve@0.2.0: - resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} - - points-on-path@0.2.1: - resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} - - postcss-selector-parser@6.0.10: - resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} - engines: {node: '>=4'} - - postcss@8.5.9: - resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} - engines: {node: ^10 || ^12 || >=14} - - process-nextick-args@2.0.1: - resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - - property-information@7.1.0: - resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} - - protobufjs@7.6.5: - resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==} - engines: {node: '>=12.0.0'} - - quansync@0.2.11: - resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} - - react-complex-tree@2.6.2: - resolution: {integrity: sha512-6SEnSCMpxRlvTp4BUMEuQAv9Fiwo8EPr1HHjpJrk326JHIBw4S3f4M7nLhbzdFqbZ+2xfwKSswOFZaAjKWyWSw==} - peerDependencies: - react: '>=16.0.0' - - react-dom@19.2.5: - resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==} - peerDependencies: - react: ^19.2.5 - - react@19.2.5: - resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} - engines: {node: '>=0.10.0'} - - reactivity-store@0.4.0: - resolution: {integrity: sha512-uL9uoREOBg2o4zUa8vMU0AbvAOk0osPloizscmyZqMvJzcuuKX3ELFYYr1DX8gAcfvlhPduz4QuLZn1eChCu4Q==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - readable-stream@2.3.8: - resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} - - regex-recursion@6.0.2: - resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} - - regex-utilities@2.3.0: - resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} - - regex@6.1.0: - resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} - - rehype-harden@1.1.8: - resolution: {integrity: sha512-Qn7vR1xrf6fZCrkm9TDWi/AB4ylrHy+jqsNm1EHOAmbARYA6gsnVJBq/sdBh6kmT4NEZxH5vgIjrscefJAOXcw==} - - rehype-katex@7.0.1: - resolution: {integrity: sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==} - - rehype-raw@7.0.0: - resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} - - rehype-sanitize@6.0.0: - resolution: {integrity: sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==} - - remark-breaks@4.0.0: - resolution: {integrity: sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==} - - remark-cjk-friendly-gfm-strikethrough@2.0.1: - resolution: {integrity: sha512-pWKj25O2eLXIL1aBupayl1fKhco+Brw8qWUWJPVB9EBzbQNd7nGLj0nLmJpggWsGLR5j5y40PIdjxby9IEYTuA==} - engines: {node: '>=18'} - peerDependencies: - '@types/mdast': ^4.0.0 - unified: ^11.0.0 - peerDependenciesMeta: - '@types/mdast': - optional: true - - remark-cjk-friendly@2.0.1: - resolution: {integrity: sha512-6WwkoQyZf/4j5k53zdFYrR8Ca+UVn992jXdLUSBDZR4eBpFhKyVxmA4gUHra/5fesjGIxrDhHesNr/sVoiiysA==} - engines: {node: '>=18'} - peerDependencies: - '@types/mdast': ^4.0.0 - unified: ^11.0.0 - peerDependenciesMeta: - '@types/mdast': - optional: true - - remark-gfm@4.0.1: - resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} - - remark-math@6.0.0: - resolution: {integrity: sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==} - - remark-parse@11.0.0: - resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} - - remark-rehype@11.1.2: - resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} - - remark-stringify@11.0.0: - resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} - - remend@1.3.0: - resolution: {integrity: sha512-iIhggPkhW3hFImKtB10w0dz4EZbs28mV/dmbcYVonWEJ6UGHHpP+bFZnTh6GNWJONg5m+U56JrL+8IxZRdgWjw==} - - reselect@5.2.0: - resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==} - - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - - retry@0.13.1: - resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} - engines: {node: '>= 4'} - - robust-predicates@3.0.3: - resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} - - rolldown@1.0.0-rc.15: - resolution: {integrity: sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - - roughjs@4.6.6: - resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} - - rw@1.3.3: - resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} - - safe-buffer@5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - - scheduler@0.27.0: - resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - - semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - - setimmediate@1.0.5: - resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} - - shiki@3.23.0: - resolution: {integrity: sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==} - - snake-case@3.0.4: - resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - space-separated-tokens@2.0.2: - resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} - - ssf@0.11.2: - resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==} - engines: {node: '>=0.8'} - - streamdown@2.5.0: - resolution: {integrity: sha512-/tTnURfIOxZK/pqJAxsfCvETG/XCJHoWnk3jq9xLcuz6CSpnjjuxSRBTTL4PKGhxiZQf0lqPxGhImdpwcZ2XwA==} - peerDependencies: - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 - - string_decoder@1.1.1: - resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} - - stringify-entities@4.0.4: - resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} - - style-to-js@1.1.21: - resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} - - style-to-object@1.0.14: - resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} - - stylis@4.3.6: - resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} - - svg-parser@2.0.4: - resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} - - tailwind-merge@3.5.0: - resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} - - tailwindcss@4.2.2: - resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==} - - tapable@2.3.2: - resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} - engines: {node: '>=6'} - - tinyexec@1.1.1: - resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} - engines: {node: '>=18'} - - tinyglobby@0.2.16: - resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} - engines: {node: '>=12.0.0'} - - trim-lines@3.0.1: - resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} - - trough@2.2.0: - resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} - - ts-algebra@2.0.0: - resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} - - ts-dedent@2.2.0: - resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} - engines: {node: '>=6.10'} - - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - - typebox@1.1.38: - resolution: {integrity: sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==} - - typescript@5.4.5: - resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} - engines: {node: '>=14.17'} - hasBin: true - - typescript@6.0.2: - resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==} - engines: {node: '>=14.17'} - hasBin: true - - ufo@1.6.3: - resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} - - undici-types@8.3.0: - resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} - - unified@11.0.5: - resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} - - unist-util-find-after@5.0.0: - resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} - - unist-util-is@6.0.1: - resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} - - unist-util-position@5.0.0: - resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} - - unist-util-remove-position@5.0.0: - resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==} - - unist-util-stringify-position@4.0.0: - resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} - - unist-util-visit-parents@6.0.2: - resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} - - unist-util-visit@5.1.0: - resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} - - unplugin-icons@23.0.1: - resolution: {integrity: sha512-rv0XEJepajKzDLvRUWASM8K+8+/CCfZn2jtogXqg6RIp7kpatRc/aFrVJn8ANQA09e++lPEEv9yX8cC9enc+QQ==} - peerDependencies: - '@svgr/core': '>=7.0.0' - '@svgx/core': ^1.0.1 - '@vue/compiler-sfc': ^3.0.2 - svelte: ^3.0.0 || ^4.0.0 || ^5.0.0 - peerDependenciesMeta: - '@svgr/core': - optional: true - '@svgx/core': - optional: true - '@vue/compiler-sfc': - optional: true - svelte: - optional: true - - unplugin@2.3.11: - resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} - engines: {node: '>=18.12.0'} - - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - - use-sync-external-store@1.6.0: - resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - - uuid@11.1.0: - resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} - hasBin: true - - vfile-location@5.0.3: - resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} - - vfile-message@4.0.3: - resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} - - vfile@6.0.3: - resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - - vite@8.0.8: - resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.0 - esbuild: ^0.27.0 || ^0.28.0 - jiti: '>=1.21.0' - less: ^4.0.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - '@vitejs/devtools': - optional: true - esbuild: - optional: true - jiti: - optional: true - less: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - vscode-jsonrpc@8.2.0: - resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} - engines: {node: '>=14.0.0'} - - vscode-languageserver-protocol@3.17.5: - resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} - - vscode-languageserver-textdocument@1.0.12: - resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} - - vscode-languageserver-types@3.17.5: - resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} - - vscode-languageserver@9.0.1: - resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} - hasBin: true - - vscode-uri@3.1.0: - resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} - - web-namespaces@2.0.1: - resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} - - web-streams-polyfill@3.3.3: - resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} - engines: {node: '>= 8'} - - webpack-virtual-modules@0.6.2: - resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} - - wmf@1.0.2: - resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==} - engines: {node: '>=0.8'} - - word@0.3.0: - resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==} - engines: {node: '>=0.8'} - - ws@8.21.0: - resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - xlsx@0.18.5: - resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==} - engines: {node: '>=0.8'} - hasBin: true - - yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - - yet-another-react-lightbox@3.31.0: - resolution: {integrity: sha512-qcj8Vz5Gpbl2/XCXW/4vAdLGganPHEL1D5VgHxBQWbPF8ZeomH1DSN2ci++kv/1hMlccpUkg/nNx2CrMf9cLAg==} - engines: {node: '>=14'} - peerDependencies: - '@types/react': ^16 || ^17 || ^18 || ^19 - '@types/react-dom': ^16 || ^17 || ^18 || ^19 - react: ^16.8.0 || ^17 || ^18 || ^19 - react-dom: ^16.8.0 || ^17 || ^18 || ^19 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - zod-to-json-schema@3.25.2: - resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} - peerDependencies: - zod: ^3.25.28 || ^4 - - zod@4.4.3: - resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} - - zwitch@2.0.4: - resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} - -snapshots: - - '@alloc/quick-lru@5.2.0': {} - - '@antfu/install-pkg@1.1.0': - dependencies: - package-manager-detector: 1.6.0 - tinyexec: 1.1.1 - - '@anthropic-ai/sdk@0.91.1(zod@4.4.3)': - dependencies: - json-schema-to-ts: 3.1.1 - optionalDependencies: - zod: 4.4.3 - - '@aws-crypto/sha256-browser@5.2.0': - dependencies: - '@aws-crypto/sha256-js': 5.2.0 - '@aws-crypto/supports-web-crypto': 5.2.0 - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.974.0 - '@aws-sdk/util-locate-window': 3.965.8 - '@smithy/util-utf8': 2.3.0 - tslib: 2.8.1 - - '@aws-crypto/sha256-js@5.2.0': - dependencies: - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.974.0 - tslib: 2.8.1 - - '@aws-crypto/supports-web-crypto@5.2.0': - dependencies: - tslib: 2.8.1 - - '@aws-crypto/util@5.2.0': - dependencies: - '@aws-sdk/types': 3.974.0 - '@smithy/util-utf8': 2.3.0 - tslib: 2.8.1 - - '@aws-sdk/client-bedrock-runtime@3.1048.0': - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.975.1 - '@aws-sdk/credential-provider-node': 3.972.66 - '@aws-sdk/eventstream-handler-node': 3.972.26 - '@aws-sdk/middleware-eventstream': 3.972.22 - '@aws-sdk/middleware-websocket': 3.972.39 - '@aws-sdk/token-providers': 3.1048.0 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.2 - '@smithy/fetch-http-handler': 5.6.4 - '@smithy/node-http-handler': 4.9.4 - '@smithy/types': 4.16.0 - tslib: 2.8.1 - - '@aws-sdk/core@3.975.1': - dependencies: - '@aws-sdk/types': 3.974.0 - '@aws-sdk/xml-builder': 3.972.34 - '@aws/lambda-invoke-store': 0.3.0 - '@smithy/core': 3.29.2 - '@smithy/signature-v4': 5.6.3 - '@smithy/types': 4.16.0 - bowser: 2.14.1 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-env@3.972.57': - dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.2 - '@smithy/types': 4.16.0 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-http@3.972.59': - dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.2 - '@smithy/fetch-http-handler': 5.6.4 - '@smithy/node-http-handler': 4.9.4 - '@smithy/types': 4.16.0 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-ini@3.973.1': - dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/credential-provider-env': 3.972.57 - '@aws-sdk/credential-provider-http': 3.972.59 - '@aws-sdk/credential-provider-login': 3.972.63 - '@aws-sdk/credential-provider-process': 3.972.57 - '@aws-sdk/credential-provider-sso': 3.973.1 - '@aws-sdk/credential-provider-web-identity': 3.972.63 - '@aws-sdk/nested-clients': 3.997.31 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.2 - '@smithy/credential-provider-imds': 4.4.7 - '@smithy/types': 4.16.0 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-login@3.972.63': - dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/nested-clients': 3.997.31 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.2 - '@smithy/types': 4.16.0 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-node@3.972.66': - dependencies: - '@aws-sdk/credential-provider-env': 3.972.57 - '@aws-sdk/credential-provider-http': 3.972.59 - '@aws-sdk/credential-provider-ini': 3.973.1 - '@aws-sdk/credential-provider-process': 3.972.57 - '@aws-sdk/credential-provider-sso': 3.973.1 - '@aws-sdk/credential-provider-web-identity': 3.972.63 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.2 - '@smithy/credential-provider-imds': 4.4.7 - '@smithy/types': 4.16.0 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-process@3.972.57': - dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.2 - '@smithy/types': 4.16.0 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-sso@3.973.1': - dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/nested-clients': 3.997.31 - '@aws-sdk/token-providers': 3.1083.0 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.2 - '@smithy/types': 4.16.0 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-web-identity@3.972.63': - dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/nested-clients': 3.997.31 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.2 - '@smithy/types': 4.16.0 - tslib: 2.8.1 - - '@aws-sdk/eventstream-handler-node@3.972.26': - dependencies: - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.2 - '@smithy/types': 4.16.0 - tslib: 2.8.1 - - '@aws-sdk/middleware-eventstream@3.972.22': - dependencies: - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.2 - '@smithy/types': 4.16.0 - tslib: 2.8.1 - - '@aws-sdk/middleware-websocket@3.972.39': - dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.2 - '@smithy/fetch-http-handler': 5.6.4 - '@smithy/signature-v4': 5.6.3 - '@smithy/types': 4.16.0 - tslib: 2.8.1 - - '@aws-sdk/nested-clients@3.997.31': - dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/signature-v4-multi-region': 3.996.39 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.2 - '@smithy/fetch-http-handler': 5.6.4 - '@smithy/node-http-handler': 4.9.4 - '@smithy/types': 4.16.0 - tslib: 2.8.1 - - '@aws-sdk/signature-v4-multi-region@3.996.39': - dependencies: - '@aws-sdk/types': 3.974.0 - '@smithy/signature-v4': 5.6.3 - '@smithy/types': 4.16.0 - tslib: 2.8.1 - - '@aws-sdk/token-providers@3.1048.0': - dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/nested-clients': 3.997.31 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.2 - '@smithy/types': 4.16.0 - tslib: 2.8.1 - - '@aws-sdk/token-providers@3.1083.0': - dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/nested-clients': 3.997.31 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.2 - '@smithy/types': 4.16.0 - tslib: 2.8.1 - - '@aws-sdk/types@3.974.0': - dependencies: - '@smithy/types': 4.16.0 - tslib: 2.8.1 - - '@aws-sdk/util-locate-window@3.965.8': - dependencies: - tslib: 2.8.1 - - '@aws-sdk/xml-builder@3.972.34': - dependencies: - '@smithy/types': 4.16.0 - tslib: 2.8.1 - - '@aws/lambda-invoke-store@0.3.0': {} - - '@babel/code-frame@7.29.0': - dependencies: - '@babel/helper-validator-identifier': 7.29.7 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/compat-data@7.29.3': {} - - '@babel/core@7.29.0': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helpers': 7.29.2 - '@babel/parser': 7.29.3 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - '@jridgewell/remapping': 2.3.5 - convert-source-map: 2.0.0 - debug: 4.4.3 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/generator@7.29.1': - dependencies: - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 - - '@babel/helper-compilation-targets@7.28.6': - dependencies: - '@babel/compat-data': 7.29.3 - '@babel/helper-validator-option': 7.29.7 - browserslist: 4.28.2 - lru-cache: 5.1.1 - semver: 6.3.1 - - '@babel/helper-globals@7.29.7': {} - - '@babel/helper-module-imports@7.28.6': - dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-string-parser@7.29.7': {} - - '@babel/helper-validator-identifier@7.29.7': {} - - '@babel/helper-validator-option@7.29.7': {} - - '@babel/helpers@7.29.2': - dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - - '@babel/parser@7.29.3': - dependencies: - '@babel/types': 7.29.0 - - '@babel/runtime@7.29.7': {} - - '@babel/template@7.28.6': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 - - '@babel/traverse@7.29.0': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-globals': 7.29.7 - '@babel/parser': 7.29.3 - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - '@babel/types@7.29.0': - dependencies: - '@babel/helper-string-parser': 7.29.7 - '@babel/helper-validator-identifier': 7.29.7 - - '@base-ui/react@1.6.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@babel/runtime': 7.29.7 - '@base-ui/utils': 0.3.1(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@floating-ui/react-dom': 2.1.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@floating-ui/utils': 0.2.11 - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) - use-sync-external-store: 1.6.0(react@19.2.5) - optionalDependencies: - '@types/react': 19.2.14 - - '@base-ui/utils@0.3.1(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@babel/runtime': 7.29.7 - '@floating-ui/utils': 0.2.11 - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) - reselect: 5.2.0 - use-sync-external-store: 1.6.0(react@19.2.5) - optionalDependencies: - '@types/react': 19.2.14 - - '@biomejs/biome@2.5.2': - optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.5.2 - '@biomejs/cli-darwin-x64': 2.5.2 - '@biomejs/cli-linux-arm64': 2.5.2 - '@biomejs/cli-linux-arm64-musl': 2.5.2 - '@biomejs/cli-linux-x64': 2.5.2 - '@biomejs/cli-linux-x64-musl': 2.5.2 - '@biomejs/cli-win32-arm64': 2.5.2 - '@biomejs/cli-win32-x64': 2.5.2 - - '@biomejs/cli-darwin-arm64@2.5.2': - optional: true - - '@biomejs/cli-darwin-x64@2.5.2': - optional: true - - '@biomejs/cli-linux-arm64-musl@2.5.2': - optional: true - - '@biomejs/cli-linux-arm64@2.5.2': - optional: true - - '@biomejs/cli-linux-x64-musl@2.5.2': - optional: true - - '@biomejs/cli-linux-x64@2.5.2': - optional: true - - '@biomejs/cli-win32-arm64@2.5.2': - optional: true - - '@biomejs/cli-win32-x64@2.5.2': - optional: true - - '@braintree/sanitize-url@7.1.2': {} - - '@bufbuild/protobuf@2.12.1': {} - - '@bufbuild/protoc-gen-es@2.12.1(@bufbuild/protobuf@2.12.1)': - dependencies: - '@bufbuild/protoplugin': 2.12.1 - optionalDependencies: - '@bufbuild/protobuf': 2.12.1 - transitivePeerDependencies: - - supports-color - - '@bufbuild/protoplugin@2.12.1': - dependencies: - '@bufbuild/protobuf': 2.12.1 - '@typescript/vfs': 1.6.4(typescript@5.4.5) - typescript: 5.4.5 - transitivePeerDependencies: - - supports-color - - '@chevrotain/cst-dts-gen@12.0.0': - dependencies: - '@chevrotain/gast': 12.0.0 - '@chevrotain/types': 12.0.0 - - '@chevrotain/gast@12.0.0': - dependencies: - '@chevrotain/types': 12.0.0 - - '@chevrotain/regexp-to-ast@12.0.0': {} - - '@chevrotain/types@12.0.0': {} - - '@chevrotain/utils@12.0.0': {} - - '@earendil-works/pi-ai@0.80.6(ws@8.21.0)(zod@4.4.3)': - dependencies: - '@anthropic-ai/sdk': 0.91.1(zod@4.4.3) - '@aws-sdk/client-bedrock-runtime': 3.1048.0 - '@google/genai': 1.52.0 - '@mistralai/mistralai': 2.2.6(@opentelemetry/api@1.9.0) - '@opentelemetry/api': 1.9.0 - '@smithy/node-http-handler': 4.7.3 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 - openai: 6.26.0(ws@8.21.0)(zod@4.4.3) - partial-json: 0.1.7 - typebox: 1.1.38 - transitivePeerDependencies: - - '@modelcontextprotocol/sdk' - - bufferutil - - supports-color - - utf-8-validate - - ws - - zod - - '@emnapi/core@1.9.2': - dependencies: - '@emnapi/wasi-threads': 1.2.1 - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.9.2': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.2.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@floating-ui/core@1.7.5': - dependencies: - '@floating-ui/utils': 0.2.11 - - '@floating-ui/dom@1.7.6': - dependencies: - '@floating-ui/core': 1.7.5 - '@floating-ui/utils': 0.2.11 - - '@floating-ui/react-dom@2.1.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@floating-ui/dom': 1.7.6 - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) - - '@floating-ui/utils@0.2.11': {} - - '@git-diff-view/core@0.1.5': - dependencies: - '@git-diff-view/lowlight': 0.1.5 - fast-diff: 1.3.0 - highlight.js: 11.11.1 - lowlight: 3.3.0 - - '@git-diff-view/file@0.1.3': - dependencies: - '@git-diff-view/core': 0.1.5 - diff: 8.0.4 - fast-diff: 1.3.0 - highlight.js: 11.11.1 - lowlight: 3.3.0 - - '@git-diff-view/lowlight@0.1.5': - dependencies: - '@types/hast': 3.0.4 - highlight.js: 11.11.1 - lowlight: 3.3.0 - - '@git-diff-view/react@0.1.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@git-diff-view/core': 0.1.5 - '@types/hast': 3.0.4 - fast-diff: 1.3.0 - highlight.js: 11.11.1 - lowlight: 3.3.0 - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) - reactivity-store: 0.4.0(react@19.2.5) - use-sync-external-store: 1.6.0(react@19.2.5) - - '@google/genai@1.52.0': - dependencies: - google-auth-library: 10.9.0 - p-retry: 4.6.2 - protobufjs: 7.6.5 - ws: 8.21.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - '@iconify-json/gravity-ui@1.2.12': - dependencies: - '@iconify/types': 2.0.0 - - '@iconify-json/logos@1.2.11': - dependencies: - '@iconify/types': 2.0.0 - - '@iconify-json/lucide@1.2.108': - dependencies: - '@iconify/types': 2.0.0 - - '@iconify-json/material-icon-theme@1.2.67': - dependencies: - '@iconify/types': 2.0.0 - - '@iconify/types@2.0.0': {} - - '@iconify/utils@3.1.0': - dependencies: - '@antfu/install-pkg': 1.1.0 - '@iconify/types': 2.0.0 - mlly: 1.8.2 - - '@jridgewell/gen-mapping@0.3.13': - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/remapping@2.3.5': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/sourcemap-codec@1.5.5': {} - - '@jridgewell/trace-mapping@0.3.31': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - - '@mermaid-js/parser@1.1.0': - dependencies: - langium: 4.2.2 - - '@mistralai/mistralai@2.2.6(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/semantic-conventions': 1.42.0 - ws: 8.21.0 - zod: 4.4.3 - zod-to-json-schema: 3.25.2(zod@4.4.3) - optionalDependencies: - '@opentelemetry/api': 1.9.0 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@napi-rs/wasm-runtime@1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': - dependencies: - '@emnapi/core': 1.9.2 - '@emnapi/runtime': 1.9.2 - '@tybys/wasm-util': 0.10.1 - optional: true - - '@opentelemetry/api@1.9.0': {} - - '@opentelemetry/semantic-conventions@1.42.0': {} - - '@oxc-project/types@0.124.0': {} - - '@protobufjs/aspromise@1.1.2': {} - - '@protobufjs/base64@1.1.2': {} - - '@protobufjs/codegen@2.0.5': {} - - '@protobufjs/eventemitter@1.1.1': {} - - '@protobufjs/fetch@1.1.1': - dependencies: - '@protobufjs/aspromise': 1.1.2 - - '@protobufjs/float@1.0.2': {} - - '@protobufjs/path@1.1.2': {} - - '@protobufjs/pool@1.1.0': {} - - '@protobufjs/utf8@1.1.2': {} - - '@rolldown/binding-android-arm64@1.0.0-rc.15': - optional: true - - '@rolldown/binding-darwin-arm64@1.0.0-rc.15': - optional: true - - '@rolldown/binding-darwin-x64@1.0.0-rc.15': - optional: true - - '@rolldown/binding-freebsd-x64@1.0.0-rc.15': - optional: true - - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': - optional: true - - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': - optional: true - - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': - optional: true - - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': - optional: true - - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': - optional: true - - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': - optional: true - - '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': - optional: true - - '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': - optional: true - - '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': - dependencies: - '@emnapi/core': 1.9.2 - '@emnapi/runtime': 1.9.2 - '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) - optional: true - - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': - optional: true - - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': - optional: true - - '@rolldown/pluginutils@1.0.0-rc.15': {} - - '@rolldown/pluginutils@1.0.0-rc.7': {} - - '@shikijs/core@3.23.0': - dependencies: - '@shikijs/types': 3.23.0 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - hast-util-to-html: 9.0.5 - - '@shikijs/engine-javascript@3.23.0': - dependencies: - '@shikijs/types': 3.23.0 - '@shikijs/vscode-textmate': 10.0.2 - oniguruma-to-es: 4.3.5 - - '@shikijs/engine-oniguruma@3.23.0': - dependencies: - '@shikijs/types': 3.23.0 - '@shikijs/vscode-textmate': 10.0.2 - - '@shikijs/langs@3.23.0': - dependencies: - '@shikijs/types': 3.23.0 - - '@shikijs/themes@3.23.0': - dependencies: - '@shikijs/types': 3.23.0 - - '@shikijs/types@3.23.0': - dependencies: - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - - '@shikijs/vscode-textmate@10.0.2': {} - - '@sinclair/typebox@0.34.49': {} - - '@smithy/core@3.29.2': - dependencies: - '@smithy/types': 4.16.0 - tslib: 2.8.1 - - '@smithy/credential-provider-imds@4.4.7': - dependencies: - '@smithy/core': 3.29.2 - '@smithy/types': 4.16.0 - tslib: 2.8.1 - - '@smithy/fetch-http-handler@5.6.4': - dependencies: - '@smithy/core': 3.29.2 - '@smithy/types': 4.16.0 - tslib: 2.8.1 - - '@smithy/is-array-buffer@2.2.0': - dependencies: - tslib: 2.8.1 - - '@smithy/node-http-handler@4.7.3': - dependencies: - '@smithy/core': 3.29.2 - '@smithy/types': 4.16.0 - tslib: 2.8.1 - - '@smithy/node-http-handler@4.9.4': - dependencies: - '@smithy/core': 3.29.2 - '@smithy/types': 4.16.0 - tslib: 2.8.1 - - '@smithy/signature-v4@5.6.3': - dependencies: - '@smithy/core': 3.29.2 - '@smithy/types': 4.16.0 - tslib: 2.8.1 - - '@smithy/types@4.16.0': - dependencies: - tslib: 2.8.1 - - '@smithy/util-buffer-from@2.2.0': - dependencies: - '@smithy/is-array-buffer': 2.2.0 - tslib: 2.8.1 - - '@smithy/util-utf8@2.3.0': - dependencies: - '@smithy/util-buffer-from': 2.2.0 - tslib: 2.8.1 - - '@streamdown/cjk@1.0.3(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(react@19.2.5)(unified@11.0.5)': - dependencies: - react: 19.2.5 - remark-cjk-friendly: 2.0.1(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(unified@11.0.5) - remark-cjk-friendly-gfm-strikethrough: 2.0.1(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(unified@11.0.5) - unist-util-visit: 5.1.0 - transitivePeerDependencies: - - '@types/mdast' - - micromark - - micromark-util-types - - unified - - '@streamdown/code@1.1.1(react@19.2.5)': - dependencies: - react: 19.2.5 - shiki: 3.23.0 - - '@streamdown/math@1.0.2(react@19.2.5)': - dependencies: - katex: 0.16.45 - react: 19.2.5 - rehype-katex: 7.0.1 - remark-math: 6.0.0 - transitivePeerDependencies: - - supports-color - - '@streamdown/mermaid@1.0.2(react@19.2.5)': - dependencies: - mermaid: 11.14.0 - react: 19.2.5 - - '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - - '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - - '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - - '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - - '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - - '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - - '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - - '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - - '@svgr/babel-preset@8.1.0(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.29.0) - '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.29.0) - '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.29.0) - '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.29.0) - '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.29.0) - '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.29.0) - '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.29.0) - '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.29.0) - - '@svgr/core@8.1.0(typescript@6.0.2)': - dependencies: - '@babel/core': 7.29.0 - '@svgr/babel-preset': 8.1.0(@babel/core@7.29.0) - camelcase: 6.3.0 - cosmiconfig: 8.3.6(typescript@6.0.2) - snake-case: 3.0.4 - transitivePeerDependencies: - - supports-color - - typescript - - '@svgr/hast-util-to-babel-ast@8.0.0': - dependencies: - '@babel/types': 7.29.0 - entities: 4.5.0 - - '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@6.0.2))': - dependencies: - '@babel/core': 7.29.0 - '@svgr/babel-preset': 8.1.0(@babel/core@7.29.0) - '@svgr/core': 8.1.0(typescript@6.0.2) - '@svgr/hast-util-to-babel-ast': 8.0.0 - svg-parser: 2.0.4 - transitivePeerDependencies: - - supports-color - - '@tailwindcss/node@4.2.2': - dependencies: - '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.20.1 - jiti: 2.6.1 - lightningcss: 1.32.0 - magic-string: 0.30.21 - source-map-js: 1.2.1 - tailwindcss: 4.2.2 - - '@tailwindcss/oxide-android-arm64@4.2.2': - optional: true - - '@tailwindcss/oxide-darwin-arm64@4.2.2': - optional: true - - '@tailwindcss/oxide-darwin-x64@4.2.2': - optional: true - - '@tailwindcss/oxide-freebsd-x64@4.2.2': - optional: true - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': - optional: true - - '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': - optional: true - - '@tailwindcss/oxide-linux-arm64-musl@4.2.2': - optional: true - - '@tailwindcss/oxide-linux-x64-gnu@4.2.2': - optional: true - - '@tailwindcss/oxide-linux-x64-musl@4.2.2': - optional: true - - '@tailwindcss/oxide-wasm32-wasi@4.2.2': - optional: true - - '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': - optional: true - - '@tailwindcss/oxide-win32-x64-msvc@4.2.2': - optional: true - - '@tailwindcss/oxide@4.2.2': - optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.2.2 - '@tailwindcss/oxide-darwin-arm64': 4.2.2 - '@tailwindcss/oxide-darwin-x64': 4.2.2 - '@tailwindcss/oxide-freebsd-x64': 4.2.2 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.2 - '@tailwindcss/oxide-linux-arm64-gnu': 4.2.2 - '@tailwindcss/oxide-linux-arm64-musl': 4.2.2 - '@tailwindcss/oxide-linux-x64-gnu': 4.2.2 - '@tailwindcss/oxide-linux-x64-musl': 4.2.2 - '@tailwindcss/oxide-wasm32-wasi': 4.2.2 - '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 - '@tailwindcss/oxide-win32-x64-msvc': 4.2.2 - - '@tailwindcss/postcss@4.2.2': - dependencies: - '@alloc/quick-lru': 5.2.0 - '@tailwindcss/node': 4.2.2 - '@tailwindcss/oxide': 4.2.2 - postcss: 8.5.9 - tailwindcss: 4.2.2 - - '@tailwindcss/typography@0.5.20(tailwindcss@4.2.2)': - dependencies: - postcss-selector-parser: 6.0.10 - tailwindcss: 4.2.2 - - '@tanstack/react-virtual@3.14.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@tanstack/virtual-core': 3.17.4 - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) - - '@tanstack/virtual-core@3.17.4': {} - - '@tybys/wasm-util@0.10.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@types/d3-array@3.2.2': {} - - '@types/d3-axis@3.0.6': - dependencies: - '@types/d3-selection': 3.0.11 - - '@types/d3-brush@3.0.6': - dependencies: - '@types/d3-selection': 3.0.11 - - '@types/d3-chord@3.0.6': {} - - '@types/d3-color@3.1.3': {} - - '@types/d3-contour@3.0.6': - dependencies: - '@types/d3-array': 3.2.2 - '@types/geojson': 7946.0.16 - - '@types/d3-delaunay@6.0.4': {} - - '@types/d3-dispatch@3.0.7': {} - - '@types/d3-drag@3.0.7': - dependencies: - '@types/d3-selection': 3.0.11 - - '@types/d3-dsv@3.0.7': {} - - '@types/d3-ease@3.0.2': {} - - '@types/d3-fetch@3.0.7': - dependencies: - '@types/d3-dsv': 3.0.7 - - '@types/d3-force@3.0.10': {} - - '@types/d3-format@3.0.4': {} - - '@types/d3-geo@3.1.0': - dependencies: - '@types/geojson': 7946.0.16 - - '@types/d3-hierarchy@3.1.7': {} - - '@types/d3-interpolate@3.0.4': - dependencies: - '@types/d3-color': 3.1.3 - - '@types/d3-path@3.1.1': {} - - '@types/d3-polygon@3.0.2': {} - - '@types/d3-quadtree@3.0.6': {} - - '@types/d3-random@3.0.3': {} - - '@types/d3-scale-chromatic@3.1.0': {} - - '@types/d3-scale@4.0.9': - dependencies: - '@types/d3-time': 3.0.4 - - '@types/d3-selection@3.0.11': {} - - '@types/d3-shape@3.1.8': - dependencies: - '@types/d3-path': 3.1.1 - - '@types/d3-time-format@4.0.3': {} - - '@types/d3-time@3.0.4': {} - - '@types/d3-timer@3.0.2': {} - - '@types/d3-transition@3.0.9': - dependencies: - '@types/d3-selection': 3.0.11 - - '@types/d3-zoom@3.0.8': - dependencies: - '@types/d3-interpolate': 3.0.4 - '@types/d3-selection': 3.0.11 - - '@types/d3@7.4.3': - dependencies: - '@types/d3-array': 3.2.2 - '@types/d3-axis': 3.0.6 - '@types/d3-brush': 3.0.6 - '@types/d3-chord': 3.0.6 - '@types/d3-color': 3.1.3 - '@types/d3-contour': 3.0.6 - '@types/d3-delaunay': 6.0.4 - '@types/d3-dispatch': 3.0.7 - '@types/d3-drag': 3.0.7 - '@types/d3-dsv': 3.0.7 - '@types/d3-ease': 3.0.2 - '@types/d3-fetch': 3.0.7 - '@types/d3-force': 3.0.10 - '@types/d3-format': 3.0.4 - '@types/d3-geo': 3.1.0 - '@types/d3-hierarchy': 3.1.7 - '@types/d3-interpolate': 3.0.4 - '@types/d3-path': 3.1.1 - '@types/d3-polygon': 3.0.2 - '@types/d3-quadtree': 3.0.6 - '@types/d3-random': 3.0.3 - '@types/d3-scale': 4.0.9 - '@types/d3-scale-chromatic': 3.1.0 - '@types/d3-selection': 3.0.11 - '@types/d3-shape': 3.1.8 - '@types/d3-time': 3.0.4 - '@types/d3-time-format': 4.0.3 - '@types/d3-timer': 3.0.2 - '@types/d3-transition': 3.0.9 - '@types/d3-zoom': 3.0.8 - - '@types/debug@4.1.13': - dependencies: - '@types/ms': 2.1.0 - - '@types/estree-jsx@1.0.5': - dependencies: - '@types/estree': 1.0.8 - - '@types/estree@1.0.8': {} - - '@types/geojson@7946.0.16': {} - - '@types/hast@3.0.4': - dependencies: - '@types/unist': 3.0.3 - - '@types/katex@0.16.8': {} - - '@types/mdast@4.0.4': - dependencies: - '@types/unist': 3.0.3 - - '@types/ms@2.1.0': {} - - '@types/node@26.1.1': - dependencies: - undici-types: 8.3.0 - - '@types/react-dom@19.2.3(@types/react@19.2.14)': - dependencies: - '@types/react': 19.2.14 - - '@types/react@19.2.14': - dependencies: - csstype: 3.2.3 - - '@types/retry@0.12.0': {} - - '@types/trusted-types@2.0.7': - optional: true - - '@types/unist@2.0.11': {} - - '@types/unist@3.0.3': {} - - '@typescript/vfs@1.6.4(typescript@5.4.5)': - dependencies: - debug: 4.4.3 - typescript: 5.4.5 - transitivePeerDependencies: - - supports-color - - '@ungap/structured-clone@1.3.0': {} - - '@upsetjs/venn.js@2.0.0': - optionalDependencies: - d3-selection: 3.0.0 - d3-transition: 3.0.1(d3-selection@3.0.0) - - '@vitejs/plugin-react@6.0.1(vite@8.0.8(@types/node@26.1.1)(jiti@2.6.1))': - dependencies: - '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.8(@types/node@26.1.1)(jiti@2.6.1) - - '@vue/reactivity@3.5.35': - dependencies: - '@vue/shared': 3.5.35 - - '@vue/shared@3.5.35': {} - - '@xterm/addon-fit@0.11.0': {} - - '@xterm/xterm@6.0.0': {} - - acorn@8.16.0: {} - - adler-32@1.3.1: {} - - agent-base@7.1.4: {} - - argparse@2.0.1: {} - - bail@2.0.2: {} - - base64-js@1.5.1: {} - - baseline-browser-mapping@2.10.30: {} - - bignumber.js@9.3.1: {} - - bowser@2.14.1: {} - - browserslist@4.28.2: - dependencies: - baseline-browser-mapping: 2.10.30 - caniuse-lite: 1.0.30001793 - electron-to-chromium: 1.5.357 - node-releases: 2.0.44 - update-browserslist-db: 1.2.3(browserslist@4.28.2) - - buffer-equal-constant-time@1.0.1: {} - - callsites@3.1.0: {} - - camelcase@6.3.0: {} - - caniuse-lite@1.0.30001793: {} - - ccount@2.0.1: {} - - cfb@1.2.2: - dependencies: - adler-32: 1.3.1 - crc-32: 1.2.2 - - character-entities-html4@2.1.0: {} - - character-entities-legacy@3.0.0: {} - - character-entities@2.0.2: {} - - character-reference-invalid@2.0.1: {} - - chevrotain-allstar@0.4.1(chevrotain@12.0.0): - dependencies: - chevrotain: 12.0.0 - lodash-es: 4.18.1 - - chevrotain@12.0.0: - dependencies: - '@chevrotain/cst-dts-gen': 12.0.0 - '@chevrotain/gast': 12.0.0 - '@chevrotain/regexp-to-ast': 12.0.0 - '@chevrotain/types': 12.0.0 - '@chevrotain/utils': 12.0.0 - - class-variance-authority@0.7.1: - dependencies: - clsx: 2.1.1 - - clsx@2.1.1: {} - - codepage@1.15.0: {} - - comma-separated-tokens@2.0.3: {} - - commander@7.2.0: {} - - commander@8.3.0: {} - - confbox@0.1.8: {} - - confbox@0.2.4: {} - - convert-source-map@2.0.0: {} - - core-util-is@1.0.3: {} - - cose-base@1.0.3: - dependencies: - layout-base: 1.0.2 - - cose-base@2.2.0: - dependencies: - layout-base: 2.0.1 - - cosmiconfig@8.3.6(typescript@6.0.2): - dependencies: - import-fresh: 3.3.1 - js-yaml: 4.1.1 - parse-json: 5.2.0 - path-type: 4.0.0 - optionalDependencies: - typescript: 6.0.2 - - crc-32@1.2.2: {} - - cssesc@3.0.0: {} - - csstype@3.2.3: {} - - cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.2): - dependencies: - cose-base: 1.0.3 - cytoscape: 3.33.2 - - cytoscape-fcose@2.2.0(cytoscape@3.33.2): - dependencies: - cose-base: 2.2.0 - cytoscape: 3.33.2 - - cytoscape@3.33.2: {} - - d3-array@2.12.1: - dependencies: - internmap: 1.0.1 - - d3-array@3.2.4: - dependencies: - internmap: 2.0.3 - - d3-axis@3.0.0: {} - - d3-brush@3.0.0: - dependencies: - d3-dispatch: 3.0.1 - d3-drag: 3.0.0 - d3-interpolate: 3.0.1 - d3-selection: 3.0.0 - d3-transition: 3.0.1(d3-selection@3.0.0) - - d3-chord@3.0.1: - dependencies: - d3-path: 3.1.0 - - d3-color@3.1.0: {} - - d3-contour@4.0.2: - dependencies: - d3-array: 3.2.4 - - d3-delaunay@6.0.4: - dependencies: - delaunator: 5.1.0 - - d3-dispatch@3.0.1: {} - - d3-drag@3.0.0: - dependencies: - d3-dispatch: 3.0.1 - d3-selection: 3.0.0 - - d3-dsv@3.0.1: - dependencies: - commander: 7.2.0 - iconv-lite: 0.6.3 - rw: 1.3.3 - - d3-ease@3.0.1: {} - - d3-fetch@3.0.1: - dependencies: - d3-dsv: 3.0.1 - - d3-force@3.0.0: - dependencies: - d3-dispatch: 3.0.1 - d3-quadtree: 3.0.1 - d3-timer: 3.0.1 - - d3-format@3.1.2: {} - - d3-geo@3.1.1: - dependencies: - d3-array: 3.2.4 - - d3-hierarchy@3.1.2: {} - - d3-interpolate@3.0.1: - dependencies: - d3-color: 3.1.0 - - d3-path@1.0.9: {} - - d3-path@3.1.0: {} - - d3-polygon@3.0.1: {} - - d3-quadtree@3.0.1: {} - - d3-random@3.0.1: {} - - d3-sankey@0.12.3: - dependencies: - d3-array: 2.12.1 - d3-shape: 1.3.7 - - d3-scale-chromatic@3.1.0: - dependencies: - d3-color: 3.1.0 - d3-interpolate: 3.0.1 - - d3-scale@4.0.2: - dependencies: - d3-array: 3.2.4 - d3-format: 3.1.2 - d3-interpolate: 3.0.1 - d3-time: 3.1.0 - d3-time-format: 4.1.0 - - d3-selection@3.0.0: {} - - d3-shape@1.3.7: - dependencies: - d3-path: 1.0.9 - - d3-shape@3.2.0: - dependencies: - d3-path: 3.1.0 - - d3-time-format@4.1.0: - dependencies: - d3-time: 3.1.0 - - d3-time@3.1.0: - dependencies: - d3-array: 3.2.4 - - d3-timer@3.0.1: {} - - d3-transition@3.0.1(d3-selection@3.0.0): - dependencies: - d3-color: 3.1.0 - d3-dispatch: 3.0.1 - d3-ease: 3.0.1 - d3-interpolate: 3.0.1 - d3-selection: 3.0.0 - d3-timer: 3.0.1 - - d3-zoom@3.0.0: - dependencies: - d3-dispatch: 3.0.1 - d3-drag: 3.0.0 - d3-interpolate: 3.0.1 - d3-selection: 3.0.0 - d3-transition: 3.0.1(d3-selection@3.0.0) - - d3@7.9.0: - dependencies: - d3-array: 3.2.4 - d3-axis: 3.0.0 - d3-brush: 3.0.0 - d3-chord: 3.0.1 - d3-color: 3.1.0 - d3-contour: 4.0.2 - d3-delaunay: 6.0.4 - d3-dispatch: 3.0.1 - d3-drag: 3.0.0 - d3-dsv: 3.0.1 - d3-ease: 3.0.1 - d3-fetch: 3.0.1 - d3-force: 3.0.0 - d3-format: 3.1.2 - d3-geo: 3.1.1 - d3-hierarchy: 3.1.2 - d3-interpolate: 3.0.1 - d3-path: 3.1.0 - d3-polygon: 3.0.1 - d3-quadtree: 3.0.1 - d3-random: 3.0.1 - d3-scale: 4.0.2 - d3-scale-chromatic: 3.1.0 - d3-selection: 3.0.0 - d3-shape: 3.2.0 - d3-time: 3.1.0 - d3-time-format: 4.1.0 - d3-timer: 3.0.1 - d3-transition: 3.0.1(d3-selection@3.0.0) - d3-zoom: 3.0.0 - - dagre-d3-es@7.0.14: - dependencies: - d3: 7.9.0 - lodash-es: 4.18.1 - - data-uri-to-buffer@4.0.1: {} - - dayjs@1.11.20: {} - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - decode-named-character-reference@1.3.0: - dependencies: - character-entities: 2.0.2 - - delaunator@5.1.0: - dependencies: - robust-predicates: 3.0.3 - - dequal@2.0.3: {} - - detect-libc@2.1.2: {} - - devlop@1.1.0: - dependencies: - dequal: 2.0.3 - - diff@8.0.4: {} - - docx-preview@0.4.0: - dependencies: - jszip: 3.10.1 - - dompurify@3.2.7: - optionalDependencies: - '@types/trusted-types': 2.0.7 - - dompurify@3.3.3: - optionalDependencies: - '@types/trusted-types': 2.0.7 - - dot-case@3.0.4: - dependencies: - no-case: 3.0.4 - tslib: 2.8.1 - - ecdsa-sig-formatter@1.0.11: - dependencies: - safe-buffer: 5.1.2 - - electron-to-chromium@1.5.357: {} - - enhanced-resolve@5.20.1: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.3.2 - - entities@4.5.0: {} - - entities@6.0.1: {} - - error-ex@1.3.4: - dependencies: - is-arrayish: 0.2.1 - - escalade@3.2.0: {} - - escape-string-regexp@5.0.0: {} - - estree-util-is-identifier-name@3.0.0: {} - - exsolve@1.0.8: {} - - extend@3.0.2: {} - - fast-diff@1.3.0: {} - - fdir@6.5.0(picomatch@4.0.4): - optionalDependencies: - picomatch: 4.0.4 - - fetch-blob@3.2.0: - dependencies: - node-domexception: 1.0.0 - web-streams-polyfill: 3.3.3 - - formdata-polyfill@4.0.10: - dependencies: - fetch-blob: 3.2.0 - - frac@1.1.2: {} - - fsevents@2.3.3: - optional: true - - gaxios@7.2.0: - dependencies: - extend: 3.0.2 - https-proxy-agent: 7.0.6 - node-fetch: 3.3.2 - transitivePeerDependencies: - - supports-color - - gcp-metadata@8.1.2: - dependencies: - gaxios: 7.2.0 - google-logging-utils: 1.1.3 - json-bigint: 1.0.0 - transitivePeerDependencies: - - supports-color - - gensync@1.0.0-beta.2: {} - - get-east-asian-width@1.5.0: {} - - google-auth-library@10.9.0: - dependencies: - base64-js: 1.5.1 - ecdsa-sig-formatter: 1.0.11 - gaxios: 7.2.0 - gcp-metadata: 8.1.2 - google-logging-utils: 1.1.3 - jws: 4.0.1 - transitivePeerDependencies: - - supports-color - - google-logging-utils@1.1.3: {} - - graceful-fs@4.2.11: {} - - hachure-fill@0.5.2: {} - - hast-util-from-dom@5.0.1: - dependencies: - '@types/hast': 3.0.4 - hastscript: 9.0.1 - web-namespaces: 2.0.1 - - hast-util-from-html-isomorphic@2.0.0: - dependencies: - '@types/hast': 3.0.4 - hast-util-from-dom: 5.0.1 - hast-util-from-html: 2.0.3 - unist-util-remove-position: 5.0.0 - - hast-util-from-html@2.0.3: - dependencies: - '@types/hast': 3.0.4 - devlop: 1.1.0 - hast-util-from-parse5: 8.0.3 - parse5: 7.3.0 - vfile: 6.0.3 - vfile-message: 4.0.3 - - hast-util-from-parse5@8.0.3: - dependencies: - '@types/hast': 3.0.4 - '@types/unist': 3.0.3 - devlop: 1.1.0 - hastscript: 9.0.1 - property-information: 7.1.0 - vfile: 6.0.3 - vfile-location: 5.0.3 - web-namespaces: 2.0.1 - - hast-util-is-element@3.0.0: - dependencies: - '@types/hast': 3.0.4 - - hast-util-parse-selector@4.0.0: - dependencies: - '@types/hast': 3.0.4 - - hast-util-raw@9.1.0: - dependencies: - '@types/hast': 3.0.4 - '@types/unist': 3.0.3 - '@ungap/structured-clone': 1.3.0 - hast-util-from-parse5: 8.0.3 - hast-util-to-parse5: 8.0.1 - html-void-elements: 3.0.0 - mdast-util-to-hast: 13.2.1 - parse5: 7.3.0 - unist-util-position: 5.0.0 - unist-util-visit: 5.1.0 - vfile: 6.0.3 - web-namespaces: 2.0.1 - zwitch: 2.0.4 - - hast-util-sanitize@5.0.2: - dependencies: - '@types/hast': 3.0.4 - '@ungap/structured-clone': 1.3.0 - unist-util-position: 5.0.0 - - hast-util-to-html@9.0.5: - dependencies: - '@types/hast': 3.0.4 - '@types/unist': 3.0.3 - ccount: 2.0.1 - comma-separated-tokens: 2.0.3 - hast-util-whitespace: 3.0.0 - html-void-elements: 3.0.0 - mdast-util-to-hast: 13.2.1 - property-information: 7.1.0 - space-separated-tokens: 2.0.2 - stringify-entities: 4.0.4 - zwitch: 2.0.4 - - hast-util-to-jsx-runtime@2.3.6: - dependencies: - '@types/estree': 1.0.8 - '@types/hast': 3.0.4 - '@types/unist': 3.0.3 - comma-separated-tokens: 2.0.3 - devlop: 1.1.0 - estree-util-is-identifier-name: 3.0.0 - hast-util-whitespace: 3.0.0 - mdast-util-mdx-expression: 2.0.1 - mdast-util-mdx-jsx: 3.2.0 - mdast-util-mdxjs-esm: 2.0.1 - property-information: 7.1.0 - space-separated-tokens: 2.0.2 - style-to-js: 1.1.21 - unist-util-position: 5.0.0 - vfile-message: 4.0.3 - transitivePeerDependencies: - - supports-color - - hast-util-to-parse5@8.0.1: - dependencies: - '@types/hast': 3.0.4 - comma-separated-tokens: 2.0.3 - devlop: 1.1.0 - property-information: 7.1.0 - space-separated-tokens: 2.0.2 - web-namespaces: 2.0.1 - zwitch: 2.0.4 - - hast-util-to-text@4.0.2: - dependencies: - '@types/hast': 3.0.4 - '@types/unist': 3.0.3 - hast-util-is-element: 3.0.0 - unist-util-find-after: 5.0.0 - - hast-util-whitespace@3.0.0: - dependencies: - '@types/hast': 3.0.4 - - hastscript@9.0.1: - dependencies: - '@types/hast': 3.0.4 - comma-separated-tokens: 2.0.3 - hast-util-parse-selector: 4.0.0 - property-information: 7.1.0 - space-separated-tokens: 2.0.2 - - highlight.js@11.11.1: {} - - html-url-attributes@3.0.1: {} - - html-void-elements@3.0.0: {} - - http-proxy-agent@7.0.2: - dependencies: - agent-base: 7.1.4 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - https-proxy-agent@7.0.6: - dependencies: - agent-base: 7.1.4 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - iconv-lite@0.6.3: - dependencies: - safer-buffer: 2.1.2 - - immediate@3.0.6: {} - - import-fresh@3.3.1: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - - inherits@2.0.4: {} - - inline-style-parser@0.2.7: {} - - internmap@1.0.1: {} - - internmap@2.0.3: {} - - is-alphabetical@2.0.1: {} - - is-alphanumerical@2.0.1: - dependencies: - is-alphabetical: 2.0.1 - is-decimal: 2.0.1 - - is-arrayish@0.2.1: {} - - is-decimal@2.0.1: {} - - is-hexadecimal@2.0.1: {} - - is-plain-obj@4.1.0: {} - - isarray@1.0.0: {} - - jiti@2.6.1: {} - - js-tokens@4.0.0: {} - - js-yaml@4.1.1: - dependencies: - argparse: 2.0.1 - - jsesc@3.1.0: {} - - json-bigint@1.0.0: - dependencies: - bignumber.js: 9.3.1 - - json-parse-even-better-errors@2.3.1: {} - - json-schema-to-ts@3.1.1: - dependencies: - '@babel/runtime': 7.29.7 - ts-algebra: 2.0.0 - - json5@2.2.3: {} - - jszip@3.10.1: - dependencies: - lie: 3.3.0 - pako: 1.0.11 - readable-stream: 2.3.8 - setimmediate: 1.0.5 - - jwa@2.0.1: - dependencies: - buffer-equal-constant-time: 1.0.1 - ecdsa-sig-formatter: 1.0.11 - safe-buffer: 5.1.2 - - jws@4.0.1: - dependencies: - jwa: 2.0.1 - safe-buffer: 5.1.2 - - katex@0.16.45: - dependencies: - commander: 8.3.0 - - katex@0.17.0: - dependencies: - commander: 8.3.0 - - khroma@2.1.0: {} - - langium@4.2.2: - dependencies: - '@chevrotain/regexp-to-ast': 12.0.0 - chevrotain: 12.0.0 - chevrotain-allstar: 0.4.1(chevrotain@12.0.0) - vscode-languageserver: 9.0.1 - vscode-languageserver-textdocument: 1.0.12 - vscode-uri: 3.1.0 - - layout-base@1.0.2: {} - - layout-base@2.0.1: {} - - lie@3.3.0: - dependencies: - immediate: 3.0.6 - - lightningcss-android-arm64@1.32.0: - optional: true - - lightningcss-darwin-arm64@1.32.0: - optional: true - - lightningcss-darwin-x64@1.32.0: - optional: true - - lightningcss-freebsd-x64@1.32.0: - optional: true - - lightningcss-linux-arm-gnueabihf@1.32.0: - optional: true - - lightningcss-linux-arm64-gnu@1.32.0: - optional: true - - lightningcss-linux-arm64-musl@1.32.0: - optional: true - - lightningcss-linux-x64-gnu@1.32.0: - optional: true - - lightningcss-linux-x64-musl@1.32.0: - optional: true - - lightningcss-win32-arm64-msvc@1.32.0: - optional: true - - lightningcss-win32-x64-msvc@1.32.0: - optional: true - - lightningcss@1.32.0: - dependencies: - detect-libc: 2.1.2 - optionalDependencies: - lightningcss-android-arm64: 1.32.0 - lightningcss-darwin-arm64: 1.32.0 - lightningcss-darwin-x64: 1.32.0 - lightningcss-freebsd-x64: 1.32.0 - lightningcss-linux-arm-gnueabihf: 1.32.0 - lightningcss-linux-arm64-gnu: 1.32.0 - lightningcss-linux-arm64-musl: 1.32.0 - lightningcss-linux-x64-gnu: 1.32.0 - lightningcss-linux-x64-musl: 1.32.0 - lightningcss-win32-arm64-msvc: 1.32.0 - lightningcss-win32-x64-msvc: 1.32.0 - - lines-and-columns@1.2.4: {} - - local-pkg@1.1.2: - dependencies: - mlly: 1.8.2 - pkg-types: 2.3.1 - quansync: 0.2.11 - - lodash-es@4.18.1: {} - - long@5.3.2: {} - - longest-streak@3.1.0: {} - - lower-case@2.0.2: - dependencies: - tslib: 2.8.1 - - lowlight@3.3.0: - dependencies: - '@types/hast': 3.0.4 - devlop: 1.1.0 - highlight.js: 11.11.1 - - lru-cache@5.1.1: - dependencies: - yallist: 3.1.1 - - magic-string@0.30.21: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - - markdown-table@3.0.4: {} - - marked@14.0.0: {} - - marked@16.4.2: {} - - marked@17.0.6: {} - - mdast-util-find-and-replace@3.0.2: - dependencies: - '@types/mdast': 4.0.4 - escape-string-regexp: 5.0.0 - unist-util-is: 6.0.1 - unist-util-visit-parents: 6.0.2 - - mdast-util-from-markdown@2.0.3: - dependencies: - '@types/mdast': 4.0.4 - '@types/unist': 3.0.3 - decode-named-character-reference: 1.3.0 - devlop: 1.1.0 - mdast-util-to-string: 4.0.0 - micromark: 4.0.2 - micromark-util-decode-numeric-character-reference: 2.0.2 - micromark-util-decode-string: 2.0.1 - micromark-util-normalize-identifier: 2.0.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - unist-util-stringify-position: 4.0.0 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm-autolink-literal@2.0.1: - dependencies: - '@types/mdast': 4.0.4 - ccount: 2.0.1 - devlop: 1.1.0 - mdast-util-find-and-replace: 3.0.2 - micromark-util-character: 2.1.1 - - mdast-util-gfm-footnote@2.1.0: - dependencies: - '@types/mdast': 4.0.4 - devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 - mdast-util-to-markdown: 2.1.2 - micromark-util-normalize-identifier: 2.0.1 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm-strikethrough@2.0.0: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.3 - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm-table@2.0.0: - dependencies: - '@types/mdast': 4.0.4 - devlop: 1.1.0 - markdown-table: 3.0.4 - mdast-util-from-markdown: 2.0.3 - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm-task-list-item@2.0.0: - dependencies: - '@types/mdast': 4.0.4 - devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm@3.1.0: - dependencies: - mdast-util-from-markdown: 2.0.3 - mdast-util-gfm-autolink-literal: 2.0.1 - mdast-util-gfm-footnote: 2.1.0 - mdast-util-gfm-strikethrough: 2.0.0 - mdast-util-gfm-table: 2.0.0 - mdast-util-gfm-task-list-item: 2.0.0 - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-math@3.0.0: - dependencies: - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - devlop: 1.1.0 - longest-streak: 3.1.0 - mdast-util-from-markdown: 2.0.3 - mdast-util-to-markdown: 2.1.2 - unist-util-remove-position: 5.0.0 - transitivePeerDependencies: - - supports-color - - mdast-util-mdx-expression@2.0.1: - dependencies: - '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-mdx-jsx@3.2.0: - dependencies: - '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - '@types/unist': 3.0.3 - ccount: 2.0.1 - devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 - mdast-util-to-markdown: 2.1.2 - parse-entities: 4.0.2 - stringify-entities: 4.0.4 - unist-util-stringify-position: 4.0.0 - vfile-message: 4.0.3 - transitivePeerDependencies: - - supports-color - - mdast-util-mdxjs-esm@2.0.1: - dependencies: - '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-newline-to-break@2.0.0: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-find-and-replace: 3.0.2 - - mdast-util-phrasing@4.1.0: - dependencies: - '@types/mdast': 4.0.4 - unist-util-is: 6.0.1 - - mdast-util-to-hast@13.2.1: - dependencies: - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - '@ungap/structured-clone': 1.3.0 - devlop: 1.1.0 - micromark-util-sanitize-uri: 2.0.1 - trim-lines: 3.0.1 - unist-util-position: 5.0.0 - unist-util-visit: 5.1.0 - vfile: 6.0.3 - - mdast-util-to-markdown@2.1.2: - dependencies: - '@types/mdast': 4.0.4 - '@types/unist': 3.0.3 - longest-streak: 3.1.0 - mdast-util-phrasing: 4.1.0 - mdast-util-to-string: 4.0.0 - micromark-util-classify-character: 2.0.1 - micromark-util-decode-string: 2.0.1 - unist-util-visit: 5.1.0 - zwitch: 2.0.4 - - mdast-util-to-string@4.0.0: - dependencies: - '@types/mdast': 4.0.4 - - mermaid@11.14.0: - dependencies: - '@braintree/sanitize-url': 7.1.2 - '@iconify/utils': 3.1.0 - '@mermaid-js/parser': 1.1.0 - '@types/d3': 7.4.3 - '@upsetjs/venn.js': 2.0.0 - cytoscape: 3.33.2 - cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.2) - cytoscape-fcose: 2.2.0(cytoscape@3.33.2) - d3: 7.9.0 - d3-sankey: 0.12.3 - dagre-d3-es: 7.0.14 - dayjs: 1.11.20 - dompurify: 3.3.3 - katex: 0.16.45 - khroma: 2.1.0 - lodash-es: 4.18.1 - marked: 16.4.2 - roughjs: 4.6.6 - stylis: 4.3.6 - ts-dedent: 2.2.0 - uuid: 11.1.0 - - micromark-core-commonmark@2.0.3: - dependencies: - decode-named-character-reference: 1.3.0 - devlop: 1.1.0 - micromark-factory-destination: 2.0.1 - micromark-factory-label: 2.0.1 - micromark-factory-space: 2.0.1 - micromark-factory-title: 2.0.1 - micromark-factory-whitespace: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-chunked: 2.0.1 - micromark-util-classify-character: 2.0.1 - micromark-util-html-tag-name: 2.0.1 - micromark-util-normalize-identifier: 2.0.1 - micromark-util-resolve-all: 2.0.1 - micromark-util-subtokenize: 2.1.0 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-cjk-friendly-gfm-strikethrough@2.0.1(micromark-util-types@2.0.2)(micromark@4.0.2): - dependencies: - devlop: 1.1.0 - get-east-asian-width: 1.5.0 - micromark: 4.0.2 - micromark-extension-cjk-friendly-util: 3.0.1(micromark-util-types@2.0.2) - micromark-util-character: 2.1.1 - micromark-util-chunked: 2.0.1 - micromark-util-resolve-all: 2.0.1 - micromark-util-symbol: 2.0.1 - optionalDependencies: - micromark-util-types: 2.0.2 - - micromark-extension-cjk-friendly-util@3.0.1(micromark-util-types@2.0.2): - dependencies: - get-east-asian-width: 1.5.0 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - optionalDependencies: - micromark-util-types: 2.0.2 - - micromark-extension-cjk-friendly@2.0.1(micromark-util-types@2.0.2)(micromark@4.0.2): - dependencies: - devlop: 1.1.0 - micromark: 4.0.2 - micromark-extension-cjk-friendly-util: 3.0.1(micromark-util-types@2.0.2) - micromark-util-chunked: 2.0.1 - micromark-util-resolve-all: 2.0.1 - micromark-util-symbol: 2.0.1 - optionalDependencies: - micromark-util-types: 2.0.2 - - micromark-extension-gfm-autolink-literal@2.1.0: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-sanitize-uri: 2.0.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-gfm-footnote@2.1.0: - dependencies: - devlop: 1.1.0 - micromark-core-commonmark: 2.0.3 - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-normalize-identifier: 2.0.1 - micromark-util-sanitize-uri: 2.0.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-gfm-strikethrough@2.1.0: - dependencies: - devlop: 1.1.0 - micromark-util-chunked: 2.0.1 - micromark-util-classify-character: 2.0.1 - micromark-util-resolve-all: 2.0.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-gfm-table@2.1.1: - dependencies: - devlop: 1.1.0 - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-gfm-tagfilter@2.0.0: - dependencies: - micromark-util-types: 2.0.2 - - micromark-extension-gfm-task-list-item@2.1.0: - dependencies: - devlop: 1.1.0 - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-gfm@3.0.0: - dependencies: - micromark-extension-gfm-autolink-literal: 2.1.0 - micromark-extension-gfm-footnote: 2.1.0 - micromark-extension-gfm-strikethrough: 2.1.0 - micromark-extension-gfm-table: 2.1.1 - micromark-extension-gfm-tagfilter: 2.0.0 - micromark-extension-gfm-task-list-item: 2.1.0 - micromark-util-combine-extensions: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-math@3.1.0: - dependencies: - '@types/katex': 0.16.8 - devlop: 1.1.0 - katex: 0.16.45 - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-factory-destination@2.0.1: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-factory-label@2.0.1: - dependencies: - devlop: 1.1.0 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-factory-space@2.0.1: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-types: 2.0.2 - - micromark-factory-title@2.0.1: - dependencies: - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-factory-whitespace@2.0.1: - dependencies: - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-character@2.1.1: - dependencies: - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-chunked@2.0.1: - dependencies: - micromark-util-symbol: 2.0.1 - - micromark-util-classify-character@2.0.1: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-combine-extensions@2.0.1: - dependencies: - micromark-util-chunked: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-decode-numeric-character-reference@2.0.2: - dependencies: - micromark-util-symbol: 2.0.1 - - micromark-util-decode-string@2.0.1: - dependencies: - decode-named-character-reference: 1.3.0 - micromark-util-character: 2.1.1 - micromark-util-decode-numeric-character-reference: 2.0.2 - micromark-util-symbol: 2.0.1 - - micromark-util-encode@2.0.1: {} - - micromark-util-html-tag-name@2.0.1: {} - - micromark-util-normalize-identifier@2.0.1: - dependencies: - micromark-util-symbol: 2.0.1 - - micromark-util-resolve-all@2.0.1: - dependencies: - micromark-util-types: 2.0.2 - - micromark-util-sanitize-uri@2.0.1: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-encode: 2.0.1 - micromark-util-symbol: 2.0.1 - - micromark-util-subtokenize@2.1.0: - dependencies: - devlop: 1.1.0 - micromark-util-chunked: 2.0.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-symbol@2.0.1: {} - - micromark-util-types@2.0.2: {} - - micromark@4.0.2: - dependencies: - '@types/debug': 4.1.13 - debug: 4.4.3 - decode-named-character-reference: 1.3.0 - devlop: 1.1.0 - micromark-core-commonmark: 2.0.3 - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-chunked: 2.0.1 - micromark-util-combine-extensions: 2.0.1 - micromark-util-decode-numeric-character-reference: 2.0.2 - micromark-util-encode: 2.0.1 - micromark-util-normalize-identifier: 2.0.1 - micromark-util-resolve-all: 2.0.1 - micromark-util-sanitize-uri: 2.0.1 - micromark-util-subtokenize: 2.1.0 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - transitivePeerDependencies: - - supports-color - - mlly@1.8.2: - dependencies: - acorn: 8.16.0 - pathe: 2.0.3 - pkg-types: 1.3.1 - ufo: 1.6.3 - - monaco-editor@0.55.1: - dependencies: - dompurify: 3.2.7 - marked: 14.0.0 - - ms@2.1.3: {} - - nanoid@3.3.11: {} - - no-case@3.0.4: - dependencies: - lower-case: 2.0.2 - tslib: 2.8.1 - - node-domexception@1.0.0: {} - - node-fetch@3.3.2: - dependencies: - data-uri-to-buffer: 4.0.1 - fetch-blob: 3.2.0 - formdata-polyfill: 4.0.10 - - node-releases@2.0.44: {} - - obug@2.1.1: {} - - oniguruma-parser@0.12.1: {} - - oniguruma-to-es@4.3.5: - dependencies: - oniguruma-parser: 0.12.1 - regex: 6.1.0 - regex-recursion: 6.0.2 - - openai@6.26.0(ws@8.21.0)(zod@4.4.3): - optionalDependencies: - ws: 8.21.0 - zod: 4.4.3 - - p-retry@4.6.2: - dependencies: - '@types/retry': 0.12.0 - retry: 0.13.1 - - package-manager-detector@1.6.0: {} - - pako@1.0.11: {} - - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - - parse-entities@4.0.2: - dependencies: - '@types/unist': 2.0.11 - character-entities-legacy: 3.0.0 - character-reference-invalid: 2.0.1 - decode-named-character-reference: 1.3.0 - is-alphanumerical: 2.0.1 - is-decimal: 2.0.1 - is-hexadecimal: 2.0.1 - - parse-json@5.2.0: - dependencies: - '@babel/code-frame': 7.29.0 - error-ex: 1.3.4 - json-parse-even-better-errors: 2.3.1 - lines-and-columns: 1.2.4 - - parse5@7.3.0: - dependencies: - entities: 6.0.1 - - partial-json@0.1.7: {} - - path-data-parser@0.1.0: {} - - path-type@4.0.0: {} - - pathe@2.0.3: {} - - picocolors@1.1.1: {} - - picomatch@4.0.4: {} - - pkg-types@1.3.1: - dependencies: - confbox: 0.1.8 - mlly: 1.8.2 - pathe: 2.0.3 - - pkg-types@2.3.1: - dependencies: - confbox: 0.2.4 - exsolve: 1.0.8 - pathe: 2.0.3 - - points-on-curve@0.2.0: {} - - points-on-path@0.2.1: - dependencies: - path-data-parser: 0.1.0 - points-on-curve: 0.2.0 - - postcss-selector-parser@6.0.10: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - - postcss@8.5.9: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - process-nextick-args@2.0.1: {} - - property-information@7.1.0: {} - - protobufjs@7.6.5: - dependencies: - '@protobufjs/aspromise': 1.1.2 - '@protobufjs/base64': 1.1.2 - '@protobufjs/codegen': 2.0.5 - '@protobufjs/eventemitter': 1.1.1 - '@protobufjs/fetch': 1.1.1 - '@protobufjs/float': 1.0.2 - '@protobufjs/path': 1.1.2 - '@protobufjs/pool': 1.1.0 - '@protobufjs/utf8': 1.1.2 - '@types/node': 26.1.1 - long: 5.3.2 - - quansync@0.2.11: {} - - react-complex-tree@2.6.2(react@19.2.5): - dependencies: - react: 19.2.5 - - react-dom@19.2.5(react@19.2.5): - dependencies: - react: 19.2.5 - scheduler: 0.27.0 - - react@19.2.5: {} - - reactivity-store@0.4.0(react@19.2.5): - dependencies: - '@vue/reactivity': 3.5.35 - '@vue/shared': 3.5.35 - react: 19.2.5 - use-sync-external-store: 1.6.0(react@19.2.5) - - readable-stream@2.3.8: - dependencies: - core-util-is: 1.0.3 - inherits: 2.0.4 - isarray: 1.0.0 - process-nextick-args: 2.0.1 - safe-buffer: 5.1.2 - string_decoder: 1.1.1 - util-deprecate: 1.0.2 - - regex-recursion@6.0.2: - dependencies: - regex-utilities: 2.3.0 - - regex-utilities@2.3.0: {} - - regex@6.1.0: - dependencies: - regex-utilities: 2.3.0 - - rehype-harden@1.1.8: - dependencies: - unist-util-visit: 5.1.0 - - rehype-katex@7.0.1: - dependencies: - '@types/hast': 3.0.4 - '@types/katex': 0.16.8 - hast-util-from-html-isomorphic: 2.0.0 - hast-util-to-text: 4.0.2 - katex: 0.16.45 - unist-util-visit-parents: 6.0.2 - vfile: 6.0.3 - - rehype-raw@7.0.0: - dependencies: - '@types/hast': 3.0.4 - hast-util-raw: 9.1.0 - vfile: 6.0.3 - - rehype-sanitize@6.0.0: - dependencies: - '@types/hast': 3.0.4 - hast-util-sanitize: 5.0.2 - - remark-breaks@4.0.0: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-newline-to-break: 2.0.0 - unified: 11.0.5 - - remark-cjk-friendly-gfm-strikethrough@2.0.1(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(unified@11.0.5): - dependencies: - micromark-extension-cjk-friendly-gfm-strikethrough: 2.0.1(micromark-util-types@2.0.2)(micromark@4.0.2) - unified: 11.0.5 - optionalDependencies: - '@types/mdast': 4.0.4 - transitivePeerDependencies: - - micromark - - micromark-util-types - - remark-cjk-friendly@2.0.1(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(unified@11.0.5): - dependencies: - micromark-extension-cjk-friendly: 2.0.1(micromark-util-types@2.0.2)(micromark@4.0.2) - unified: 11.0.5 - optionalDependencies: - '@types/mdast': 4.0.4 - transitivePeerDependencies: - - micromark - - micromark-util-types - - remark-gfm@4.0.1: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-gfm: 3.1.0 - micromark-extension-gfm: 3.0.0 - remark-parse: 11.0.0 - remark-stringify: 11.0.0 - unified: 11.0.5 - transitivePeerDependencies: - - supports-color - - remark-math@6.0.0: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-math: 3.0.0 - micromark-extension-math: 3.1.0 - unified: 11.0.5 - transitivePeerDependencies: - - supports-color - - remark-parse@11.0.0: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.3 - micromark-util-types: 2.0.2 - unified: 11.0.5 - transitivePeerDependencies: - - supports-color - - remark-rehype@11.1.2: - dependencies: - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - mdast-util-to-hast: 13.2.1 - unified: 11.0.5 - vfile: 6.0.3 - - remark-stringify@11.0.0: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-to-markdown: 2.1.2 - unified: 11.0.5 - - remend@1.3.0: {} - - reselect@5.2.0: {} - - resolve-from@4.0.0: {} - - retry@0.13.1: {} - - robust-predicates@3.0.3: {} - - rolldown@1.0.0-rc.15: - dependencies: - '@oxc-project/types': 0.124.0 - '@rolldown/pluginutils': 1.0.0-rc.15 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.15 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.15 - '@rolldown/binding-darwin-x64': 1.0.0-rc.15 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.15 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.15 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.15 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.15 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.15 - '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.15 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.15 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.15 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.15 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.15 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.15 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.15 - - roughjs@4.6.6: - dependencies: - hachure-fill: 0.5.2 - path-data-parser: 0.1.0 - points-on-curve: 0.2.0 - points-on-path: 0.2.1 - - rw@1.3.3: {} - - safe-buffer@5.1.2: {} - - safer-buffer@2.1.2: {} - - scheduler@0.27.0: {} - - semver@6.3.1: {} - - setimmediate@1.0.5: {} - - shiki@3.23.0: - dependencies: - '@shikijs/core': 3.23.0 - '@shikijs/engine-javascript': 3.23.0 - '@shikijs/engine-oniguruma': 3.23.0 - '@shikijs/langs': 3.23.0 - '@shikijs/themes': 3.23.0 - '@shikijs/types': 3.23.0 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - - snake-case@3.0.4: - dependencies: - dot-case: 3.0.4 - tslib: 2.8.1 - - source-map-js@1.2.1: {} - - space-separated-tokens@2.0.2: {} - - ssf@0.11.2: - dependencies: - frac: 1.1.2 - - streamdown@2.5.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5): - dependencies: - clsx: 2.1.1 - hast-util-to-jsx-runtime: 2.3.6 - html-url-attributes: 3.0.1 - marked: 17.0.6 - mermaid: 11.14.0 - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) - rehype-harden: 1.1.8 - rehype-raw: 7.0.0 - rehype-sanitize: 6.0.0 - remark-gfm: 4.0.1 - remark-parse: 11.0.0 - remark-rehype: 11.1.2 - remend: 1.3.0 - tailwind-merge: 3.5.0 - unified: 11.0.5 - unist-util-visit: 5.1.0 - unist-util-visit-parents: 6.0.2 - transitivePeerDependencies: - - supports-color - - string_decoder@1.1.1: - dependencies: - safe-buffer: 5.1.2 - - stringify-entities@4.0.4: - dependencies: - character-entities-html4: 2.1.0 - character-entities-legacy: 3.0.0 - - style-to-js@1.1.21: - dependencies: - style-to-object: 1.0.14 - - style-to-object@1.0.14: - dependencies: - inline-style-parser: 0.2.7 - - stylis@4.3.6: {} - - svg-parser@2.0.4: {} - - tailwind-merge@3.5.0: {} - - tailwindcss@4.2.2: {} - - tapable@2.3.2: {} - - tinyexec@1.1.1: {} - - tinyglobby@0.2.16: - dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - - trim-lines@3.0.1: {} - - trough@2.2.0: {} - - ts-algebra@2.0.0: {} - - ts-dedent@2.2.0: {} - - tslib@2.8.1: {} - - typebox@1.1.38: {} - - typescript@5.4.5: {} - - typescript@6.0.2: {} - - ufo@1.6.3: {} - - undici-types@8.3.0: {} - - unified@11.0.5: - dependencies: - '@types/unist': 3.0.3 - bail: 2.0.2 - devlop: 1.1.0 - extend: 3.0.2 - is-plain-obj: 4.1.0 - trough: 2.2.0 - vfile: 6.0.3 - - unist-util-find-after@5.0.0: - dependencies: - '@types/unist': 3.0.3 - unist-util-is: 6.0.1 - - unist-util-is@6.0.1: - dependencies: - '@types/unist': 3.0.3 - - unist-util-position@5.0.0: - dependencies: - '@types/unist': 3.0.3 - - unist-util-remove-position@5.0.0: - dependencies: - '@types/unist': 3.0.3 - unist-util-visit: 5.1.0 - - unist-util-stringify-position@4.0.0: - dependencies: - '@types/unist': 3.0.3 - - unist-util-visit-parents@6.0.2: - dependencies: - '@types/unist': 3.0.3 - unist-util-is: 6.0.1 - - unist-util-visit@5.1.0: - dependencies: - '@types/unist': 3.0.3 - unist-util-is: 6.0.1 - unist-util-visit-parents: 6.0.2 - - unplugin-icons@23.0.1(@svgr/core@8.1.0(typescript@6.0.2)): - dependencies: - '@antfu/install-pkg': 1.1.0 - '@iconify/utils': 3.1.0 - local-pkg: 1.1.2 - obug: 2.1.1 - unplugin: 2.3.11 - optionalDependencies: - '@svgr/core': 8.1.0(typescript@6.0.2) - - unplugin@2.3.11: - dependencies: - '@jridgewell/remapping': 2.3.5 - acorn: 8.16.0 - picomatch: 4.0.4 - webpack-virtual-modules: 0.6.2 - - update-browserslist-db@1.2.3(browserslist@4.28.2): - dependencies: - browserslist: 4.28.2 - escalade: 3.2.0 - picocolors: 1.1.1 - - use-sync-external-store@1.6.0(react@19.2.5): - dependencies: - react: 19.2.5 - - util-deprecate@1.0.2: {} - - uuid@11.1.0: {} - - vfile-location@5.0.3: - dependencies: - '@types/unist': 3.0.3 - vfile: 6.0.3 - - vfile-message@4.0.3: - dependencies: - '@types/unist': 3.0.3 - unist-util-stringify-position: 4.0.0 - - vfile@6.0.3: - dependencies: - '@types/unist': 3.0.3 - vfile-message: 4.0.3 - - vite@8.0.8(@types/node@26.1.1)(jiti@2.6.1): - dependencies: - lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.9 - rolldown: 1.0.0-rc.15 - tinyglobby: 0.2.16 - optionalDependencies: - '@types/node': 26.1.1 - fsevents: 2.3.3 - jiti: 2.6.1 - - vscode-jsonrpc@8.2.0: {} - - vscode-languageserver-protocol@3.17.5: - dependencies: - vscode-jsonrpc: 8.2.0 - vscode-languageserver-types: 3.17.5 - - vscode-languageserver-textdocument@1.0.12: {} - - vscode-languageserver-types@3.17.5: {} - - vscode-languageserver@9.0.1: - dependencies: - vscode-languageserver-protocol: 3.17.5 - - vscode-uri@3.1.0: {} - - web-namespaces@2.0.1: {} - - web-streams-polyfill@3.3.3: {} - - webpack-virtual-modules@0.6.2: {} - - wmf@1.0.2: {} - - word@0.3.0: {} - - ws@8.21.0: {} - - xlsx@0.18.5: - dependencies: - adler-32: 1.3.1 - cfb: 1.2.2 - codepage: 1.15.0 - crc-32: 1.2.2 - ssf: 0.11.2 - wmf: 1.0.2 - word: 0.3.0 - - yallist@3.1.1: {} - - yet-another-react-lightbox@3.31.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): - dependencies: - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - zod-to-json-schema@3.25.2(zod@4.4.3): - dependencies: - zod: 4.4.3 - - zod@4.4.3: {} - - zwitch@2.0.4: {} diff --git a/crates/agent-gateway/web/src/App.tsx b/crates/agent-gateway/web/src/App.tsx deleted file mode 100644 index eca7bf60f..000000000 --- a/crates/agent-gateway/web/src/App.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import GatewayApp from "@/app/GatewayApp"; - -export default GatewayApp; diff --git a/crates/agent-gateway/web/src/app/AgentSelector.tsx b/crates/agent-gateway/web/src/app/AgentSelector.tsx deleted file mode 100644 index 8ea4efca2..000000000 --- a/crates/agent-gateway/web/src/app/AgentSelector.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import { useEffect, useState } from "react"; -import { Check } from "@/components/icons"; -import { DropdownMenuItem, DropdownMenuLabel } from "@/components/ui/dropdown-menu"; -import { useLocale } from "@/i18n"; -import type { GatewayWebSocketClientLike } from "@/lib/gatewaySocket"; -import type { AgentStatus } from "@/lib/gatewayTypes"; - -function truncateMiddle(value: string, maxLength = 30): string { - if (value.length <= maxLength) return value; - const headLength = Math.ceil((maxLength - 1) / 2); - const tailLength = Math.floor((maxLength - 1) / 2); - return `${value.slice(0, headLength)}…${value.slice(-tailLength)}`; -} - -function agentLabel(agent: AgentStatus): string { - return agent.name?.trim() || agent.agent_id?.trim() || "Agent"; -} - -function sortAgents(agents: AgentStatus[], activeAgent: string): AgentStatus[] { - return [...agents].sort((left, right) => { - const leftID = left.agent_id?.trim() || ""; - const rightID = right.agent_id?.trim() || ""; - if ((leftID === activeAgent) !== (rightID === activeAgent)) { - return leftID === activeAgent ? -1 : 1; - } - if (left.online !== right.online) { - return left.online ? -1 : 1; - } - return agentLabel(left).localeCompare(agentLabel(right)); - }); -} - -// AgentSelector 渲染头像菜单内的 Agent 目录。目录由打标 status 事件与 -// agent_list 响应驱动;单客户端显示身份与状态,多客户端才显示切换列表。 -export function AgentSelector({ - api, - onAgentChange, -}: { - api: GatewayWebSocketClientLike; - onAgentChange?: (agentId: string) => void; -}) { - const { t } = useLocale(); - const [agents, setAgents] = useState([]); - const [activeAgent, setActiveAgent] = useState(() => api.getActiveAgent()); - - useEffect(() => { - const unsubscribe = api.subscribeAgents(setAgents); - // 主动拉一次目录:离线/仅签发凭证的 Agent 不会有 status 事件。 - api - .listAgents() - .then(() => { - const agentId = api.getActiveAgent(); - setActiveAgent(agentId); - onAgentChange?.(agentId); - }) - .catch(() => { - // 目录拉取失败不阻塞页面;status 事件仍会渐进填充在线条目。 - }); - return unsubscribe; - }, [api, onAgentChange]); - - if (agents.length === 0) { - return null; - } - - const handleChange = (agentId: string) => { - setActiveAgent(agentId); - api.setActiveAgent(agentId); - onAgentChange?.(agentId); - }; - - const sortedAgents = sortAgents(agents, activeAgent); - - if (sortedAgents.length === 1) { - const [agent] = sortedAgents; - const agentID = agent.agent_id?.trim() || ""; - const name = agent.name?.trim() || ""; - const statusLabel = agent.online - ? t("settings.devicesOnlineStatus") - : t("settings.devicesOfflineStatus"); - return ( -
- - {t("settings.devicesTitle")} - -
-
- - {statusLabel} - - - - {name || truncateMiddle(agentID)} - - - {truncateMiddle(agentID)} - - - - {statusLabel} - -
-
-
- ); - } - - return ( - <> - - {t("settings.devicesTitle")} - -
- {sortedAgents.map((agent) => { - const agentID = agent.agent_id?.trim() || ""; - const name = agent.name?.trim() || ""; - const selected = agentID === activeAgent; - const statusLabel = agent.online - ? t("settings.devicesOnlineStatus") - : t("settings.devicesOfflineStatus"); - return ( - handleChange(agentID)} - > - - {statusLabel} - - - - {name || truncateMiddle(agentID)} - - {name ? ( - - {truncateMiddle(agentID)} - - ) : null} - - {selected ? : null} - - ); - })} -
- - ); -} diff --git a/crates/agent-gateway/web/src/app/FileDropOverlay.tsx b/crates/agent-gateway/web/src/app/FileDropOverlay.tsx deleted file mode 100644 index 169112666..000000000 --- a/crates/agent-gateway/web/src/app/FileDropOverlay.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { Ban, Upload } from "@/components/icons"; - -type FileDropOverlayProps = { - canDropUpload: boolean; - title: string; - description: string; - limitHint: string; -}; - -export function FileDropOverlay({ - canDropUpload, - title, - description, - limitHint, -}: FileDropOverlayProps) { - return ( - - } - > - - - ) : null} - {workspaceSshTerminalMounted && terminalClient && sftpClient ? ( - - {translate("workspaceSshTerminal.loading", locale)} - - } - > - - - ) : null} - - ); -} diff --git a/crates/agent-gateway/web/src/app/chatDraft.ts b/crates/agent-gateway/web/src/app/chatDraft.ts deleted file mode 100644 index a125d7b4a..000000000 --- a/crates/agent-gateway/web/src/app/chatDraft.ts +++ /dev/null @@ -1,129 +0,0 @@ -import type { - MentionComposerCommitMention, - MentionComposerDraft, - MentionComposerGitFileMention, - MentionComposerLargePaste, -} from "@/components/chat/MentionComposer"; -import { formatCodeMentionToken, formatFileMentionToken } from "@/lib/chat/mentionReferences"; -import type { PendingUploadedFile } from "@/lib/chat/uploadedFiles"; -import { withPastedTextDisplayMetadata } from "@/lib/chat/uploadedFiles"; -import { importReadableFiles } from "@/lib/uploadReadableFiles"; - -function buildPastedTextFileName(paste: MentionComposerLargePaste, index: number) { - const baseName = paste.label - .trim() - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-+|-+$/g, ""); - return `${baseName || `pasted-text-${index + 1}`}.txt`; -} - -function escapeComposerCommitLinkLabel(value: string) { - return value.replace(/\\/g, "\\\\").replace(/]/g, "\\]"); -} - -function formatComposerCommitLinkDestination(value: string) { - const normalized = value.replace(/\\/g, "/"); - if (/[\s()<>]/.test(normalized)) { - return `<${normalized.replace(//g, "%3E")}>`; - } - return normalized; -} - -function formatComposerCommitMention(commit: MentionComposerCommitMention) { - const shortSha = commit.shortSha || commit.sha.slice(0, 7); - const subject = commit.subject.trim() || shortSha; - const label = `commit ${shortSha}: ${subject}`; - if (commit.githubUrl?.trim()) { - return `[${escapeComposerCommitLinkLabel(label)}](${formatComposerCommitLinkDestination(commit.githubUrl.trim())})`; - } - return `${label} (${commit.sha})`; -} - -function formatComposerGitFileMention(file: MentionComposerGitFileMention) { - const refLabel = file.refName || file.shortSha || file.commitSha.slice(0, 7); - const label = `git file ${refLabel}: ${file.path}`; - if (file.githubUrl?.trim()) { - return `[${escapeComposerCommitLinkLabel(label)}](${formatComposerCommitLinkDestination(file.githubUrl.trim())})`; - } - return `${label} (${file.commitSha})`; -} - -export function buildTextFromComposerDraft( - draft: MentionComposerDraft, - pastedFileById?: Map, -) { - return draft.segments - .map((segment) => { - if (segment.type === "text") { - return segment.text; - } - if (segment.type === "fileMention") { - return formatFileMentionToken(segment.reference); - } - if (segment.type === "skillMention") { - return `$${segment.skill.name}`; - } - if (segment.type === "commitMention") { - return formatComposerCommitMention(segment.commit); - } - if (segment.type === "gitFileMention") { - return formatComposerGitFileMention(segment.file); - } - if (segment.type === "codeMention") { - return formatCodeMentionToken(segment.reference); - } - const file = pastedFileById?.get(segment.paste.id); - return file ? `[${segment.paste.label}: ${file.relativePath}]` : segment.paste.text; - }) - .join("") - .replace(/\u00A0/g, " "); -} - -export async function importPastedTextsAsFiles(params: { - token: string; - agentId: string; - workdir: string; - pastes: MentionComposerLargePaste[]; -}) { - const { token, agentId, workdir, pastes } = params; - const normalizedWorkdir = workdir.trim(); - if (!normalizedWorkdir) { - throw new Error("项目目录未选择,无法发送大段粘贴内容。"); - } - if (pastes.length === 0) { - return { - files: [], - fileByPasteId: new Map(), - }; - } - - const textFiles = pastes.map( - (paste, index) => - new File([paste.text], buildPastedTextFileName(paste, index), { - type: "text/plain", - }), - ); - const response = await importReadableFiles(token, agentId, normalizedWorkdir, textFiles); - if (response.files.length !== pastes.length) { - const skipped = response.skipped.length > 0 ? `\n${response.skipped.join("\n")}` : ""; - throw new Error(`部分大段粘贴内容未能导入为附件。${skipped}`); - } - - const files = response.files.map((file, index) => { - const paste = pastes[index]; - return paste ? withPastedTextDisplayMetadata(file, paste) : file; - }); - - const fileByPasteId = new Map(); - files.forEach((file, index) => { - const paste = pastes[index]; - if (paste) { - fileByPasteId.set(paste.id, file); - } - }); - return { - files, - fileByPasteId, - }; -} diff --git a/crates/agent-gateway/web/src/app/chatEventUtils.ts b/crates/agent-gateway/web/src/app/chatEventUtils.ts deleted file mode 100644 index ce251f78c..000000000 --- a/crates/agent-gateway/web/src/app/chatEventUtils.ts +++ /dev/null @@ -1,117 +0,0 @@ -import type { ChatEvent, GatewaySelectedModel } from "@/lib/gatewayTypes"; -import { - type AppSettings, - normalizeSelectedModelForProviders, - parseSelectedModelJson, - type SelectedModel, -} from "@/lib/settings"; - -import type { ModelProviderSource, TunnelManagerToolChange } from "./types"; - -export function asErrorMessage(error: unknown, fallback: string) { - if (error instanceof Error && error.message.trim()) return error.message.trim(); - const text = String(error ?? "").trim(); - return text || fallback; -} - -export function isAbortError(error: unknown) { - if ( - (error instanceof DOMException && error.name === "AbortError") || - (error instanceof Error && error.name === "AbortError") - ) { - return true; - } - const message = error instanceof Error ? error.message : String(error ?? ""); - const normalized = message.trim().toLowerCase(); - return ( - normalized.includes("cancelled") || - normalized.includes("canceled") || - normalized.includes("已取消") || - normalized.includes("abort") || - normalized.includes("aborted") - ); -} - -export function readChatEventTitle(event: ChatEvent): string { - if ("title" in event && typeof event.title === "string") { - return event.title.trim(); - } - return ""; -} - -export function isChatEventTitleFinal(event: ChatEvent) { - return event.type === "done" || ("titleFinal" in event && event.titleFinal === true); -} - -function asRecord(value: unknown): Record { - return value && typeof value === "object" && !Array.isArray(value) - ? (value as Record) - : {}; -} - -export function readTunnelManagerToolChange(event: ChatEvent): TunnelManagerToolChange | null { - if (event.type !== "tool_result" || event.isError === true) { - return null; - } - const details = asRecord(event.details); - if (details.kind !== "tunnel_manager") { - return null; - } - const action = typeof details.action === "string" ? details.action.trim() : ""; - if (action !== "create" && action !== "close") { - return null; - } - const tunnel = asRecord(details.tunnel); - const projectPathKey = - (typeof tunnel.projectPathKey === "string" ? tunnel.projectPathKey.trim() : "") || - (typeof tunnel.project_path_key === "string" ? tunnel.project_path_key.trim() : "") || - event.workdir?.trim() || - ""; - return { action, projectPathKey }; -} - -// 会话生效模型的唯一派生点:本地未持久化的切换(override)> -// history-sync 带回的会话持久化选择 > 全局默认(新会话语义)。 -// 前两级都按当前 providers 校验,失效则逐级回退。 -export function resolveActiveModelSelection(params: { - settings: AppSettings; - override?: SelectedModel; - persistedSelectedModelJson?: string; -}): SelectedModel | undefined { - const { settings, override, persistedSelectedModelJson } = params; - return ( - normalizeSelectedModelForProviders(override, settings.customProviders) ?? - normalizeSelectedModelForProviders( - parseSelectedModelJson(persistedSelectedModelJson), - settings.customProviders, - ) ?? - settings.selectedModel - ); -} - -export function buildGatewaySelectedModel( - selectedModel: SelectedModel | undefined, - providers: ModelProviderSource[], -): GatewaySelectedModel | undefined { - if (!selectedModel) { - return undefined; - } - - const provider = providers.find((item) => item.id === selectedModel.customProviderId); - if (!provider) { - return undefined; - } - - return { - customProviderId: provider.id, - model: selectedModel.model, - providerType: provider.type, - }; -} - -export function buildGatewaySystemSettings(settings: AppSettings, workdirOverride?: string) { - return { - executionMode: settings.system.executionMode, - workdir: workdirOverride ?? settings.system.workdir.trim(), - }; -} diff --git a/crates/agent-gateway/web/src/app/constants.ts b/crates/agent-gateway/web/src/app/constants.ts deleted file mode 100644 index 3d4dc898f..000000000 --- a/crates/agent-gateway/web/src/app/constants.ts +++ /dev/null @@ -1,22 +0,0 @@ -export const MAX_UPLOAD_FILES = 9; - -export const PROTECTED_DRAFT_CONVERSATION = "__protected_draft__"; -export const HISTORY_LIST_PAGE_SIZE = 80; -export const HISTORY_DETAIL_INITIAL_MAX_MESSAGES = 360; -// One "load earlier history" click grows the loaded window by this many -// persisted messages (same size as the initial tail window). -export const HISTORY_DETAIL_LOAD_EARLIER_PAGE_MESSAGES = 360; -export const PROJECT_HISTORY_DELETE_PAGE_SIZE = 200; -export const SHARED_HISTORY_LIST_PAGE_SIZE = 200; -export const CHAT_RUNTIME_PREPARE_TIMEOUT_MS = 2_500; -export const CHAT_RUNTIME_FOREGROUND_PREPARE_TIMEOUT_MS = 1_500; -export const CHAT_RUNTIME_KEEP_WARM_INTERVAL_MS = 10_000; -export const CHAT_RUNTIME_PREPARING_STATUS = "Preparing request..."; - -export const DEFAULT_BROWSER_TITLE = "LiveAgent Gateway"; -export const NEW_CONVERSATION_BROWSER_TITLE = "LiveAgent"; -export const SHARED_HISTORY_BROWSER_TITLE = "分享会话"; -export const SKILLS_HUB_BROWSER_TITLE = "Skills Hub"; -export const MCP_HUB_BROWSER_TITLE = "MCP Hub"; - -export const MOBILE_SIDEBAR_MEDIA_QUERY = "(max-width: 820px)"; diff --git a/crates/agent-gateway/web/src/app/domUtils.ts b/crates/agent-gateway/web/src/app/domUtils.ts deleted file mode 100644 index 7c7f32056..000000000 --- a/crates/agent-gateway/web/src/app/domUtils.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { DragEvent } from "react"; - -export function dragEventHasFiles(event: DragEvent) { - return Array.from(event.dataTransfer.types).includes("Files"); -} diff --git a/crates/agent-gateway/web/src/app/historyUtils.ts b/crates/agent-gateway/web/src/app/historyUtils.ts deleted file mode 100644 index b1e88fb90..000000000 --- a/crates/agent-gateway/web/src/app/historyUtils.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { formatConversationTitle } from "@/lib/chatUi"; -import type { ConversationSummary } from "@/lib/gatewayTypes"; -import { - type AppSettings, - DEFAULT_WORKSPACE_PROJECT_ID, - resolveWorkspaceProjects, - type WorkspaceProject, -} from "@/lib/settings"; -import { buildGatewaySettingsSyncPayload } from "@/lib/settings/sync"; - -function isLocalDraftConversationId(id: string) { - return id.trim().startsWith("__local_draft__:"); -} - -import { fallbackWorkspaceProjectName } from "@/lib/workspaceProjects"; - -import { MOBILE_SIDEBAR_MEDIA_QUERY } from "./constants"; - -export function formatTranslation(template: string, values: Record) { - return Object.entries(values).reduce( - (text, [key, value]) => text.replaceAll(`{${key}}`, String(value)), - template, - ); -} - -export function getDefaultWorkspaceProjectPath(system: AppSettings["system"]) { - return ( - system.workspaceProjects.find((project) => project.id === DEFAULT_WORKSPACE_PROJECT_ID)?.path || - system.workdir - ); -} - -export function createWorkspaceProjectFromPath(path: string, kind: WorkspaceProject["kind"]) { - const now = Date.now(); - return { - id: `${kind}-${now}-${Math.random().toString(36).slice(2, 8)}`, - name: fallbackWorkspaceProjectName(path), - path, - kind, - createdAt: now, - updatedAt: now, - } satisfies WorkspaceProject; -} - -export function hasSettingsSyncChanged(prev: AppSettings, next: AppSettings) { - return ( - JSON.stringify(buildGatewaySettingsSyncPayload(prev)) !== - JSON.stringify(buildGatewaySettingsSyncPayload(next)) - ); -} - -export function resolveAppWorkspaceProjects(settings: AppSettings): AppSettings { - return { - ...settings, - system: resolveWorkspaceProjects( - settings.system, - getDefaultWorkspaceProjectPath(settings.system), - ), - }; -} - -export function resolveConversationTitle( - summary: ConversationSummary | null, - fallbackConversationId: string, -) { - return formatConversationTitle(summary, fallbackConversationId); -} - -export function hasLocalDraftConversation(params: { - conversationId: string; - selectedHistoryId: string; - requestedConversationId?: string; - chatMessageCount: number; - pendingUploadCount: number; - draftPinned: boolean; -}) { - const { - conversationId, - selectedHistoryId, - requestedConversationId = "", - chatMessageCount, - pendingUploadCount, - draftPinned, - } = params; - - const isDraftConversation = conversationId === "" || isLocalDraftConversationId(conversationId); - const isDraftSelected = selectedHistoryId === "" || selectedHistoryId === conversationId; - - return ( - isDraftConversation && - isDraftSelected && - requestedConversationId === "" && - (draftPinned || chatMessageCount > 0 || pendingUploadCount > 0) - ); -} - -export function resolveVisibleConversationId(selectedHistoryId: string, conversationId: string) { - const selectedId = selectedHistoryId.trim(); - if (selectedId) { - return selectedId; - } - return conversationId.trim(); -} - -export function isMobileSidebarLayout() { - if (typeof window === "undefined") { - return false; - } - return window.matchMedia(MOBILE_SIDEBAR_MEDIA_QUERY).matches; -} - -export function shouldOpenSidebarByDefault() { - return !isMobileSidebarLayout(); -} diff --git a/crates/agent-gateway/web/src/app/hooks/useGatewayClients.ts b/crates/agent-gateway/web/src/app/hooks/useGatewayClients.ts deleted file mode 100644 index 8f21de7e1..000000000 --- a/crates/agent-gateway/web/src/app/hooks/useGatewayClients.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useMemo } from "react"; - -import { getGatewayWebSocketClient } from "@/lib/gatewaySocket"; -import { createGatewayGitClient } from "@/lib/git/gatewayGitClient"; -import { createGatewaySftpClient } from "@/lib/sftp/gatewaySftpClient"; -import { createGatewayTerminalClient } from "@/lib/terminal/gatewayTerminalClient"; - -export function useGatewayClients(token: string) { - const api = useMemo(() => (token ? getGatewayWebSocketClient(token) : null), [token]); - const terminalClient = useMemo(() => (api ? createGatewayTerminalClient(api) : null), [api]); - const sftpClient = useMemo(() => (api ? createGatewaySftpClient(api) : null), [api]); - const gitClient = useMemo(() => (api ? createGatewayGitClient(api) : null), [api]); - - return { - api, - terminalClient, - sftpClient, - gitClient, - }; -} diff --git a/crates/agent-gateway/web/src/app/hooks/useGatewaySession.ts b/crates/agent-gateway/web/src/app/hooks/useGatewaySession.ts deleted file mode 100644 index c9712470a..000000000 --- a/crates/agent-gateway/web/src/app/hooks/useGatewaySession.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from "react"; - -import { normalizeGatewayAccessToken, verifyGatewayAccessToken } from "@/lib/gatewayAuth"; -import { resetGatewayWebSocketClient } from "@/lib/gatewaySocket"; -import { clearToken, loadToken, saveToken } from "@/lib/storage"; - -import { asErrorMessage } from "../chatEventUtils"; - -export function useGatewaySession(historyShareToken: string | null) { - const initialStoredTokenRef = useRef(historyShareToken ? "" : loadToken()); - const [token, setToken] = useState(""); - const [loginToken, setLoginToken] = useState(initialStoredTokenRef.current); - const [authSubmitting, setAuthSubmitting] = useState( - () => normalizeGatewayAccessToken(initialStoredTokenRef.current) !== "", - ); - const [authError, setAuthError] = useState(null); - - useEffect(() => { - const storedToken = normalizeGatewayAccessToken(initialStoredTokenRef.current); - if (!storedToken) { - return; - } - - let cancelled = false; - setAuthError(null); - resetGatewayWebSocketClient(); - - void verifyGatewayAccessToken(storedToken) - .then((verifiedToken) => { - if (cancelled) { - return; - } - initialStoredTokenRef.current = verifiedToken; - saveToken(verifiedToken); - setLoginToken(verifiedToken); - setToken(verifiedToken); - }) - .catch((error) => { - if (cancelled) { - return; - } - initialStoredTokenRef.current = ""; - clearToken(); - resetGatewayWebSocketClient(); - setToken(""); - setAuthError(asErrorMessage(error, "Access Token 验证失败。")); - setLoginToken(storedToken); - }) - .finally(() => { - if (!cancelled) { - setAuthSubmitting(false); - } - }); - - return () => { - cancelled = true; - }; - }, []); - - const login = useCallback(async () => { - const draftToken = loginToken; - const normalizedToken = normalizeGatewayAccessToken(draftToken); - if (!normalizedToken) { - setAuthError("请输入 Access Token。"); - return; - } - - setAuthSubmitting(true); - setAuthError(null); - resetGatewayWebSocketClient(); - - try { - const verifiedToken = await verifyGatewayAccessToken(draftToken); - initialStoredTokenRef.current = verifiedToken; - saveToken(verifiedToken); - setLoginToken(verifiedToken); - setToken(verifiedToken); - } catch (error) { - initialStoredTokenRef.current = ""; - clearToken(); - resetGatewayWebSocketClient(); - setToken(""); - setAuthError(asErrorMessage(error, "Access Token 验证失败。")); - } finally { - setAuthSubmitting(false); - } - }, [loginToken]); - - const clearSession = useCallback(() => { - clearToken(); - resetGatewayWebSocketClient(); - initialStoredTokenRef.current = ""; - setAuthSubmitting(false); - setAuthError(null); - setLoginToken(""); - setToken(""); - }, []); - - return { - token, - loginToken, - authSubmitting, - authError, - setToken, - setLoginToken, - setAuthError, - login, - clearSession, - }; -} diff --git a/crates/agent-gateway/web/src/app/hooks/useGatewaySettingsSync.ts b/crates/agent-gateway/web/src/app/hooks/useGatewaySettingsSync.ts deleted file mode 100644 index 2973a94f6..000000000 --- a/crates/agent-gateway/web/src/app/hooks/useGatewaySettingsSync.ts +++ /dev/null @@ -1,242 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from "react"; - -import { - type CronSnapshot, - feedCronSnapshot, - feedHooksSnapshot, - type HooksSnapshot, - initAutomation, -} from "@/lib/automation"; -import { applyFontFamilies } from "@/lib/fontFamily"; -import type { GatewayWebSocketClientLike } from "@/lib/gatewaySocket"; -import { setPreferredMonacoNlsLocale } from "@/lib/monacoNls"; -import { - type AppSettings, - normalizeSettings, - resolveEffectiveTheme, - subscribeToSystemThemePreference, -} from "@/lib/settings"; -import { - applyGatewaySettingsSyncPayload, - buildGatewaySettingsSyncUpdatePayload, - type GatewaySettingsSyncPayload, - redactSettingsForWebStorage, -} from "@/lib/settings/sync"; -import { loadToken } from "@/lib/storage"; -import { loadWebSettings, persistWebSettings, type WebSettingsSaveState } from "@/lib/webSettings"; - -import { asErrorMessage } from "../chatEventUtils"; -import { hasSettingsSyncChanged, resolveAppWorkspaceProjects } from "../historyUtils"; - -export function useGatewaySettingsSync(params: { - token: string; - api: GatewayWebSocketClientLike | null; - activeAgentId: string; -}) { - const { token, api, activeAgentId } = params; - const [settings, setSettingsState] = useState(() => loadWebSettings(loadToken())); - const [settingsSyncReady, setSettingsSyncReady] = useState(() => token.trim() === ""); - const [settingsSyncError, setSettingsSyncError] = useState(null); - const [settingsSaveState, setSettingsSaveState] = useState({ - status: "saved", - }); - const settingsSaveSequenceRef = useRef(0); - const settingsSaveChainRef = useRef>(Promise.resolve()); - // Mirrors `settings` so setSettings/applyGatewaySettings can read the latest value - // synchronously without passing a (side-effecting) function into setSettingsState — - // React 18 StrictMode double-invokes functional state updaters in development, - // which would otherwise run those side effects (and any non-idempotent work like - // crypto.randomUUID() inside caller updaters) twice per call. - const settingsRef = useRef(settings); - settingsRef.current = settings; - const [systemThemeVersion, setSystemThemeVersion] = useState(0); - - // Monaco reads NLS globals while the lazy editor module imports monaco-editor. - setPreferredMonacoNlsLocale(settings.locale); - - useEffect(() => { - if (settings.theme !== "system") return; - return subscribeToSystemThemePreference(() => { - setSystemThemeVersion((version) => version + 1); - }); - }, [settings.theme]); - - useEffect(() => { - const root = document.documentElement; - root.classList.toggle("dark", resolveEffectiveTheme(settings.theme) === "dark"); - }, [settings.theme, systemThemeVersion]); - - useEffect(() => { - applyFontFamilies({ - interfaceFontFamily: settings.customSettings.interfaceFontFamily, - chatFontFamily: settings.customSettings.chatFontFamily, - codeFontFamily: settings.customSettings.codeFontFamily, - }); - }, [ - settings.customSettings.interfaceFontFamily, - settings.customSettings.chatFontFamily, - settings.customSettings.codeFontFamily, - ]); - - useEffect(() => { - setSettingsState((prev) => - resolveAppWorkspaceProjects( - normalizeSettings({ - ...prev, - remote: { - ...prev.remote, - gatewayUrl: window.location.origin, - token: token.trim(), - enabled: token.trim() !== "" || prev.remote.enabled, - }, - }), - ), - ); - }, [token]); - - const queueSettingsSave = useCallback( - (prev: AppSettings, next: AppSettings, fallback: string, syncGateway: boolean) => { - const saveSequence = ++settingsSaveSequenceRef.current; - setSettingsSaveState({ status: "saving" }); - const redactedNext = redactSettingsForWebStorage(next); - const gatewayUpdate = - syncGateway && api - ? buildGatewaySettingsSyncUpdatePayload(prev, next, { - includeProviderApiKeyUpdates: true, - }) - : null; - - settingsSaveChainRef.current = settingsSaveChainRef.current - .catch(() => undefined) - .then(() => { - persistWebSettings(redactedNext); - }) - .then(async () => { - if (gatewayUpdate && Object.keys(gatewayUpdate).length > 0) { - await api?.updateSettings(gatewayUpdate); - } - }) - .then(() => { - if (settingsSaveSequenceRef.current === saveSequence) { - setSettingsSaveState({ status: "saved" }); - } - }) - .catch((error) => { - if (syncGateway && api) { - void api - .getSettings() - .then((payload) => { - const current = settingsRef.current; - const refreshed = redactSettingsForWebStorage( - resolveAppWorkspaceProjects(applyGatewaySettingsSyncPayload(current, payload)), - ); - settingsRef.current = refreshed; - persistWebSettings(refreshed); - setSettingsState(refreshed); - }) - .catch(() => undefined); - } - if (settingsSaveSequenceRef.current === saveSequence) { - setSettingsSaveState({ - status: "error", - message: asErrorMessage(error, fallback), - }); - } - }); - }, - [api], - ); - - const applyGatewaySettings = useCallback( - (payload: GatewaySettingsSyncPayload) => { - // Automation snapshots ride along on the settings-sync channel but are - // desktop-owned state with their own revision — feed them straight into - // the automation store instead of the settings state. - const automation = payload as { - automationCron?: CronSnapshot; - automationHooks?: HooksSnapshot; - }; - if (automation.automationCron) { - feedCronSnapshot(automation.automationCron); - } - if (automation.automationHooks) { - feedHooksSnapshot(automation.automationHooks); - } - const prev = settingsRef.current; - const rawNext = resolveAppWorkspaceProjects(applyGatewaySettingsSyncPayload(prev, payload)); - const next = redactSettingsForWebStorage(rawNext); - if (!hasSettingsSyncChanged(prev, next)) { - return; - } - settingsRef.current = next; - setSettingsState(next); - queueSettingsSave(prev, next, "同步桌面端设置失败。", false); - }, - [queueSettingsSave], - ); - - const setSettings = useCallback( - (updater: (prev: AppSettings) => AppSettings) => { - const prev = settingsRef.current; - const updated = updater(prev); - if (updated === prev) return; - const rawNext = resolveAppWorkspaceProjects(normalizeSettings(updated)); - const next = redactSettingsForWebStorage(rawNext); - settingsRef.current = next; - setSettingsState(next); - queueSettingsSave(prev, rawNext, "保存 WebUI 设置失败。", true); - }, - [queueSettingsSave], - ); - - useEffect(() => { - if (!api) { - setSettingsSyncReady(token.trim() === ""); - setSettingsSyncError(null); - return; - } - - let cancelled = false; - setSettingsSyncReady(false); - setSettingsSyncError(null); - // Best-effort: the desktop may be offline; the settings-sync push - // populates the store once it connects. - void initAutomation().catch(() => undefined); - const unsubscribe = api.subscribeSettings((payload) => { - if (cancelled) { - return; - } - applyGatewaySettings(payload); - setSettingsSyncError(null); - }); - - void api - .getSettings() - .then((payload) => { - if (!cancelled) { - applyGatewaySettings(payload); - setSettingsSyncReady(true); - setSettingsSyncError(null); - } - }) - .catch((error) => { - if (!cancelled) { - setSettingsSyncError(asErrorMessage(error, "同步桌面端设置失败")); - setSettingsSyncReady(true); - } - }); - - return () => { - cancelled = true; - unsubscribe(); - }; - }, [api, activeAgentId, applyGatewaySettings, token]); - - return { - settings, - setSettings, - settingsSyncReady, - settingsSyncError, - settingsSaveState, - }; -} diff --git a/crates/agent-gateway/web/src/app/hooks/usePendingUploads.ts b/crates/agent-gateway/web/src/app/hooks/usePendingUploads.ts deleted file mode 100644 index 2af7b1b8a..000000000 --- a/crates/agent-gateway/web/src/app/hooks/usePendingUploads.ts +++ /dev/null @@ -1,393 +0,0 @@ -import { type DragEvent, type RefObject, useCallback, useEffect, useRef, useState } from "react"; - -import type { MentionComposerHandle } from "@/components/chat/MentionComposer"; -import type { NotifyItem } from "@/components/chat/NotifyToast"; -import { t as translate } from "@/i18n"; -import type { PendingUploadedFile } from "@/lib/chat/uploadedFiles"; -import { mergePendingUploadedFiles } from "@/lib/chat/uploadedFiles"; -import { registerLocalUploadedImagePreviews } from "@/lib/chat/uploadedImagePreview"; -import { - clipboardHasFileSignal, - extractClipboardFiles, - readClipboardFiles, -} from "@/lib/clipboardFiles"; -import type { AppSettings } from "@/lib/settings"; -import { importReadableFiles } from "@/lib/uploadReadableFiles"; - -import { asErrorMessage } from "../chatEventUtils"; -import { MAX_UPLOAD_FILES } from "../constants"; -import { dragEventHasFiles } from "../domUtils"; -import { formatTranslation } from "../historyUtils"; - -type UsePendingUploadsParams = { - token: string; - resolveAgentID: () => Promise; - historyShareToken: string | null; - settingsSyncReady: boolean; - settingsOpen: boolean; - activeView: "chat" | "skills-hub" | "mcp-hub"; - locale: AppSettings["locale"]; - executionMode: AppSettings["system"]["executionMode"]; - conversationId: string; - selectedHistoryId: string; - displayedConversationWorkdirRef: RefObject; - composerRef: RefObject; - // Upload feedback goes to the top-right toast stack, never into the - // transcript area — a failed upload is not conversation output. - addNotify: (type: NotifyItem["type"], message: string) => void; -}; - -export function usePendingUploads(params: UsePendingUploadsParams) { - const { - token, - resolveAgentID, - historyShareToken, - settingsSyncReady, - settingsOpen, - activeView, - locale, - executionMode, - conversationId, - selectedHistoryId, - displayedConversationWorkdirRef, - composerRef, - addNotify, - } = params; - - const [pendingUploadedFiles, setPendingUploadedFiles] = useState([]); - const [isUploadingFiles, setIsUploadingFiles] = useState(false); - const [isFileDropActive, setIsFileDropActive] = useState(false); - const fileInputRef = useRef(null); - const pendingUploadedFilesRef = useRef(pendingUploadedFiles); - const pendingUploadsByConversationRef = useRef>(new Map()); - const isUploadingFilesRef = useRef(isUploadingFiles); - const uploadDragDepthRef = useRef(0); - const displayedConversationIdRef = useRef(""); - // Render-assigned mirror: an in-flight import settling between a render and - // its effects must still see the latest mode when it decides whether its - // result is stale. - const executionModeRef = useRef(executionMode); - executionModeRef.current = executionMode; - - const displayedConversationId = (selectedHistoryId || conversationId).trim(); - displayedConversationIdRef.current = displayedConversationId; - - const setUploadingFiles = useCallback((active: boolean) => { - isUploadingFilesRef.current = active; - setIsUploadingFiles(active); - }, []); - - const isDisplayedConversation = useCallback((targetConversationId: string) => { - const conversationIdValue = targetConversationId.trim(); - return conversationIdValue !== "" && displayedConversationIdRef.current === conversationIdValue; - }, []); - - const getPendingUploadsForConversation = useCallback( - (targetConversationId: string) => { - const conversationIdValue = targetConversationId.trim(); - if (!conversationIdValue || isDisplayedConversation(conversationIdValue)) { - return pendingUploadedFilesRef.current; - } - return pendingUploadsByConversationRef.current.get(conversationIdValue) ?? []; - }, - [isDisplayedConversation], - ); - - const setPendingUploadsForConversation = useCallback( - (targetConversationId: string, nextFiles: PendingUploadedFile[]) => { - const conversationIdValue = targetConversationId.trim(); - const normalizedFiles = nextFiles.slice(); - if (conversationIdValue) { - if (normalizedFiles.length > 0) { - pendingUploadsByConversationRef.current.set(conversationIdValue, normalizedFiles); - } else { - pendingUploadsByConversationRef.current.delete(conversationIdValue); - } - } - if (!conversationIdValue || isDisplayedConversation(conversationIdValue)) { - pendingUploadedFilesRef.current = normalizedFiles; - setPendingUploadedFiles(normalizedFiles); - } - }, - [isDisplayedConversation], - ); - - const updatePendingUploadsForConversation = useCallback( - ( - targetConversationId: string, - updater: (current: PendingUploadedFile[]) => PendingUploadedFile[], - ) => { - const conversationIdValue = targetConversationId.trim(); - const currentFiles = getPendingUploadsForConversation(conversationIdValue); - const nextFiles = updater(currentFiles); - setPendingUploadsForConversation(conversationIdValue, nextFiles); - return nextFiles; - }, - [getPendingUploadsForConversation, setPendingUploadsForConversation], - ); - - // A draft conversation got its real id: re-key its stored uploads without - // touching the rendered state — the displayed id flips to `nextId` in the - // same commit, so the switch effect below re-reads the moved entry. - const moveConversationUploads = useCallback((previousId: string, nextId: string) => { - const previous = previousId.trim(); - const next = nextId.trim(); - if (!previous || !next || previous === next) { - return; - } - const files = pendingUploadsByConversationRef.current.get(previous); - if (files === undefined) { - return; - } - pendingUploadsByConversationRef.current.delete(previous); - pendingUploadsByConversationRef.current.set(next, files); - }, []); - - const clearPendingUploads = useCallback(() => { - pendingUploadedFilesRef.current = []; - pendingUploadsByConversationRef.current.clear(); - isUploadingFilesRef.current = false; - uploadDragDepthRef.current = 0; - setPendingUploadedFiles([]); - setIsUploadingFiles(false); - setIsFileDropActive(false); - }, []); - - useEffect(() => { - const nextFiles = displayedConversationId - ? (pendingUploadsByConversationRef.current.get(displayedConversationId) ?? []) - : []; - pendingUploadedFilesRef.current = nextFiles; - setPendingUploadedFiles(nextFiles); - }, [displayedConversationId]); - - const handleImportReadableFiles = useCallback( - async (filesToImport: File[]) => { - if (filesToImport.length === 0) { - return; - } - if (isUploadingFilesRef.current) { - addNotify("warning", translate("chat.upload.uploading", locale)); - return; - } - if (executionMode === "text") { - addNotify("warning", translate("chat.upload.onlyInTools", locale)); - return; - } - const workdir = displayedConversationWorkdirRef.current.trim(); - if (!workdir) { - addNotify("warning", translate("chat.upload.requireWorkdir", locale)); - return; - } - const targetConversationId = displayedConversationIdRef.current; - if (!targetConversationId) { - addNotify("warning", "请先选择或创建会话后再上传文件。"); - return; - } - - const currentUploads = getPendingUploadsForConversation(targetConversationId); - setPendingUploadsForConversation(targetConversationId, currentUploads); - const remainingFileSlots = Math.max(0, MAX_UPLOAD_FILES - currentUploads.length); - if (remainingFileSlots === 0) { - addNotify( - "warning", - formatTranslation(translate("chat.upload.maxFilesIgnored", locale), { - max: MAX_UPLOAD_FILES, - count: filesToImport.length, - }), - ); - return; - } - - const importBatch = filesToImport.slice(0, remainingFileSlots); - const ignoredForLimit = filesToImport.length - importBatch.length; - setUploadingFiles(true); - try { - const agentID = await resolveAgentID(); - const result = await importReadableFiles(token, agentID, workdir, importBatch); - // An import that settles after its upload context was invalidated - // must not resurrect cleared attachments: files picked inside the - // old workspace are not readable from the new one. - if ( - (await resolveAgentID()) !== agentID || - executionModeRef.current === "text" || - (isDisplayedConversation(targetConversationId) && - displayedConversationWorkdirRef.current.trim() !== workdir) - ) { - addNotify("warning", "上传目标已失效,已忽略本次导入的文件"); - return; - } - registerLocalUploadedImagePreviews({ - workspaceRoot: workdir, - uploadedFiles: result.files, - sourceFiles: importBatch, - }); - - if (result.files.length > 0) { - updatePendingUploadsForConversation(targetConversationId, (current) => - mergePendingUploadedFiles(current, result.files).slice(0, MAX_UPLOAD_FILES), - ); - if (isDisplayedConversation(targetConversationId)) { - composerRef.current?.focus(); - } - } - - if (result.files.length === 0 && result.skipped.length > 0) { - addNotify("error", `所选文件均无法导入:\n${result.skipped.join("\n")}`); - } else if (result.skipped.length > 0) { - addNotify("warning", `以下文件已跳过:\n${result.skipped.join("\n")}`); - } - if (ignoredForLimit > 0) { - addNotify( - "warning", - formatTranslation(translate("chat.upload.maxFilesIgnored", locale), { - max: MAX_UPLOAD_FILES, - count: ignoredForLimit, - }), - ); - } - } catch (error) { - addNotify("error", asErrorMessage(error, "导入文件失败")); - } finally { - setUploadingFiles(false); - } - }, - [ - addNotify, - composerRef, - displayedConversationWorkdirRef, - executionMode, - getPendingUploadsForConversation, - isDisplayedConversation, - locale, - resolveAgentID, - setPendingUploadsForConversation, - setUploadingFiles, - token, - updatePendingUploadsForConversation, - ], - ); - - useEffect(() => { - if ( - !token || - historyShareToken || - !settingsSyncReady || - settingsOpen || - activeView !== "chat" - ) { - return; - } - - const handleDocumentPaste = (event: globalThis.ClipboardEvent) => { - if (event.defaultPrevented) return; - const clipboardFiles = extractClipboardFiles(event.clipboardData); - if (clipboardFiles.length > 0) { - event.preventDefault(); - event.stopPropagation(); - void handleImportReadableFiles(clipboardFiles); - return; - } - if (!clipboardHasFileSignal(event.clipboardData)) return; - - event.preventDefault(); - event.stopPropagation(); - void readClipboardFiles() - .then((files) => { - if (files.length === 0) { - addNotify("warning", "无法读取剪贴板中的文件,请尝试拖拽或点击上传。"); - return; - } - return handleImportReadableFiles(files); - }) - .catch((error) => { - addNotify("error", asErrorMessage(error, "读取剪贴板文件失败")); - }); - }; - - document.addEventListener("paste", handleDocumentPaste, true); - return () => { - document.removeEventListener("paste", handleDocumentPaste, true); - }; - }, [ - activeView, - addNotify, - handleImportReadableFiles, - historyShareToken, - settingsOpen, - settingsSyncReady, - token, - ]); - - const handleFileDragEnter = useCallback((event: DragEvent) => { - if (!dragEventHasFiles(event)) return; - event.preventDefault(); - event.stopPropagation(); - uploadDragDepthRef.current += 1; - setIsFileDropActive(true); - }, []); - - const handleFileDragOver = useCallback( - (event: DragEvent, canDropUpload: boolean) => { - if (!dragEventHasFiles(event)) return; - event.preventDefault(); - event.stopPropagation(); - event.dataTransfer.dropEffect = canDropUpload ? "copy" : "none"; - setIsFileDropActive(true); - }, - [], - ); - - const handleFileDragLeave = useCallback((event: DragEvent) => { - if (!dragEventHasFiles(event)) return; - event.preventDefault(); - event.stopPropagation(); - uploadDragDepthRef.current = Math.max(0, uploadDragDepthRef.current - 1); - if (uploadDragDepthRef.current === 0) { - setIsFileDropActive(false); - } - }, []); - - const handleFileDrop = useCallback( - ( - event: DragEvent, - options: { - canDropUpload: boolean; - disabledMessage: string; - }, - ) => { - if (!dragEventHasFiles(event)) return; - event.preventDefault(); - event.stopPropagation(); - uploadDragDepthRef.current = 0; - setIsFileDropActive(false); - - const files = Array.from(event.dataTransfer.files ?? []); - if (files.length === 0) return; - if (!options.canDropUpload) { - addNotify("warning", options.disabledMessage); - return; - } - void handleImportReadableFiles(files); - }, - [addNotify, handleImportReadableFiles], - ); - - return { - pendingUploadedFiles, - isUploadingFiles, - isFileDropActive, - fileInputRef, - setUploadingFiles, - getPendingUploadsForConversation, - setPendingUploadsForConversation, - updatePendingUploadsForConversation, - moveConversationUploads, - clearPendingUploads, - handleImportReadableFiles, - handleFileDragEnter, - handleFileDragOver, - handleFileDragLeave, - handleFileDrop, - }; -} diff --git a/crates/agent-gateway/web/src/app/hooks/useProjectToolsRuntime.ts b/crates/agent-gateway/web/src/app/hooks/useProjectToolsRuntime.ts deleted file mode 100644 index a3fdd571b..000000000 --- a/crates/agent-gateway/web/src/app/hooks/useProjectToolsRuntime.ts +++ /dev/null @@ -1,353 +0,0 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; - -import type { WorkspaceCodeEditorOpenRequest } from "@/components/workspace-editor/WorkspaceCodeEditorOverlay"; -import type { WorkspaceFilePreviewOpenRequest } from "@/components/workspace-editor/WorkspaceFilePreviewOverlay"; -import type { WorkspaceSshTerminalOpenRequest } from "@/components/workspace-editor/WorkspaceSshTerminalOverlay"; -import { isWorkspacePreviewPath } from "@/components/workspace-editor/workspaceImagePreview"; -import { - applyTerminalEventToSessions, - replaceTerminalSessionsForProject, - sortTerminalSessions, - terminalSessionBelongsToProject, -} from "@/lib/terminal/sessionStore"; -import type { TerminalClient, TerminalSession } from "@/lib/terminal/types"; - -type UseProjectToolsRuntimeParams = { - terminalClient: TerminalClient | null; - settingsSyncReady: boolean; - isAgentMode: boolean; - webTerminalSessionsEnabled: boolean; - statusOnline?: boolean; - statusSessionId?: string | null; - terminalProjectPath: string; - terminalProjectPathKey: string; - rightDockFileTreeOpen: boolean; - rightDockSshTunnelOpen: boolean; -}; - -export function useProjectToolsRuntime(params: UseProjectToolsRuntimeParams) { - const { - terminalClient, - settingsSyncReady, - isAgentMode, - webTerminalSessionsEnabled, - statusOnline, - statusSessionId, - terminalProjectPath, - terminalProjectPathKey, - rightDockFileTreeOpen, - rightDockSshTunnelOpen, - } = params; - - const previousRightDockFileTreeOpenRef = useRef(false); - const [workspaceEditorMounted, setWorkspaceEditorMounted] = useState(false); - const [workspaceEditorOpen, setWorkspaceEditorOpen] = useState(false); - const [workspaceEditorCleanupPending, setWorkspaceEditorCleanupPending] = useState(false); - const [workspaceEditorOpenRequest, setWorkspaceEditorOpenRequest] = - useState(null); - const [workspaceEditorCloseRequestId, setWorkspaceEditorCloseRequestId] = useState(0); - const workspaceEditorRequestIdRef = useRef(0); - const [workspaceFilePreviewMounted, setWorkspaceFilePreviewMounted] = useState(false); - const [workspaceFilePreviewOpen, setWorkspaceFilePreviewOpen] = useState(false); - const [workspaceFilePreviewOpenRequest, setWorkspaceFilePreviewOpenRequest] = - useState(null); - const workspaceFilePreviewRequestIdRef = useRef(0); - const [workspaceSshTerminalMounted, setWorkspaceSshTerminalMounted] = useState(false); - const [workspaceSshTerminalOpen, setWorkspaceSshTerminalOpen] = useState(false); - const [workspaceSshTerminalOpenRequest, setWorkspaceSshTerminalOpenRequest] = - useState(null); - const workspaceSshTerminalRequestIdRef = useRef(0); - const [terminalSessions, setTerminalSessions] = useState([]); - const [terminalSessionsLoaded, setTerminalSessionsLoaded] = useState(false); - const terminalSessionsVersionRef = useRef(0); - const terminalStatusSessionIdRef = useRef(""); - - const hideWorkspaceSshTerminalOverlay = useCallback(() => { - setWorkspaceSshTerminalOpen(false); - }, []); - - const openWorkspaceSshTerminalRequest = useCallback( - (request: WorkspaceSshTerminalOpenRequest) => { - setWorkspaceFilePreviewOpen(false); - setWorkspaceEditorOpen(false); - setWorkspaceSshTerminalMounted(true); - setWorkspaceSshTerminalOpen(true); - setWorkspaceSshTerminalOpenRequest(request); - }, - [], - ); - - const requestWorkspaceEditorClose = useCallback(() => { - setWorkspaceEditorCloseRequestId((current) => current + 1); - }, []); - - const handleWorkspaceEditorHide = useCallback(() => { - setWorkspaceEditorOpen(false); - }, []); - - const handleWorkspaceEditorClosed = useCallback(() => { - setWorkspaceEditorOpen(false); - setWorkspaceEditorMounted(false); - setWorkspaceEditorCleanupPending(false); - setWorkspaceEditorOpenRequest(null); - setWorkspaceEditorCloseRequestId(0); - }, []); - - const openWorkspaceEditorFile = useCallback( - (request: Omit) => { - hideWorkspaceSshTerminalOverlay(); - setWorkspaceFilePreviewOpen(false); - workspaceEditorRequestIdRef.current += 1; - setWorkspaceEditorCleanupPending(false); - setWorkspaceEditorMounted(true); - setWorkspaceEditorOpen(true); - setWorkspaceEditorOpenRequest({ - id: workspaceEditorRequestIdRef.current, - ...request, - }); - }, - [hideWorkspaceSshTerminalOverlay], - ); - - const openWorkspaceFilePreview = useCallback( - (request: Omit) => { - hideWorkspaceSshTerminalOverlay(); - setWorkspaceEditorOpen(false); - workspaceFilePreviewRequestIdRef.current += 1; - setWorkspaceFilePreviewMounted(true); - setWorkspaceFilePreviewOpen(true); - setWorkspaceFilePreviewOpenRequest({ - id: workspaceFilePreviewRequestIdRef.current, - ...request, - }); - }, - [hideWorkspaceSshTerminalOverlay], - ); - - const handleOpenWorkspaceFile = useCallback( - (path: string, imagePaths?: string[]) => { - if (!terminalProjectPath || !terminalProjectPathKey) return; - const request = { - projectPathKey: terminalProjectPathKey, - workdir: terminalProjectPath, - path, - imagePaths, - }; - if (isWorkspacePreviewPath(path)) { - openWorkspaceFilePreview(request); - return; - } - openWorkspaceEditorFile(request); - }, - [ - openWorkspaceEditorFile, - openWorkspaceFilePreview, - terminalProjectPath, - terminalProjectPathKey, - ], - ); - - const handleOpenSshTerminal = useCallback( - (session: TerminalSession, kind: WorkspaceSshTerminalOpenRequest["kind"] = "bash") => { - if (session.kind !== "ssh") return; - workspaceSshTerminalRequestIdRef.current += 1; - openWorkspaceSshTerminalRequest({ - id: workspaceSshTerminalRequestIdRef.current, - sessionId: session.id, - kind, - }); - }, - [openWorkspaceSshTerminalRequest], - ); - - const requestWorkspaceFilePreviewClose = useCallback(() => { - setWorkspaceFilePreviewOpen(false); - }, []); - - const handleWorkspaceFilePreviewClosed = useCallback(() => { - setWorkspaceFilePreviewOpen(false); - setWorkspaceFilePreviewMounted(false); - setWorkspaceFilePreviewOpenRequest(null); - }, []); - - useEffect(() => { - const previousOpen = previousRightDockFileTreeOpenRef.current; - previousRightDockFileTreeOpenRef.current = rightDockFileTreeOpen; - if (rightDockFileTreeOpen && workspaceEditorCleanupPending) { - setWorkspaceEditorCleanupPending(false); - } - if (previousOpen && !rightDockFileTreeOpen && workspaceEditorMounted) { - setWorkspaceEditorCleanupPending(true); - setWorkspaceEditorOpen(true); - requestWorkspaceEditorClose(); - } - if (previousOpen && !rightDockFileTreeOpen && workspaceFilePreviewMounted) { - requestWorkspaceFilePreviewClose(); - } - }, [ - rightDockFileTreeOpen, - requestWorkspaceEditorClose, - requestWorkspaceFilePreviewClose, - workspaceEditorCleanupPending, - workspaceEditorMounted, - workspaceFilePreviewMounted, - ]); - - const projectTerminalSessions = useMemo( - () => - terminalProjectPathKey - ? terminalSessions.filter((session) => - terminalSessionBelongsToProject(session, terminalProjectPathKey), - ) - : [], - [terminalProjectPathKey, terminalSessions], - ); - - const handleProjectTerminalSessionsChange = useCallback((sessions: TerminalSession[]) => { - terminalSessionsVersionRef.current += 1; - setTerminalSessions(sortTerminalSessions(sessions)); - }, []); - - useEffect(() => { - // Loaded flips false whenever the gates or the gateway session identity - // change, and true only once list() settles below — RightDockPanel uses it - // to defer terminal-tab GC until the session list is actually known. - setTerminalSessionsLoaded(false); - if (!terminalClient) { - terminalSessionsVersionRef.current += 1; - setTerminalSessions([]); - return; - } - if (!settingsSyncReady) { - return; - } - if (!isAgentMode || !webTerminalSessionsEnabled || statusOnline === false) { - terminalSessionsVersionRef.current += 1; - setTerminalSessions([]); - return; - } - if (statusOnline !== true) { - return; - } - const normalizedStatusSessionId = statusSessionId?.trim() ?? ""; - if ( - normalizedStatusSessionId && - terminalStatusSessionIdRef.current !== normalizedStatusSessionId - ) { - const hadPreviousSession = terminalStatusSessionIdRef.current !== ""; - terminalStatusSessionIdRef.current = normalizedStatusSessionId; - if (hadPreviousSession) { - terminalSessionsVersionRef.current += 1; - setTerminalSessions([]); - } - } - let cancelled = false; - const requestVersion = terminalSessionsVersionRef.current; - void terminalClient - .list() - .then((sessions) => { - if (!cancelled && terminalSessionsVersionRef.current === requestVersion) { - setTerminalSessions(sortTerminalSessions(sessions)); - } - }) - .catch(() => undefined) - .finally(() => { - if (!cancelled) { - setTerminalSessionsLoaded(true); - } - }); - return () => { - cancelled = true; - }; - }, [ - isAgentMode, - settingsSyncReady, - statusOnline, - statusSessionId, - terminalClient, - webTerminalSessionsEnabled, - ]); - - useEffect(() => { - if (!terminalClient) return; - return terminalClient.subscribe((event) => { - if (event.kind === "output") return; - terminalSessionsVersionRef.current += 1; - setTerminalSessions((current) => applyTerminalEventToSessions(current, event)); - }); - }, [terminalClient]); - - useEffect(() => { - // One catch-up fetch when the ssh-tunnel tab becomes usable (opened, agent - // back online, gateway session ready). Ongoing freshness is event-driven: - // the terminalClient.subscribe effect above applies created/updated/closed - // broadcasts, and SshTunnelPanel's own active-gated reconcile feeds - // list() results back through onSessionsReconcile -> onSessionsChange. - // A parallel 5s poll here only duplicated that traffic. - if (!terminalClient) return; - if (!settingsSyncReady) return; - if (!isAgentMode || !webTerminalSessionsEnabled || statusOnline !== true) return; - if (!rightDockSshTunnelOpen || !terminalProjectPathKey) return; - - let cancelled = false; - void terminalClient - .list(terminalProjectPathKey) - .then((sessions) => { - if (cancelled) return; - terminalSessionsVersionRef.current += 1; - setTerminalSessions((current) => - replaceTerminalSessionsForProject(current, terminalProjectPathKey, sessions), - ); - }) - .catch(() => undefined); - return () => { - cancelled = true; - }; - }, [ - isAgentMode, - rightDockSshTunnelOpen, - settingsSyncReady, - statusOnline, - terminalClient, - terminalProjectPathKey, - webTerminalSessionsEnabled, - ]); - - const resetTerminalSessions = useCallback(() => { - terminalSessionsVersionRef.current += 1; - terminalStatusSessionIdRef.current = ""; - setTerminalSessions([]); - setTerminalSessionsLoaded(false); - }, []); - - return { - workspaceEditorMounted, - workspaceEditorOpen, - workspaceEditorCleanupPending, - workspaceEditorOpenRequest, - workspaceEditorCloseRequestId, - workspaceFilePreviewMounted, - workspaceFilePreviewOpen, - workspaceFilePreviewOpenRequest, - workspaceSshTerminalMounted, - workspaceSshTerminalOpen, - workspaceSshTerminalOpenRequest, - terminalSessions, - terminalSessionsLoaded, - setTerminalSessions, - terminalSessionsVersionRef, - terminalStatusSessionIdRef, - projectTerminalSessions, - openWorkspaceEditorFile, - openWorkspaceFilePreview, - handleWorkspaceEditorHide, - handleWorkspaceEditorClosed, - requestWorkspaceFilePreviewClose, - handleWorkspaceFilePreviewClosed, - handleOpenWorkspaceFile, - handleOpenSshTerminal, - handleProjectTerminalSessionsChange, - resetTerminalSessions, - hideWorkspaceSshTerminalOverlay, - }; -} diff --git a/crates/agent-gateway/web/src/app/sidebar/GatewaySidebarContainer.tsx b/crates/agent-gateway/web/src/app/sidebar/GatewaySidebarContainer.tsx deleted file mode 100644 index 938a1062c..000000000 --- a/crates/agent-gateway/web/src/app/sidebar/GatewaySidebarContainer.tsx +++ /dev/null @@ -1,385 +0,0 @@ -// Sidebar container for the web end: owns every useSidebarSelector -// subscription plus the rename UI state, so store commits (activity ticks, -// list updates, per-row mutations) re-render this subtree only — never -// GatewayApp. Renders the per-end view. - -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { ChatHistorySidebar } from "@/components/chat/ChatHistorySidebar"; -import { useLocale } from "@/i18n"; -import type { ChatHistorySummary } from "@/lib/chat/chatHistory"; -import type { WorkspaceProject } from "@/lib/settings"; -import type { SidebarBatchDeleteOptions } from "@/lib/sidebar/batchDelete"; -import { deleteSidebarConversations } from "@/lib/sidebar/batchDelete"; -import { - selectConversations, - selectListState, - selectProjectActivityInputs, - selectRunningConversationIds, - sidebarShallowEqual, -} from "@/lib/sidebar/selectors"; -import type { SidebarSnapshot, SidebarStore } from "@/lib/sidebar/store"; -import type { SidebarErrorCode } from "@/lib/sidebar/types"; -import { useSidebarSelector } from "@/lib/sidebar/useSidebarSelector"; -import { sortWorkspaceProjectsByActivity } from "@/lib/workspaceProjects"; - -function selectMutations(snapshot: SidebarSnapshot) { - return snapshot.mutations; -} - -function selectMutationErrors(snapshot: SidebarSnapshot) { - return snapshot.mutationErrors; -} - -function selectConversationIndex(snapshot: SidebarSnapshot) { - return snapshot.byId; -} - -// Transport-shaped list errors merely restate "the read path is down or -// congested right now"; the page-level banner and Online/Offline pill own -// that story, so the sidebar never repeats it — including the stale copy -// that lingers until the next reconcile tick or reconnect refetch lands. -// Three sources produce this class of message: -// - browser⇄gateway socket failures (mirrors isRecoverableGatewayTransportError -// in lib/gatewaySocket.ts) plus the client-side request timeout, which the -// status poll ignores under fresh inbound activity for the same reason; -// - the Go hub rejecting a roundtrip while the desktop agent is briefly -// offline or re-registering ("agent offline", websocket_roundtrip.go) — -// the socket stays up in that window, so connectionLost never covers it; -// - gateway-side context outcomes on the hub⇄agent roundtrip -// ("request timed out"/"request canceled", websocket_roundtrip.go). -// Genuine desktop read failures arrive as other strings and still surface. -function isGatewayTransportErrorDetail(detail: string | null | undefined) { - const message = (detail ?? "").trim(); - return ( - message.startsWith("Gateway WebSocket disconnected") || - message === "Gateway WebSocket is not connected" || - message.startsWith("Gateway transport stalled") || - message.startsWith("Gateway WebSocket request timed out") || - message === "agent offline" || - message === "request timed out" || - message === "request canceled" - ); -} - -// Stable identity wrapper so callback props from GatewayApp (recreated per -// render) never churn effects or the memo'd view rows. -function useStableCallback( - handler: (...args: Args) => Return, -): (...args: Args) => Return { - const handlerRef = useRef(handler); - handlerRef.current = handler; - return useCallback((...args: Args) => handlerRef.current(...args), []); -} - -export type GatewaySidebarContainerProps = { - store: SidebarStore; - currentConversationId: string; - isOpen: boolean; - fontScale?: number; - activeView: "chat" | "skills-hub" | "mcp-hub"; - showProjects: boolean; - // Merged (settings + history workdirs), unsorted: sorting happens here on - // the store's activity snapshot so project reordering never re-renders - // GatewayApp. - projects: WorkspaceProject[]; - activeProjectId?: string; - missingProjectPathKeys: ReadonlySet; - projectRenamingId: string | null; - projectRenameDraft: string; - projectsCollapsed: boolean; - recentCollapsed: boolean; - canShareConversations: boolean; - sharedConversationCount: number; - // GatewayApp-level sidebar errors (project removal flow); store errors are - // derived locally and take precedence. - externalErrorMessage: string | null; - // Gateway socket dropped after having been connected: transport-shaped - // error cards are suppressed because the page banner owns that messaging. - connectionLost: boolean; - // Workspace and recent-conversation interactions are available only while - // both the browser transport and the desktop Agent are confirmed online. - sectionsDisabled: boolean; - isLocalDraftConversationId: (id: string) => boolean; - onProjectsCollapsedChange: (collapsed: boolean) => void; - onRecentCollapsedChange: (collapsed: boolean) => void; - onCreateProject: () => void; - onSelectProject: (project: WorkspaceProject) => void; - onNewConversationForProject: (project: WorkspaceProject) => void; - onBrowseProjectInFileTree: (project: WorkspaceProject) => void; - onStartRenamingProject: (project: WorkspaceProject) => void; - onProjectRenameDraftChange: (value: string) => void; - onCommitProjectRename: () => void; - onCancelProjectRename: () => void; - onSetProjectPinned: (project: WorkspaceProject, isPinned: boolean) => void; - onRemoveProject: (project: WorkspaceProject) => void; - onArchiveProject: (project: WorkspaceProject) => void; - onUnarchiveProject: (project: WorkspaceProject) => void; - archivedProjectPathKeys?: ReadonlySet; - onNewConversation: () => void; - onSelectConversation: (id: string) => void; - onShareConversation: (item: ChatHistorySummary) => void; - onOpenSharedConversations: () => void; - // User-initiated removal of a local draft row (never hits the backend). - onLocalDraftDeleted: (id: string) => void; - // Conversations that left the authoritative index (remote delete, local - // delete confirmation, reconcile drop): GatewayApp cleans caches and - // migrates the selection when the displayed conversation vanished. - onConversationsRemoved: (ids: readonly string[]) => void; - onCloseSidebar: () => void; - onOpenSettings: () => void; - onOpenSkillsHub: () => void; - onOpenMcpHub: () => void; -}; - -export function GatewaySidebarContainer(props: GatewaySidebarContainerProps) { - const { - store, - projects, - externalErrorMessage, - connectionLost, - sectionsDisabled, - isLocalDraftConversationId, - } = props; - const { t } = useLocale(); - - const items = useSidebarSelector(store, selectConversations); - const listState = useSidebarSelector(store, selectListState, sidebarShallowEqual); - const scopeKey = useSidebarSelector(store, (snapshot) => snapshot.scopeKey); - const runningConversationIds = useSidebarSelector(store, selectRunningConversationIds); - const mutations = useSidebarSelector(store, selectMutations); - const mutationErrors = useSidebarSelector(store, selectMutationErrors); - const projectActivityInputs = useSidebarSelector( - store, - selectProjectActivityInputs, - sidebarShallowEqual, - ); - const conversationIndex = useSidebarSelector(store, selectConversationIndex); - - // --- Rename UI state (moved out of GatewayApp) --------------------------- - const [renamingId, setRenamingId] = useState(null); - const [renameDraft, setRenameDraft] = useState(""); - - useEffect(() => { - if (!sectionsDisabled) { - return; - } - setRenamingId(null); - setRenameDraft(""); - }, [sectionsDisabled]); - - const clearMutationErrors = useCallback(() => { - for (const id of store.getSnapshot().mutationErrors.keys()) { - store.clearMutationError(id); - } - }, [store]); - - const handleStartRenaming = useStableCallback((item: ChatHistorySummary) => { - if (sectionsDisabled) { - return; - } - setRenamingId(item.id); - setRenameDraft(item.title); - }); - - const handleCommitRename = useStableCallback(() => { - if (sectionsDisabled) { - setRenamingId(null); - setRenameDraft(""); - return; - } - if (!renamingId) { - return; - } - const conversationId = renamingId; - const title = renameDraft.trim(); - setRenamingId(null); - setRenameDraft(""); - if (!title || title === store.peek(conversationId)?.title) { - return; - } - clearMutationErrors(); - void store.rename(conversationId, title); - }); - - const handleCancelRename = useStableCallback(() => { - setRenamingId(null); - setRenameDraft(""); - }); - - const handleSetPinned = useStableCallback((id: string, isPinned: boolean) => { - if (sectionsDisabled) { - return; - } - clearMutationErrors(); - void store.setPinned(id, isPinned); - }); - - const handleDeleteConversation = useStableCallback((id: string) => { - if (sectionsDisabled) { - return; - } - clearMutationErrors(); - const existing = store.peek(id); - if (existing?.isPending === true || isLocalDraftConversationId(id)) { - store.removeLocal(id); - props.onLocalDraftDeleted(id); - return; - } - void store.remove(id); - }); - - const handleDeleteConversations = useStableCallback( - async (ids: readonly string[], options?: SidebarBatchDeleteOptions) => { - if (sectionsDisabled) { - return { deletedIds: [], failedIds: [...ids], skippedIds: [] }; - } - clearMutationErrors(); - return deleteSidebarConversations( - ids, - async (id) => { - const existing = store.peek(id); - if (existing?.isPending === true || isLocalDraftConversationId(id)) { - store.removeLocal(id); - props.onLocalDraftDeleted(id); - return true; - } - return store.remove(id); - }, - options, - ); - }, - ); - - const handleLoadMore = useStableCallback(() => { - if (sectionsDisabled) { - return; - } - void store.loadMore(); - }); - - // --- Authoritative-removal watcher --------------------------------------- - // byId is the cross-scope index: entries only leave it on delete events, - // confirmed local deletes, or authoritative reconcile drops — a scope - // switch does not evict, so this never fires for out-of-scope selections. - const onConversationsRemoved = useStableCallback(props.onConversationsRemoved); - const knownConversationIdsRef = useRef | null>(null); - useEffect(() => { - const previous = knownConversationIdsRef.current; - const next = new Set(conversationIndex.keys()); - knownConversationIdsRef.current = next; - if (!previous || previous.size === 0) { - return; - } - const removed: string[] = []; - for (const id of previous) { - if (!next.has(id)) { - removed.push(id); - } - } - if (removed.length > 0) { - onConversationsRemoved(removed); - } - }, [conversationIndex, onConversationsRemoved]); - - // --- Errors --------------------------------------------------------------- - const translateErrorCode = useCallback( - (code: SidebarErrorCode) => t(`chat.history.${code}`), - [t], - ); - const listErrorMessage = useMemo(() => { - if (connectionLost) { - return null; - } - if (listState.error && !isGatewayTransportErrorDetail(listState.errorDetail)) { - return listState.errorDetail?.trim() || translateErrorCode(listState.error); - } - return null; - }, [connectionLost, listState.error, listState.errorDetail, translateErrorCode]); - const actionErrorMessage = useMemo(() => { - if (connectionLost) { - return null; - } - let lastMutationError: SidebarErrorCode | null = null; - for (const code of mutationErrors.values()) { - lastMutationError = code; - } - if (lastMutationError) { - return translateErrorCode(lastMutationError); - } - return externalErrorMessage; - }, [connectionLost, externalErrorMessage, mutationErrors, translateErrorCode]); - - // --- Projects ------------------------------------------------------------- - const sortedProjects = useMemo( - () => - sortWorkspaceProjectsByActivity(projects, { - projectActivityUpdatedAts: projectActivityInputs.workdirActivity, - runningProjectPathKeys: projectActivityInputs.runningWorkdirPathKeys, - }), - [projectActivityInputs.runningWorkdirPathKeys, projectActivityInputs.workdirActivity, projects], - ); - - return ( - - ); -} diff --git a/crates/agent-gateway/web/src/app/sidebar/gatewaySidebarAvailability.ts b/crates/agent-gateway/web/src/app/sidebar/gatewaySidebarAvailability.ts deleted file mode 100644 index 97ad4af3c..000000000 --- a/crates/agent-gateway/web/src/app/sidebar/gatewaySidebarAvailability.ts +++ /dev/null @@ -1,39 +0,0 @@ -export type GatewaySidebarStatusFreshnessState = { - socketConnected: boolean; - agentStatusFresh: boolean; -}; - -export type GatewaySidebarStatusFreshnessEvent = - | { type: "connection"; connected: boolean } - | { type: "status" }; - -export const INITIAL_GATEWAY_SIDEBAR_STATUS_FRESHNESS: GatewaySidebarStatusFreshnessState = { - socketConnected: false, - agentStatusFresh: false, -}; - -export function reduceGatewaySidebarStatusFreshness( - state: GatewaySidebarStatusFreshnessState, - event: GatewaySidebarStatusFreshnessEvent, -): GatewaySidebarStatusFreshnessState { - if (event.type === "connection") { - return { - socketConnected: event.connected, - // Every authenticated socket starts a new status epoch. The previous - // socket's cached online verdict cannot make the new path interactive. - agentStatusFresh: false, - }; - } - return { - ...state, - agentStatusFresh: state.socketConnected, - }; -} - -export function shouldDisableGatewaySidebarSections(input: { - connectionLost: boolean; - agentStatusFresh: boolean; - agentOnline: boolean | null | undefined; -}): boolean { - return input.connectionLost || !input.agentStatusFresh || input.agentOnline !== true; -} diff --git a/crates/agent-gateway/web/src/app/types.ts b/crates/agent-gateway/web/src/app/types.ts deleted file mode 100644 index 2bb6df7cd..000000000 --- a/crates/agent-gateway/web/src/app/types.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { HistoryMessageRef } from "@/lib/chat/conversationState"; -import type { ChatCommandOutcome } from "@/lib/chat/stream/chatCommandPipeline"; -import type { PendingUploadedFile } from "@/lib/chat/uploadedFiles"; -import type { ChatRuntimeControls, CustomProvider } from "@/lib/settings"; - -export type OverlayState = "closed" | "entering" | "open" | "leaving"; - -export type SendChatOptions = { - conversationId?: string; - clientRequestId?: string; - uploadedFiles?: PendingUploadedFile[]; - runtimeControls?: ChatRuntimeControls; - workdir?: string; - editMessageRef?: HistoryMessageRef; - queuePolicy?: "auto" | "append" | "interrupt"; - // false for queue-destined sends: no transcript echo, the queue panel owns - // the prompt until it actually runs. - optimisticEcho?: boolean; -}; - -export type SendChatFn = ( - message: string, - options?: SendChatOptions, -) => Promise; - -export type ModelProviderSource = Pick; - -export type TunnelManagerToolChange = { - action: "create" | "close"; - projectPathKey: string; -}; diff --git a/crates/agent-gateway/web/src/components/GatewayTranscript.tsx b/crates/agent-gateway/web/src/components/GatewayTranscript.tsx deleted file mode 100644 index f3a110549..000000000 --- a/crates/agent-gateway/web/src/components/GatewayTranscript.tsx +++ /dev/null @@ -1,1916 +0,0 @@ -import { useVirtualizer } from "@tanstack/react-virtual"; -import { - type Dispatch, - type MutableRefObject, - memo, - type SetStateAction, - useCallback, - useEffect, - useLayoutEffect, - useMemo, - useRef, - useState, -} from "react"; -import { ImagePreview, type ImagePreviewSlide } from "@/components/chat/ImagePreview"; -import { Markdown } from "@/components/Markdown"; -import { useLocale } from "@/i18n/LocaleContext"; -import type { ChatFileLink } from "@/lib/chat/chatFileLinks"; -import { normalizeLiveToolStatus, VIBING_STATUS } from "@/lib/chat/chatPageHelpers"; -import type { HistoryMessageRef } from "@/lib/chat/conversationState"; -import { getRoundText, getRoundToolTrace } from "@/lib/chat/uiMessages"; -import { - formatUploadedFileSize, - type PendingUploadedFile, - parsePastedTextDisplayReferences, -} from "@/lib/chat/uploadedFiles"; -import { - getUploadedImagePreviewCacheKey, - loadUploadedImagePreview, - readUploadedImagePreviewCache, - type UploadedImagePreviewLoader, -} from "@/lib/chat/uploadedImagePreview"; -import { - buildGitHubCommitUrl, - type CommitDetailsLoader, - type CommitDisplayReference, - UserMessageContent, -} from "@/lib/chat/userMessageContent"; -import type { GitClient } from "@/lib/git/types"; -import { DEFAULT_CHAT_TRANSCRIPT_WIDTH } from "@/lib/settings"; -import { cn } from "@/lib/shared/utils"; -import { extractLiveRange } from "@/lib/transcript-virtual/liveRangeExtractor"; -import { createLiveRowScrollAdjustPolicy } from "@/lib/transcript-virtual/liveScrollAdjustPolicy"; -import { - buildTranscriptLayoutKey, - createTranscriptMeasurementsLru, -} from "@/lib/transcript-virtual/measurementsLru"; -import { - CHECKPOINT_ROW_ESTIMATE_PX, - estimateAssistantRowHeight, - estimateUserRowHeight, - measureEstimateText, -} from "@/lib/transcript-virtual/rowEstimates"; -import { - AssistantAvatar, - AssistantBubble, - AssistantStatus, - CompactingText, - RetryDetailsBlock, - VibingText, -} from "@/pages/chat/AssistantBubble"; -import type { RetryAttemptRecord, TranscriptRow } from "../lib/chat/transcript/types"; - -import type { GatewayTranscriptRound } from "../lib/chatUi"; -import type { SectionId } from "../pages/settings/types"; -import { ChatEmptyState } from "./chat/ChatEmptyState"; -import { getUploadedFileTypeIcon } from "./chat/fileTypeIcons"; -import { - Check, - CheckCircle2, - ChevronDown, - Copy, - GitBranch, - Loader2, - Pencil, - RefreshCw, - X, -} from "./icons"; -import { ConfirmActionPopover } from "./ui/confirm-action-popover"; - -type GatewayTranscriptProps = { - conversationId?: string; - // The whole transcript as one row list, rendered by one virtualizer. Rows - // come from one store assembly, so a row can never render twice. - rows: readonly TranscriptRow[]; - // Index of the first unfolded-turn row (-1 when everything is folded); - // rows at or after it are force-mounted so a streaming reply never - // unmounts mid-run. - liveStartIndex?: number; - // Key of the actively streaming turn (caret / live structural state). - activeTurnKey?: string | null; - contentWidth?: number; - // Whether the scroll-follow engine is attached to the bottom; gates the - // virtualizer's resize-compensation carve-out for live-row growth. - isViewportFollowing?: () => boolean; - // Imperative jump handle for the floor navigation rail. - navRef?: MutableRefObject; - // Reports the user row at the viewport's top edge (the "current floor"). - onAnchorUserRowChange?: (rowKey: string | null) => void; - error?: string | null; - toolStatus?: string | null; - toolStatusIsCompaction?: boolean; - // Live run's stream-retry history; renders as an expandable details block - // under the live status (mirrors the desktop app). - retryAttempts?: readonly RetryAttemptRecord[]; - isStreaming?: boolean; - isLoading?: boolean; - loadingTitle?: string; - hasModels?: boolean; - onOpenSettings?: (section?: SectionId) => void; - hasMoreHistory?: boolean; - isLoadingMoreHistory?: boolean; - onLoadEarlierHistory?: () => void; - isAgentMode?: boolean; - showUsage?: boolean; - usageContextWindow?: number; - workspaceRoot?: string; - gitClient?: GitClient | null; - onOpenFileLink?: (link: ChatFileLink) => void; - onLoadUploadedImagePreview?: UploadedImagePreviewLoader; - onResendFromEdit?: ( - messageRef: HistoryMessageRef, - text: string, - uploadedFiles: PendingUploadedFile[], - ) => void; - onBranchConversation?: (messageRef: HistoryMessageRef) => void; - // Anchor messageId of the branch request in flight; the matching row shows - // a spinner and every branch button disables until it settles. - branchPendingMessageId?: string | null; - onSuggestionSelect?: (text: string) => void; - suggestionsDisabled?: boolean; - readOnly?: boolean; - redactToolContent?: boolean; -}; - -// Stream-born rows keep Streamdown's streaming render mode forever — even -// after their turn folds — so the streaming→static mode flip (and its full -// re-parse) can never happen. History-born rows render static from the -// start. -function rowRenderMode(row: Extract) { - return row.origin === "stream" ? ("streaming" as const) : ("static" as const); -} - -export type GatewayTranscriptNavHandle = { - // Aligns the row to the viewport top and keeps re-aligning for a few - // frames while dynamic measurements land (convergent, cancelled by user - // scroll input). - scrollToRowKey: (rowKey: string) => void; -}; - -const TRANSCRIPT_ROW_ESTIMATED_HEIGHT = 260; -const TRANSCRIPT_ROW_GAP = 18; -const TRANSCRIPT_ROW_OVERSCAN_COUNT = 5; - -// Measured row heights survive conversation switches: saved on unmount, -// restored (width-gated) on the next open so the switch lays out with exact -// heights instead of estimates. -const transcriptMeasurementsLru = createTranscriptMeasurementsLru(); - -type GatewayTranscriptVirtualItem = - | { key: string; kind: "loadRemoteHistory" } - | { key: string; kind: "row"; row: TranscriptRow } - | { key: string; kind: "pendingBubble" }; - -function resolveNearestScrollViewport(element: HTMLElement | null) { - return element?.closest("[data-scroll-viewport]") as HTMLDivElement | null; -} - -function LiveStatusFooter(props: { status: string; isCompaction?: boolean }) { - const { status, isCompaction = false } = props; - return ( -
- {isCompaction ? ( - - ) : status === VIBING_STATUS ? ( - - ) : ( - {status} - )} -
- ); -} - -function shouldShowLiveStatusForRounds(rounds: GatewayTranscriptRound[]) { - const activeRound = rounds[rounds.length - 1]; - if (!activeRound) { - return true; - } - const visibleToolKeys = new Set( - getRoundToolTrace(activeRound).map((item) => `${item.toolCall.id}\u0000${item.toolCall.name}`), - ); - - for (let index = activeRound.blocks.length - 1; index >= 0; index -= 1) { - const block = activeRound.blocks[index]; - if (!block) { - continue; - } - if (block.kind === "tool") { - if (visibleToolKeys.has(`${block.item.toolCall.id}\u0000${block.item.toolCall.name}`)) { - return true; - } - continue; - } - if (block.kind === "hostedSearch") { - return false; - } - if (block.text.trim() === "") { - continue; - } - return block.kind !== "text"; - } - - return true; -} - -function HistoryLoadingState(props: { title?: string }) { - const title = props.title?.trim(); - return ( -
-
-
-
- -
-
- 正在加载会话历史 -
- {title ? ( -
- {title} -
- ) : null} -
-
-
- ); -} - -function CheckpointCard(props: { - item: Extract; - readOnly?: boolean; -}) { - const { item, readOnly = false } = props; - const [expanded, setExpanded] = useState(false); - const isExpanded = expanded; - const messageCountLabel = - item.coveredMessageCount > 0 ? `${item.coveredMessageCount} 条消息` : "已压缩"; - const headerContent = ( - <> -
- -
- -
-
- - 上下文检查点 - - - {messageCountLabel} - -
-
- {item.generatedBy.providerId} · {item.generatedBy.model} -
-
- - - - ); - - return ( -
- - ); -} - -function useGatewayUploadedImagePreview( - file?: PendingUploadedFile, - workspaceRoot?: string, - loader?: UploadedImagePreviewLoader, -) { - const normalizedWorkspaceRoot = typeof workspaceRoot === "string" ? workspaceRoot.trim() : ""; - const absolutePath = typeof file?.absolutePath === "string" ? file.absolutePath.trim() : ""; - const cacheKey = file ? getUploadedImagePreviewCacheKey(normalizedWorkspaceRoot, file) : ""; - const [imageSrc, setImageSrc] = useState(() => { - if (!file || !normalizedWorkspaceRoot) return null; - return readUploadedImagePreviewCache(normalizedWorkspaceRoot, file); - }); - - useEffect(() => { - if (!file || !cacheKey || !normalizedWorkspaceRoot) { - setImageSrc(null); - return; - } - - const cached = readUploadedImagePreviewCache(normalizedWorkspaceRoot, file); - if (cached !== undefined) { - setImageSrc(cached); - return; - } - if (!absolutePath || !loader) { - setImageSrc(null); - return; - } - - let cancelled = false; - setImageSrc(undefined); - void loadUploadedImagePreview({ - workspaceRoot: normalizedWorkspaceRoot, - file, - loader, - }).then((value) => { - if (!cancelled) { - setImageSrc(value); - } - }); - return () => { - cancelled = true; - }; - }, [absolutePath, cacheKey, file, loader, normalizedWorkspaceRoot]); - - return { - imageSrc: imageSrc ?? null, - isLoading: Boolean(cacheKey && absolutePath && loader) && imageSrc === undefined, - }; -} - -function GatewayUserImageAttachmentCard(props: { - file: PendingUploadedFile; - imageSrc: string | null; - isLoading: boolean; - compact: boolean; - onRemove?: (relativePath: string) => void; - removeLabel?: string; - previewLabel: string; - closePreviewLabel: string; -}) { - const { - file, - imageSrc, - isLoading, - compact, - onRemove, - removeLabel, - previewLabel, - closePreviewLabel, - } = props; - const [previewOpen, setPreviewOpen] = useState(false); - const labeledPreview = `${previewLabel}: ${file.fileName}`; - const FallbackIcon = getUploadedFileTypeIcon(file); - const previewSlides = useMemo( - () => - imageSrc - ? [ - { - src: imageSrc, - alt: file.fileName, - title: file.fileName, - }, - ] - : [], - [file.fileName, imageSrc], - ); - return ( -
- {onRemove ? ( - - ) : null} - {imageSrc ? ( - <> - - {previewOpen ? ( - setPreviewOpen(false)} - /> - ) : null} - - ) : ( -
-
- {isLoading ? null : } -
-
- )} -
-
-
- {file.fileName} -
-
- - {formatUploadedFileSize(file.sizeBytes)} - -
-
- ); -} - -function GatewayUserFileAttachmentCard(props: { - file: PendingUploadedFile; - onRemove?: (relativePath: string) => void; - removeLabel?: string; - compact: boolean; -}) { - const { file, onRemove, removeLabel, compact } = props; - const TypeIcon = getUploadedFileTypeIcon(file); - return ( -
-
- -
-
-
- {file.fileName} -
-
- {formatUploadedFileSize(file.sizeBytes)} -
-
- {onRemove ? ( - - ) : null} -
- ); -} - -function GatewayUserAttachmentCard(props: { - file: PendingUploadedFile; - workspaceRoot?: string; - onLoadUploadedImagePreview?: UploadedImagePreviewLoader; - compactImageLayout: boolean; - compactFileLayout: boolean; - onRemove?: (relativePath: string) => void; - removeLabel?: string; - previewLabel: string; - closePreviewLabel: string; -}) { - const { - file, - workspaceRoot, - onLoadUploadedImagePreview, - compactImageLayout, - compactFileLayout, - onRemove, - removeLabel, - previewLabel, - closePreviewLabel, - } = props; - const shouldPreviewImage = - file.kind === "image" && typeof workspaceRoot === "string" && workspaceRoot.trim(); - const { imageSrc, isLoading } = useGatewayUploadedImagePreview( - shouldPreviewImage ? file : undefined, - shouldPreviewImage ? workspaceRoot : undefined, - onLoadUploadedImagePreview, - ); - - if (shouldPreviewImage) { - return ( - - ); - } - - return ( - - ); -} - -function GatewayUserAttachmentCards(props: { - files: PendingUploadedFile[]; - workspaceRoot?: string; - onLoadUploadedImagePreview?: UploadedImagePreviewLoader; - onRemove?: (relativePath: string) => void; - removeLabel?: string; -}) { - const { files, workspaceRoot, onLoadUploadedImagePreview, onRemove, removeLabel } = props; - const { t } = useLocale(); - if (files.length === 0) return null; - - const imageFiles = files.filter((file) => file.kind === "image"); - const otherFiles = files.filter((file) => file.kind !== "image"); - const compactImageLayout = imageFiles.length > 1; - const compactFileLayout = otherFiles.length > 1; - const previewLabel = t("chat.upload.previewImage"); - const closePreviewLabel = t("chat.upload.closePreview"); - - return ( -
- {imageFiles.length > 0 ? ( -
- {imageFiles.map((file) => ( - - ))} -
- ) : null} - {otherFiles.length > 0 ? ( -
- {otherFiles.map((file) => ( - - ))} -
- ) : null} -
- ); -} - -function splitUserAttachmentsForDisplay(files: PendingUploadedFile[], text: string) { - const pastedTextReferences = parsePastedTextDisplayReferences(text); - if (pastedTextReferences.length === 0 || files.length === 0) { - return { - visibleFiles: files, - pastedTextFiles: [], - }; - } - - const pastedTextPaths = new Set(pastedTextReferences.map((reference) => reference.relativePath)); - const pastedTextFiles: PendingUploadedFile[] = []; - const visibleFiles: PendingUploadedFile[] = []; - - for (const file of files) { - if (pastedTextPaths.has(file.relativePath)) { - pastedTextFiles.push(file); - } else { - visibleFiles.push(file); - } - } - - return { - visibleFiles, - pastedTextFiles, - }; -} - -function formatMessageTimestamp(timestamp: number | undefined, now = new Date()): string { - if (!timestamp || !Number.isFinite(timestamp) || timestamp <= 0) return ""; - const date = new Date(timestamp); - const pad = (value: number) => String(value).padStart(2, "0"); - const time = `${pad(date.getHours())}:${pad(date.getMinutes())}`; - if ( - date.getFullYear() === now.getFullYear() && - date.getMonth() === now.getMonth() && - date.getDate() === now.getDate() - ) { - return time; - } - const monthDay = `${pad(date.getMonth() + 1)}-${pad(date.getDate())}`; - if (date.getFullYear() === now.getFullYear()) { - return `${monthDay} ${time}`; - } - return `${date.getFullYear()}-${monthDay} ${time}`; -} - -function GatewayUserMessageBubbleBody(props: { - text: string; - attachments: PendingUploadedFile[]; - workspaceRoot?: string; - onLoadUploadedImagePreview?: UploadedImagePreviewLoader; - loadCommitDetails?: CommitDetailsLoader; -}) { - const { text, attachments, workspaceRoot, onLoadUploadedImagePreview, loadCommitDetails } = props; - const { visibleFiles, pastedTextFiles } = splitUserAttachmentsForDisplay(attachments, text); - - return ( -
- - {text ? ( - - ) : null} -
- ); -} - -const MIN_EDIT_BUBBLE_HEIGHT_PX = 72; - -function resizeEditableTextarea(textarea: HTMLTextAreaElement | null) { - if (!textarea) { - return; - } - textarea.style.height = "0px"; - textarea.style.height = `${Math.max(textarea.scrollHeight, MIN_EDIT_BUBBLE_HEIGHT_PX)}px`; -} - -const EditableUserMessageBubble = memo(function EditableUserMessageBubble(props: { - initialText: string; - attachments: PendingUploadedFile[]; - workspaceRoot?: string; - onLoadUploadedImagePreview?: UploadedImagePreviewLoader; - onCancel: () => void; - onSubmit: (text: string, attachments: PendingUploadedFile[]) => void; -}) { - const { - initialText, - attachments, - workspaceRoot, - onLoadUploadedImagePreview, - onCancel, - onSubmit, - } = props; - const { t } = useLocale(); - const [draftText, setDraftText] = useState(initialText); - const [draftAttachments, setDraftAttachments] = useState(attachments); - const textareaRef = useRef(null); - - useLayoutEffect(() => { - const textarea = textareaRef.current; - if (!textarea) { - return; - } - resizeEditableTextarea(textarea); - textarea.focus(); - textarea.selectionStart = textarea.selectionEnd = textarea.value.length; - }, []); - - useEffect(() => { - setDraftAttachments(attachments); - }, [attachments]); - - useLayoutEffect(() => { - resizeEditableTextarea(textareaRef.current); - }, [draftText]); - - // A large paste is stored as an uploaded text file *plus* a - // "[Pasted text N: path]" marker inlined into the message text (rendered - // as a chip once sent, see GatewayUserMessageBubbleBody above). Editing - // must hide that same file's attachment card while its marker is still - // present in the text, otherwise the paste shows up twice: once as a - // card, once as raw marker text in the textarea below. The full - // (unfiltered) list — including pasted-text files — is still what gets - // submitted, so nothing is lost on resend; only the card list is - // narrowed for display. - const visibleAttachments = useMemo( - () => splitUserAttachmentsForDisplay(draftAttachments, draftText).visibleFiles, - [draftAttachments, draftText], - ); - - const canSubmit = draftText.trim().length > 0 || draftAttachments.length > 0; - - return ( -
- { - setDraftAttachments((current) => - current.filter((file) => file.relativePath !== relativePath), - ); - }} - removeLabel={t("settings.delete")} - /> -