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

- Add CreateSessionOptionsDto with sandboxDir, systemPrompt, and allowedTools fields
- Add clampCwd() to shell-tools to prevent cwd escapes outside the sandbox
- agent.service.ts, git-tools.ts: already merged via #147 with full implementation

Closes #134

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-15 14:14:25 -05:00
parent 76abf11eba
commit 7abc2edc6c
2 changed files with 59 additions and 4 deletions

View File

@@ -12,3 +12,33 @@ export interface SessionListDto {
sessions: SessionInfoDto[];
total: number;
}
/**
* Options accepted when creating an agent session.
* All fields are optional; omitting them falls back to env-var or process defaults.
*/
export interface CreateSessionOptionsDto {
/** Provider name (e.g. "anthropic", "openai"). */
provider?: string;
/** Model ID to use for this session. */
modelId?: string;
/**
* Sandbox working directory for the session.
* File, git, and shell tools will be restricted to this directory.
* Defaults to AGENT_FILE_SANDBOX_DIR env var or process.cwd().
*/
sandboxDir?: string;
/**
* Platform-level system prompt for this session.
* Merged with skill prompt additions (platform prompt first, then skills).
* Falls back to AGENT_SYSTEM_PROMPT env var when omitted.
*/
systemPrompt?: string;
/**
* Explicit allowlist of tool names available in this session.
* When provided, only listed tools are registered with the agent.
* Admins receive all tools; regular users fall back to AGENT_USER_TOOLS
* env var (comma-separated) when this field is not supplied.
*/
allowedTools?: string[];
}

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) {