Merge origin/master into worktree/web-multimodal-image-input

# Conflicts:
#	docs/architecture.i18n.yaml
#	docs/architecture.md
#	docs/architecture.zh.md
#	docs/cordis-catalog/events.md
#	docs/cordis-catalog/services.md
#	docs/core-data-structures/llm-streaming.i18n.yaml
#	docs/event-producer-consumer.md
#	docs/module-graph.md
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
#	packages/client/connection/src/client/fixture.ts
#	packages/client/connection/tests/node-half.spec.ts
#	packages/client/ui-conversation/README.i18n.yaml
#	packages/client/ui-conversation/README.md
#	packages/client/ui-conversation/README.zh.md
#	packages/compact/compact-basic/README.i18n.yaml
#	packages/llm/llm/README.i18n.yaml
#	scripts/test-invariants.ts
This commit is contained in:
creatixchu
2026-07-31 17:39:06 +08:00
400 changed files with 79356 additions and 4154 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: dca54330bc888af9ecac21aa92019d8a2b0140bd
README.zh.md: a201d48c81f563fc3d85495e964bb67432517a3c
README.md: b2512790c5cf5b3a06523cf91b50dd5d288b1522
README.zh.md: dda98b7aa4775ac2453704295df87c3c92025093

View File

@@ -2,11 +2,11 @@
English | [中文](README.zh.md)
The single owner of sandbox-policy resolution: the deployment's default [`SandboxMode`](../sandbox/README.md) and fallback root, plus each session's durable mode override and immutable workspace root. Every enforcing capability family receives one resolved mode-and-root policy per call.
The single owner of sandbox-policy resolution: the deployment's default [`SandboxMode`](../sandbox/README.md) and fallback root, plus each session's durable mode override and immutable workspace root. Every enforcing capability receives one resolved mode-and-root policy per call; before each request, the model receives the current policy without a separate capability inventory.
## Why a shared home
Two families enforce the same mode vocabulary: the sandboxed bash executor (`@deepseek-ai/dsh-bash-sandbox`) and the sandboxed filesystem provider (`@deepseek-ai/dsh-fs-sandbox`). If each resolved its own `mode` + `workspaceRoot`, the two could drift into a split world — bash confined to one root while fs fences another, exactly what [the sandbox RFC](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md) warns against. Both tool layers resolve policy through `ctx.sandboxPolicy`, and both enforcing backends consume that complete per-call result. The [cross-family fs sandbox RFC](../../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md) records the shared-policy decision.
Filesystem tools, one-shot bash commands, and terminal sessions may enforce the same mode vocabulary in different combinations. If each resolved its own `mode` + `workspaceRoot`, they could drift into a split world, exactly what [the sandbox Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md) warns against. Each enforcing backend consumes the complete owner-resolved policy, while the current context describes only what that policy means for any available operation the DSH file sandbox enforces. The [cross-family fs sandbox Agent Note](../../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md) records the shared-policy decision.
## Config
@@ -17,25 +17,53 @@ Two families enforce the same mode vocabulary: the sandboxed bash executor (`@de
- `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()`.
- `sandbox:policy` — a request-time cache-safe context contribution derived directly from `resolve({ session })`. It states the mode's capability-neutral file-effect contract and the canonical session workspace under `workspace-write`; tool owners retain operation-specific denial and escalation guidance.
- `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 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 is log-only (the `approval/*` precedent): the model learns the mode from the enforcing tools' denial markers, never from the event.
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
Indirectly, through `dsh-tool-bash` and `dsh-tool-fs`, which render the effective mode this service holds in their `[sandbox: …]` denial markers and escalation prompts; the `sandbox/mode` event itself never reaches the model.
### Current file sandbox policy
#### What the model sees
One `sandbox:policy` contribution in the current runtime-context snapshot for every agent session. It does not enumerate mounted capabilities. 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. 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.
```
##### Workspace-write
```markdown
Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: "<workspace root>". Some platform temporary areas may also be writable.
```
##### Danger-full-access
```markdown
Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.
```
#### Token effect
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
No direct invalidation; the named consumers own any request-prefix changes, and the mode is deliberately absent from the prompt.
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 current context.

View File

@@ -2,11 +2,11 @@
[English](README.md) | 中文
沙箱策略解析的唯一归属位置:部署默认 [`SandboxMode`](../sandbox/README.md) 与回退根目录,加上每个会话的持久模式覆盖和不可变工作区根目录。每个强制执行策略的能力家族在每次调用时都会收到一项解析完成的模式与根目录策略。
沙箱策略解析的唯一归属位置:部署默认 [`SandboxMode`](../sandbox/README.md) 与回退根目录,加上每个会话的持久模式覆盖和不可变工作区根目录。每项负责强制执行的能力在每次调用时都会收到一项解析完成的模式与根目录策略;模型在每次请求前会收到当前策略,而不会另收一份能力清单。
## 为何需要共享归属位置
两个家族强制执行同一套模式词汇:沙箱化 bash 执行器(`@deepseek-ai/dsh-bash-sandbox`)与沙箱化文件系统提供方(`@deepseek-ai/dsh-fs-sandbox`)。如果两者各自解析 `mode` + `workspaceRoot`,就可能漂移成分裂世界:bash 限制在一个根目录,fs 却隔离另一个根目录,正是[沙箱 RFC](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)所警告的情况。两个工具层都通过 `ctx.sandboxPolicy` 解析策略,两个强制执行后端也都消费完整的逐调用结果。[跨家族 fs 沙箱 RFC](../../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md)记录了共享策略决策。
文件系统工具、一次性 bash 命令和终端会话可以用不同组合强制执行同一套模式词汇。如果各自解析 `mode` + `workspaceRoot`,就可能漂移成分裂世界,正是[沙箱 Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)所警告的情况。每个强制执行后端都会消费归属方解析出的完整策略,而当前上下文只说明该策略对于任何受 DSH 文件沙箱强制执行的可用操作有何含义。[跨家族 fs 沙箱 Agent Note](../../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md)记录了共享策略决策。
## 配置
@@ -17,25 +17,53 @@
- `ctx.sandboxPolicy.resolve({ session?, mode? })`:解析一项完整的逐调用策略。显式批准的模式优先于会话最后一条 `sandbox/mode` 事件,后者又优先于 `defaultMode`;会话不可变的 `cwd` 会先按文件系统语义规范化,再成为 `workspaceRoot`,否则使用配置的回退值。规范化先于词法归一化,因此 `symlink/..` 与进程工作目录解析保持一致。
- `ctx.sandboxPolicy.defaultMode`/`ctx.sandboxPolicy.workspaceRoot`:`resolve()` 使用的部署默认值与回退根目录。
- `sandbox:policy`:直接派生自 `resolve({ session })` 的请求时缓存安全上下文贡献。它说明该模式中与具体能力无关的文件效果契约,以及 `workspace-write` 下规范化的会话工作区;工具归属方仍负责操作特定的拒绝与升权引导。
- `effectiveSandboxMode(events)`:会话 `sandbox/mode` 事件的纯 fold(最后一次切换胜出,没有则为 `undefined`),在 `resolve()` 内使用。
- `setSandboxMode(session, mode)`:逐会话覆盖的唯一写入路径:恰好追加一条 `sandbox/mode` 事件。切换本身就是事件;不会在带外修改模式。
- `SANDBOX_MODES`:所有模式,用于选项展示与运行时验证。
可选的 `./invariant` 配套组件会拒绝伪造的持久 `sandbox/mode` 事件,只要其值不在该封闭词汇中;Session 与其配套组件负责相关存储与核心执行封闭规则。
可选的 `./invariant` 配套组件会拒绝伪造的持久 `sandbox/mode` 事件,只要其值不在该封闭词汇中;Session 与其配套组件负责相关存储与核心执行封闭规则。agent loop(智能体循环)会将组装后的完整运行时上下文快照记录为一条带来源的 `user/message`,因此无需内存中的「上次告知」镜像,也能重建确切的策略输入。
## 逐会话存储
运行时切换是在对应会话日志中追加的一条 `sandbox/mode` 事件。`effective = explicit grant ?? fold(events) ?? deployment default`,因此覆盖会通过回放跨重启保留,两个会话也绝不会看到彼此状态。工作区标识无需另一条事件:创建时记录的不可变 `SessionHeader.cwd` 是该会话每次调用使用的根。该事件只进入日志(沿用 `approval/*` 先例):模型通过强制执行工具的拒绝标记获知模式,绝不会从事件获知。
运行时切换是在对应会话日志中追加的一条 `sandbox/mode` 事件。`effective = explicit grant ?? fold(events) ?? deployment default`,因此覆盖会通过回放跨重启保留,两个会话也绝不会看到彼此状态。工作区标识无需另一条事件:创建时记录的不可变 `SessionHeader.cwd` 是该会话每次调用使用的根。该事件仍只进入日志;在下一次请求前,归属方会将当前事实贡献给完整运行时上下文快照。
## 模型体验
通过 `dsh-tool-bash` 和 `dsh-tool-fs` 间接影响;它们会在 `[sandbox: …]` 拒绝标记和升权提示词中渲染该服务持有的有效模式,`sandbox/mode` 事件本身绝不会到达模型。
### 当前文件沙箱策略
#### 模型看到的内容
每个 agent 会话的当前运行时上下文快照中都有一项 `sandbox:policy` 贡献。它不枚举已装载的能力。工具插件继续负责操作与升级引导,批准策略单独贡献给同一份快照,计划引导仍由 `dsh-plan-mode` 的系统段落管理。
##### 只读
```markdown
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.
```
##### 工作区写入
```markdown
Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: "<workspace root>". Some platform temporary areas may also be writable.
```
##### 完全访问
```markdown
Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.
```
#### Token 影响
首次请求和有效策略每次变化时增加一条简洁的持久上下文消息;未变化的请求不增加内容。`workspace-write` 只携带规范化的会话工作区路径;平台特定的临时路径会以摘要表述,不会加入依赖主机的字节。
#### KV Cache 影响
不会直接使 KV Cache 失效;请求前缀变更由上述消费方负责,且提示词有意不包含模式。
模式切换时,稳定的系统提示词仍逐字节相同。变化后的完整上下文快照会追加到保留的历史之后,从而保留此前已缓存的前缀;后续未变化的请求会复用该保留快照。
## 已知限制与暂缓事项
- **每个会话只有一个主要工作区根目录**:策略解析 `SessionHeader.cwd`;额外可写根目录不属于 `SandboxExecutionPolicy`。
- **仅限文件操作模式**:`SandboxMode` 管控文件操作;网络和进程策略不在其词汇中,因此这里没有限制它们的旋钮。
- **有意概述临时区域**:强制执行后端会授予不同的平台临时区域,这些区域在策略解析后才会选定,因此无法在当前上下文中如实枚举。

View File

@@ -1,6 +1,6 @@
{
"name": "@deepseek-ai/dsh-sandbox-policy",
"description": "Per-call sandbox policy resolver (ctx.sandboxPolicy): deployment fallbacks plus each session's mode and workspace root, shared by every enforcing capability family",
"description": "Per-call sandbox policy resolver and current model context: deployment fallbacks plus each session's mode and workspace root, shared by every enforcing capability family",
"version": "0.0.1",
"private": true,
"type": "module",
@@ -27,18 +27,22 @@
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-agent": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-sandbox": "^0.0.1",
"@deepseek-ai/dsh-session": "^0.0.1",
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
"cordis": "^4.0.0-rc.7"
},
"dependencies": {
"schemastery": "^3.18.0"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-sandbox": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"cordis": "^4.0.0-rc.7"
}
}

View File

@@ -3,13 +3,17 @@
* 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.
*
* Both enforcing capability families read the SAME policy here: the sandboxed
* bash executor (`@deepseek-ai/dsh-bash-sandbox`) and the sandboxed filesystem
* provider (`@deepseek-ai/dsh-fs-sandbox`) consume the SAME resolved per-call
* policy, so bash and fs can never confine to different roots — the split
* world the sandbox RFC warns about. The service reads session state once at
* the tool boundary; executors and providers remain session-free.
* 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
*/
@@ -17,8 +21,10 @@
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'
@@ -28,6 +34,23 @@ 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
@@ -61,9 +84,9 @@ export interface SandboxPolicyRequest {
/**
* The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment
* default mode and fallback workspace root. Tool layers call {@link resolve}
* for each execution so a session's mode log and immutable cwd travel together
* to every enforcing capability.
* 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.
@@ -78,7 +101,6 @@ export class SandboxPolicyService extends Service {
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
@@ -86,6 +108,19 @@ export class SandboxPolicyService extends Service {
// 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 }))
},
})
})
}
/**

View File

@@ -5,11 +5,11 @@
* `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 model learns the mode from the boundary
* markers in the enforcing tools, never from the event itself. EXECUTION
* honors the 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.
* `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

View File

@@ -1,7 +1,7 @@
/**
* Tests for the sandbox-policy home: the deployment default (mode +
* workspaceRoot) the service exposes, and the per-session `sandbox/mode`
* override kit (fold + write path) both enforcing families read.
* override kit (fold + write path) every enforcing capability reads.
*/
import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync } from 'node:fs'
@@ -9,8 +9,10 @@ import { tmpdir } from 'node:os'
import { join, resolve, sep } from 'node:path'
import { describe, expect, it } from 'vitest'
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, { 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()
@@ -28,6 +30,15 @@ function session(id: string, cwd?: string): Session {
})
}
function agentFor(activeSession: Session): Agent {
return { session: activeSession } as unknown as Agent
}
async function policyContext(ctx: Context, activeSession: Session): Promise<string | undefined> {
return (await ctx.systemPrompt.assemble({ agent: agentFor(activeSession) }))
.contexts.find(context => context.name === 'sandbox:policy')?.text
}
describe('SandboxPolicyService', () => {
it('defaults to read-only under the process cwd', async () => {
const ctx = await mounted()
@@ -114,12 +125,81 @@ describe('SandboxPolicyService', () => {
await expect(ctx.plugin(SandboxPolicyService, { mode: 'yolo' as never })).rejects.toThrow()
})
it('unregisters cleanly from a child fiber (HMR safety)', async () => {
it('disposes the service and context contribution from a child fiber (HMR safety)', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
const fiber = await ctx.plugin(SandboxPolicyService, {})
expect(ctx.sandboxPolicy).toBeDefined()
expect(await policyContext(ctx, session('sess-hmr'))).toContain('read-only')
await fiber.dispose()
expect(ctx.get('sandboxPolicy')).toBeUndefined()
expect((await ctx.systemPrompt.assemble()).contexts.find(context => context.name === 'sandbox:policy')).toBeUndefined()
})
})
describe('sandbox:policy request context', () => {
async function promptMounted(config: { mode?: 'read-only' | 'workspace-write' | 'danger-full-access'; workspaceRoot?: string } = {}): Promise<Context> {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(SandboxPolicyService, config)
return ctx
}
it.each(['read-only', 'workspace-write', 'danger-full-access'] as const)('renders the exact %s policy without a capability inventory', async (mode) => {
const ctx = await promptMounted({ mode, workspaceRoot: '/fallback' })
const workspaceRoot = resolve('/projects/current')
const expected = {
'read-only': '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.',
'workspace-write': `Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: ${JSON.stringify(workspaceRoot)}. Some platform temporary areas may also be writable.`,
'danger-full-access': 'Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.',
} as const
expect(await policyContext(ctx, session(`sess-${mode}`, '/projects/../projects/current'))).toBe(expected[mode])
})
it('keeps the complete rendered prompt byte-stable across TMPDIR changes', async () => {
const ctx = await promptMounted({ mode: 'workspace-write' })
const active = session('sess-tmpdir-stability', '/projects/current')
const previous = process.env.TMPDIR
try {
process.env.TMPDIR = '/tmp/first-host-temp'
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 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
}
})
it('reflects the latest durable switch on the next assembly and stays byte-stable otherwise', async () => {
const ctx = await promptMounted()
const active = session('sess-switch', '/projects/current')
const first = await policyContext(ctx, active)
expect(await policyContext(ctx, active)).toBe(first)
setSandboxMode(active, 'danger-full-access')
const danger = await policyContext(ctx, active)
expect(danger).toBe('Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.')
expect(await policyContext(ctx, active)).toBe(danger)
setSandboxMode(active, 'workspace-write')
expect(await policyContext(ctx, active)).toBe(`Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: ${JSON.stringify(resolve('/projects/current'))}. Some platform temporary areas may also be writable.`)
})
it('reconstructs resumed policy from the session log and omits diagnostics without an agent', async () => {
const active = session('sess-resume', '/projects/current')
setSandboxMode(active, 'workspace-write')
const resumed = new Session(active.id, active.events, active.header)
const ctx = await promptMounted({ mode: 'read-only' })
expect(await policyContext(ctx, resumed)).toContain('workspace-write')
expect((await ctx.systemPrompt.assemble()).contexts.find(context => context.name === 'sandbox:policy')?.text).toBe('')
})
})

View File

@@ -17,12 +17,18 @@
{
"path": "../../../vendor/schemastery"
},
{
"path": "../../core/agent"
},
{
"path": "../sandbox"
},
{
"path": "../../core/session"
},
{
"path": "../../core/system-prompt"
},
{
"path": "../../support/invariants"
}