Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,966 changes: 1,899 additions & 67 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,16 @@
"node": ">=18.0.0"
},
"peerDependencies": {
"@ai-sdk/anthropic": "^0.0.42",
"@ai-sdk/openai": "^0.0.42",
"ai": "^4.3.19",
"ollama-ai-provider": "^1.2.0",
"zod": "^3.25.76"
},
"peerDependenciesMeta": {
"@ai-sdk/anthropic": {
"optional": true
},
"@ai-sdk/openai": {
"optional": true
},
Expand All @@ -96,13 +100,17 @@
}
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.30.1",
"dockerode": "^5.0.1",
"nanoid": "^5.0.7",
"undici": "^8.11.2"
},
"devDependencies": {
"@ai-sdk/anthropic": "^0.0.42",
"@opentelemetry/api": "^1.9.1",
"@opentelemetry/sdk-trace-base": "^2.11.0",
"@opentelemetry/sdk-trace-node": "^2.11.0",
"@types/dockerode": "^4.0.1",
"@types/node": "^20",
"@typescript-eslint/eslint-plugin": "^8.70.1",
"@typescript-eslint/parser": "^8.70.1",
Expand Down
37 changes: 31 additions & 6 deletions src/execution/AgentExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import { nanoid } from 'nanoid';
import { LLMProvider, Message, ToolCall, GenerateOptions, GenerateResult } from '../providers';
import { AgentConfig } from '../types';
import { ToolRegistry } from '../tools';
import { SandboxAdapter, NoopSandbox } from '../security/sandbox';
import { ApprovalStore, ExecutionSnapshot, PendingApproval } from './ApprovalGate';
import { Checkpoint, CheckpointStore } from './checkpoint';
import { TraceExporter, withSpan } from './tracing';
import { executeToolWithSandboxGuard } from './sandboxGuard';

/**
* Base class for tool errors that must NOT be swallowed by
Expand Down Expand Up @@ -161,6 +163,14 @@ export interface ExecuteOptions {
* redacted. Defaults to false.
*/
redactContent?: boolean;
/**
* SandboxAdapter used for tools flagged `requiresSandbox` (LOU-F5). Since
* AgentExecutor is a static, instance-free API, this is read per-call
* (`options.sandbox ?? NoopSandbox`) rather than held as construction
* state. Defaults to NoopSandbox - the zero-isolation, trusted-host
* adapter - when omitted, so existing callers see no behavior change.
*/
sandbox?: SandboxAdapter;
}

/**
Expand Down Expand Up @@ -236,6 +246,7 @@ export class AgentExecutor {
onToolResult,
exporter,
redactContent = false,
sandbox = NoopSandbox,
} = options;

// Emit start event
Expand Down Expand Up @@ -374,7 +385,8 @@ export class AgentExecutor {
agent,
toolRegistry,
onToolCall,
onToolResult
onToolResult,
sandbox
);
let parsedArgs: unknown = executed.args;
if (parsedArgs === undefined) {
Expand Down Expand Up @@ -584,7 +596,8 @@ export class AgentExecutor {
_agent: AgentConfig,
toolRegistry?: ToolRegistry,
onToolCall?: ExecuteOptions['onToolCall'],
onToolResult?: ExecuteOptions['onToolResult']
onToolResult?: ExecuteOptions['onToolResult'],
sandbox: SandboxAdapter = NoopSandbox
): Promise<{
toolCallId: string;
toolName: string;
Expand All @@ -611,7 +624,7 @@ export class AgentExecutor {
let thrown: unknown;

try {
outcome = await this.doExecuteToolCall(toolCall, toolRegistry);
outcome = await this.doExecuteToolCall(toolCall, toolRegistry, sandbox);
return outcome;
} catch (error) {
thrown = error;
Expand All @@ -631,7 +644,8 @@ export class AgentExecutor {
*/
private static async doExecuteToolCall(
toolCall: ToolCall,
toolRegistry?: ToolRegistry
toolRegistry?: ToolRegistry,
sandbox: SandboxAdapter = NoopSandbox
): Promise<{
toolCallId: string;
toolName: string;
Expand Down Expand Up @@ -677,8 +691,19 @@ export class AgentExecutor {
};
}

// The 'ai' SDK tool.execute expects (args, context)
const result = await toolDesc.tool.execute(args, {} as any);
// The 'ai' SDK tool.execute expects (args, context). Tools flagged
// `requiresSandbox` (LOU-F5) are routed through the configured
// SandboxAdapter instead of being invoked directly here; a tool
// WITHOUT the flag takes this exact, unchanged branch. This
// branching now lives in the shared executeToolWithSandboxGuard()
// helper (LOU-F fix) so FlowExecutor.ts and resume.ts share the
// exact same fail-closed behavior instead of each reimplementing it.
const result = await executeToolWithSandboxGuard(
toolCall.function.name,
toolDesc,
args,
sandbox
);

return {
toolCallId: toolCall.id,
Expand Down
17 changes: 15 additions & 2 deletions src/execution/resume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { ToolRegistry } from '../tools';
import { ApprovalDecision, ApprovalStore } from './ApprovalGate';
import { AgentExecutor, ExecuteOptions, ExecutionResult, PropagatingToolError } from './AgentExecutor';
import { CheckpointStore } from './checkpoint';
import { NoopSandbox } from '../security/sandbox';
import { executeToolWithSandboxGuard } from './sandboxGuard';

/**
* Options passed through to the underlying AgentExecutor.execute() call
Expand Down Expand Up @@ -82,12 +84,23 @@ export async function resumeAfterApproval(

if (decision.approved) {
const toolDesc = toolRegistry.get(pending.toolName);
if (!toolDesc || !toolDesc.tool || !toolDesc.tool.execute) {
// A `requiresSandbox` tool may have no `tool.execute` implementation at
// all - its real work happens in `sandboxExecute()` instead - so the
// "not found" guard below must not reject that case outright; it only
// means there is genuinely no way to run the tool (neither a direct
// `execute` nor a `sandboxExecute`).
if (!toolDesc || !toolDesc.tool || (!toolDesc.tool.execute && !toolDesc.sandboxExecute)) {
throw new Error(`Tool '${pending.toolName}' not found in registry`);
}

try {
const result = await toolDesc.tool.execute(pending.args, {} as any);
// Mirrors AgentExecutor.executeToolCall()'s fail-closed handling of
// `requiresSandbox` tools (LOU-F5) via the shared
// executeToolWithSandboxGuard() helper (LOU-F fix), so a deferred
// tool executed after human approval can't silently bypass the
// sandbox seam the way it previously did.
const sandbox = executeOptions.sandbox ?? NoopSandbox;
const result = await executeToolWithSandboxGuard(pending.toolName, toolDesc, pending.args, sandbox);

messages.push({
role: 'tool',
Expand Down
Loading
Loading