feat(agent): session cwd sandbox, system prompt config, tool restrictions (#148)
Some checks failed
ci/woodpecker/push/ci Pipeline failed

Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #148.
This commit is contained in:
2026-03-15 19:15:05 +00:00
committed by jason.woltje
parent 76abf11eba
commit d1bef49b4e
2 changed files with 59 additions and 4 deletions

View File

@@ -1,6 +1,7 @@
import { Type } from '@sinclair/typebox';
import type { ToolDefinition } from '@mariozechner/pi-coding-agent';
import { spawn } from 'node:child_process';
import { resolve, relative } from 'node:path';
const DEFAULT_TIMEOUT_MS = 30_000;
const MAX_OUTPUT_BYTES = 100 * 1024; // 100 KB
@@ -67,6 +68,22 @@ function extractBaseCommand(command: string): string {
return firstToken.split('/').pop() ?? firstToken;
}
/**
* Clamp a user-supplied cwd to within the sandbox directory.
* If the resolved path escapes the sandbox (via ../ or absolute path outside),
* falls back to the sandbox directory itself.
*/
function clampCwd(sandboxDir: string, requestedCwd?: string): string {
if (!requestedCwd) return sandboxDir;
const resolved = resolve(sandboxDir, requestedCwd);
const rel = relative(sandboxDir, resolved);
if (rel.startsWith('..') || rel.startsWith('/')) {
// Escape attempt — fall back to sandbox root
return sandboxDir;
}
return resolved;
}
function runCommand(
command: string,
options: { timeoutMs: number; cwd?: string },
@@ -127,15 +144,22 @@ function runCommand(
});
}
export function createShellTools(defaultCwd?: string): ToolDefinition[] {
export function createShellTools(sandboxDir?: string): ToolDefinition[] {
const defaultCwd = sandboxDir ?? process.cwd();
const shellExec: ToolDefinition = {
name: 'shell_exec',
label: 'Shell Execute',
description:
'Execute a shell command with timeout and output limits. Dangerous commands (rm, sudo, docker, etc.) are blocked.',
'Execute a shell command with timeout and output limits. Dangerous commands (rm, sudo, docker, etc.) are blocked. Working directory is restricted to the session sandbox.',
parameters: Type.Object({
command: Type.String({ description: 'Shell command to execute' }),
cwd: Type.Optional(Type.String({ description: 'Working directory for the command.' })),
cwd: Type.Optional(
Type.String({
description:
'Working directory for the command (relative to sandbox or absolute within it).',
}),
),
timeout: Type.Optional(
Type.Number({ description: 'Timeout in milliseconds (default 30000, max 60000)' }),
),
@@ -161,10 +185,11 @@ export function createShellTools(defaultCwd?: string): ToolDefinition[] {
}
const timeoutMs = Math.min(timeout ?? DEFAULT_TIMEOUT_MS, 60_000);
const safeCwd = clampCwd(defaultCwd, cwd);
const result = await runCommand(command, {
timeoutMs,
cwd: cwd ?? defaultCwd,
cwd: safeCwd,
});
if (result.timedOut) {