feat(core): propagate trusted invocation context - #6895
Conversation
E2E Test ReportResult: PASS Real CLI
Real WebShell
Process-level propagation
Automated regression and build
|
|
Thanks for the PR! Template looks good ✓ — all required sections present, bilingual, reviewer test plan is detailed. Problem: This is infrastructure for runtime invocation provenance — tools genuinely cannot tell which entry point (CLI, daemon, ACP, channel, etc.) initiated a call. This is a real gap, not theoretical hardening: the author provides concrete E2E captures showing Direction: Aligned. Invocation provenance is standard infrastructure for a multi-ingress system like Qwen Code. The design choices — AsyncLocalStorage propagation, transport-level gating (stdio only), capability-based ACP child trust, and daemon authority over metadata — are sensible security boundaries. The explicit decision to keep this runtime-only and out of chat history is the right call. CHANGELOG has no direct reference but this area (daemon lifecycle, MCP metadata, ACP trust) is clearly core to the project. Size: This is a large cross-package feature. Breakdown:
Approach: The scope is large but the pieces fit together — you can't propagate trusted client identity through the serve-bridge without fixing the session binding lifecycle. That said, the PR effectively ships two features: (1) invocation context propagation through core/CLI/ACP, and (2) serve-bridge multi-session lifecycle hardening. A maintainer may want to review these as separate logical units even if they ship together. The Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ — 所有必填段落齐全,双语,reviewer test plan 详细。 问题: 这是运行期调用来源追溯的基础设施——工具确实无法判断是哪个入口(CLI、daemon、ACP、channel 等)发起的调用。这是真实的缺口,不是理论性加固:作者提供了具体的 E2E 抓包,展示 方向: 对齐。调用来源追溯是 Qwen Code 这种多入口系统的标准基础设施。设计选择——AsyncLocalStorage 传播、transport 级别门控(仅 stdio)、基于 capability 的 ACP 子进程信任、daemon 对 metadata 的权威控制——都是合理的安全边界。明确决定保持仅运行期存在、不写入聊天历史是正确的。CHANGELOG 没有直接参考,但这个领域(daemon 生命周期、MCP metadata、ACP 信任)显然是项目核心。 规模: 这是一个大型跨包 feature。分解:
方案: 范围很大但各部分相互配合——不修复 session binding 生命周期就无法在 serve-bridge 中传播可信客户端身份。不过,这个 PR 实际上交付了两个功能:(1) 通过 core/CLI/ACP 的 invocation context 传播,(2) serve-bridge 多 session 生命周期加固。维护者可能希望将它们作为独立的逻辑单元来审查,即使一起交付。 进入代码审查 🔍 — Qwen Code · qwen3.7-max Reviewed at |
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
Code ReviewNo critical blockers found. The implementation is security-conscious and well-structured. Independent proposal comparison: I would have designed this similarly — Security review — the capability flow is correct:
Architecture notes:
Reuse check: No existing utilities cover the invocation context pattern. The Propagation FlowsequenceDiagram
participant P1 as CLI entry
participant P2 as runNonInteractive
participant P3 as AsyncLocalStorage
participant P4 as MCP tool invocation
participant P5 as Stdio transport
P1->>P2: runWithInvocationContext (ingress cli)
P2->>P3: store context in ALS
P2->>P4: process tool call batch
P4->>P3: getInvocationContext()
alt allowInvocationContext is true
P4->>P5: callTool with _meta containing context
else non-stdio transport
P4->>P5: callTool without _meta
end
P5-->>P4: tool result
The daemon path mirrors this but rebuilds context at admission: the daemon strips all caller-supplied Files changed (30 of 61 shown)
Test ResultsBuild: ✅ Unit tests (all passed):
Real-scenario testing: This PR modifies runtime metadata that's invisible in TUI output — verifying invocation context propagation requires a configured MCP capture server and real provider. The author's E2E report (posted as a PR comment) shows real CLI and WebShell captures confirming 中文说明代码审查未发现关键阻塞问题。 实现安全意识强,结构良好。 独立方案对比: 我会采用类似设计——AsyncLocalStorage 传播、transport 级别 MCP 门控、基于 capability 的 ACP 子进程信任、daemon 对 metadata 的权威控制。PR 的方案匹配或超越了这个基线。 安全审查——capability 流程正确:
架构注释:
复用检查: 没有现有工具覆盖 invocation context 模式。AsyncLocalStorage 使用是标准 Node.js 用法,没有不必要的抽象。 测试结果构建: ✅ 单元测试(全部通过): 约 2,225 个测试全部通过,覆盖 core、CLI、ACP bridge、channel base 和 SDK。 真实场景测试: 这个 PR 修改的是 TUI 输出中不可见的运行期 metadata——验证 invocation context 传播需要配置好的 MCP 捕获服务和真实 provider。作者的 E2E 报告显示真实的 CLI 和 WebShell 抓包确认了 — Qwen Code · qwen3.7-max Reviewed at |
|
Confidence: 3/5 — clean review across all stages, but the Stage 0 size escalation and the scope of the serve-bridge lifecycle changes need a maintainer's sign-off before approving. The code is solid — security-conscious capability flow, correct transport gating, idiomatic AsyncLocalStorage usage, and ~2,225 tests passing across all affected packages. The design doc is thorough and the E2E evidence is convincing. If this were a 300-line PR, I'd approve without hesitation. What gives me pause is the scope. This PR effectively ships two features in one: (1) invocation context propagation through core/CLI/ACP with strict trust boundaries, and (2) a substantial rewrite of the serve-bridge session lifecycle ( The private ACP capability flow also touches a security boundary that deserves a careful human review: env var consumption, child-only passthrough, and the one-shot initialize validation. I didn't find issues, but this is the kind of code where a subtle bug has real consequences. Deferring to a maintainer for the final call. 👆 中文说明置信度:3/5 — 所有阶段审查均通过,但 Stage 0 的规模升级和 serve-bridge 生命周期变更的范围需要维护者在批准前签字。 代码质量扎实——安全意识强的 capability 流程、正确的 transport 门控、标准的 AsyncLocalStorage 用法,约 2,225 个测试在所有受影响的包中全部通过。设计文档详尽,E2E 证据有说服力。如果这是一个 300 行的 PR,我会毫不犹豫地批准。 让我犹豫的是范围。这个 PR 实际上在一个 PR 中交付了两个功能:(1) 通过 core/CLI/ACP 的 invocation context 传播,带有严格的信任边界;(2) serve-bridge session 生命周期的大幅重写( 私有 ACP capability 流程也触及了一个值得仔细人工审查的安全边界:环境变量消费、仅子进程传递、一次性 initialize 验证。我没有发现问题,但这种代码中一个微妙的 bug 可能有真实后果。 转交维护者做最终决定。👆 — Qwen Code · qwen3.7-max Reviewed at |
|
Qwen Code review did not complete successfully. Qwen review aborted with an API error before posting comments. See workflow logs. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and did not run locally.
Not reviewed: build-and-test — Test (macos-latest, Node 22.x) was skipped in CI.
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI.
— qwen3.7-max via Qwen Code /review
|
@qwen-code /review Review follow-up is pushed in b932b84. Four actionable threads were fixed; the transient session_close suggestion was answered with the retry-preservation contract and existing test evidence. Local verification on the current head: build and bundle passed, typecheck passed, lint passed, serve-bridge tests passed 57/57, and the bundled CLI plus two-session external_mcp daemon runtime test passed 2/2 on macOS. The POSIX daemon case now skips dynamically on Windows while the direct CLI case remains enabled. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. 1 Suggestion-level finding(s) could not be anchored to the diff; see the terminal output. Not reviewed: chunk 1, chunk 2, chunk 3, chunk 5, chunk 6, chunk 7, chunk 8, chunk 9, chunk 10, chunk 11, chunk 12, chunk 13, chunk 14, chunk 15, chunk 16, chunk 17, chunk 18, chunk 19, chunk 20, chunk 21, chunk 22 — no agent reported covering these; nobody read them.
— qwen3.7-max via Qwen Code /review
|
@qwen-code /review Review follow-up is pushed in I also checked the terminal-only Seatbelt suggestion. The relevant file is Current-head verification: serve-bridge 58/58, ACP 248/248, core parser 14/14, build, bundle, typecheck, lint, and bundled runtime propagation 2/2. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. Not reviewed: chunk 1, chunk 2, chunk 3, chunk 4, chunk 5, chunk 6, chunk 7, chunk 8, chunk 9, chunk 10, chunk 11, chunk 12, chunk 13, chunk 14, chunk 15, chunk 16, chunk 17, chunk 18, chunk 19, chunk 20, chunk 21, chunk 22 — no agent reported covering these; nobody read them.
— qwen3.7-max via Qwen Code /review
|
Qwen Code review did not complete successfully. Qwen review exited with status 1. See workflow logs. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestions are inline.
— qwen3.7-max via Qwen Code /review
|
Qwen Code review did not complete successfully. Qwen review exited with status 1. See workflow logs. |
…ext-v1 # Conflicts: # packages/cli/src/gemini.test.tsx # packages/cli/src/gemini.tsx # packages/cli/src/utils/relaunch.test.ts # packages/cli/src/utils/relaunch.ts # packages/cli/src/utils/sandbox.test.ts
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. 1 Suggestion-level finding(s) could not be anchored to the diff; see the terminal output. Not reviewed: chunk 1, chunk 2, chunk 3, chunk 4, chunk 5, chunk 6, chunk 7, chunk 8, chunk 9, chunk 10, chunk 11, chunk 12, chunk 13, chunk 14, chunk 15, chunk 16, chunk 17, chunk 18, chunk 19, chunk 20, chunk 21, chunk 22 — no agent reported covering these; nobody read them. Not reviewed: chunk 2 — launched with a prompt that never named the diff file, so it could not have read it (build the prompt with qwen review agent-prompt). Not reviewed: chunk 8 — launched with a prompt that never named the diff file, so it could not have read it (build the prompt with qwen review agent-prompt). Not reviewed: chunk 19 — launched with a prompt that never named the diff file, so it could not have read it (build the prompt with qwen review agent-prompt). Not reviewed: chunk 13 — launched with a prompt that never named the diff file, so it could not have read it (build the prompt with qwen review agent-prompt). Not reviewed: chunk 7 — launched with a prompt that never named the diff file, so it could not have read it (build the prompt with qwen review agent-prompt). Not reviewed: chunk 15 — launched with a prompt that never named the diff file, so it could not have read it (build the prompt with qwen review agent-prompt). Not reviewed: chunk 18 — launched with a prompt that never named the diff file, so it could not have read it (build the prompt with qwen review agent-prompt). Not reviewed: chunk 1 — launched with a prompt that never named the diff file, so it could not have read it (build the prompt with qwen review agent-prompt). Not reviewed: chunk 22 — launched with a prompt that never named the diff file, so it could not have read it (build the prompt with qwen review agent-prompt). Not reviewed: chunk 5 — launched with a prompt that never named the diff file, so it could not have read it (build the prompt with qwen review agent-prompt). Not reviewed: chunk 16 — launched with a prompt that never named the diff file, so it could not have read it (build the prompt with qwen review agent-prompt). Not reviewed: chunk 11 — launched with a prompt that never named the diff file, so it could not have read it (build the prompt with qwen review agent-prompt). Not reviewed: chunk 6 — launched with a prompt that never named the diff file, so it could not have read it (build the prompt with qwen review agent-prompt). Not reviewed: chunk 21 — launched with a prompt that never named the diff file, so it could not have read it (build the prompt with qwen review agent-prompt). Not reviewed: chunk 12 — launched with a prompt that never named the diff file, so it could not have read it (build the prompt with qwen review agent-prompt). Not reviewed: chunk 17 — launched with a prompt that never named the diff file, so it could not have read it (build the prompt with qwen review agent-prompt). Not reviewed: chunk 10 — launched with a prompt that never named the diff file, so it could not have read it (build the prompt with qwen review agent-prompt). Not reviewed: chunk 14 — launched with a prompt that never named the diff file, so it could not have read it (build the prompt with qwen review agent-prompt). Not reviewed: chunk 4 — launched with a prompt that never named the diff file, so it could not have read it (build the prompt with qwen review agent-prompt). Not reviewed: chunk 20 — launched with a prompt that never named the diff file, so it could not have read it (build the prompt with qwen review agent-prompt). Not reviewed: chunk 3 — launched with a prompt that never named the diff file, so it could not have read it (build the prompt with qwen review agent-prompt). Not reviewed: chunk 9 — launched with a prompt that never named the diff file, so it could not have read it (build the prompt with qwen review agent-prompt).
— qwen3.7-max via Qwen Code /review
…ext-v1 # Conflicts: # packages/cli/src/ui/hooks/useGeminiStream.ts
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. Not reviewed: chunk 1, chunk 2, chunk 3, chunk 4, chunk 5, chunk 6, chunk 7, chunk 8, chunk 9, chunk 10, chunk 11, chunk 12, chunk 13, chunk 14, chunk 15, chunk 16, chunk 17, chunk 18, chunk 19, chunk 20, chunk 21, chunk 22 — no agent reported covering these; nobody read them.
— qwen3.7-max via Qwen Code /review
🖼️ web-shell visual previewAuto-rendered from this PR head Screenshots · light / dark
Full-resolution recordings (.webm) are attached to the workflow run. — Qwen Code · web-shell visuals |
|
@qwen-code /review --timeout=240 |
| _Qwen Code review request accepted. Review is queued in [workflow run](https://github.com/QwenLM/qwen-code/actions/runs/29415744274)._ |
|
Qwen Code review did not complete successfully. Qwen review exited with status 1. See workflow logs. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. 2 Suggestion-level finding(s) could not be anchored to the diff; see the terminal output. Not reviewed: coverage — no plan was given, so this run cannot show that any of the diff was read.
— qwen3.7-max via Qwen Code /review
doudouOUC
left a comment
There was a problem hiding this comment.
[API Error: Team API AKday消费金额已达上限]
|
@qwen-code /resolve |
Resolve conflict in memory-intent-classifier.ts by combining both sides:
- Keep buildMemoryManifest(entries) in the prompt text (from main)
- Keep { invocationIngress: 'internal' } options (from PR)
|
Qwen Code resolved the merge conflicts and pushed the branch update. Merge conflict resolution summary — PR #6895ConflictFile: What each side changed (relative to the merge base)
ResolutionCombined both changes — they are orthogonal:
const response = await bridge.prompt(
sessionId,
`${CLASSIFIER_PROMPT}${JSON.stringify(text)}${buildMemoryManifest(entries)}`,
{ invocationIngress: 'internal' },
);VerificationThe resulting diff against Other auto-merged files (no manual intervention needed)
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and did not run locally.
— qwen3.7-max via Qwen Code /review
|
@qwen-code /resolve |
|
Qwen Code resolved the merge conflicts and pushed the branch update. Merge Conflict Resolution SummaryPR: #6895 — feat(core): propagate trusted invocation context Conflicted File
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. 3 Suggestion-level finding(s) could not be anchored to the diff; see the terminal output. Not reviewed: reverse audit — skipped for efficiency after 27 review agents, 2 verification agents, and 9 prior review rounds.
— qwen3.7-max via Qwen Code /review
| this.turn += 1; | ||
| const sessionId = this.config.getSessionId(); | ||
| const promptId = sessionId + '########' + this.turn; |
There was a problem hiding this comment.
[Suggestion] this.turn += 1 executes before parseInvocationContext and ingress validation, both of which can throw RequestError.invalidParams. If either throws, the turn counter retains a permanent gap (e.g., turn 5 succeeds, turn 6 increments then throws, turn 7 succeeds with no record for turn 6). While prompt IDs only need uniqueness (not contiguity), any consumer that infers turn count from promptId suffixes would see misleading gaps.
Concrete cost: promptId sequence has cosmetic gaps on validation failures.
| this.turn += 1; | |
| const sessionId = this.config.getSessionId(); | |
| const promptId = sessionId + '########' + this.turn; | |
| const sessionId = this.config.getSessionId(); |
(Move this.turn += 1 and promptId construction to after both validation checks pass.)
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Fixed in 8084e28e0. The turn counter and durable prompt ID are now allocated only after both trusted invocation-context and ingress validation succeed. Both invalid-metadata regression cases now assert that the turn count remains zero. The focused Session suite passes 270/270.
| export async function disposeBindings(state: BridgeState): Promise<void> { | ||
| state.disposed = true; | ||
| while (state.pendingLifecycles.size > 0) { |
There was a problem hiding this comment.
[Suggestion] disposeBindings drains pendingLifecycles before calling releaseBinding on any binding. An in-flight prompt handler is tracked as a pending lifecycle and blocks on collector.promise (with a 30-second timeout). Since releaseBinding (which calls stopEventStream and resolves the collector) happens only after lifecycle draining, shutdown is delayed up to 30 seconds per in-flight prompt.
Failure scenario: User calls dispose() while a prompt is waiting for _meta. The collector is never resolved by SSE, so disposal hangs for the full COLLECT_TIMEOUT_MS (30s).
Consider stopping all event streams (resolve collectors) before or concurrently with draining lifecycles.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Fixed in 8084e28e0. Disposal now marks the bridge disposed, lets already-tracked handlers enter their synchronous setup, interrupts any active collectors, and only then drains lifecycle promises. This avoids the 30-second collector timeout without tearing down the SSE stream before an in-flight cancellation settles. The new immediate-dispose regression failed on the previous ordering and now completes before the collector timeout; the focused serve-bridge suites pass 63/63.
| meta[INVOCATION_INGRESS_META_KEY] = | ||
| options?.invocationIngress ?? this.invocationIngress; |
There was a problem hiding this comment.
[Suggestion] withPromptIngress unconditionally overwrites meta[INVOCATION_INGRESS_META_KEY] with options?.invocationIngress ?? this.invocationIngress. The test title asserts three-way precedence (per-prompt > caller meta > client default) but the implementation only realizes per-prompt > client default (two levels). A caller that passes _meta['qwen-code/invocation-ingress'] without a per-prompt option expecting it to override the client default will have its value silently replaced.
Concrete cost: test title misleads future maintainers about the actual contract.
Either implement the three-way precedence the test describes, or tighten the test name to match the two-level behavior.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
No code change here because the suggested caller-meta precedence contradicts the documented trusted-stamping contract. docs/design/invocation-context/README.md states that metadata is copied first and then stamped with precedence: per-prompt override, DaemonClient default, then daemon. The test title says only that an explicit per-prompt value overrides both caller metadata and the client default; it does not claim caller metadata overrides the default. The adjacent default-stamping test also verifies that unrelated caller metadata is preserved while the ingress key is replaced. Allowing the untrusted caller key to beat the client default would weaken the intended boundary.
| ...(options?.invocationIngress | ||
| ? { | ||
| _meta: { | ||
| 'qwen-code/invocation-ingress': options.invocationIngress, | ||
| }, | ||
| } | ||
| : {}), |
There was a problem hiding this comment.
[Suggestion] No test verifies that invocationIngress is forwarded as _meta['qwen-code/invocation-ingress'] on the ACP conn.prompt() call. AcpBridge.test.ts tests private capability generation but has no assertion on the ingress metadata pass-through.
Concrete cost: A regression that drops the _meta spread would silently prevent channel/scheduler/internal ingress labels from reaching the daemon through standalone ACP bridges.
Add a test that calls bridge.prompt(sessionId, 'text', { invocationIngress: 'channel' }) and asserts the ACP connection's prompt call includes _meta['qwen-code/invocation-ingress']: 'channel'.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Added the requested regression coverage in 8084e28e0. The test calls bridge.prompt(..., { invocationIngress: "channel" }) and verifies that the ACP prompt request contains _meta["qwen-code/invocation-ingress"] === "channel". The focused AcpBridge suite passes 31/31.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. 3 Suggestion-level finding(s) could not be anchored to the diff; see the terminal output.
— qwen3.7-max via Qwen Code /review
| sessionId: string, | ||
| fn: () => Promise<T>, | ||
| ): Promise<T> { | ||
| const previous = state.sessionLocks.get(sessionId) ?? Promise.resolve(); |
There was a problem hiding this comment.
[Suggestion] The promise-chain mutex has no rejection guard. If previous ever rejects (an unexpected error escaping through the chain), await previous throws before the try block, unlock() is never called, and the stored tail also rejects — permanently poisoning all subsequent lock acquisitions for that session ID. Every future withSessionLock call for the same session would hang forever.
| const previous = state.sessionLocks.get(sessionId) ?? Promise.resolve(); | |
| const previous = (state.sessionLocks.get(sessionId) ?? Promise.resolve()).catch(() => {}); |
— qwen3.7-max via Qwen Code /review
| } catch (err) { | ||
| await rethrowBindingError(state, binding, err); | ||
| } | ||
| await releaseBinding(state, binding, false); |
There was a problem hiding this comment.
[Suggestion] When closeSession throws a non-invalid_client_id error, rethrowBindingError re-throws and releaseBinding on this line is never reached — the SSE stream for the session leaks until the idle TTL cleanup (30 min) reclaims it. The old code had a finally block that always cleaned up.
| await releaseBinding(state, binding, false); | |
| await releaseBinding(state, binding, false); |
Move this into a finally block (or call it in the catch path before rethrowing for non-client-id errors). releaseBinding is idempotent via its releasePromise latch, so calling it unconditionally is safe.
— qwen3.7-max via Qwen Code /review
| export type { AvailableCommand, ToolCallEvent } from './ChannelAgentBridge.js'; | ||
|
|
||
| const MID_TURN_QUEUE_DRAIN_METHOD = 'craft/drainMidTurnQueue'; | ||
| const PRIVATE_ACP_CAPABILITY_ENV = 'QWEN_CODE_PRIVATE_ACP_CAPABILITY'; |
There was a problem hiding this comment.
[Suggestion] PRIVATE_ACP_CAPABILITY_ENV and PRIVATE_PARENT_CAPABILITY_META_KEY are hardcoded string literals here, while bridge.ts imports them from @qwen-code/qwen-code-core. If the canonical string in core ever changes, this site would silently diverge, breaking the parent-child capability handshake.
Concrete cost: a future rename of the meta key in core would leave AcpBridge.ts sending the old key, causing the child to reject every initialize call.
— qwen3.7-max via Qwen Code /review
|
Closing this PR in favor of a clean, narrower replacement built from the latest The original goal is a generic Qwen-side prerequisite for Browser Use: propagate daemon-attested per-turn correlation metadata to Qwen-launched local stdio MCP calls. During implementation, this PR expanded well beyond that vertical slice and now also includes ingress enumeration across channels/scheduler/CLI, TypeScript SDK API changes, and qwen-serve MCP bridge binding/shutdown/session-lifecycle refactors. Those changes are not required for the minimum contract and make a core, cross-package PR unnecessarily difficult to review with high confidence. The replacement PR will keep only CI on this PR is green; this closure is a deliberate scope reset, not a CI workaround. I will follow up with the clean replacement PR and reference this PR for history. 中文说明关闭此 PR,后续将基于最新 原始目标只是 Browser Use 所需的通用 Qwen 侧前置能力:把由 daemon 验证的单轮关联信息传递给 Qwen 启动的本地 stdio MCP 调用。实现过程中,本 PR 的范围扩展到了 channel/scheduler/CLI 的 ingress 枚举与传播、TypeScript SDK 公共 API,以及 qwen-serve MCP bridge 的 binding、shutdown 和 session lifecycle 重构。这些内容并不是最小合同所必需的,也使 core 跨包 PR 难以被 maintainer 以足够高的置信度审阅。 替代 PR 只保留 本 PR 的 CI 已通过;此次关闭是主动收缩范围,不是绕过 CI。后续替代 PR 会引用本 PR 保留历史背景。 |










What this PR does
This PR introduces a trusted, runtime-only
InvocationContextV1that identifies the ingress, native session, root prompt, and validated originating daemon client for one invocation chain. CLI, direct ACP, daemon, channel, scheduler, and internal roots establish their own context, while in-process subagents and explicitly resumed delayed work inherit or restore the appropriate snapshot.Private ACP children authenticate their parent with a per-spawn capability before accepting propagated context. Daemon requests discard caller-supplied full context, validate the client first, and rebuild the metadata at admission. Only locally created stdio MCP transports may receive the namespaced invocation metadata; remote, reverse, client-hosted, Computer Use, and A2UI paths remain denied.
The TypeScript daemon SDK gains an additive ingress advisory for trusted adapters. The qwen-serve MCP bridge also gains isolated multi-session bindings, identity-safe cleanup, deterministic cancellation and close behavior, and an idempotent shutdown path that waits for pending detach work.
Why it's needed
Tools currently cannot reliably tell which trusted runtime entry point initiated a call, and forwarding arbitrary caller metadata would create a spoofing boundary. The new contract provides trustworthy runtime correlation without changing durable prompt IDs or persisting provenance into chat history, while keeping remote MCP transports outside the propagation boundary.
The qwen-serve lifecycle work is required for that provenance to remain correct when multiple daemon sessions run concurrently: stale stream callbacks, replacement bindings, cancellation, and process shutdown must never detach or delete another session's current client binding.
Reviewer Test Plan
How to verify
cli, the CLI session and turn IDs, no originating client ID, the existing progress token, and no private ACP capability in the child environment.daemon; its session, prompt, and originating client IDs should exactly match the WebShell URL and the daemon's prompt admission record.external_mcp, keep its own validated client ID, survive closing the other session, and receive a fresh prompt ID for each root request.Local automated coverage passed across the relevant Core (503 tests), CLI (789 passed, 1 skipped), ACP bridge (394), channel base (425), and TypeScript SDK (363) suites.
npm run build,npm run typecheck,npm run lint, andnpm run bundlealso passed.Evidence (Before & After)
N/A for UI screenshots. This is runtime metadata and lifecycle behavior. A real configured
qwen3.5-plusCLI call capturedingress: "cli"; a real built WebShell call capturedingress: "daemon", and its session, prompt, and client IDs matched the daemon admission and completion logs exactly. Both retainedprogressTokenand reported no private capability in the stdio MCP environment.Tested on
Environment (optional)
macOS, Node.js 22+, source-built bundled CLI and WebShell, configured
qwen3.5-plusprovider, local stdio MCP capture server, and real daemon/private ACP processes.Risk & Scope
Linked Issues
N/A
中文说明
这个 PR 做了什么
这个 PR 引入可信、仅运行期存在的
InvocationContextV1,用于标识一条调用链的入口、原生会话、根 prompt,以及经过校验的 daemon 来源客户端。CLI、直连 ACP、daemon、channel、scheduler 和 internal 入口分别建立自己的根上下文;同进程子 Agent 继承上下文,延迟恢复的工作则显式恢复正确快照。私有 ACP 子进程必须先通过每次 spawn 独立的 capability 验证 parent,之后才接受传播的上下文。daemon 会丢弃调用方携带的完整上下文,先验证客户端,再在 admission 阶段重建元数据。只有由可信代码创建的本机 stdio MCP transport 可以收到 namespaced invocation metadata;远程、reverse、client-hosted、Computer Use 和 A2UI 路径继续禁止传播。
TypeScript daemon SDK 以兼容方式增加可信 adapter 使用的 ingress advisory。qwen-serve MCP bridge 同时增加隔离的多 session binding、基于对象身份的安全清理、确定性的取消和关闭行为,以及等待 pending detach 完成的幂等 shutdown。
为什么需要
工具目前无法可靠判断一次调用来自哪个可信运行时入口,而直接转发调用方任意 metadata 会形成可伪造的安全边界。新合同在不改变 durable prompt ID、不把来源信息写入聊天历史的前提下提供可信运行时关联,同时继续把远程 MCP 排除在传播边界之外。
qwen-serve 的生命周期改动是多 daemon session 并发时保证来源正确所必需的:旧 stream 回调、binding 替换、取消和进程关闭都不能 detach 或删除另一个 session 当前有效的 client binding。
Reviewer Test Plan
如何验证
cliingress、CLI session 和 turn ID,不包含来源 client ID,同时保留原有 progress token,且子进程环境中没有私有 ACP capability。daemoningress;session、prompt 和来源 client ID 必须与 WebShell URL 及 daemon prompt admission 记录完全一致。external_mcpingress,保持各自经过验证的 client ID;关闭另一个 session 后仍可继续使用,并且每次根请求获得新的 prompt ID。本地相关自动化覆盖全部通过:Core 503 个测试、CLI 789 passed/1 skipped、ACP bridge 394、channel base 425、TypeScript SDK 363。
npm run build、npm run typecheck、npm run lint和npm run bundle也全部通过。Evidence (Before & After)
不适用于 UI 截图;这是运行时 metadata 与生命周期行为。使用真实配置的
qwen3.5-plus调用源码构建 CLI,抓到ingress: "cli";使用真实 built WebShell 调用,抓到ingress: "daemon",且 session、prompt 和 client ID 与 daemon admission/completion 日志完全一致。两次抓包都保留progressToken,且 stdio MCP 环境中没有私有 capability。Tested on
Environment (optional)
macOS、Node.js 22+、源码构建 bundled CLI 和 WebShell、已配置的
qwen3.5-plusprovider、本机 stdio MCP 抓包服务,以及真实 daemon/private ACP 进程。Risk & Scope
Linked Issues
N/A