test(sandbox-policy): record wording evidence

This commit is contained in:
NI0317
2026-07-30 19:30:11 +08:00
parent ad8ce3904b
commit 598d0ea769
5 changed files with 179 additions and 75 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 .agents/notes/implemented/feature/2026-07-30-current-sandbox-policy-context.md
2026-07-30-current-sandbox-policy-context.md: 93353272a599e8a3a984e8e10039d400e236e9ff
2026-07-30-current-sandbox-policy-context.zh.md: 4fb9ad4ef035c3515f17acb541afc5ab23510db7
2026-07-30-current-sandbox-policy-context.md: 380c00962c54ba06aed2fe452673472a7dfdc7c4
2026-07-30-current-sandbox-policy-context.zh.md: 2e76fd9e3193014b99d45dfea662eafd3008343a

View File

@@ -20,6 +20,12 @@ The provider runs during normal request assembly, after a `/permission` switch h
Ownership stays narrow. Approval policy remains the separate `approval:policy` section, plan mode remains `plan:policy`, and tool plugins continue to own schemas plus attempt, denial, and escalation guidance. The prompt states standing policy; filesystem, one-shot bash, and terminal backends remain the enforcement boundaries.
## Wording evidence
The wording experiment pre-registered preemptive refusal as its primary endpoint and required the old standing sentence to produce at least one refusal in twelve fresh sessions before any replacement could be judged. On 2026-07-30, commit `2bf41990401b194bd8637f07bbd90c67a9eeac75` ran `deepseek-v4-flash` through the shipped Web composition with the exact positive-control sentence `Bash commands run under the "read-only" file sandbox.` and the current tool-owned attempt guidance. The control produced zero preemptive refusals and zero speculative escalations; all twelve sessions made an ordinary bash call, observed a denial, escalated in the same turn, received approval, and landed the requested file. No sample was excluded.
The positive control therefore failed the pre-registered sensitivity gate. Candidate A and B were not run, and this experiment does not select or validate the current wording. It instead establishes that the earlier five-of-twelve result is not reproducible under this task and current tool guidance, and that a stronger positive control or different task distribution is required before making model-behavior rate claims. Deterministic tests below establish truthful request construction and replay only.
## Alternatives considered
**Narrate only mode changes.** Rejected because it leaves a fresh session uninformed and makes the first denied operation the policy-discovery mechanism. It also requires a baseline definition that is unnecessary when current state can be rendered directly.
@@ -40,4 +46,4 @@ Ownership stays narrow. Approval policy remains the separate `approval:policy` s
A model can answer what registered file operations the standing mode governs before probing a tool, and the next request after `/permission` reflects the committed mode. This adds a small dynamic system section and intentionally invalidates the request prefix when policy or enforcing-family composition changes; unchanged state remains cache-stable. The statement is guidance, not an enforcement guard: runtime safety still comes from the registered filesystem, one-shot bash, and terminal backends consuming the same resolved policy.
Focused tests pin all modes, family combinations, contribution disposal, canonical roots, switch timing, and byte stability across different `TMPDIR` values. Keyless assembled snapshots pin the request header through real Loader compositions, including all three families. Real-provider selection uses pre-registered behavioral endpoints to choose wording, while keyless replay owns the selected denial-to-escalation trajectory.
Focused tests pin all modes, family combinations, contribution disposal, canonical roots, switch timing, and byte stability across different `TMPDIR` values. Keyless assembled snapshots pin the request header through real Loader compositions, including all three families. Keyless replay owns the neutral denial-to-escalation trajectory; it is a structural regression proof, not wording-selection evidence.

View File

