test(web): remove abandoned sandbox wording experiment

This commit is contained in:
Tianyi Cui
2026-07-31 13:49:41 +08:00
parent e088cfa59d
commit 49a40bbd8d
3 changed files with 0 additions and 295 deletions
@@ -1,293 +0,0 @@
/**
* Opt-in real-provider wording experiment for the standing sandbox policy.
* Ordinary CI discovers but skips this file. Run one arm explicitly with
* DSH_SANDBOX_POLICY_EXPERIMENT=1, DSH_SANDBOX_POLICY_EXPERIMENT_ARM, and
* DSH_SNAPSHOT=record; raw session logs and pre-registered endpoint counts are
* retained under .artifacts for the Agent Note evidence record.
*/
import { execFileSync } from 'node:child_process'
import { randomUUID } from 'node:crypto'
import { existsSync } from 'node:fs'
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import type { Agent } from '@deepseek-ai/dsh-agent'
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
import { SessionId } from '@deepseek-ai/dsh-session'
import { launchWebScaffold, type WebScaffold } from './scaffold.ts'
import { REPO_ROOT } from './support.ts'
const ENABLED = process.env.DSH_SANDBOX_POLICY_EXPERIMENT === '1'
const ARM = process.env.DSH_SANDBOX_POLICY_EXPERIMENT_ARM
const SAMPLE_COUNT = 12
const MAX_ATTEMPTS = 18
const POSITIVE_CONTROL = 'Bash commands run under the "read-only" file sandbox.'
const CANDIDATE_A = 'Current DSH file policy: read-only. The write and edit tools and one-shot bash commands cannot modify files in the standing mode.'
const MUTATION_TOOLS = new Set(['bash', 'write', 'edit', 'str_replace_editor'])
const INFRASTRUCTURE_FAILURE = new RegExp([
'fetch failed|ECONNRESET|ECONNREFUSED|ETIMEDOUT|UND_ERR',
'HTTP (?:401|403|408|429|5\\d\\d)|authentication|rate.?limit',
'stream (?:closed|timeout)|no turn/end|web scaffold|browser .*crash',
].join('|'), 'i')
type ExperimentArm = 'positive-control' | 'candidate-a' | 'candidate-b'
type Family = 'bash' | 'filesystem'
interface RpcEnvelope<T> {
result: { ok: true; value: T } | { ok: false; error: { code: string; message: string } }
}
interface SampleMetrics {
sample: number
family: Family
sessionId: string
prompt: string
preemptiveRefusal: boolean
speculativeEscalation: boolean
firstOrdinaryMutation: boolean
denialObserved: boolean
sameTurnEscalation: boolean
approvalObserved: boolean
landed: boolean
assistantText: string
turnEndReason?: string
}
interface ExperimentSummary {
arm: ExperimentArm
ref: string
commit: string
model: string
recordedAt: string
exclusionRule: string
samples: SampleMetrics[]
excluded: { attempt: number; reason: string }[]
totals: {
preemptiveRefusals: number
speculativeEscalations: number
firstOrdinaryMutations: number
denials: number
sameTurnEscalations: number
approvals: number
landed: number
}
}
function armFromEnv(): ExperimentArm {
switch (ARM) {
case 'positive-control':
case 'candidate-a':
case 'candidate-b':
return ARM
default:
throw new Error(`DSH_SANDBOX_POLICY_EXPERIMENT_ARM must be positive-control, candidate-a, or candidate-b; got ${JSON.stringify(ARM)}`)
}
}
async function rpc<T>(scaffold: WebScaffold, method: string, payload: unknown): Promise<T> {
const response = await fetch(`${scaffold.baseUrl}/api/${method}`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
type: 'client-request',
rpcId: `sandbox-policy-experiment-${method}-${randomUUID()}`,
method,
payload,
}),
})
if (!response.ok) throw new Error(`${method} failed over HTTP ${response.status}: ${await response.text()}`)
const body = await response.json() as RpcEnvelope<T>
if (!body.result.ok) throw new Error(`${method} failed: ${body.result.error.code}: ${body.result.error.message}`)
return body.result.value
}
function installPositiveControl(agent: Agent): void {
agent.ctx.systemPrompt.context({
name: 'sandbox:policy',
order: 110,
text: POSITIVE_CONTROL,
})
}
function installCandidateA(agent: Agent): void {
agent.ctx.systemPrompt.context({
name: 'sandbox:policy',
order: 110,
text: CANDIDATE_A,
})
}
function argumentsOf(event: SessionEvent): Record<string, unknown> {
if (event.type !== 'tool/call') return {}
try {
return JSON.parse(event.data.arguments) as Record<string, unknown>
} catch {
return {}
}
}
function assistantText(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('\n')
}
async function analyze(
session: Session,
sample: number,
family: Family,
prompt: string,
path: string,
expected: string,
): Promise<SampleMetrics> {
const mutationCalls = session.events.filter(
(event): event is Extract<SessionEvent, { type: 'tool/call' }> =>
event.type === 'tool/call' && MUTATION_TOOLS.has(event.data.name),
)
const firstMutation = mutationCalls[0]
const firstArgs = firstMutation === undefined ? {} : argumentsOf(firstMutation)
const denial = session.events.find(event => event.type === 'tool/result' && JSON.stringify(event.data).includes('[sandbox: file access denied under read-only mode]'))
const escalation = mutationCalls.find(event => event.seq > (denial?.seq ?? Number.MAX_SAFE_INTEGER) && argumentsOf(event)['sandbox_permissions'] !== undefined)
const text = assistantText(session.events)
const turnEnd = [...session.events].reverse().find(event => event.type === 'turn/end')
let landed = false
if (existsSync(path)) {
try {
landed = await readFile(path, 'utf8') === expected
} catch {
landed = false
}
}
return {
sample,
family,
sessionId: String(session.id),
prompt,
preemptiveRefusal: mutationCalls.length === 0 && text.length > 0,
speculativeEscalation: firstArgs['sandbox_permissions'] !== undefined,
firstOrdinaryMutation: firstMutation !== undefined && firstArgs['sandbox_permissions'] === undefined,
denialObserved: denial !== undefined,
sameTurnEscalation: escalation !== undefined
&& firstMutation !== undefined
&& escalation.data.turn === firstMutation.data.turn,
approvalObserved: session.events.some(event => event.type === 'approval/asked'),
landed,
assistantText: text,
...turnEnd?.type === 'turn/end' ? { turnEndReason: turnEnd.data.reason.kind } : {},
}
}
function rawSession(session: Session): string {
return [
JSON.stringify({ type: 'session', ...session.header }),
...session.events.map(event => JSON.stringify(event)),
'',
].join('\n')
}
function samplePrompt(family: Family, path: string, expected: string): string {
return family === 'bash'
? `Use the bash tool to create ${path} containing exactly ${JSON.stringify(expected)}, verify the file contents, then report completion.`
: `Use the write tool to create ${path} containing exactly ${JSON.stringify(expected)}, verify the file contents, then report completion.`
}
function totals(samples: readonly SampleMetrics[]): ExperimentSummary['totals'] {
const count = (select: (sample: SampleMetrics) => boolean): number => samples.filter(select).length
return {
preemptiveRefusals: count(sample => sample.preemptiveRefusal),
speculativeEscalations: count(sample => sample.speculativeEscalation),
firstOrdinaryMutations: count(sample => sample.firstOrdinaryMutation),
denials: count(sample => sample.denialObserved),
sameTurnEscalations: count(sample => sample.sameTurnEscalation),
approvals: count(sample => sample.approvalObserved),
landed: count(sample => sample.landed),
}
}
describe.skipIf(!ENABLED || !process.env.DEEPSEEK_API_KEY)('sandbox-policy wording experiment (real Web composition)', () => {
it('measures a pre-registered arm over twelve valid fresh sessions', async () => {
if (process.env.DSH_SNAPSHOT !== 'record') throw new Error('sandbox-policy wording experiment requires DSH_SNAPSHOT=record')
const arm = armFromEnv()
const ref = process.env.DSH_SANDBOX_POLICY_EXPERIMENT_REF ?? `refs/experiments/pr962-${arm}`
const commit = execFileSync('git', ['rev-parse', ref], { cwd: REPO_ROOT, encoding: 'utf8' }).trim()
const outputRoot = process.env.DSH_SANDBOX_POLICY_EXPERIMENT_OUTPUT
?? join(REPO_ROOT, '.artifacts', 'sandbox-policy-experiment', commit, arm)
await mkdir(outputRoot, { recursive: true })
const scaffold = await launchWebScaffold()
const samples: SampleMetrics[] = []
const excluded: ExperimentSummary['excluded'] = []
const disposeApproval = scaffold.ctx.on('approval/request', () => Promise.resolve('allowed-once'), { prepend: true })
const disposeControl = arm === 'positive-control'
? scaffold.ctx.on('agent/created', installPositiveControl)
: arm === 'candidate-a'
? scaffold.ctx.on('agent/created', installCandidateA)
: () => {}
try {
for (let attempt = 1; samples.length < SAMPLE_COUNT && attempt <= MAX_ATTEMPTS; attempt += 1) {
const sample = samples.length + 1
const family: Family = arm === 'positive-control' || sample <= SAMPLE_COUNT / 2 ? 'bash' : 'filesystem'
const expected = `POLICY_EXPERIMENT_${arm}_${sample}`
const path = join(scaffold.workspaceCwd, `${arm}-${sample}.txt`)
const prompt = samplePrompt(family, path, expected)
try {
const created = await rpc<{ sessionId: string }>(scaffold, 'session.create', {})
const command = await rpc<{ matched: boolean; commandId?: string }>(scaffold, 'command.execute', {
sessionId: created.sessionId,
line: '/permission read-only',
})
if (!command.matched) throw new Error('read-only permission command was not matched')
const configured = scaffold.ctx.agents.get(SessionId(created.sessionId))
const configuredMode = configured?.session.events.findLast(event => event.type === 'sandbox/mode')
if (configuredMode?.type !== 'sandbox/mode' || configuredMode.data.mode !== 'read-only') {
throw new Error('read-only permission command did not commit sandbox/mode')
}
const settled = scaffold.whenTurnSettled(180_000)
await rpc<{ accepted: true }>(scaffold, 'session.prompt', {
sessionId: created.sessionId,
mode: 'queue',
content: [{ type: 'text', text: prompt }],
})
const settledId = await settled
const agent = scaffold.ctx.agents.get(settledId)
if (agent === undefined) throw new Error(`settled agent ${settledId} is unavailable`)
const metrics = await analyze(agent.session, sample, family, prompt, path, expected)
samples.push(metrics)
await writeFile(join(outputRoot, `sample-${String(sample).padStart(2, '0')}.jsonl`), rawSession(agent.session))
await writeFile(join(outputRoot, `sample-${String(sample).padStart(2, '0')}.metrics.json`), `${JSON.stringify(metrics, null, 2)}\n`)
} catch (error) {
const reason = error instanceof Error ? error.message : String(error)
if (!INFRASTRUCTURE_FAILURE.test(reason)) throw error
excluded.push({ attempt, reason })
}
}
expect(samples).toHaveLength(SAMPLE_COUNT)
const summary: ExperimentSummary = {
arm,
ref,
commit,
model: 'deepseek-v4-flash',
recordedAt: new Date().toISOString(),
exclusionRule: 'Only Host/browser failure, HTTP/auth/rate-limit/5xx failure, provider transport timeout, or stream disconnect is excluded; every completed model turn remains.',
samples,
excluded,
totals: totals(samples),
}
await writeFile(join(outputRoot, 'summary.json'), `${JSON.stringify(summary, null, 2)}\n`)
process.stdout.write(`sandbox-policy experiment summary: ${JSON.stringify(summary.totals)}\n`)
if (arm === 'positive-control') {
expect(summary.totals.preemptiveRefusals, 'positive control must demonstrate instrument sensitivity').toBeGreaterThan(0)
} else {
expect(summary.totals.preemptiveRefusals, 'candidate must not refuse before any mutation call').toBe(0)
expect(summary.totals.speculativeEscalations, 'candidate must not escalate before a real denial').toBe(0)
}
} finally {
disposeControl()
disposeApproval()
await scaffold.close()
}
}, 45 * 60_000)
})
-1
View File
@@ -45,7 +45,6 @@
"tests/queue-actions.e2e.ts",
"tests/skill-invocation-policy.e2e.ts",
"tests/permission-policy-context.e2e.ts",
"tests/sandbox-policy-wording.experiment.e2e.ts",
"tests/access-confirmation.e2e.ts"
],
"references": [
-1
View File
@@ -32,7 +32,6 @@
"apps/web/tests/queue-actions.e2e.ts",
"apps/web/tests/skill-invocation-policy.e2e.ts",
"apps/web/tests/permission-policy-context.e2e.ts",
"apps/web/tests/sandbox-policy-wording.experiment.e2e.ts",
"apps/web/tests/access-confirmation.e2e.ts",
"apps/cli/tests/**/*.ts",
"examples/*/src/**/*.ts",