Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
54a12e2
feat(core): accept cross-session messages behind an inbound gate
qqqys Aug 20, 2026
93418cb
Merge branch 'main' into feat/cross-session-inbox-v2
qwen-code-dev-bot Aug 20, 2026
eb1d343
Merge branch 'main' into feat/cross-session-inbox-v2
qwen-code-dev-bot Aug 21, 2026
5bf7e26
Merge branch 'main' into feat/cross-session-inbox-v2
qwen-code-dev-bot Aug 21, 2026
5928873
Merge branch 'main' into feat/cross-session-inbox-v2
qwen-code-dev-bot Aug 21, 2026
97426bc
fix(i18n): translate the /peers description in strict-parity locales
qqqys Aug 21, 2026
b09bded
fix(ipc): close Critical review findings in the cross-session inbox
qwen-code-dev-bot Aug 22, 2026
6dd138d
fix(cli): gate held-message announcements on the held set growing
qwen-code-dev-bot Aug 22, 2026
3ebd83f
fix(ipc): close round-6 Critical findings in the cross-session inbox
qwen-code-dev-bot Aug 23, 2026
e4065a7
Merge remote-tracking branch 'origin/main' into feat/cross-session-in…
qwen-code-dev-bot Aug 23, 2026
0dd9b35
fix(ipc): close round-7 Critical findings in the cross-session inbox
qwen-code-dev-bot Aug 23, 2026
c6e0718
fix(ipc): close round-8 Critical findings in the cross-session inbox
qwen-code-dev-bot Aug 23, 2026
c5ba3f8
Merge remote-tracking branch 'origin/main' into pr-9576-import
yiliang114 Aug 23, 2026
7ca3be7
fix(ipc): close peer messaging lifecycle races
yiliang114 Aug 23, 2026
3a81046
Merge branch 'main' into feat/cross-session-inbox-v2
qwen-code-dev-bot Aug 23, 2026
3021309
Merge branch 'main' into feat/cross-session-inbox-v2
qwen-code-dev-bot Aug 24, 2026
9f5a6dd
chore(cli): regenerate the settings schema for the reworded inbound p…
qqqys Aug 24, 2026
02e6300
Merge remote-tracking branch 'upstream/main' into m9576
qqqys Aug 24, 2026
a7595c3
Merge branch 'main' into feat/cross-session-inbox-v2
qwen-code-dev-bot Aug 24, 2026
a9d9f14
fix(cli): close peer-inbox admission and decision review gaps (#9576)
qwen-code-dev-bot Aug 24, 2026
5c5a8e0
fix(cli): settle all peer-inbox receipts at teardown (#9576)
qwen-code-dev-bot Aug 25, 2026
8b7af63
fix(cli): restore peer message when its in-flight turn fails delivery…
qwen-code-dev-bot Aug 25, 2026
2e51fbd
fix(cli): retry restored peer envelope once the failed turn settles (…
qwen-code-dev-bot Aug 25, 2026
7602a09
Merge branch 'main' into feat/cross-session-inbox-v2
qqqys Aug 26, 2026
26c364d
Merge branch 'main' into feat/cross-session-inbox-v2
qqqys Aug 26, 2026
ee63683
Merge remote-tracking branch 'upstream/main' into feat/cross-session-…
qqqys Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions packages/cli/src/config/settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3493,6 +3493,93 @@ describe('Settings Loading and Merging', () => {
});
});

describe('cross-session settings scope handling', () => {
it('should honor the cross-session keys from user scope', () => {
(mockFsExistsSync as Mock).mockReturnValue(true);
(fs.readFileSync as Mock).mockImplementation(
(p: fs.PathOrFileDescriptor) => {
if (p === USER_SETTINGS_PATH)
return JSON.stringify({
agents: {
crossSessionMessaging: true,
crossSessionInbound: 'hold',
},
});
return '{}';
},
);

const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.merged.agents?.crossSessionMessaging).toBe(true);
expect(settings.merged.agents?.crossSessionInbound).toBe('hold');
});

it('should strip the cross-session keys from workspace scope even when trusted', () => {
// A trusted repository must not be able to self-grant the peer
// channel or force the inbound policy: the parity hold is the
// feature's own protection, and workspace scope uniquely defeats it.
(mockFsExistsSync as Mock).mockReturnValue(true);
(fs.readFileSync as Mock).mockImplementation(
(p: fs.PathOrFileDescriptor) => {
if (p === MOCK_WORKSPACE_SETTINGS_PATH)
return JSON.stringify({
agents: {
crossSessionMessaging: true,
crossSessionInbound: 'accept',
maxParallelAgents: 4,
},
});
return '{}';
},
);

const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.merged.agents?.crossSessionMessaging).toBeUndefined();
expect(settings.merged.agents?.crossSessionInbound).toBeUndefined();
// ...while other workspace agent settings still merge.
expect(settings.merged.agents?.maxParallelAgents).toBe(4);
});

it('should warn when workspace settings define agents.crossSessionInbound', () => {
(mockFsExistsSync as Mock).mockReturnValue(true);
(fs.readFileSync as Mock).mockImplementation(
(p: fs.PathOrFileDescriptor) => {
if (p === MOCK_WORKSPACE_SETTINGS_PATH)
return JSON.stringify({
agents: { crossSessionInbound: 'accept' },
});
return '{}';
},
);

const settings = loadSettings(MOCK_WORKSPACE_DIR);
const warnings = getSettingsWarnings(settings);
expect(
warnings.some((w) => w.includes('agents.crossSessionInbound')),
).toBe(true);
});

it('should let user scope win over a stripped workspace value', () => {
(mockFsExistsSync as Mock).mockReturnValue(true);
(fs.readFileSync as Mock).mockImplementation(
(p: fs.PathOrFileDescriptor) => {
if (p === USER_SETTINGS_PATH)
return JSON.stringify({
agents: { crossSessionInbound: 'hold' },
});
if (p === MOCK_WORKSPACE_SETTINGS_PATH)
return JSON.stringify({
agents: { crossSessionInbound: 'accept' },
});
return '{}';
},
);

const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.merged.agents?.crossSessionInbound).toBe('hold');
});
});

