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:
@@ -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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user