Skip to content

feat(ipc): hold peer messages across review classes, let repositories only tighten inbound settings - #11026

Open
qqqys wants to merge 2 commits into
QwenLM:mainfrom
qqqys:feat/peer-messaging-gate-matrix
Open

feat(ipc): hold peer messages across review classes, let repositories only tighten inbound settings#11026
qqqys wants to merge 2 commits into
QwenLM:mainfrom
qqqys:feat/peer-messaging-gate-matrix

Conversation

@qqqys

@qqqys qqqys commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

With no explicit agents.crossSessionInbound, the inbound gate decides from approval-mode parity, and one row of that table let a receiver that reviews every action accept anything at all: a bypassing sender, or a sender that asserted no mode at all. This PR replaces the table with one rule, lets a repository make its sessions more cautious but never less, and makes a hold notice say which scope is holding the message.

One rule. Every approval mode is in one of two review classes, prompting (default, plan) or bypass (auto-edit, auto, yolo), and a message auto-delivers only between sessions of the same class. A prompting receiver now holds messages from a bypassing sender, and every receiver holds a message whose sender asserted no class. The classification is a single exported function that the send side uses too, so two sessions in the same mode always agree on their class. A message held on a class mismatch is still released the moment the receiver's mode moves into the sender's class, without the user approving it by hand. The explicit setting wins in both directions, own-process frames are still accepted, and the wire protocol does not change.

A repository may only tighten. Both agents.* keys used to sit in the restricted list, so a workspace value was dropped whole, with a warning that read as a bug. They move to a new tighten-only list: at merge time a trusted workspace's value is kept only when it is strictly stricter than what User or SystemDefaults set (accept < unset < hold < refuse; true < false = unset), or than the feature's default when neither does. Equal is dropped silently. Looser is dropped with a warning that names the scope it lost to. System still overrides outright. An unrecognized workspace value ranks strictest, so it is kept and reaches the readers, which already fail closed: the gate holds on it and the switch is off for anything but true. The serve routes' workspace write guard refuses the loosest value of such a key up front rather than persisting an entry the merge would always drop.

The hold cause names the scope. The gate takes an optional scope getter beside the policy getter; the CLI answers it from the loaded settings. A held message carries the scope for the two setting-driven causes, and the notice and /peers word it as "this repository's settings hold messages from other sessions" or "a system setting holds …" instead of "your setting". The scope is decoration: a throwing scope getter is ignored, never turned into a verdict.

The trusted-controller layer proposed in the same issue is deliberately not here; it is a separate change.

Why it's needed

The per-action backstop guards single actions. It does not guard what the session is being talked into. A message from a session that runs unreviewed is model-authored input that no person has looked at; delivered straight into a session whose user chose "review everything", it can steer that user's model one benign-looking step at a time, and the per-action prompts are exactly the surface that fatigue turns into rubber stamps. The user chose a mode in which they see everything, and that choice did not include hearing from agents nobody is watching. The old row also made the table hard to hold in one's head: five rows, with the receiver's class deciding whether the sender's class mattered at all.

Dropping a workspace value in the loosening direction is right — a cloned repository must not open the user's session to peers or force accept — but dropping refuse and false as well took away the one direction a repository has a reason to set: a monorepo whose automation agents must not be able to reach a person's session, say. And a workspace value the gate could not read was stripped before the merge, so the user's looser value took its place and the unreadable value never reached the code that fails closed on it.

Reviewer Test Plan

How to verify

Unit level: cd packages/core && npx vitest run src/ipc/inbound-gate.test.ts src/ipc/peer-send.test.ts src/ipc/peer-frames.test.ts src/ipc/uds-inbox.test.ts (261 tests; the gate suite now includes the full ten-row receiver-class × sender-class table, the unasserted sender held for every mode, release on a class change, the scope carried on held entries and dropped when the setting goes away, a throwing scope getter ignored, and the scope-aware wording). cd packages/cli && npx vitest run src/config/settings.test.ts src/config/settingsUtils.test.ts src/peerMessaging src/ui/commands/peers-command.test.ts src/serve/routes/workspace-settings.test.ts src/ui/startInteractiveUI.test.tsx src/ui/AppContainer.test.tsx (589 tests, including a loosening workspace value dropped with its warning, a tightening one honored, a repeat dropped silently, System overriding with its own warning, an unrecognized value kept, an untrusted workspace dropped, which scope the merged value came from, and an end-to-end hold of a bypassing sender over a real socket that releases when the receiver switches class). tsc --build on the cli package is clean, as are eslint and prettier --check on the changed files.

End to end, with { "agents": { "crossSessionMessaging": true } } on both sides:

  1. Receiver in default mode, no crossSessionInbound. A sender that asserts bypass is held; the notice says the two are in different review modes; /peers lists it with that cause; the sender gets a held receipt.
  2. Then the receiver switches to yolo: the held message is delivered without /peers accept, and the sender gets a delivered receipt after its held one.
  3. A sender that asserts no mode is held with "did not say whether it reviews each action".
  4. A sender that asserts prompting is delivered straight away.
  5. User settings crossSessionInbound: "accept", workspace "refuse": no warning at startup, the sender gets refused, nothing is held or delivered.
  6. User "hold", workspace "accept": a startup warning says the workspace value would loosen the User value, and the message is held with a notice naming your setting.
  7. No user value, workspace "hold": held, and the notice and /peers say the repository's settings hold messages from other sessions.
  8. User "accept", workspace "maybe": held with "could not be read … workspace settings", no loosening warning.

Evidence (Before & After)

Before: a receiver in default mode delivered a frame from a yolo session, or one with no fromMode, straight to its model. A workspace crossSessionInbound: "refuse" was dropped with Warning: … is ignored because it is restricted in workspace settings, and a workspace "maybe" was dropped the same way, leaving the user's "accept" in force.

After — Linux, bundled build of this branch rebased onto current main, under tmux with an isolated QWEN_HOME, dummy OpenAI-compatible provider. The sender is a small script that binds a socket, sends one frame with the fromMode the step names, and prints the receipts that come back over the same wire a real session uses.

1–2. Prompting receiver, bypassing sender; release on class switch
sent 55850329-f43b-4fba-9763-75949292477a (fromMode=bypass) to ws-plain-fc [fc35da]
RECEIPT held: Your message is held for the recipient user to review before it reaches their Qwen Code session.

Receiver:

  ●︎ Held a message from another session (the sender and this session are in different review modes: one reviews each action and the other
    can apply some without per-action review). 1 waiting — /peers to review.

  > /peers
      558503  [peer] smoke-sender
          step1: bypass sender to prompting receiver
          held because the sender and this session are in different review modes: one reviews each action and the other can apply some
    without per-action review, 5 minutes left