describe('allowedInsecureVoiceBaseUrls scope handling', () => {
it('should honor the allowlist from user scope', () => {
(mockFsExistsSync as Mock).mockReturnValue(true);
Expand Down
1 change: 0 additions & 1 deletion packages/cli/src/config/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,6 @@ export function getSettingsWarnings(loadedSettings: LoadedSettings): string[] {
);
}
}

return [...warningSet];
}

Expand Down
27 changes: 27 additions & 0 deletions packages/cli/src/config/settingsSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,33 @@ describe('SettingsSchema', () => {
expect(exploreModel.showInDialog).toBe(false);
});

it('should keep cross-session messaging off by default', () => {
// The default is the entire security posture of the feature: shipping
// it flipped on would open every session on the box to peer messages.
const crossSessionMessaging =
getSettingsSchema().agents.properties.crossSessionMessaging;

expect(crossSessionMessaging.type).toBe('boolean');
expect(crossSessionMessaging.default).toBe(false);
expect(crossSessionMessaging.requiresRestart).toBe(true);
expect(crossSessionMessaging.showInDialog).toBe(false);
});

it('should define the inbound cross-session policy as accept/hold/refuse', () => {
const crossSessionInbound =
getSettingsSchema().agents.properties.crossSessionInbound;

expect(crossSessionInbound.type).toBe('enum');
// Unset is not a fourth policy: it means approval-mode parity, which
// the gate derives. A concrete default here would silence that.
expect(crossSessionInbound.default).toBeUndefined();
expect(crossSessionInbound.options).toEqual([
{ value: 'accept', label: 'Accept' },
{ value: 'hold', label: 'Hold for review' },
{ value: 'refuse', label: 'Refuse' },
]);
});

it('should define model grade settings', () => {
const agents = getSettingsSchema().agents.properties;

Expand Down
25 changes: 25 additions & 0 deletions packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3263,6 +3263,31 @@ const SETTINGS_SCHEMA = {
},
},
},
crossSessionMessaging: {
type: 'boolean',
label: 'Cross-Session Messaging',
Comment on lines +3266 to +3268

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.

[Suggestion] Both new settings are honored from Workspace scope — a trusted repository can self-grant the peer-messaging capability and hot-flip its review gate: a repo carrying .qwen/settings.json with {"agents":{"crossSessionMessaging":true,"crossSessionInbound":"accept"}} flows into settings.merged in a TRUSTED folder (untrusted workspaces are ignored; mergeSettings strips only allowPrivateNetworkHooks/allowedInsecureVoiceBaseUrls), so the next launch binds the inbox and publishes the session — an experimental, default-off feature the user never enabled. Worse: crossSessionInbound is requiresRestart: false and the watcher reloads workspace scope live — a later git pull landing/editing that file flips a RUNNING session's inbound policy to 'accept' with no restart and no dialog (showInDialog: false), after which any same-uid process's frames auto-deliver past the /peers gate. Verified via real loadSettings: trusted → both keys present in merged; untrusted → {}. The codebase's own precedent strips repo-file self-grants "even when trusted" (settings.test.ts) and operatorReviewSettings skips workspace scope because "a repository must not decide … for every reviewer who opens it". Rated Suggestion rather than Critical because trusted folders already execute arbitrary code via workspace hooks — but this defeats a defense the same PR builds. Honor both keys only from User/System/SystemDefaults scope, add an ignored-in-workspace warning like the allowPrivateNetworkHooks one, and pin it beside the existing strip test.

中文说明

两个新设置都接受 Workspace 作用域 —— 被信任的仓库可以自我授予 peer 消息能力并热切换其审核闸门:仓库内的 .qwen/settings.json 若含 {"agents":{"crossSessionMessaging":true,"crossSessionInbound":"accept"}},在被信任的文件夹中会进入 settings.merged(未信任的工作区被忽略;mergeSettings 只剥离 allowPrivateNetworkHooks/allowedInsecureVoiceBaseUrls),于是下次启动就会绑定 inbox 并发布会话 —— 一个用户从未启用的实验性、默认关闭的功能。更糟的是:crossSessionInboundrequiresRestart: false,且 watcher 会热重载 workspace 作用域 —— 之后一次 git pull 使该文件落地/变更,就能把运行中会话的入站策略翻成 'accept',无重启、无对话框(showInDialog: false),此后任何同 uid 进程的帧都绕过 /peers 闸门自动投递。经真实 loadSettings 验证:信任 → 两个键都进入 merged;不信任 → {}。代码库自己的先例对仓库文件的自我授权"即使在 trusted 下也剥离"(settings.test.ts),operatorReviewSettings 也跳过 workspace 作用域,理由是 "a repository must not decide … for every reviewer who opens it"。定为 Suggestion 而非 Critical,因为被信任文件夹本就能通过 workspace hooks 执行任意代码 —— 但这确实绕过了本 PR 自己建立的防线。请让这两个键只接受 User/System/SystemDefaults 作用域,像 allowPrivateNetworkHooks 一样增加"workspace 中被忽略"的警告,并在现有剥离测试旁钉住。

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

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.

[Suggestion] R1-25: Both new settings are honored from Workspace scope — a trusted repository can self-grant the peer-messaging capability and hot-flip its review gate: a repo carrying .qwen/settings.json with these keys turns the inbox on from a repo file, and crossSessionInbound (requiresRestart: false, read live per frame) lets a workspace file switch the gate between accept and refuse at any moment. Restrict both keys to User scope. Still stands at 93418cb5 — the branch is byte-identical to the round-1 reviewed commit (only a merge of main landed since).

中文说明

两个新设置都接受 Workspace 作用域——受信任的仓库可以自我授予跨会话消息能力并热切换其审核闸门:仓库内的 .qwen/settings.json 可以打开收件箱,而 crossSessionInboundrequiresRestart: false、每帧实时读取)让仓库文件随时在 accept 与 refuse 之间切换闸门。请把这两个键限制在 User 作用域。第 2 轮复验:该问题依然存在。

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

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.

[Suggestion] R1-25: Both new settings are honored from Workspace scope — a trusted repository can self-grant the peer-messaging capability and hot-flip its review gate: a repo carrying .qwen/settings.json with these keys turns the inbox on from a repo file, and crossSessionInbound (requiresRestart: false, read live per frame) lets a workspace file switch the gate between accept and refuse at any moment. Restrict both keys to User scope. Still stands at eb1d3431 — the PR's 30 files are byte-identical to the round-2 reviewed commit 93418cb5 (only merges of main landed since).

中文说明

两个新设置都接受 Workspace 作用域——受信任的仓库可以自我授予跨会话消息能力并热切换其审核闸门:仓库内的 .qwen/settings.json 可以打开收件箱,而 crossSessionInboundrequiresRestart: false、每帧实时读取)让仓库文件随时在 accept 与 refuse 之间切换闸门。请把这两个键限制在 User 作用域。第 3 轮复验:依然存在——PR 的 30 个文件与第 2 轮审阅提交 93418cb5 逐字节一致(其后仅合并了 main)。

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