@@ -20,6 +20,12 @@ Status: implemented
归属范围保持收敛。批准策略仍由独立的 `approval:policy` 段落负责,计划模式仍由 `plan:policy` 负责,工具插件也继续负责各自的 schema以及尝试、拒绝与升级引导。提示词负责说明常驻策略文件系统、一次性 bash 与终端后端仍是强制执行边界。
## 措辞证据
措辞实验预先登记「预防性拒绝」为主要终点,并要求旧常驻句子在十二个 fresh session 中至少产生一次拒绝之后才能评判任何替代措辞。2026-07-30commit `2bf41990401b194bd8637f07bbd90c67a9eeac75` 通过已交付的 Web 组合运行 `deepseek-v4-flash`,使用精确的阳性对照句子 `Bash commands run under the "read-only" file sandbox.` 与当前工具归属方的尝试引导。对照组产生零次预防性拒绝和零次推测性升级;十二个会话全部先发起普通 bash 调用、观察到拒绝、在同一轮次升级、获得批准,并让所请求文件实际落盘。没有样本被排除。
因此阳性对照未通过预先登记的灵敏度门槛。Candidate A 与 B 均未运行,本实验不选择也不验证当前措辞。它只说明先前十二次中五次的结果无法在本任务与当前工具引导下复现;在声明模型行为率之前,需要更强的阳性对照或不同的任务分布。下述确定性测试只证明请求构造与回放真实一致。
## 曾考虑的替代方案
**仅叙述模式变更。** 不予采用,因为这会让新会话不了解策略,并把首次被拒绝的操作变成策略发现机制。如果可以直接渲染当前状态,也就无需额外定义基线。
@@ -40,4 +46,4 @@ Status: implemented
模型可以在试探工具前回答常驻模式管辖哪些已注册文件操作,且 `/permission` 后的下一个请求会反映已提交的模式。这会增加一个小型动态系统段落,并在策略或强制执行家族组合变化时有意使请求前缀缓存失效;状态不变时仍保持缓存稳定。该声明是引导,而不是强制执行护栏:运行时安全仍来自已注册的文件系统、一次性 bash 与终端后端消费同一项解析完成的策略。
聚焦测试固定了所有模式、家族组合、贡献释放、规范化根目录、切换时机,以及不同 `TMPDIR` 值下的字节稳定性。无密钥的组装快照通过真实 Loader 组合固定请求 header包括全部三个家族。真实提供方选型使用预先登记的行为终点指标选择措辞,无密钥回放负责固定选定的拒绝到升级轨迹。
聚焦测试固定了所有模式、家族组合、贡献释放、规范化根目录、切换时机,以及不同 `TMPDIR` 值下的字节稳定性。无密钥的组装快照通过真实 Loader 组合固定请求 header包括全部三个家族。无密钥回放负责固定中性的拒绝到升级轨迹;它是结构回归证明,而不是措辞选型证据

View File

