feat(system-prompt): cache dynamic policy context

This commit is contained in:
NI0317
2026-07-30 22:09:15 +08:00
parent 936a487bb0
commit 8b4cbe4293
45 changed files with 722 additions and 374 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/sandbox/sandbox-policy/README.md
README.md: 45349f7b0bbb6e035dd2aa6f4695735124dd9f2d
README.zh.md: 9393d2a22aa3df310287ccb9c5e486880453a838
README.md: 0f5c9b7c21acddd789b21edf6c5a7e1316fe4053
README.zh.md: 10a4af24d82e8472803d96af1d9b7046731a7833

View File

@@ -18,16 +18,17 @@ Filesystem tools, one-shot bash commands, and terminal sessions may enforce the
- `ctx.sandboxPolicy.resolve({ session?, mode? })` — resolves one complete per-call policy. An explicit approved mode outranks the session's last `sandbox/mode` event, which outranks `defaultMode`; the session's immutable `cwd` is canonicalized with filesystem semantics before becoming `workspaceRoot`, otherwise the configured fallback applies. Canonicalization precedes lexical normalization so `symlink/..` agrees with process working-directory resolution.
- `ctx.sandboxPolicy.defaultMode` / `ctx.sandboxPolicy.workspaceRoot` — the deployment default and fallback root used by `resolve()`.
- `ctx.sandboxPolicy.registerEnforcedFamily(family)` — independently registers `filesystem`, `bash`, or `terminal` and returns the exact effect disposer. Equal families remain separate contributions; the section uses canonical family order and removes a family only after its final contribution leaves.
- `sandbox:policy` — a request-time system-prompt section derived from `resolve({ session })` and the active family contributions. It is empty without an enforcing family and states only the mode, the affected model-facing operations, and the canonical session workspace under `workspace-write`.
- `ctx.sandboxPolicy.registerEscalatableFamily(family)` — independently registers a family whose actual tool schema and execution path offer an approved wider retry. Anti-refusal guidance names only families that are both enforced and escalatable; contributions dispose independently.
- `sandbox:policy` — a request-time cache-safe context contribution derived from `resolve({ session })` and the active family contributions. It is empty without an enforcing family and states only the mode, the affected model-facing operations, and the canonical session workspace under `workspace-write`.
- `effectiveSandboxMode(events)` — the pure fold of a session's `sandbox/mode` events (the last switch wins, or `undefined`), used inside `resolve()`.
- `setSandboxMode(session, mode)` — THE write path for a per-session override: appends exactly one `sandbox/mode` event. The switch IS its event; nothing mutates the mode out of band.
- `SANDBOX_MODES` — every mode, for option advertisement and runtime validation.
The optional `./invariant` companion rejects a forged durable `sandbox/mode` event whose value falls outside that closed vocabulary; Session and its companion own the surrounding storage and core execution-enclosure rules. The rendered section is logged inside `request/header`, so the exact effective policy remains reconstructable without another event or an in-memory “last told” mirror.
The optional `./invariant` companion rejects a forged durable `sandbox/mode` event whose value falls outside that closed vocabulary; Session and its companion own the surrounding storage and core execution-enclosure rules. The agent loop logs the assembled full runtime-context snapshot as a sourced `user/message`, so exact policy input remains reconstructable without an in-memory “last told” mirror.
## The per-session store
A runtime switch is one log-only `sandbox/mode` event on the session it applies to. `effective = explicit grant ?? fold(events) ?? deployment default`, so an override survives restart by replay and two sessions never see each other's state. Workspace identity does not need another event: the immutable `SessionHeader.cwd` recorded at creation is the root for every call in that session. The event stays log-only; the next request assembles the current section from the fold before any tool call.
A runtime switch is one log-only `sandbox/mode` event on the session it applies to. `effective = explicit grant ?? fold(events) ?? deployment default`, so an override survives restart by replay and two sessions never see each other's state. Workspace identity does not need another event: the immutable `SessionHeader.cwd` recorded at creation is the root for every call in that session. The event stays log-only; before the next request, the owner contributes the current fact to the full runtime-context snapshot.
## Model Experience
@@ -35,12 +36,12 @@ A runtime switch is one log-only `sandbox/mode` event on the session it applies
#### What the model sees
One `sandbox:policy` system section on each agent request when at least one enforcing family is registered. The examples below show all three families; absent families are omitted. Tool plugins retain operation and escalation guidance, approval policy remains `dsh-user-approval`'s section, and plan guidance remains `dsh-plan-mode`'s section.
One `sandbox:policy` contribution in the current runtime-context snapshot when at least one enforcing family is registered. The examples below show all three families; absent families are omitted. Tool plugins retain operation and escalation guidance, approval policy contributes separately to the same snapshot, and plan guidance remains `dsh-plan-mode`'s system section.
##### Read-only
```markdown
Current DSH file policy: read-only. The write and edit tools, one-shot bash commands, and terminal sessions cannot modify files under this policy.
Current DSH file policy: read-only. The write and edit tools, one-shot bash commands, and terminal sessions cannot modify files in the standing mode. For the write and edit tools and one-shot bash commands, do not refuse a required modification from this standing mode alone: attempt it normally and follow the tool's denial and escalation guidance.
```
##### Workspace-write
@@ -57,14 +58,14 @@ Current DSH file policy: danger-full-access. The DSH file sandbox does not restr
#### Token effect
One concise system section per request. `workspace-write` carries only the canonical session workspace path; platform-specific temporary paths are summarized without adding host-dependent bytes.
One concise durable context message on the first request and each effective policy change; unchanged requests add nothing. `workspace-write` carries only the canonical session workspace path; platform-specific temporary paths are summarized without adding host-dependent bytes.
#### KV Cache effect
The request prefix is byte-stable while the session mode and immutable workspace root stay unchanged. A mode switch changes the section on the next request; the resulting `request/header` records the new prefix.
The stable system prompt remains byte-identical across mode changes. A changed full context snapshot is appended after retained history, preserving the prior cached prefix; subsequent unchanged requests reuse that retained snapshot.
## Known Limitations and Deferred Work
- **One primary workspace root per session** — policy resolves `SessionHeader.cwd`; extra writable roots are not part of `SandboxExecutionPolicy`.
- **File-effect modes only** — `SandboxMode` governs file effects; network and process policy are outside its vocabulary, so no knob here restricts them.
- **Temporary areas are deliberately summarized** — enforcing backends grant different platform temporary areas, which are selected after policy resolution and therefore cannot be enumerated truthfully in the standing section.
- **Temporary areas are deliberately summarized** — enforcing backends grant different platform temporary areas, which are selected after policy resolution and therefore cannot be enumerated truthfully in the current context.

