test(snapshot): cover session-scoped sandbox roots

This commit is contained in:
Tianyi Cui
2026-07-21 01:16:29 +08:00
parent dc874a90bd
commit 7677d49c93
16 changed files with 186 additions and 19 deletions

View File

@@ -43,7 +43,7 @@ The default tree composes [`@deepseek-ai/dsh-sandbox-local`](../../packages/sand
- **Every approval is one-shot**: the choices are `Allow once` and `Reject`; a dismissal, rejection, missing editor, or unavailable runner fails closed.
- **The boundary spans bash and the filesystem tools per session**: bash confines through the OS runner and the `read`/`write`/`edit` tools through an in-process path fence ([`dsh-fs-sandbox`](../../packages/fs/fs-sandbox/)); both receive the calling session's cwd as `workspaceRoot`.
`tests/escalation.e2e.ts` boots this default tree keyless, drives the permission select, and—with a key and usable runner—proves both approval outcomes against the filesystem. The agent-spine e2e independently boots one context with two project sessions and world-verifies concurrent own-root success plus sibling-root denial through both shipped tool families. Most snapshots use the ACP tree and start at `danger-full-access` so bash fixtures remain runner-independent; scenarios that call `read`, `write`, or `edit` use the full-access fs overlay and a separate request-header pin. The permission-switching and escalation inputs select `workspace-write` before exercising the bash policy path. No fixture pins real runner denial text because its dialect is platform-specific.
`tests/escalation.e2e.ts` boots this default tree keyless, drives the permission select, and—with a key and usable runner—proves both approval outcomes against the filesystem. The agent-spine e2e independently boots one context with two project sessions and world-verifies concurrent own-root success plus sibling-root denial through both shipped tool families. The keyless `session-sandbox-root` ACP snapshot places its generated project under the user home while an overlay points the deployment fallback at `/tmp`; its successful `workspace-write` call proves the assembled app used the session cwd. Most snapshots start at `danger-full-access` so bash fixtures remain runner-independent. No fixture pins real runner denial text because its dialect is platform-specific.
## MVP limitations

View File

@@ -0,0 +1,36 @@
# Keyless replay counterpart of session-sandbox-root.cordis.yml. Patches do not
# compose across nested includes, so the replay swap and the deliberately
# distinct sandbox fallback are applied together to the live tree.
- id: base
name: '@cordisjs/plugin-include'
config:
path: ./cordis.yml
patches:
- id: llm-deepseek
name: '@deepseek-ai/dsh-llm-deepseek'
disabled: true
- id: sandbox
name: '@deepseek-ai/dsh-sandbox-local'
config:
runnerCommand:
- bash
- -c
- while [ "$1" != "--" ]; do shift; done; shift; exec "$@"
- passthrough-runner
runnerFailureSignatures:
- 'passthrough-runner: profile rejected'
- id: sandbox-policy
name: '@deepseek-ai/dsh-sandbox-policy'
config:
mode: danger-full-access
workspaceRoot: /tmp
- insert:
- id: llm-replay
name: '@deepseek-ai/dsh-llm-replay'
config:
providers:
- id: deepseek
name: DeepSeek
models:
- id: deepseek-v4-flash
- id: deepseek-v4-pro

View File

@@ -0,0 +1,14 @@
# Session-root sandbox snapshot overlay. The generated ACP session cwd lives
# under the user's home, while this deployment fallback deliberately points at
# /tmp. A workspace-write mutation can therefore succeed only when the calling
# session's cwd replaces the process-level fallback root.
- id: base
name: '@cordisjs/plugin-include'
config:
path: ./cordis.yml
patches:
- id: sandbox-policy
name: '@deepseek-ai/dsh-sandbox-policy'
config:
mode: !!js "process.env.DSH_PERMISSION_MODE ?? (process.env.DSH_SNAPSHOT === undefined ? 'workspace-write' : 'danger-full-access')"
workspaceRoot: /tmp

View File

@@ -1,5 +1,6 @@
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'
import { homedir } from 'node:os'
import { defineAcpSnapshotSuite, type Scenario, type SnapshotSuiteOptions } from '@deepseek-ai/dsh-acp-snapshot'
/**
@@ -31,6 +32,7 @@ const WORKSPACE_CONTEXT_CONFIG = fileURLToPath(new URL('../workspace-context.cor
const ADVANCED_CONFIG = fileURLToPath(new URL('../advanced.cordis.yml', import.meta.url))
const FS_CONFIG = fileURLToPath(new URL('../fs.cordis.yml', import.meta.url))
const DEPTH_TWO_CONFIG = fileURLToPath(new URL('../depth-two.cordis.yml', import.meta.url))
const SESSION_SANDBOX_ROOT_CONFIG = fileURLToPath(new URL('../session-sandbox-root.cordis.yml', import.meta.url))
function snapshotModeFromEnv(value: string | undefined): SnapshotSuiteOptions['mode'] {
switch (value) {
@@ -187,6 +189,19 @@ const SCENARIOS: Scenario[] = [
{ name: 'escalation-approved', hasModelTurn: true, recorded: true, headerClass: 'sandbox' },
{ name: 'escalation-rejected', hasModelTurn: true, recorded: true, headerClass: 'sandbox' },
{ name: 'fs-escalation-approved', hasModelTurn: true, recorded: true, headerClass: 'sandbox' },
// Unlike ordinary snapshots, this session cwd is outside the platform temp
// roots that workspace-write always grants. The overlay points the
// deployment fallback at /tmp, so a successful relative write proves the
// assembled app replaced that process-level fallback with SessionHeader.cwd.
{
name: 'session-sandbox-root',
hasModelTurn: true,
recorded: false,
overridden: true,
headerClass: 'sandbox',
configPath: SESSION_SANDBOX_ROOT_CONFIG,
workspaceParent: homedir(),
},
]
defineAcpSnapshotSuite({

View File

@@ -0,0 +1,8 @@
{
"steps": [
{ "op": "initialize" },
{ "op": "newSession" },
{ "op": "setConfigOption", "configId": "permission", "value": "workspace-write" },
{ "op": "prompt", "text": "Use the write tool (NOT bash) to create session-root.txt in the current directory containing exactly: session root. Then reply with exactly the single word DONE." }
]
}

View File

@@ -0,0 +1,22 @@
[
{
"kind": "chunks",
"chunks": [
{ "type": "block-start", "index": 0, "blockType": "tool-call" },
{ "type": "tool-call-delta", "index": 0, "id": "call_session_root", "name": "write", "argumentsDelta": "{\"file_path\":\"session-root.txt\",\"content\":\"session root\"}" },
{ "type": "block-end", "index": 0, "block": { "type": "tool-call", "id": "call_session_root", "name": "write", "arguments": "{\"file_path\":\"session-root.txt\",\"content\":\"session root\"}" } },
{ "type": "usage", "usage": { "inputTokens": 10, "outputTokens": 5 } },
{ "type": "finish", "reason": { "kind": "tool-calls" } }
]
},
{
"kind": "chunks",
"chunks": [
{ "type": "block-start", "index": 0, "blockType": "text" },
{ "type": "text-delta", "index": 0, "text": "DONE" },
{ "type": "block-end", "index": 0, "block": { "type": "text", "text": "DONE" } },
{ "type": "usage", "usage": { "inputTokens": 10, "outputTokens": 2 } },
{ "type": "finish", "reason": { "kind": "stop" } }
]
}
]

View File

@@ -0,0 +1,26 @@
{"type":"session","version":0,"id":"00000000-0000-0000-0000-000000000000","createdAt":0,"cwd":"/Users/cty/acp-snap-cwd-MABAjO","delegationDepth":0}
{"type":"turn/start","seq":0,"time":1784567324138,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}
{"type":"permission/preset","seq":1,"time":1784567324138,"data":{"preset":"workspace-write"}}
{"type":"sandbox/mode","seq":2,"time":1784567324138,"data":{"mode":"workspace-write"}}
{"type":"approval/policy","seq":3,"time":1784567324138,"data":{"policy":"ask"}}
{"type":"user/message","seq":4,"time":1784567324138,"data":{"content":[{"type":"text","text":"Use the write tool (NOT bash) to create session-root.txt in the current directory containing exactly: session root. Then reply with exactly the single word DONE."}],"source":{"kind":"user"}},"surfaceOp":"append"}
{"type":"step/start","seq":5,"time":1784567324142,"data":{"turn":1,"step":1}}
{"type":"request/header","seq":6,"time":1784567324142,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"assistant/chunk","seq":7,"time":1784567324143,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","seq":8,"time":1784567324143,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_session_root","name":"write","argumentsDelta":"{\"file_path\":\"session-root.txt\",\"content\":\"session root\"}"}}}
{"type":"assistant/chunk","seq":9,"time":1784567324143,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_session_root","name":"write","arguments":"{\"file_path\":\"session-root.txt\",\"content\":\"session root\"}"}}}}
{"type":"assistant/chunk","seq":10,"time":1784567324143,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":11,"time":1784567324143,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":12,"time":1784567324144,"data":{"turn":1,"step":1,"content":[{"type":"tool-call","id":"call_session_root","name":"write","arguments":"{\"file_path\":\"session-root.txt\",\"content\":\"session root\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[7,8,9,10,11],"surfaceOp":"append"}
{"type":"tool/call","seq":13,"time":1784567324145,"data":{"turn":1,"step":1,"callId":"call_session_root","name":"write","arguments":"{\"file_path\":\"session-root.txt\",\"content\":\"session root\"}"}}
{"type":"tool/result","seq":14,"time":1784567324155,"data":{"turn":1,"step":1,"callId":"call_session_root","content":[{"type":"text","text":"<path>/Users/cty/acp-snap-cwd-MABAjO/session-root.txt</path>\n<type>file</type>\n<content>\nCreated file\n</content>"}],"isError":false},"sourceEventSeqs":[13],"surfaceOp":"append"}
{"type":"step/end","seq":15,"time":1784567324157,"data":{"turn":1,"step":1}}
{"type":"step/start","seq":16,"time":1784567324157,"data":{"turn":1,"step":2}}
{"type":"assistant/chunk","seq":17,"time":1784567324157,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","seq":18,"time":1784567324157,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"DONE"}}}
{"type":"assistant/chunk","seq":19,"time":1784567324158,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}}}
{"type":"assistant/chunk","seq":20,"time":1784567324158,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}}
{"type":"assistant/chunk","seq":21,"time":1784567324158,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","seq":22,"time":1784567324158,"data":{"turn":1,"step":2,"content":[{"type":"text","text":"DONE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":2}},"sourceEventSeqs":[17,18,19,20,21],"surfaceOp":"append"}
{"type":"step/end","seq":23,"time":1784567324158,"data":{"turn":1,"step":2}}
{"type":"turn/end","seq":24,"time":1784567324158,"data":{"turn":1,"reason":{"kind":"completed"}}}

View File

@@ -0,0 +1,8 @@
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}}
{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"available_commands_update","availableCommands":[{"name":"goal","description":"set or view the goal for a long-running task","input":{"hint":"[<objective>|clear|edit <objective>|pause|resume]"}}]}}}
{"jsonrpc":"2.0","id":3,"result":{"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"workspace-write","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_session_root","title":"Write session-root.txt","kind":"edit","status":"in_progress","locations":[{"path":"session-root.txt"}],"content":[{"type":"diff","path":"session-root.txt","oldText":null,"newText":"session root"}]}}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"call_session_root","status":"completed","content":[{"type":"diff","path":"session-root.txt","oldText":null,"newText":"session root"}],"title":"Write session-root.txt"}}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"DONE"}}}}
{"jsonrpc":"2.0","id":4,"result":{"stopReason":"end_turn"}}