fix(pwsh): stamp the calling session's sandbox policy onto pwsh tool calls
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import { mkdtempSync } from 'node:fs'
|
||||
import { mkdtempSync, realpathSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve as resolvePath } from 'node:path'
|
||||
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 { BashExecutor } 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 BashEnvPlugin from '@deepseek-ai/dsh-bash-env'
|
||||
import type { BashProcessRead } from '@deepseek-ai/dsh-bash'
|
||||
@@ -150,9 +151,59 @@ async function setupWithTasks(toolConfig: Partial<ToolPwsh.Config> = {}, dshHome
|
||||
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
|
||||
* 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 {
|
||||
const scopeFiber = ctx.plugin(() => {})
|
||||
@@ -160,7 +211,7 @@ function registerFakeAgent(ctx: Context, sessionId: string): Agent {
|
||||
const agent = {
|
||||
id,
|
||||
ctx: scopeFiber.ctx,
|
||||
session: { id, header: { version: 0, id, createdAt: 0 } },
|
||||
session: { id, header: { version: 0, id, createdAt: 0 }, events: [] },
|
||||
} as unknown as Agent
|
||||
ctx.agents.register(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', () => {
|
||||
it('run_in_background acks with the task id, readable through the REAL task_output tool', async () => {
|
||||
const { ctx } = await setupWithTasks()
|
||||
|
||||
Reference in New Issue
Block a user