/** * The sandbox POLICY home (`ctx.sandboxPolicy`): the single owner of the * deployment's sandbox fallbacks plus per-session resolution: the file-effect * {@link SandboxMode}, the `workspace-write` root, and the override kit (the * `sandbox/mode` event, its fold, and its write path, from `./session-mode.ts`). * Before each agent request, the owner also contributes the resolved policy to * the cache-safe runtime-context snapshot. The agent loop logs that snapshot as * model history, so replay reconstructs the same mode and root the enforcing * consumers resolve without rewriting the stable system prompt. * * Enforcing filesystem, one-shot bash, and terminal backends read the SAME * resolved policy here. The context describes that policy without inventorying * capabilities, while each backend retains its own enforcement dialect and each * tool owns its operation-specific denial and escalation guidance. The service * reads session state once at each operation boundary; executors and providers * remain session-free. * * @module @deepseek-ai/dsh-sandbox-policy */ import { resolve as resolvePath } from 'node:path' import { Context, Service } from 'cordis' import z from 'schemastery' import type {} from '@deepseek-ai/dsh-agent' import { canonicalPath, type SandboxExecutionPolicy, type SandboxMode } from '@deepseek-ai/dsh-sandbox' import type { Session } from '@deepseek-ai/dsh-session' import type {} from '@deepseek-ai/dsh-system-prompt' import { effectiveSandboxMode } from './session-mode.ts' export { SANDBOX_MODES, effectiveSandboxMode, setSandboxMode } from './session-mode.ts' /** Resolve filesystem identity before lexical normalization can erase symlink-sensitive components. */ function resolveWorkspaceRoot(path: string): string { return resolvePath(canonicalPath(path)) } /** Render the policy without claiming which capabilities are mounted. */ function renderPolicyContext(policy: SandboxExecutionPolicy): string { switch (policy.mode) { case 'read-only': return 'Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.' case 'workspace-write': return `Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: ${JSON.stringify(policy.workspaceRoot)}. Some platform temporary areas may also be writable.` case 'danger-full-access': return 'Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.' /* v8 ignore next 4 -- SandboxMode is a typed same-process closed union; this branch is only the static exhaustiveness guard. */ default: { const mode: never = policy.mode throw new Error(`unreachable sandbox mode: ${String(mode)}`) } } } declare module 'cordis' { interface Context { sandboxPolicy: SandboxPolicyService } } /** * Plugin config: the deployment's sandbox default. All optional — `Config` * supplies the defaults (`mode: 'read-only'` is the fail-safe default; a * deployment that wants a workspace-writable agent opts in explicitly). The * runner choice is NOT here (it is the `ctx.sandbox` provider's config), nor * is any per-family knob: this is the one shared policy home. */ export interface Config { /** File-sandbox mode a session starts from (default: `read-only`). */ mode?: SandboxMode /** * Fallback root for agentless calls and sessions without a cwd (default: * `process.cwd()`). Normal agent calls use their session cwd instead. */ workspaceRoot?: string } /** Inputs that select the sandbox policy for one capability call. */ export interface SandboxPolicyRequest { /** Calling session; its immutable cwd becomes the workspace boundary. */ session?: Session /** Explicit approved mode override, which outranks session policy. */ mode?: SandboxMode } /** * The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment * default mode, fallback workspace root, and current request-time policy * section. Tool layers call {@link resolve} for each execution so a session's * mode log and immutable cwd travel together to every enforcing capability. */ export class SandboxPolicyService extends Service { // Inline schema call: the config catalog walks `static Config` statically. static Config: z = z.object({ mode: z.union(['read-only', 'workspace-write', 'danger-full-access'] as const).default('read-only'), // No schema default: process.cwd() is resolved in the constructor so the // stored root is always absolute regardless of how it was supplied. workspaceRoot: z.string(), }) /** The deployment default mode — the fallback beneath a session override. */ readonly defaultMode: SandboxMode /** The absolute `workspace-write` fallback root for calls without a session cwd. */ readonly workspaceRoot: string constructor(ctx: Context, config: Config) { super(ctx, 'sandboxPolicy') // schemastery (static Config) already filled `mode`; the cast records that // runtime fact. `workspaceRoot` has NO schema default, so its fallback to // the process cwd is real branching, resolved absolute either way. this.defaultMode = config.mode as SandboxMode this.workspaceRoot = resolveWorkspaceRoot(config.workspaceRoot ?? process.cwd()) ctx.inject(['systemPrompt'], (scope: Context) => { scope.systemPrompt.context({ name: 'sandbox:policy', order: 110, text: (context) => { const session = context.agent?.session return session === undefined ? '' : renderPolicyContext(this.resolve({ session })) }, }) }) } /** * Resolve the complete policy for one capability call. An approved explicit * mode outranks the session's last `sandbox/mode` event, which outranks the * deployment default. A session cwd is its workspace-write boundary; the * configured root is the fallback for agentless calls and sessions without a * cwd. * @param request - optional session and approved mode override. * @returns the fully resolved per-call mode and absolute workspace root. */ resolve(request: SandboxPolicyRequest = {}): SandboxExecutionPolicy { const { session } = request return { mode: request.mode ?? (session === undefined ? undefined : this.overrideOf(session)) ?? this.defaultMode, workspaceRoot: resolveWorkspaceRoot(session?.header.cwd ?? this.workspaceRoot), } } /** * Read the session override without applying the deployment default. * @param session - session whose log supplies the override. * @returns the last logged mode, or `undefined` without one. */ overrideOf(session: Session): SandboxMode | undefined { return effectiveSandboxMode(session.events) } } export default SandboxPolicyService