Merge branch 'worktree-session-modes-rfc' (master: sandbox stack #169, skills #109, prompt snapshots #254)

The stack rebases onto a moved master through its base branch. Beyond
mechanical unions (both branches' demo scripts, example rows, service
roles, tool lists, acp deps, doc budgets — each side fit alone, the
union needs the higher ceilings), three semantic reconciliations:

- The ACP bridge now carries BOTH per-session surfaces: the sandbox
  stack's config options + approval answerer and this branch's session
  modes; session/new and session/load advertise modes AND configOptions
  side by side.
- The feature matrix supersedes the sandbox stance per the RFC's
  second-lander rule: session/set_mode and current_mode_update flip to
  shipped-by-dsh-mode, config-option rows stay as #169 wrote them, and
  §6 records both landed features under the picker-to-modes /
  knobs-to-config-options division.
- The snapshot pin grammar (#254: one header snapshot + declared deltas
  + a Markdown prompt golden) gains a symmetric declaration for what a
  delta cannot express: expectedHeaderSnapshots — a plan-mode flip
  resorts the canonical tool list, so its widening lands as a second
  full snapshot, now its own Markdown section. The pin-less-class and
  model-turn-only-pin amendments carry over; new fixtures cover the
  extended writer paths, and the plan-acp-agent scenarios re-recorded
  under the merged composition (the app now bundles the skill tool)
  with the suite's refresh mode wired through.
This commit is contained in:
kingwl
2026-07-12 20:08:00 +08:00
341 changed files with 27410 additions and 7518 deletions

View File

@@ -0,0 +1,108 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk'
import { CallId } from '@deepseek-ai/dsh-llm'
import { AgentId, type Agent } from '@deepseek-ai/dsh-agent'
import ApprovalService, { type ApprovalRequest } from '@deepseek-ai/dsh-user-approval'
import { makeBridgeHarness, type BridgeHarness } from './harness.ts'
/**
* The bridge's `approval/request` answerer: an ask for an agent the bridge
* owns becomes a `session/request_permission` prompt attached to the tool
* call; foreign or call-less requests delegate down to the fail-closed
* default. Driven through `ctx.approval` — the same path dsh-tools' ask
* routing takes — against the harness's scriptable client.
*/
describe('acp bridge — approval answerer', () => {
let storageDir: string
let harness: BridgeHarness | undefined
beforeEach(async () => { storageDir = await mkdtemp(join(tmpdir(), 'acp-approval-')) })
afterEach(async () => {
await harness?.dispose()
harness = undefined
await rm(storageDir, { recursive: true, force: true })
})
async function ownedAgentRequest(
h: BridgeHarness, overrides: Partial<ApprovalRequest> = {},
): Promise<{ agent: Agent; request: ApprovalRequest }> {
await h.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
const { sessionId } = await h.client.newSession({ cwd: process.cwd(), mcpServers: [] })
const agent = h.ctx.agents.get(AgentId(sessionId))
if (agent === undefined) throw new Error('newSession created no agent')
// In production an ask always fires mid-turn (tool execution); open one so
// request()'s turn-enclosure precondition holds for the direct drive below.
agent.session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
return { agent, request: { agent, toolName: 'echo', callId: CallId('call-9'), ...overrides } }
}
it('prompts the editor for an owned agent and maps allow-once → allowed-once', async () => {
harness = await makeBridgeHarness({ storageDir })
await harness.ctx.plugin(ApprovalService)
harness.onPermission = () => ({ outcome: { outcome: 'selected', optionId: 'allow-once' } })
const { request } = await ownedAgentRequest(harness)
await expect(harness.ctx.approval.request(request)).resolves.toBe('allowed-once')
expect(harness.permissionRequests).toHaveLength(1)
const wire = harness.permissionRequests[0]
expect(wire?.toolCall).toEqual({ toolCallId: 'call-9' })
expect(wire?.options.map(o => ({ optionId: o.optionId, kind: o.kind }))).toEqual([
{ optionId: 'allow-once', kind: 'allow_once' },
{ optionId: 'reject-once', kind: 'reject_once' },
])
})
it('maps reject-once → rejected', async () => {
harness = await makeBridgeHarness({ storageDir })
await harness.ctx.plugin(ApprovalService)
harness.onPermission = () => ({ outcome: { outcome: 'selected', optionId: 'reject-once' } })
const { request } = await ownedAgentRequest(harness)
await expect(harness.ctx.approval.request(request)).resolves.toBe('rejected')
})
it('maps a client cancellation → cancelled', async () => {
harness = await makeBridgeHarness({ storageDir })
await harness.ctx.plugin(ApprovalService)
harness.onPermission = () => ({ outcome: { outcome: 'cancelled' } })
const { request } = await ownedAgentRequest(harness)
await expect(harness.ctx.approval.request(request)).resolves.toBe('cancelled')
})
it('treats an unknown optionId from a non-conforming client as a rejection, never a grant', async () => {
harness = await makeBridgeHarness({ storageDir })
await harness.ctx.plugin(ApprovalService)
harness.onPermission = () => ({ outcome: { outcome: 'selected', optionId: 'allow-always-i-insist' } })
const { request } = await ownedAgentRequest(harness)
await expect(harness.ctx.approval.request(request)).resolves.toBe('rejected')
})
it('delegates a foreign agent down to the fail-closed default', async () => {
harness = await makeBridgeHarness({ storageDir })
await harness.ctx.plugin(ApprovalService)
harness.onPermission = () => ({ outcome: { outcome: 'selected', optionId: 'allow-once' } })
// Not created through the bridge: no bySession entry, so the answerer must
// call next() — nobody else answers, so the seam fails closed.
const foreign = { session: { events: [{ type: 'turn/start' }], append: () => ({}) } } as unknown as Agent
await expect(harness.ctx.approval.request({ agent: foreign, toolName: 'echo', callId: CallId('c') }))
.resolves.toBe('unavailable')
expect(harness.permissionRequests).toHaveLength(0)
})
it('delegates a call-less request — the protocol prompt must attach to a tool call', async () => {
harness = await makeBridgeHarness({ storageDir })
await harness.ctx.plugin(ApprovalService)
harness.onPermission = () => ({ outcome: { outcome: 'selected', optionId: 'allow-once' } })
const { agent } = await ownedAgentRequest(harness)
await expect(harness.ctx.approval.request({ agent, toolName: 'echo' })).resolves.toBe('unavailable')
expect(harness.permissionRequests).toHaveLength(0)
})
})

View File

@@ -0,0 +1,260 @@
/**
* Session config options over the bridge: the two per-session knobs
* (`sandbox-mode`, `approval-policy`) advertised from composition capability,
* their current values folded from each session's own log, switching via
* `session/set_config_option` (one log-only event per switch — the log is the
* store), and a resumed session reporting its overrides back on
* `session/load` with no catch-up machinery.
*/
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk'
import * as Invariants from '@deepseek-ai/dsh-invariants'
import ApprovalService from '@deepseek-ai/dsh-user-approval'
import type { ApprovalPolicy } from '@deepseek-ai/dsh-user-approval'
import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local'
import type { SandboxMode } from '@deepseek-ai/dsh-sandbox'
import { makeBridgeHarness, textResponse, type BridgeHarness } from './harness.ts'
/**
* The REAL local executor reporting a confining default — `sandboxMode` is
* the documented capability override point (`dsh-bash-sandbox` overrides it
* the same way), so the bridge sees exactly what a sandboxing composition
* advertises without this suite dragging in a kernel sandbox stack.
*/
class SandboxedLocalExecutor extends LocalBashExecutor {
override get sandboxMode(): SandboxMode {
return 'read-only'
}
}
/** The exact option payloads the bridge advertises (pinned verbatim). */
function sandboxOption(currentValue: SandboxMode): object {
return {
id: 'sandbox-mode',
name: 'Sandbox',
description: 'The file sandbox mode bash commands in this session run under.',
category: 'mode',
type: 'select',
currentValue,
options: [
{ value: 'read-only', name: 'read-only' },
{ value: 'workspace-write', name: 'workspace-write' },
{ value: 'danger-full-access', name: 'danger-full-access' },
],
}
}
function approvalOption(currentValue: ApprovalPolicy): object {
return {
id: 'approval-policy',
name: 'Approvals',
description: 'ask: permission prompts reach you; never: they are rejected automatically.',
type: 'select',
currentValue,
options: [
{ value: 'ask', name: 'ask' },
{ value: 'never', name: 'never' },
],
}
}
describe('acp bridge — session config options', () => {
let storageDir: string
let h: BridgeHarness | undefined
let loader: BridgeHarness | undefined
beforeEach(async () => { storageDir = await mkdtemp(join(tmpdir(), 'acp-config-')) })
afterEach(async () => {
if (h) await h.dispose()
if (loader) await loader.dispose()
h = loader = undefined
await rm(storageDir, { recursive: true, force: true })
})
/** A harness whose composition can honor both knobs (sandboxed executor + approval seam). */
async function bothKnobs(options: { policy?: ApprovalPolicy; script?: NonNullable<Parameters<typeof makeBridgeHarness>[0]>['script'] } = {}): Promise<BridgeHarness> {
const harness = await makeBridgeHarness({ storageDir, ...options.script !== undefined ? { script: options.script } : {} })
// The dev invariants police turn-enclosure: an idle switch that appended
// outside a turn would throw right here in the suite, not in production.
await harness.ctx.plugin(Invariants)
await harness.ctx.plugin(SandboxedLocalExecutor, { timeoutMs: 10_000 })
await harness.ctx.plugin(ApprovalService, options.policy !== undefined ? { policy: options.policy } : {})
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
return harness
}
it('advertises no configOptions in a composition with neither knob', async () => {
h = await makeBridgeHarness({ storageDir })
await h.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
const res = await h.client.newSession({ cwd: process.cwd(), mcpServers: [] })
expect(res.configOptions).toBeUndefined()
})
it('a non-confining executor advertises no sandbox option (nothing would honor it)', async () => {
h = await makeBridgeHarness({ storageDir, withBash: true })
await h.ctx.plugin(ApprovalService)
await h.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
const res = await h.client.newSession({ cwd: process.cwd(), mcpServers: [] })
expect(res.configOptions).toEqual([approvalOption('ask')])
})
it('advertises both knobs with capability-derived currents (config default included)', async () => {
h = await bothKnobs({ policy: 'never' })
const res = await h.client.newSession({ cwd: process.cwd(), mcpServers: [] })
expect(res.configOptions).toEqual([sandboxOption('read-only'), approvalOption('never')])
})
it('an idle switch is pending (overlaid, not yet logged), then anchors INSIDE the next turn', async () => {
h = await bothKnobs({ script: [textResponse('ok')] })
const { sessionId } = await h.client.newSession({ cwd: process.cwd(), mcpServers: [] })
const afterSandbox = await h.client.setSessionConfigOption({ sessionId, configId: 'sandbox-mode', value: 'workspace-write' })
expect(afterSandbox.configOptions).toEqual([sandboxOption('workspace-write'), approvalOption('ask')])
const afterApproval = await h.client.setSessionConfigOption({ sessionId, configId: 'approval-policy', value: 'never' })
expect(afterApproval.configOptions).toEqual([sandboxOption('workspace-write'), approvalOption('never')])
// Idle: nothing in the log yet — turn-enclosure forbids a bare append
// (the dev invariants in this suite would throw), so the switch lives on
// the record until a turn opens.
const session = h.ctx.agents.list()[0]?.session
expect(session?.events.some(e => e.type === 'bash/sandbox-mode' || e.type === 'approval/policy')).toBe(false)
// The next turn anchors both switches inside itself, one event per knob.
await h.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'anchor' }] })
const events = session?.events ?? []
expect(events.filter(e => e.type === 'bash/sandbox-mode').map(e => e.data)).toEqual([{ mode: 'workspace-write' }])
expect(events.filter(e => e.type === 'approval/policy').map(e => e.data)).toEqual([{ policy: 'never' }])
const turnStart = events.findIndex(e => e.type === 'turn/start')
const anchored = events.findIndex(e => e.type === 'bash/sandbox-mode')
expect(turnStart).toBeGreaterThanOrEqual(0)
expect(anchored).toBeGreaterThan(turnStart)
})
it('an idle flip-flop anchors as ONE event (last write per knob wins)', async () => {
h = await bothKnobs({ script: [textResponse('ok')] })
const { sessionId } = await h.client.newSession({ cwd: process.cwd(), mcpServers: [] })
await h.client.setSessionConfigOption({ sessionId, configId: 'sandbox-mode', value: 'workspace-write' })
await h.client.setSessionConfigOption({ sessionId, configId: 'sandbox-mode', value: 'danger-full-access' })
await h.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'anchor' }] })
const events = h.ctx.agents.list()[0]?.session.events ?? []
expect(events.filter(e => e.type === 'bash/sandbox-mode').map(e => e.data)).toEqual([{ mode: 'danger-full-access' }])
// Idle again AFTER a completed turn (the log now ends in turn/end): a new
// switch pends rather than appending outside the closed turn.
const again = await h.client.setSessionConfigOption({ sessionId, configId: 'sandbox-mode', value: 'read-only' })
expect(again.configOptions?.find(option => option.id === 'sandbox-mode')).toMatchObject({ currentValue: 'read-only' })
expect(events.filter(e => e.type === 'bash/sandbox-mode')).toHaveLength(1)
})
it('a no-op switch (the value already shown) records nothing and keeps a live pending', async () => {
h = await bothKnobs({ script: [textResponse('ok')] })
const { sessionId } = await h.client.newSession({ cwd: process.cwd(), mcpServers: [] })
// Re-pushing the composition default (what clients that echo current
// selections on session start do) must not mint an override event.
const echo = await h.client.setSessionConfigOption({ sessionId, configId: 'approval-policy', value: 'ask' })
expect(echo.configOptions?.find(option => option.id === 'approval-policy')).toMatchObject({ currentValue: 'ask' })
// Re-sending a PENDING value keeps the pending switch alive (it is what
// the session shows), rather than cancelling it.
await h.client.setSessionConfigOption({ sessionId, configId: 'sandbox-mode', value: 'workspace-write' })
const repeat = await h.client.setSessionConfigOption({ sessionId, configId: 'sandbox-mode', value: 'workspace-write' })
expect(repeat.configOptions?.find(option => option.id === 'sandbox-mode')).toMatchObject({ currentValue: 'workspace-write' })
await h.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'anchor' }] })
const events = h.ctx.agents.list()[0]?.session.events ?? []
expect(events.filter(e => e.type === 'approval/policy')).toHaveLength(0)
expect(events.filter(e => e.type === 'bash/sandbox-mode').map(e => e.data)).toEqual([{ mode: 'workspace-write' }])
})
it('a net-zero idle flip-flop anchors NOTHING (switches are recorded, select clicks are not)', async () => {
h = await bothKnobs({ script: [textResponse('ok')] })
const { sessionId } = await h.client.newSession({ cwd: process.cwd(), mcpServers: [] })
await h.client.setSessionConfigOption({ sessionId, configId: 'sandbox-mode', value: 'workspace-write' })
const back = await h.client.setSessionConfigOption({ sessionId, configId: 'sandbox-mode', value: 'read-only' })
expect(back.configOptions?.find(option => option.id === 'sandbox-mode')).toMatchObject({ currentValue: 'read-only' })
await h.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'anchor' }] })
const events = h.ctx.agents.list()[0]?.session.events ?? []
expect(events.filter(e => e.type === 'bash/sandbox-mode')).toHaveLength(0)
})
it('a mid-turn switch anchors immediately (the open turn encloses it)', async () => {
h = await bothKnobs({ script: ['hang'] })
const { sessionId } = await h.client.newSession({ cwd: process.cwd(), mcpServers: [] })
const hung = h.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'go' }] })
// Give the loop a tick to open the turn (the turns.spec hang idiom).
await new Promise(resolve => setTimeout(resolve, 30))
await h.client.setSessionConfigOption({ sessionId, configId: 'sandbox-mode', value: 'workspace-write' })
await h.client.setSessionConfigOption({ sessionId, configId: 'approval-policy', value: 'never' })
const events = h.ctx.agents.list()[0]?.session.events ?? []
const turnStart = events.findIndex(e => e.type === 'turn/start')
const anchored = events.findIndex(e => e.type === 'bash/sandbox-mode')
expect(turnStart).toBeGreaterThanOrEqual(0)
expect(anchored).toBeGreaterThan(turnStart)
expect(events.some(e => e.type === 'approval/policy')).toBe(true)
await h.client.cancel({ sessionId })
await hung
})
it('tolerates a provided approval stand-in whose config skipped the plugin schema', async () => {
h = await makeBridgeHarness({ storageDir, script: [textResponse('ok')] })
h.ctx.provide('approval', { config: {} } as unknown as InstanceType<typeof ApprovalService>)
await h.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
const res = await h.client.newSession({ cwd: process.cwd(), mcpServers: [] })
expect(res.configOptions).toEqual([approvalOption('ask')])
const sessionId = res.sessionId
// The schema-less config also shields the no-op guard ('ask' by the ?? fallback)…
const echo = await h.client.setSessionConfigOption({ sessionId, configId: 'approval-policy', value: 'ask' })
expect(echo.configOptions).toEqual([approvalOption('ask')])
// …and the anchor-time comparison: a real switch under the stand-in still anchors.
await h.client.setSessionConfigOption({ sessionId, configId: 'approval-policy', value: 'never' })
await h.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'anchor' }] })
const events = h.ctx.agents.list()[0]?.session.events ?? []
expect(events.filter(e => e.type === 'approval/policy').map(e => e.data)).toEqual([{ policy: 'never' }])
})
it('rejects unknown ids, unadvertised ids, boolean values, and out-of-vocabulary values', async () => {
h = await makeBridgeHarness({ storageDir })
await h.ctx.plugin(ApprovalService)
await h.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
const { sessionId } = await h.client.newSession({ cwd: process.cwd(), mcpServers: [] })
await expect(h.client.setSessionConfigOption({ sessionId, configId: 'reasoning-effort', value: 'max' }))
.rejects.toThrow(/unknown config option/)
// sandbox-mode exists as a concept but THIS composition never advertised it.
await expect(h.client.setSessionConfigOption({ sessionId, configId: 'sandbox-mode', value: 'workspace-write' }))
.rejects.toThrow(/unknown sandbox-mode value/)
await expect(h.client.setSessionConfigOption({ sessionId, configId: 'approval-policy', type: 'boolean', value: true }))
.rejects.toThrow(/select; boolean values are not accepted/)
await expect(h.client.setSessionConfigOption({ sessionId, configId: 'approval-policy', value: 'always' }))
.rejects.toThrow(/unknown approval-policy value/)
})
it('a switch in one session never leaks into a concurrent one (state and pending both per-session)', async () => {
h = await bothKnobs()
const a = await h.client.newSession({ cwd: process.cwd(), mcpServers: [] })
const b = await h.client.newSession({ cwd: process.cwd(), mcpServers: [] })
await h.client.setSessionConfigOption({ sessionId: a.sessionId, configId: 'sandbox-mode', value: 'danger-full-access' })
// B sees its own composition defaults, not A's pending switch...
const bAfter = await h.client.setSessionConfigOption({ sessionId: b.sessionId, configId: 'approval-policy', value: 'never' })
expect(bAfter.configOptions).toEqual([sandboxOption('read-only'), approvalOption('never')])
// ...and A keeps its own state, untouched by B's.
const aAfter = await h.client.setSessionConfigOption({ sessionId: a.sessionId, configId: 'sandbox-mode', value: 'danger-full-access' })
expect(aAfter.configOptions).toEqual([sandboxOption('danger-full-access'), approvalOption('ask')])
})
it('session/load reports a resumed session\'s overrides from its own log', async () => {
h = await bothKnobs({ script: [textResponse('ok')] })
const { sessionId } = await h.client.newSession({ cwd: process.cwd(), mcpServers: [] })
await h.client.setSessionConfigOption({ sessionId, configId: 'sandbox-mode', value: 'danger-full-access' })
await h.client.setSessionConfigOption({ sessionId, configId: 'approval-policy', value: 'never' })
// One turn checkpoints the log (the switch events flush with it).
await h.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'persist me' }] })
await h.dispose()
h = undefined
loader = await bothKnobs()
const res = await loader.client.loadSession({ sessionId, cwd: process.cwd(), mcpServers: [] })
expect(res.configOptions).toEqual([sandboxOption('danger-full-access'), approvalOption('never')])
})
})