fix(mode): the default mode hides the exit binding from the Code Mode SDK too

Review finding, valid — the previous SDK fix covered only the
non-default branch: in the default mode under Code Mode the wire filter
dropped exit_plan_mode but the registry-rendered tools:sdk section
still advertised its binding, offering default-mode agents a call that
can only error and breaking the byte-identical claim (a no-dsh-mode
deployment's registry never saw the tool, so its SDK never listed it).

The SDK re-render extracts to one helper both branches share: the
non-default branch passes the mode's visibility rule, the default
branch hides exactly the exit binding. The pinning test now compares
the default-mode SDK byte-for-byte against a bare deployment without
dsh-mode — the strongest form of the invariant the RFC states.
This commit is contained in:
kingwl
2026-07-10 22:09:36 +08:00
parent bedd908331
commit 0c2e773d67
4 changed files with 45 additions and 19 deletions

View File

@@ -10,7 +10,7 @@ The `default` mode is the absence of policy: no section, no filtering, no gate.
## Two layers of enforcement
**Soft — what the model sees.** A `system-prompt/assemble` listener filters the returned assembly's tools down to the mode's allowlist and the `mode:policy` section (order 50) renders the mode's guidance text. Every transition therefore surfaces as an attributable `request/header` event on the next step (a delta when expressible; adding `exit_plan_mode` resorts the canonical tool list, which the delta encoding cannot express, so entering plan mode logs the full fallback snapshot). The `exit_plan_mode` tool is visible IFF the folded mode is `plan`.
**Soft — what the model sees.** A `system-prompt/assemble` listener filters the returned assembly's tools down to the mode's allowlist and the `mode:policy` section (order 50) renders the mode's guidance text. Every transition therefore surfaces as an attributable `request/header` event on the next step (a delta when expressible; adding `exit_plan_mode` resorts the canonical tool list, which the delta encoding cannot express, so entering plan mode logs the full fallback snapshot). The `exit_plan_mode` tool is visible IFF the folded mode is `plan` — on the wire and, under the registry's Code Mode, in the `tools:sdk` section alike.
**Hard — what can run.** A `tools/pre-execute` listener denies, deny-by-default against the same allowlist, any call the mode does not permit — a hallucinated call to a still-registered (or freshly re-widened) tool cannot run. Agent-less executions and the default mode pass through; the gate judges by the LOGGED mode only, never a pending intent. `run_code` passes both layers as a TRANSPORT: under the registry's Code Mode it is the only wire tool, every bridged sub-call re-enters this gate with the same agent, and the `tools:sdk` section is re-rendered under the mode's visibility rule — the allowlist governs each capability individually and the prompt documents exactly the callable set.

View File

@@ -264,6 +264,13 @@ export class ModesService extends Service {
const active = this.activeDefinition(agent.session)
if (active === undefined) {
result.tools = result.tools.filter(tool => tool.name !== EXIT_PLAN_MODE)
// The default mode hides exactly one thing on BOTH soft surfaces: the
// exit tool (registered always, callable only in plan). Without the
// SDK re-render a Code Mode deployment would still advertise an
// exit_plan_mode binding that can only error — and the default-mode
// assembly would no longer be byte-identical to a no-dsh-mode
// deployment, whose registry never saw the tool at all.
rerenderSdk(result, name => name !== EXIT_PLAN_MODE)
return result
}
const allowed = new Set(active.definition.tools)
@@ -277,19 +284,27 @@ export class ModesService extends Service {
result.tools = result.tools.filter(tool => visible(tool.name) || tool.name === RUN_CODE_NAME)
// Code Mode's soft surface is the SDK section, not the wire schemas —
// section text resolves in assemble's base, so the outermost wrapper
// can re-render it here from the same visibility rule the wire filter
// applies (minus run_code, mirroring the registry's own exclusion).
// re-renders it under the same visibility rule the wire filter applies.
// Without this the prompt would document bindings the gate denies.
const sdkIndex = result.sections.findIndex(section => section.name === 'tools:sdk')
if (sdkIndex >= 0) {
const sdkText = renderToolsSdk(ctx.tools.schemas().filter(schema =>
visible(schema.name) && schema.name !== RUN_CODE_NAME))
result.sections = result.sections.map((section, index) =>
index === sdkIndex ? { ...section, text: sdkText } : section)
}
rerenderSdk(result, visible)
return result
}, { prepend: true })
/**
* Re-render the `tools:sdk` section (present only under the registry's
* Code Mode) from the registry schemas the given rule admits — minus
* `run_code` itself, mirroring the registry's own exclusion. A no-op when
* the section is absent (native mode).
*/
function rerenderSdk(result: { sections: { name: string; order: number; text: string }[] }, include: (name: string) => boolean): void {
const sdkIndex = result.sections.findIndex(section => section.name === 'tools:sdk')
if (sdkIndex < 0) return
const sdkText = renderToolsSdk(ctx.tools.schemas().filter(schema =>
include(schema.name) && schema.name !== RUN_CODE_NAME))
result.sections = result.sections.map((section, index) =>
index === sdkIndex ? { ...section, text: sdkText } : section)
}
ctx.on('tools/pre-execute', (exec, next): Promise<PreToolDecision> => {
if (exec.agent === undefined) return next()
const active = this.activeDefinition(exec.agent.session)

View File

@@ -402,22 +402,33 @@ describe('the soft layer', () => {
expect(sdk).not.toContain('write(args:')
})
it('leaves the Code Mode SDK section untouched in the default mode', async () => {
it('default-mode Code Mode SDK is byte-identical to a no-dsh-mode deployment (exit binding hidden)', async () => {
class FakeRuntime extends CodeRuntime {
readonly language = 'typescript'
readonly isolation = 'fake'
run(_request: CodeRunRequest): Promise<CodeRunResult> { return Promise.resolve({ logs: [] }) }
}
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry, { mode: 'code' })
await ctx.plugin(FakeRuntime)
await ctx.plugin(ModesService)
registerNamedTools(ctx, ['read', 'write'])
const withModes = new Context()
await withModes.plugin(SystemPrompt)
await withModes.plugin(ToolRegistry, { mode: 'code' })
await withModes.plugin(FakeRuntime)
await withModes.plugin(ModesService)
registerNamedTools(withModes, ['read', 'write'])
const agent = agentWithSession()
const sdk = (await ctx.systemPrompt.assemble({ agent })).sections.find(section => section.name === 'tools:sdk')?.text ?? ''
const sdk = (await withModes.systemPrompt.assemble({ agent })).sections.find(section => section.name === 'tools:sdk')?.text ?? ''
expect(sdk).toContain('read(args:')
expect(sdk).toContain('write(args:')
// The always-registered exit tool is callable only in plan mode, so a
// default-mode SDK advertising it would offer a binding that can only
// error — and diverge from a deployment that never loaded dsh-mode:
const bare = new Context()
await bare.plugin(SystemPrompt)
await bare.plugin(ToolRegistry, { mode: 'code' })
await bare.plugin(FakeRuntime)
registerNamedTools(bare, ['read', 'write'])
const bareSdk = (await bare.systemPrompt.assemble({ agent })).sections.find(section => section.name === 'tools:sdk')?.text ?? ''
expect(sdk).toBe(bareSdk)
expect(sdk).not.toContain('exit_plan_mode(args:')
})
it('treats a dropped folded definition as the default mode', async () => {