fix(sandbox): preserve filesystem cwd semantics

This commit is contained in:
Tianyi Cui
2026-07-21 19:53:02 +08:00
parent 7677d49c93
commit ae98294ec5
14 changed files with 119 additions and 27 deletions

View File

@@ -17,12 +17,12 @@ The plugin also contributes the `tool:bash` prompt section (order 105): check th
| `command` | string (required) | Run via `bash -c`. No state persists between calls — use `workdir`, not `cd`. |
| `description` | string (required) | One-line, active-voice summary of the command (5-10 words), for UI/log display only — no effect on execution. |
| `timeoutMs` | number | Timeout override in milliseconds. The executor applies its configured default and cap. |
| `workdir` | string | Working directory for this call. Defaults to the calling agent's session cwd (`session.header.cwd`) so each session runs in its own workspace; a relative `workdir` is resolved against that session cwd. |
| `workdir` | string | Working directory for this call. Defaults to the filesystem identity of the calling agent's session cwd (`session.header.cwd`) so each session runs in its own workspace; a relative `workdir` is resolved against that same identity. |
| `run_in_background` | boolean | Return a task id immediately; no timeout applies. |
| `sandbox_permissions` | string enum | ADVERTISED ONLY when the mounted executor sandboxes (`ctx.bash.sandboxMode` reports a confining default): the wider mode a denied command needs, from the closed target vocabulary `workspace-write`/`danger-full-access` (never cut down to the executor's default — the effective mode is per-session; strict widening is checked at execution against it, and a non-widening request fails without prompting anyone). |
| `justification` | string | Required together with `sandbox_permissions` (each without the other is a validation error): one sentence for the user explaining why this exact command needs the wider access. |
`command`, `workdir`, and `timeoutMs` are resolved against the executor's config defaults via `ctx.bash.resolve()` before execution, so the executor seam (`BashExecSpec`) receives explicit `workdir`/`timeoutMs` values. The workdir default is applied in the tool layer (from the calling agent's `session.header.cwd`) BEFORE `resolve()` — the per-session cwd must come from `exec.agent`, since N sessions share one executor; only when no session cwd is available does the executor fall back to its own config / `process.cwd()`.
`command`, `workdir`, and `timeoutMs` are resolved against the executor's config defaults via `ctx.bash.resolve()` before execution, so the executor seam (`BashExecSpec`) receives explicit `workdir`/`timeoutMs` values. The workdir default is applied in the tool layer from the calling agent's `session.header.cwd` BEFORE `resolve()` — the per-session cwd must come from `exec.agent`, since N sessions share one executor; only when no session cwd is available does the executor fall back to its own config / `process.cwd()`. When sandbox policy is present, the tool reuses its already-canonical `workspaceRoot` as the workdir base so confinement and process launch cannot resolve the same session spelling differently.
### Managed shell environment

View File

@@ -19,7 +19,7 @@ import type {} from '@deepseek-ai/dsh-system-prompt'
import type {} from '@deepseek-ai/dsh-tasks'
import type {} from '@deepseek-ai/dsh-user-approval'
import type { SandboxExecutionPolicy, SandboxMode } from '@deepseek-ai/dsh-sandbox'
import { ESCALATION_TARGETS, approveEscalation, validateEscalationArgs } from '@deepseek-ai/dsh-sandbox'
import { ESCALATION_TARGETS, approveEscalation, canonicalPath, validateEscalationArgs } from '@deepseek-ai/dsh-sandbox'
import type { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy'
import { DSH_ENV_PREFIX } from '@deepseek-ai/dsh-bash'
import type { DshEnvironment, DshEnvironmentKey } from '@deepseek-ai/dsh-bash'
@@ -299,11 +299,18 @@ function presentBashResult(args: unknown, result: ToolResult): ToolResultView |
}
/**
* Resolve an explicit workdir first, making a relative one session-cwd-relative;
* otherwise use the session cwd and leave executor defaulting as the fallback.
* Resolve an explicit workdir first, making a relative one session-workspace-relative;
* otherwise use the filesystem identity of the session cwd and leave executor
* defaulting as the fallback. A resolved sandbox-policy root wins so workdir
* and confinement use the exact same per-call identity.
*/
function resolveWorkdir(modelWorkdir: string | undefined, exec: { agent?: Agent }): string | undefined {
const sessionCwd = exec.agent?.session.header.cwd
function resolveWorkdir(
modelWorkdir: string | undefined,
exec: { agent?: Agent },
policyWorkspaceRoot?: string,
): string | undefined {
const headerCwd = exec.agent?.session.header.cwd
const sessionCwd = policyWorkspaceRoot ?? (headerCwd === undefined ? undefined : canonicalPath(headerCwd))
if (modelWorkdir === undefined) return sessionCwd
if (sessionCwd !== undefined && !isAbsolute(modelWorkdir)) {
return resolvePath(sessionCwd, modelWorkdir)
@@ -418,7 +425,7 @@ export function apply(ctx: Context, config: Config = {}): void {
const policy = approvedMode === undefined
? standingPolicy
: { ...(standingPolicy as SandboxExecutionPolicy), mode: approvedMode }
const workdir = resolveWorkdir(args.workdir, exec)
const workdir = resolveWorkdir(args.workdir, exec, standingPolicy?.workspaceRoot)
const dshEnv = bashEnv.collect(exec)
const request = {
command: args.command,