Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
4212655
fix(agent-core): honor [tools].disabled in v1 engine
C0d3N1nja97342 Aug 2, 2026
e232a3b
chore: add changeset for tools.disabled fix
C0d3N1nja97342 Aug 2, 2026
82958df
fix: also apply [tools].disabled to spawned subagents
C0d3N1nja97342 Aug 2, 2026
84112ee
fix(agent-core): filter disabled tools from subagent descriptions
C0d3N1nja97342 Aug 5, 2026
35e3f23
fix(agent-core): reapply disabled tools when restoring agents
C0d3N1nja97342 Aug 5, 2026
a4e1651
fix(agent-core): preserve replayed tools when no disabled config
C0d3N1nja97342 Aug 5, 2026
42eb65f
fix(agent-core): don't persist config-disabled tools into profile wir…
C0d3N1nja97342 Aug 5, 2026
7f6a47a
fix(agent-core): preserve global tool denylist across setActiveTools
C0d3N1nja97342 Aug 5, 2026
0d8505c
fix(agent-core): apply config denylist on Agent profile activation
C0d3N1nja97342 Aug 5, 2026
bf82349
fix(agent-core): preserve replayed enabled tools when adding resume d…
C0d3N1nja97342 Aug 5, 2026
dd3e923
fix(agent-core): rebuild builtins after resume denies and stabilize w…
C0d3N1nja97342 Aug 6, 2026
0cbcda7
chore: include kimi-code-sdk in changeset for tools.disabled fix
C0d3N1nja97342 Aug 6, 2026
ef02f39
fix(agent-core): pass undefined for empty disallowedNames per repo co…
C0d3N1nja97342 Aug 6, 2026
08fd35a
fix(agent-core): stop persisting config-only tool denies in wire record
C0d3N1nja97342 Aug 6, 2026
950c5ef
fix(agent-core): reapply config denies after standalone Agent.resume
C0d3N1nja97342 Aug 6, 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
17 changes: 17 additions & 0 deletions .changeset/fix-tools-disabled-v1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@moonshot-ai/kimi-code": patch

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include the SDK in this changeset

When this core fix is consumed through the published SDK (for example SDKRpcClient/KimiHarness v1 sessions honoring [tools].disabled), it will not reach npm users because @moonshot-ai/agent-core is private and packages/node-sdk/tsdown.config.ts bundles it into @moonshot-ai/kimi-code-sdk. With only @moonshot-ai/kimi-code listed here, the release PR will version/publish the CLI but leave SDK consumers on the old behavior, so please add the SDK package to the changeset as well.

Useful? React with 👍 / 👎.

"@moonshot-ai/kimi-code-sdk": patch
---

fix(agent-core): honor [tools].disabled config in v1 engine

The `[tools].disabled` array in config.toml was silently ignored by the
v1 engine (v2 has a dedicated toolPolicy service for this). Read the
section from config.raw in bootstrapAgentProfile and merge it into the
profile's disallowedTools so disabled tools are filtered from both the
top-level tool list and the subagent Agent tool description.

agent-core is bundled into the SDK artifact (node-sdk `alwaysBundle`), so
in-process SDK consumers (createKimiHarness/SDKRpcClient v1 sessions)
that set `[tools].disabled` need the SDK version bumped to receive the
fix.
44 changes: 44 additions & 0 deletions packages/agent-core/src/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,33 @@ export class Agent {
): void {
this.setActiveProfile(profile, brandHome);
this.updateSystemPromptFromProfile(profile, context, subagentNames);
// Persist only the profile's own denylist via setActiveTools (it is
// replayed on resume). The global [tools].disabled denylist is applied
// separately via addDisallowedTools, which mutates the deny set without
// logging a set_active_tools record - otherwise a config deny added at
// startup would be written into the wire record and survive a later
// config removal on cold resume. #2534.
this.tools.setActiveTools(profile.tools, profile.disallowedTools);
const toolsDisabled = this.configDisabledTools();
if (toolsDisabled.length > 0) {
this.tools.addDisallowedTools(toolsDisabled);
Comment on lines +460 to +462

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reapply config denies after standalone resume

Applying [tools].disabled from useProfile still misses the standalone resume path: a caller can construct new Agent({ config, persistence }) and call agent.resume(), which replays the persisted tools.set_active_tools record but never calls useProfile, setActiveTools RPC, or the Session-only restoreAgentProfileHandle. In that context, adding disabled = ["Bash"] before resuming an existing Agent leaves Bash active, even though the same config is honored for freshly profiled Agents; the repo explicitly keeps Agent usable without a Session, so the post-replay path needs the same non-persistent addDisallowedTools application.

AGENTS.md reference: AGENTS.md:L60-L60

Useful? React with 👍 / 👎.

}
}

