feat(agent): session sandbox, system prompt, and tool restrictions (#134)
Some checks failed
ci/woodpecker/push/ci Pipeline failed
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:
@@ -12,3 +12,33 @@ export interface SessionListDto {
|
|||||||
sessions: SessionInfoDto[];
|
sessions: SessionInfoDto[];
|
||||||
total: number;
|
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[];
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Type } from '@sinclair/typebox';
|
import { Type } from '@sinclair/typebox';
|
||||||
import type { ToolDefinition } from '@mariozechner/pi-coding-agent';
|
import type { ToolDefinition } from '@mariozechner/pi-coding-agent';
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
|
import { resolve, relative } from 'node:path';
|
||||||
|
|
||||||
const DEFAULT_TIMEOUT_MS = 30_000;
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
||||||
const MAX_OUTPUT_BYTES = 100 * 1024; // 100 KB
|
const MAX_OUTPUT_BYTES = 100 * 1024; // 100 KB
|
||||||
@@ -67,6 +68,22 @@ function extractBaseCommand(command: string): string {
|
|||||||
return firstToken.split('/').pop() ?? firstToken;
|
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(
|
function runCommand(
|
||||||
command: string,
|
command: string,
|
||||||
options: { timeoutMs: number; cwd?: 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 = {
|
const shellExec: ToolDefinition = {
|
||||||
name: 'shell_exec',
|
name: 'shell_exec',
|
||||||
label: 'Shell Execute',
|
label: 'Shell Execute',
|
||||||
description:
|
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({
|
parameters: Type.Object({
|
||||||
command: Type.String({ description: 'Shell command to execute' }),
|
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(
|
timeout: Type.Optional(
|
||||||
Type.Number({ description: 'Timeout in milliseconds (default 30000, max 60000)' }),
|
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 timeoutMs = Math.min(timeout ?? DEFAULT_TIMEOUT_MS, 60_000);
|
||||||
|
const safeCwd = clampCwd(defaultCwd, cwd);
|
||||||
|
|
||||||
const result = await runCommand(command, {
|
const result = await runCommand(command, {
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
cwd: cwd ?? defaultCwd,
|
cwd: safeCwd,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.timedOut) {
|
if (result.timedOut) {
|
||||||
|
|||||||
Reference in New Issue
Block a user