Skip to content

feat(serve): add a required external tool guard provider - #8125

Merged
yiliang114 merged 15 commits into
QwenLM:mainfrom
chiga0:feat/external-tool-guard-provider
Aug 4, 2026
Merged

feat(serve): add a required external tool guard provider#8125
yiliang114 merged 15 commits into
QwenLM:mainfrom
chiga0:feat/external-tool-guard-provider

Conversation

@chiga0

@chiga0 chiga0 commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Adds an opt-in external pre-execution policy provider for managed qwen serve ACP deployments. The new process-start mode is off by default. When explicitly set to required, the daemon completes an authenticated, versioned handshake with an origin-only loopback HTTP(S) provider and requires one bounded, no-retry allow/deny decision immediately before every supported top-level tool executor.

Required mode keeps provider credentials in the daemon, validates channel-owned Session and active Prompt identity before forwarding a decision request, requires each ACP child to attest that the executor callback is installed before Session creation, and fails closed on cancellation, timeout, transport failure, malformed responses, stale identity, or explicit denial. Unsupported nested/delegated agent entry points are rejected, and guarded MCP invocations are not reconnected or replayed after an ambiguous transport failure.

The provider request carries runtime-owned sessionId and promptId, a runtime-accepted toolCallId correlation label, the canonical tool name, and final arguments. Existing tool lifecycle events remain the result-observation channel; this PR does not add a result callback, audit store, or product-specific Task API.

This is Milestone 2 of #8102 and now builds on the executor-boundary host Guard merged in #8032. The branch was rebased after #8032 merged, so this PR contains only its three provider/managed-ACP commits.

Why it's needed

Managed and unattended Agent-to-Agent deployments need a host-owned decision at the last safe point before a tool side effect. Existing permission rules and hooks remain useful, but they do not provide a mandatory external, machine-verifiable, fail-closed decision over the final invocation identity and arguments. This PR supplies that narrow enforcement path without changing standalone CLI, ordinary ACP, or default daemon behavior.

Reviewer Test Plan

How to verify

  1. Start qwen serve without the new mode and confirm that no provider request occurs and /capabilities omits external_tool_guard.
  2. Start a loopback provider that implements the v1 handshake and prepare contract, then start qwen serve with required mode and its token. Confirm startup fails for invalid configuration, a failed/incompatible handshake, or an ACP child that does not acknowledge enforcement.
  3. Submit a managed Prompt whose tool has an observable one-shot effect. For allow, confirm exactly one handshake and one prepare request, matching Session/Prompt/tool-call correlation, one executor effect, and a completed terminal event.
  4. Repeat with explicit deny, provider timeout, and Prompt cancellation. Confirm the prepare request is not retried, the terminal state is failed/cancelled as applicable, and the executor effect count remains zero.
  5. Confirm provider credentials are absent from ACP child, relaunch, channel-worker, hook/tool, and executor environments. Confirm nested/delegated agent entry points are rejected in required mode and a guarded MCP connection error is not reconnected or replayed.

Exact rebased head ac9e93bb7 local evidence on upstream main@2d2bdab2b:

  • Core Guard/scheduler/speculation/MCP: 4 files, 418 tests passed.
  • ACP bridge: 3 files, 593 tests passed.
  • CLI affected suite: 11 files, 1,852 tests passed.
  • Core, ACP bridge, and CLI build/typecheck: passed.
  • Changed-file ESLint and Prettier checks: passed.
  • npm run check:serve-fast-path-bundle: passed.
  • Real built daemon/private ACP/loopback-provider E2E: off, allow, deny, timeout, and cancel passed. Off made zero provider requests; allow executed exactly once; deny/timeout/cancel executed zero times; correlation, final arguments, terminal outcomes, no-retry behavior, and credential isolation all passed.

Evidence (Before & After)

N/A — non-UI runtime and protocol change.

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

macOS with Node.js 24. The required-mode E2E used a real built daemon, private ACP child, loopback fake v1 provider, fake model, and an observable executor effect counter.

Risk & Scope

  • Main risk or tradeoff: In required mode, provider and child-attestation availability intentionally become tool availability; failures deny rather than fall back. A top-level background shell/monitor invocation authorizes only its start, not continuous execution or completion.
  • Not validated / out of scope: Hooks and slash-command actions run outside this tool-executor boundary; explicit daemon management APIs retain their existing authorization; nested/delegated AgentCore execution, result callbacks, audit storage, semantic sandboxing, and a business Task/Plan/grant protocol are not included.
  • Breaking changes / migration notes: None by default. The feature is dark unless --external-tool-guard-mode=required is explicitly selected. Managed deployments adopting required mode must provide the loopback v1 provider and should disable or independently govern hooks and slash commands when they require an all-effects boundary.

Linked Issues

References #8102 and builds on merged #8032.

中文说明

本 PR 做了什么

为受管 qwen serve ACP 部署增加一个显式开启的外部执行前策略 Provider。新的进程启动模式默认是 off。只有显式设置为 required 时,daemon 才会与仅允许回环地址的 HTTP(S) Provider 完成带认证、带版本的握手,并要求每个受支持的顶层工具在真正进入执行器之前进行一次有界、无重试的允许/拒绝决策。

required 模式把 Provider 凭据保留在 daemon 内;转发决策请求前校验通道拥有的 Session 和当前活跃 Prompt;在创建 Session 前要求每个 ACP 子进程证明执行器回调已经安装;遇到取消、超时、传输失败、非法响应、过期身份或明确拒绝时全部失败关闭。不支持的嵌套/委派 Agent 入口会被拒绝,受 Guard 保护的 MCP 调用遇到不确定传输失败后不会自动重连或重放。

Provider 请求携带运行时拥有的 sessionIdpromptId、由运行时接受的 toolCallId 关联标签、规范化工具名和最终参数。结果观察继续使用已有工具生命周期事件;本 PR 不增加结果回调、审计存储或产品特定的 Task API。

这是 #8102 的里程碑 2,现在基于已由 #8032 合入的执行器边界 Host Guard。#8032 合入后已重新 rebase,因此本 PR 只包含自身的 3 个 Provider/受管 ACP 提交。

为什么需要

受管和无人值守的 Agent-to-Agent 部署需要在工具副作用发生前的最后安全点,由宿主做出决策。现有权限规则和 hooks 仍然有价值,但无法针对最终调用身份和参数提供强制、外部、机器可验证且失败关闭的决策。本 PR 提供这一窄范围执行路径,同时不改变独立 CLI、普通 ACP 和默认 daemon 行为。

Reviewer 测试计划

如何验证

  1. 不使用新模式启动 qwen serve,确认没有 Provider 请求,且 /capabilities 不包含 external_tool_guard
  2. 启动实现 v1 握手与 prepare 协议的回环 Provider,再使用 required 模式和 token 启动 qwen serve。确认非法配置、失败/不兼容握手、或 ACP 子进程未确认 Guard 时启动/Session 创建失败。
  3. 提交一个工具具有可观察单次副作用的受管 Prompt。Provider 允许时,确认只有一次握手和一次 prepare,请求中的 Session/Prompt/tool-call 关联一致,执行器只产生一次副作用,并产生 completed 终态事件。
  4. 分别测试明确拒绝、Provider 超时和 Prompt 取消。确认 prepare 不重试,终态按场景为 failed/cancelled,且执行器副作用次数始终为零。
  5. 确认 Provider 凭据不会进入 ACP 子进程、relaunch、channel worker、hook/tool 或执行器环境。确认 required 模式拒绝嵌套/委派 Agent 入口,并且受 Guard 保护的 MCP 连接错误不会被自动重连或重放。

在上游 main@2d2bdab2b 上对 rebase 后精确 head ac9e93bb7 的本地证据:

  • Core Guard/scheduler/speculation/MCP:4 个文件、418 个测试通过。
  • ACP bridge:3 个文件、593 个测试通过。
  • CLI 受影响测试:11 个文件、1,852 个测试通过。
  • Core、ACP bridge 和 CLI 的 build/typecheck:通过。
  • 变更文件 ESLint 和 Prettier:通过。
  • npm run check:serve-fast-path-bundle:通过。
  • 真实构建 daemon/私有 ACP/回环 Provider E2E:offallowdenytimeoutcancel 全部通过。Off 发起 0 次 Provider 请求;allow 只执行 1 次;deny/timeout/cancel 执行 0 次;关联字段、最终参数、终态、无重试和凭据隔离断言全部通过。

前后证据

N/A —— 非 UI 的运行时与协议变更。

测试平台

OS 状态
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

环境

macOS、Node.js 24。required 模式 E2E 使用真实构建后的 daemon、私有 ACP 子进程、回环 fake v1 Provider、fake model 和可观察的执行器副作用计数器。

风险与范围

  • 主要风险或取舍:在 required 模式下,Provider 和子进程 attestation 的可用性会有意成为工具可用性的前提;失败时拒绝执行,不会降级放行。顶层后台 shell/monitor 调用只对启动动作授权,不持续授权其运行或完成。
  • 未验证/范围外:hooks 和 slash-command 动作不经过该工具执行器边界;显式 daemon 管理 API 继续使用现有授权;嵌套/委派 AgentCore、结果回调、审计存储、语义沙箱以及业务 Task/Plan/grant 协议不在本 PR 范围内。
  • 破坏性变更/迁移说明:默认无破坏性变更。只有显式选择 --external-tool-guard-mode=required 才会启用。采用 required 模式的受管部署必须提供回环 v1 Provider;若要求覆盖所有副作用,还应禁用或独立治理 hooks 和 slash commands。

关联 Issue

关联 #8102,并基于已合入的 #8032

@chiga0

chiga0 commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator Author

Managed ACP external Tool Guard E2E report

Commit: fd1a5a9fd

Environment: macOS, Node.js 24, built qwen serve, real private ACP child, loopback fake v1 provider, and a deterministic test executor with an observable effect counter.

Command: npx tsx .qwen/e2e-tests/pr2-external-tool-guard.mts

Scenario Provider traffic Prompt outcome Executor effect
off 0 requests completed / end_turn exactly 1
allow 1 handshake + 1 prepare completed / end_turn exactly 1
deny 1 handshake + 1 prepare failed / end_turn 0
timeout 1 handshake + 1 prepare, no retry failed / end_turn 0
cancel 1 handshake + 1 prepare, no retry failed / cancelled 0

All five scenarios passed the following assertions:

  • capability presence/absence matched mode;
  • handshake and prepare counts matched the contract;
  • bearer authorization was present only on provider requests;
  • sessionId, promptId, and toolCallId matched the existing runtime lifecycle;
  • the provider received the final canonical tool arguments;
  • terminal Prompt and tool outcomes matched the scenario;
  • provider token and private activation marker were absent from the executor environment.

