From d1bef49b4e0ee6c2cc381e8272430f81df4a001c Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sun, 15 Mar 2026 19:15:05 +0000 Subject: [PATCH] feat(agent): session cwd sandbox, system prompt config, tool restrictions (#148) Co-authored-by: Jason Woltje Co-committed-by: Jason Woltje --- apps/gateway/src/agent/session.dto.ts | 30 +++++++++++++++++++ apps/gateway/src/agent/tools/shell-tools.ts | 33 ++++++++++++++++++--- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/apps/gateway/src/agent/session.dto.ts b/apps/gateway/src/agent/session.dto.ts index 32865c3..d4a372c 100644 --- a/apps/gateway/src/agent/session.dto.ts +++ b/apps/gateway/src/agent/session.dto.ts @@ -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[]; +} diff --git a/apps/gateway/src/agent/tools/shell-tools.ts b/apps/gateway/src/agent/tools/shell-tools.ts index 6e801e5..18cd6e0 100644 --- a/apps/gateway/src/agent/tools/shell-tools.ts +++ b/apps/gateway/src/agent/tools/shell-tools.ts @@ -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) {