View File

@@ -18,16 +18,17 @@
- `ctx.sandboxPolicy.resolve({ session?, mode? })`:解析一项完整的逐调用策略。显式批准的模式优先于会话最后一条 `sandbox/mode` 事件,后者又优先于 `defaultMode`;会话不可变的 `cwd` 会先按文件系统语义规范化,再成为 `workspaceRoot`,否则使用配置的回退值。规范化先于词法归一化,因此 `symlink/..` 与进程工作目录解析保持一致。
- `ctx.sandboxPolicy.defaultMode`/`ctx.sandboxPolicy.workspaceRoot`:`resolve()` 使用的部署默认值与回退根目录。
- `ctx.sandboxPolicy.registerEnforcedFamily(family)`:独立注册 `filesystem`、`bash` 或 `terminal`,并返回对应的精确 effect disposer。相同家族仍是彼此独立的贡献;该段落使用规范的家族顺序,并且只有最后一项贡献离开后才移除对应家族。
- `sandbox:policy`:由 `resolve({ session })` 和当前家族贡献派生的请求时系统提示词段落。没有强制执行家族时为空,只说明模式、受影响的面向模型操作,以及 `workspace-write` 下规范化的会话工作区。
- `ctx.sandboxPolicy.registerEscalatableFamily(family)`:独立注册实际工具 schema 与执行路径可提供经批准的更宽松模式重试的家族。反预防性拒绝引导只会列出既受强制执行又可升权的家族;各项贡献独立释放。
- `sandbox:policy`:由 `resolve({ session })` 和当前家族贡献派生的请求时缓存安全上下文贡献。没有强制执行家族时为空,只说明模式、受影响的面向模型操作,以及 `workspace-write` 下规范化的会话工作区。
- `effectiveSandboxMode(events)`:会话 `sandbox/mode` 事件的纯 fold(最后一次切换胜出,没有则为 `undefined`),在 `resolve()` 内使用。
- `setSandboxMode(session, mode)`:逐会话覆盖的唯一写入路径:恰好追加一条 `sandbox/mode` 事件。切换本身就是事件;不会在带外修改模式。
- `SANDBOX_MODES`:所有模式,用于选项展示与运行时验证。
可选的 `./invariant` 配套组件会拒绝伪造的持久 `sandbox/mode` 事件,只要其值不在该封闭词汇中;Session 与其配套组件负责相关存储与核心执行封闭规则。渲染后的段落记录在 `request/header` 中,因此无需另一条事件或内存中的「上次告知」镜像,也能重建确切的有效策略。
可选的 `./invariant` 配套组件会拒绝伪造的持久 `sandbox/mode` 事件,只要其值不在该封闭词汇中;Session 与其配套组件负责相关存储与核心执行封闭规则。agent loop(智能体循环)会将组装后的完整运行时上下文快照记录为一条带来源的 `user/message`,因此无需内存中的「上次告知」镜像,也能重建确切的策略输入。
## 逐会话存储
运行时切换是在对应会话日志中追加的一条 `sandbox/mode` 事件。`effective = explicit grant ?? fold(events) ?? deployment default`,因此覆盖会通过回放跨重启保留,两个会话也绝不会看到彼此状态。工作区标识无需另一条事件:创建时记录的不可变 `SessionHeader.cwd` 是该会话每次调用使用的根。该事件仍只进入日志;下一次请求会在任何工具调用发生前,根据 fold 组装当前段落。
运行时切换是在对应会话日志中追加的一条 `sandbox/mode` 事件。`effective = explicit grant ?? fold(events) ?? deployment default`,因此覆盖会通过回放跨重启保留,两个会话也绝不会看到彼此状态。工作区标识无需另一条事件:创建时记录的不可变 `SessionHeader.cwd` 是该会话每次调用使用的根。该事件仍只进入日志;在下一次请求前,归属方会将当前事实贡献给完整运行时上下文快照。
## 模型体验
@@ -35,12 +36,12 @@
#### 模型看到的内容
只要至少注册了一个强制执行家族,每次 agent 请求就会有一个 `sandbox:policy` 系统段落。以下示例展示全部三个家族;缺失的家族会被省略。工具插件继续负责操作与升级引导,批准策略仍由 `dsh-user-approval` 的段落管理,计划引导仍由 `dsh-plan-mode` 的段落管理。
只要至少注册了一个强制执行家族,当前运行时上下文快照中就会有一项 `sandbox:policy` 贡献。以下示例展示全部三个家族;缺失的家族会被省略。工具插件继续负责操作与升级引导,批准策略单独贡献给同一份快照,计划引导仍由 `dsh-plan-mode` 的系统段落管理。
##### 只读
```markdown
Current DSH file policy: read-only. The write and edit tools, one-shot bash commands, and terminal sessions cannot modify files under this policy.
Current DSH file policy: read-only. The write and edit tools, one-shot bash commands, and terminal sessions cannot modify files in the standing mode. For the write and edit tools and one-shot bash commands, do not refuse a required modification from this standing mode alone: attempt it normally and follow the tool's denial and escalation guidance.
```
##### 工作区写入
@@ -57,14 +58,14 @@ Current DSH file policy: danger-full-access. The DSH file sandbox does not restr
#### Token 影响
每个请求增加一个简洁的系统段落。`workspace-write` 只携带规范化的会话工作区路径;平台特定的临时路径会以摘要表述,不会加入依赖主机的字节。
首次请求和有效策略每次变化时增加一条简洁的持久上下文消息;未变化的请求不增加内容。`workspace-write` 只携带规范化的会话工作区路径;平台特定的临时路径会以摘要表述,不会加入依赖主机的字节。
#### KV Cache 影响
只要会话模式与不可变工作区根目录不变,请求前缀就在字节层面保持稳定。模式切换会在下一次请求中改变该段落;生成的 `request/header` 会记录新的前缀。
模式切换时,稳定的系统提示词仍逐字节相同。变化后的完整上下文快照会追加到保留的历史之后,从而保留此前已缓存的前缀;后续未变化的请求会复用该保留快照。
## 已知限制与暂缓事项
- **每个会话只有一个主要工作区根目录**:策略解析 `SessionHeader.cwd`;额外可写根目录不属于 `SandboxExecutionPolicy`。
- **仅限文件操作模式**:`SandboxMode` 管控文件操作;网络和进程策略不在其词汇中,因此这里没有限制它们的旋钮。
- **有意概述临时区域**:强制执行后端会授予不同的平台临时区域,这些区域在策略解析后才会选定,因此无法在常驻段落中如实枚举。
- **有意概述临时区域**:强制执行后端会授予不同的平台临时区域,这些区域在策略解析后才会选定,因此无法在当前上下文中如实枚举。

