feat(security): enforce elevated mode and sandbox execution
This commit is contained in:
+43
-7
@@ -7,6 +7,8 @@ import { auditLogger } from '../audit/index.js';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { matchesAnyPattern, patternToRegex } from './policy.js';
|
||||
import { redactForAudit, containsSecretLikeKeys } from '../audit/redact.js';
|
||||
import type { SandboxManager } from '../sandbox/index.js';
|
||||
import { createSandboxedProcessStartTool, createSandboxedShellTool } from '../sandbox/index.js';
|
||||
|
||||
export interface ToolExecutorConfig {
|
||||
defaultTimeoutMs?: number;
|
||||
@@ -18,6 +20,7 @@ export class ToolExecutor {
|
||||
private hooks: HookEngine;
|
||||
private defaultTimeoutMs: number;
|
||||
private maxOutputBytes: number;
|
||||
private sandboxManager?: SandboxManager;
|
||||
|
||||
constructor(registry: ToolRegistry, hooks: HookEngine, config?: ToolExecutorConfig) {
|
||||
this.registry = registry;
|
||||
@@ -26,9 +29,26 @@ export class ToolExecutor {
|
||||
this.maxOutputBytes = config?.maxOutputBytes ?? 51_200;
|
||||
}
|
||||
|
||||
setSandboxManager(manager?: SandboxManager): void {
|
||||
this.sandboxManager = manager;
|
||||
}
|
||||
|
||||
private isElevationActive(context?: ToolPolicyContext): boolean {
|
||||
const untilMs = context?.elevatedHostUntilMs;
|
||||
return typeof untilMs === 'number' && Number.isFinite(untilMs) && untilMs > Date.now();
|
||||
}
|
||||
|
||||
private resolveEffectiveExecutionEnvironment(toolName: string, context?: ToolPolicyContext): 'host' | 'sandbox' {
|
||||
const base = context?.executionEnvironment ?? 'host';
|
||||
if (this.isHighRiskTool(toolName) && this.isElevationActive(context)) {
|
||||
return 'host';
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
async execute(toolName: string, args: unknown, context?: ToolPolicyContext): Promise<ToolResult> {
|
||||
const executionId = randomUUID();
|
||||
const executionEnvironment = context?.executionEnvironment;
|
||||
const executionEnvironment = this.resolveEffectiveExecutionEnvironment(toolName, context);
|
||||
const skillName = context?.skillName;
|
||||
|
||||
const tool = this.registry.getByApiName(toolName);
|
||||
@@ -69,7 +89,7 @@ export class ToolExecutor {
|
||||
}
|
||||
|
||||
// Capability enforcement: filesystem + network constraints
|
||||
const capabilityViolation = this.checkCapabilityConstraints(tool.name, args, context);
|
||||
const capabilityViolation = this.checkCapabilityConstraints(tool.name, args, context, executionEnvironment);
|
||||
if (capabilityViolation) {
|
||||
auditLogger?.toolDenied({
|
||||
tool_name: tool.name,
|
||||
@@ -127,7 +147,12 @@ export class ToolExecutor {
|
||||
const baseAction = this.hooks.getAction(toolName);
|
||||
const autonomyLevel = context?.autonomyLevel ?? 'standard';
|
||||
const autonomyDecision = resolveAutonomy(toolName, baseAction, autonomyLevel);
|
||||
const finalAction = autonomyDecision.action;
|
||||
let finalAction = autonomyDecision.action;
|
||||
|
||||
// Elevated mode must always require explicit confirmation for host high-risk tool calls.
|
||||
if (executionEnvironment === 'host' && this.isHighRiskTool(toolName) && this.isElevationActive(context)) {
|
||||
finalAction = 'confirm';
|
||||
}
|
||||
|
||||
// Log autonomy override if applicable
|
||||
if (autonomyDecision.overridden) {
|
||||
@@ -201,7 +226,19 @@ export class ToolExecutor {
|
||||
|
||||
try {
|
||||
const result = await Promise.race([
|
||||
tool.execute(args),
|
||||
(async () => {
|
||||
if (executionEnvironment === 'sandbox' && this.sandboxManager) {
|
||||
const sandboxSessionId = context?.sessionId ?? `${context?.channel ?? 'unknown'}:${context?.sender ?? 'unknown'}`;
|
||||
const sandbox = await this.sandboxManager.getOrCreate(sandboxSessionId);
|
||||
if (toolName === 'shell.exec') {
|
||||
return createSandboxedShellTool(sandbox).execute(args);
|
||||
}
|
||||
if (toolName === 'process.start') {
|
||||
return createSandboxedProcessStartTool(sandbox).execute(args);
|
||||
}
|
||||
}
|
||||
return tool.execute(args);
|
||||
})(),
|
||||
new Promise<ToolResult>((_, reject) =>
|
||||
setTimeout(() => reject(new Error(`Tool '${toolName}' timed out after ${this.defaultTimeoutMs}ms`)), this.defaultTimeoutMs),
|
||||
),
|
||||
@@ -286,7 +323,7 @@ export class ToolExecutor {
|
||||
].includes(toolName);
|
||||
}
|
||||
|
||||
private checkCapabilityConstraints(toolName: string, args: unknown, context?: ToolPolicyContext): string | null {
|
||||
private checkCapabilityConstraints(toolName: string, args: unknown, context: ToolPolicyContext | undefined, effectiveEnv: 'host' | 'sandbox'): string | null {
|
||||
const perms = context?.skillPermissions;
|
||||
if (!perms) {
|
||||
if (context?.skillName && this.isHighRiskTool(toolName)) {
|
||||
@@ -297,9 +334,8 @@ export class ToolExecutor {
|
||||
|
||||
// Sandbox enforcement for high-risk tools unless explicitly allowed.
|
||||
if (this.isHighRiskTool(toolName)) {
|
||||
const env = context?.executionEnvironment ?? 'host';
|
||||
const requested = perms.execution_environment ?? 'sandbox';
|
||||
if (context?.skillName && env === 'host' && requested !== 'host') {
|
||||
if (context?.skillName && effectiveEnv === 'host' && requested !== 'host' && !this.isElevationActive(context)) {
|
||||
return 'high-risk tool execution on host is not allowed for this skill (requires execution_environment=host)';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user