fix(mode): the Code Mode SDK section is re-rendered under the mode's visibility rule
Review follow-up on the residual the previous commit accepted — and the acceptance was wrong, because the fix is clean: in Code Mode the SDK section IS the soft surface (the wire carries only run_code), section text resolves in assemble's base, and renderToolsSdk is an exported pure renderer. The outermost wrapper therefore re-renders tools:sdk from the same visibility predicate the wire filter applies (allowlist, exit-IFF-plan, minus run_code mirroring the registry's own exclusion): a plan-mode program is documented exactly the callable bindings — read and the exit, never the denied write. The default mode leaves the section untouched (absence of policy), both pinned by tests. The soft layer's promise — the model is never encouraged toward a tool the gate denies — now holds in Code Mode too; the only remaining prompt-honesty residual is a prepend-after-load assemble listener, where the gate still covers execution.
This commit is contained in:
@@ -12,7 +12,7 @@ The `default` mode is the absence of policy: no section, no filtering, no gate.
|
||||
|
||||
**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`.
|
||||
|
||||
**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, and every bridged sub-call re-enters this gate with the same agent, so the allowlist governs each capability individually.
|
||||
**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.
|
||||
|
||||
## `ctx.modes`
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
import { Context, Service } from 'cordis'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import { defineTool, RUN_CODE_NAME } from '@deepseek-ai/dsh-tools'
|
||||
import { defineTool, renderToolsSdk, RUN_CODE_NAME } from '@deepseek-ai/dsh-tools'
|
||||
import type { PreToolDecision } from '@deepseek-ai/dsh-tools'
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import type {} from '@deepseek-ai/dsh-user-interaction'
|
||||
@@ -267,14 +267,26 @@ export class ModesService extends Service {
|
||||
return result
|
||||
}
|
||||
const allowed = new Set(active.definition.tools)
|
||||
const visible = (name: string): boolean =>
|
||||
allowed.has(name) && (name !== EXIT_PLAN_MODE || active.name === PLAN_MODE)
|
||||
// run_code is a TRANSPORT, not a capability: under the registry's Code
|
||||
// Mode it is the only wire tool (filtering it would leave the model
|
||||
// with nothing, not even the exit), and every bridged sub-call
|
||||
// re-enters tools/pre-execute with the same agent, where the allowlist
|
||||
// governs each capability individually.
|
||||
result.tools = result.tools.filter(tool =>
|
||||
(allowed.has(tool.name) || tool.name === RUN_CODE_NAME)
|
||||
&& (tool.name !== EXIT_PLAN_MODE || active.name === PLAN_MODE))
|
||||
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).
|
||||
// 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)
|
||||
}
|
||||
return result
|
||||
}, { prepend: true })
|
||||
|
||||
|
||||
@@ -370,6 +370,31 @@ describe('the soft layer', () => {
|
||||
// Code Mode's only wire tool survives the filter — without it the model
|
||||
// would have NO tools at all, not even a path to the exit review.
|
||||
expect(assembly.tools.map(tool => tool.name)).toEqual(['run_code'])
|
||||
// The SDK section is Code Mode's soft surface: it is re-rendered under
|
||||
// the same visibility rule, so plan mode documents exactly the callable
|
||||
// bindings — the allowlisted read and the exit — and never the denied write.
|
||||
const sdk = assembly.sections.find(section => section.name === 'tools:sdk')?.text ?? ''
|
||||
expect(sdk).toContain('read(args:')
|
||||
expect(sdk).toContain('exit_plan_mode(args:')
|
||||
expect(sdk).not.toContain('write(args:')
|
||||
})
|
||||
|
||||
it('leaves the Code Mode SDK section untouched in the default mode', 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 agent = agentWithSession()
|
||||
const sdk = (await ctx.systemPrompt.assemble({ agent })).sections.find(section => section.name === 'tools:sdk')?.text ?? ''
|
||||
expect(sdk).toContain('read(args:')
|
||||
expect(sdk).toContain('write(args:')
|
||||
})
|
||||
|
||||
it('treats a dropped folded definition as the default mode', async () => {
|
||||
|
||||
Reference in New Issue
Block a user