A second bypassing sender stayed connected; the receiver ran /approval-mode yolo. Both held messages were delivered with no /peers accept, and the live sender saw:

RECEIPT held: Your message is held for the recipient user to review before it reaches their Qwen Code session.
RECEIPT delivered: Your message was released to the recipient session.
3–4. Unasserted sender held, prompting sender delivered
sent acfd1217-1c5c-41a9-8a19-e11027fea492 (fromMode=none) to ws-plain-fc [fc35da]
RECEIPT held: Your message is held for the recipient user to review before it reaches their Qwen Code session.
  ●︎ Held a message from another session (the sender did not say whether it reviews each action). 1 waiting — /peers to review.

sent 0bdc8c3c-ab65-4364-87e4-5665bee2dcd3 (fromMode=prompting) to ws-plain-fc [fc35da]
RECEIPT delivered: Your message was released to the recipient session.
5. Workspace refuse over user accept

No warning at startup.

sent 19125fb9-f197-43dd-a595-32b9864e6236 (fromMode=prompting) to ws-refuse-52 [52b55c]
RECEIPT refused: The recipient session does not accept messages from other sessions, so nobody saw this one. Don't re-send it; reach that session's user another way.

  > /peers
  ●︎ No messages from other sessions are waiting.
6. Workspace accept under user hold is ignored

Startup:

Warning: agents.crossSessionInbound in workspace settings (/run/user/1001/smoke-a3/ws-accept/.qwen/settings.json) is ignored because it
would loosen the User value. A workspace may only make this setting stricter.
sent 537944fc-fb76-42a8-be77-17c65566b445 (fromMode=prompting) to ws-accept-a3 [a3daca]
RECEIPT held: Your message is held for the recipient user to review before it reaches their Qwen Code session.
  ●︎ Held a message from another session (your crossSessionInbound setting is "hold"). 1 waiting — /peers to review.
          held because your crossSessionInbound setting is "hold", 5 minutes left
7. Workspace hold names the repository

No warning at startup.

sent 8616e140-6fcd-486b-a4bf-8c4a3c672126 (fromMode=prompting) to ws-hold-cd [cd231d]
RECEIPT held: Your message is held for the recipient user to review before it reaches their Qwen Code session.
  ●︎ Held a message from another session (this repository's settings hold messages from other sessions (agents.crossSessionInbound is
    "hold" in workspace settings)). 1 waiting — /peers to review.
          held because this repository's settings hold messages from other sessions (agents.crossSessionInbound is "hold" in workspace
    settings), 5 minutes left
8. Unrecognized workspace value fails closed

No loosening warning at startup.

