refactor(mode): modes are collaboration states — drop the access cap; enforcement axes stay independent
Review follow-up (tianyicui): plan mode and the sandbox are orthogonal
AXES, not just orthogonal state — entering plan must not change what the
sandbox enforces, matching Codex's separation of Plan/Default
collaboration presets from sandbox and approval settings.
ModeDefinition.access, the bash/resolve-mode clamp, and both cap-derived
guards are removed; a ModeDefinition is exactly { section }, and a mode
now carries only its guidance section plus the exit_plan_mode review.
The bash seam's resolveMode + waterfall go with their only listener:
dsh-bash and dsh-tool-bash revert to master byte-for-byte, and the
dsh-mode → dsh-bash dependency edge is gone. A deployment that wants
kernel-enforced read-only planning pairs the mode picker with the
independent sandbox-mode option, in either order.
The RFC archives this as the second removed enforcement shape (after
the interim allowlist) with the same restart trigger — effects
self-declaration; the orthogonality FAQ now answers with the two-axis
rule. The plan example demonstrates the axes side by side, and the
re-recorded fixtures pin the guidance-only section.
This commit is contained in:
@@ -3,13 +3,6 @@
|
||||
* register process handles with `ctx.tasks`; their work uses task cancellation
|
||||
* rather than the tool-call signal after an id is returned.
|
||||
*
|
||||
* Each call is stamped `escalation grant > ctx.bash.resolveMode()` — the
|
||||
* seam's resolution (session override ?? executor default) run through the
|
||||
* `bash/resolve-mode` waterfall, where policy plugins (e.g. a session mode's
|
||||
* `access` cap) narrow it per call. The prompt deliberately does not state
|
||||
* the mode and no switch is narrated: the model learns the boundary from the
|
||||
* denial marker exactly when it matters.
|
||||
*
|
||||
* TODO(permissions): deployment policy belongs in `tools/pre-execute` and
|
||||
* sandboxing executors; see docs/architecture.md § Extending The Harness.
|
||||
* @module @deepseek-ai/dsh-tool-bash
|
||||
@@ -27,7 +20,7 @@ import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import type {} from '@deepseek-ai/dsh-tasks'
|
||||
import type {} from '@deepseek-ai/dsh-user-approval'
|
||||
import type { SandboxMode } from '@deepseek-ai/dsh-sandbox'
|
||||
import { DSH_ENV_PREFIX } from '@deepseek-ai/dsh-bash'
|
||||
import { DSH_ENV_PREFIX, effectiveSandboxMode } from '@deepseek-ai/dsh-bash'
|
||||
import type { DshEnvironment, DshEnvironmentKey } from '@deepseek-ai/dsh-bash'
|
||||
import { DSH_HOME_ENV, resolveDshHome } from '@deepseek-ai/dsh-home'
|
||||
import { processOutcome } from './background.ts'
|
||||
@@ -350,14 +343,14 @@ export function apply(ctx: Context, config: Config = {}): void {
|
||||
const defaultMode = ctx.bash.sandboxMode
|
||||
const escalationModes: readonly SandboxMode[] = defaultMode === undefined ? [] : ESCALATION_TARGETS
|
||||
|
||||
const sessionOverride = (exec: ToolExecution): SandboxMode | undefined =>
|
||||
defaultMode === undefined || exec.agent === undefined ? undefined : effectiveSandboxMode(exec.agent.session.events)
|
||||
|
||||
const approveEscalation = async (mode: string, justification: string, exec: ToolExecution): Promise<SandboxMode> => {
|
||||
if (escalationModes.length === 0) {
|
||||
throw new Error('sandbox_permissions is not available in this composition (no sandboxing executor to escalate)')
|
||||
}
|
||||
// Strict widening runs against the seam's resolution — the same value
|
||||
// ordinary calls are stamped with. The cast is exact: escalationModes
|
||||
// non-empty proved the executor confines, resolveMode's only undefined path.
|
||||
const effectiveMode = (await ctx.bash.resolveMode(exec.agent?.session)) as SandboxMode
|
||||
const effectiveMode = (sessionOverride(exec) ?? defaultMode) as SandboxMode
|
||||
if (!(WIDER_MODES[effectiveMode] ?? []).includes(mode as SandboxMode)) {
|
||||
throw new Error(`sandbox escalation to "${mode}" is not strictly wider than this call's current "${effectiveMode}" mode`)
|
||||
}
|
||||
@@ -425,7 +418,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
||||
// Description is display metadata; workdir defaults to the caller's session.
|
||||
const sandboxMode = args.sandbox_permissions !== undefined && args.justification !== undefined
|
||||
? await approveEscalation(args.sandbox_permissions, args.justification, exec)
|
||||
: await ctx.bash.resolveMode(exec.agent?.session)
|
||||
: sessionOverride(exec)
|
||||
const workdir = resolveWorkdir(args.workdir, exec)
|
||||
const dshEnv = bashEnv.collect(exec)
|
||||
const request = {
|
||||
|
||||
@@ -613,25 +613,6 @@ describe('sandbox escalation through the generic task producer', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('the bash/resolve-mode waterfall at the tool layer', () => {
|
||||
it('stamps the waterfall result — a listener narrows ordinary calls and the escalation baseline alike', async () => {
|
||||
const { ctx, bash } = await setupSandboxed(true)
|
||||
ctx.on('bash/resolve-mode', async (_session, next) => {
|
||||
await next()
|
||||
return 'read-only'
|
||||
})
|
||||
const agent = sandboxAgent('workspace-write')
|
||||
await call(ctx, 'bash', { command: 'true', description: 'clamped ordinary' }, agent)
|
||||
// Escalating TO workspace-write is strictly wider than the CLAMPED
|
||||
// read-only baseline — without the clamp it would be a non-widening no-op
|
||||
// against the standing override — and the freshly-approved grant outranks
|
||||
// the clamp for exactly that call.
|
||||
ctx.on('approval/request', () => Promise.resolve<ApprovalOutcome>('allowed-once'))
|
||||
await call(ctx, 'bash', { command: 'true', description: 'd', sandbox_permissions: 'workspace-write', justification: 'wider than the clamped baseline' }, agent)
|
||||
expect(bash.modes).toEqual(['read-only', 'workspace-write'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('renderProcessRead', () => {
|
||||
const base: BashProcessRead = { delta: 'out\n', lossy: false }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user