refactor(sandbox-policy): remove capability family registries
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: 0f5c9b7c21acddd789b21edf6c5a7e1316fe4053
|
||||
README.zh.md: 10a4af24d82e8472803d96af1d9b7046731a7833
|
||||
README.md: b2512790c5cf5b3a06523cf91b50dd5d288b1522
|
||||
README.zh.md: dda98b7aa4775ac2453704295df87c3c92025093
|
||||
|
||||
@@ -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 family receives one resolved mode-and-root policy per call and registers whether the current runtime fences filesystem tools, one-shot bash commands, or terminal sessions; the model receives only those current facts before each request.
|
||||
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
|
||||
|
||||
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 and contributes its model-facing family; the current section therefore does not claim that an unfenced family shares another family's restrictions. The [cross-family fs sandbox Agent Note](../../../.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,9 +17,7 @@ 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.
|
||||
- `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`.
|
||||
- `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.
|
||||
@@ -36,24 +34,24 @@ A runtime switch is one log-only `sandbox/mode` event on the session it applies
|
||||
|
||||
#### What the model sees
|
||||
|
||||
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.
|
||||
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. 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.
|
||||
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. The write and edit tools, one-shot bash commands, and terminal sessions may modify files under the session workspace: "<workspace root>". Some platform temporary areas may also be writable.
|
||||
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 the write and edit tools, one-shot bash commands, or terminal sessions.
|
||||
Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.
|
||||
```
|
||||
|
||||
#### Token effect
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
沙箱策略解析的唯一归属位置:部署默认 [`SandboxMode`](../sandbox/README.md) 与回退根目录,加上每个会话的持久模式覆盖和不可变工作区根目录。每个强制执行家族在每次调用时都会收到一项解析完成的模式与根目录策略,并登记当前运行时对文件系统工具、一次性 bash 命令和终端会话中的哪些家族施加围栏;模型在每次请求前只会收到这些当前事实。
|
||||
沙箱策略解析的唯一归属位置:部署默认 [`SandboxMode`](../sandbox/README.md) 与回退根目录,加上每个会话的持久模式覆盖和不可变工作区根目录。每项负责强制执行的能力在每次调用时都会收到一项解析完成的模式与根目录策略;模型在每次请求前会收到当前策略,而不会另收一份能力清单。
|
||||
|
||||
## 为何需要共享归属位置
|
||||
|
||||
文件系统工具、一次性 bash 命令和终端会话可以用不同组合强制执行同一套模式词汇。如果各自解析 `mode` + `workspaceRoot`,就可能漂移成分裂世界,正是[沙箱 Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)所警告的情况。每个强制执行后端都会消费归属方解析出的完整策略,并贡献其面向模型的家族;因此,当前段落不会声称不受围栏约束的家族也受另一家族的限制。[跨家族 fs 沙箱 Agent Note](../../../.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,9 +17,7 @@
|
||||
|
||||
- `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。相同家族仍是彼此独立的贡献;该段落使用规范的家族顺序,并且只有最后一项贡献离开后才移除对应家族。
|
||||
- `ctx.sandboxPolicy.registerEscalatableFamily(family)`:独立注册实际工具 schema 与执行路径可提供经批准的更宽松模式重试的家族。反预防性拒绝引导只会列出既受强制执行又可升权的家族;各项贡献独立释放。
|
||||
- `sandbox:policy`:由 `resolve({ session })` 和当前家族贡献派生的请求时缓存安全上下文贡献。没有强制执行家族时为空,只说明模式、受影响的面向模型操作,以及 `workspace-write` 下规范化的会话工作区。
|
||||
- `sandbox:policy`:直接派生自 `resolve({ session })` 的请求时缓存安全上下文贡献。它说明该模式中与具体能力无关的文件效果契约,以及 `workspace-write` 下规范化的会话工作区;工具归属方仍负责操作特定的拒绝与升权引导。
|
||||
- `effectiveSandboxMode(events)`:会话 `sandbox/mode` 事件的纯 fold(最后一次切换胜出,没有则为 `undefined`),在 `resolve()` 内使用。
|
||||
- `setSandboxMode(session, mode)`:逐会话覆盖的唯一写入路径:恰好追加一条 `sandbox/mode` 事件。切换本身就是事件;不会在带外修改模式。
|
||||
- `SANDBOX_MODES`:所有模式,用于选项展示与运行时验证。
|
||||
@@ -36,24 +34,24 @@
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
只要至少注册了一个强制执行家族,当前运行时上下文快照中就会有一项 `sandbox:policy` 贡献。以下示例展示全部三个家族;缺失的家族会被省略。工具插件继续负责操作与升级引导,批准策略单独贡献给同一份快照,计划引导仍由 `dsh-plan-mode` 的系统段落管理。
|
||||
每个 agent 会话的当前运行时上下文快照中都有一项 `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 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.
|
||||
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. The write and edit tools, one-shot bash commands, and terminal sessions may modify files under the session workspace: "<workspace root>". Some platform temporary areas may also be writable.
|
||||
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 the write and edit tools, one-shot bash commands, or terminal sessions.
|
||||
Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.
|
||||
```
|
||||
|
||||
#### Token 影响
|
||||
|
||||
@@ -9,11 +9,9 @@
|
||||
* 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. 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
|
||||
* 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.
|
||||
*
|
||||
@@ -36,47 +34,15 @@ function resolveWorkspaceRoot(path: string): string {
|
||||
return resolvePath(canonicalPath(path))
|
||||
}
|
||||
|
||||
/** Model-facing operation family whose current file policy is enforced by a runtime contribution. */
|
||||
type FilePolicyFamily = 'filesystem' | 'bash' | 'terminal'
|
||||
|
||||
/** Canonical model-facing order, independent of plugin load order. */
|
||||
const FILE_POLICY_FAMILIES: readonly FilePolicyFamily[] = ['filesystem', 'bash', 'terminal']
|
||||
|
||||
const FAMILY_LABELS: Readonly<Record<FilePolicyFamily, string>> = {
|
||||
filesystem: 'the write and edit tools',
|
||||
bash: 'one-shot bash commands',
|
||||
terminal: 'terminal sessions',
|
||||
}
|
||||
|
||||
/** Join model-facing family names with stable English punctuation. */
|
||||
function familyList(families: readonly FilePolicyFamily[], conjunction: 'and' | 'or'): string {
|
||||
const labels = families.map(family => FAMILY_LABELS[family])
|
||||
if (labels.length === 1) return labels[0] as string
|
||||
if (labels.length === 2) return `${labels[0]} ${conjunction} ${labels[1]}`
|
||||
return `${labels.slice(0, -1).join(', ')}, ${conjunction} ${labels.at(-1)}`
|
||||
}
|
||||
|
||||
/** Render only policy facts shared by every backend enforcing each registered family. */
|
||||
function renderPolicyContext(
|
||||
policy: SandboxExecutionPolicy,
|
||||
families: readonly FilePolicyFamily[],
|
||||
escalatableFamilies: readonly FilePolicyFamily[],
|
||||
): string {
|
||||
if (families.length === 0) return ''
|
||||
/** Render the policy without claiming which capabilities are mounted. */
|
||||
function renderPolicyContext(policy: SandboxExecutionPolicy): string {
|
||||
switch (policy.mode) {
|
||||
case 'read-only': {
|
||||
const subjects = familyList(families, 'and')
|
||||
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')
|
||||
return `Current DSH file policy: workspace-write. ${subjects[0]?.toUpperCase()}${subjects.slice(1)} may modify files under the session workspace: ${JSON.stringify(policy.workspaceRoot)}. Some platform temporary areas may also be writable.`
|
||||
}
|
||||
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 ${familyList(families, 'or')}.`
|
||||
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
|
||||
@@ -118,10 +84,9 @@ export interface SandboxPolicyRequest {
|
||||
|
||||
/**
|
||||
* The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment
|
||||
* default mode, fallback workspace root, enforcing-family contributions, 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.
|
||||
* 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.
|
||||
@@ -136,11 +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
|
||||
/** 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')
|
||||
// schemastery (static Config) already filled `mode`; the cast records that
|
||||
@@ -157,59 +117,12 @@ export class SandboxPolicyService extends Service {
|
||||
const session = context.agent?.session
|
||||
return session === undefined
|
||||
? ''
|
||||
: renderPolicyContext(this.resolve({ session }), this.activeFamilies(), this.activeEscalatableFamilies())
|
||||
: renderPolicyContext(this.resolve({ session }))
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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 = registry.get(family) ?? new Set<symbol>()
|
||||
contributions.add(token)
|
||||
registry.set(family, contributions)
|
||||
this.emitPromptChange()
|
||||
return () => {
|
||||
contributions.delete(token)
|
||||
if (contributions.size === 0 && registry.get(family) === contributions) {
|
||||
registry.delete(family)
|
||||
}
|
||||
this.emitPromptChange()
|
||||
}
|
||||
}, label)
|
||||
return () => void dispose()
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the complete policy for one capability call. An approved explicit
|
||||
* mode outranks the session's last `sandbox/mode` event, which outranks the
|
||||
@@ -235,21 +148,6 @@ export class SandboxPolicyService extends Service {
|
||||
overrideOf(session: Session): SandboxMode | undefined {
|
||||
return effectiveSandboxMode(session.events)
|
||||
}
|
||||
|
||||
/** Active families in canonical model-facing order. */
|
||||
private activeFamilies(): FilePolicyFamily[] {
|
||||
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')
|
||||
}
|
||||
}
|
||||
|
||||
export default SandboxPolicyService
|
||||
|
||||
@@ -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'
|
||||
@@ -44,8 +44,6 @@ 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 () => {
|
||||
@@ -127,11 +125,10 @@ 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, {})
|
||||
ctx.sandboxPolicy.registerEnforcedFamily('filesystem')
|
||||
expect(ctx.sandboxPolicy).toBeDefined()
|
||||
expect(await policyContext(ctx, session('sess-hmr'))).toContain('read-only')
|
||||
await fiber.dispose()
|
||||
@@ -148,89 +145,20 @@ describe('sandbox:policy request context', () => {
|
||||
return ctx
|
||||
}
|
||||
|
||||
it('omits policy prose when no enforcing family is registered', async () => {
|
||||
const ctx = await promptMounted()
|
||||
expect(await policyContext(ctx, session('sess-no-family'))).toBe('')
|
||||
})
|
||||
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
|
||||
|
||||
it.each([
|
||||
[['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 policyContext(ctx, session(`sess-read-only-${families.join('-')}`))).toBe(expected)
|
||||
})
|
||||
|
||||
it('states the portable workspace guarantee without enumerating host temp paths', async () => {
|
||||
const ctx = await promptMounted({ mode: 'workspace-write', workspaceRoot: '/fallback' })
|
||||
ctx.sandboxPolicy.registerEnforcedFamily('filesystem')
|
||||
ctx.sandboxPolicy.registerEnforcedFamily('bash')
|
||||
ctx.sandboxPolicy.registerEnforcedFamily('terminal')
|
||||
const active = session('sess-workspace-write', '/projects/../projects/current')
|
||||
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 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 () => {
|
||||
const ctx = await promptMounted()
|
||||
const active = session('sess-family-lifecycle')
|
||||
const filesystemFiber = await ctx.plugin(Object.assign((inner: Context) => {
|
||||
inner.sandboxPolicy.registerEnforcedFamily('filesystem')
|
||||
}, { inject: ['sandboxPolicy'] }))
|
||||
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) => {
|
||||
disposeBashFirst = inner.sandboxPolicy.registerEnforcedFamily('bash')
|
||||
}, { inject: ['sandboxPolicy'] }))
|
||||
const bashSecondFiber = await ctx.plugin(Object.assign((inner: Context) => {
|
||||
inner.sandboxPolicy.registerEnforcedFamily('bash')
|
||||
}, { inject: ['sandboxPolicy'] }))
|
||||
expect(await policyContext(ctx, active)).toContain('The write and edit tools and one-shot bash commands')
|
||||
disposeBashFirst()
|
||||
disposeBashFirst()
|
||||
expect(await policyContext(ctx, active)).toContain('The write and edit tools and one-shot bash commands')
|
||||
await bashSecondFiber.dispose()
|
||||
expect(await policyContext(ctx, active)).toContain('The write and edit tools cannot modify files')
|
||||
await bashFirstFiber.dispose()
|
||||
await filesystemFiber.dispose()
|
||||
expect(await policyContext(ctx, active)).toBe('')
|
||||
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' })
|
||||
ctx.sandboxPolicy.registerEnforcedFamily('filesystem')
|
||||
const active = session('sess-tmpdir-stability', '/projects/current')
|
||||
const previous = process.env.TMPDIR
|
||||
try {
|
||||
@@ -251,18 +179,17 @@ describe('sandbox:policy request context', () => {
|
||||
|
||||
it('reflects the latest durable switch on the next assembly and stays byte-stable otherwise', async () => {
|
||||
const ctx = await promptMounted()
|
||||
ctx.sandboxPolicy.registerEnforcedFamily('filesystem')
|
||||
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).toContain('does not restrict the write and edit tools')
|
||||
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)).toContain(JSON.stringify(resolve('/projects/current')))
|
||||
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 () => {
|
||||
@@ -270,7 +197,6 @@ describe('sandbox:policy request context', () => {
|
||||
setSandboxMode(active, 'workspace-write')
|
||||
const resumed = new Session(active.id, active.events, active.header)
|
||||
const ctx = await promptMounted({ mode: 'read-only' })
|
||||
ctx.sandboxPolicy.registerEnforcedFamily('filesystem')
|
||||
|
||||
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