feat(system-prompt): cache dynamic policy context
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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` 管控文件操作;网络和进程策略不在其词汇中,因此这里没有限制它们的旋钮。
|
||||
- **有意概述临时区域**:强制执行后端会授予不同的平台临时区域,这些区域在策略解析后才会选定,因此无法在常驻段落中如实枚举。
|
||||
- **有意概述临时区域**:强制执行后端会授予不同的平台临时区域,这些区域在策略解析后才会选定,因此无法在当前上下文中如实枚举。
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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('')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user