/** * Per-session sandbox-mode override: the session log as the store. A runtime * switch (a UI policy control or test scenario) is recorded as one * `sandbox/mode` event on the session it applies to; * `effective = fold(events) ?? the deployment default`, so an override * survives restart by replay, two sessions can never see each other's state, * and there is no external config store. The event is log-only (the * `approval/*` precedent): the policy owner projects the fold into each model * request, while enforcing tools report operation-specific boundary markers. * EXECUTION honors the same fold through `ctx.sandboxPolicy.resolve()` — it * stamps the mode together with the calling session's workspace root onto each * capability call, weakest-precedence beneath an escalation grant. * * The override is policy state shared by every enforcing family (bash and * filesystem alike), so it lives here in the policy package rather than in any * one capability's seam. * * @module dsh-sandbox-policy/session-mode */ import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' import type { SandboxMode } from '@deepseek-ai/dsh-sandbox' declare module '@deepseek-ai/dsh-session' { interface SessionEventMap { /** * The session's sandbox mode was switched — log-only (like `approval/*`; * NOT a surface event, carries no `surfaceOp`): durable and replayable, * never in the model transcript. The LAST such event is the session's * override ({@link effectiveSandboxMode}). `source: 'delegation'` marks * an override seeded into a child; an absent source is a runtime switch. */ 'sandbox/mode': { mode: SandboxMode /** Marks an override seeded into a child at delegation. */ source?: 'delegation' } } } /** Every {@link SandboxMode}, for option advertisement and runtime validation of untrusted mode strings. */ export const SANDBOX_MODES: readonly SandboxMode[] = ['read-only', 'workspace-write', 'danger-full-access'] /** * The session's sandbox-mode override: the last `sandbox/mode` event in the * log, or undefined when the session never switched (callers apply the * deployment default). The pure fold — resume needs no catch-up machinery * because replaying the log IS the state. * @param events - session events in log order (other event types are skipped). * @returns the mode of the last switch event, or undefined without one. */ export function effectiveSandboxMode(events: readonly SessionEvent[]): SandboxMode | undefined { for (let index = events.length - 1; index >= 0; index -= 1) { const event = events[index] as SessionEvent if (event.type === 'sandbox/mode') return event.data.mode } return undefined } /** * THE write path for a session's sandbox-mode override: appends exactly one * `sandbox/mode` event — the switch IS its event; nothing mutates mode state * out of band. Takes effect on the session's next confined call (bash or fs) * — the consumers fold on every read. * @param session - the session the override belongs to. * @param mode - the mode every subsequent confined call in this session runs * under (until the next switch). */ export function setSandboxMode(session: Session, mode: SandboxMode): void { session.append('sandbox/mode', { mode }) }