Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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.

58 changes: 29 additions & 29 deletions docs/users/configuration/settings.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2813,7 +2813,7 @@ const SETTINGS_SCHEMA = {
requiresRestart: true,
default: undefined as string[] | undefined,
description:
'Allowlist of eager-by-default built-in tool names whose schemas remain eligible for the initial model request. Unlisted non-exempt tools are deferred but stay registered, listed in /tools, callable, and discoverable via tool_search. Tools already deferred by default stay on demand even when listed; use tools.visible to surface one at startup. tool_search, structured_output, plan-mode lifecycle tools, task_stop, MCP tools, and computer_use__* tools are unaffected. An explicitly empty list ([]) defers every non-exempt eager-by-default tool; omit the setting for no restriction. Pairs with tool_search: when ToolSearch is not registered (tools.toolSearch.enabled false, a tool_search deny rule, or the automatic opt-out for DeepSeek models) the schemas are still withheld but nothing can load them back, so the demoted tools are out of reach for that session and a warning is logged. Differs from tools.disabled, which removes tools entirely, and from permissions.allow, which only auto-approves calls.',
'Allowlist of eager-by-default built-in tool names whose schemas remain eligible for the initial model request. Unlisted non-exempt tools are deferred but stay registered, listed in /tools, callable, and discoverable via tool_search. Tools already deferred by default stay on demand even when listed; use tools.visible to surface one at startup. tool_search, structured_output, plan-mode lifecycle tools, task_stop, MCP tools, and computer_use__* tools are unaffected. An explicitly empty list ([]) defers every non-exempt eager-by-default tool; omit the setting for no restriction. Pairs with tool_search: when ToolSearch is not registered (tools.toolSearch.enabled false, a tool_search deny rule, or the automatic opt-out for DeepSeek models) the schemas are still withheld but nothing can load them back, so the demoted tools are out of reach for that session and a warning is logged. Two carve-outs: demoted tools referenced in resumed session history get their schemas re-sent without a warning, and demoted tools listed in tools.visible are declared up front. Differs from tools.disabled, which removes tools entirely, and from permissions.allow, which only auto-approves calls.',
Comment thread
yiliang114 marked this conversation as resolved.
showInDialog: false,
},
approvalMode: {
Expand Down
38 changes: 38 additions & 0 deletions packages/core/src/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9314,6 +9314,44 @@ describe('Server Config (config.ts)', () => {
expect(deferred).toContain(ToolNames.LS);
});

it('registers an enabled LS eagerly when tools.eager covers it (#10400)', async () => {
// Third cell of the LS x tools.eager matrix: enabled AND covered by
// the allowlist (via the ListFiles alias) -> registered eagerly via
// registerFactory, not demoted to deferred. Guards against a
// registerLazy mutant that demotes LS whenever an eager list is
// active, ignoring entry coverage (#10400).
const params: ConfigParameters = {
...baseParams,
useRipgrep: false,
coreTools: undefined,
lsToolEnabled: true,
eagerTools: ['Shell', 'ListFiles'],
};
const config = new Config(params);
await config.initialize();

const { registerFactory, registerPermissionDeferredFactory } = (
(await vi.importMock('../tools/tool-registry')) as {
ToolRegistry: {
prototype: {
registerFactory: Mock;
registerPermissionDeferredFactory: Mock;
};
};
}
).ToolRegistry.prototype;

const registered = (registerFactory as Mock).mock.calls.map(
(call) => call[0],
) as string[];
const deferred = (
registerPermissionDeferredFactory as Mock
).mock.calls.map((call) => call[0]) as string[];

expect(registered).toContain(ToolNames.LS);
expect(deferred).not.toContain(ToolNames.LS);
});

it('registers the full built-in set when no permissionsAllow is set (#9827 regression guard)', async () => {
const params: ConfigParameters = {
...baseParams,
Expand Down
56 changes: 56 additions & 0 deletions packages/core/src/permissions/permission-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,23 @@ describe('resolveToolName', () => {
expect(resolveToolName('mcp__server__tool')).toBe('mcp__server__tool');
expect(resolveToolName('constructor')).toBe('constructor');
});

it('returns Object.prototype-keyed names unchanged (#10400)', async () => {
// Keys inherited from Object.prototype must never resolve to the
// prototype value (e.g. the `constructor` function): only own
// properties of the alias table are aliases (#10400).
for (const name of [
'toString',
'valueOf',
'hasOwnProperty',
'isPrototypeOf',
'propertyIsEnumerable',
'toLocaleString',
'__proto__',
]) {
expect(resolveToolName(name)).toBe(name);
}
});
});

// ─── resolveToolName exhaustiveness (#9827) ─────────────────────────────────
Expand Down Expand Up @@ -2869,6 +2886,45 @@ describe('PermissionManager', () => {
);
});

it('tolerates Object.prototype-keyed entries without crashing (#10400)', async () => {
// Entries named after Object.prototype keys used to read the inherited
// prototype value through the plain-object alias table and surface a
// non-string toolName, crashing initialize() with
// `rule.toolName.startsWith is not a function` (CLI startup crash).
// They must behave like any other unknown canonical name: resolve to
// themselves as strings, match no registered tool, and never abort
// initialization (#10400).
pm = new PermissionManager(
makeConfig({
eagerTools: [
'constructor',
'toString',
'valueOf',
'hasOwnProperty',
'isPrototypeOf',
'propertyIsEnumerable',
'toLocaleString',
'__proto__',
'ReadFile',
],
}),
);
expect(() => pm.initialize()).not.toThrow();
expect(pm.isEagerToolAllowListActive()).toBe(true);
// The valid entry still works and the prototype-keyed entries do not
// disturb the rest of the allowlist.
expect(await pm.getToolRegistrationStatus('read_file')).toBe(
'registered',
);
expect(await pm.getToolRegistrationStatus('send_message')).toBe(
'deferred',
);
// The lookup itself must survive a prototype-keyed tool name too.
await expect(
pm.getToolRegistrationStatus('constructor'),
).resolves.toBeDefined();
});

it('malformed entries drop out but still leave the list active', async () => {
// Deferring more than intended is recoverable (ToolSearch still
// reaches every tool); silently ignoring a configured list would
Expand Down
22 changes: 22 additions & 0 deletions packages/core/src/tools/tool-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -930,6 +930,28 @@ describe('ToolRegistry', () => {
);
});

it('keeps the tool visible when listed in visibleTools', async () => {
const registry = new ToolRegistry(
new Config({
...baseConfigParams,
visibleTools: ['hidden_by_allowlist'],
}),
);
registry.registerPermissionDeferredFactory(
'hidden_by_allowlist',
async () => new MockTool({ name: 'hidden_by_allowlist' }),
);
await registry.warmAll();

expect(registry.getFunctionDeclarations().map((d) => d.name)).toContain(
'hidden_by_allowlist',
);
expect(registry.isDeferredAndHidden('hidden_by_allowlist')).toBe(false);
expect(registry.getDeferredToolSummary().map((t) => t.name)).not.toContain(
'hidden_by_allowlist',
);
});

it('reveals the schema once ToolSearch loads the tool', async () => {
toolRegistry.registerPermissionDeferredFactory(
'hidden_by_allowlist',
Expand Down
Loading
Loading