Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
cd8509f
fix(core): make permissions.allow restrict the tool schemas sent to t…
yiliang114 Aug 23, 2026
6af0884
Merge branch 'main' into fix/issue-9827-permissions-tool-schemas
wenshao Aug 23, 2026
a554edb
fix(core): honor permissions.allow in the list_directory opt-in gate …
yiliang114 Aug 24, 2026
b8ba258
fix(core): keep plan-mode lifecycle tools registered under the allowl…
yiliang114 Aug 24, 2026
477c36f
docs(sdk): correct allowedTools registry-allowlist contract (#9827)
yiliang114 Aug 24, 2026
105e5f0
docs(settings): note plan-mode lifecycle exemption in the allowlist (…
yiliang114 Aug 24, 2026
4d30e18
fix(core): exempt the computer_use__* family from the registry allowl…
yiliang114 Aug 24, 2026
c68f473
fix(core): gate command-discovered tools through the registry allowli…
yiliang114 Aug 24, 2026
4f1ee39
fix(core): make registry-allowlist membership monotonic within the se…
yiliang114 Aug 24, 2026
a9031c1
fix(core): narrow the skill allowedTools grant contract to restart-sc…
yiliang114 Aug 24, 2026
e9afe40
fix(core): count ask rules toward registry-allowlist membership (#9827)
yiliang114 Aug 24, 2026
1494aad
docs(settings): note that ask rules keep tools registered under allow…
yiliang114 Aug 24, 2026
de9a8e3
test(cli): pin registry-allowlist strip in bare mode (#9827)
yiliang114 Aug 24, 2026
c965468
fix(core): attribute registry-allowlist misses to permissions.allow (…
yiliang114 Aug 24, 2026
df5fada
test(core): pin resolveToolName coverage of every ToolNames entry (#9…
yiliang114 Aug 24, 2026
b0adfa7
fix(core): expose isPermissionsAllowListActive on scoped PM shims (#9…
yiliang114 Aug 24, 2026
23189f2
fix(core): honour ask-only list_directory coverage in the opt-in gate…
yiliang114 Aug 24, 2026
55f1fb5
docs: align registry-allowlist contract wording across docs and JSDoc…
yiliang114 Aug 24, 2026
0869a84
docs: scope settings.md removal and whole-tool-deny claims precisely …
yiliang114 Aug 24, 2026
f25d7ce
Merge remote-tracking branch 'origin/main' into fix/issue-9827-permis…
yiliang114 Aug 24, 2026
be96336
fix(core): count merged allow coverage in the list_directory opt-in g…
yiliang114 Aug 24, 2026
519be05
test(core): pin activation source and merged-allow coverage of the li…
yiliang114 Aug 24, 2026
56b1db6
fix(core): attribute scheduler denials to permissions.allow only for …
yiliang114 Aug 24, 2026
ced9a08
test(core): pin the covered-tool fallback for scheduler denial messag…
yiliang114 Aug 24, 2026
5437a89
fix(core): register request_shutdown in the permission rule alias map…
yiliang114 Aug 24, 2026
a4ee647
fix(core): guard the list_directory allowlist gate against non-string…
yiliang114 Aug 24, 2026
640ec59
fix(core): exempt task_stop from the permissions.allow registry gate …
yiliang114 Aug 24, 2026
63970a3
fix(core): keep shim denials on the pre-#9827 message when coverage i…
yiliang114 Aug 24, 2026
6c2694b
test(core): pin that ask-only rules never activate the allowlist (#9827)
yiliang114 Aug 24, 2026
c9b670e
fix(core): exempt tool_search from the permissions.allow registry gat…
yiliang114 Aug 25, 2026
3ca966c
test(core): pin the deny-rule arm's precedence in the scheduler permi…
yiliang114 Aug 25, 2026
7283ed1
test(core): pin deny/ask sibling semantics at the discovery gate (#9827)
yiliang114 Aug 25, 2026
5e63280
docs: match the allowlist activation wording to the real predicate (#…
yiliang114 Aug 25, 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
42 changes: 21 additions & 21 deletions docs/developers/sdk-typescript.md

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions docs/users/configuration/settings.md

Large diffs are not rendered by default.

86 changes: 86 additions & 0 deletions packages/cli/src/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4038,6 +4038,92 @@ describe('loadCliConfig safe mode', () => {
});
});

describe('loadCliConfig registry allowlist wiring (#9827)', () => {
const originalArgv = process.argv;

beforeEach(() => {
vi.resetAllMocks();
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
vi.spyOn(process, 'cwd').mockReturnValue(
path.resolve(path.sep, 'home', 'user', 'project'),
);
});

afterEach(() => {
process.argv = originalArgv;
vi.unstubAllEnvs();
vi.restoreAllMocks();
});

it('passes settings.permissions.allow as the registry allowlist', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments();
const settings: Settings = {
permissions: {
allow: ['ReadFile', 'Shell'],
},
};
const config = await loadCliConfig(settings, argv, undefined, []);

expect(config.getRegistryAllowList()).toEqual(['ReadFile', 'Shell']);
});

it('does not treat --allowed-tools as a registry allowlist', async () => {
process.argv = ['node', 'script.js', '--allowed-tools', 'ReadFile'];
const argv = await parseArguments();
const config = await loadCliConfig({}, argv, undefined, []);

// Auto-approval grant only — the full toolset stays registered
expect(config.getPermissionsAllow()).toContain('ReadFile');
expect(config.getRegistryAllowList()).toEqual([]);
});

it('does not treat the legacy tools.allowed key as a registry allowlist', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments();
const settings: Settings = {
tools: {
allowed: ['ShellTool'],
},
};
const config = await loadCliConfig(settings, argv, undefined, []);

expect(config.getPermissionsAllow()).toContain('ShellTool');
expect(config.getRegistryAllowList()).toEqual([]);
});

it('strips the registry allowlist in safe mode', async () => {
process.argv = ['node', 'script.js', '--safe-mode'];
Comment thread
yiliang114 marked this conversation as resolved.
Comment thread
yiliang114 marked this conversation as resolved.
const argv = await parseArguments();
const settings: Settings = {
permissions: {
allow: ['ReadFile'],
},
};
const config = await loadCliConfig(settings, argv, undefined, []);

expect(config.getRegistryAllowList()).toEqual([]);
});

it('strips the registry allowlist in bare mode', async () => {
// Mirror of the safe-mode test: bare mode drops settings
// `permissions.allow` from the merged allow rules, so an allowlist
// activated from the same settings would run with zero in-force
// membership rules and strip the bare registry's minimal toolset.
process.argv = ['node', 'script.js', '--bare'];
const argv = await parseArguments();
const settings: Settings = {
permissions: {
allow: ['ReadFile'],
},
};
const config = await loadCliConfig(settings, argv, undefined, []);

expect(config.getRegistryAllowList()).toEqual([]);
});
});

describe('loadCliConfig chatCompression', () => {
const originalArgv = process.argv;

Expand Down
10 changes: 10 additions & 0 deletions packages/cli/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2198,6 +2198,16 @@ export async function loadCliConfig(
allow: mergedAllow.length > 0 ? mergedAllow : undefined,
ask: mergedAsk.length > 0 ? mergedAsk : undefined,
deny: mergedDeny.length > 0 ? mergedDeny : undefined,
// Only `settings.permissions.allow` (never `--allowed-tools` nor the
// legacy `tools.allowed` key, which stay pure auto-approval grants)
// activates the registry-level allowlist that hides unlisted built-in
// tools from the model request (#9827).
registryAllowList:
bareMode || safeMode
? undefined
: settings.permissions?.allow?.length
? settings.permissions.allow
: undefined,
Comment on lines +2205 to +2210

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.

[Critical] R2-1: A single "Always allow" confirmation choice persists into settings.permissions.allow — the exact key this PR makes the registry-allowlist activator — so the next restart collapses the entire built-in toolset to the one always-allowed tool family, violating the invariant the PR itself states for the mid-session path. Chain verified at HEAD: the shell confirmation dialog offers "Always allow in this project/user" → shell.ts builds the Bash(npm test) permission rule → coreToolScheduler._handleConfirmationResponseInnerpersistPermissionOutcome (permission-helpers.ts:192) → the CLI's onPersistPermissionRule (config.ts:2227-2239) writes permissions.allow into the workspace or user-scope settings file → the next launch feeds that array into registryAllowList here (no filtering of interactive-granted rules) → permissionsAllowListActive becomes true → every uncovered built-in is dropped from the registry. The permissionsAllowListActive JSDoc names the exact harm ("approving one tool would suddenly permission-error every tool not on the list") and guards the in-session state — but the same click, persisted, delivers the identical outcome one restart later, silently, for every subsequent session; "Always allow for this user" writes the user-scope file, so every project opened afterwards boots with the collapsed toolset. Witness (round-2 probe, scratch tree, unmodified PR code — both arms deterministic): fresh project → persist('project','allow','Bash(npm test)') → WORKSPACE_SETTINGS_FILE: {"permissions":{"allow":["Bash(npm test)"]}}; simulated restart → REGISTRY_ALLOW_LIST_AFTER_RESTART: ["Bash(npm test)"], ACTIVE_WITH_ONE_PERSISTED_RULE: true, ENABLED[read_file/edit/write_file/grep_search/glob/list_directory/agent/todo_write/web_fetch]: false, ENABLED[run_shell_command/monitor]: true; flip arm: no persisted rules → ACTIVE_WITH_NO_RULES: false, ENABLED[read_file]: true. Don't let interactive grants silently activate the allowlist: persist always-allow rules under a key that stays a pure auto-approval grant (mirroring how this PR keeps --allowed-tools / tools.allowed out of registryAllowList), or gate activation behind explicit hand-authored intent. At minimum, emit a visible startup notice naming the tools hidden by an active allowlist and warn in the confirmation dialog that persisting the rule restricts the toolset after restart.

中文说明

一次"始终允许"确认选择会持久化到 settings.permissions.allow——正是本 PR 设为注册表白名单激活键的那个键——于是下次重启时整个内置工具集塌缩为那一个被始终允许的工具家族,违反了 PR 自己为会话中路径声明的不变量。已在 HEAD 验证完整链路:shell 确认弹窗提供"在此项目/此用户始终允许" → shell.ts 构造 Bash(npm test) 权限规则 → coreToolScheduler._handleConfirmationResponseInnerpersistPermissionOutcome(permission-helpers.ts:192)→ CLI 的 onPersistPermissionRule(config.ts:2227-2239)把 permissions.allow 写入工作区或用户级设置文件 → 下次启动把该数组原样喂给此处的 registryAllowList(交互式授予的规则没有任何过滤)→ permissionsAllowListActive 变为 true → 每个未被覆盖的内置工具被逐出注册表。permissionsAllowListActive 的 JSDoc 明确写出了这一危害("批准一个工具就会让列表外的所有工具突然权限报错")并防护了会话内状态——但同一次点击持久化之后,下一次启动就会静默产生完全相同的结果,且影响之后每个会话;"此用户始终允许"写的是用户级设置文件,此后打开的每个项目都以塌缩的工具集启动。见证(本轮探针,隔离树,未改动的 PR 代码——两臂均确定性):新项目 → 持久化 'Bash(npm test)' → 设置文件写入该规则;模拟重启 → 白名单激活、read_file/edit/write_file/grep_search/glob/list_directory/agent/todo_write/web_fetch 全部 false,仅 run_shell_command/monitor 为 true;翻转臂:无持久化规则 → 未激活、read_file 为 true。建议不要让交互式授予静默激活白名单:把"始终允许"规则持久化到一个保持纯自动批准语义的键(参照本 PR 刻意把 --allowed-tools / tools.allowed 排除在 registryAllowList 之外的做法),或把激活限定为显式手写意图。至少应在启动时输出可见提示、列出被激活白名单隐藏的工具,并在确认弹窗中警告:持久化该规则会在重启后收缩工具集。

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Confirmed real at HEAD — the full chain reproduces (persist Bash(npm test) via "Always allow" -> next start registryAllowList=["Bash(npm test)"], allowlist active, read_file/edit/grep_search/glob/... all isToolEnabled=false). This directly violates the mid-session invariant the PR documents for itself, one restart later and silently.

I'm escalating rather than fixing because the only correct fixes are a data-semantics decision that shouldn't be made unilaterally inside this PR. Interactively-persisted rules and hand-authored rules are indistinguishable once both live in settings.permissions.allow, so there is no small, correct way to keep an interactive grant from activating the allowlist without choosing one of:

  1. Persist "Always allow" grants under a separate key that stays a pure auto-approval grant (mirrors how this PR keeps --allowed-tools / tools.allowed out of registryAllowList). Cost: new settings key + load-path merge into the auto-approval path + migration for rules users already accumulated under permissions.allow.
  2. Record provenance metadata on persisted rules and gate activation on hand-authored intent. Cost: settings-schema change.
  3. Mitigation only (startup notice naming the hidden tools + confirmation-dialog warning). Cheap, but does not stop the toolset collapse.

My lean is (1) since it matches the grant-vs-allowlist split this PR already establishes, but it changes the on-disk settings format and the public permissions contract, so it needs a maintainer sign-off. Which direction do you want? Leaving this thread open until then.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Already escalated in the round-2 thread — see reply 3840449009: the chain is confirmed real at HEAD (persisting an "Always allow" rule writes settings.permissions.allow, which activates the allowlist on next restart and collapses the toolset to that one family). The remediation requires a maintainer settings data-format decision — persist interactive grants under a separate non-activating key, or gate allowlist activation behind explicit hand-authored intent — so it is not being fixed in this review round. Leaving this thread open to track that decision.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Still maintainer-gated this round — no code change on this surface (the round-6 R6-1 fix in c9b670e only touches the tool_search exemption). Note: round 6's R1-8 finding reduces to the same root question — what activates the permissions.allow registry allowlist — so both threads are consolidated under the decision laid out in reply 3840449009 (separate non-activating grant key vs. provenance-gated activation vs. mitigation-only). Leaving this thread open for maintainer sign-off.

autoMode:
bareMode || safeMode ? undefined : settings.permissions?.autoMode,
},
Expand Down
Loading
Loading