Additional final gates:

  • ACP bridge: 3 files, 583 tests passed.
  • CLI affected suite with file parallelism disabled: 9 files, 1724 tests passed.
  • Core executor/speculation/MCP: 3 files, 403 tests passed.
  • Repository build, typecheck, lint, staged diff check, and Prettier check passed.

The local E2E plan and harness are intentionally kept under the repository's ignored .qwen/e2e-tests evidence area; this comment records the reproducible behavior and exact observed outcomes without adding test-only daemon fixtures to the product change.

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

Built the PR base vs this PR head 9ab84a0, drove a fixed endpoint set against each, and diffed the JSON responses. Only fields that changed are shown.

No response changes against the PR base across 4 scenario(s).

Qwen Code · serve A/B

@chiga0

chiga0 commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator Author

CI follow-up for commit 4628195

The Ubuntu Test failure was isolated to the serve pre-listen bundle-closure gate, not to Guard behavior. The new serve entry imported Guard constants from acp-bridge/status; that module has runtime Core dependencies, so the static import expanded the startup closure.

This follow-up moves the three protocol/env constants into a zero-runtime-dependency acp-bridge/externalToolGuard subpath and updates imports only. It does not change the protocol, opt-in default, provider behavior, or user-facing surface.

Local evidence at this commit:

  • npm run build -- --cli-only: PASS
  • npx cross-env DEV=true npm run bundle: PASS
  • node scripts/check-serve-fast-path-bundle.js: PASS
  • npm run typecheck: PASS
  • npm run lint:ci: PASS
  • acpAgent.test.ts: 350/350 PASS
  • affected ACP bridge tests: 492/492 PASS
  • affected CLI tests: 1724/1724 PASS

The pushed commit now triggers the upstream matrix again.

@chiga0

chiga0 commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up for the Ubuntu Test failure at 462819593:

The full suite completed with 15,879 passing tests and two failures. Both failures were exhaustive test declarations introduced by the guard startup surface, not runtime behavior regressions:

  1. fast-path.test.ts now records that the explicit --external-tool-guard-* options intentionally fall back to the full yargs parser. With no guard option, the existing fast path and default-off behavior are unchanged.
  2. process-env-guard.test.ts now accounts for the single computed read of EXTERNAL_TOOL_GUARD_TOKEN_ENV in the daemon launcher.

Fixed in e70e8b4b4.

Local verification:

  • npx vitest run src/serve/fast-path.test.ts src/serve/process-env-guard.test.ts: 2 files, 72/72 tests passed
  • npx prettier --check src/serve/fast-path.test.ts src/serve/process-env-guard.test.ts: passed

A new full GitHub Actions run is now pending for the exact head.

@chiga0
chiga0 force-pushed the feat/external-tool-guard-provider branch from e70e8b4 to ac9e93b Compare August 3, 2026 02:21
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration.

中文

请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动压缩(squash)为单个提交。

@chiga0

chiga0 commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased after #8032 merged.

  • Old stacked head: e70e8b4b4
  • New head: ac9e93bb7
  • Base used for rebase: main@2d2bdab2b
  • PR history now contains only the three external-provider commits; the two old feat(core): add a host tool invocation guard #8032 commits and its duplicated Core diff are gone.

Rebase conflict audit:

  • Preserved feat(core): add a host tool invocation guard #8032's final Guard debug/fail-closed behavior while adding immutable managed invocation context.
  • Preserved the current ACP duplicate-session fail-safe argument and injected the host Guard after it.
  • Preserved current serve memory/project-scope additions and updated the affected test expectation for the new argument order.

Exact-head local gates:

  • Core focused suite: 418/418 PASS
  • ACP bridge focused suite: 593/593 PASS
  • CLI affected suite: 1,852/1,852 PASS
  • Core/ACP/CLI build and typecheck: PASS
  • Changed-file ESLint and Prettier: PASS
  • Serve fast-path bundle closure: PASS
  • Real daemon/private ACP/loopback Provider E2E (off, allow, deny, timeout, cancel): PASS; off made zero provider requests, allow executed once, and deny/timeout/cancel executed zero times.

The PR remains Draft until the new exact-head GitHub checks complete.