sent 8f28c67e-df5d-4cd1-8925-4b30b109e669 (fromMode=prompting) to ws-maybe-b8 [b8b7eb]
RECEIPT held: Your message is held for the recipient user to review before it reaches their Qwen Code session.
  ●︎ Held a message from another session (the agents.crossSessionInbound value in this repository's workspace settings could not be read).
    1 waiting — /peers to review.

Tested on

OS Status
🍏 macOS ⚠️
🪟 Windows N/A
🐧 Linux

Windows is N/A because the inbox is POSIX-only today; macOS runs the same code path but was not exercised locally.

Environment (optional)

Bundled build of this branch, run under tmux with a dummy OpenAI-compatible provider.

Risk & Scope

  • Main risk or tradeoff: a prompting receiver now holds messages from bypassing senders that it used to deliver. That is the point, and the cost is one /peers accept per message, or switching the receiver into the sender's class, which releases the backlog. Two sessions a user runs in the same mode notice nothing. Messages from senders that assert no class are held for prompting receivers too, which affects hand-written scripts that reach a session over its published token rather than its child token; such a script can assert fromMode honestly, and the child-token path is unaffected. Ranking unset for the inbound key means a workspace hold is honored when the user set nothing, because parity delivers some messages and hold delivers none; a workspace that wants exactly the parity default has no value to write for it, and unset is that value.
  • Not validated / out of scope: the trusted-controller layer (an external process the user wants driving their session earns delivery through explicit trust) is the follow-up in the same issue. Neither key is offered through the serve settings routes today, so the write guard there is exercised by unit test only. Headless and ACP sessions do not bind an inbox and are unaffected.
  • Breaking changes / migration notes: the wire protocol, fromMode's vocabulary, receipts, and every explicit setting path are unchanged. A workspace crossSessionInbound of hold or refuse, or crossSessionMessaging: false, that was previously dropped is now honored; a workspace value that would loosen the user's is still dropped, with a warning that now says why.

Linked Issues

Implements the first two parts of #10925 (the third part is a separate change). Continues #10800 and #10809. Related to #10118, whose trusted-tier dependency this line answers.

中文说明

本 PR 做了什么

在没有显式 agents.crossSessionInbound 的情况下,入站闸门按审批模式的对等关系判定,而那张表里有一行让"逐项审阅每个动作"的接收方接受任何东西:绕过审阅的发送方,或者根本没有声明模式的发送方。本 PR 把那张表换成一条规则,让仓库可以让它的会话更谨慎但绝不能更宽松,并让留置提示说明是哪个作用域在留置这条消息。

一条规则。 每种审批模式属于两个审阅类别之一:prompting(default、plan)或 bypass(auto-edit、auto、yolo),消息只在同一类别的会话之间自动投递。逐项审阅的接收方现在会留置来自绕过类发送方的消息,而所有接收方都会留置没有声明类别的消息。分类是一个导出的函数,发送端也用它,所以处于同一模式的两个会话对自己的类别永远一致。因类别不匹配被留置的消息,在接收方的模式切换到发送方的类别时仍会立即释放,无需用户手动批准。显式设置在两个方向上都优先,本进程的帧仍被接受,线协议不变。

仓库只能收紧。 两个 agents.* 键此前都在受限列表中,工作区的值会被整个丢弃,警告读起来像是个 bug。它们移到一个新的"仅收紧"列表:合并时,受信工作区的值只有在严格严于 User 或 SystemDefaults 所设的值(accept < 未设置 < hold < refusetrue < false = 未设置)、或两者都未设置时严于功能默认值,才会被保留。相等的静默丢弃。更宽松的丢弃并给出警告,点名它输给了哪个作用域。System 仍然直接覆盖。无法识别的工作区值排在最严,因此会被保留并到达读取方,而读取方本就 fail-closed:闸门据此留置,开关对 true 以外的任何值都视为关闭。serve 路由的工作区写入守卫会在前端就拒绝这类键的最宽松值,而不是持久化一条合并时总会被丢弃的条目。

留置原因点名作用域。 闸门在策略读取器旁增加一个可选的作用域读取器;CLI 从已加载的设置中回答它。被留置的消息对两种由设置驱动的原因携带作用域,提示与 /peers 的措辞变为"this repository's settings hold messages from other sessions"或"a system setting holds …",而不是"your setting"。作用域只是修饰:作用域读取器抛错会被忽略,绝不会变成判定。

同一 issue 中提出的受信 controller 层刻意不在本 PR 中,它是单独的改动。

为什么需要

逐项审阅的后盾守的是单个动作,守不住会话正在被说服去做什么。来自无人审阅会话的消息是没有任何人看过的、由模型撰写的输入;直接投递到一个用户选择了"审阅一切"的会话里,它可以一步一步、每步看起来都无害地引导那个用户的模型,而逐项审阅的提示正是审阅疲劳变成盖章的地方。用户选择了一种能看到一切的模式,这个选择并不包括听取没人盯着的 agent 的话。旧的那一行还让表难以记住:五行,且由接收方的类别决定发送方的类别是否重要。

在放宽方向上丢弃工作区的值是对的——克隆下来的仓库不能把用户的会话向对等方打开,或强制 accept——但连 refusefalse 也一起丢掉,就拿走了仓库唯一有理由设置的方向:比如一个 monorepo 的自动化 agent 不得触达人的会话。而闸门无法读取的工作区值在合并前就被剥掉,用户更宽松的值取而代之,那个无法读取的值从未到达本会 fail-closed 的代码。

审阅者测试计划

如何验证

单元层面:cd packages/core && npx vitest run src/ipc/inbound-gate.test.ts src/ipc/peer-send.test.ts src/ipc/peer-frames.test.ts src/ipc/uds-inbox.test.ts(261 个测试;闸门套件现在包含完整的十行"接收方类别 × 发送方类别"表、未声明的发送方在每种模式下都被留置、类别切换时释放、留置条目携带作用域并在设置消失时丢弃、抛错的作用域读取器被忽略、以及按作用域的措辞)。cd packages/cli && npx vitest run src/config/settings.test.ts src/config/settingsUtils.test.ts src/peerMessaging src/ui/commands/peers-command.test.ts src/serve/routes/workspace-settings.test.ts src/ui/startInteractiveUI.test.tsx src/ui/AppContainer.test.tsx(589 个测试,包括放宽的工作区值被丢弃并给出警告、收紧的被采纳、重复的静默丢弃、System 覆盖并给出自己的警告、无法识别的值被保留、不受信工作区被丢弃、合并值来自哪个作用域、以及一条经真实 socket 的端到端用例:绕过类发送方被留置,接收方切换类别后释放)。cli 包的 tsc --build 干净,改动文件的 eslintprettier --check 也干净。

端到端,双方都开启 { "agents": { "crossSessionMessaging": true } }

  1. 接收方处于 default 模式,无 crossSessionInbound。声明 bypass 的发送方被留置;提示说两者处于不同的审阅模式;/peers 以该原因列出它;发送方收到 held 回执。
  2. 随后接收方切换到 yolo:留置的消息无需 /peers accept 即投递,发送方在 held 之后收到 delivered 回执。
  3. 未声明模式的发送方被留置,原因为"did not say whether it reviews each action"。
  4. 声明 prompting 的发送方立即投递。
  5. 用户设置 crossSessionInbound: "accept",工作区 "refuse":启动无警告,发送方收到 refused,无留置无投递。
  6. 用户 "hold",工作区 "accept":启动警告说工作区的值会放宽 User 的值,消息被留置且提示点名的是你的设置。
  7. 无用户值,工作区 "hold":被留置,提示和 /peers 说是仓库的设置在留置来自其他会话的消息。
  8. 用户 "accept",工作区 "maybe":被留置,原因为"could not be read … workspace settings",无放宽警告。

证据(之前与之后)

之前:default 模式的接收方会把来自 yolo 会话的帧、或没有 fromMode 的帧直接投递给它的模型。工作区的 crossSessionInbound: "refuse" 被丢弃并提示 Warning: … is ignored because it is restricted in workspace settings,工作区的 "maybe" 也以同样方式被丢弃,留下用户的 "accept" 生效。

之后——Linux,本分支 rebase 到当前 main 后的打包构建,在 tmux 下使用隔离的 QWEN_HOME 与 dummy 的 OpenAI 兼容 provider。发送方是一个小脚本:绑定一个 socket、按步骤指定的 fromMode 发一帧、打印回来的回执,走的是与真实会话相同的线协议。英文部分的折叠块是八个步骤的实机记录。

测试平台

见英文表格:Linux 已测;inbox 目前仅支持 POSIX,因此 Windows 为 N/A;macOS 走同一条代码路径但本地未实测。

运行环境(可选)

本分支的打包构建,在 tmux 下配合一个 dummy 的 OpenAI 兼容 provider 运行。

风险与范围

  • 主要风险或取舍:逐项审阅的接收方现在会留置此前会投递的、来自绕过类发送方的消息。这正是目的,代价是每条消息一次 /peers accept,或者把接收方切换到发送方的类别以释放积压。用户以同一模式运行的两个会话不会察觉任何变化。未声明类别的发送方的消息对逐项审阅的接收方也会被留置,这会影响通过已发布令牌(而非子令牌)触达会话的手写脚本;这类脚本可以如实声明 fromMode,子令牌路径不受影响。对入站键把"未设置"纳入排序意味着用户什么都没设时工作区的 hold 会被采纳,因为对等判定会投递一部分消息而 hold 一条都不投递;想要恰好等于对等默认值的工作区没有可写的值,未设置就是那个值。
  • 未验证 / 范围之外:受信 controller 层(用户希望驱动自己会话的外部进程通过显式授信获得投递)是同一 issue 中的后续改动。两个键目前都不经 serve 的设置路由提供,因此那里的写入守卫仅由单元测试覆盖。headless 与 ACP 会话不绑定 inbox,不受影响。
  • 破坏性变更 / 迁移说明:线协议、fromMode 的词汇、回执以及所有显式设置路径均未改变。此前被丢弃的工作区 crossSessionInboundholdrefuse、或 crossSessionMessaging: false,现在会被采纳;会放宽用户值的工作区值仍被丢弃,警告现在会说明原因。

关联 Issue

实现 #10925 的前两部分(第三部分是单独的改动)。接续 #10800#10809。与 #10118 相关,本线回答其受信分层的依赖。

… only tighten inbound settings

Auto-deliver only between sessions of the same review class, hold frames
that assert no mode, and move the two agents.* keys from restricted to
tighten-only so a repository can refuse peers but never open a session.
The hold notice names the scope that holds the message.
@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 4, 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 one is unusually well documented, and the design doc plus the before/after evidence made it easy to follow.

Template ✓ — every required heading is present, Tested on is filled in honestly (macOS ⚠️, Windows N/A with a reason), and the Chinese translation is complete.

Problem: real, not theoretical — and the strongest evidence for it is already in main. packages/core/src/tools/send-message.ts sets approvalMode = null when the mode getter throws, with the comment "Asserting nothing makes the receiver hold the message rather than trust a mode class this side could not read." That comment is currently false for a prompting receiver: the receiverReviewsActions(mode) → accept shortcut in resolvePolicy delivers the frame anyway. So the send side already documents the intent this PR implements, and the two sides disagree today. Same for the workspace-settings half — a repository writing crossSessionInbound: "refuse" gets "is ignored because it is restricted", which reads as a bug because it is one.

Direction: sound. Both halves move fail-closed: the gate holds strictly more than it did, and a repository gains the ability to tighten but never to loosen. It sits inside the multi-agent safety work the roadmap already carries, and it follows #10925 parts 1–2 with part 3 explicitly deferred rather than smuggled in. One honest flag: this is a category/security change to a trust boundary between processes, and #10925 is authored by the same contributor and still open — so the policy choice (is symmetric class parity the right rule, and should a repository be able to tighten at all) is a call a maintainer should own rather than something this gate should settle. Raising that as a question, not a block.

Size: Stage 0 applies — this touches packages/core/src/ipc/** and packages/cli/src/config/**. Breakdown of the 1358 changed lines: 451 production code, 723 tests, 184 docs. feat type, so no size hard block, and 451 is under the 500-line maintainer-awareness threshold and well under the 1000-line advisory. Worth noting the ratio: more test lines than production lines, which is the right shape for a gate change.

Approach: the gate half is genuinely minimal — delete the shortcut, compare classes, export modeClass so the send side stops keeping its own copy. The settings half is where the complexity is, so I tried to find a cheaper version and could not. The obvious simplification is "drop a workspace value only when it is the loosest possible (accept / true), keep everything else" — which the PR's own isLoosestTightenOnlyValue already expresses in one function. That covers every case #10925 asked for except one: a workspace hold over a user refuse would be kept and then loosen the effective policy. The strictness ranking exists precisely to stop that, so it earns its place. The scope-naming piece (PolicyScope, inbound-policy-scope.ts, scope-aware describeHoldCause) looked like bundled polish at first, but it is coupled rather than creeping: before this PR a workspace could never set the key, so "your setting is hold" was always true. Part 2 is what makes that wording a lie, and part 3 fixes it in the same change. The one thing I would genuinely consider splitting is part 1 from parts 2+3 — the gate fix is the security-relevant half and could land on its own without waiting on the settings-precedence work. Not a blocker; your call, and I can see the argument either way since #10925 frames them together.

Risk: no elevated risk signals — none of the changed files match the revert-correlated high-risk paths. The behaviour change is user-visible by design and the PR names its cost plainly (one /peers accept per mismatched message, or switch the receiver into the sender's class).

Moving on to code review. 🔍

中文说明

感谢贡献!这个 PR 的文档质量相当高,设计文档加上 before/after 证据让它很容易看懂。

模板 ✓ —— 所有必需标题齐全,Tested on 填写诚实(macOS ⚠️、Windows N/A 并说明原因),中文翻译完整。

问题: 真实存在,不是理论性加固 —— 最有力的证据已经在 main 上了。packages/core/src/tools/send-message.ts 在 mode getter 抛异常时会设 approvalMode = null,注释写着"什么都不声明会让接收方留置这条消息,而不是信任本侧读不到的 mode 类别"。但对逐项审阅的接收方来说,这句注释目前是假的resolvePolicy 里的 receiverReviewsActions(mode) → accept 捷径照样投递。也就是说发送侧早已写明了本 PR 要实现的意图,而两侧今天互相矛盾。工作区设置那一半同理 —— 仓库写 crossSessionInbound: "refuse" 会得到"is ignored because it is restricted",这读起来像 bug,因为它就是 bug。

方向: 合理。两半都是 fail-closed 方向移动:网关比原来留置更多,仓库获得收紧能力但永远无法放宽。它属于路线图已经在推进的多智能体安全工作,并且遵循 #10925 的第 1、2 部分,第 3 部分明确推迟而不是夹带进来。一个诚实的提醒:这是一个触及进程间信任边界的 category/security 改动,而 #10925 由同一位贡献者提出且仍处于 open 状态 —— 所以策略选择(对称类别对等是否是正确的规则、仓库是否应当有权收紧)应该由 maintainer 来定,而不是由这个关卡来裁定。这是作为问题提出,不是阻塞。

规模: Stage 0 适用 —— 触及 packages/core/src/ipc/**packages/cli/src/config/**。1358 行改动的构成:451 行生产代码、723 行测试、184 行文档。feat 类型,因此不触发规模硬阻塞;451 低于 500 行的维护者关注阈值,也远低于 1000 行的大 PR 建议阈值。值得一提的是比例:测试行数多于生产行数,这对网关改动来说是正确的形态。

方案: 网关那一半确实是最小改动 —— 删掉捷径、比较类别、导出 modeClass 让发送侧不再保留自己的副本。设置那一半是复杂度所在,所以我试着找一个更便宜的版本,没找到。显而易见的简化是"只在工作区值是最宽松(accept / true)时丢弃,其余全部保留"—— PR 自己的 isLoosestTightenOnlyValue 用一个函数就表达了它。它覆盖 #10925 要求的所有情况,只除了一种:工作区 hold 叠在用户 refuse 之上时会被保留,然后放宽生效策略。严格度排序正是为了阻止这一点而存在,所以它有存在的价值。作用域命名那部分(PolicyScopeinbound-policy-scope.ts、感知作用域的 describeHoldCause)一开始看起来像夹带的润色,但它是耦合的而非蔓延:本 PR 之前工作区根本无法设置该键,所以"your setting is hold"永远是真的。是第 2 部分让这句措辞变成了谎言,第 3 部分在同一个改动里修好它。我唯一真正会考虑拆分的是把第 1 部分与第 2+3 部分分开 —— 网关修复是安全相关的那一半,本可以独立落地,不必等设置优先级的工作。这不是阻塞项;由你决定,而且我认为两种说法都成立,因为 #10925 是把它们放在一起表述的。

风险: 无升级风险信号 —— 改动的文件都不匹配与回滚相关的高风险路径。行为变化是设计上就用户可见的,PR 也直接说明了代价(每条不匹配消息一次 /peers accept,或把接收方切换到发送方的类别)。

进入代码审查 🔍

Qwen Code · qwen3.8-max-2026-09-02

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

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Code review

I read the diff against the base tree rather than just the hunks, and traced every consumer of the symbols this PR adds or removes. One blocking problem, and it is not in the TypeScript.

🚫 Blocker: unresolved merge-conflict markers shipped in user docs

docs/users/features/commands.md contains a committed, unresolved rebase conflict. Verified against the exact reviewed commit by fetching the file's raw bytes at 1a3bf76ae7c369c8278093b586f0c6b8de2174ca:

827:<<<<<<< HEAD
852:=======
873:>>>>>>> 28cefb43e (feat(ipc): hold peer messages across review classes, let repositories only tighten inbound settings)

The same file at main has zero conflict markers, so this is introduced by this PR, not inherited. Three consequences:

  • The published page renders literal VCS markers mid-sentence, right after "agents.crossSessionInbound (accept, hold, or refuse). When unset,".
  • Both the old rule and the new rule appear, contradicting each other for a security-relevant setting. The HEAD side still tells users a message "is delivered if the receiving session still reviews each action (default or plan mode)" — the exact behaviour this PR removes.
  • The resolution is not "take theirs". The HEAD side carries three paragraphs that exist only there — agents.crossSessionHeldExpiry expiry semantics, the inbox-bind-failure path (XDG_RUNTIME_DIR / TMPDIR), and the duplicate-inbox-address behaviour, i.e. the docs added by the reachability and receipt work this PR says it follows. Accepting only the PR side would silently delete all three. The correct resolution keeps the new class-parity paragraph and the repository-tightening paragraph, then re-appends the expiry, bind-failure, and duplicate-address paragraphs from HEAD.

No CI check catches this — the markdown is not linted for conflict markers, and Lint & Static does not cover it — so this would merge green. It also makes me want to re-confirm the tmux evidence below was captured against this head commit, since the branch evidently went through a rebase after that capture.

What I verified as correct

The gate change is small and I could not break it:

  • Ordering is preserved. Explicit setting still beats selfSent, selfSent still beats parity, mode-unknown still fails closed. Splitting explicit === 'hold' out of the shared return only attaches a scope; dropping cause from the accept/refuse return is inert because admit() reads decision.cause only when policy === 'hold'.
  • Fail-closed holds end to end. peer-frames.ts keeps fromMode only when it is exactly 'bypass' or 'prompting', so a hostile or older wire value becomes undefinedno-mode-asserted → hold, not accept. Unrecognized setting values rank Infinity, so they are kept and reach the gate, which holds on policy-unreadable — that is the fix for the strip-lets-a-looser-value-through bug, and it is the right way round.
  • No dead switches. Every optional surface this PR adds is actually populated and actually read: getPolicyScope is set in startInteractiveUI.tsx, HeldMessage.policyScope is read at both consumers (AppContainer.tsx hold notice, peers-command.ts listing), and describeHoldCause's new scope parameter is passed at both call sites. PolicyScope / ModeClass / modeClass reach the CLI through the existing export * from './ipc/inbound-gate.js', so no index change was needed.
  • Scope really is decoration. policyScope() catches a throwing getter and returns undefined — never a verdict — and withCause drops a stale policyScope when the re-evaluated decision carries none, while preserving object identity when neither cause nor scope changed. Both are pinned by tests, including the identity check by reference.
  • Release-on-class-change is real, not just claimed. AppContainer.tsx already calls reevaluate('approval-mode-changed'), and the new test drives DEFAULT → hold → YOLO → released with no /peers accept. The complementary test confirms an unasserted sender stays held across a mode change, which is correct since there is no class to converge on.
  • The tests are load-bearing. The old assertion 'accepts anything when the receiver still prompts' expected accept for a bypass sender and for no assertion; this PR replaces it with held expectations and adds a ten-row receiver-class × sender-class table written specifically so a future row cannot reopen the shortcut. Reverting the production change fails the suite — that is the property that matters.
  • The wire protocol genuinely does not change. senderModeClass already returned 'bypass' | 'prompting'; it now delegates to modeClass, removing the duplicated classification rather than adding one.

Non-blocking

  • The serve-route guard is unreachable today. Both keys have showInDialog: false in settingsSchema.ts, so getAllowedKeys() never contains them and the !allowedKeys.has(key) check answers 400 disallowed_key before rejectWorkspaceRestrictedWrite runs. The design doc discloses this ("the guard is there for when one is"), which is fair — but it is 8 lines defending a path that cannot be reached, and the house rule is no speculative code. Your call.
  • Related latent drift. getDialogSettingKeys({ excludeWorkspaceRestricted: true }) filters on WORKSPACE_RESTRICTED_SETTING_KEYS, and these two keys just left that list. Harmless while showInDialog is off, but if either key is ever surfaced, the Workspace-scope dialog would offer accept / true and write a dead entry — precisely the trap the serve guard exists for. The new tighten-only list has no dialog-side counterpart, and Cross-session messaging: symmetric class parity, tighten-only repository settings, and a trusted-controller layer #10925 asked for one list driving all three consumers so they cannot drift. Worth a line in the design doc naming that gap.
  • tightenOnlyVerdict compares against the stricter of User and SystemDefaults. This matches your stated rule and the design doc, and it is unreachable in practice unless an admin writes the key into the SystemDefaults file. But the consequence is worth confirming as intended: with SystemDefaults hold and an explicit User accept, a workspace hold is dropped silently as same while the effective policy stays accept — a repository tightening to exactly the operator floor gets neither an effect nor a warning.

The flow under review

sequenceDiagram
    participant P1 as send_message tool
    participant P2 as peer frame over UDS
    participant P3 as InboundGate
    participant P4 as held buffer
    participant P5 as receiver UI

    P1->>P1: read approval mode, null when the getter throws
    P1->>P2: fromMode is modeClass of the mode, omitted when null
    P2->>P3: admit frame with origin from the transport
    P3->>P3: explicit setting wins, scope only decorates the cause
    P3->>P3: own process via child token still accepts
    P3->>P3: unknown receiver mode still holds
    P3->>P3: compare sender class with modeClass of receiver mode
    alt same class
        P3->>P5: deliver
    else different class, or sender asserted nothing
        P3->>P4: park with cause and policyScope
        P4->>P5: hold notice and /peers name the cause and the scope
    end
    P5->>P3: reevaluate when the approval mode changes
    P3->>P4: classes now agree, release with no manual accept
Loading

Test evidence

This is an unattended CI run, so per the gate's rules I did not build, run, or execute anything from this branch — no vitest, no bundle, no gh pr checkout. The evidence below is this PR's own CI, read through the API for the reviewed commit, plus static reading of the diff against the base tree. Nothing here is my own execution, and the tmux transcripts in the PR description are the author's claim, not independently re-run by me.

At the time of writing there are 0 failed checks on 1a3bf76ae7c369c8278093b586f0c6b8de2174ca. The unit suite (Test (ubuntu-latest, Node 22.x)), Lint & Static, Serve A/B, and Real daemon E2E / Java 11 are still in progress — I fetched once and am reporting what exists rather than polling, so those rows are pending, not green. Test (macos-latest) and Test (windows-latest) are skipped on this commit, so the suite signal is Linux-only so far. The finalize workflow updates the table in place once CI settles.

Worth saying plainly: no check in this table would catch the conflict markers above. Markdown is not linted for VCS markers, so a green run here does not mean the docs are shippable.

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

Check Conclusion
web-shell E2E Smoke (ubuntu-latest, Node 22.x) 🚫 cancelled
Classify PR ✅ success
Dependency CVE audit ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Integration Tests (no-AK, No Sandbox) ✅ success
Lint & Static (ubuntu-latest, Node 22.x) ✅ success
macos-latest / Java 21 ✅ success
OpenTUI no-flicker gate ✅ success
Real daemon E2E / Java 11 ✅ success
Secret scan (TruffleHog) ✅ success
Serve A/B (ubuntu-latest, Node 22.x) ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success
TUI parity snapshots (ink vs opentui) ✅ success
ubuntu-latest / Java 11 ✅ success
ubuntu-latest / Java 17 ✅ success
ubuntu-latest / Java 21 ✅ success
windows-latest / Java 21 ✅ success

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

Sandboxed verification would settle the part I cannot: @qwen-code /tmux — that the hold notice and the /peers listing actually render the scope-aware wording ("this repository's settings hold messages from other sessions") instead of "your setting", and that switching the receiver into the sender's class really releases the backlog with no manual accept. The unit tests assert on substrings of describeHoldCause, so a wiring mistake between policyScope and the rendered string would pass them and still show the user the wrong blame. And @qwen-code /verify — that the new hold is load-bearing A/B against the base build, specifically that the ten-row class table fails on base and passes here, and that an unrecognized workspace value is genuinely kept and fails closed rather than stripped in favour of a looser user value. You have write access, so both lanes are available without sponsorship; given the branch went through a rebase that left a conflict behind, re-confirming the behaviour at the current head is worth more than usual here.

Real-scenario testing: N/A for this run — unattended CI never drives the product, and I will not substitute the PR description's transcripts for a capture of my own.

中文说明

代码审查

我把 diff 对照 base 树完整读了一遍,而不只看 hunk,并追踪了这个 PR 新增或删除的每个符号的所有使用方。有一个阻塞问题,而且它不在 TypeScript 里。

🚫 阻塞项:用户文档中残留未解决的合并冲突标记

docs/users/features/commands.md 里有一个被提交进来的、未解决的 rebase 冲突。已对照被审查的那个 commit 验证 —— 通过 API 拉取 1a3bf76ae7c369c8278093b586f0c6b8de2174ca 上该文件的原始字节:

827:<<<<<<< HEAD
852:=======
873:>>>>>>> 28cefb43e (feat(ipc): hold peer messages across review classes, let repositories only tighten inbound settings)

同一文件在 main 上冲突标记数为 ,所以这是本 PR 引入的,不是继承来的。三个后果:

  • 发布后的页面会在句子中间渲染出字面的版本控制标记,位置正好在 "agents.crossSessionInbound (acceptholdrefuse)。未设置时," 之后。
  • 规则和规则同时出现,对一个安全相关的设置自相矛盾。HEAD 那一侧仍然告诉用户消息 "在接收会话仍逐项审阅每个动作时(default 或 plan 模式)会被投递" —— 正是本 PR 要移除的行为。
  • 解法不是"取 theirs"。 HEAD 那一侧带着三段只存在于那里的内容 —— agents.crossSessionHeldExpiry 过期语义、inbox 绑定失败路径(XDG_RUNTIME_DIR / TMPDIR)、以及 inbox 地址重复时的行为,也就是本 PR 说自己接续的可达性与回执工作所添加的文档。只接受 PR 那一侧会静默删掉这三段。正确的解法是保留新的类别对等段落和仓库收紧段落,然后把 HEAD 侧的过期、绑定失败、地址重复三段重新接回去。

没有任何 CI 检查会捕获这一点 —— markdown 不做冲突标记检查,Lint & Static 也不覆盖 —— 所以它会以全绿状态被合并。这也让我想重新确认下面的 tmux 证据是否是在这个 head commit 上采集的,因为该分支显然在那次采集之后又经历过一次 rebase。

我验证为正确的部分

网关改动很小,我没能把它弄坏:

  • 顺序保持不变。 显式设置仍然优先于 selfSentselfSent 仍然优先于对等判定,mode-unknown 仍然 fail closed。把 explicit === 'hold' 从共用 return 中拆出来只是为了附加作用域;从 accept/refuse 的返回值中去掉 cause 是惰性的,因为 admit() 只在 policy === 'hold' 时读取 decision.cause
  • 端到端都 fail closed。 peer-frames.ts 只在 fromMode 恰好是 'bypass''prompting' 时保留它,所以恶意或旧版本的线上值会变成 undefinedno-mode-asserted → 留置,而不是接受。无法识别的设置值排序为 Infinity,因此会被保留并送达网关,网关以 policy-unreadable 留置 —— 这正是对"剥离导致更宽松的值生效"这个 bug 的修复,而且方向是对的。
  • 没有死开关。 本 PR 新增的每个可选面都确实被填充、也确实被读取:getPolicyScopestartInteractiveUI.tsx 中被设置,HeldMessage.policyScope 在两个使用方都被读取(AppContainer.tsx 的留置提示、peers-command.ts 的列表),describeHoldCause 新增的 scope 参数在两个调用点都被传入。PolicyScope / ModeClass / modeClass 通过已有的 export * from './ipc/inbound-gate.js' 到达 CLI,因此不需要改 index。
  • 作用域确实只是装饰。 policyScope() 会捕获抛异常的 getter 并返回 undefined —— 绝不返回裁决 —— 而 withCause 在重新评估后的决定不携带作用域时会丢弃过期的 policyScope,同时在 cause 和 scope 都未变化时保留对象标识。两者都有测试固定,包括按引用比较的标识检查。
  • 类别切换后释放是真实的,不只是声称。 AppContainer.tsx 已经在调用 reevaluate('approval-mode-changed'),新测试驱动了 DEFAULT → 留置 → YOLO → 无需 /peers accept 即释放。配套的测试确认未声明类别的发送方在模式切换后仍被留置,这是正确的,因为没有类别可以收敛。
  • 测试是承重的。 旧断言 'accepts anything when the receiver still prompts' 对 bypass 发送方和无声明都期望 accept;本 PR 用期望 held 的测试替换了它,并新增了一个十行的接收方类别 × 发送方类别全表,明确写成让未来的某一行无法悄悄重新打开这个捷径。回滚生产代码改动会让测试套件失败 —— 这才是关键性质。
  • 线上协议确实没有变化。 senderModeClass 本来就返回 'bypass' | 'prompting';现在它委托给 modeClass,这是移除重复的分类逻辑而不是新增一份。

非阻塞

  • serve 路由的写入守卫今天不可达。 两个键在 settingsSchema.ts 中都是 showInDialog: false,所以 getAllowedKeys() 从不包含它们,!allowedKeys.has(key) 检查会在 rejectWorkspaceRestrictedWrite 运行之前就以 400 disallowed_key 回应。设计文档披露了这一点("守卫是为将来某个键开放时准备的"),这是合理的 —— 但它是 8 行在守护一条无法到达的路径,而项目规则是不写推测性代码。由你决定。
  • 相关的潜在漂移。 getDialogSettingKeys({ excludeWorkspaceRestricted: true }) 基于 WORKSPACE_RESTRICTED_SETTING_KEYS 过滤,而这两个键刚刚离开了那个列表。在 showInDialog 关闭时无害,但如果任一键将来被暴露,Workspace 作用域的设置对话框就会提供 accept / true 并写入一个死条目 —— 正是那个写入守卫存在的理由所要防的陷阱。新的 tighten-only 列表没有对话框侧的对应物,而 Cross-session messaging: symmetric class parity, tighten-only repository settings, and a trusted-controller layer #10925 要求的是由一个列表驱动全部三个使用方以免漂移。值得在设计文档里写一行点明这个缺口。
  • tightenOnlyVerdict 是与 User 和 SystemDefaults 中更严格的那个比较。 这符合你陈述的规则和设计文档,而且实际上不可达,除非管理员把该键写进 SystemDefaults 文件。但其后果值得确认是否为预期:当 SystemDefaults 为 hold 且 User 显式为 accept 时,工作区的 hold 会被当作 same 静默丢弃,而生效策略仍是 accept —— 一个试图收紧到恰好等于运维基线的仓库,既得不到效果也得不到警告。

测试证据

这是一次无人值守的 CI 运行,因此按照关卡规则我没有构建、运行或执行本分支的任何东西 —— 没有 vitest,没有打包,没有 gh pr checkout。下面的证据是本 PR 自己的 CI,通过 API 针对被审查的 commit 读取,加上把 diff 对照 base 树的静态阅读。这里没有我自己的执行,PR 描述中的 tmux 记录是作者的声明,不是我独立重跑的结果

撰写本文时,1a3bf76ae7c369c8278093b586f0c6b8de2174ca失败检查数为 0。单元测试套件(Test (ubuntu-latest, Node 22.x))、Lint & StaticServe A/BReal daemon E2E / Java 11 仍在进行中 —— 我只拉取一次并报告当前状态而不轮询,所以这些行是待定而非绿色。Test (macos-latest)Test (windows-latest) 在此 commit 上被跳过,因此目前的套件信号只覆盖 Linux。CI 结束后 finalize 工作流会就地更新该表格。

需要直说:这张表里没有任何检查会捕获上面的冲突标记。 Markdown 不做版本控制标记检查,所以这里全绿并不意味着文档可以发布。

沙箱化验证可以解决我无法解决的那部分:@qwen-code /tmux —— 验证留置提示和 /peers 列表是否真的渲染出感知作用域的措辞("this repository's settings hold messages from other sessions")而不是 "your setting",以及把接收方切换到发送方的类别是否真的无需手动接受就释放积压。单元测试断言的是 describeHoldCause 的子串,所以 policyScope 与渲染字符串之间的接线错误会通过测试,却仍然向用户显示错误的归因。以及 @qwen-code /verify —— 对照 base 构建做 A/B,验证新的留置是承重的,特别是十行类别全表在 base 上失败、在这里通过,以及无法识别的工作区值确实被保留并 fail closed,而不是被剥离从而让更宽松的用户值生效。你有写权限,所以两条通道都无需赞助即可使用;鉴于该分支经历过一次留下冲突的 rebase,在当前 head 上重新确认行为比平时更有价值。

真实场景测试:本次运行 N/A —— 无人值守 CI 从不驱动产品,我也不会用 PR 描述里的记录来替代我自己的采集。

Qwen Code · qwen3.8-max-2026-09-02

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 2/5 — the TypeScript is good and I could not break it, but the branch ships an unresolved rebase conflict in user-facing docs for a security-relevant setting, and that is not a nit.

Stepping back: the engineering here is well above the bar. The gate change is the minimal version of itself, the fail-closed reasoning is consistent at every layer I checked (wire parse, strictness ranking, throwing getters, stale scope on re-evaluation), and the tests would actually fail if someone reverted the production change — which is the property most PRs in this area only claim. The design doc is committed rather than pasted into the PR body, and it names its own trade-offs including the one I would have pushed on.

So this is not a "rethink the approach" request. It is one concrete defect and one question.

The defect is the conflict markers in docs/users/features/commands.md. What makes it more than cosmetic is that the file currently documents both the old and the new gate rule at once, and the old one is the rule this PR exists to remove — so the docs for a category/security setting are actively wrong on the exact point the change turns on. It also needs care to fix: taking the PR side would delete the hold-expiry, inbox-bind-failure, and duplicate-address paragraphs that only exist on the HEAD side. Please resolve it as both — the new class-parity and repository-tightening paragraphs, then the three existing paragraphs re-appended — and grep the branch for <<<<<<< before re-pushing, since one leftover usually means the rebase was abandoned rather than finished.

The question is about ownership, not code. This changes who a session will listen to and what a repository may decide about that, which is a security-policy call, and #10925 is your own issue and still open. I think the direction is right — both halves move fail-closed, and the send-side comment in main already assumes the rule you are implementing — but "the gate agrees" is not the same as "a maintainer has signed off on the policy". I tried to resolve an accountable owner to hand this to and could not: no maintainer handle is configured, the PR carries no labels so the area map matched nothing, and there is no human reviewer on the thread yet. I am not going to guess a login, so I am leaving that routing to whoever picks this up. Once the docs conflict is resolved, my read is that the code needs no further changes and the remaining call is the policy one.

Requesting changes on the strength of the docs conflict alone. 🙏

中文说明

信心:2/5 —— TypeScript 部分很好,我没能把它弄坏,但分支在面向用户的文档里带进了一个未解决的 rebase 冲突,而且是一个安全相关设置的文档,这不是小问题。

退一步看:这里的工程质量高于门槛。网关改动是它自己的最小版本,fail-closed 的推理在我检查的每一层都一致(线上解析、严格度排序、抛异常的 getter、重新评估时的过期作用域),而且如果有人回滚生产代码改动,测试确实会失败 —— 这是这个领域大多数 PR 只是声称具备的性质。设计文档是提交进仓库的而不是粘贴在 PR 正文里,并且它自己点名了取舍,包括我本来会质疑的那一条。

所以这不是一个"重新考虑方案"的请求。它是一个具体缺陷加一个问题。

缺陷是 docs/users/features/commands.md 里的冲突标记。让它不只是外观问题的是:该文件目前同时记录了旧的和新的网关规则,而旧的那条正是本 PR 存在所要移除的规则 —— 所以一个 category/security 设置的文档,在这个改动所围绕的关键点上主动写错了。修它也需要小心:取 PR 那一侧会删掉只存在于 HEAD 一侧的留置过期、inbox 绑定失败、地址重复这三段。请把它解成两侧都要 —— 新的类别对等段落和仓库收紧段落,然后重新接回已有的三段 —— 并在重新推送前 grep 分支里的 <<<<<<<,因为残留一处通常意味着 rebase 是被放弃了而不是完成了。

问题是关于归属,不是关于代码。这改变了会话愿意听谁的,以及仓库对此能决定什么,这是一个安全策略判断,而 #10925 是你自己的 issue 且仍然 open。我认为方向是对的 —— 两半都朝 fail-closed 移动,而且 main 里发送侧的注释已经假设了你要实现的规则 —— 但"关卡同意"和"maintainer 已批准该策略"不是一回事。我试着解析出一个可交接的责任人但没成功:没有配置 maintainer handle,PR 没有标签所以区域映射匹配不到任何东西,线程上也还没有人类审查者。我不会去猜一个登录名,所以我把这个路由留给接手的人。文档冲突解决之后,我的判断是代码不需要进一步改动,剩下的是策略判断。

仅凭文档冲突这一项请求修改。 🙏

Qwen Code · qwen3.8-max-2026-09-02

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

@qwen-code-ci-bot qwen-code-ci-bot 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.

@qqqys Requesting changes on one concrete blocker — see the stage 2 and stage 3 notes above for the detail. 🙏

docs/users/features/commands.md ships an unresolved rebase conflict at the reviewed commit (markers at lines 827, 852, 873; the same file on main has none). The page therefore renders literal VCS markers and documents both the old and the new gate rule at once — and the old one is exactly the behaviour this PR removes, so the docs for a security-relevant setting are wrong on the point the change turns on.

Please resolve it as both sides, not "take theirs": keep the new class-parity and repository-tightening paragraphs, then re-append the three paragraphs that exist only on the HEAD side (agents.crossSessionHeldExpiry expiry, the inbox-bind-failure path, and the duplicate-inbox-address behaviour). Taking only the PR side would silently delete documentation added by the reachability and receipt work this PR builds on. Worth grepping the branch for leftover markers before re-pushing, and re-confirming the tmux evidence at the new head since the branch was rebased after it was captured.

No CI check catches this, so it would have merged green.

The TypeScript itself needs nothing further from my side — the gate change is minimal, fails closed at every layer I traced, and the tests genuinely pin it. The remaining open question is a policy one for a maintainer: this decides who a session will listen to, and #10925 is still open.

中文说明

@qqqys 因一个具体阻塞项请求修改 —— 详见上面的 stage 2 与 stage 3 说明。🙏

docs/users/features/commands.md 在被审查的 commit 上带着一个未解决的 rebase 冲突(标记位于第 827、852、873 行;同一文件在 main 上一个都没有)。因此该页面会渲染出字面的版本控制标记,并且同时记录了旧的和新的网关规则 —— 而旧的那条正是本 PR 要移除的行为,所以一个安全相关设置的文档在这个改动所围绕的关键点上是错的。

请把它解成两侧都保留,而不是"取 theirs":保留新的类别对等段落和仓库收紧段落,然后重新接回只存在于 HEAD 一侧的三段(agents.crossSessionHeldExpiry 过期、inbox 绑定失败路径、inbox 地址重复行为)。只取 PR 那一侧会静默删掉本 PR 所依托的可达性与回执工作添加的文档。重新推送前值得 grep 一下分支里是否还有残留标记,并在新 head 上重新确认 tmux 证据,因为分支在那次采集之后经历过 rebase。

没有任何 CI 检查会捕获这一点,所以它本来会以全绿状态被合并。

TypeScript 本身我这边不需要再改什么 —— 网关改动是最小的,在我追踪的每一层都 fail closed,测试也确实固定住了它。剩下的开放问题是给 maintainer 的策略判断:这决定了会话愿意听谁的,而 #10925 仍然 open。

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

Built the PR base vs this PR head 1a3bf76, 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 12 scenario(s).

Qwen Code · serve A/B

@qwen-code-ci-bot qwen-code-ci-bot 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.

Partially reviewed — gaps disclosed.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

中文说明

仅完成部分审查,审查缺口已披露。

未审查(原文为英文):build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

— qwen3.8-max via Qwen Code /review (v0.23.0)

Comment thread docs/users/features/commands.md Outdated
Comment thread packages/cli/src/config/settingsUtils.ts Outdated
Comment thread packages/cli/src/peerMessaging/inbound-policy-scope.ts Outdated
Comment thread packages/core/src/ipc/inbound-gate.ts
Comment thread packages/cli/src/serve/routes/workspace-settings.ts Outdated
Comment thread packages/cli/src/config/settings.ts
Comment thread packages/cli/src/ui/startInteractiveUI.tsx
Comment thread docs/users/configuration/settings.md Outdated
Comment thread packages/cli/src/ui/startInteractiveUI.tsx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants