fix(pwsh): stamp the calling session's sandbox policy onto pwsh tool calls
This commit is contained in:
@@ -445,6 +445,7 @@ flowchart TD
|
|||||||
pkg_sandbox_local --> pkg_invariants
|
pkg_sandbox_local --> pkg_invariants
|
||||||
pkg_sandbox_local --> pkg_llm
|
pkg_sandbox_local --> pkg_llm
|
||||||
pkg_sandbox_local --> pkg_sandbox
|
pkg_sandbox_local --> pkg_sandbox
|
||||||
|
pkg_sandbox_local --> pkg_session
|
||||||
pkg_session_projection --> pkg_invariants
|
pkg_session_projection --> pkg_invariants
|
||||||
pkg_session_projection --> pkg_session
|
pkg_session_projection --> pkg_session
|
||||||
pkg_llm_retry --> pkg_agent
|
pkg_llm_retry --> pkg_agent
|
||||||
@@ -868,6 +869,8 @@ flowchart TD
|
|||||||
pkg_tool_pwsh --> pkg_bash_env
|
pkg_tool_pwsh --> pkg_bash_env
|
||||||
pkg_tool_pwsh --> pkg_invariants
|
pkg_tool_pwsh --> pkg_invariants
|
||||||
pkg_tool_pwsh --> pkg_llm
|
pkg_tool_pwsh --> pkg_llm
|
||||||
|
pkg_tool_pwsh --> pkg_sandbox
|
||||||
|
pkg_tool_pwsh --> pkg_sandbox_policy
|
||||||
pkg_tool_pwsh --> pkg_system_prompt
|
pkg_tool_pwsh --> pkg_system_prompt
|
||||||
pkg_tool_pwsh --> pkg_tasks
|
pkg_tool_pwsh --> pkg_tasks
|
||||||
pkg_tool_pwsh --> pkg_tools
|
pkg_tool_pwsh --> pkg_tools
|
||||||
@@ -1232,7 +1235,7 @@ flowchart TD
|
|||||||
| [`app-boot`](../packages/ui/app-boot) | `ui` | [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`system-prompt`](../packages/core/system-prompt) |
|
| [`app-boot`](../packages/ui/app-boot) | `ui` | [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`system-prompt`](../packages/core/system-prompt) |
|
||||||
| [`code-runtime-worker`](../packages/code-runtime/code-runtime-worker) | `code-runtime` | [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
|
| [`code-runtime-worker`](../packages/code-runtime/code-runtime-worker) | `code-runtime` | [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
|
||||||
| [`lsp-local`](../packages/lsp/lsp-local) | `lsp` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
| [`lsp-local`](../packages/lsp/lsp-local) | `lsp` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||||
| [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
|
| [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) |
|
||||||
| [`session-projection`](../packages/session-projection/session-projection) | `session-projection` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
|
| [`session-projection`](../packages/session-projection/session-projection) | `session-projection` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
|
||||||
| [`llm-retry`](../packages/llm/llm-retry) | `llm` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
|
| [`llm-retry`](../packages/llm/llm-retry) | `llm` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
|
||||||
| [`token-meter`](../packages/llm/token-meter) | `llm` | [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session-projection/session-projection) |
|
| [`token-meter`](../packages/llm/token-meter) | `llm` | [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session-projection/session-projection) |
|
||||||
@@ -1313,7 +1316,7 @@ flowchart TD
|
|||||||
| [`session-telemetry-otel`](../packages/telemetry/session-telemetry-otel) | `telemetry` | [`brand`](../packages/util/brand), [`command-feedback`](../packages/feedback/command-feedback), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`paths`](../packages/util/paths), [`session`](../packages/core/session), [`session-telemetry`](../packages/telemetry/session-telemetry) |
|
| [`session-telemetry-otel`](../packages/telemetry/session-telemetry-otel) | `telemetry` | [`brand`](../packages/util/brand), [`command-feedback`](../packages/feedback/command-feedback), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`paths`](../packages/util/paths), [`session`](../packages/core/session), [`session-telemetry`](../packages/telemetry/session-telemetry) |
|
||||||
| [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
|
| [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
|
||||||
| [`tool-bash`](../packages/bash/tool-bash) | `bash` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`bash-env`](../packages/bash/bash-env), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) |
|
| [`tool-bash`](../packages/bash/tool-bash) | `bash` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`bash-env`](../packages/bash/bash-env), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) |
|
||||||
| [`tool-pwsh`](../packages/bash/tool-pwsh) | `bash` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`bash-env`](../packages/bash/bash-env), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) |
|
| [`tool-pwsh`](../packages/bash/tool-pwsh) | `bash` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`bash-env`](../packages/bash/bash-env), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) |
|
||||||
| [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
| [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||||
| [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
| [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||||
| [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) |
|
| [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) |
|
||||||
|
|||||||
@@ -30,6 +30,8 @@
|
|||||||
"@deepseek-ai/dsh-bash-env": "^0.0.1",
|
"@deepseek-ai/dsh-bash-env": "^0.0.1",
|
||||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||||
"@deepseek-ai/dsh-llm": "^0.0.1",
|
"@deepseek-ai/dsh-llm": "^0.0.1",
|
||||||
|
"@deepseek-ai/dsh-sandbox": "^0.0.1",
|
||||||
|
"@deepseek-ai/dsh-sandbox-policy": "^0.0.1",
|
||||||
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
|
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
|
||||||
"@deepseek-ai/dsh-tasks": "^0.0.1",
|
"@deepseek-ai/dsh-tasks": "^0.0.1",
|
||||||
"@deepseek-ai/dsh-tools": "^0.0.1",
|
"@deepseek-ai/dsh-tools": "^0.0.1",
|
||||||
@@ -46,6 +48,8 @@
|
|||||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||||
"@deepseek-ai/dsh-loader-smoke": "workspace:^",
|
"@deepseek-ai/dsh-loader-smoke": "workspace:^",
|
||||||
"@deepseek-ai/dsh-pwsh-local": "workspace:^",
|
"@deepseek-ai/dsh-pwsh-local": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-sandbox": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-sandbox-policy": "workspace:^",
|
||||||
"@deepseek-ai/dsh-subprocess-local": "workspace:^",
|
"@deepseek-ai/dsh-subprocess-local": "workspace:^",
|
||||||
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||||
"@deepseek-ai/dsh-tasks": "workspace:^",
|
"@deepseek-ai/dsh-tasks": "workspace:^",
|
||||||
|
|||||||
@@ -4,11 +4,13 @@
|
|||||||
* `@deepseek-ai/dsh-pwsh-local`) backs `ctx.bash`; the tool contract is
|
* `@deepseek-ai/dsh-pwsh-local`) backs `ctx.bash`; the tool contract is
|
||||||
* PowerShell-dialect: native `C:\...` paths and `$env:NAME` variables.
|
* PowerShell-dialect: native `C:\...` paths and `$env:NAME` variables.
|
||||||
*
|
*
|
||||||
* Behavior mirrors `dsh-tool-bash` call-for-call minus the sandbox surface:
|
* Behavior mirrors `dsh-tool-bash` call-for-call minus the escalation
|
||||||
* foreground and `run_in_background` execution (background handles register
|
* surface: foreground and `run_in_background` execution (background handles
|
||||||
* with the generic `ctx.tasks` runtime), the managed `DSH_*` environment
|
* register with the generic `ctx.tasks` runtime), the managed `DSH_*`
|
||||||
* through the shared `bash-env` registry, and the bash marker/truncation
|
* environment through the shared `bash-env` registry, the per-call sandbox
|
||||||
* rendering story. UI presentation mirrors the bash tool's too: a completed
|
* policy resolution (the calling session's mode and cwd travel to the
|
||||||
|
* confining executor), and the bash marker/truncation rendering story. UI
|
||||||
|
* presentation mirrors the bash tool's too: a completed
|
||||||
* foreground call is a terminal card with the parsed exit-status pill, using
|
* foreground call is a terminal card with the parsed exit-status pill, using
|
||||||
* the shared exit-status parse from `@deepseek-ai/dsh-bash`.
|
* the shared exit-status parse from `@deepseek-ai/dsh-bash`.
|
||||||
*
|
*
|
||||||
@@ -19,12 +21,14 @@ import { isAbsolute, resolve as resolvePath } from 'node:path'
|
|||||||
import type { Context } from 'cordis'
|
import type { Context } from 'cordis'
|
||||||
import z from 'schemastery'
|
import z from 'schemastery'
|
||||||
import { defineTool, TOOL_ABORTED } from '@deepseek-ai/dsh-tools'
|
import { defineTool, TOOL_ABORTED } from '@deepseek-ai/dsh-tools'
|
||||||
import type { GenericCallView, TerminalCallView, ToolResult, ToolResultView } from '@deepseek-ai/dsh-tools'
|
import type { GenericCallView, TerminalCallView, ToolExecution, ToolResult, ToolResultView } from '@deepseek-ai/dsh-tools'
|
||||||
import { HarnessError } from '@deepseek-ai/dsh-llm'
|
import { HarnessError } from '@deepseek-ai/dsh-llm'
|
||||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||||
import type {} from '@deepseek-ai/dsh-tasks'
|
import type {} from '@deepseek-ai/dsh-tasks'
|
||||||
import type {} from '@deepseek-ai/dsh-bash-env'
|
import type {} from '@deepseek-ai/dsh-bash-env'
|
||||||
|
import type { SandboxExecutionPolicy } from '@deepseek-ai/dsh-sandbox'
|
||||||
|
import type { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy'
|
||||||
import type { BashRunResult } from '@deepseek-ai/dsh-bash'
|
import type { BashRunResult } from '@deepseek-ai/dsh-bash'
|
||||||
import { parseExitStatus } from '@deepseek-ai/dsh-bash'
|
import { parseExitStatus } from '@deepseek-ai/dsh-bash'
|
||||||
import { processOutcome } from './background.ts'
|
import { processOutcome } from './background.ts'
|
||||||
@@ -141,6 +145,14 @@ const BACKGROUND_OUTPUT_PROPERTIES = {
|
|||||||
|
|
||||||
export function apply(ctx: Context, config: Config = {}): void {
|
export function apply(ctx: Context, config: Config = {}): void {
|
||||||
const backgroundEnabled = config.enableRunInBackground ?? true
|
const backgroundEnabled = config.enableRunInBackground ?? true
|
||||||
|
const defaultMode = ctx.bash.sandboxMode
|
||||||
|
const sandboxPolicy: SandboxPolicyService | undefined = defaultMode === undefined ? undefined : ctx.get('sandboxPolicy')
|
||||||
|
if (defaultMode !== undefined && sandboxPolicy === undefined) {
|
||||||
|
throw new Error('tool-pwsh: the mounted bash executor confines but ctx.sandboxPolicy is missing')
|
||||||
|
}
|
||||||
|
/** Resolve the complete standing policy for this call when a confining executor is mounted. */
|
||||||
|
const resolveSandboxPolicy = (exec: ToolExecution): SandboxExecutionPolicy | undefined =>
|
||||||
|
sandboxPolicy?.resolve(exec.agent === undefined ? {} : { session: exec.agent.session })
|
||||||
|
|
||||||
ctx.systemPrompt.section({
|
ctx.systemPrompt.section({
|
||||||
name: 'tool:pwsh',
|
name: 'tool:pwsh',
|
||||||
@@ -224,12 +236,15 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|||||||
/* jscpd:ignore-start -- the execute path mirrors dsh-tool-bash's by design (see the pwsh-tool-and-executor Agent Note). */
|
/* jscpd:ignore-start -- the execute path mirrors dsh-tool-bash's by design (see the pwsh-tool-and-executor Agent Note). */
|
||||||
async execute(args: PwshToolArgs, exec) {
|
async execute(args: PwshToolArgs, exec) {
|
||||||
validatePwshArgs(args)
|
validatePwshArgs(args)
|
||||||
|
// Description is display metadata; workdir defaults to the caller's session.
|
||||||
|
const standingPolicy = resolveSandboxPolicy(exec)
|
||||||
const workdir = resolveWorkdir(args.workdir, exec)
|
const workdir = resolveWorkdir(args.workdir, exec)
|
||||||
const request = {
|
const request = {
|
||||||
command: args.command,
|
command: args.command,
|
||||||
...workdir !== undefined ? { workdir } : {},
|
...workdir !== undefined ? { workdir } : {},
|
||||||
...args.timeoutMs !== undefined ? { timeoutMs: args.timeoutMs } : {},
|
...args.timeoutMs !== undefined ? { timeoutMs: args.timeoutMs } : {},
|
||||||
dshEnv: ctx.bashEnv.collect(exec),
|
dshEnv: ctx.bashEnv.collect(exec),
|
||||||
|
...standingPolicy !== undefined ? { sandboxPolicy: standingPolicy } : {},
|
||||||
}
|
}
|
||||||
if (args.run_in_background === true) {
|
if (args.run_in_background === true) {
|
||||||
// Undeclared keys are allowed, so schema omission also needs enforcement.
|
// Undeclared keys are allowed, so schema omission also needs enforcement.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import { Context } from 'cordis'
|
import { Context } from 'cordis'
|
||||||
import { mkdtempSync } from 'node:fs'
|
import { mkdtempSync, realpathSync } from 'node:fs'
|
||||||
import { tmpdir } from 'node:os'
|
import { tmpdir } from 'node:os'
|
||||||
import { join, resolve as resolvePath } from 'node:path'
|
import { join, resolve as resolvePath } from 'node:path'
|
||||||
import { CallId } from '@deepseek-ai/dsh-llm'
|
import { CallId } from '@deepseek-ai/dsh-llm'
|
||||||
@@ -24,6 +24,7 @@ import type { Agent } from '@deepseek-ai/dsh-agent'
|
|||||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||||
import { BashExecutor } from '@deepseek-ai/dsh-bash'
|
import { BashExecutor } from '@deepseek-ai/dsh-bash'
|
||||||
import type { BashExecRequest, BashExecSpec, BashProcess, BashRunResult } from '@deepseek-ai/dsh-bash'
|
import type { BashExecRequest, BashExecSpec, BashProcess, BashRunResult } from '@deepseek-ai/dsh-bash'
|
||||||
|
import SandboxPolicyService from '@deepseek-ai/dsh-sandbox-policy'
|
||||||
import * as ToolPwsh from '@deepseek-ai/dsh-tool-pwsh'
|
import * as ToolPwsh from '@deepseek-ai/dsh-tool-pwsh'
|
||||||
import * as BashEnvPlugin from '@deepseek-ai/dsh-bash-env'
|
import * as BashEnvPlugin from '@deepseek-ai/dsh-bash-env'
|
||||||
import type { BashProcessRead } from '@deepseek-ai/dsh-bash'
|
import type { BashProcessRead } from '@deepseek-ai/dsh-bash'
|
||||||
@@ -150,9 +151,59 @@ async function setupWithTasks(toolConfig: Partial<ToolPwsh.Config> = {}, dshHome
|
|||||||
return { ctx, bash }
|
return { ctx, bash }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A CONFINING fake executor (`sandboxMode` advertised): the tool must resolve
|
||||||
|
* the calling session's standing policy and stamp it on the request, exactly
|
||||||
|
* like the bash tool — the per-session sandbox-policy regression surface.
|
||||||
|
*/
|
||||||
|
class ConfiningFakeBash extends BashExecutor {
|
||||||
|
requests: BashExecRequest[] = []
|
||||||
|
|
||||||
|
override get sandboxMode() {
|
||||||
|
return 'read-only' as const
|
||||||
|
}
|
||||||
|
|
||||||
|
override resolve(request: BashExecRequest): BashExecSpec {
|
||||||
|
this.requests.push(request)
|
||||||
|
return {
|
||||||
|
command: request.command,
|
||||||
|
workdir: request.workdir ?? process.cwd(),
|
||||||
|
timeoutMs: request.timeoutMs ?? 60_000,
|
||||||
|
stdoutMaxBytes: request.stdoutMaxBytes ?? 64_000,
|
||||||
|
...request.signal ? { signal: request.signal } : {},
|
||||||
|
...request.dshEnv !== undefined ? { dshEnv: request.dshEnv } : {},
|
||||||
|
sandboxPolicy: request.sandboxPolicy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override async run(_spec: BashExecSpec): Promise<BashRunResult> {
|
||||||
|
return runResult('ok\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
override start(_spec: BashExecSpec): BashProcess {
|
||||||
|
return fakeProcess()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sandboxed composition: the shared policy service + a confining executor + the pwsh tool. */
|
||||||
|
async function setupSandboxed(toolConfig: Partial<ToolPwsh.Config> = {}) {
|
||||||
|
const ctx = new Context()
|
||||||
|
await ctx.plugin(SystemPrompt)
|
||||||
|
await ctx.plugin(ToolRegistry)
|
||||||
|
await ctx.plugin(AgentRegistry)
|
||||||
|
await ctx.plugin(BashEnvPlugin)
|
||||||
|
await ctx.plugin(SandboxPolicyService, {})
|
||||||
|
await ctx.plugin(ConfiningFakeBash)
|
||||||
|
await ctx.plugin(ToolPwsh, toolConfig)
|
||||||
|
const bash = ctx.bash as ConfiningFakeBash
|
||||||
|
return { ctx, bash }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a fake {@link Agent} with the shared agent/session identity, give it a
|
* Build a fake {@link Agent} with the shared agent/session identity, give it a
|
||||||
* dedicated lifecycle fiber for `Agent.ctx`, and register it in `ctx.agents`.
|
* dedicated lifecycle fiber for `Agent.ctx`, and register it in `ctx.agents`.
|
||||||
|
* The fake session carries an empty event log (the sandbox-policy resolver
|
||||||
|
* folds the log for mode overrides, mirroring a real session).
|
||||||
*/
|
*/
|
||||||
function registerFakeAgent(ctx: Context, sessionId: string): Agent {
|
function registerFakeAgent(ctx: Context, sessionId: string): Agent {
|
||||||
const scopeFiber = ctx.plugin(() => {})
|
const scopeFiber = ctx.plugin(() => {})
|
||||||
@@ -160,7 +211,7 @@ function registerFakeAgent(ctx: Context, sessionId: string): Agent {
|
|||||||
const agent = {
|
const agent = {
|
||||||
id,
|
id,
|
||||||
ctx: scopeFiber.ctx,
|
ctx: scopeFiber.ctx,
|
||||||
session: { id, header: { version: 0, id, createdAt: 0 } },
|
session: { id, header: { version: 0, id, createdAt: 0 }, events: [] },
|
||||||
} as unknown as Agent
|
} as unknown as Agent
|
||||||
ctx.agents.register(agent)
|
ctx.agents.register(agent)
|
||||||
return agent
|
return agent
|
||||||
@@ -397,6 +448,52 @@ describe('execution through the bash seam', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('per-call sandbox policy resolution', () => {
|
||||||
|
it('stamps the CALLING SESSION\'s resolved policy onto the request (session cwd, not the server launch dir)', async () => {
|
||||||
|
const { ctx, bash } = await setupSandboxed()
|
||||||
|
const sessionCwd = mkdtempSync(join(tmpdir(), 'dsh-tool-pwsh-policy-'))
|
||||||
|
const agent = registerFakeAgent(ctx, 'policy-session')
|
||||||
|
Object.assign(agent.session.header, { cwd: sessionCwd })
|
||||||
|
const result = await call(ctx, 'pwsh', { command: 'Write-Output hi', description: 'say hi' }, agent)
|
||||||
|
expect(result.isError).toBe(false)
|
||||||
|
// The policy's workspace root is the session cwd canonicalized by the
|
||||||
|
// policy service (realpath + resolve), NEVER the web server's launch dir;
|
||||||
|
// the calling session's identity rides along for backend per-session state.
|
||||||
|
expect(bash.requests[0]?.sandboxPolicy).toEqual({
|
||||||
|
mode: 'read-only',
|
||||||
|
workspaceRoot: resolvePath(realpathSync.native(sessionCwd)),
|
||||||
|
sessionId: 'policy-session',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls back to the deployment policy without an agent, and omits the field entirely without a confining executor', async () => {
|
||||||
|
const { ctx, bash } = await setupSandboxed()
|
||||||
|
await call(ctx, 'pwsh', { command: 'Write-Output hi', description: 'say hi' })
|
||||||
|
expect(bash.requests[0]?.sandboxPolicy).toEqual({
|
||||||
|
mode: 'read-only',
|
||||||
|
workspaceRoot: resolvePath(realpathSync.native(process.cwd())),
|
||||||
|
})
|
||||||
|
|
||||||
|
// The base FakeBash advertises no sandboxMode, so the tool must not stamp
|
||||||
|
// any policy (the executor defaulting stays the executor's own).
|
||||||
|
const plain = await setup()
|
||||||
|
await call(plain.ctx, 'pwsh', { command: 'Write-Output hi', description: 'say hi' })
|
||||||
|
expect(plain.bash.requests[0]).not.toHaveProperty('sandboxPolicy')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fails load when a confining executor has no shared sandbox-policy resolver', async () => {
|
||||||
|
const ctx = new Context()
|
||||||
|
await ctx.plugin(SystemPrompt)
|
||||||
|
await ctx.plugin(ToolRegistry)
|
||||||
|
await ctx.plugin(AgentRegistry)
|
||||||
|
await ctx.plugin(BashEnvPlugin)
|
||||||
|
await ctx.plugin(ConfiningFakeBash)
|
||||||
|
await expect(ctx.plugin(ToolPwsh)).rejects.toThrow(
|
||||||
|
'tool-pwsh: the mounted bash executor confines but ctx.sandboxPolicy is missing',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('background execution through the task runtime', () => {
|
describe('background execution through the task runtime', () => {
|
||||||
it('run_in_background acks with the task id, readable through the REAL task_output tool', async () => {
|
it('run_in_background acks with the task id, readable through the REAL task_output tool', async () => {
|
||||||
const { ctx } = await setupWithTasks()
|
const { ctx } = await setupWithTasks()
|
||||||
|
|||||||
@@ -38,6 +38,12 @@
|
|||||||
{
|
{
|
||||||
"path": "../../core/system-prompt"
|
"path": "../../core/system-prompt"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "../../sandbox/sandbox"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../sandbox/sandbox-policy"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "../../support/invariants"
|
"path": "../../support/invariants"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user