@chiga0
chiga0 marked this pull request as ready for review August 3, 2026 03:09
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR — this is a carefully scoped piece of work, and it shows.

  • Template: complete ✓ (including the bilingual body and a thorough Reviewer Test Plan).
  • Problem: this is Milestone 2 of the tracked proposal proposal(core): deterministic tool-execution boundaries for a trustworthy agent runtime #8102, building on the executor-boundary guard merged in feat(core): add a host tool invocation guard #8032 — so it is not a fix in search of a bug, it is a tracked feature direction. One note: proposal(core): deterministic tool-execution boundaries for a trustworthy agent runtime #8102 itself is still open with the need-discussion label, so this PR is landing for review while the umbrella proposal discussion has not formally concluded.
  • Direction: aligned with the managed/enterprise qwen serve surface the project has been investing in, and the merged feat(core): add a host tool invocation guard #8032 gives it a legitimate hook point. No direct reference in the upstream Claude Code CHANGELOG, but that is expected — this is a Qwen-specific managed-ACP capability, and the tracked issue plus the merged foundation are the relevant direction signals here.
  • Size: core paths are touched (packages/core/src/** plus cross-package wiring through acp-bridge and cli). Breakdown: 898 production-logic lines (888 added + 10 deleted), 1302 test lines, 504 docs lines. Since this is a feat touching core at 500+ production lines, it is escalated to the maintainer for awareness per the core-module gate — not blocked on size, but a human owner of the daemon/ACP surface should be in the loop before merge.
  • Approach: the scope feels consistent with the stated goal — every edit serves the guard wiring (flags, credential capture/scrubbing, child attestation, bridge-side identity validation, loopback provider client, capability advertisement, core invocation-context plumbing, MCP no-replay, docs). No drive-by changes spotted, and the PR already shed Milestone 1 into feat(core): add a host tool invocation guard #8032, which is the right split. The one question a reviewer should keep asking is whether the v1 entry-point denylist (agent, workflow, create_sub_session, send_message, /fork, agent-backed memory) is the minimal safe boundary or a growing surface — the design doc argues the former, and I agree for v1.
  • Risk: Stage 1e matches packages/cli/src/acp-integration/ (acpAgent.ts, session/Session.ts) — a path with elevated post-merge revert correlation in this repo. That does not block anything, but it means full CI evidence is required before approval and the review below gives that area extra attention.

Moving on to code review. 🔍

中文说明

感谢贡献——这个 PR 的范围划分非常严谨。

  • 模板:完整 ✓(含双语正文和详尽的 Reviewer Test Plan)。
  • 问题:这是跟踪提案 proposal(core): deterministic tool-execution boundaries for a trustworthy agent runtime #8102 的 Milestone 2,建立在已合并的 feat(core): add a host tool invocation guard #8032(执行器边界 Guard)之上,不是无中生有的 fix。注意:proposal(core): deterministic tool-execution boundaries for a trustworthy agent runtime #8102 本身仍处于 open 且带 need-discussion 标签, umbrella 提案的讨论尚未正式收尾。
  • 方向:与项目持续投入的受管/企业级 qwen serve 方向一致,且 feat(core): add a host tool invocation guard #8032 已提供合法挂载点。上游 Claude Code CHANGELOG 无直接引用,但属预期——这是 Qwen 特有的受管 ACP 能力,方向信号来自跟踪 issue 与已合并的地基。
  • 规模:触及核心路径(packages/core/src/**,并跨 acp-bridgecli 包)。拆分:898 行生产逻辑(+888/−10)、1302 行测试、504 行文档。作为触及核心且生产行数 500+ 的 feat,按核心模块门禁升级给 maintainer 知悉——不因规模阻塞,但合并前应有 daemon/ACP 面的人类负责人参与。
  • 方案:范围与目标一致——每处改动都服务于 Guard 接线(flags、凭据捕获/清除、子进程确认、bridge 身份校验、回环 Provider 客户端、能力通告、core 调用上下文管道、MCP 不重放、文档),未发现夹带改动;Milestone 1 已拆入 feat(core): add a host tool invocation guard #8032,拆法正确。值得 reviewer 持续追问的一点:v1 的入口拒绝清单(agentworkflowcreate_sub_sessionsend_message/fork、agent 记忆)是最小安全边界还是会持续膨胀——设计文档论证了前者,v1 范围内我认同。
  • 风险:Stage 1e 命中 packages/cli/src/acp-integration/acpAgent.tssession/Session.ts)——该路径在本仓库有较高的合并后 revert 相关性。不构成阻塞,但意味着批准前必须有完整 CI 证据,下面的代码审查也会重点看这一区域。

进入代码审查 🔍

Qwen Code · qwen3.8-max-preview

Reviewed at ac9e93bb7f7375f725a55171bafc02f5841f580d · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Code review

My independent baseline for this problem — opt-in qwen serve flag defaulting off, an authenticated startup handshake against a loopback-only endpoint, one bounded no-retry prepare decision at the executor boundary #8032 created, credentials kept daemon-side, child attestation, conditional capability — is essentially what this PR builds, and in the places it goes beyond my proposal the additions are justified rather than speculative:

  • BridgeClient validates session ownership and active-prompt identity before calling the provider, then re-validates after the decision returns and discards an allow if the prompt stopped in flight — a race I had not planned for.
  • The private activation marker survives the existing relaunch path but is consumed and deleted before any tool, hook, or MCP server initializes, and loadCliConfig receives the guard through a trailing hostPolicy parameter that is deliberately unreachable from argv, settings, or env — only an embedding host can install it.
  • The provider client uses a direct http/https request with agent: false so a process-global fetch dispatcher or proxy installed by an embedding cannot reroute the security boundary, and the serve fast path simply falls back to the full yargs parser when the new flags appear (verified against the base fast-path code — no fail-open path exists).
  • Guarded MCP invocations give up the reconnect-and-retry on ambiguous transport errors, so one allow cannot authorize two execution attempts.

I walked every path that can return allow: handshake completion, strict response schema (hasExactKeys, echoed requestId/nonce, boolean-only decision, bounded denial reasons), and post-decision re-validation all gate it; everything else throws or denies. The token is scrubbed at seven distinct boundaries (cli bootstrap, gemini arg parsing, the serve handler, the serve runner, daemon worker, channel worker, ACP child spawn), each with a dedicated test. I found no critical correctness or security issues.

One non-blocking observation: the control-character/bounded-reason validation predicate is written out inline three times (provider client, BridgeClient, ACP child guard), as is the strict allow/deny shape check. It is consistent today, but a single shared helper next to the constants in the acp-bridge externalToolGuard module would keep the three copies from drifting. Worth a follow-up, not a merge blocker.

The Stage 1e high-risk area (acp-integration/acpAgent.ts, session/Session.ts) got the extra attention: the child-side guard fails closed without an invocation context, rejects the nested/delegated entries before contacting the provider, and the Session.ts change is a two-line context forward reusing an existing import. No concerns there.

Runtime flow

sequenceDiagram
    participant P1 as Host operator
    participant P2 as qwen serve daemon
    participant P3 as Loopback guard provider
    participant P4 as ACP bridge BridgeClient
    participant P5 as Private ACP child
    participant P6 as Tool executor
    P1->>P2: start with mode required
    P2->>P3: POST v1 handshake with Bearer token
    P3-->>P2: version, nonce, prepare capability
    P2->>P5: spawn with private required marker
    P5-->>P4: initialize ack required-v1
    P1->>P2: prompt
    P5->>P5: permission and PreToolUse gates
    P5->>P4: ext prepare - sessionId, promptId, toolCallId, name, args
    P4->>P4: verify owned session and active prompt
    P4->>P3: POST v1 prepare, exactly once, no retry
    P3-->>P4: allow or deny
    P4->>P4: re-verify prompt still active
    P4-->>P5: decision
    alt allow
        P5->>P6: execute final invocation once
        P5-->>P2: existing terminal tool_call_update event
    else deny, timeout, cancel, or malformed
        P5-->>P2: failed or cancelled terminal event, executor never ran
    end
Loading
Files changed (29 of 43 shown)
File What changed
packages/cli/src/serve/external-tool-guard-provider.ts New daemon-side loopback v1 client: config validation, startup handshake, one-shot prepare, strict response schema, size limits
packages/cli/src/serve/run-qwen-serve.ts Constructs the provider and blocks startup on bad config or failed handshake before listening; wires the handler into all three bridge constructions; passes the private marker via child env overrides
packages/cli/src/serve/types.ts ServeOptions.externalToolGuard, required-only shape where omission means fully off
packages/cli/src/serve/capabilities.ts Conditional external_tool_guard capability, advertised only when required mode completed its handshake
packages/cli/src/commands/serve.ts Three new serve flags; captures and deletes the token env before handing off to the runner
packages/cli/src/cli.ts Deletes the guard token on every non-serve bootstrap route
packages/cli/src/gemini.tsx Captures the private marker for the relaunch path, scrubs the token after arg parsing, threads externalToolGuardRequired into runAcpAgent
packages/cli/src/config/config.ts New trailing hostPolicy parameter on loadCliConfig, deliberately not sourced from argv, settings, or env
packages/cli/src/acp-integration/acpAgent.ts Child side: createManagedExternalToolGuard, ready acknowledgment in initialize metadata, rejection of unsupported nested entries, per-session guard injection
packages/cli/src/acp-integration/session/Session.ts Forwards the runtime invocation context into the existing guard call site
packages/cli/src/commands/channel/daemon-worker.ts Scrubs the guard token from daemon-worker env
packages/cli/src/serve/channel-worker-supervisor.ts Scrubs the guard token from channel-worker env
packages/acp-bridge/src/bridgeClient.ts Parent-side prepare handler: ownership and active-prompt validation before and after the provider call, strict result normalization
packages/acp-bridge/src/bridge.ts Rejects a channel whose child did not acknowledge required-v1; passes the handler into BridgeClient
packages/acp-bridge/src/bridgeOptions.ts ExternalToolGuard request/result/handler types and the new optional BridgeOptions field
packages/acp-bridge/src/externalToolGuard.ts Three constants: private marker env, ready meta key, token env
packages/acp-bridge/src/spawnChannel.ts Adds the guard token to the child-env scrub denylist
packages/acp-bridge/src/status.ts New private control ext-method name for prepare
packages/acp-bridge/package.json Exports the externalToolGuard subpath
packages/core/src/core/tool-invocation-guard.ts Optional invocationContext on the guard context, frozen before the guard is called
packages/core/src/core/coreToolScheduler.ts Forwards invocation context at the scheduler call site
packages/core/src/followup/speculation.ts Forwards invocation context at the speculation call site
packages/core/src/tools/mcp-tool.ts Guarded MCP invocations skip reconnect-retry after ambiguous transport errors
docs/design/2026-07-30-external-tool-guard-provider.md Design doc: safety contract, wire protocol, identity model, verification plan, non-goals
docs/users/qwen-serve.md User-facing flag documentation and deployment guidance
docs/developers/daemon/… Developer docs updates (acp-bridge options table, configuration, serve protocol note)
packages/cli/src/serve/external-tool-guard-provider.test.ts 464 new lines covering validation, handshake, allow/deny/timeout/malformed, size limits
packages/acp-bridge/src/bridgeClient.test.ts Identity validation, stale-prompt discard, foreign-session rejection, malformed-result fail-closed
packages/cli/src/acp-integration/acpAgent.test.ts Child attestation, credential scrubbing, nested-entry rejection, guard forwarding
…and 12 more files Remaining test coverage (serve args, cli bootstrap, gemini relaunch, run-qwen-serve pre-listen, capabilities, fast-path fallback, server registry, MCP no-replay, env scrubs)

Testing

Unattended CI run — I did not build or execute any PR code (per the gate rules); the evidence below is the PR's own CI on the reviewed commit, fetched via the API.

All three pull_request-event workflow runs on this head completed green: Qwen Code CI, Serve A/B, and SDK Java. The Serve A/B job is the meaningful default-off regression signal here — it builds base vs this head and diffs responses over a fixed endpoint set, so the off-mode parity claim is not just the author's word. The only in-flight check-runs (triage, review-pr) are pull_request_target bot orchestrations, not PR CI.

What CI does not settle: the built-daemon required-mode E2E described in the PR body (allow executes exactly once; deny/timeout/cancel execute zero times; no retry; credential isolation across child, relaunch, channel-worker, hook, and executor environments). Those numbers are the author's self-reported results on macOS, not independently re-run here.

Sandboxed verification would settle this: @qwen-code /verify — the central claim is behavioural (required mode fails closed end-to-end on the real built daemon), and the unit suite plus Serve A/B, while extensive, cannot prove the live deny/timeout/cancel executor counts. This is a fork PR, so it would be a sponsored run: a maintainer's @qwen-code /verify comment approves the head it is written against, and that run carries a pre-execution risk screen plus a full workspace wipe before any PR code executes — the resulting report should still be read with the same skepticism as the fork's own CI logs, since a crafted PR can shape what a report says even when the sandbox bounds what it can do.

Final CI results for ac9e93b (auto-updated by the triage finalize job after CI completed):

Check Conclusion
Classify PR ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
macos-latest / Java 21 ✅ success
Real daemon E2E / Java 11 ✅ success
route ✅ success
Serve A/B (ubuntu-latest, Node 22.x) ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success
ubuntu-latest / Java 11 ✅ success
ubuntu-latest / Java 17 ✅ success
ubuntu-latest / Java 21 ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
windows-latest / Java 21 ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

Real-scenario (tmux) testing: N/A on this path — this is an unattended CI run, and live-behaviour signal comes from the sandboxed lane above instead.

中文说明

代码审查:我独立构想的基线方案(默认关闭的 serve 开关、仅回环端点的认证握手、在 8032 执行器边界做一次有界无重试的 prepare 决策、凭据留在 daemon、子进程确认、条件能力通告)与本 PR 基本一致;PR 超出基线的部分也都是有依据的——BridgeClient 在调用 Provider 前后双重校验会话归属与活跃 Prompt(允许决策返回时若 Prompt 已停止则作废)、私有激活标记可穿越 relaunch 但在任何工具/hook/MCP 初始化前被消费删除、loadCliConfig 的 hostPolicy 参数刻意不可从 argv/settings/env 驱动、Provider 客户端用 agent: false 的直连请求避免被全局 dispatcher/代理劫持、serve fast path 遇到新 flag 直接回退完整解析器(已核对基线代码,不存在 fail-open 路径)、受 Guard 的 MCP 调用在传输歧义错误后不再重连重试。我逐条走查了所有能返回 allow 的路径,未发现关键正确性或安全问题。

唯一非阻塞观察:控制字符/有界拒绝理由的校验谓词在三处内联重复(Provider 客户端、BridgeClient、ACP 子进程 Guard),allow/deny 形状检查同理;建议后续抽到 acp-bridge externalToolGuard 模块中共享,避免三处漂移。不构成合并阻塞。

Stage 1e 高风险区(acp-integration)已重点查看:子进程 Guard 在无调用上下文时 fail-closed、嵌套/委托入口在联系 Provider 前即被拒绝、Session.ts 只是两行上下文转发且复用已有导入。无顾虑。

测试:无人值守 CI 运行——未构建或执行任何 PR 代码。本 commit 上三个 pull_request 事件的工作流全部绿灯(Qwen Code CI、Serve A/B、SDK Java)。Serve A/B 是默认关闭路径回归的有效信号(base 与本 PR 对照构建并 diff 固定端点集)。CI 无法覆盖的是 PR 正文所述的真实 daemon required 模式 E2E(allow 恰好执行一次、deny/timeout/cancel 零执行、无重试、凭据隔离)——那是作者的自报结果(macOS),未在此独立复跑。沙箱验证可补齐:@qwen-code /verify(fork PR 为赞助运行,由 maintainer 评论触发,带预执行风险筛查与完整工作区清除;报告仍应以审视 fork CI 日志的怀疑态度阅读)。仍在进行的 triage/review-pr 检查属于 pull_request_target 机器人编排,不是 PR CI。实时场景(tmux)测试:CI 路径不适用。

Qwen Code · qwen3.8-max-preview

Reviewed at ac9e93bb7f7375f725a55171bafc02f5841f580d · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — clean review across every stage, but the core-module size escalation is pure policy: a fork feat at ~900 production lines spanning three packages needs a maintainer's sign-off, and that cap — not doubt about the code — is what keeps this from approving.

Stepping back honestly: the PR's approach matches the independent proposal I wrote before reading the diff, and where it diverges it diverges upward — the post-decision re-validation in the bridge, the relaunch-surviving private marker, the hostPolicy seam that config and env cannot reach, the MCP no-replay rule. I looked for the materially simpler path and did not find one; the milestone split already shipped the smaller half as #8032, and what remains here is the coherent rest. Every change in the diff serves the stated goal. Six months from now, the design doc plus 1300 lines of targeted tests would make this a thank-the-author file set, with the one drift risk being the thrice-inlined validation predicate noted in Stage 2.

What keeps this at 3/5 rather than higher, in order of weight:

  1. Policy cap. Stage 0 escalated this for maintainer awareness (feat touching core at 500+ production lines), so approval is not mine to give.
  2. Direction is tracked but not concluded. The umbrella proposal proposal(core): deterministic tool-execution boundaries for a trustworthy agent runtime #8102 is still open with need-discussion; Milestone 1 merging shows real interest, but a maintainer should confirm the project wants Milestone 2 — an opt-in enterprise enforcement surface with its own wire protocol — in now.
  3. The central behavioural claim rests on the author's E2E. CI is green and Serve A/B pins default-off parity, but the required-mode fail-closed end-to-end behaviour is exactly what the named @qwen-code /verify sponsored run would settle before merge.

⏸️ Deferring to maintainers@wenshao @tanzhenxin @yiliang114 @LaZzyMan @doudouOUC (owners of the touched /packages/core/ paths): needs a human call on (a) whether #8102's direction is concluded enough to land Milestone 2, and (b) the daemon/ACP security-surface review itself. No blocking issues found in the diff — this is an escalation, not a rejection.

中文说明

置信度:3/5——各阶段审查都干净,但核心模块规模升级是纯策略性的:一个约 900 行生产代码、跨三个包的 fork feat 需要 maintainer 签核;是这个上限(而非对代码的怀疑)阻止了自动批准。

诚实复盘:PR 方案与我读 diff 前独立写出的提案一致,且所有偏离都是向上的——bridge 的决策后二次校验、可穿越 relaunch 的私有标记、config/env 不可触达的 hostPolicy 接缝、MCP 不重放规则。我找过更简路径,没找到:里程碑拆分已把较小的一半作为 8032 合入,这里是连贯的剩余部分。diff 中每处改动都服务于既定目标,无夹带。六个月后,设计文档加 1300 行针对性测试会让人感谢作者;唯一的漂移风险是 Stage 2 提到的三处内联校验谓词。

停在 3/5 的原因(按权重):其一,策略上限——Stage 0 已因规模升级给 maintainer 知悉,批准权不在我;其二,方向已跟踪但未收尾——umbrella 提案 8102 仍 open 且带 need-discussion,Milestone 1 合入说明有真实兴趣,但项目是否现在要 Milestone 2(一个自带 wire protocol 的可选企业强制面)应由 maintainer 确认;其三,核心行为声明目前依赖作者的 E2E——CI 全绿且 Serve A/B 钉住了默认关闭的等价性,但 required 模式端到端 fail-closed 行为正是已点名的 @qwen-code /verify 赞助运行可以补齐的。

已转交 maintainer(所触及 /packages/core/ 路径的 owner):需要人类判断 8102 方向是否足以落地 Milestone 2,以及 daemon/ACP 安全面本身的评审。diff 中未发现阻塞问题——这是升级,不是拒绝。

Qwen Code · qwen3.8-max-preview

Reviewed at ac9e93bb7f7375f725a55171bafc02f5841f580d · re-run with @qwen-code /triage

@chiga0

chiga0 commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover

@qwen-code-dev-bot qwen-code-dev-bot added the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Aug 3, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This is a fork PR, so the first round comes from the next scheduled scan (usually within minutes). Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本 PR 来自 fork,首轮处理将由下一次定时扫描执行(通常几分钟内)。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@qwen-code-dev-bot

qwen-code-dev-bot commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

AutoFix round 4 finishedview run. See this round's report below.

中文说明

AutoFix 第 4 轮已完成 —— 查看运行。本轮报告见下方。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

🤖 Autofix review round — no actionable feedback

Head reviewed: ac9e93bb7 · Base: main · Outcome: no code changes this round.

Feedback triage

This round surfaced no reviews, no inline comments, and no failed checks. The five issue-level comments since the last evaluation are all informational and require no code change:

Comment Author Disposition
Managed ACP external Tool Guard E2E report (5 scenarios, all assertions passed) @chiga0 Evidence record — no action requested
serve daemon A/B: no response changes against base across 4 scenarios @qwen-code-ci-bot Passing regression signal — no action
CI follow-up isolating the Ubuntu failure to the serve bundle-closure gate, fixed by moving constants to the zero-runtime externalToolGuard subpath @chiga0 Fix already committed in this PR's history
Follow-up on the two exhaustive-test failures in fast-path.test.ts / process-env-guard.test.ts @chiga0 Fixed in e70e8b4b4, already in this PR's history
Rebase onto main@2d2bdab2b after #8032 merged; new head ac9e93bb7 @chiga0 Status update — checked-out head matches

Check state

All completed checks are green (22 success, 23 skipped). The single in-progress item is the review-pr bot orchestration (pull_request_target), not PR CI.

Pending items for maintainers (not autofix work)

  • The qwen-triage review found no critical correctness or security issues and no blocking findings in the diff. It escalated for a human call on two policy points: (a) the core-module size gate (a feat at ~900 production lines touching packages/core needs maintainer sign-off), and (b) whether umbrella proposal proposal(core): deterministic tool-execution boundaries for a trustworthy agent runtime #8102 — still open with need-discussion — is concluded enough to land Milestone 2. Both are product/maintainer decisions, so they ride along untouched here.
  • The triage stage-2 review noted one explicitly non-blocking observation (the control-character/bounded-reason validation predicate is inlined in three places; a shared helper could prevent drift). Per its own wording — "worth a follow-up, not a merge blocker" — and AGENTS.md's simplicity-first rule, this round does not grow the diff for it; it is recorded here so it is not silently dropped.

No commit was made and no verification commands were run, since nothing was changed.

中文说明

🤖 Autofix 审查轮次 — 无可处理反馈

审查的 head: ac9e93bb7 · Base: main · 结果: 本轮无代码改动。

反馈分诊

本轮没有浮现任何 review、inline comment 或失败检查。自上次评估以来的 5 条 issue 级评论均为信息性内容,无需代码改动:

评论 作者 处置
Managed ACP external Tool Guard E2E 报告(5 个场景,全部断言通过) @chiga0 证据记录 — 未提出行动请求
serve daemon A/B:4 个场景下与 base 无响应差异 @qwen-code-ci-bot 通过的回归信号 — 无需行动
CI 跟进:将 Ubuntu 失败定位到 serve bundle-closure 门禁,已通过把常量移入零运行时依赖的 externalToolGuard 子路径修复 @chiga0 修复已包含在本 PR 历史中
关于 fast-path.test.ts / process-env-guard.test.ts 中两个穷举测试声明失败的跟进 @chiga0 已在 e70e8b4b4 修复,且已在本 PR 历史中
#8032 合并后 rebase 到 main@2d2bdab2b;新 head 为 ac9e93bb7 @chiga0 状态更新 — 当前检出的 head 与之相符

检查状态

所有已完成的检查均为绿色(22 成功、23 跳过)。唯一进行中的条目是 review-pr 机器人编排(pull_request_target),并非 PR CI。

待 maintainer 处理的事项(不属于 autofix 工作范围)

  • qwen-triage 审查未发现关键正确性或安全问题,diff 中也无阻塞性发现。它就两个策略性问题升级给人类判断:(a) 核心模块规模门禁(触及 packages/core、约 900 行生产代码的 feat 需要 maintainer 签核);(b) umbrella 提案 proposal(core): deterministic tool-execution boundaries for a trustworthy agent runtime #8102(仍 open 且带 need-discussion 标签)的方向是否已足以落地 Milestone 2。两者均为产品/maintainer 决策,本轮原样保留、不做处理。
  • triage 第 2 阶段审查提出了一个明确非阻塞的观察(控制字符/有界拒绝理由的校验谓词在三处内联重复;可抽共享 helper 防止漂移)。按其原话——"值得后续跟进,但不构成合并阻塞"——以及 AGENTS.md 的简洁优先原则,本轮不为此扩大 diff;在此记录以免被静默遗漏。

由于未做任何改动,本轮未产生提交,也未运行验证命令。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review timed out. Qwen review timed out after 14400 seconds (of the 240-minute budget). This run already used the maximum 240 minute timeout. See workflow logs.

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🔀 Base updated: red check(s) [review-pr] pass on current main — merged current main via update-branch; CI will re-run.

中文说明

🔀 已更新 base:红色检查 [review-pr] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。

qwen-code-ci-bot and others added 2 commits August 3, 2026 19:58
…wenLM#8125)

The reconnect-on-timeout test still built its mock tools without server
trust or tool annotations, which the safe replay change now requires
before automatically replaying a connection-loss failure. Update the
fixtures the same way the surrounding reconnect tests were updated,
keeping the test's original assertion that a timeout on a known
disconnected server goes through the reconnect path. Mirrors the same
alignment already landed on main.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 2/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 2/100 轮)。改动内容与我反驳保留之处如下:

Autofix round summary — PR #8125

This round had no new inline review comments or reviews to triage. The actionable item was the persisting red Test (ubuntu-latest, Node 22.x) check; the other red check (review-pr) is the automated Qwen review timing out after its full 240-minute budget, which is workflow infrastructure and cannot be fixed from this checkout.

Root cause of the failing Test check

Reproduced the CI npm run test:ci step locally in a CI-like clean environment (env -i, fresh HOME, NO_COLOR=true, no API keys, no ambient QWEN_HOME). Exactly one deterministic failure remained across all workspaces:

packages/core/src/tools/mcp-tool.test.ts > DiscoveredMCPTool > auto-reconnect on connection error > reconnects instead of reporting a timeout when the server is known disconnected

Error: MCP tool execution may have completed before the connection failed.
Automatic replay was skipped because the call could not be verified as safe
to replay. ...
 ❯ DiscoveredMCPToolInvocation.handleReconnectOnError src/tools/mcp-tool.ts:381

The branch already contains 0cb109f51 fix(core): Avoid replaying unsafe MCP tool calls (#8387) (via the last main merge), whose safe-replay policy requires server trust and idempotent/read-only tool annotations before an automatic reconnect replay. This test still built both mock tools without trust or annotations, so the reconnect path now fails closed with the unsafe-replay error. Main already received e68f617bf fix(core): align MCP reconnect timeout test with safe replay policy (#8478) for exactly this, but that commit landed after this branch's last main merge, and CI tests the PR head itself (refs/pull/8125/head), so the stale fixture kept failing the Test job.

Change made

Ported the #8478 fixture alignment onto this branch (commit eb79c8426): in that one test, pass trust = true and idempotentAnnotations to both DiscoveredMCPTool mock constructions (the post-reconnect invocation's canSafelyReplay() check needs them too). 8 insertions, 2 deletions, test-only; the test's original assertion (a timeout on a known-disconnected server goes through the reconnect path) is unchanged. No production code touched.

Dispositions of other feedback

  • review-pr FAILURE (Qwen review timed out after 14400 s): no code action possible — the review harness exhausted its 240-minute budget; nothing in the working tree can change that. Left to the workflow.
  • Local-only test noise observed during reproduction, classified with evidence and not "fixed":
    • packages/cli/src/ui/auth/AuthDialog.test.tsx flakes only inside this shared container (a different test fails on each run; keypress-delivery races under container load). The file is byte-identical to origin/main, which is green through the merge queue, and the PR touches no auth-dialog or input-path code.
    • In a local run of the no-AK integration set, cli/qwen-serve-streaming.test.ts "Todo Stop Guard replay" exhausted its 30 s poll budget with 2 of 4 expected daemon turns (~15 s per turn on this machine). The test file is byte-identical to origin/main, and every PR change on that daemon path is gated behind the required-guard mode and verified no-op when the guard is off (scrubChildEnv drops undefined overrides; guard wiring in acpAgent.ts/bridge.ts/run-qwen-serve.ts only activates with externalToolGuard.mode === 'required'). Not the step that failed in CI (the job died inside test:ci before reaching the gate).

Verification

Commands actually run and their results (clean CI-like env = env -i + fresh HOME + NO_COLOR=true + no API keys):

  • npm run build — passed (before and after the fix)
  • npm run typecheck — passed (before and after the fix)
  • npm run lint — passed (before and after the fix)
  • npm run check:lockfile — passed
  • npm run check:serve-fast-path-bundle — passed (also produces the DEV bundle used by the integration gate)
  • npm run check-i18n — passed
  • npm run check:desktop-isolation — passed
  • npm run audit:runtime:critical — passed
  • node scripts/lint.js --sensitive-keywords — passed
  • node --test over the CI HELPER_TESTS set — 198 passed, 0 failed
  • npm run generate:settings-schema + git status — schema artifact already up-to-date (no drift)
  • npm run test:ci (all workspaces, clean env) — reproduced the failure: packages/core 1 failing test file (mcp-tool.test.ts, the reconnect timeout test above) and a container-only flake in packages/cli AuthDialog.test.tsx; every other workspace passed (acp-bridge 23 files, sdk 31, vscode-ide-companion 51, web-shell 164, webui 32, all channels, etc.)
  • Focused npx vitest run src/tools/mcp-tool.test.ts in packages/core (clean env), after fix — 92/92 passed
  • Full npx vitest run in packages/core (clean env), after fix — 567 test files passed | 1 skipped, 0 failed (19160 tests passed)
  • No-AK integration set (vitest run --root ./integration-tests with the same 11 files as test:integration:no-ak:sandbox:none, clean env, DEV bundle) — 10/11 files passed (128/129 tests); the single failure is the container pacing artifact described above, on a file byte-identical to origin/main
  • Prettier: the CI step runs prettier --write . (write mode, cannot fail the job); the changed file is not among locally flagged files

Unavailable here: the exact GitHub runner's Test job logs (no GitHub credentials in this environment); the workflow's independent CI remains the final verification gate.

中文说明

Autofix 本轮总结 — PR #8125

本轮没有新的行内评论或 review 需要处理。可操作的问题是持续失败的 Test (ubuntu-latest, Node 22.x) 检查;另一个红色检查(review-pr)是 Qwen 自动 review 在 240 分钟预算内超时,属于工作流基础设施问题,无法在本检出中修复。

Test 检查失败的根因

在类 CI 的干净环境(env -i、全新 HOMENO_COLOR=true、无 API key、无环境残留 QWEN_HOME)中本地复现了 CI 的 npm run test:ci 步骤。所有 workspace 中只剩一个确定性失败:

packages/core/src/tools/mcp-tool.test.ts > DiscoveredMCPTool > auto-reconnect on connection error > reconnects instead of reporting a timeout when the server is known disconnected

Error: MCP tool execution may have completed before the connection failed.
Automatic replay was skipped because the call could not be verified as safe
to replay. ...
 ❯ DiscoveredMCPToolInvocation.handleReconnectOnError src/tools/mcp-tool.ts:381

分支通过上次合并 main 已包含 0cb109f51 fix(core): Avoid replaying unsafe MCP tool calls (#8387),其安全重放策略要求在自动重连重放之前具备服务器信任(trust)和幂等/只读工具注解(annotations)。而该测试的两个 mock 工具构造仍未传 trust 和注解,因此重连路径现在以 unsafe-replay 错误失败关闭。main 上已经合入了针对同一问题的 e68f617bf fix(core): align MCP reconnect timeout test with safe replay policy (#8478),但它是在本分支上次合并 main 之后才落地的,而 CI 测试的是 PR 头本身(refs/pull/8125/head),所以过时的测试夹具一直让 Test 任务失败。

所做的修改

#8478 的夹具对齐移植到本分支(提交 eb79c8426):在该测试中为两个 DiscoveredMCPTool mock 构造传入 trust = trueidempotentAnnotations(重连后新调用的 canSafelyReplay() 检查同样需要它们)。8 行新增、2 行删除,纯测试改动;测试原有断言(已知断开连接的服务器上的超时走重连路径)保持不变。未触碰任何生产代码。

其他反馈的处理

  • review-pr FAILURE(Qwen review 在 14400 秒后超时):无法通过代码修复——review 运行器耗尽了 240 分钟预算,工作树中的任何改动都无法改变这一点。交由工作流处理。
  • 复现过程中观察到的仅本地存在的测试噪音,已用证据分类、"修复":
    • packages/cli/src/ui/auth/AuthDialog.test.tsx 仅在本共享容器内抖动(每次运行失败的用例都不同;容器负载下按键投递存在竞态)。该文件与 origin/main 逐字节一致,main 经合并队列 CI 为绿色,且本 PR 未触碰任何 auth-dialog 或输入路径代码。
    • 本地运行 no-AK 集成测试集时,cli/qwen-serve-streaming.test.ts 的 "Todo Stop Guard replay" 在 30 秒轮询预算内只等到 4 个预期 daemon 请求中的 2 个(本机上每个 turn 约 15 秒)。该测试文件与 origin/main 逐字节一致,且 PR 在该 daemon 路径上的所有改动都被 required-guard 模式门控,guard 关闭时已逐一验证为无操作(scrubChildEnv 会丢弃 undefined 覆盖项;acpAgent.ts/bridge.ts/run-qwen-serve.ts 中的 guard 接线仅在 externalToolGuard.mode === 'required' 时激活)。它也不是 CI 中失败的步骤(任务在进入该门前已在 test:ci 中失败)。

验证

实际执行的命令及结果(干净类 CI 环境 = env -i + 全新 HOME + NO_COLOR=true + 无 API key):

  • npm run build — 通过(修复前后各一次)
  • npm run typecheck — 通过(修复前后各一次)
  • npm run lint — 通过(修复前后各一次)
  • npm run check:lockfile — 通过
  • npm run check:serve-fast-path-bundle — 通过(同时生成集成门使用的 DEV bundle)
  • npm run check-i18n — 通过
  • npm run check:desktop-isolation — 通过
  • npm run audit:runtime:critical — 通过
  • node scripts/lint.js --sensitive-keywords — 通过
  • 对 CI HELPER_TESTS 集合执行 node --test — 198 通过,0 失败
  • npm run generate:settings-schema + git status — schema 产物已是最新(无漂移)
  • npm run test:ci(全部 workspace,干净环境)— 复现失败:packages/core 有 1 个失败测试文件(mcp-tool.test.ts,即上述 reconnect timeout 用例),以及 packages/cli 中仅容器内抖动的 AuthDialog.test.tsx;其余所有 workspace 通过(acp-bridge 23 个文件、sdk 31、vscode-ide-companion 51、web-shell 164、webui 32、全部 channels 等)
  • 修复后在 packages/core 中定向执行 npx vitest run src/tools/mcp-tool.test.ts(干净环境)— 92/92 通过
  • 修复后在 packages/core 中执行完整 npx vitest run(干净环境)— 567 个测试文件通过 | 1 跳过,0 失败(19160 个用例通过)
  • no-AK 集成测试集(vitest run --root ./integration-tests,与 test:integration:no-ak:sandbox:none 相同的 11 个文件,干净环境,DEV bundle)— 10/11 文件通过(128/129 用例);唯一失败是上文描述的容器节奏问题,且该文件与 origin/main 逐字节一致
  • Prettier:CI 步骤运行的是 prettier --write .(写入模式,不会使任务失败);本次改动的文件不在本地标记列表内

本环境不可用:GitHub runner 上 Test 任务的原始日志(此环境无 GitHub 凭据);工作流的独立 CI 仍是最终验证门。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

⚠️ The branch received new commits while this round ran; they were merged into this push, but this round's verification predates that merge — re-check anything that landed mid-run. · 本轮运行期间分支收到了新的提交;本次推送已将其合并,但本轮验证在合并之前完成——请复查运行期间落地的改动。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

wenshao and others added 3 commits August 4, 2026 07:22
QwenLM#8125)

This PR added `@qwen-code/acp-bridge/externalToolGuard` imports to cli
serve/acp modules but not the vitest source alias every other acp-bridge
subpath carries. Without it, any vitest run whose acp-bridge dist is
stale or absent fails to resolve the import and the five serve test
files die at transform time. Add the alias following the documented
convention in the config so tests read the live source.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 3/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 3/100 轮)。改动内容与我反驳保留之处如下:

Autofix review round — PR #8125

Feedback handled

The only feedback item this round was a failed check: Test (ubuntu-latest, Node 22.x). There were no review bodies, inline comments, or issue-level comments to triage, and no base-conflict to resolve (--conflict false). No CI log content was supplied to this round, so every step of that job was reproduced locally instead.

Root-cause defect found and fixed

This PR introduced @qwen-code/acp-bridge/externalToolGuard subpath imports in six cli modules but did not add the matching vitest source alias in packages/cli/vitest.config.ts. Every other acp-bridge subpath has one — the config's own comment explains that tests must read live source because dist/ may be stale or absent. Without the alias, vitest fails hard at transform time with Failed to resolve import "@qwen-code/acp-bridge/externalToolGuard" and the five serve test files (external-tool-guard-provider, run-qwen-serve, server, fast-path, channel-worker-supervisor) all die before running a single test. Reproduced deterministically, then fixed with the same 4-line alias pattern the file already uses for its 20+ sibling subpaths. After the fix those files pass (1251 tests), including under a CI-equivalent environment (wiped API keys, temp HOME), and with dist/ removed entirely.

Other failure candidates investigated and attributed

  • Full core and cli suites were run locally. The only failures (87 core / 40 cli tests) are in files untouched by this PR and are provable artifacts of this evaluation sandbox, not the branch: golden Footer snapshots render a literal docker badge because this harness runs inside a container; settings/home tests expect QWEN_HOME unset while this environment exports it; path-mocking tests see the harness's redirected HOME.
  • The no-AK integration gate (11 files, exact CI env) passed 10/11. The one failure — qwen-serve-streaming > Todo Stop Guard replay (2 requests instead of 4) — was traced to this checkout's own .qwen/settings.json (harness-generated): its tools.core allowlist omits todo_write, so the daemon session disables the tool and the Todo Stop Guard never arms. With that file temporarily set aside the test passes. CI removes stale .qwen/ before checkout, so this cannot occur there.
  • Every other step of the Test job was reproduced and passes on this branch: linters, formatting, sensitive-keyword lint, i18n, settings-schema freshness, VS Code companion notices freshness, serve fast-path bundle closure, .github/scripts helper tests, and test:scripts.
  • The previous round's MCP reconnect-timeout fixture fix was re-verified: all 92 mcp-tool tests pass, and the branch matches main for that file except this PR's new guarded-reconnection test (which passes).

With logs unavailable, the exact failing CI step/test of the red run cannot be named from here; every locally reproducible step of the job is green on this branch after the fix, and the workflow's independent CI remains the final gate.

Changes

  • packages/cli/vitest.config.ts: added the @qwen-code/acp-bridge/externalToolGuard../acp-bridge/src/externalToolGuard.ts alias (4 lines), following the existing convention. Committed as 25a9a6ed8.

Verification

Commands actually run this round and their results:

  • npm run build — passed (acp-bridge dist now emits externalToolGuard.js/.d.ts)
  • npm run typecheck — passed
  • npm run lint — passed
  • npx prettier --check packages/cli/vitest.config.ts — passed
  • node scripts/lint.js --sensitive-keywords — passed
  • npm run check-i18n — passed
  • npm run generate:settings-schema + git status on the schema — no drift
  • npm run generate:notices --workspace=qwen-code-vscode-ide-companion + git status on NOTICES.txt — no drift
  • npm run check:serve-fast-path-bundle — passed ("Startup bundle closure checks passed.")
  • node --test on the 12 HELPER_TESTS files — 198 passed, 0 failed
  • npm run test:scripts — passed
  • Focused Vitest, cli (all 11 touched test files, CI-equivalent env) — 1854 passed
  • Focused Vitest, core touched files (mcp-tool, coreToolScheduler, tool-invocation-guard, speculation) — 92 + 383 passed
  • Focused Vitest, acp-bridge touched files (bridge, bridgeClient, spawnChannel) — 594 passed
  • Full Vitest suites: acp-bridge — all passed; core — 19087 passed / 87 failed (all failures in PR-untouched files, proven sandbox-env artifacts); cli — 17092 passed / 40 failed (same class of artifacts)
  • No-AK integration gate, exact CI env (11 files) — 10 passed; 1 failure attributed to the harness's project .qwen/settings.json (passes with it set aside; CI cleans .qwen/ before checkout)
  • No integration tests run after npm run bundle beyond the gate above — the changed behavior (test-module resolution alias) is test infrastructure only and is not exercised through the bundled CLI
中文说明

Autofix 审查轮次 — PR #8125

处理的反馈

本轮唯一的反馈项是一个失败的检查:Test (ubuntu-latest, Node 22.x)。没有需要分类的审查正文、行内评论或 issue 级评论,也没有需要解决的基线冲突(--conflict false)。本轮没有获得 CI 日志内容,因此该作业的每个步骤都在本地逐一复现。

发现并修复的根因缺陷

本 PR 在六个 cli 模块中引入了 @qwen-code/acp-bridge/externalToolGuard 子路径导入,但没有在 packages/cli/vitest.config.ts 中添加对应的 vitest 源码别名。其他所有 acp-bridge 子路径都有这个别名——配置文件自身的注释解释了原因:测试必须读取实时代码,因为 dist/ 可能过期或缺失。缺少该别名时,vitest 会在转换阶段硬性报错 Failed to resolve import "@qwen-code/acp-bridge/externalToolGuard",五个 serve 测试文件(external-tool-guard-providerrun-qwen-serveserverfast-pathchannel-worker-supervisor)会在运行任何测试之前全部失败。该问题被确定性地复现,随后用与该文件已有的 20 多个同级子路径完全相同的 4 行别名模式修复。修复后这些文件全部通过(1251 个测试),包括在 CI 等效环境(清空 API key、临时 HOME)下,以及在完全移除 dist/ 的情况下。

其他被调查并已归因的失败候选项

  • 本地完整运行了 corecli 套件。仅有的失败(core 87 个 / cli 40 个测试)全部位于本 PR 未触碰的文件中,并且可以证明是本评估沙箱的环境产物,而非分支问题:Footer 黄金快照渲染出了字面 docker 徽标,因为本 harness 运行在容器内;settings/home 测试要求 QWEN_HOME 未设置,而本环境导出了它;路径 mock 测试看到了 harness 重定向后的 HOME
  • no-AK 集成门禁(11 个文件,完全按 CI 环境)通过 10/11。唯一的失败——qwen-serve-streaming > Todo Stop Guard replay(收到 2 个请求而不是 4 个)——被追溯到本检出自身的 .qwen/settings.json(harness 生成):其 tools.core 允许列表缺少 todo_write,导致 daemon 会话禁用该工具,Todo Stop Guard 始终未进入武装状态。临时移开该文件后测试通过。CI 在检出前会清理陈旧的 .qwen/,因此该情况不可能在 CI 发生。
  • Test 作业的其余每个步骤都已复现且在本分支上通过:各 linter、格式化、敏感词 lint、i18n、settings-schema 新鲜度、VS Code companion notices 新鲜度、serve fast-path 包闭包检查、.github/scripts 辅助测试,以及 test:scripts
  • 上一轮对 MCP 重连超时测试 fixture 的修复已重新验证:全部 92 个 mcp-tool 测试通过,且该文件与 main 的差异仅剩本 PR 新增的带守卫重连测试(该测试通过)。

由于无法获得日志,无法从此处指名红色运行中确切失败的 CI 步骤/测试;修复后该作业所有可本地复现的步骤在本分支上均为绿色,工作流的独立 CI 仍是最终验证门禁。

变更

  • packages/cli/vitest.config.ts:新增 @qwen-code/acp-bridge/externalToolGuard../acp-bridge/src/externalToolGuard.ts 别名(4 行),遵循既有惯例。已提交为 25a9a6ed8

验证

本轮实际执行的命令及结果:

  • npm run build — 通过(acp-bridge 的 dist 现在输出 externalToolGuard.js/.d.ts
  • npm run typecheck — 通过
  • npm run lint — 通过
  • npx prettier --check packages/cli/vitest.config.ts — 通过
  • node scripts/lint.js --sensitive-keywords — 通过
  • npm run check-i18n — 通过
  • npm run generate:settings-schema + 对该 schema 的 git status — 无漂移
  • npm run generate:notices --workspace=qwen-code-vscode-ide-companion + 对 NOTICES.txt 的 git status — 无漂移
  • npm run check:serve-fast-path-bundle — 通过("Startup bundle closure checks passed.")
  • 对 12 个 HELPER_TESTS 文件运行 node --test — 198 通过,0 失败
  • npm run test:scripts — 通过
  • 聚焦 Vitest,cli(全部 11 个被修改的测试文件,CI 等效环境)— 1854 通过
  • 聚焦 Vitest,core 被修改文件(mcp-toolcoreToolSchedulertool-invocation-guardspeculation)— 92 + 383 通过
  • 聚焦 Vitest,acp-bridge 被修改文件(bridgebridgeClientspawnChannel)— 594 通过
  • 完整 Vitest 套件:acp-bridge — 全部通过;core — 19087 通过 / 87 失败(全部失败位于 PR 未触碰的文件,已证明为沙箱环境产物);cli — 17092 通过 / 40 失败(同类环境产物)
  • no-AK 集成门禁,完全按 CI 环境(11 个文件)— 10 通过;1 个失败归因于 harness 的项目级 .qwen/settings.json(移开后通过;CI 在检出前会清理 .qwen/
  • 除上述门禁外,未在 npm run bundle 之后额外运行集成测试——本次变更的行为(测试模块解析别名)仅属测试基础设施,不经由打包后的 CLI 执行

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

⚠️ The branch received new commits while this round ran; they were merged into this push, but this round's verification predates that merge — re-check anything that landed mid-run. · 本轮运行期间分支收到了新的提交;本次推送已将其合并,但本轮验证在合并之前完成——请复查运行期间落地的改动。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review timed out. Qwen review timed out after 21600 seconds (of the 360-minute budget). This run already used the maximum 360 minute timeout. See workflow logs.

yiliang114
yiliang114 previously approved these changes Aug 4, 2026

@yiliang114 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — security contract holds: fail-closed is complete across all paths (timeout/abort/transport/malformed/stale identity/unowned session/child-without-ack), loopback strictly enforced (no DNS/proxy/redirect), token hygiene solid (env-only, 8-layer scrub, never forwarded), identity runtime-owned (compromised child cannot forge), nested-agent bypass surface closed, MCP no-replay verified.

Non-blocking suggestions:

  1. (P2) 1 MiB payload cliff: large write_file/edit calls are permanently denied with a generic error. Consider documenting the operational cliff or providing a typed size-limit error.
  2. (P2) Missing e2e test for the full daemon → ACP child → ack → provider chain (design doc verification plan item 13).
  3. (P3) Add length cap on toolCallId/toolName; add non-200/HTTPS path tests; consider named options object instead of 17th positional param.

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

🤖 Reviewed the latest feedback — no code changes needed this round.

Feedback triaged

The only new feedback since the last evaluation is the automated comment "Qwen Code review timed out after 21600 seconds (of the 360-minute budget)" and the corresponding failed review-pr check. There are no new inline comments, no new human reviews, and no maintainer requests to triage.

Why this is not a code defect

Every substantive CI check on this head is green. Unit tests (Test (ubuntu-latest, Node 22.x), ~28 min), Serve A/B, all SDK Java matrix jobs (including the real daemon E2E), and the web-shell E2E Smoke all completed SUCCESS on exactly this push. The only red check is the automated deep-review job itself.

No test in this PR hangs or runs slow. I ran every touched test file locally under hard timeouts:

  • packages/cli — the 10 changed suites (acpAgent, run-qwen-serve, server, fast-path, process-env-guard, channel-worker-supervisor, serve, daemon-worker, cli, gemini): 1833 passed in ~52 s
  • packages/cli — new external-tool-guard-provider.test.ts: 21 passed in ~9 s
  • packages/acp-bridgebridge, bridgeClient, spawnChannel: 594 passed in ~12 s
  • packages/coremcp-tool.test.ts: 92 passed

One local-only failure appeared on the first run — serve startup import boundary in packages/cli/src/commands/serve.test.ts exited with ERR_MODULE_NOT_FOUND for @qwen-code/acp-bridge/dist/externalToolGuard.js. Root cause: a stale local packages/acp-bridge/dist in this checkout, not a defect in the PR — rebuilding the package (npm run build in packages/acp-bridge) emits the file and the test then passes (43/43). CI builds from a clean state before testing, which is why this never failed there.

The timeout pattern points at review workload vs. budget, not at this branch. This is the second consecutive budget exhaustion (240 min on 2026-08-03, 360 min now), and the run that timed out also sat ~4 hours in the runner queue before starting (push 23:53 UTC → job start 03:57 UTC). Meanwhile the round-1 review of this same ~2800-line diff completed and posted its findings in roughly 70 minutes on 2026-08-03 — so the review can finish; the variance matches the load-dependent behavior documented in the review workflow itself. The budget repository variables are already at their maximum (the bot comment confirms "already used the maximum 360 minute timeout"), so there is no further headroom to grant from code.

I also re-read the full diff for anything that could stall a reviewer's probes (spawned servers that never exit, unbounded timers, watch-mode traps) and found nothing: the new HTTP guard client bounds request/response sizes, its timeout timer is unref'd and covered by a dedicated test, the serve test cleanup kills the whole process group with SIGTERM→SIGKILL and waits bounded retries, and the core guard changes (coreToolScheduler, tool-invocation-guard, speculation, mcp-tool fail-closed replay) are small, conditional, and fully covered by the passing suites above.

Recommended next step (maintainer's call)

The remaining remedies are outside code scope, so I have not acted on them:

  1. Retry the review with @qwen-code /review — a less loaded runner/model window may be enough, as it was for round 1.
  2. If timeouts persist, the options are splitting this PR or raising reviewer/runner capacity or budgets (repository variables/settings) — product and infrastructure decisions for the maintainers.
中文说明

🤖 已审阅最新反馈——本轮无需修改代码。

反馈分类

自上次评估以来,唯一的新反馈是自动评论 "Qwen Code review timed out after 21600 seconds (of the 360-minute budget)"(Qwen Code 评审在 360 分钟预算中超时,耗时 21600 秒)以及对应的 review-pr 检查失败。没有新的行内评论、没有新的人工评审,也没有需要处理的维护者请求。

为什么这不是代码缺陷

本次提交头上所有实质性 CI 检查均为绿色。 单元测试(Test (ubuntu-latest, Node 22.x),约 28 分钟)、Serve A/B、全部 SDK Java 矩阵任务(含真实 daemon E2E)、web-shell E2E Smoke 均在完全相同的这次推送上 SUCCESS 完成。唯一标红的检查是自动深度评审任务本身。

本 PR 中没有任何测试挂起或运行缓慢。 我在硬性超时限制下本地运行了所有被改动的测试文件:

  • packages/cli —— 10 个被改动的测试套件(acpAgentrun-qwen-serveserverfast-pathprocess-env-guardchannel-worker-supervisorservedaemon-workercligemini):1833 通过,约 52 秒
  • packages/cli —— 新增的 external-tool-guard-provider.test.ts21 通过,约 9 秒
  • packages/acp-bridge —— bridgebridgeClientspawnChannel594 通过,约 12 秒
  • packages/core —— mcp-tool.test.ts92 通过

首次运行时出现了一个仅本地存在的失败——packages/cli/src/commands/serve.test.ts 中的 serve startup import boundaryERR_MODULE_NOT_FOUND(找不到 @qwen-code/acp-bridge/dist/externalToolGuard.js)退出。根因:本检出中 packages/acp-bridge/dist 过期(陈旧),而非 PR 缺陷——重新构建该包(在 packages/acp-bridge 中执行 npm run build)后会生成该文件,测试随即通过(43/43)。CI 在测试前从干净状态构建,因此该问题从未在 CI 上出现。

超时模式指向评审工作量与预算的矛盾,而非本分支的问题。 这已是连续第二次预算耗尽(2026-08-03 为 240 分钟,本次为 360 分钟),且本次超时的运行在开始前还在运行器队列中排队约 4 小时(推送时间 23:53 UTC → 任务启动 03:57 UTC)。与此同时,针对同一份约 2800 行 diff 的第 1 轮评审在 2026-08-03 约 70 分钟内完成并发布了所有发现——说明评审能够完成;这种波动与评审工作流自身已记录的负载相关行为一致。预算相关的仓库变量已处于最大值(机器人评论确认 "already used the maximum 360 minute timeout",即已使用 360 分钟的最大超时上限),因此代码层面已无法再提供任何额外余量。

我还重新通读了完整 diff,排查任何可能卡住评审探针的内容(永不退出的被启动服务器、无界定时器、watch 模式陷阱等),未发现任何问题:新的 HTTP 守卫客户端限制了请求/响应大小,其超时定时器已 unref 并有专门的测试覆盖;serve 测试的清理逻辑以 SIGTERM→SIGKILL 终止整个进程组,并以有界重试等待;core 侧的守卫改动(coreToolSchedulertool-invocation-guardspeculationmcp-tool 的失败即关闭重放)都很小、有条件触发,并被上述全部通过的测试套件完整覆盖。

建议的下一步(由维护者决定)

其余补救手段超出代码范围,因此我未采取行动:

  1. 重试评审:使用 @qwen-code /review —— 负载较低的运行器/模型窗口可能就足够了,第 1 轮评审即是如此。
  2. 如果超时持续存在,可选方案是拆分本 PR,或提升评审器/运行器容量或预算(仓库变量/设置)——这些是留给维护者的产品与基础设施决策。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🔀 Base updated: red check(s) [review-pr] pass on current main — merged current main via update-branch; CI will re-run.

中文说明

🔀 已更新 base:红色检查 [review-pr] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。

@wenshao

wenshao commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification — real local stack ✅

I rebuilt this branch locally and ran the reviewer test plan against a real built daemon, a real private ACP child, a loopback v1 provider, a mock OpenAI model, and a shell tool with an observable one-shot side effect (append a line to a file). All 16 scenarios behave as the PR claims.

Verified head: 9ab84a0a8 (note: the PR body still cites ac9e93bb7; the branch has taken further main merges since). At run time git merge-base 9ab84a0a8 origin/main == 874e46d73 == origin/main, so the branch fully contains main and the 44-file diff is exactly this PR.

E2E matrix

Runtime behaviour

scenario handshake / prepare executor effects prompt stopReason tool state
off (baseline) 0 / 0 1 end_turn completed
off + token present in daemon env 0 / 0 1 end_turn completed
provider allows 1 / 1 1 end_turn completed
provider denies 1 / 1 0 end_turn failed
provider never answers (--external-tool-guard-timeout-ms=800) 1 / 1 0 end_turn failed
prompt cancelled while the decision is outstanding 1 / 1 0 cancelled failed
model calls agent (nested/delegated) 1 / 0 0 end_turn failed
permission rejected before the executor boundary 1 / 0 0 end_turn failed

No scenario ever issued a second prepare — the no-retry contract holds on deny, timeout and cancel.

Fail-closed startup / session admission — all refused with exit 1

protocolVersion: 2 handshake · wrong nonce echo · provider not listening · non-loopback endpoint · endpoint carrying a path · required with no endpoint · blank token — and, separately, child attestation: I pointed QWEN_CLI_ENTRY at a bundle built from baseline main, so the daemon (PR code, handshake already succeeded) spawns an ACP child that cannot acknowledge. POST /session500 ACP child did not acknowledge the required external tool guard (received: undefined). No session is created.

Raw wire evidence

Correlation and credential isolation

The prepare request the provider actually received on the allow path carries protocolVersion: 1, a fresh requestId, sessionId identical to the daemon's POST /session id, the active promptId, toolCallId, canonical toolName, and the final arguments, under Authorization: Bearer <token>.

Credential isolation, checked from two independent angles:

  • executor environment, captured by the guarded shell tool itself (env > dump): no QWEN_CODE_EXTERNAL_TOOL_GUARD_TOKEN, no token value, and no QWEN_CODE_PRIVATE_EXTERNAL_TOOL_GUARD marker — the child consumed and deleted the marker before any tool could inherit it.
  • live process environments read from the kernel (ps -Eww): the daemon holds the token (by design); the ACP child has the private activation marker but not the token.

Cancel is genuinely non-blocking: the provider was configured to hold the decision for 8000 ms, and the prompt reached its cancelled terminal frame 519 ms after admission without waiting for the provider — the abort/listener race in createManagedExternalToolGuard is closed in practice.

Quality gates on this head

gate result
npm run build + npm run bundle pass
packages/cli (9 affected files) 1692 tests pass
packages/acp-bridge (bridge, bridgeClient, spawnChannel) 594 tests pass
packages/core (mcp-tool, tool-invocation-guard, coreToolScheduler, speculation) 475 tests pass
ESLint on the 38 changed .ts/.tsx files clean
Prettier on all 44 changed files clean
npm run typecheck (all workspaces) pass
npm run check:serve-fast-path-bundle pass — "Startup bundle closure checks passed."

Notes for reviewers (non-blocking)

  1. A denial fails the tool call, not the turn. On deny and on timeout the prompt still terminates turn_complete / end_turn; only an explicit cancel yields stopReason: cancelled. The agent loop observes the failed tool and continues, and it may re-issue the same tool — each attempt legitimately costs a fresh prepare. That is the correct executor-boundary semantic, but a managed deployment expecting "denied ⇒ turn aborted" or "one decision per user turn" should not assume it.
  2. The provider's reason string reaches the model. It is rendered verbatim into the tool result (blocked by host policy (verification harness) in my run). The docs correctly warn that final arguments are sensitive; the reverse direction — denial reasons becoming model-visible context — is worth one line in docs/users/qwen-serve.md too.
  3. What I did not cover end-to-end: the guarded-MCP no-reconnect/no-replay path (exercised by the PR's mcp-tool.test.ts, which passes), and credential scrubbing for the relaunch and channel-worker paths (the code scrubs them; I confirmed the daemon → ACP child → executor chain directly).

Nothing here blocks merge from my side.

中文版本

维护者验证 —— 本地真实环境 ✅

我在本地重新构建了这个分支,并用真实构建的 daemon真实的私有 ACP 子进程回环 v1 Provider、mock OpenAI 模型,以及一个带可观察单次副作用的 shell 工具(向文件追加一行)跑完了 Reviewer 测试计划。16 个场景全部符合 PR 的描述。

验证的 head: 9ab84a0a8(注意 PR 描述里写的还是 ac9e93bb7,之后分支又合并过 main)。运行时 git merge-base 9ab84a0a8 origin/main == 874e46d73 == origin/main,说明分支已完整包含 main,44 个文件的 diff 就是本 PR 本身。

运行时行为

场景 handshake / prepare 执行器副作用 Prompt stopReason 工具状态
off(基线) 0 / 0 1 end_turn completed
off + daemon 环境里存在 token 0 / 0 1 end_turn completed
Provider 允许 1 / 1 1 end_turn completed
Provider 拒绝 1 / 1 0 end_turn failed
Provider 不响应(--external-tool-guard-timeout-ms=800 1 / 1 0 end_turn failed
决策未返回时取消 Prompt 1 / 1 0 cancelled failed
模型调用 agent(嵌套/委派) 1 / 0 0 end_turn failed
在执行器边界之前被权限拒绝 1 / 0 0 end_turn failed

没有任何场景发出第二次 prepare——拒绝、超时、取消三种情况下"无重试"约定都成立。

失败关闭的启动 / Session 准入 —— 全部拒绝并 exit 1

protocolVersion: 2 握手 · nonce 回显错误 · Provider 未监听 · 非回环 endpoint · endpoint 带 path · required 但未给 endpoint · 空 token;另外单独验证了子进程 attestation:我把 QWEN_CLI_ENTRY 指向由基线 main 构建的 bundle,于是 daemon(PR 代码,握手已成功)拉起的 ACP 子进程无法确认。POST /session500 ACP child did not acknowledge the required external tool guard (received: undefined).,Session 不会被创建。

关联字段与凭据隔离

allow 路径上 Provider 实际收到的 prepare 请求包含 protocolVersion: 1、新的 requestId、与 daemon POST /session 完全一致的 sessionId、当前活跃的 promptIdtoolCallId、规范化 toolName,以及最终参数,并带 Authorization: Bearer <token>

凭据隔离用两个互相独立的来源验证:

  • 执行器环境,由受 Guard 保护的 shell 工具自己抓取(env > dump):没有 QWEN_CODE_EXTERNAL_TOOL_GUARD_TOKEN、没有 token 值,也没有 QWEN_CODE_PRIVATE_EXTERNAL_TOOL_GUARD 标记——子进程在任何工具能继承之前就消费并删除了该标记。
  • 活跃进程环境,从内核读取(ps -Eww):daemon 持有 token(设计如此);ACP 子进程只有私有激活标记,没有 token。

取消确实不阻塞:Provider 被配置为把决策挂起 8000 ms,而 Prompt 在被接纳后 519 ms 就到达 cancelled 终态,完全没有等 Provider——createManagedExternalToolGuard 里那段 abort/listener 竞态在实测中确实被关掉了。

该 head 上的质量门禁

门禁 结果
npm run build + npm run bundle 通过
packages/cli(9 个受影响文件) 1692 个测试通过
packages/acp-bridge(bridge、bridgeClient、spawnChannel) 594 个测试通过
packages/core(mcp-tool、tool-invocation-guard、coreToolScheduler、speculation) 475 个测试通过
对 38 个变更 .ts/.tsx 文件跑 ESLint 干净
对 44 个变更文件跑 Prettier 干净
npm run typecheck(全部 workspace) 通过
npm run check:serve-fast-path-bundle 通过 —— "Startup bundle closure checks passed."

给 Reviewer 的说明(不阻塞合并)

  1. 拒绝失败的是工具调用,不是整个 turn。 拒绝和超时时 Prompt 仍然以 turn_complete / end_turn 结束,只有显式取消才会得到 stopReason: cancelled。Agent 循环会看到失败的工具并继续,也可能再次发起同一个工具——每次尝试都会合理地产生一次新的 prepare。这是正确的执行器边界语义,但如果受管部署预期"拒绝 ⇒ 整轮中止"或"每个用户回合只有一次决策",就不能这样假设。
  2. Provider 的 reason 字符串会进入模型上下文。 它会原样渲染到工具结果里(我这次是 blocked by host policy (verification harness))。文档已经正确提醒"最终参数"敏感;反方向——拒绝原因会成为模型可见的上下文——也值得在 docs/users/qwen-serve.md 里补一句。
  3. 我没有端到端覆盖的部分: 受 Guard 保护的 MCP 不重连/不重放路径(由 PR 自带的 mcp-tool.test.ts 覆盖,已通过),以及 relaunch 和 channel worker 路径的凭据擦除(代码里有擦除逻辑;我直接验证的是 daemon → ACP 子进程 → 执行器这条链)。

从我这边看,没有阻塞合并的问题。

@doudouOUC doudouOUC left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the exact head 9ab84a0a82e5140d075364278235026792f404c2. I found one P2 compatibility issue in the external provider bearer-token validation. The fail-closed architecture otherwise looks sound, and I found no authentication bypass or additional blocking correctness issue. I am leaving this as a comment review rather than approving until the inline issue is addressed and the required CI is green.

Comment thread packages/cli/src/serve/external-tool-guard-provider.ts Outdated
@yiliang114
yiliang114 enabled auto-merge August 4, 2026 14:24

@chiga0 chiga0 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Overview

This is a COMMENT review because the PR author is chiga0 (myself). I performed a blind independent review before reading existing reviews.

Verdict: The fail-closed security contract is structurally sound — loopback-only endpoint, authenticated handshake, single prepare request, no retries, token scrubbing, runtime-owned identity, and nested-agent rejection are all in place. I found no authentication bypass or blocking correctness issue. However, I identified one P2 compatibility issue in the provider HTTP client and a few maintainability/robustness gaps.

Cross-Validation

Finding Other Reviewer My Assessment
Non-ASCII bearer token causes ERR_INVALID_CHAR at startup doudouOUC (P2) Confirmed — constructor accepts 密钥, initialize() throws before handshake. Validate ASCII/b64token grammar.
17th positional param in loadCliConfig yiliang114 (P3) Confirmed — at packages/cli/src/config/config.ts, adding another positional argument increases fragility.
Missing timer.unref() regression test doudouOUC Confirmed observation, no additional comment.
Method-routing guard lacks negative-path test doudouOUC Confirmed observation, no additional comment.
Denial-with-reason path untested doudouOUC Confirmed observation, no additional comment.
Duplicated denial-reason validation across packages doudouOUC Confirmed — suggest centralizing in externalToolGuard.ts.
Raw 'required-v1' literal in 12+ locations doudouOUC Confirmed — centralize constant.
Ambiguous ack-rejection error message doudouOUC Confirmed — include received value for diagnostics.
Stale-prompt guard happy path untested doudouOUC / bot rebuttal Acknowledge the rebuttal: existing tests do exercise this path; observation may be probe-specific.
1 MiB payload cliff yiliang114 (P2) Confirmed — large writes are silently denied; consider typed size-limit error.
Missing full daemon→ACP→provider e2e yiliang114 (P2) Confirmed.
toolCallId/toolName length caps yiliang114 (P3) Confirmed as defensive hardening.

Unique Findings

ID Severity File:Line Summary
U1 P2 packages/cli/src/serve/external-tool-guard-provider.ts:~258 Provider HTTP request does not send Accept-Encoding: identity. If the guard endpoint returns gzip/deflate, the client concatenates raw compressed bytes and JSON.parse fails, causing every tool invocation to be denied even though the provider allowed it. Add accept-encoding: 'identity' to the request headers or decompress the response.
U2 P3 packages/cli/src/serve/run-qwen-serve.ts:~1197 Capability external_tool_guard is advertised from config (mode === 'required') rather than from an explicit handshake succeeded flag. If initialize() is skipped or fails silently, the capability could still be advertised. Bind advertisement to the provider's handshake success.
U3 P3 packages/cli/src/serve/run-qwen-serve.ts:~2757 childEnvOverrides does not explicitly set [EXTERNAL_TOOL_GUARD_TOKEN_ENV]: undefined. While other layers scrub the token, making the override explicit here documents the invariant and hardens against future refactor drift.
U4 P3 packages/cli/src/acp-integration/acpAgent.ts:~2747 Nested-agent rejection uses a hardcoded list (AGENT, WORKFLOW, CREATE_SUB_SESSION, SEND_MESSAGE). Future tools that spawn an independent AgentCore/Session could bypass the list. Consider a capability/trait-based mechanism or a central registry for "spawns independent session" tools.
U5 P3 packages/cli/src/acp-integration/acpAgent.ts:~9569 /fork rejection happens after sessionId/directive parameter validation. Move the managed-guard rejection to the top of the sessionForkAgent case so the guard decision runs before parsing unrelated parameters.

Additional Audit Coverage

Areas I independently checked that go beyond existing findings:

  • HTTP transport compatibility: verified request headers and response parsing path; identified the missing Accept-Encoding handling.
  • Capability advertisement semantics: checked that the advertised feature derives from config rather than handshake state.
  • Child environment overrides: traced token-scrubbing layers and found one missing explicit override in the serve bootstrap.
  • Nested-agent bypass surface: reviewed the denylist approach and noted fragility against future tool additions.
  • Control-method ordering: verified /fork rejects managed guard, but only after parameter validation.

Comment thread packages/cli/src/serve/external-tool-guard-provider.ts
Comment thread packages/cli/src/serve/run-qwen-serve.ts
Comment thread packages/cli/src/serve/run-qwen-serve.ts
Comment thread packages/cli/src/acp-integration/acpAgent.ts
Comment thread packages/cli/src/acp-integration/acpAgent.ts
wenshao
wenshao previously approved these changes Aug 4, 2026
@yiliang114
yiliang114 added this pull request to the merge queue Aug 4, 2026
A token outside the ASCII range passed construction but made the
handshake throw ERR_INVALID_CHAR when interpolated into the
Authorization header, blocking qwen serve startup in required mode
with an unexplained error. Enforce printable ASCII (0x21-0x7E) at
validation time so the configuration fails fast with a clear message.
@chiga0
chiga0 dismissed stale reviews from wenshao and yiliang114 via c81d4b8 August 4, 2026 14:37
Merged via the queue into QwenLM:main with commit 5631f4b Aug 4, 2026
2 checks passed
@chiga0

chiga0 commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Heads-up for maintainers: the squash merge appears to have raced with a fix push and landed on the pre-fix head.

Timeline (UTC):

  • 14:36 — pushed c81d4b8 to this branch (fix for the non-ASCII token issue raised in review)
  • 14:37:32 — PR merged as 5631f4b, whose tree still contains the OLD normalizeToken (verified: no printable ASCII validation, no Accept-Encoding header)
  • ~15:04 — second fix batch 57058e9 pushed, after the PR was already closed

So two reviewed-and-confirmed fixes did not reach main:

  1. c81d4b8cdcfix(serve): reject non-ASCII external tool guard bearer tokens (the P2 from review: a non-ASCII token passed construction but made the handshake throw ERR_INVALID_CHAR, blocking qwen serve startup in required mode)
  2. 57058e95e1fix(serve): harden external tool guard per review feedback (P2 Accept-Encoding: identity so a compressed allow can't fail closed into a denial; explicit token scrub in per-handle child env overrides; distinguishable guard-ack error message; regression tests)

Both commits are intact on chiga0/qwen-code:feat/external-tool-guard-provider and were verified locally (relevant suites green, typecheck clean). Happy to open a follow-up PR if wanted — flagging here first so a maintainer can decide.

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.6.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants