review pass: purge stale enforcement language after the axis decoupling

Self-review findings, all prose/metadata (no behavior change): the
package description still claimed hard (execution) enforcement; the RFC
Problem framing still defined plan mode as a read-only tool policy;
integration/mode/snapshot-suite comments still referenced the removed
sandbox clamp and cap; the header-fallback explanation said
'front-of-list insertion' where the actual mechanism is a non-tail
insertion the append-only tools delta cannot express (exit_plan_mode
sits mid-list alphabetically); AGENTS.md's layout line and the RFC's
'policy state/listener' phrasing move to collaboration-state wording.
This commit is contained in:
kingwl
2026-07-20 14:01:53 +08:00
parent c0146b9c4a
commit 994eaedc02
7 changed files with 32 additions and 31 deletions

View File

@@ -10,7 +10,7 @@ The `default` mode is the absence of policy: no section, no extra tool. An agent
## What a mode carries
**The guidance section.** A `system-prompt/assemble` listener renders the mode's `section` text as the `mode:policy` section (order 50) while the mode is in force, and shows the `exit_plan_mode` tool IFF the folded mode is `plan` — on the wire and, under the registry's Code Mode, in the `tools:sdk` section alike. Every transition therefore surfaces as an attributable `request/header` event on the next step (entering plan adds the exit tool, a front-of-list insertion with no delta form → the full fallback snapshot; the approved exit removes exactly that tool and the section, a pure removal → one `request/header-delta`).
**The guidance section.** A `system-prompt/assemble` listener renders the mode's `section` text as the `mode:policy` section (order 50) while the mode is in force, and shows the `exit_plan_mode` tool IFF the folded mode is `plan` — on the wire and, under the registry's Code Mode, in the `tools:sdk` section alike. Every transition therefore surfaces as an attributable `request/header` event on the next step (entering plan adds the exit tool at its canonical alphabetical position — a non-tail insertion the append-only tools delta cannot express → the full fallback snapshot; the approved exit removes exactly that tool and the section, a pure removal → one `request/header-delta`).
**Deliberately absent: enforcement.** A mode never gates execution, filters the toolset, or touches the sandbox/approval knobs — those are independent axes the user switches separately (a deployment that wants a hard read-only floor while planning flips the sandbox-mode option beside the mode picker, in either order; neither disturbs the other). A per-mode tool allow/deny list is likewise out: which tools a mode admits is an effects question — a per-tool read-only/mutating classification the harness does not yet have — parked until tool definitions declare their effects (the plan-mode RFC's deferred item). The config vocabulary is exactly `{ section }`, and an unknown key (a `tools` list or an `access` cap included) fails loud at load.

View File

@@ -1,6 +1,6 @@
{
"name": "@deepseek-ai/dsh-mode",
"description": "Session modes for the DeepSeek Harness: plan mode as logged per-agent policy state with soft (prompt) and hard (execution) enforcement",
"description": "Session modes for the DeepSeek Harness: plan mode as a logged per-agent collaboration state with a guidance section and a user-reviewed exit",
"version": "0.0.1",
"private": true,
"type": "module",

View File

@@ -82,9 +82,9 @@ describe('plan mode through the agent loop', () => {
expect(header.data.header.tools?.map(tool => tool.name)).toEqual(['exit_plan_mode', 'read', 'write'])
expect(header.data.header.system).toContain('plan mode')
// No general tool gate: the write RUNS — plan's non-shell restraint is
// the section's guidance (the shell restraint is the sandbox clamp,
// pinned in mode.spec). The mode itself stays plan throughout.
// No tool gate: the write RUNS — plan restrains by the section's
// guidance alone (enforcement lives on the independent sandbox/approval
// axes). The mode itself stays plan throughout.
const result = findEvent(log, 'tool/result')
expect(result.data.isError).toBe(false)
expect(foldMode(log)).toBe(PLAN_MODE)
@@ -115,8 +115,10 @@ describe('plan mode through the agent loop', () => {
{ type: 'text', text: 'The user switched this session to plan mode.' },
])
// The header change is logged as a FULL fallback snapshot, not a delta:
// adding exit_plan_mode reorders the canonical tool list (it sorts
// first), and a pure reordering is inexpressible in the delta encoding.
// the tools delta can only APPEND additions, while the canonical
// (alphabetical) order places exit_plan_mode before read/write — a
// non-tail insertion the writer's round-trip guard rejects, so it
// falls back.
const second = findEvent(log, 'request/header', 'last')
expect(second.data.reason).toBe('fallback')
expect(second.data.header.tools?.map(tool => tool.name)).toEqual(['exit_plan_mode', 'read', 'write'])

View File

@@ -350,10 +350,10 @@ describe('the soft layer', () => {
it('leaves foreign post-next() additions alone in plan mode (no general tool filtering)', async () => {
// A foreign listener that post-processes await next(): the mode filter
// wraps outside it (prepend) but hides only the exit tool outside plan
// and the bash trio under an unhonorable cap — a foreign addition
// survives, because which tools a mode admits is deliberately not this
// plugin's decision (the effects question stays parked; module doc).
// wraps outside it (prepend) but hides only the exit tool outside plan
// a foreign addition survives, because which tools a mode admits is
// deliberately not this plugin's decision (the effects question stays
// parked; module doc).
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
@@ -388,8 +388,8 @@ describe('the soft layer', () => {
agent.session.append('mode/set', { mode: PLAN_MODE })
const assembly = await ctx.systemPrompt.assemble({ agent })
expect(assembly.tools.map(tool => tool.name)).toEqual(['run_code'])
// The SDK documents the full binding set plus the exit — plan mode no
// longer prunes capabilities; its restraint is the section + the sandbox.
// The SDK documents the full binding set plus the exit — a mode never
// prunes capabilities; it restrains by the section's guidance alone.
const sdk = assembly.sections.find(section => section.name === 'tools:sdk')?.text ?? ''
expect(sdk).toContain('read(args:')
expect(sdk).toContain('write(args:')