/** Global [tools].disabled from options.config.raw, if provided. #2534. */
private configDisabledTools(): string[] {
const tools = this.kimiConfig?.raw?.['tools'];
if (
tools === undefined ||
typeof tools !== 'object' ||
tools === null ||
!Array.isArray((tools as Record<string, unknown>)['disabled'])
) {
return [];
}
return ((tools as Record<string, unknown>)['disabled'] as string[]).filter(
(v): v is string => typeof v === 'string',
);
}

/** Push a refreshed session config snapshot and rebuild config-dependent builtin tools. */
Expand Down Expand Up @@ -532,6 +558,15 @@ export class Agent {

async resume(options?: AgentRecordsReplayOptions): Promise<{ warning?: string }> {
const result = await this.records.replay(options);
// A standalone Agent (no Session) replays persisted tool state here but
// never routes through Session.restoreAgentProfileHandle, so re-apply the
// global [tools].disabled denylist non-persistently (mirrors useProfile).
// Without this a config deny added before resuming an existing Agent has
// no effect until the next useProfile / setActiveTools RPC. #2534.
const toolsDisabled = this.configDisabledTools();
if (toolsDisabled.length > 0) {
this.tools.addDisallowedTools(toolsDisabled);
}
this.flushPendingAnthropicThinkingEffortWarnings();
try {
this.replayBuilder.postRestoring = true;
Expand Down Expand Up @@ -648,7 +683,16 @@ export class Agent {
this.tools.unregisterUserTool(payload.name);
},
setActiveTools: (payload) => {
// Persist the runtime selection only. The global [tools].disabled
// denylist is re-applied via addDisallowedTools (no record) so a
// runtime selection cannot reactivate a disabled tool, while keeping
// config-only denies out of the persisted set_active_tools record.
// #2534.
this.tools.setActiveTools(payload.names);
const toolsDisabled = this.configDisabledTools();
if (toolsDisabled.length > 0) {
this.tools.addDisallowedTools(toolsDisabled);
}
},
stopBackground: (payload) => {
void this.background.stop(payload.taskId, payload.reason);
Expand Down
34 changes: 33 additions & 1 deletion packages/agent-core/src/agent/tool/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -530,10 +530,15 @@ export class ToolManager {
}

setActiveTools(names: readonly string[], disallowedNames?: readonly string[]): void {
// Callers compose [tools].disabled into an array, but an empty denylist
// carries no information; normalize to undefined so the serialized record
// omits it (matching the pre-config behavior) instead of changing every
// set_active_tools record with an empty disallowedNames. #2534.
this.agent.records.logRecord({
type: 'tools.set_active_tools',
names,
disallowedNames,
disallowedNames:
disallowedNames && disallowedNames.length > 0 ? disallowedNames : undefined,
});
// MCP entries are glob patterns gated separately; the rest are exact
// builtin/user tool names. The split keeps every caller on one string[].
Expand All @@ -552,6 +557,33 @@ export class ToolManager {
}
}

/**
* Add to the denied set without replacing the replayed enabled set. Used by
* the resume path so applying [tools].disabled to a restored agent does not
* drop unrelated replayed host/user tools or a previous runtime selection.
* #2534.
*/
addDisallowedTools(names: readonly string[]): void {
if (names.length === 0) return;
const denials = names.filter((name) => !isMcpToolName(name));
const mcpDenies = names.filter((name) => isMcpToolName(name));
if (denials.length > 0) {
this.disabledTools = new Set([...this.disabledTools, ...denials]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Rebuild builtins after adding resume denies

When [tools].disabled is added before resuming an existing v1 session and it names one of TaskList/TaskOutput/TaskStop, this resume-only helper only mutates disabledTools. The built-in Bash/Agent tools were already reconstructed during wire replay, and initializeBuiltinTools computes allowBackground from those Task* tools, so they keep accepting background Bash/subagents until another setActiveTools rebuild happens. Please refresh the builtins after adding exact denies that can affect built-in construction.

Useful? React with 👍 / 👎.

}
if (mcpDenies.length > 0) {
this.mcpDenyPatterns = [...this.mcpDenyPatterns, ...mcpDenies];
}
// Builtin construction bakes `allowBackground` from the Task* trio into
// Bash/Agent (see setActiveTools). The resume path already rebuilt the
// builtins during wire replay, before these denies were applied, so a
// newly-denied Task* tool would leave allowBackground stuck on until the
// next setActiveTools. Rebuild here so the denylist takes effect on the
// already-constructed builtins. #2534.
if (this.agent.config.hasProvider) {
this.initializeBuiltinTools();
}
}

copyLoopToolsFrom(source: ToolManager): void {
this.loopToolsOverride = source.loopTools;
}
Expand Down
37 changes: 37 additions & 0 deletions packages/agent-core/src/session/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -778,7 +778,17 @@ export class Session {
{ additionalDirs: this.additionalDirs },
);
const subagentNames = Object.keys(this.agentCatalog.delegatableSubagents(profile.name));

// Apply global [tools].disabled without persisting it into the agent's
// profile/wire record. useProfile persists only profile.disallowedTools
// (replayed on resume); config denies are added via addDisallowedTools,
// which mutates the deny set without logging a record, so removing the
// config later does not leave a stale persisted deny. #2534.
const toolsDisabled = this.readToolsDisabled();
agent.useProfile(profile, context, this.options.kimiHomeDir, subagentNames);
if (toolsDisabled.length > 0) {
agent.tools.addDisallowedTools(toolsDisabled);
}
const { agentsMdWarning } = context;
if (agentsMdWarning !== undefined) {
this.agentsMdWarning = agentsMdWarning;
Expand All @@ -791,6 +801,22 @@ export class Session {
}
}

/**
* Read the `[tools].disabled` array from the raw config. v1's
* KimiConfigSchema does not have a typed `tools` section (v2 uses a
* dedicated toolPolicy service), so unknown sections land in
* `config.raw`. This extracts the disabled tool names safely. #2534.
*/
readToolsDisabled(): string[] {
const raw = this.kimiConfig?.raw;
if (!raw) return [];
const toolsSection = raw['tools'];
if (typeof toolsSection !== 'object' || toolsSection === null) return [];
const disabled = (toolsSection as Record<string, unknown>)['disabled'];
if (!Array.isArray(disabled)) return [];
return disabled.filter((v): v is string => typeof v === 'string');
}

async getSessionWarnings(): Promise<readonly SessionWarning[]> {
const warnings: SessionWarning[] = [];
const agentsMdWarning = await this.computeAgentsMdWarning();
Expand Down Expand Up @@ -1315,7 +1341,18 @@ export class Session {
if (agent.config.systemPrompt === '') return;
const profile = this.resolvePersistedProfile(agent, meta, parentAgent);
if (profile === undefined) return;
// Apply global [tools].disabled to resumed/reloaded agents too. The
// bootstrap path (bootstrapAgentProfile) merges it at session start, but
// restoreAgentProfileHandle only calls setActiveProfile - which does not
// re-apply the tool denylist. agent.resume() has already replayed
// persisted tool state, so add the denylist without replacing the
// replayed enabled set (which would drop unrelated runtime selections).
// #2534.
agent.setActiveProfile(profile, this.options.kimiHomeDir);
const toolsDisabled = this.readToolsDisabled();
if (toolsDisabled.length > 0) {
agent.tools.addDisallowedTools(toolsDisabled);
}
}

private resolvePersistedProfile(
Expand Down
26 changes: 25 additions & 1 deletion packages/agent-core/src/session/subagent-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,21 @@ export class SessionSubagentHost {
*/
delegatableSubagents(callerProfileName?: string): Record<string, ResolvedAgentProfile> {
const owner = this.getOwnerAgent?.() ?? this.session.getReadyAgent(this.ownerAgentId);
return this.resolveDelegatableSubagents(callerProfileName, owner?.config.subagentNames);
const profiles = this.resolveDelegatableSubagents(callerProfileName, owner?.config.subagentNames);
// Apply global [tools].disabled to the profiles exposed to the parent
// agent's Agent tool description, so the model does not advertise a
// subagent as having a tool the spawned child will not receive. #2534.
const toolsDisabled = this.session.readToolsDisabled();
if (toolsDisabled.length === 0) return profiles;
return Object.fromEntries(
Object.entries(profiles).map(([name, profile]) => [
name,
{
...profile,
disallowedTools: [...(profile.disallowedTools ?? []), ...toolsDisabled],
},
]),
);
}

private resolveDelegatableSubagents(
Expand Down Expand Up @@ -457,7 +471,17 @@ export class SessionSubagentHost {
const subagentNames = Object.keys(
this.session.agentCatalog.delegatableSubagents(profile.name),
);

// Apply global [tools].disabled to subagents without persisting it into
// the child's profile/wire record. useProfile persists only
// profile.disallowedTools; config denies are added via addDisallowedTools,
// which mutates the deny set without logging a record, so a config deny
// does not survive a later config removal on resume. #2534.
const toolsDisabled = this.session.readToolsDisabled();
child.useProfile(profile, context, this.session.options.kimiHomeDir, subagentNames);
if (toolsDisabled.length > 0) {
child.tools.addDisallowedTools(toolsDisabled);
}
child.tools.inheritUserTools(parent.tools);
}

Expand Down
1 change: 1 addition & 0 deletions packages/agent-core/test/session/subagent-host.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1856,6 +1856,7 @@ function fakeSession(
custom: {},
},
writeMetadata: vi.fn(async () => {}),
readToolsDisabled: vi.fn((): string[] => []),
systemContextKaos: vi.fn((cwd: string) => parent.kaos.withCwd(cwd)),
getReadyAgent: vi.fn((id: string) => agents.get(id)),
ensureAgentResumed: vi.fn(async (id: string) => {
Expand Down