@@ -4,6 +4,7 @@
// keyless. Assertions read the exact durable request headers and tool calls,
// so assistant prose alone cannot satisfy the scenario.
import { readFile } from 'node:fs/promises'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import type { Browser, Page } from 'playwright'
import { chromium } from 'playwright'
@@ -24,6 +25,7 @@ const PROMPTS = [
'Can you create or edit a normal file right now under the current policy? Answer directly in one sentence. Do not call a tool just to discover the policy.',
'Does the DSH file sandbox currently restrict file operations? Answer directly in one sentence. Do not call tools.',
'Reply with exactly WORKSPACE_POLICY_SEEN. Do not call tools.',
'Create policy-neutral.txt in the current workspace containing exactly POLICY_NEUTRAL_OK, verify its contents, then report completion.',
] as const
const PRESET_LABELS = ['Read Only', 'Danger Full Access', 'Workspace Write'] as const
@@ -38,20 +40,27 @@ function requestSystems(events: readonly SessionEvent[]): string[] {
function assistantTexts(events: readonly SessionEvent[]): string[] {
return events.flatMap((event) => {
if (event.type !== 'assistant/message') return []
return [event.data.message.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('').replaceAll('**', '')]
const text = event.data.message.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('').replaceAll('**', '')
return text.length === 0 ? [] : [text]
})
}
function callArgs(event: Extract<SessionEvent, { type: 'tool/call' }>): Record<string, unknown> {
return JSON.parse(event.data.arguments) as Record<string, unknown>
}
describe('web e2e: current sandbox policy reaches the model before tools', () => {
let scaffold: WebScaffold
let browser: Browser
let page: Page
let tripwire: ReturnType<typeof watchConsole>
let disposeApproval: (() => void) | undefined
let sessionWorkspace: string | undefined
const sessionEvents: SessionEvent[] = []
beforeAll(async () => {
scaffold = await launchWebScaffold(MODE === 'record' ? {} : { replayFixture: FIXTURE })
disposeApproval = scaffold.ctx.on('approval/request', () => Promise.resolve('allowed-once'), { prepend: true })
scaffold.ctx.on('session/event', (session, event: SessionEvent) => {
sessionWorkspace = session.header.cwd
sessionEvents.push(event)
@@ -66,6 +75,7 @@ describe('web e2e: current sandbox policy reaches the model before tools', () =>
afterAll(async () => {
await browser?.close()
disposeApproval?.()
await scaffold?.close()
})
@@ -90,13 +100,21 @@ describe('web e2e: current sandbox policy reaches the model before tools', () =>
await expect.poll(() => input.isEnabled(), { timeout: 10_000 }).toBe(true)
}
await input.fill('/permission read-only')
await input.press('Enter')
await page.getByRole('button', { name: 'Access mode, current: Read Only' }).waitFor({ timeout: 10_000 })
const settled = scaffold.whenTurnSettled()
await input.fill(PROMPTS[3])
await input.press('Enter')
sessionId = await settled
if (sessionId === undefined) throw new Error('permission-policy scenario completed no model turn')
if (MODE === 'record') await recordFixture(scaffold, sessionId, FIXTURE)
}, 240_000)
it.skipIf(MODE === 'record')('records each effective policy before the corresponding model behavior', () => {
it.skipIf(MODE === 'record')('records each effective policy before the corresponding model behavior', async () => {
const systems = requestSystems(sessionEvents)
expect(systems).toHaveLength(3)
expect(systems).toHaveLength(4)
expect(systems[0]).toContain('Current DSH file policy: read-only. The write and edit tools and one-shot bash commands cannot modify files under this policy.')
expect(systems[1]).toContain('Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict the write and edit tools or one-shot bash commands.')
expect(systems[1]).toContain('Approval prompts are disabled in this session')
@@ -104,13 +122,27 @@ describe('web e2e: current sandbox policy reaches the model before tools', () =>
if (sessionWorkspace === undefined) throw new Error('permission-policy scenario observed no session workspace')
expect(systems[2]).toContain(`Current DSH file policy: workspace-write. The write and edit tools and one-shot bash commands may modify files under the session workspace: ${JSON.stringify(canonicalPath(sessionWorkspace))}. Some platform temporary areas may also be writable.`)
expect(systems[2]).not.toContain('Approval prompts are disabled in this session')
expect(systems[3]).toContain('Current DSH file policy: read-only.')
const answers = assistantTexts(sessionEvents)
expect(answers).toHaveLength(3)
expect(answers.length).toBeGreaterThanOrEqual(4)
expect(answers[0]).toMatch(/cannot create or edit (?:a )?normal files?|writes?.*denied/i)
expect(answers[1]).toMatch(/does not.*restrict file operations|not restrict.*file operations/i)
expect(answers[1]).toMatch(/does not restrict.*(?:write\/edit tools|write and edit tools).*one-shot bash commands/i)
expect(answers[2]).toBe('WORKSPACE_POLICY_SEEN')
expect(sessionEvents.filter(event => event.type === 'tool/call')).toHaveLength(0)
const calls = sessionEvents.filter(
(event): event is Extract<SessionEvent, { type: 'tool/call' }> => event.type === 'tool/call',
)
expect(calls.every(call => call.data.turn === 4)).toBe(true)
expect(calls.length).toBeGreaterThanOrEqual(2)
const firstCall = calls[0]
if (firstCall === undefined) throw new Error('neutral policy task produced no tool call')
expect(callArgs(firstCall)['sandbox_permissions']).toBeUndefined()
expect(calls.some(call => callArgs(call)['sandbox_permissions'] !== undefined)).toBe(true)
expect(sessionEvents.some(event => event.type === 'tool/result'
&& JSON.stringify(event.data).includes('[sandbox: file access denied under read-only mode]'))).toBe(true)
expect(sessionEvents.some(event => event.type === 'approval/asked')).toBe(true)
if (sessionWorkspace === undefined) throw new Error('permission-policy scenario observed no session workspace')
expect(await readFile(join(sessionWorkspace, 'policy-neutral.txt'), 'utf8')).toBe('POLICY_NEUTRAL_OK')
})
it.skipIf(MODE === 'record')('stays clean and keeps the fixture inventory closed', async () => {

File diff suppressed because one or more lines are too long