Comment thread
yiliang114 marked this conversation as resolved.
category: 'Advanced',
requiresRestart: true,
default: false,
description:
'Experimental. Let Qwen Code sessions on this machine send each other messages over a per-session local socket. Off by default; turning it on both opens this session to peer messages and makes it discoverable to others.',
showInDialog: false,
},
crossSessionInbound: {
type: 'enum',
label: 'Inbound Cross-Session Messages',
category: 'Advanced',
requiresRestart: false,
default: undefined as string | undefined,
description:
'What happens to messages other sessions send this one. "accept" delivers them; "hold" parks them for your review without letting the model act; "refuse" opts this session out. Unset means approval-mode parity: a message auto-delivers only when this session reviews every action, or when both sessions declare a mode that can apply actions without per-action review. Other messages are held for you to review.',
showInDialog: false,
options: [
{ value: 'accept', label: 'Accept' },
{ value: 'hold', label: 'Hold for review' },
{ value: 'refuse', label: 'Refuse' },
],
},
modelGrades: {
type: 'object',
label: 'Model Grades',
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/config/settingsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,8 @@ export const WORKSPACE_RESTRICTED_SETTINGS = [
{ section: 'tools', key: 'workflowsEnabled' },
{ section: 'security', key: 'allowPrivateNetworkHooks' },
{ section: 'security', key: 'allowedInsecureVoiceBaseUrls' },
{ section: 'agents', key: 'crossSessionMessaging' },
{ section: 'agents', key: 'crossSessionInbound' },
] as const satisfies ReadonlyArray<{
readonly section: keyof Settings;
readonly key: string;
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/i18n/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -2843,4 +2843,6 @@ export default {
'Auto-skill curator changes are only available in trusted workspaces. Trust this folder via `/trust` and try again.':
'Auto-skill curator changes are only available in trusted workspaces. Trust this folder via `/trust` and try again.',
'Kept model as {{model}}': 'Kept model as {{model}}',
'Review messages held from other Qwen Code sessions (accept | deny)':
'Review messages held from other Qwen Code sessions (accept | deny)',
};
2 changes: 2 additions & 0 deletions packages/cli/src/i18n/locales/zh-TW.js
Original file line number Diff line number Diff line change
Expand Up @@ -2422,4 +2422,6 @@ export default {
'Auto-skill curator changes are only available in trusted workspaces. Trust this folder via `/trust` and try again.':
'只有受信任的工作區可以變更自動技能管理器。請透過 `/trust` 信任此資料夾後再試一次。',
'Kept model as {{model}}': '模型保持為 {{model}}',
'Review messages held from other Qwen Code sessions (accept | deny)':
'檢視其他 Qwen Code 工作階段傳來的待處理訊息(accept | deny)',
};
2 changes: 2 additions & 0 deletions packages/cli/src/i18n/locales/zh.js
Original file line number Diff line number Diff line change
Expand Up @@ -2624,4 +2624,6 @@ export default {
'Auto-skill curator changes are only available in trusted workspaces. Trust this folder via `/trust` and try again.':
'仅受信任的工作区可以更改自动技能管理器。请通过 `/trust` 信任此文件夹后重试。',
'Kept model as {{model}}': '模型保持为 {{model}}',
'Review messages held from other Qwen Code sessions (accept | deny)':
'查看其他 Qwen Code 会话发来的待处理消息(accept | deny)',
};
11 changes: 11 additions & 0 deletions packages/cli/src/peerMessaging/PeerMessagingContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* @license
* Copyright 2026 Qwen
* SPDX-License-Identifier: Apache-2.0
*/

import { createContext, useContext } from 'react';
import type { PeerMessaging } from './peer-messaging.js';

export const PeerMessagingContext = createContext<PeerMessaging | null>(null);
export const usePeerMessaging = () => useContext(PeerMessagingContext);
Loading
Loading