View File

@@ -3,16 +3,19 @@
* 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 renders the resolved policy as the
* `sandbox:policy` system section; request headers therefore reconstruct the
* same mode and roots the enforcing consumers resolve.
* 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 and register their independently disposable model-facing
* families. The request section therefore describes only operations this
* runtime actually fences, while each backend retains its own enforcement
* dialect. The service reads session state once at each operation boundary;
* executors and providers remain session-free.
* families. Tool owners separately register families whose schemas expose an
* approved wider retry. The context therefore describes only operations this
* runtime actually fences and adds anti-refusal guidance only where escalation
* exists, while each backend retains its own enforcement dialect. The service
* reads session state once at each operation boundary; executors and providers
* remain session-free.
*
* @module @deepseek-ai/dsh-sandbox-policy
*/
@@ -54,12 +57,19 @@ function familyList(families: readonly FilePolicyFamily[], conjunction: 'and' |
}
/** Render only policy facts shared by every backend enforcing each registered family. */
function renderPolicyContext(policy: SandboxExecutionPolicy, families: readonly FilePolicyFamily[]): string {
function renderPolicyContext(
policy: SandboxExecutionPolicy,
families: readonly FilePolicyFamily[],
escalatableFamilies: readonly FilePolicyFamily[],
): string {
if (families.length === 0) return ''
switch (policy.mode) {
case 'read-only': {
const subjects = familyList(families, 'and')
return `Current DSH file policy: read-only. ${subjects[0]?.toUpperCase()}${subjects.slice(1)} cannot modify files under this policy.`
const standing = `Current DSH file policy: read-only. ${subjects[0]?.toUpperCase()}${subjects.slice(1)} cannot modify files in the standing mode.`
if (escalatableFamilies.length === 0) return standing
const escalatable = familyList(escalatableFamilies, 'and')
return `${standing} For ${escalatable}, do not refuse a required modification from this standing mode alone: attempt it normally and follow the tool's denial and escalation guidance.`
}
case 'workspace-write': {
const subjects = familyList(families, 'and')
@@ -128,6 +138,8 @@ export class SandboxPolicyService extends Service {
readonly workspaceRoot: string
/** Independently disposable enforcement-family contributions. */
private readonly enforcedFamilies = new Map<FilePolicyFamily, Set<symbol>>()
/** Independently disposable tool families that expose an approved wider retry. */
private readonly escalatableFamilies = new Map<FilePolicyFamily, Set<symbol>>()
constructor(ctx: Context, config: Config) {
super(ctx, 'sandboxPolicy')
@@ -138,12 +150,14 @@ export class SandboxPolicyService extends Service {
this.workspaceRoot = resolveWorkspaceRoot(config.workspaceRoot ?? process.cwd())
ctx.inject(['systemPrompt'], (scope: Context) => {
scope.systemPrompt.section({
scope.systemPrompt.context({
name: 'sandbox:policy',
order: 110,
text: (context) => {
const session = context.agent?.session
return session === undefined ? '' : renderPolicyContext(this.resolve({ session }), this.activeFamilies())
return session === undefined
? ''
: renderPolicyContext(this.resolve({ session }), this.activeFamilies(), this.activeEscalatableFamilies())
},
})
})
@@ -152,26 +166,47 @@ export class SandboxPolicyService extends Service {
/**
* Register one runtime contribution that enforces the shared file policy for
* a model-facing operation family. Equal families remain independently
* disposable; registration and removal invalidate assembled prompt caches
* disposable; registration and removal invalidate request-input assemblies
* when a system-prompt service is active.
* @param family - operation family whose file effects this contribution enforces.
* @returns the exact Cordis effect disposer for this contribution.
*/
registerEnforcedFamily(family: 'filesystem' | 'bash' | 'terminal'): () => void {
return this.registerFamily(this.enforcedFamilies, family, 'sandboxPolicy.registerEnforcedFamily()')
}
/**
* Register one model-facing family whose tool schema and execution path offer
* an approved wider retry after a real denial. Equal contributions remain
* independently disposable; a family is narrated as escalatable only while
* it is also enforced.
* @param family - operation family whose tools expose escalation.
* @returns the exact Cordis effect disposer for this contribution.
*/
registerEscalatableFamily(family: 'filesystem' | 'bash' | 'terminal'): () => void {
return this.registerFamily(this.escalatableFamilies, family, 'sandboxPolicy.registerEscalatableFamily()')
}
/** Register one independently disposable family contribution in an owned map. */
private registerFamily(
registry: Map<FilePolicyFamily, Set<symbol>>,
family: FilePolicyFamily,
label: string,
): () => void {
const token = Symbol(family)
const dispose = this.ctx.effect(() => {
const contributions = this.enforcedFamilies.get(family) ?? new Set<symbol>()
const contributions = registry.get(family) ?? new Set<symbol>()
contributions.add(token)
this.enforcedFamilies.set(family, contributions)
registry.set(family, contributions)
this.emitPromptChange()
return () => {
contributions.delete(token)
if (contributions.size === 0 && this.enforcedFamilies.get(family) === contributions) {
this.enforcedFamilies.delete(family)
if (contributions.size === 0 && registry.get(family) === contributions) {
registry.delete(family)
}
this.emitPromptChange()
}
}, 'sandboxPolicy.registerEnforcedFamily()')
}, label)
return () => void dispose()
}
@@ -206,6 +241,11 @@ export class SandboxPolicyService extends Service {
return FILE_POLICY_FAMILIES.filter(family => (this.enforcedFamilies.get(family)?.size ?? 0) > 0)
}
/** Escalatable families that are also currently enforced, in canonical order. */
private activeEscalatableFamilies(): FilePolicyFamily[] {
return this.activeFamilies().filter(family => (this.escalatableFamilies.get(family)?.size ?? 0) > 0)
}
/** Notify prompt consumers only after their registry exists. */
private emitPromptChange(): void {
if (this.ctx.get('systemPrompt') !== undefined) this.ctx.emit('system-prompt/change')

View File

@@ -12,7 +12,7 @@ import { Context } from 'cordis'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { Session, SessionId } from '@deepseek-ai/dsh-session'
import SandboxPolicyService, { SANDBOX_MODES, effectiveSandboxMode, setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy'
import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
import SystemPrompt, { renderContextSnapshot, renderPrompt } from '@deepseek-ai/dsh-system-prompt'
async function mounted(config: { mode?: 'read-only' | 'workspace-write' | 'danger-full-access'; workspaceRoot?: string } = {}) {
const ctx = new Context()
@@ -34,9 +34,9 @@ function agentFor(activeSession: Session): Agent {
return { session: activeSession } as unknown as Agent
}
async function policySection(ctx: Context, activeSession: Session): Promise<string | undefined> {
async function policyContext(ctx: Context, activeSession: Session): Promise<string | undefined> {
return (await ctx.systemPrompt.assemble({ agent: agentFor(activeSession) }))
.sections.find(section => section.name === 'sandbox:policy')?.text
.contexts.find(context => context.name === 'sandbox:policy')?.text
}
describe('SandboxPolicyService', () => {
@@ -44,6 +44,8 @@ describe('SandboxPolicyService', () => {
const ctx = await mounted()
expect(ctx.sandboxPolicy.defaultMode).toBe('read-only')
expect(ctx.sandboxPolicy.workspaceRoot).toBe(resolve(process.cwd()))
const dispose = ctx.sandboxPolicy.registerEscalatableFamily('bash')
expect(() =>{ dispose() }).not.toThrow()
})
it('carries a configured mode and resolves the workspace root absolute', async () => {
@@ -131,10 +133,10 @@ describe('SandboxPolicyService', () => {
const fiber = await ctx.plugin(SandboxPolicyService, {})
ctx.sandboxPolicy.registerEnforcedFamily('filesystem')
expect(ctx.sandboxPolicy).toBeDefined()
expect(await policySection(ctx, session('sess-hmr'))).toContain('read-only')
expect(await policyContext(ctx, session('sess-hmr'))).toContain('read-only')
await fiber.dispose()
expect(ctx.get('sandboxPolicy')).toBeUndefined()
expect((await ctx.systemPrompt.assemble()).sections.find(section => section.name === 'sandbox:policy')).toBeUndefined()
expect((await ctx.systemPrompt.assemble()).contexts.find(context => context.name === 'sandbox:policy')).toBeUndefined()
})
})
@@ -148,21 +150,21 @@ describe('sandbox:policy request context', () => {
it('omits policy prose when no enforcing family is registered', async () => {
const ctx = await promptMounted()
expect(await policySection(ctx, session('sess-no-family'))).toBe('')
expect(await policyContext(ctx, session('sess-no-family'))).toBe('')
})
it.each([
[['filesystem'], 'Current DSH file policy: read-only. The write and edit tools cannot modify files under this policy.'],
[['bash'], 'Current DSH file policy: read-only. One-shot bash commands cannot modify files under this policy.'],
[['terminal'], 'Current DSH file policy: read-only. Terminal sessions cannot modify files under this policy.'],
[['filesystem', 'bash'], 'Current DSH file policy: read-only. The write and edit tools and one-shot bash commands cannot modify files under this policy.'],
[['filesystem', 'terminal'], 'Current DSH file policy: read-only. The write and edit tools and terminal sessions cannot modify files under this policy.'],
[['bash', 'terminal'], 'Current DSH file policy: read-only. One-shot bash commands and terminal sessions cannot modify files under this policy.'],
[['filesystem', 'bash', 'terminal'], 'Current DSH file policy: read-only. The write and edit tools, one-shot bash commands, and terminal sessions cannot modify files under this policy.'],
[['filesystem'], 'Current DSH file policy: read-only. The write and edit tools cannot modify files in the standing mode.'],
[['bash'], 'Current DSH file policy: read-only. One-shot bash commands cannot modify files in the standing mode.'],
[['terminal'], 'Current DSH file policy: read-only. Terminal sessions cannot modify files in the standing mode.'],
[['filesystem', 'bash'], 'Current DSH file policy: read-only. The write and edit tools and one-shot bash commands cannot modify files in the standing mode.'],
[['filesystem', 'terminal'], 'Current DSH file policy: read-only. The write and edit tools and terminal sessions cannot modify files in the standing mode.'],
[['bash', 'terminal'], 'Current DSH file policy: read-only. One-shot bash commands and terminal sessions cannot modify files in the standing mode.'],
[['filesystem', 'bash', 'terminal'], 'Current DSH file policy: read-only. The write and edit tools, one-shot bash commands, and terminal sessions cannot modify files in the standing mode.'],
] as const)('states read-only consequences for %j', async (families, expected) => {
const ctx = await promptMounted()
for (const family of [...families].reverse()) ctx.sandboxPolicy.registerEnforcedFamily(family)
expect(await policySection(ctx, session(`sess-read-only-${families.join('-')}`))).toBe(expected)
expect(await policyContext(ctx, session(`sess-read-only-${families.join('-')}`))).toBe(expected)
})
it('states the portable workspace guarantee without enumerating host temp paths', async () => {
@@ -171,14 +173,33 @@ describe('sandbox:policy request context', () => {
ctx.sandboxPolicy.registerEnforcedFamily('bash')
ctx.sandboxPolicy.registerEnforcedFamily('terminal')
const active = session('sess-workspace-write', '/projects/../projects/current')
expect(await policySection(ctx, active)).toBe('Current DSH file policy: workspace-write. The write and edit tools, one-shot bash commands, and terminal sessions may modify files under the session workspace: "/projects/current". Some platform temporary areas may also be writable.')
expect(await policyContext(ctx, active)).toBe('Current DSH file policy: workspace-write. The write and edit tools, one-shot bash commands, and terminal sessions may modify files under the session workspace: "/projects/current". Some platform temporary areas may also be writable.')
})
it('adds anti-refusal guidance only for enforced families with a real escalation path', async () => {
const ctx = await promptMounted()
ctx.sandboxPolicy.registerEnforcedFamily('filesystem')
ctx.sandboxPolicy.registerEnforcedFamily('bash')
ctx.sandboxPolicy.registerEnforcedFamily('terminal')
ctx.sandboxPolicy.registerEscalatableFamily('filesystem')
const disposeBash = ctx.sandboxPolicy.registerEscalatableFamily('bash')
ctx.sandboxPolicy.registerEscalatableFamily('terminal')
const isolated = await promptMounted()
isolated.sandboxPolicy.registerEnforcedFamily('filesystem')
isolated.sandboxPolicy.registerEscalatableFamily('terminal')
expect(await policyContext(isolated, session('sess-unenforced-escalation'))).not.toContain('do not refuse')
const active = session('sess-escalatable-families')
expect(await policyContext(ctx, active)).toContain('For the write and edit tools, one-shot bash commands, and terminal sessions, do not refuse')
disposeBash()
expect(await policyContext(ctx, active)).toContain('For the write and edit tools and terminal sessions, do not refuse')
})
it('states the exact families bypassed by danger-full-access', async () => {
const ctx = await promptMounted({ mode: 'danger-full-access' })
ctx.sandboxPolicy.registerEnforcedFamily('filesystem')
ctx.sandboxPolicy.registerEnforcedFamily('terminal')
expect(await policySection(ctx, session('sess-danger', '/projects/current'))).toBe('Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict the write and edit tools or terminal sessions.')
expect(await policyContext(ctx, session('sess-danger', '/projects/current'))).toBe('Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict the write and edit tools or terminal sessions.')
})
it('renders family contributions independently across mount and repeated disposal', async () => {
@@ -187,7 +208,7 @@ describe('sandbox:policy request context', () => {
const filesystemFiber = await ctx.plugin(Object.assign((inner: Context) => {
inner.sandboxPolicy.registerEnforcedFamily('filesystem')
}, { inject: ['sandboxPolicy'] }))
expect(await policySection(ctx, active)).toContain('The write and edit tools cannot modify files')
expect(await policyContext(ctx, active)).toContain('The write and edit tools cannot modify files')
let disposeBashFirst!: () => void
const bashFirstFiber = await ctx.plugin(Object.assign((inner: Context) => {
@@ -196,15 +217,15 @@ describe('sandbox:policy request context', () => {
const bashSecondFiber = await ctx.plugin(Object.assign((inner: Context) => {
inner.sandboxPolicy.registerEnforcedFamily('bash')
}, { inject: ['sandboxPolicy'] }))
expect(await policySection(ctx, active)).toContain('The write and edit tools and one-shot bash commands')
expect(await policyContext(ctx, active)).toContain('The write and edit tools and one-shot bash commands')
disposeBashFirst()
disposeBashFirst()
expect(await policySection(ctx, active)).toContain('The write and edit tools and one-shot bash commands')
expect(await policyContext(ctx, active)).toContain('The write and edit tools and one-shot bash commands')
await bashSecondFiber.dispose()
expect(await policySection(ctx, active)).toContain('The write and edit tools cannot modify files')
expect(await policyContext(ctx, active)).toContain('The write and edit tools cannot modify files')
await bashFirstFiber.dispose()
await filesystemFiber.dispose()
expect(await policySection(ctx, active)).toBe('')
expect(await policyContext(ctx, active)).toBe('')
})
it('keeps the complete rendered prompt byte-stable across TMPDIR changes', async () => {
@@ -214,11 +235,14 @@ describe('sandbox:policy request context', () => {
const previous = process.env.TMPDIR
try {
process.env.TMPDIR = '/tmp/first-host-temp'
const first = renderPrompt(await ctx.systemPrompt.assemble({ agent: agentFor(active) }))
const firstAssembly = await ctx.systemPrompt.assemble({ agent: agentFor(active) })
const firstPrompt = renderPrompt(firstAssembly)
const firstContext = renderContextSnapshot(firstAssembly)
process.env.TMPDIR = '/tmp/second-host-temp'
const second = renderPrompt(await ctx.systemPrompt.assemble({ agent: agentFor(active) }))
expect(second).toBe(first)
expect(second).not.toContain('host-temp')
const secondAssembly = await ctx.systemPrompt.assemble({ agent: agentFor(active) })
expect(renderPrompt(secondAssembly)).toBe(firstPrompt)
expect(renderContextSnapshot(secondAssembly)).toBe(firstContext)
expect(firstContext).not.toContain('host-temp')
} finally {
if (previous === undefined) delete process.env.TMPDIR
else process.env.TMPDIR = previous
@@ -229,16 +253,16 @@ describe('sandbox:policy request context', () => {
const ctx = await promptMounted()
ctx.sandboxPolicy.registerEnforcedFamily('filesystem')
const active = session('sess-switch', '/projects/current')
const first = await policySection(ctx, active)
expect(await policySection(ctx, active)).toBe(first)
const first = await policyContext(ctx, active)
expect(await policyContext(ctx, active)).toBe(first)
setSandboxMode(active, 'danger-full-access')
const danger = await policySection(ctx, active)
const danger = await policyContext(ctx, active)
expect(danger).toContain('does not restrict the write and edit tools')
expect(await policySection(ctx, active)).toBe(danger)
expect(await policyContext(ctx, active)).toBe(danger)
setSandboxMode(active, 'workspace-write')
expect(await policySection(ctx, active)).toContain(JSON.stringify(resolve('/projects/current')))
expect(await policyContext(ctx, active)).toContain(JSON.stringify(resolve('/projects/current')))
})
it('reconstructs resumed policy from the session log and omits diagnostics without an agent', async () => {
@@ -248,8 +272,8 @@ describe('sandbox:policy request context', () => {
const ctx = await promptMounted({ mode: 'read-only' })
ctx.sandboxPolicy.registerEnforcedFamily('filesystem')
expect(await policySection(ctx, resumed)).toContain('workspace-write')
expect((await ctx.systemPrompt.assemble()).sections.find(section => section.name === 'sandbox:policy')?.text).toBe('')
expect(await policyContext(ctx, resumed)).toContain('workspace-write')
expect((await ctx.systemPrompt.assemble()).contexts.find(context => context.name === 'sandbox:policy')?.text).toBe('')
})
})