const std = @import("std"); const builtin = @import("builtin"); const agent_stream_provider = @import("../agent/stream_provider.zig"); const auth_runtime = @import("../auth/auth_runtime.zig"); const oauth_transport = @import("../auth/oauth_transport.zig"); const command_contract = @import("../execution/command_contract.zig"); const command_environment = @import("../execution/command_environment.zig"); const background_process_provider = @import( "../execution/background_process_provider.zig", ); const debug_trace = @import("../shared/debug_trace.zig"); const diagnostics = @import("../workspace/diagnostics.zig"); const image_attachments = @import("../images/image_attachments.zig"); const io_mod = @import("../shared/io.zig"); const tool_contracts = @import("../agent/runtime/tool_contracts.zig"); const vision_executor = @import("../agent/runtime/vision_executor.zig"); const background_runtime = @import("../background/background_runtime.zig"); const background_launch_identity = @import("../background/background_launch_identity.zig"); const process_supervisor = @import("../background/process_supervisor.zig"); const change_tracker = @import("../workspace/change_tracker.zig"); const diff_mod = @import("../output/diff.zig"); const file_mutation = @import("file_mutation.zig"); const file_mutation_contract = @import("file_mutation_contract.zig"); const hooks = @import("../hooks/hooks.zig"); const permission_auto_classifier = @import("../permissions/auto_classifier.zig"); const glob_pattern = @import("../workspace/glob_pattern.zig"); const permission_prompter = @import("../permissions/permission_prompter.zig"); const permission_request = @import("../permissions/permission_request.zig"); const command_admission = @import("../permissions/command_admission.zig"); const pathing = @import("../workspace/pathing.zig"); const devbox_executor = @import("../execution/devbox_executor.zig"); const execution_router = @import("../execution/router.zig"); const sandbox = @import("../permissions/sandbox.zig"); const skill_runtime = @import("../skills/skill_runtime.zig"); const subagent_authority = @import("../subagent/authority.zig"); const subagent_communication_store = @import("../subagent/communication_store.zig"); const subagent_control_store = @import("../subagent/control_store.zig"); const subagent_create_store = @import("../subagent/create_store.zig"); const subagent_domain = @import("../subagent/domain.zig"); const subagent_tool_host = @import("../subagent/tool_host.zig"); const subagent_tool_provider = @import("../subagent/tool_provider.zig"); const subagent_tool_result = @import("../subagent/tool_result.zig"); const session_runtime = @import("../session/session.zig"); const session_permission_state = @import("../permissions/session_permission_state.zig"); const session_codec_mod = @import("../session/session_codec.zig"); const task_helpers = @import("../tasks/task_helpers.zig"); const session_child_store = @import("../session/session_child_store.zig"); const command_replay_store = @import("../session/command_replay_store.zig"); const session_store = @import("../session/session_store.zig"); const text_utils = @import("../shared/text_utils.zig"); const model_capabilities = @import("../config/model_capabilities.zig"); const mcp_access_policy = @import("../mcp/access_policy.zig"); const tool_admission = @import("tool_admission.zig"); const tool_args = @import("tool_args.zig"); const command_result_mapping = @import("command_result_mapping.zig"); const tool_dispatch = @import("tool_dispatch.zig"); const tool_specs = @import("tool_specs.zig"); const tool_result_errors = @import("tool_result_errors.zig"); const tool_result_limits = @import("tool_result_limits.zig"); const file_mutation_execution = @import("file_mutation_execution.zig"); const tool_mcp_registry = @import("tool_mcp_registry.zig"); const tool_mcp_runtime = @import("tool_mcp_runtime.zig"); const tool_mcp_feature_dispatch = @import("tool_mcp_feature_dispatch.zig"); const tool_presentation = @import("tool_presentation.zig"); const web_fetch_runtime = @import("web_fetch_runtime.zig"); const web_search_contract = @import("web_search_contract.zig"); const web_fetch_artifacts = @import("../session/web_fetch_artifacts.zig"); const types = @import("../shared/types.zig"); const worker_runtime = @import("../agent/worker_runtime.zig"); const context_contract = @import("../workspace/context_contract.zig"); const test_builtin_tools = if (builtin.is_test) @import("../../builtins/tools.zig") else struct {}; const test_builtin_gateway = if (builtin.is_test) @import("../../builtins/gateway.zig") else struct {}; const test_browser_workspace_tools = if (builtin.is_test) @import("../../builtins/browser_workspace_tools.zig") else struct {}; const js_host_workspace = @import("../hosts/js_host_workspace.zig"); const agent_test_support = if (builtin.is_test) @import("../agent/runtime/tests/support.zig") else struct {}; const Allocator = std.mem.Allocator; const ToolCall = types.ToolCall; const ChatMessage = types.ChatMessage; const PermissionGrant = types.PermissionGrant; const PermissionMode = types.PermissionMode; const ToolPermissionDecision = types.ToolPermissionDecision; const subagent_tool_name = "subagent"; const ToolExecutionResult = tool_contracts.ToolExecutionResult; const BackgroundRuntime = background_runtime.BackgroundRuntime; const SessionRuntime = session_runtime.SessionRuntime; const WorkerRuntime = worker_runtime.WorkerRuntime; const max_file_mutation_success_bytes: usize = 8 * 1024; const helpers = struct { const requiredStringArg = tool_args.requiredStringArg; const parseToolArgsObject = tool_args.parseToolArgsObject; }; const optionalIntArg = tool_args.optionalIntArg; const parseToolArgsObject = helpers.parseToolArgsObject; const context_limits = @import("../config/context_limits.zig"); const workspace_access = @import("../workspace/workspace_access.zig"); const host_capabilities = @import("../hosts/host.zig"); const terminal_client_runtime = @import("../terminal/client.zig"); test { _ = tool_admission; _ = command_result_mapping; _ = @import("../session/command_replay_store.zig"); _ = file_mutation_execution; _ = tool_presentation; } pub const Context = struct { workspace_root: []const u8, access_scope: ?workspace_access.AccessScope = null, ignored_list_entries: []const []const u8, max_list_entries: usize, max_read_file_bytes: usize, max_read_file_lines: usize, max_read_file_line_len: usize, max_command_output_bytes: usize, max_tool_result_bytes: usize = tool_result_limits.default_max_tool_result_bytes, api_key: []const u8, agent_stream_provider: agent_stream_provider.Provider = agent_stream_provider.unavailable_provider, gateway_team: ?[]const u8 = null, credential_source: ?types.CredentialSource = null, oauth_transport: oauth_transport.Provider = oauth_transport.unavailable_provider, model: []const u8, permission_review_turn: ?permission_auto_classifier.ReviewTurnContext = null, root_user_intent_context: []const u8 = "", root_user_messages: []const []const u8 = &.{}, root_user_evidence_complete: bool = false, current_turn_messages: []const ChatMessage = &.{}, gateway_retry_count: usize, gateway_chat_url: []const u8, gateway_models_path: []const u8 = "/v1/models", agent_step_limit: usize, fast_mode: bool = false, effort: types.ReasoningEffort = .auto, first_call_tool_choice: types.ToolChoice = .auto, tool_registry: tool_dispatch.Registry = .{}, subagent_host: ?*subagent_tool_host.Runtime = null, subagent_caller_id: ?[]const u8 = null, permission_mode: PermissionMode, permission_grants: []const PermissionGrant, session_grants: []const PermissionGrant = &.{}, permission_rules: types.PermissionRuleSet, permission_state_override: ?*const session_permission_state.State = null, worker: *WorkerRuntime, /// Sole prompt capability admission consults. When null, admission never /// prompts: it resolves by rule, automatic review, or fail-closed denial /// (e.g. ACP hosts prompt over JSON-RPC by setting this). permission_prompter: ?permission_prompter.Prompter = null, cancel_flag: ?*std.atomic.Value(bool) = null, background: *BackgroundRuntime, session: *SessionRuntime, session_allocator: Allocator = std.heap.c_allocator, skills_dir: []const u8 = "", context_limits: context_limits.Values = .{}, context_registry: context_contract.Registry, context_enabled: bool = true, output_chunk_lifecycle_id: ?types.ToolLifecycleId = null, output_chunk_ctx: *anyopaque, on_output_chunk: command_contract.CommandOutputCallback, background_url_ctx: *anyopaque, on_background_url_ready: *const fn (*anyopaque, u64, []const u8) void, command_artifact_dir: ?[]const u8 = null, tool_result_dir: ?[]const u8 = null, session_child_capability: ?*session_child_store.SessionChildCapability = null, terminal_client: ?*terminal_client_runtime.Runtime = null, command_timeout_ms: ?usize = null, command_timeout_started_ms: ?i64 = null, command_replay_capture: ?*command_replay_store.Capture = null, command_replay_unavailable: bool = false, sandbox_backend: sandbox.BackendKind = .none, devbox_provider: ?devbox_executor.Provider = null, tracker: ?*change_tracker.ChangeTracker = null, mcp_ctx: ?*anyopaque = null, mcp_has_tool: ?tool_mcp_runtime.HasToolFn = null, mcp_validate_tool: ?tool_mcp_runtime.ValidateToolFn = null, mcp_call_tool: ?tool_mcp_runtime.CallToolFn = null, mcp_search_tools: ?tool_mcp_runtime.SearchToolsFn = null, mcp_tool_schema: ?tool_mcp_runtime.ToolSchemaFn = null, mcp_call_feature: ?tool_mcp_runtime.FeatureCallFn = null, mcp_access: tool_mcp_runtime.Access = .unrestricted, mcp_input_responder: ?tool_mcp_runtime.InputResponder = null, mcp_progress_ctx: ?*anyopaque = null, on_mcp_progress: ?*const fn (*anyopaque, types.ToolLifecycleId, []const u8) void = null, advertised_dynamic_tool_names: []const []const u8 = &.{}, permission_reviewer_provider: ?permission_auto_classifier.Provider = null, auto_classifier: permission_auto_classifier.Classifier = permission_auto_classifier.Classifier.disabled(), web_fetch_runtime: ?*web_fetch_runtime.Runtime = null, web_fetch_artifact_store: ?*web_fetch_artifacts.Store = null, web_fetch_artifact_error: ?anyerror = null, web_search_runtime_ready: bool = false, web_search_backend: ?tool_dispatch.WebSearchBackend = null, web_search_progress_ctx: ?*anyopaque = null, on_web_search_progress: ?tool_dispatch.WebSearchProgressFn = null, web_fetch_progress_ctx: ?*anyopaque = null, on_web_fetch_progress: ?tool_dispatch.WebFetchProgressFn = null, workspace_executor: ?js_host_workspace.Executor = null, host_sandbox_default: tool_admission.HostSandboxDefault = .none, model_capability_resolver: ?model_capabilities.Resolver = null, /// False when running outside an interactive TUI (e.g. ACP). Tools /// that require a live user (like `ask_user_question`) short-circuit /// in that case. interactive: bool = true, lifecycle_view: hooks.RuntimeView = hooks.RuntimeView.empty(), lifecycle_scope: hooks.Scope = .{ .kind = .interactive, .workspace_root = "", }, /// Projects only the borrowed capabilities consumed by admission. pub fn admissionInput(self: Context) tool_admission.Input { var input: tool_admission.Input = .{ .workspace_root = self.workspace_root, .access_scope = self.access_scope, .permission_review_turn = self.permission_review_turn, .permission_grants = self.permission_grants, .permission_rules = self.permission_rules, .session_permission_state = self.permission_state_override, .session_permission_state_provider = .{ .context = @ptrCast(self.session), .snapshot_fn = snapshotSessionPermissionState, }, .tool_registry = self.tool_registry, .worker = self.worker, .permission_prompter = self.permission_prompter, .background = self.background, .sandbox_backend = self.sandbox_backend, .advertised_dynamic_tool_names = self.advertised_dynamic_tool_names, .mcp_runtime = mcpRuntimeCapabilities(self), .context_limits = self.context_limits, .auto_classifier = self.admissionAutoClassifier(), .host_sandbox_default = self.host_sandbox_default, }; if (self.permission_state_override != null) { input.session_permission_state_provider = null; } return input; } pub fn admissionInputWithLiveAuthority( self: Context, authority: ?tool_contracts.LiveToolAuthority, ) tool_admission.Input { var input = self.admissionInput(); if (authority) |live| { input.permission_grants = live.grants; input.permission_rules = live.rules; input.session_permission_state = live.permission_state; if (live.permission_state != null) { input.session_permission_state_provider = null; } input.sandbox_backend = live.sandbox_backend; input.advertised_dynamic_tool_names = live.integrations; } return input; } fn admissionAutoClassifier(self: Context) permission_auto_classifier.Classifier { if (self.auto_classifier.enabled()) return self.auto_classifier; const provider = self.permission_reviewer_provider orelse return permission_auto_classifier.Classifier.disabled(); return permission_auto_classifier.Classifier.withProvider(provider, .{ .credential = self.api_key, .tenant = self.gateway_team, .endpoint = self.gateway_chat_url, .cancel_flag = self.cancel_flag, .usage = &self.session.usage, .usage_allocator = self.session_allocator, }); } }; fn snapshotSessionPermissionState( raw_session: *anyopaque, alloc: Allocator, ) !session_permission_state.State { const session: *SessionRuntime = @ptrCast(@alignCast(raw_session)); return session.snapshotPermissionState(alloc); } fn registeredToolSpec(ctx: Context, name: []const u8) ?*const tool_specs.ToolSpec { return ctx.tool_registry.lookup(name); } pub fn validateToolCall(ctx: Context, arena: Allocator, call: ToolCall) !tool_contracts.ToolCallValidationResult { const spec = registeredToolSpec(ctx, call.name) orelse { return switch (try tool_mcp_runtime.validateAdvertisedDynamicTool(.{ .is_registered_tool = false, .advertised_dynamic_tool_names = ctx.advertised_dynamic_tool_names, .runtime = mcpRuntimeCapabilities(ctx), }, arena, call.name, call.arguments_json)) { .valid => .valid, .invalid => |reason| .{ .failure = reason }, .not_available => .not_registered, }; }; switch (spec.executor_kind) { // Preserve execution-time argument failures for MCP control tool calls. .mcp_search_tools, .mcp_select_tool, .mcp_features => return .valid, // File mutation arguments are decoded once by shared permission preflight. .write_file, .edit_file => return .valid, else => {}, } var dispatch_ctx = typedDispatchContext(ctx, arena); dispatch_ctx.captured_command_host = spec.captured_command_host; return switch (try tool_dispatch.validateRegisteredToolCall(dispatch_ctx, ctx.tool_registry, call)) { .not_registered => .not_registered, .valid => .valid, .failure => |reason| .{ .failure = reason }, }; } pub fn checkToolAvailability(ctx: Context, arena: Allocator, call: ToolCall) !?[]const u8 { return tool_dispatch.localToolAvailabilityFailureForCall( typedDispatchContext(ctx, arena), ctx.tool_registry, call, ); } pub fn executeToolCallAuthorized( ctx: Context, request: tool_contracts.ToolExecutionRequest, ) !ToolExecutionResult { var replay_continuation_transferred = request.command_replay_capture == null; defer if (!replay_continuation_transferred) { request.command_replay_capture.?.abort(request.result_allocator); }; var execution_ctx = ctx; if (request.live_authority) |authority| { if (!containsName(authority.tools, request.call.name) and !containsName(authority.integrations, request.call.name)) { return error.LiveToolAuthorityUnavailable; } execution_ctx.permission_mode = authority.permission_mode; execution_ctx.permission_grants = authority.grants; execution_ctx.session_grants = authority.grants; execution_ctx.permission_rules = authority.rules; execution_ctx.sandbox_backend = authority.sandbox_backend; execution_ctx.advertised_dynamic_tool_names = authority.integrations; execution_ctx.mcp_access = rebindMcpAuthorityGeneration( execution_ctx.mcp_access, authority.generation, ); } else { execution_ctx.session_grants = request.session_grants; execution_ctx.advertised_dynamic_tool_names = request.advertised_dynamic_tool_names; } execution_ctx.max_tool_result_bytes = request.max_tool_result_bytes; execution_ctx.current_turn_messages = request.current_turn_messages; execution_ctx.output_chunk_lifecycle_id = request.lifecycle_id; execution_ctx.command_timeout_started_ms = request.command_timeout_started_ms; execution_ctx.command_replay_capture = request.command_replay_capture; execution_ctx.command_replay_unavailable = request.command_replay_unavailable; const started_at_ms = io_mod.milliTimestamp(); const uses_file_mutation_contract = if (registeredToolSpec( execution_ctx, request.call.name, )) |spec| spec.take_file_mutation_input_fn != null else false; const result = (if (comptime builtin.os.tag == .wasi) executeWorkspaceToolCallInner( execution_ctx, request.result_allocator, request.call, request.authority, request.classification_complete, ) else if (uses_file_mutation_contract) file_mutation_execution.execute(.{ .call_allocator = request.call_allocator, .result_allocator = request.result_allocator, .call = request.call, .authorization = switch (request.authority) { .file_mutation => |value| value, else => null, }, .maybe_cancel_flag = execution_ctx.cancel_flag, .lifecycle_id = request.lifecycle_id, }) else executeToolCallInner( execution_ctx, request.result_allocator, request.call, request.authority, request.classification_complete, request.authorized_image_catalog, )) catch |err| { if (request.isSandboxWideningRetryCancellationError(err)) return err; diagnostics.recordToolCallResult(.{ .name = request.call.name, .arguments_json = request.call.arguments_json, .model_output = "", .ok = false, .started_at_ms = started_at_ms, }); return if (err == error.CancelledBeforeExecution) error.Cancelled else err; }; if (!request.resultAwaitsSandboxWidening(result)) { const ok = switch (result.status) { .success => true, else => false, }; diagnostics.recordToolCallResult(.{ .name = request.call.name, .arguments_json = request.call.arguments_json, .model_output = result.model_output, .ok = ok, .started_at_ms = started_at_ms, }); } if (request.command_replay_capture) |continued| { replay_continuation_transferred = result.command_replay_capture == continued or (result.sandbox_scope_required != null and result.sandbox_scope_required.?.command_replay_capture == continued); } return result; } fn rebindMcpAuthorityGeneration( access: tool_mcp_runtime.Access, authority_generation: u64, ) tool_mcp_runtime.Access { return switch (access) { .scoped => |scope| .{ .scoped = .{ .captured = scope.captured, .admission_authority_generation = scope.admission_authority_generation, .live = scope.live, .action_authority_generation = authority_generation, } }, else => access, }; } fn containsName(values: []const []const u8, needle: []const u8) bool { for (values) |value| { if (std.mem.eql(u8, value, needle)) return true; } return false; } pub fn executeToolCall( ctx: Context, arena: Allocator, call: ToolCall, ) !ToolExecutionResult { return executeToolCallAuthorized(ctx, .{ .call_allocator = arena, .result_allocator = arena, .call = call, .authority = .ordinary, .current_turn_messages = ctx.current_turn_messages, .session_grants = ctx.session_grants, .advertised_dynamic_tool_names = ctx.advertised_dynamic_tool_names, .max_tool_result_bytes = ctx.max_tool_result_bytes, }); } const ToolDispatchPrelude = union(enum) { completed: ToolExecutionResult, registered_static, registered_dynamic: tool_dispatch.Tool, }; fn executeToolCallInner( ctx: Context, arena: Allocator, call: ToolCall, authority: command_admission.ToolExecutionAuthority, classification_complete: bool, authorized_image_catalog: []const types.ImageAttachment, ) !ToolExecutionResult { return switch (try resolveToolDispatchPrelude( ctx, arena, call, classification_complete, )) { .completed => |result| return result, .registered_static => try executeRegisteredTool( ctx, arena, call, authority, authorized_image_catalog, ctx.tool_registry, ), .registered_dynamic => |dynamic_tool| blk: { const tools = [_]tool_dispatch.Tool{dynamic_tool}; break :blk try executeRegisteredTool( ctx, arena, call, authority, authorized_image_catalog, .{ .tools = tools[0..] }, ); }, }; } fn executeWorkspaceToolCallInner( ctx: Context, arena: Allocator, call: ToolCall, authority: command_admission.ToolExecutionAuthority, classification_complete: bool, ) !ToolExecutionResult { if (!classification_complete) { if (try checkToolAvailability(ctx, arena, call)) |reason| { return semanticFailure(reason); } } const spec = registeredToolSpec(ctx, call.name) orelse return semanticFailure(try std.fmt.allocPrint(arena, "Unsupported tool: {s}", .{call.name})); if (ctx.tool_registry.tools.len != 1 or !std.mem.eql(u8, spec.name, "terminal") or spec.executor_kind != .run_command or spec.runtime_provider != .run_command) { return semanticFailure(try std.fmt.allocPrint(arena, "Unsupported tool: {s}", .{call.name})); } var command_backend = RunCommandBackendState{ .runtime = ctx }; var dispatch_ctx = typedDispatchContextForCall(ctx, arena, call); dispatch_ctx.execution_authority = authority; dispatch_ctx.captured_command_host = spec.captured_command_host; dispatch_ctx.run_command_backend = .{ .ctx = &command_backend, .execute_fn = executeRunCommandBackend, }; const dispatched = try tool_dispatch.dispatchAuthorizedToolCall( dispatch_ctx, ctx.tool_registry, call, ); if (command_backend.execution_error) |err| { dispatched.deinit(arena); return err; } var execution = command_backend.completion orelse toolExecutionResultFromDispatch(dispatched); execution.model_output = dispatched.body; if (dispatched.status_detail) |detail| execution.status_detail = detail; return execution; } fn resolveToolDispatchPrelude( ctx: Context, arena: Allocator, call: ToolCall, classification_complete: bool, ) !ToolDispatchPrelude { if (!classification_complete) { if (try checkToolAvailability(ctx, arena, call)) |reason| { return .{ .completed = semanticFailure(reason) }; } } if (registeredToolSpec(ctx, call.name) != null) return .registered_static; const mcp_runtime = mcpRuntimeCapabilities(ctx); return switch (tool_mcp_registry.resolve( ctx.advertised_dynamic_tool_names, mcp_runtime, call.name, )) { .registered => |tool| .{ .registered_dynamic = tool }, .not_selected => .{ .completed = semanticFailure( try tool_mcp_runtime.notSelectedOutput(arena, call.name), ) }, .unsupported => .{ .completed = semanticFailure( try std.fmt.allocPrint(arena, "Unsupported tool: {s}", .{call.name}), ) }, }; } const McpProgressBridge = struct { ctx: Context, }; fn emitMcpProgress(raw_context: *anyopaque, progress: tool_mcp_runtime.Progress) void { const bridge: *McpProgressBridge = @ptrCast(@alignCast(raw_context)); var clipped_buf: [256]u8 = undefined; var generated_buf: [96]u8 = undefined; const text = if (progress.message) |message| text_utils.clippedLabel(&clipped_buf, message, clipped_buf.len) else if (progress.total) |total| std.fmt.bufPrint( &generated_buf, "MCP progress {d:.2}/{d:.2}", .{ progress.progress, total }, ) catch "MCP progress" else std.fmt.bufPrint( &generated_buf, "MCP progress {d:.2}", .{progress.progress}, ) catch "MCP progress"; if (bridge.ctx.on_mcp_progress) |publish| { if (bridge.ctx.output_chunk_lifecycle_id) |lifecycle_id| { publish( bridge.ctx.mcp_progress_ctx orelse bridge.ctx.output_chunk_ctx, lifecycle_id, text, ); return; } } bridge.ctx.on_output_chunk( bridge.ctx.output_chunk_ctx, bridge.ctx.output_chunk_lifecycle_id, .stdout, text, ) catch |err| { debug_trace.logf("mcp", "failed to publish MCP progress err={s}", .{@errorName(err)}); }; } fn executeRegisteredTool( ctx: Context, arena: Allocator, call: ToolCall, authority: command_admission.ToolExecutionAuthority, authorized_image_catalog: []const types.ImageAttachment, registry: tool_dispatch.Registry, ) !ToolExecutionResult { var selected_dynamic_tool_sink = SelectedDynamicToolSinkState{ .allocator = arena }; var context_notice_sink = ContextNoticeSinkState{ .allocator = arena }; var command_backend = RunCommandBackendState{ .runtime = ctx }; var vision_provider = VisionProviderState{ .runtime = ctx, .authorized_image_catalog = authorized_image_catalog, }; var subagent_provider = SubagentProviderState{ .runtime = ctx }; var mcp_progress_bridge = McpProgressBridge{ .ctx = ctx }; var mcp_call_status: ?tool_mcp_runtime.CallStatus = null; var mcp_execution_error: ?anyerror = null; var dispatch_ctx = typedDispatchContextForCall(ctx, arena, call); dispatch_ctx.execution_authority = authority; dispatch_ctx.mcp_call_options = .{ .cancel_flag = dispatch_ctx.cancel_flag, .progress = .{ .context = @ptrCast(&mcp_progress_bridge), .callback = emitMcpProgress, }, .input_responder = ctx.mcp_input_responder, .access = ctx.mcp_access, }; dispatch_ctx.mcp_call_status_sink = &mcp_call_status; const runtime_provider = if (registry.lookup(call.name)) |tool| tool.runtime_provider else .none; if (registry.lookup(call.name)) |tool| { dispatch_ctx.captured_command_host = tool.captured_command_host; } switch (runtime_provider) { .none => {}, .run_command => dispatch_ctx.run_command_backend = .{ .ctx = &command_backend, .execute_fn = executeRunCommandBackend, }, .vision => dispatch_ctx.vision_provider = .{ .ctx = &vision_provider, .execute_fn = executeVisionProvider, }, .subagent => dispatch_ctx.subagent_provider = .{ .context = &subagent_provider, .execute_fn = executeSubagentProvider, }, } dispatch_ctx.mcp_execution_error_sink = &mcp_execution_error; attachSelectedDynamicToolSink(&dispatch_ctx, &selected_dynamic_tool_sink); attachContextNoticeSink(&dispatch_ctx, &context_notice_sink); const dispatched = try tool_dispatch.dispatchAuthorizedToolCall( dispatch_ctx, registry, call, ); if (command_backend.execution_error) |err| { dispatched.deinit(arena); return err; } if (vision_provider.execution_error) |err| { dispatched.deinit(arena); return err; } if (mcp_execution_error) |err| { dispatched.deinit(arena); return err; } var execution = if (command_backend.completion) |completion| completion else if (vision_provider.completion) |completion| completion else toolExecutionResultFromDispatch(dispatched); execution.model_output = dispatched.body; if (dispatched.status_detail) |detail| execution.status_detail = detail; if (mcp_call_status == .input_required or (execution.status == .failure and tool_mcp_feature_dispatch.isInputRequiredFailure(execution.model_output))) { execution.finish_turn = true; execution.status_detail = "McpInputRequired"; } execution.selected_dynamic_tool_name = selected_dynamic_tool_sink.name; execution.selected_dynamic_tool_schema_json = selected_dynamic_tool_sink.schema_json; execution.context_notices = context_notice_sink.notices.items; return execution; } const RunCommandBackendState = struct { runtime: Context, completion: ?ToolExecutionResult = null, execution_error: ?anyerror = null, }; fn executeRunCommandBackend( maybe_ctx: ?*anyopaque, dispatch_ctx: tool_dispatch.DispatchContext, request: tool_dispatch.RunCommandRequest, ) tool_dispatch.DispatchError!tool_dispatch.ToolResult { const raw_ctx = maybe_ctx orelse unreachable; const state: *RunCommandBackendState = @ptrCast(@alignCast(raw_ctx)); const authority = switch (dispatch_ctx.execution_authority orelse { state.execution_error = error.InvalidRunCommandExecutionAuthority; return .{ .failure = try dispatch_ctx.allocator.dupe(u8, "") }; }) { .run_command => |command_authority| command_authority, .ordinary, .file_mutation, .vision_paths => { state.execution_error = error.InvalidRunCommandExecutionAuthority; return .{ .failure = try dispatch_ctx.allocator.dupe(u8, "") }; }, }; const execution = toolRunCommand( state.runtime, dispatch_ctx.allocator, request, authority, ) catch |err| { state.execution_error = err; return .{ .failure = try dispatch_ctx.allocator.dupe(u8, "") }; }; state.completion = execution; return switch (execution.status) { .success => .{ .success = @constCast(execution.model_output) }, .failure => .{ .failure = @constCast(execution.model_output) }, }; } fn toolExecutionResultFromDispatch(result: tool_dispatch.DispatchResult) ToolExecutionResult { return switch (result.status) { .success => .{ .model_output = result.body, .status_detail = result.status_detail, .inner_usage = result.inner_usage, .web_search_completion = result.web_search_completion, .web_fetch_completion = result.web_fetch_completion, .tool_result_memory = result.tool_result_memory, }, .failure => .{ .status = .failure, .model_output = result.body, .status_detail = result.status_detail, .inner_usage = result.inner_usage, .web_search_completion = result.web_search_completion, .web_fetch_completion = result.web_fetch_completion, .tool_result_memory = result.tool_result_memory, }, }; } const SelectedDynamicToolSinkState = struct { allocator: Allocator, name: ?[]const u8 = null, schema_json: ?[]const u8 = null, }; const ContextNoticeSinkState = struct { allocator: Allocator, notices: std.ArrayList([]const u8) = .empty, }; fn attachContextNoticeSink(dispatch_ctx: *tool_dispatch.DispatchContext, state: *ContextNoticeSinkState) void { dispatch_ctx.context_notice_ctx = state; dispatch_ctx.on_context_notice = recordContextNoticeForDispatch; } fn recordContextNoticeForDispatch(raw_ctx: ?*anyopaque, notice: []const u8) error{OutOfMemory}!void { const state_ptr = raw_ctx orelse return; const state: *ContextNoticeSinkState = @ptrCast(@alignCast(state_ptr)); const duplicate = try state.allocator.dupe(u8, notice); errdefer state.allocator.free(duplicate); try state.notices.append(state.allocator, duplicate); } fn attachSelectedDynamicToolSink( dispatch_ctx: *tool_dispatch.DispatchContext, state: *SelectedDynamicToolSinkState, ) void { dispatch_ctx.selected_dynamic_tool_ctx = state; dispatch_ctx.on_selected_dynamic_tool = recordSelectedDynamicToolForDispatch; } fn recordSelectedDynamicToolForDispatch( raw_ctx: ?*anyopaque, name: []const u8, schema_json: []const u8, ) error{OutOfMemory}!void { const state_ptr = raw_ctx orelse return; const state: *SelectedDynamicToolSinkState = @ptrCast(@alignCast(state_ptr)); const owned_name = try state.allocator.dupe(u8, name); errdefer state.allocator.free(owned_name); const owned_schema_json = try state.allocator.dupe(u8, schema_json); state.name = owned_name; state.schema_json = owned_schema_json; } fn typedDispatchContext(ctx: Context, arena: Allocator) tool_dispatch.DispatchContext { var capabilities = tool_dispatch.ToolCapabilities.for_host( host_capabilities.current(), ); capabilities.web_search_runtime_ready = ctx.web_search_runtime_ready and ctx.web_search_backend != null; return .{ .allocator = arena, .permission_mode = ctx.permission_mode, .workspace_root = ctx.workspace_root, .access_scope = ctx.access_scope, .change_tracker = ctx.tracker, .skills_dir = ctx.skills_dir, .context_limits = ctx.context_limits, .ignored_list_entries = ctx.ignored_list_entries, .max_list_entries = ctx.max_list_entries, .max_read_file_bytes = ctx.max_read_file_bytes, .max_read_file_lines = ctx.max_read_file_lines, .max_read_file_line_len = ctx.max_read_file_line_len, .max_tool_result_bytes = ctx.max_tool_result_bytes, .tool_result_dir = ctx.tool_result_dir, .session_child_capability = ctx.session_child_capability, .terminal_client = ctx.terminal_client, .terminal_owner_session_id = ctx.lifecycle_scope.session_id, .terminal_transport_role = switch (ctx.lifecycle_scope.kind) { .interactive, .subagent => .interactive, .ask => .headless, .acp => .acp, }, .background_lifecycle_allocator = ctx.session_allocator, .cancel_flag = runtimeCancelFlag(ctx), .output_chunk_lifecycle_id = ctx.output_chunk_lifecycle_id, .output_chunk_ctx = ctx.output_chunk_ctx, .on_output_chunk = ctx.on_output_chunk, .command_timeout_ms = ctx.command_timeout_ms, .sandbox_backend = ctx.sandbox_backend, .devbox_provider = ctx.devbox_provider, .ask_question_ctx = if (ctx.interactive) ctx.worker else null, .ask_question_batch = if (ctx.interactive) requestQuestionBatchWithWorker else null, .web_fetch_runtime = ctx.web_fetch_runtime, .web_fetch_artifact_store = ctx.web_fetch_artifact_store, .web_fetch_artifact_error = ctx.web_fetch_artifact_error, .tool_capabilities = capabilities, .web_search_backend = ctx.web_search_backend, .web_search_progress_ctx = ctx.web_search_progress_ctx, .on_web_search_progress = ctx.on_web_search_progress, .web_fetch_progress_ctx = ctx.web_fetch_progress_ctx, .on_web_fetch_progress = ctx.on_web_fetch_progress, .mcp_ctx = ctx.mcp_ctx, .mcp_call_tool = ctx.mcp_call_tool, .mcp_search_tools = ctx.mcp_search_tools, .mcp_tool_schema = ctx.mcp_tool_schema, .mcp_call_feature = ctx.mcp_call_feature, .mcp_access = ctx.mcp_access, .mcp_input_responder = ctx.mcp_input_responder, .mcp_permission_rules = if (ctx.permission_mode == .yolo) .{} else ctx.permission_rules, }; } fn requestQuestionBatchWithWorker( raw_ctx: ?*anyopaque, response_alloc: Allocator, entries: []const types.QuestionBatchEntry, ) Allocator.Error!?[][]u8 { const worker: *WorkerRuntime = @ptrCast(@alignCast(raw_ctx.?)); const worker_alloc = std.heap.c_allocator; const worker_answers = try worker.requestQuestionBatchAnswerBlocking(worker_alloc, entries); defer freeQuestionAnswers(worker_alloc, worker_answers); const answers = worker_answers orelse return null; const copied_answers = try dupeQuestionAnswers(response_alloc, answers); return copied_answers; } fn dupeQuestionAnswers(alloc: Allocator, answers: []const []const u8) Allocator.Error![][]u8 { const copy = try alloc.alloc([]u8, answers.len); errdefer alloc.free(copy); var copied: usize = 0; errdefer { for (copy[0..copied]) |answer| alloc.free(answer); } while (copied < answers.len) : (copied += 1) { copy[copied] = try alloc.dupe(u8, answers[copied]); } return copy; } fn freeQuestionAnswers(alloc: Allocator, maybe_answers: ?[][]u8) void { const answers = maybe_answers orelse return; for (answers) |answer| alloc.free(answer); alloc.free(answers); } fn runtimeCancelFlag(ctx: Context) *std.atomic.Value(bool) { return ctx.cancel_flag orelse &ctx.worker.worker_cancel_requested; } fn typedDispatchContextForCall( ctx: Context, arena: Allocator, call: ToolCall, ) tool_dispatch.DispatchContext { var dispatch = typedDispatchContext(ctx, arena); dispatch.tool_call_id = call.id; dispatch.tool_call_name = call.name; return dispatch; } const VisionProviderState = struct { runtime: Context, authorized_image_catalog: []const types.ImageAttachment, completion: ?ToolExecutionResult = null, execution_error: ?anyerror = null, }; fn executeVisionProvider( maybe_ctx: ?*anyopaque, dispatch_ctx: tool_dispatch.DispatchContext, input: tool_dispatch.ToolInput, ) tool_dispatch.DispatchError!tool_dispatch.ToolResult { const raw_ctx = maybe_ctx orelse unreachable; const state: *VisionProviderState = @ptrCast(@alignCast(raw_ctx)); const request = input.as(tool_contracts.vision.VisionRequest); const execution = executeVisionRequest( state, dispatch_ctx.allocator, request.*, dispatch_ctx.execution_authority orelse { state.execution_error = error.InvalidVisionExecutionAuthority; return .{ .failure = try dispatch_ctx.allocator.dupe(u8, "") }; }, ) catch |err| { state.execution_error = err; return .{ .failure = try dispatch_ctx.allocator.dupe(u8, "") }; }; state.completion = execution; return switch (execution.status) { .success => .{ .success = @constCast(execution.model_output) }, .failure => .{ .failure = @constCast(execution.model_output) }, }; } fn executeVisionRequest( state: *VisionProviderState, alloc: Allocator, request: tool_contracts.vision.VisionRequest, authority: command_admission.ToolExecutionAuthority, ) !ToolExecutionResult { const config: vision_executor.Config = .{ .stream_provider = state.runtime.agent_stream_provider, .api_key = state.runtime.api_key, .gateway_team = state.runtime.gateway_team, .session_id = state.runtime.lifecycle_scope.session_id, .retry_count = state.runtime.gateway_retry_count, .chat_url = state.runtime.gateway_chat_url, .cancel_flag = state.runtime.cancel_flag, .usage = &state.runtime.session.usage, .usage_allocator = state.runtime.session_allocator, .trace_ctx = .{}, .output_limit = state.runtime.context_limits.image_adapter_output_bytes, }; if (request.image_ids() != null) { if (authority != .ordinary) return error.InvalidVisionExecutionAuthority; return vision_executor.executeRequest( alloc, request, state.authorized_image_catalog, config, ); } const approved_targets = switch (authority) { .vision_paths => |vision_authority| vision_authority.targets, .ordinary, .run_command, .file_mutation => return error.InvalidVisionExecutionAuthority, }; const raw_paths = request.paths() orelse return error.InvalidVisionExecutionAuthority; if (raw_paths.len != approved_targets.len) return error.InvalidVisionExecutionAuthority; return executeVisionPathRequest(state, alloc, request, approved_targets, config); } fn executeVisionPathRequest( state: *VisionProviderState, alloc: Allocator, request: tool_contracts.vision.VisionRequest, approved_targets: []const command_admission.VisionPathExecutionTarget, config: vision_executor.Config, ) !ToolExecutionResult { const catalog = try alloc.alloc(types.ImageAttachment, approved_targets.len); defer alloc.free(catalog); var initialized: usize = 0; const snapshot_dir = image_attachments.createTempSnapshotDir(alloc) catch |err| return visionPathPreparationFailure(alloc, err); defer alloc.free(snapshot_dir); defer image_attachments.cleanupSnapshotDir(snapshot_dir); defer { image_attachments.discardImageSnapshots(alloc, catalog[0..initialized]); for (catalog[0..initialized]) |image| types.freeImageAttachment(alloc, image); } for (approved_targets, 0..) |target, index| { const image = image_attachments.captureBoundImageAttachment( alloc, target.canonical_path, target.identity, index + 1, snapshot_dir, .{ .cancel_flag = state.runtime.cancel_flag }, ) catch |err| return visionPathPreparationFailure(alloc, err); catalog[index] = image; initialized += 1; } const image_ids = try alloc.alloc(usize, catalog.len); defer alloc.free(image_ids); for (image_ids, 0..) |*image_id, index| image_id.* = index + 1; const id_request: tool_contracts.vision.VisionRequest = .{ .source = .{ .image_ids = image_ids }, .focus = request.focus, }; return vision_executor.executeRequest(alloc, id_request, catalog, config); } fn visionPathFailure(alloc: Allocator, err: anyerror) Allocator.Error!ToolExecutionResult { const details = [_]tool_result_errors.Detail{ .{ .name = "error", .value = .{ .string = @errorName(err) } }, }; return .{ .status = .failure, .status_detail = @errorName(err), .model_output = try tool_result_errors.toolExecutionFailureJson(alloc, .{ .tool_name = "vision", .message = "Vision could not load an approved image path.", .details = &details, .suggestion = "Use an existing supported image under 20 MiB, or choose another image.", }), }; } fn visionPathPreparationFailure( alloc: Allocator, err: anyerror, ) !ToolExecutionResult { return switch (err) { error.OutOfMemory, error.Cancelled => err, else => visionPathFailure(alloc, err), }; } fn semanticFailure(output: []const u8) ToolExecutionResult { return .{ .status = .failure, .model_output = output }; } fn mcpRuntimeCapabilities(ctx: Context) tool_mcp_runtime.RuntimeCapabilities { return .{ .context = ctx.mcp_ctx, .has_tool = ctx.mcp_has_tool, .validate_tool = ctx.mcp_validate_tool, .call_tool = ctx.mcp_call_tool, .tool_schema = ctx.mcp_tool_schema, .access = ctx.mcp_access, }; } pub fn withAdvertisedDynamicToolNames(ctx: Context, advertised_dynamic_tool_names: []const []const u8) Context { var copy = ctx; copy.advertised_dynamic_tool_names = advertised_dynamic_tool_names; return copy; } fn toolRunCommand( ctx: Context, arena: Allocator, request: tool_dispatch.RunCommandRequest, authority: command_admission.CommandExecutionAuthority, ) !ToolExecutionResult { const command = request.command; const cwd = request.resolved_cwd; const resolved_backend = sandbox.resolveBackend(ctx.sandbox_backend); const authority_scope = commandAuthorityFingerprint(authority).scope; const needs_preflight_widening = authority_scope == .restricted and resolved_backend == .macos and sandbox.needsBroaderFileAccess(command); const command_ctx = command_admission.CommandContext{ .command = command, .resolved_cwd = cwd, .background = false, .resolved_backend = resolved_backend, .target_os = builtin.os.tag, .environment = request.environment, .scope = authority_scope, }; const timeout_started_ms = ctx.command_timeout_started_ms orelse if (ctx.command_timeout_ms != null) io_mod.milliTimestamp() else null; if (comptime builtin.os.tag == .wasi or builtin.is_test) { if (ctx.workspace_executor) |executor| { return executeWorkspaceRunCommand( arena, request, command_ctx, authority, executor, ctx.command_timeout_ms, ); } } if (comptime builtin.os.tag == .wasi) return error.WorkspaceUnavailable; try execution_router.validateConfigContext(.{ .backend = ctx.sandbox_backend, .workspace_root = ctx.workspace_root, .access_scope = ctx.access_scope, .max_command_output_bytes = ctx.max_command_output_bytes, }, command_ctx); var route = try execution_router.prepareAuthorizedRoute(arena, command_ctx, authority); defer route.deinit(arena); const compatibility_result = try tool_dispatch.dispatchRunCommandCompatibility( typedDispatchContext(ctx, arena), ctx.tool_registry, request, ); if (compatibility_result) |compatibility| { if (route != .approved_shell) return error.CommandAdmissionChanged; const intercepted_replay = initCommandReplayCapture( arena, ctx.interactive, ctx.max_command_output_bytes, ctx.max_command_output_bytes, ctx.session_child_capability, ctx.command_replay_capture, ctx.command_replay_unavailable, ); const capture = intercepted_replay.capture; var transferred = false; defer if (!transferred) { if (capture) |candidate| candidate.abort(arena); }; var callback = CommandReplayCaptureCallback{ .alloc = arena, .capture = capture, }; const output = switch (compatibility) { .success => |body| body, .failure => |body| body, }; if (compatibility == .success and ctx.interactive and output.len > 0) { try callback.accept( ctx.output_chunk_lifecycle_id, .stdout, output, ); try ctx.on_output_chunk( ctx.output_chunk_ctx, ctx.output_chunk_lifecycle_id, .stdout, output, ); } return finishCommandToolResult( capture, intercepted_replay.unavailable and (ctx.command_replay_unavailable or callback.had_accepted_output), &transferred, null, .{ .status = switch (compatibility) { .success => .success, .failure => .failure, }, .model_output = output, }, ); } if (route == .approved_shell and needs_preflight_widening) { return sandboxScopeRequiredPreflight( arena, authority, timeout_started_ms, ); } const use_permissive = authority_scope == .broader; const replay_init = initCommandReplayCapture( arena, ctx.interactive, ctx.max_command_output_bytes, execution_router.foregroundResultComparisonLimit( route, ctx.max_command_output_bytes, ), ctx.session_child_capability, ctx.command_replay_capture, ctx.command_replay_unavailable, ); const replay_capture = replay_init.capture; var replay_transferred = false; defer if (!replay_transferred) { if (replay_capture) |capture| capture.abort(arena); }; var replay_callback = CommandReplayCaptureCallback{ .alloc = arena, .capture = replay_capture, }; const routed = execution_router.executePreparedRoute(.{ .backend = ctx.sandbox_backend, .workspace_root = ctx.workspace_root, .access_scope = ctx.access_scope, .max_command_output_bytes = ctx.max_command_output_bytes, .cancel_flag = runtimeCancelFlag(ctx), .output_chunk_lifecycle_id = ctx.output_chunk_lifecycle_id, .output_chunk_ctx = ctx.output_chunk_ctx, .on_output_chunk = ctx.on_output_chunk, .accepted_output_chunk_ctx = if (ctx.interactive) @ptrCast(&replay_callback) else null, .on_accepted_output_chunk = if (ctx.interactive) CommandReplayCaptureCallback.onChunk else null, .callback_projection = if (ctx.interactive) .raw else .model_safe, .permissive = use_permissive, .timeout_ms = ctx.command_timeout_ms, .timeout_started_ms = timeout_started_ms, .devbox_provider = ctx.devbox_provider, .command_artifact_capability = ctx.session_child_capability, .command_artifact_dir = ctx.command_artifact_dir, }, arena, route) catch |err| { if (err == error.TimeoutExpired) return finishCommandToolResult( replay_capture, replay_init.unavailable and (ctx.command_replay_unavailable or replay_callback.had_accepted_output), &replay_transferred, null, try command_result_mapping.Foreground.timeoutFailure( arena, command, cwd, ctx.command_timeout_ms, timeout_started_ms, ), ); return err; }; const result = routed.result; if (try command_result_mapping.Foreground.cancelledFailure(arena, result)) |cancelled| { return cancelled; } if (routed.route == .approved_shell and authority_scope == .restricted and resolved_backend == .macos and sandbox.looksLikeSandboxError(result.output)) { return finishCommandToolResult( replay_capture, replay_init.unavailable and (ctx.command_replay_unavailable or replay_callback.had_accepted_output), &replay_transferred, result, try sandboxScopeRequiredReactive( arena, authority, timeout_started_ms, result, ), ); } if (try command_result_mapping.Foreground.nonZeroFailure(arena, result)) |failure| { return finishCommandToolResult( replay_capture, replay_init.unavailable and (ctx.command_replay_unavailable or replay_callback.had_accepted_output), &replay_transferred, result, failure, ); } return finishCommandToolResult( replay_capture, replay_init.unavailable and (ctx.command_replay_unavailable or replay_callback.had_accepted_output), &replay_transferred, result, .{ .model_output = result.output, .command_result_json = if (result.command_result) |command_result| try command_result.toJson(arena) else null, }, ); } fn executeWorkspaceRunCommand( arena: Allocator, request: tool_dispatch.RunCommandRequest, command_ctx: command_admission.CommandContext, authority: command_admission.CommandExecutionAuthority, executor: js_host_workspace.Executor, configured_timeout_ms: ?usize, ) !ToolExecutionResult { if (std.meta.activeTag(request.environment) != .workspace_clean) return error.InvalidWorkspaceInput; var route = try execution_router.prepareAuthorizedRoute( arena, command_ctx, authority, ); defer route.deinit(arena); const timeout_ms: u32 = @intCast(@min( @max(configured_timeout_ms orelse js_host_workspace.max_timeout_ms, js_host_workspace.min_timeout_ms), js_host_workspace.max_timeout_ms, )); const started_ms = io_mod.milliTimestamp(); const result = executor.execute( arena, request.command, request.resolved_cwd, timeout_ms, ) catch |err| { if (err == error.WorkspaceDeadline) { return command_result_mapping.Foreground.timeoutFailure( arena, request.command, request.resolved_cwd, timeout_ms, started_ms, ); } return err; }; var replay_transferred = false; if (try command_result_mapping.Foreground.cancelledFailure(arena, result)) |cancelled| { return finishCommandToolResult( null, false, &replay_transferred, result, cancelled, ); } if (try command_result_mapping.Foreground.nonZeroFailure(arena, result)) |failure| { return finishCommandToolResult( null, false, &replay_transferred, result, failure, ); } return finishCommandToolResult( null, false, &replay_transferred, result, .{ .model_output = result.output, .command_result_json = if (result.command_result) |command_result| try command_result.toJson(arena) else null, }, ); } fn commandAuthorityFingerprint( authority: command_admission.CommandExecutionAuthority, ) command_admission.AdmissionFingerprint { return switch (authority) { .direct_only => |fingerprint| fingerprint, .shell_allowed => |allowed| allowed.fingerprint, }; } fn dupedAdmittedFingerprint( arena: Allocator, authority: command_admission.CommandExecutionAuthority, ) !command_admission.AdmissionFingerprint { var admitted = commandAuthorityFingerprint(authority); admitted.command = try arena.dupe(u8, admitted.command); admitted.resolved_cwd = try arena.dupe(u8, admitted.resolved_cwd); admitted.environment = switch (admitted.environment) { .legacy => .legacy, .workspace_clean => .workspace_clean, .clean => |path| .{ .clean = try arena.dupe(u8, path) }, .user => |path| .{ .user = try arena.dupe(u8, path) }, }; return admitted; } fn sandboxScopeRequiredPreflight( arena: Allocator, authority: command_admission.CommandExecutionAuthority, timeout_started_ms: ?i64, ) !ToolExecutionResult { return .{ .status = .failure, .model_output = "", .sandbox_scope_required = .{ .phase = .preflight, .restricted_fingerprint = try dupedAdmittedFingerprint(arena, authority), .command_timeout_started_ms = timeout_started_ms, }, }; } fn sandboxScopeRequiredReactive( arena: Allocator, authority: command_admission.CommandExecutionAuthority, timeout_started_ms: ?i64, result: command_contract.RunCommandResult, ) !ToolExecutionResult { const command_result_json = if (result.command_result) |command_result| try command_result.toJson(arena) else null; return .{ .status = .failure, .model_output = result.output, .command_result_json = command_result_json, .sandbox_scope_required = .{ .phase = .reactive, .restricted_fingerprint = try dupedAdmittedFingerprint(arena, authority), .command_timeout_started_ms = timeout_started_ms, .restricted_model_output = result.output, .restricted_command_result_json = command_result_json, }, }; } const CommandReplayCaptureCallback = struct { alloc: Allocator, capture: ?*command_replay_store.Capture, had_accepted_output: bool = false, fn onChunk( raw_ctx: *anyopaque, lifecycle_id: ?types.ToolLifecycleId, stream: command_contract.CommandOutputStream, chunk: []const u8, ) !void { const self: *@This() = @ptrCast(@alignCast(raw_ctx)); return self.accept(lifecycle_id, stream, chunk); } fn accept( self: *@This(), lifecycle_id: ?types.ToolLifecycleId, stream: command_contract.CommandOutputStream, chunk: []const u8, ) !void { _ = lifecycle_id; if (chunk.len == 0) return; self.had_accepted_output = true; if (self.capture) |capture| { capture.appendAccepted(self.alloc, stream, chunk); } } }; const CommandReplayCaptureInit = struct { capture: ?*command_replay_store.Capture = null, unavailable: bool = false, }; fn initCommandReplayCapture( arena: Allocator, interactive: bool, inline_limit: usize, comparison_limit: usize, capability: ?*session_child_store.SessionChildCapability, continued_capture: ?*command_replay_store.Capture, continued_unavailable: bool, ) CommandReplayCaptureInit { if (!interactive) return .{}; if (continued_unavailable) return .{ .unavailable = true }; if (continued_capture) |capture| { capture.setComparisonLimit(comparison_limit); return .{ .capture = capture }; } const capture = command_replay_store.Capture.create( arena, inline_limit, capability, ) catch |err| { debug_trace.logf( "session", "command replay capture initialization unavailable err={s}", .{@errorName(err)}, ); return .{ .unavailable = true }; }; capture.setComparisonLimit(comparison_limit); return .{ .capture = capture }; } fn finishCommandToolResult( capture: ?*command_replay_store.Capture, replay_unavailable: bool, replay_transferred: *bool, process_result: ?command_contract.RunCommandResult, result: ToolExecutionResult, ) !ToolExecutionResult { var owned = result; if (process_result) |command_result| { if (commandProcessPresentation(command_result)) |presentation| { var memory = owned.tool_result_memory orelse types.ToolResultMemory{}; memory.command_process_presentation = presentation; owned.tool_result_memory = memory; } } if (owned.sandbox_scope_required) |*required| { required.command_replay_capture = capture; required.command_replay_unavailable = replay_unavailable; } else { if (replay_unavailable) { var memory = owned.tool_result_memory orelse types.ToolResultMemory{}; memory.command_output_replay = .unavailable; owned.tool_result_memory = memory; } owned.command_replay_capture = capture; } if (capture != null) replay_transferred.* = true; return owned; } fn commandProcessPresentation( result: command_contract.RunCommandResult, ) ?types.CommandProcessPresentation { const command_result = result.command_result orelse return null; const foreground = switch (command_result) { .foreground => |value| value, .background => return null, }; if (foreground.signal) |signal| return .{ .signal = signal }; if (foreground.exit_code) |exit_code| { if (exit_code != 0) return .{ .exit_code = exit_code }; } return null; } test "file mutations reject ordinary execution authority without mutating" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); try writeTestFile(tmp.dir, "workspace/existing.txt", "before\n"); const workspace = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "workspace"); defer alloc.free(workspace); var rt = TestRuntime{ .workspace_root = workspace }; defer rt.deinit(alloc); const cases = [_]struct { name: []const u8, arguments_json: []const u8, }{ .{ .name = "write_file", .arguments_json = "{\"path\":\"created.txt\",\"content\":\"new\\n\"}", }, .{ .name = "edit_file", .arguments_json = "{\"path\":\"existing.txt\",\"old_string\":\"before\",\"new_string\":\"after\"}", }, }; for (cases) |case| { var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const result = try executeToolCall(rt.context(), arena_state.allocator(), .{ .id = "runtime-only", .name = case.name, .arguments_json = case.arguments_json, }); try std.testing.expectEqual( tool_contracts.ToolExecutionStatus.failure, result.status, ); try std.testing.expectEqualStrings( "file mutation execution authority is invalid", result.model_output, ); } try std.testing.expectError( error.FileNotFound, tmp.dir.statFile(io_mod.getIo(), "workspace/created.txt", .{}), ); var existing = try tmp.dir.openFile(io_mod.getIo(), "workspace/existing.txt", .{}); defer existing.close(io_mod.getIo()); const content = try io_mod.readFileToEnd(alloc, &existing, 64); defer alloc.free(content); try std.testing.expectEqualStrings("before\n", content); } const SubagentProviderState = struct { runtime: Context, }; fn subagentProviderFailure( alloc: Allocator, operation_id: []const u8, error_code: []const u8, retryable: bool, ) Allocator.Error!subagent_tool_provider.Result { const body = subagent_tool_result.failureAlloc( alloc, operation_id, null, "rejected", error_code, retryable, null, ) catch |err| return switch (err) { error.OutOfMemory, error.WriteFailed => error.OutOfMemory, }; return .{ .status = .failure, .body = body }; } fn executeSubagentProvider( raw_context: ?*anyopaque, arena: Allocator, command: *subagent_domain.Command, invocation_id: []const u8, ) Allocator.Error!subagent_tool_provider.Result { const state: *SubagentProviderState = @ptrCast(@alignCast(raw_context.?)); const ctx = state.runtime; const host = ctx.subagent_host orelse return subagentProviderFailure(arena, invocation_id, "host_unavailable", false); const caller_id = ctx.subagent_caller_id orelse return subagentProviderFailure(arena, invocation_id, "caller_unavailable", false); const permission_admitted = host.admitModelCommand( arena, command, caller_id, ctx.permission_mode, ) catch |err| { if (err == error.OutOfMemory) return error.OutOfMemory; return subagentProviderFailure( arena, invocation_id, "host_failure", true, ); }; if (!permission_admitted) { return subagentProviderFailure( arena, invocation_id, "permission_escalation", false, ); } const identity_epoch = if (command.* == .inspect) 0 else switch (try persistedSubagentIdentity( arena, ctx.current_turn_messages, ctx.session.history.items, invocation_id, )) { .absent => host.issueOperationIdentity( arena, invocation_id, .model, ) catch |err| { if (err == error.OutOfMemory) return error.OutOfMemory; return try subagentProviderFailure( arena, invocation_id, "host_failure", true, ); }, .replay => |epoch| epoch, .corrupt => return subagentProviderFailure( arena, invocation_id, "host_failure", true, ), }; const output = host.execute(arena, command, .{ .caller_id = caller_id, .invocation_id = invocation_id, .parent_permission_mode = ctx.permission_mode, .root_user_intent_context = ctx.root_user_intent_context, .root_user_messages = ctx.root_user_messages, .root_user_evidence_complete = ctx.root_user_evidence_complete, .defaults = .{ .model = ctx.model, .effort = ctx.effort, .fast_mode = ctx.fast_mode, .conversation_language = ctx.session.languageSnapshot(), }, .max_result_bytes = ctx.max_tool_result_bytes, .timestamp_ms = io_mod.milliTimestamp(), .identity_epoch = identity_epoch, }) catch |err| { if (err == error.OutOfMemory) return error.OutOfMemory; const operation_id = if (command.* == .inspect) invocation_id else try subagent_tool_result.boundOperationIdAlloc( arena, invocation_id, .model, identity_epoch, ); return subagentProviderFailure(arena, operation_id, "host_failure", true); }; return .{ .status = if (std.mem.find(u8, output, "\"ok\":false") == null) .success else .failure, .body = output, }; } const PersistedSubagentIdentity = union(enum) { absent, replay: u64, corrupt, }; fn persistedSubagentIdentity( arena: Allocator, current_turn_messages: []const ChatMessage, history: []const session_runtime.HistoryTurn, invocation_id: []const u8, ) !PersistedSubagentIdentity { switch (try currentTurnSubagentIdentity( arena, current_turn_messages, invocation_id, )) { .absent => {}, .replay => |epoch| return .{ .replay = epoch }, .corrupt => return .corrupt, } var turn_index = history.len; while (turn_index != 0) { turn_index -= 1; const execution = switch (history[turn_index]) { .assistant => |entry| entry.execution, .background_command => |entry| entry.execution, .interrupted => |entry| entry.execution, .compacted_summary => continue, }; var step_index = execution.tool_steps.len; while (step_index != 0) { step_index -= 1; const step = execution.tool_steps[step_index]; var call_index = step.tool_calls.len; while (call_index != 0) { call_index -= 1; const persisted_call = step.tool_calls[call_index]; if (!std.mem.eql(u8, persisted_call.id, invocation_id)) continue; var matching_calls: usize = 0; for (step.tool_calls) |candidate| { if (std.mem.eql(u8, candidate.id, invocation_id)) { matching_calls += 1; } } if (matching_calls != 1) return .corrupt; if (!canonicalSubagentCall(persisted_call)) return .corrupt; const result = canonicalPersistedSubagentResult( step.tool_results, invocation_id, ) orelse return .corrupt; const epoch = try persistedSubagentEpoch( arena, result.output, result.status, invocation_id, ) orelse return .corrupt; return .{ .replay = epoch }; } } } return .absent; } fn currentTurnSubagentIdentity( arena: Allocator, messages: []const ChatMessage, invocation_id: []const u8, ) !PersistedSubagentIdentity { var result_index = messages.len; while (result_index != 0) { result_index -= 1; const result = messages[result_index]; if (result.role != .tool) continue; const result_call_id = result.tool_call_id orelse continue; if (!std.mem.eql(u8, result_call_id, invocation_id)) continue; const call = canonicalCurrentTurnSubagentCall( messages, result_index, invocation_id, ) orelse return .corrupt; if (!canonicalSubagentCall(call) or result.tool_name == null or !std.mem.eql(u8, result.tool_name.?, subagent_tool_name) or result.content == null or result.tool_result_status == null or result.tool_calls.len != 0 or result.images.len != 0 or result.permission_feedback) { return .corrupt; } const epoch = try persistedSubagentEpoch( arena, result.content.?, result.tool_result_status.?, invocation_id, ) orelse return .corrupt; return .{ .replay = epoch }; } return .absent; } fn canonicalCurrentTurnSubagentCall( messages: []const ChatMessage, result_index: usize, invocation_id: []const u8, ) ?ToolCall { var assistant_index = result_index; while (assistant_index != 0) { assistant_index -= 1; switch (messages[assistant_index].role) { .tool => continue, .assistant => break, .system, .user => return null, } } if (messages[assistant_index].role != .assistant) return null; var matching_call: ?ToolCall = null; for (messages[assistant_index].tool_calls) |call| { if (!std.mem.eql(u8, call.id, invocation_id)) continue; if (matching_call != null) return null; matching_call = call; } var matching_results: usize = 0; var index = assistant_index + 1; while (index < messages.len and messages[index].role == .tool) : (index += 1) { const call_id = messages[index].tool_call_id orelse continue; if (std.mem.eql(u8, call_id, invocation_id)) matching_results += 1; } if (matching_results != 1) return null; return matching_call; } fn canonicalSubagentCall(call: ToolCall) bool { return std.mem.eql(u8, call.name, subagent_tool_name) and call.argument_integrity == .valid and call.provider_result == null and call.final_identity == .valid and call.provenance == .fx_local; } fn canonicalPersistedSubagentResult( results: []const session_runtime.PersistedToolResult, call_id: []const u8, ) ?session_runtime.PersistedToolResult { var matching: ?session_runtime.PersistedToolResult = null; for (results) |result| { if (!std.mem.eql(u8, result.tool_call_id, call_id)) continue; if (matching != null or !std.mem.eql(u8, result.tool_name, subagent_tool_name) or result.provider_native) { return null; } matching = result; } return matching; } fn persistedSubagentEpoch( arena: Allocator, output: []const u8, status: session_runtime.PersistedToolStatus, invocation_id: []const u8, ) !?u64 { var parsed = std.json.parseFromSlice(std.json.Value, arena, output, .{}) catch |err| return switch (err) { error.OutOfMemory => error.OutOfMemory, else => null, }; defer parsed.deinit(); if (parsed.value != .object) return null; const ok_value = parsed.value.object.get("ok") orelse return null; if (ok_value != .bool) return null; const expected_status: session_runtime.PersistedToolStatus = if (ok_value.bool) .success else .failure; if (status != expected_status) return null; const operation_value = parsed.value.object.get("operation_id") orelse return null; if (operation_value != .string) return null; const child_value = parsed.value.object.get("child_id") orelse return null; if (child_value != .null and child_value != .string) return null; const status_value = parsed.value.object.get("status") orelse return null; if (status_value != .string or status_value.string.len == 0) return null; const error_value = parsed.value.object.get("error_code") orelse return null; if (error_value != .null and error_value != .string) return null; const retryable_value = parsed.value.object.get("retryable") orelse return null; if (retryable_value != .bool) return null; const requested_value = parsed.value.object.get("requested") orelse return null; if (requested_value != .null and requested_value != .object) return null; const cursor_value = parsed.value.object.get("cursor") orelse return null; if (cursor_value != .null and cursor_value != .string) return null; const operation_id = operation_value.string; const identity = subagent_tool_result.parseBoundOperationId(operation_id) orelse return null; if (identity.source != .model or identity.authority != .manager or identity.epoch == 0 or !subagent_tool_result.boundOperationMatchesInvocation( operation_id, invocation_id, .model, )) { return null; } return identity.epoch; } fn splitConversationLanguage(language: session_runtime.ConversationLanguage) task_helpers.ConversationLanguage { return task_helpers.ConversationLanguage.fromSlice(language.view()) catch task_helpers.ConversationLanguage.default(); } fn noopOutput(_: *anyopaque, _: ?types.ToolLifecycleId, _: command_contract.CommandOutputStream, _: []const u8) !void {} fn noopBackgroundReady(_: *anyopaque, _: u64, _: []const u8) void {} const test_tool_registry = tool_dispatch.Registry{ .tools = &.{ test_builtin_tools.list_files, test_builtin_tools.glob_files, test_builtin_tools.grep_files, test_builtin_tools.read_file, test_builtin_tools.write_file, test_builtin_tools.edit_file, test_builtin_tools.delete_file, test_builtin_tools.rename_file, test_builtin_tools.copy_file, test_builtin_tools.create_folder, test_builtin_tools.file_info, test_builtin_tools.memory, test_builtin_tools.semantic_search, test_builtin_tools.open_file, test_builtin_tools.web_fetch, test_builtin_tools.web_search, test_builtin_tools.terminal, test_builtin_tools.skill, test_builtin_tools.install_skill, test_builtin_tools.subagent, test_builtin_tools.mcp_search_tools, test_builtin_tools.mcp_select_tool, test_builtin_tools.ask_user_question, test_builtin_tools.read_tool_result, } }; fn matchesTestRunCommandCompatibility(command: []const u8) bool { return std.mem.startsWith(u8, command, "fx-compatibility-probe"); } fn executeTestRunCommandCompatibility( ctx: tool_dispatch.DispatchContext, _: []const u8, ) tool_dispatch.DispatchError!tool_dispatch.ToolResult { return .{ .success = try ctx.allocator.dupe(u8, "registered compatibility\n") }; } const test_compatible_tool = blk: { var tool = test_builtin_tools.install_skill; tool.run_command_compatibility = .{ .matches = matchesTestRunCommandCompatibility, .execute = executeTestRunCommandCompatibility, }; break :blk tool; }; fn executeFailingRunCommandCompatibility( ctx: tool_dispatch.DispatchContext, _: []const u8, ) tool_dispatch.DispatchError!tool_dispatch.ToolResult { return .{ .failure = try tool_result_errors.formatToolExecutionErrorJson( ctx.allocator, "terminal", error.SkillInstallFailed, ) }; } const test_failing_compatible_tool = blk: { var tool = test_builtin_tools.install_skill; tool.run_command_compatibility = .{ .matches = matchesTestRunCommandCompatibility, .execute = executeFailingRunCommandCompatibility, }; break :blk tool; }; const test_compatibility_registry = tool_dispatch.Registry{ .tools = &.{ test_builtin_tools.terminal, test_compatible_tool, } }; const test_failing_compatibility_registry = tool_dispatch.Registry{ .tools = &.{ test_builtin_tools.terminal, test_failing_compatible_tool, } }; fn gatherNoopTestContext(_: Allocator, _: context_contract.InitialContextInput) context_contract.ProviderError!context_contract.ProviderContext { return .{}; } fn appendNoopTestStaticContext(_: context_contract.StaticContextInput, _: Allocator, _: *std.ArrayList(ChatMessage)) context_contract.ProviderError!void {} fn appendNoopTestTransientContext(_: context_contract.TransientContextInput, _: Allocator, _: *std.ArrayList(ChatMessage)) context_contract.ProviderError!void {} const test_context_registry = context_contract.Registry{ .default_provider = .{ .id = "test.tool_runtime_context", .gather_project_context_fn = gatherNoopTestContext, .select_applicable_project_context_fn = context_contract.selectNoApplicableProjectContext, .append_static_fn = appendNoopTestStaticContext, .append_transient_fn = appendNoopTestTransientContext, } }; const test_review_calls = [_]ToolCall{ .{ .id = "test-review", .name = "terminal", .arguments_json = "{\"action\":\"exec\",\"command\":\"printf test\"}" }, }; const test_review_root_messages = [_][]const u8{"test root request"}; fn testReviewTurn() permission_auto_classifier.ReviewTurnContext { return .{ .model = "openai/gpt-5", .pending_assistant = .{ .role = .assistant, .tool_calls = &test_review_calls }, .target_call_id = "test-review", .origin = .root, .current_root_request = test_review_root_messages[0], }; } const TestRuntime = struct { agent_stream_provider: agent_stream_provider.Provider = agent_stream_provider.unavailable_provider, tool_registry: tool_dispatch.Registry = test_tool_registry, worker: WorkerRuntime = .{}, background: BackgroundRuntime = .{}, session: SessionRuntime = .{ .max_history_turns = 8 }, subagent_host: ?*subagent_tool_host.Runtime = null, subagent_caller_id: ?[]const u8 = null, model: []const u8 = "", session_allocator: Allocator = std.testing.allocator, workspace_root: []const u8 = "/tmp", ignored_list_entries: []const []const u8 = &.{}, skills_dir: []const u8 = "", sandbox_backend: sandbox.BackendKind = .none, tracker: ?*change_tracker.ChangeTracker = null, permission_rules: types.PermissionRuleSet = .{}, permission_grants: []const PermissionGrant = &.{}, session_grants: []const PermissionGrant = &.{}, cancel_flag: ?*std.atomic.Value(bool) = null, permission_mode: PermissionMode = .ask, max_command_output_bytes: usize = 64 * 1024, max_tool_result_bytes: usize = 64 * 1024, api_key: []const u8 = "", gateway_team: ?[]const u8 = null, gateway_retry_count: usize = 0, gateway_chat_url: []const u8 = "", context_limits: context_limits.Values = .{}, command_artifact_dir: ?[]const u8 = null, session_child_capability: ?*session_child_store.SessionChildCapability = null, command_timeout_ms: ?usize = null, interactive: bool = true, mcp_ctx: ?*anyopaque = null, mcp_has_tool: ?tool_mcp_runtime.HasToolFn = null, mcp_validate_tool: ?tool_mcp_runtime.ValidateToolFn = null, mcp_call_tool: ?tool_mcp_runtime.CallToolFn = null, mcp_search_tools: ?tool_mcp_runtime.SearchToolsFn = null, mcp_tool_schema: ?tool_mcp_runtime.ToolSchemaFn = null, mcp_call_feature: ?tool_mcp_runtime.FeatureCallFn = null, mcp_input_responder: ?tool_mcp_runtime.InputResponder = null, advertised_dynamic_tool_names: []const []const u8 = &.{}, auto_classifier: permission_auto_classifier.Classifier = .disabled(), web_search_runtime_ready: bool = false, web_search_backend: ?tool_dispatch.WebSearchBackend = null, web_search_progress_ctx: ?*anyopaque = null, on_web_search_progress: ?tool_dispatch.WebSearchProgressFn = null, web_fetch_runtime: ?*web_fetch_runtime.Runtime = null, web_fetch_artifact_store: ?*web_fetch_artifacts.Store = null, web_fetch_artifact_error: ?anyerror = null, web_fetch_progress_ctx: ?*anyopaque = null, on_web_fetch_progress: ?tool_dispatch.WebFetchProgressFn = null, workspace_executor: ?js_host_workspace.Executor = null, host_sandbox_default: tool_admission.HostSandboxDefault = .none, fn deinit(self: *TestRuntime, alloc: Allocator) void { self.worker.deinit(alloc); self.background.deinit(alloc); self.session.deinit(alloc); } fn context(self: *TestRuntime) Context { return .{ .workspace_root = self.workspace_root, .ignored_list_entries = self.ignored_list_entries, .max_list_entries = 100, .max_read_file_bytes = 64 * 1024, .max_read_file_lines = 400, .max_read_file_line_len = 2000, .max_command_output_bytes = self.max_command_output_bytes, .max_tool_result_bytes = self.max_tool_result_bytes, .api_key = self.api_key, .agent_stream_provider = self.agent_stream_provider, .gateway_team = self.gateway_team, .model = self.model, .gateway_retry_count = self.gateway_retry_count, .gateway_chat_url = self.gateway_chat_url, .agent_step_limit = 0, .permission_mode = self.permission_mode, .permission_grants = self.permission_grants, .session_grants = self.session_grants, .permission_rules = self.permission_rules, .tool_registry = self.tool_registry, .subagent_host = self.subagent_host, .subagent_caller_id = self.subagent_caller_id, .worker = &self.worker, .permission_prompter = if (self.interactive) tool_admission.workerPrompter(&self.worker) else null, .cancel_flag = self.cancel_flag, .background = &self.background, .session = &self.session, .session_allocator = self.session_allocator, .skills_dir = self.skills_dir, .context_registry = test_context_registry, .context_limits = self.context_limits, .output_chunk_ctx = undefined, .on_output_chunk = noopOutput, .background_url_ctx = undefined, .on_background_url_ready = noopBackgroundReady, .command_artifact_dir = self.command_artifact_dir, .session_child_capability = self.session_child_capability, .command_timeout_ms = self.command_timeout_ms, .sandbox_backend = self.sandbox_backend, .tracker = self.tracker, .mcp_ctx = self.mcp_ctx, .mcp_has_tool = self.mcp_has_tool, .mcp_validate_tool = self.mcp_validate_tool, .mcp_call_tool = self.mcp_call_tool, .mcp_search_tools = self.mcp_search_tools, .mcp_tool_schema = self.mcp_tool_schema, .mcp_call_feature = self.mcp_call_feature, .mcp_input_responder = self.mcp_input_responder, .advertised_dynamic_tool_names = self.advertised_dynamic_tool_names, .auto_classifier = self.auto_classifier, .permission_review_turn = testReviewTurn(), .web_fetch_runtime = self.web_fetch_runtime, .web_fetch_artifact_store = self.web_fetch_artifact_store, .web_fetch_artifact_error = self.web_fetch_artifact_error, .web_search_runtime_ready = self.web_search_runtime_ready, .web_search_backend = self.web_search_backend, .web_search_progress_ctx = self.web_search_progress_ctx, .on_web_search_progress = self.on_web_search_progress, .web_fetch_progress_ctx = self.web_fetch_progress_ctx, .on_web_fetch_progress = self.on_web_fetch_progress, .workspace_executor = self.workspace_executor, .host_sandbox_default = self.host_sandbox_default, .interactive = self.interactive, }; } }; fn subagentTestState( alloc: Allocator, id: []const u8, workspace: []const u8, ) !session_codec_mod.DurableSessionState { const owned_id = try alloc.dupe(u8, id); errdefer alloc.free(owned_id); const origin = try alloc.dupe(u8, workspace); errdefer alloc.free(origin); const current = try alloc.dupe(u8, workspace); errdefer alloc.free(current); const model = try alloc.dupe(u8, "test/model"); return .{ .id = owned_id, .origin_workspace_root = origin, .workspace_root = current, .created_at_ms = 1, .updated_at_ms = 1, .conversation_language = session_runtime.ConversationLanguage.literal("en"), .preferences = .{ .model = model, .effort = types.ReasoningEffort.literal("high"), .fast_mode = false }, .history = &.{}, .total_input_tokens = 0, .total_output_tokens = 0, }; } const SubagentTestEnvironment = struct { tmp: std.testing.TmpDir, home: []u8, workspace: []u8, store: session_store.Store, fn init(alloc: Allocator) !SubagentTestEnvironment { var tmp = std.testing.tmpDir(.{}); errdefer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "home/.fx"); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); const home = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "home"); errdefer alloc.free(home); const workspace = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "workspace"); errdefer alloc.free(workspace); return .{ .tmp = tmp, .home = home, .workspace = workspace, .store = try session_store.Store.initFromHome(alloc, home, workspace), }; } fn deinit(self: *SubagentTestEnvironment, alloc: Allocator) void { self.store.deinit(alloc); alloc.free(self.home); alloc.free(self.workspace); self.tmp.cleanup(); self.* = undefined; } fn createSession( self: *SubagentTestEnvironment, alloc: Allocator, id: []const u8, ) !void { var state = try subagentTestState(alloc, id, self.workspace); defer state.deinit(alloc); var loaded = try self.store.startWritableSession(alloc, state); loaded.deinit(alloc); } }; const SubagentTestAuthority = struct { root_id: []const u8, fn resolver(self: *SubagentTestAuthority) subagent_authority.HostResolver { return .{ .context = self, .resolve_fn = resolve }; } fn resolve( raw: ?*anyopaque, alloc: Allocator, root_id: []const u8, ) subagent_authority.HostResolveError!subagent_authority.HostAuthority { const self: *SubagentTestAuthority = @ptrCast(@alignCast(raw.?)); if (!std.mem.eql(u8, self.root_id, root_id)) { return error.HostAuthorityUnavailable; } return subagent_tool_host.captureHostAuthority( alloc, .{ .tool_set = .{ .registry = test_tool_registry, .order = &.{}, .read_only_tool_names = &.{}, }, .mode = .full, }, .none, &.{}, .{}, &.{}, ); } }; fn subagentResultStringAlloc( alloc: Allocator, result_json: []const u8, field: []const u8, ) ![]u8 { var parsed = try std.json.parseFromSlice(std.json.Value, alloc, result_json, .{}); defer parsed.deinit(); const value = parsed.value.object.get(field) orelse return error.TestUnexpectedResult; if (value != .string) return error.TestUnexpectedResult; return alloc.dupe(u8, value.string); } fn persistSubagentToolResult( runtime: *TestRuntime, alloc: Allocator, call: ToolCall, result: ToolExecutionResult, ) !void { var calls = [_]ToolCall{call}; var results = [_]session_runtime.PersistedToolResult{.{ .tool_call_id = @constCast(call.id), .tool_name = @constCast(call.name), .status = switch (result.status) { .success => .success, .failure => .failure, }, .output = @constCast(result.model_output), .output_bytes = result.model_output.len, .stored_output_bytes = result.model_output.len, }}; var steps = [_]session_runtime.ToolExecutionStep{.{ .tool_calls = calls[0..], .tool_results = results[0..], }}; const turn: session_runtime.HistoryTurn = .{ .assistant = .{ .user = .{ .text = @constCast("test"), .images = &.{} }, .assistant = @constCast(""), .execution = .{ .tool_steps = steps[0..] }, } }; try runtime.session.appendHistoryEntry(alloc, turn); } fn expectSingleSubagentCreateEffects( alloc: Allocator, env: *SubagentTestEnvironment, child_id: []const u8, ) !void { var child_ids = try env.store.listSubagentControlSessionIds(alloc); defer { for (child_ids.items) |id| alloc.free(id); child_ids.deinit(alloc); } try std.testing.expectEqual(@as(usize, 2), child_ids.items.len); var capability = try env.store.openSubagentControlCapabilityReadOnly( alloc, child_id, .{}, ); defer capability.deinit(); const control = subagent_control_store.Store{ .capability = &capability, .expected_child_id = child_id, }; var record = try control.load(alloc); defer record.deinit(alloc); try std.testing.expectEqual(@as(usize, 1), record.operations.len); try std.testing.expectEqual(@as(usize, 1), record.events.len); try std.testing.expectEqual(@as(usize, 0), record.queue.len); const communications = subagent_communication_store.Store{ .capability = &capability, .expected_session_id = child_id, }; if (try communications.loadOptional(alloc)) |loaded| { var ledger = loaded; defer ledger.deinit(alloc); return error.TestUnexpectedCommunicationEffect; } var child = try env.store.loadReadOnly(alloc, child_id); defer child.deinit(alloc); try std.testing.expectEqual(@as(usize, 0), child.history.len); } test "subagent production identity inspections leave no mutation reservations" { const alloc = std.testing.allocator; const root_id = "01J00000000000000000000000"; var env = try SubagentTestEnvironment.init(alloc); defer env.deinit(alloc); try env.createSession(alloc, root_id); var test_authority = SubagentTestAuthority{ .root_id = root_id }; const host = try subagent_tool_host.Runtime.create( alloc, &env.store, root_id, test_authority.resolver(), .{}, ); defer host.deinit(); var runtime = TestRuntime{ .workspace_root = env.workspace, .subagent_host = host, .subagent_caller_id = root_id, .model = "test/model", }; defer runtime.deinit(alloc); var create_arena = std.heap.ArenaAllocator.init(alloc); defer create_arena.deinit(); const created = try executeToolCall(runtime.context(), create_arena.allocator(), .{ .id = "inspect-fixture-create", .name = "subagent", .arguments_json = \\{"command":{"create":{"name":"inspect-fixture","mode":"persistent"}}} , }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.success, created.status); const child_id = try subagentResultStringAlloc(alloc, created.model_output, "child_id"); defer alloc.free(child_id); const inspect_args = try std.fmt.allocPrint( alloc, "{{\"command\":{{\"inspect\":{{\"id\":\"{s}\",\"sections\":[\"status\"]}}}}}}", .{child_id}, ); defer alloc.free(inspect_args); for (0..3) |index| { var invocation_buffer: [64]u8 = undefined; const invocation_id = try std.fmt.bufPrint( &invocation_buffer, "inspect-{d}", .{index}, ); var inspect_arena = std.heap.ArenaAllocator.init(alloc); defer inspect_arena.deinit(); const inspected = try executeToolCall( runtime.context(), inspect_arena.allocator(), .{ .id = invocation_id, .name = "subagent", .arguments_json = inspect_args, }, ); try std.testing.expectEqual( tool_contracts.ToolExecutionStatus.success, inspected.status, ); } const configure_args = try std.fmt.allocPrint( alloc, "{{\"command\":{{\"configure\":{{\"id\":\"{s}\",\"name\":\"renamed\"}}}}}}", .{child_id}, ); defer alloc.free(configure_args); var configure_arena = std.heap.ArenaAllocator.init(alloc); defer configure_arena.deinit(); const configured = try executeToolCall( runtime.context(), configure_arena.allocator(), .{ .id = "mutation-after-inspections", .name = "subagent", .arguments_json = configure_args, }, ); try std.testing.expectEqual( tool_contracts.ToolExecutionStatus.success, configured.status, ); var root_capability = try env.store.openSubagentControlCapabilityReadOnly( alloc, root_id, .{}, ); defer root_capability.deinit(); const identity_store = subagent_create_store.Store{ .capability = &root_capability, .expected_root_id = root_id, }; var identities = (try identity_store.loadOptional(alloc)).?; defer identities.deinit(alloc); try std.testing.expectEqual( @as(usize, 0), identities.outstanding_operations.len, ); } test "subagent production stable failures leave no mutation reservations" { const alloc = std.testing.allocator; const root_id = "01J00000000000000000000000"; var env = try SubagentTestEnvironment.init(alloc); defer env.deinit(alloc); try env.createSession(alloc, root_id); var test_authority = SubagentTestAuthority{ .root_id = root_id }; const host = try subagent_tool_host.Runtime.create( alloc, &env.store, root_id, test_authority.resolver(), .{}, ); defer host.deinit(); var runtime = TestRuntime{ .workspace_root = env.workspace, .subagent_host = host, .subagent_caller_id = root_id, .model = "test/model", }; defer runtime.deinit(alloc); var create_arena = std.heap.ArenaAllocator.init(alloc); defer create_arena.deinit(); const created = try executeToolCall(runtime.context(), create_arena.allocator(), .{ .id = "stable-failure-fixture-create", .name = "subagent", .arguments_json = \\{"command":{"create":{"name":"stable-failure-fixture","mode":"persistent"}}} , }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.success, created.status); const child_id = try subagentResultStringAlloc(alloc, created.model_output, "child_id"); defer alloc.free(child_id); const missing_args = \\{"command":{"configure":{"id":"01J00000000000000000009999","name":"never applied"}}} ; for (0..3) |index| { var invocation_buffer: [64]u8 = undefined; const invocation_id = try std.fmt.bufPrint( &invocation_buffer, "stable-model-failure-{d}", .{index}, ); var failure_arena = std.heap.ArenaAllocator.init(alloc); defer failure_arena.deinit(); const rejected = try executeToolCall( runtime.context(), failure_arena.allocator(), .{ .id = invocation_id, .name = "subagent", .arguments_json = missing_args, }, ); try std.testing.expectEqual( tool_contracts.ToolExecutionStatus.failure, rejected.status, ); try expectContains( rejected.model_output, "\"error_code\":\"child_unavailable\"", ); } const configure_args = try std.fmt.allocPrint( alloc, "{{\"command\":{{\"configure\":{{\"id\":\"{s}\",\"name\":\"still writable\"}}}}}}", .{child_id}, ); defer alloc.free(configure_args); var configure_arena = std.heap.ArenaAllocator.init(alloc); defer configure_arena.deinit(); const configured = try executeToolCall( runtime.context(), configure_arena.allocator(), .{ .id = "model-mutation-after-stable-failures", .name = "subagent", .arguments_json = configure_args, }, ); try std.testing.expectEqual( tool_contracts.ToolExecutionStatus.success, configured.status, ); var root_capability = try env.store.openSubagentControlCapabilityReadOnly( alloc, root_id, .{}, ); defer root_capability.deinit(); const identity_store = subagent_create_store.Store{ .capability = &root_capability, .expected_root_id = root_id, }; var identities = (try identity_store.loadOptional(alloc)).?; defer identities.deinit(alloc); try std.testing.expectEqual( @as(usize, 0), identities.outstanding_operations.len, ); } const SubagentAgentLoopExecutor = struct { runtime: *TestRuntime, fn execute( raw: *anyopaque, request: tool_contracts.ToolExecutionRequest, ) !ToolExecutionResult { const self: *@This() = @ptrCast(@alignCast(raw)); return executeToolCallAuthorized(self.runtime.context(), request); } }; test "subagent production identity replays one invocation within an active agent turn" { const alloc = std.testing.allocator; const root_id = "01J00000000000000000000000"; const invocation_id = "production-active-turn-replay"; const create_args = \\{"command":{"create":{"name":"active-turn-worker","mode":"persistent"}}} ; const changed_args = \\{"command":{"create":{"name":"changed-active-turn-worker","mode":"persistent"}}} ; var repeated_calls = [_]ToolCall{.{ .id = invocation_id, .name = "subagent", .arguments_json = create_args, }}; var changed_calls = [_]ToolCall{.{ .id = invocation_id, .name = "subagent", .arguments_json = changed_args, }}; const completions = [_]agent_test_support.FakeCompletion{ .{ .tool_calls = repeated_calls[0..] }, .{ .tool_calls = repeated_calls[0..] }, .{ .tool_calls = changed_calls[0..] }, .{ .content = "active turn complete" }, }; var env = try SubagentTestEnvironment.init(alloc); defer env.deinit(alloc); try env.createSession(alloc, root_id); var test_authority = SubagentTestAuthority{ .root_id = root_id }; const host = try subagent_tool_host.Runtime.create( alloc, &env.store, root_id, test_authority.resolver(), .{}, ); defer host.deinit(); var runtime = TestRuntime{ .workspace_root = env.workspace, .subagent_host = host, .subagent_caller_id = root_id, .model = "test/model", }; defer runtime.deinit(alloc); var executor = SubagentAgentLoopExecutor{ .runtime = &runtime }; var test_deps = agent_test_support.FakeAgentRuntimeDeps.init(alloc); defer test_deps.deinit(); test_deps.tool_execution_override = .{ .context = &executor, .execute_fn = SubagentAgentLoopExecutor.execute, }; var gateway = agent_test_support.FakeGateway.init(alloc, &completions); defer gateway.deinit(); var fixture = agent_test_support.PromptFixture{ .workspace_root = env.workspace, }; var config = fixture.config(); config.agent_step_limit = completions.len; try agent_test_support.runFakePrompt( &gateway, &test_deps, config, fixture.job(), ); try std.testing.expectEqual(completions.len, gateway.index); try std.testing.expectEqual(@as(usize, 1), test_deps.history_turns.items.len); const history = test_deps.history_turns.items[0].assistant; try std.testing.expectEqual( @as(usize, 3), history.execution.tool_steps.len, ); const first = history.execution.tool_steps[0].tool_results[0]; const replay = history.execution.tool_steps[1].tool_results[0]; const conflict = history.execution.tool_steps[2].tool_results[0]; try std.testing.expectEqualStrings(first.output, replay.output); try std.testing.expectEqual( session_runtime.PersistedToolStatus.success, first.status, ); try std.testing.expectEqual( session_runtime.PersistedToolStatus.success, replay.status, ); try std.testing.expectEqual( session_runtime.PersistedToolStatus.failure, conflict.status, ); try expectContains( conflict.output, "\"error_code\":\"operation_conflict\"", ); const first_operation_id = try subagentResultStringAlloc( alloc, first.output, "operation_id", ); defer alloc.free(first_operation_id); const replay_operation_id = try subagentResultStringAlloc( alloc, replay.output, "operation_id", ); defer alloc.free(replay_operation_id); try std.testing.expectEqualStrings( first_operation_id, replay_operation_id, ); const child_id = try subagentResultStringAlloc( alloc, first.output, "child_id", ); defer alloc.free(child_id); try expectSingleSubagentCreateEffects( alloc, &env, child_id, ); var root_capability = try env.store.openSubagentControlCapabilityReadOnly( alloc, root_id, .{}, ); defer root_capability.deinit(); const identity_store = subagent_create_store.Store{ .capability = &root_capability, .expected_root_id = root_id, }; var identities = (try identity_store.loadOptional(alloc)).?; defer identities.deinit(alloc); try std.testing.expectEqual(@as(usize, 1), identities.entries.len); try std.testing.expectEqual( @as(usize, 0), identities.outstanding_operations.len, ); } test "subagent production identity rejects malformed active-turn evidence before effects" { const alloc = std.testing.allocator; const root_id = "01J00000000000000000000000"; const invocation_id = "production-active-turn-corrupt"; const create_args = \\{"command":{"create":{"name":"must-not-exist","mode":"persistent"}}} ; var calls = [_]ToolCall{.{ .id = invocation_id, .name = "subagent", .arguments_json = create_args, }}; const malformed_output = \\{"ok":true,"operation_id":"production-active-turn-corrupt","child_id":null,"status":"created","error_code":null,"retryable":false,"requested":null,"cursor":null} ; const messages = [_]ChatMessage{ .{ .role = .assistant, .tool_calls = calls[0..] }, .{ .role = .tool, .content = malformed_output, .tool_call_id = invocation_id, .tool_name = "subagent", .tool_result_status = .success, }, }; var env = try SubagentTestEnvironment.init(alloc); defer env.deinit(alloc); try env.createSession(alloc, root_id); var test_authority = SubagentTestAuthority{ .root_id = root_id }; const host = try subagent_tool_host.Runtime.create( alloc, &env.store, root_id, test_authority.resolver(), .{}, ); defer host.deinit(); var runtime = TestRuntime{ .workspace_root = env.workspace, .subagent_host = host, .subagent_caller_id = root_id, .model = "test/model", }; defer runtime.deinit(alloc); var ctx = runtime.context(); ctx.current_turn_messages = &messages; var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const result = try executeToolCall(ctx, arena_state.allocator(), calls[0]); try std.testing.expectEqual( tool_contracts.ToolExecutionStatus.failure, result.status, ); try expectContains(result.model_output, "\"error_code\":\"host_failure\""); var session_ids = try env.store.listSubagentControlSessionIds(alloc); defer { for (session_ids.items) |id| alloc.free(id); session_ids.deinit(alloc); } try std.testing.expectEqual(@as(usize, 1), session_ids.items.len); var root_capability = try env.store.openSubagentControlCapabilityReadOnly( alloc, root_id, .{}, ); defer root_capability.deinit(); const identity_store = subagent_create_store.Store{ .capability = &root_capability, .expected_root_id = root_id, }; try std.testing.expect((try identity_store.loadOptional(alloc)) == null); } test "subagent identity evidence prefers canonical active-turn results and authenticates bindings" { const alloc = std.testing.allocator; const invocation_id = "active-turn-identity-evidence"; const current_operation_id = try subagent_tool_result.boundOperationIdAlloc( alloc, invocation_id, .model, 7, ); defer alloc.free(current_operation_id); const history_operation_id = try subagent_tool_result.boundOperationIdAlloc( alloc, invocation_id, .model, 3, ); defer alloc.free(history_operation_id); const current_output = try subagent_tool_result.outcomeAlloc(alloc, .{ .ok = true, .operation_id = current_operation_id, .child_id = "current-child", .status = "created", .error_code = null, .retryable = false, .requested_json = "null", .cursor = null, }); defer alloc.free(current_output); const history_output = try subagent_tool_result.outcomeAlloc(alloc, .{ .ok = true, .operation_id = history_operation_id, .child_id = "history-child", .status = "created", .error_code = null, .retryable = false, .requested_json = "null", .cursor = null, }); defer alloc.free(history_output); var current_calls = [_]ToolCall{.{ .id = invocation_id, .name = "subagent", .arguments_json = "{}", }}; var current_messages = [_]ChatMessage{ .{ .role = .assistant, .tool_calls = current_calls[0..] }, .{ .role = .tool, .content = current_output, .tool_call_id = invocation_id, .tool_name = "subagent", .tool_result_status = .success, }, }; var history_calls = [_]ToolCall{current_calls[0]}; var history_results = [_]session_runtime.PersistedToolResult{.{ .tool_call_id = @constCast(invocation_id), .tool_name = @constCast("subagent"), .status = .success, .output = history_output, .output_bytes = history_output.len, .stored_output_bytes = history_output.len, }}; var history_steps = [_]session_runtime.ToolExecutionStep{.{ .tool_calls = history_calls[0..], .tool_results = history_results[0..], }}; const history = [_]session_runtime.HistoryTurn{.{ .assistant = .{ .user = .{ .text = @constCast("test") }, .assistant = @constCast(""), .execution = .{ .tool_steps = history_steps[0..] }, } }}; switch (try persistedSubagentIdentity( alloc, ¤t_messages, &history, invocation_id, )) { .replay => |epoch| try std.testing.expectEqual(@as(u64, 7), epoch), .absent, .corrupt => return error.TestUnexpectedResult, } const human_operation_id = try subagent_tool_result.boundOperationIdAlloc( alloc, invocation_id, .human, 7, ); defer alloc.free(human_operation_id); const wrong_digest_id = try subagent_tool_result.boundOperationIdAlloc( alloc, "different-invocation", .model, 7, ); defer alloc.free(wrong_digest_id); const zero_epoch_id = try subagent_tool_result.boundOperationIdAlloc( alloc, invocation_id, .model, 0, ); defer alloc.free(zero_epoch_id); const process_local_id = try std.fmt.allocPrint( alloc, "fxop:{s}", .{current_operation_id["fxop:2:".len..]}, ); defer alloc.free(process_local_id); const invalid_operation_ids = [_][]const u8{ human_operation_id, wrong_digest_id, zero_epoch_id, process_local_id, }; for (invalid_operation_ids) |operation_id| { const invalid_output = try subagent_tool_result.outcomeAlloc(alloc, .{ .ok = true, .operation_id = operation_id, .child_id = "invalid-child", .status = "created", .error_code = null, .retryable = false, .requested_json = "null", .cursor = null, }); defer alloc.free(invalid_output); current_messages[1].content = invalid_output; switch (try persistedSubagentIdentity( alloc, ¤t_messages, &history, invocation_id, )) { .corrupt => {}, .absent, .replay => return error.TestUnexpectedResult, } } const invalid_requested_output = try subagent_tool_result.outcomeAlloc(alloc, .{ .ok = true, .operation_id = current_operation_id, .child_id = "invalid-child", .status = "created", .error_code = null, .retryable = false, .requested_json = "\"invalid\"", .cursor = null, }); defer alloc.free(invalid_requested_output); current_messages[1].content = invalid_requested_output; switch (try persistedSubagentIdentity( alloc, ¤t_messages, &history, invocation_id, )) { .corrupt => {}, .absent, .replay => return error.TestUnexpectedResult, } current_messages[1].content = current_output; current_calls[0].provenance = .provider_executed; switch (try persistedSubagentIdentity( alloc, ¤t_messages, &history, invocation_id, )) { .corrupt => {}, .absent, .replay => return error.TestUnexpectedResult, } } test "subagent production identity replays persisted invocation across restart without effects" { const alloc = std.testing.allocator; const root_id = "01J00000000000000000000000"; const invocation_id = "production-adapter-replay"; const create_args = \\{"command":{"create":{"name":"replayed-worker","mode":"persistent"}}} ; const call = ToolCall{ .id = invocation_id, .name = "subagent", .arguments_json = create_args, }; var env = try SubagentTestEnvironment.init(alloc); defer env.deinit(alloc); try env.createSession(alloc, root_id); var test_authority = SubagentTestAuthority{ .root_id = root_id }; var first_output: []u8 = undefined; var child_id: []u8 = undefined; var history: []session_runtime.HistoryTurn = undefined; { const host = try subagent_tool_host.Runtime.create( alloc, &env.store, root_id, test_authority.resolver(), .{}, ); defer host.deinit(); var runtime = TestRuntime{ .workspace_root = env.workspace, .subagent_host = host, .subagent_caller_id = root_id, .model = "test/model", }; defer runtime.deinit(alloc); var first_arena = std.heap.ArenaAllocator.init(alloc); defer first_arena.deinit(); const first = try executeToolCall( runtime.context(), first_arena.allocator(), call, ); try std.testing.expectEqual( tool_contracts.ToolExecutionStatus.success, first.status, ); first_output = try alloc.dupe(u8, first.model_output); errdefer alloc.free(first_output); child_id = try subagentResultStringAlloc(alloc, first.model_output, "child_id"); errdefer alloc.free(child_id); try persistSubagentToolResult(&runtime, alloc, call, first); var replay_arena = std.heap.ArenaAllocator.init(alloc); defer replay_arena.deinit(); const replay = try executeToolCall( runtime.context(), replay_arena.allocator(), call, ); try std.testing.expectEqualStrings(first_output, replay.model_output); try expectSingleSubagentCreateEffects(alloc, &env, child_id); history = try runtime.session.snapshotHistory(alloc); } defer alloc.free(first_output); defer alloc.free(child_id); defer session_runtime.freeHistoryTurnSlice(alloc, history); const restarted_host = try subagent_tool_host.Runtime.create( alloc, &env.store, root_id, test_authority.resolver(), .{}, ); defer restarted_host.deinit(); var resumed = TestRuntime{ .workspace_root = env.workspace, .subagent_host = restarted_host, .subagent_caller_id = root_id, .model = "test/model", }; defer resumed.deinit(alloc); try resumed.session.restore( alloc, session_runtime.ConversationLanguage.literal("en"), history, ); var resumed_arena = std.heap.ArenaAllocator.init(alloc); defer resumed_arena.deinit(); const durable_replay = try executeToolCall( resumed.context(), resumed_arena.allocator(), call, ); try std.testing.expectEqualStrings(first_output, durable_replay.model_output); try expectSingleSubagentCreateEffects(alloc, &env, child_id); var conflict_arena = std.heap.ArenaAllocator.init(alloc); defer conflict_arena.deinit(); const conflict = try executeToolCall( resumed.context(), conflict_arena.allocator(), .{ .id = invocation_id, .name = "subagent", .arguments_json = \\{"command":{"create":{"name":"different-worker","mode":"persistent"}}} , }, ); try expectContains(conflict.model_output, "\"error_code\":\"operation_conflict\""); try expectSingleSubagentCreateEffects(alloc, &env, child_id); { var capability = try env.store.openSubagentControlCapabilityWritable( alloc, root_id, .{}, ); defer capability.deinit(); const store = subagent_create_store.Store{ .capability = &capability, .expected_root_id = root_id, }; var lock = try store.acquireLock(); defer lock.release(); var record = (try store.loadOptional(alloc)).?; defer record.deinit(alloc); const operation_id = try subagentResultStringAlloc( alloc, first_output, "operation_id", ); defer alloc.free(operation_id); const identity = subagent_tool_result.parseBoundOperationId(operation_id).?; for (record.entries) |entry| { alloc.free(entry.operation_id); alloc.free(entry.child_id); } alloc.free(record.entries); record.entries = try alloc.alloc(subagent_create_store.Entry, 0); record.model_replay_floor = identity.epoch +| 1; try store.save(alloc, record); } var expired_arena = std.heap.ArenaAllocator.init(alloc); defer expired_arena.deinit(); const expired = try executeToolCall( resumed.context(), expired_arena.allocator(), call, ); try expectContains( expired.model_output, "\"error_code\":\"operation_replay_expired\"", ); try expectSingleSubagentCreateEffects(alloc, &env, child_id); } const CancelTestCommandOnOutput = struct { flag: *std.atomic.Value(bool), needle: []const u8, seen: bool = false, fn onChunk(ctx: *anyopaque, _: ?types.ToolLifecycleId, _: command_contract.CommandOutputStream, chunk: []const u8) !void { const self: *@This() = @ptrCast(@alignCast(ctx)); if (std.mem.find(u8, chunk, self.needle) == null) return; self.seen = true; self.flag.store(true, .seq_cst); } }; const TestCommandOutputCapture = struct { alloc: Allocator, bytes: std.ArrayList(u8) = .empty, stdout_chunks: usize = 0, stderr_chunks: usize = 0, fn deinit(self: *@This()) void { self.bytes.deinit(self.alloc); } fn onChunk( raw_ctx: *anyopaque, _: ?types.ToolLifecycleId, stream: command_contract.CommandOutputStream, chunk: []const u8, ) !void { const self: *@This() = @ptrCast(@alignCast(raw_ctx)); try self.bytes.appendSlice(self.alloc, chunk); switch (stream) { .stdout => self.stdout_chunks += 1, .stderr => self.stderr_chunks += 1, } } }; const TestDevboxProvider = struct { calls: usize = 0, }; fn executeTestDevboxProvider( raw_ctx: ?*anyopaque, alloc: Allocator, _: []const u8, _: []const u8, control: devbox_executor.Control, ) devbox_executor.ProviderError!devbox_executor.VercelOutcome { try control.check(); const state: *TestDevboxProvider = @ptrCast(@alignCast(raw_ctx.?)); state.calls += 1; const stdout = try alloc.dupe(u8, "DEVBOX_COMPACT_ONE\nDEVBOX_COMPACT_TWO\n"); errdefer alloc.free(stdout); const stderr = try alloc.dupe(u8, ""); return .{ .success = .{ .exit_code = 0, .stdout = stdout, .stderr = stderr, .duration_ms = 9, } }; } fn runCommandArgsForTest(alloc: Allocator, command: []const u8) ![]u8 { var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try out.writer.writeAll("{\"action\":\"exec\",\"command\":"); try std.json.Stringify.value(command, .{}, &out.writer); try out.writer.writeByte('}'); return out.toOwnedSlice(); } fn executeTestRunCommand( ctx: Context, arena: Allocator, call: ToolCall, ) !ToolExecutionResult { const terminal_call = try terminalExecCallForTest(arena, call); const command_ctx = try tool_admission.runCommandContext(ctx.admissionInput(), arena, terminal_call); return executeToolCallAuthorized(ctx, .{ .call_allocator = arena, .result_allocator = arena, .call = terminal_call, .authority = .{ .run_command = .{ .shell_allowed = .{ .fingerprint = .init(command_ctx), .source = .configured_rule, } } }, .session_grants = ctx.session_grants, .advertised_dynamic_tool_names = ctx.advertised_dynamic_tool_names, .max_tool_result_bytes = ctx.max_tool_result_bytes, }); } fn terminalExecCallForTest(arena: Allocator, call: ToolCall) !ToolCall { if (std.mem.eql(u8, call.name, "terminal")) return call; if (!std.mem.eql(u8, call.name, "run_command")) return call; var args = try std.json.parseFromSliceLeaky( std.json.Value, arena, call.arguments_json, .{ .allocate = .alloc_always }, ); if (args != .object) return error.InvalidToolArguments; try args.object.put(arena, "action", .{ .string = "exec" }); var out: std.Io.Writer.Allocating = .init(arena); defer out.deinit(); try std.json.Stringify.value(args, .{}, &out.writer); var migrated = call; migrated.name = "terminal"; migrated.arguments_json = try out.toOwnedSlice(); return migrated; } test "registered terminal exec preserves invalid execution authority error" { var rt = TestRuntime{}; defer rt.deinit(std.testing.allocator); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); const call = ToolCall{ .id = "invalid-authority", .name = "terminal", .arguments_json = "{\"action\":\"exec\",\"command\":\"printf should-not-run\"}", }; try std.testing.expectError( error.InvalidRunCommandExecutionAuthority, executeToolCallAuthorized(rt.context(), .{ .call_allocator = arena, .result_allocator = arena, .call = call, .authority = .ordinary, .session_grants = &.{}, .advertised_dynamic_tool_names = &.{}, .max_tool_result_bytes = rt.max_tool_result_bytes, }), ); } test "captured command compatibility bypasses compound commands" { var rt = TestRuntime{ .tool_registry = test_compatibility_registry }; defer rt.deinit(std.testing.allocator); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); try std.testing.expect((try tool_dispatch.dispatchRunCommandCompatibility(typedDispatchContext(rt.context(), arena), rt.tool_registry, .{ .command = "fx-compatibility-probe; printf shell-fallback", .resolved_cwd = "/tmp", .environment = .legacy, })) == null); for ([_]command_environment.Environment{ .{ .clean = "/bin/zsh" }, .{ .user = "/bin/zsh" }, }) |environment| { try std.testing.expect((try tool_dispatch.dispatchRunCommandCompatibility( typedDispatchContext(rt.context(), arena), rt.tool_registry, .{ .command = "fx-compatibility-probe", .resolved_cwd = "/tmp", .environment = environment, }, )) == null); } } test "run command compatibility returns installer failure without shell fallback" { var rt = TestRuntime{ .tool_registry = test_failing_compatibility_registry, .interactive = false, }; defer rt.deinit(std.testing.allocator); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const result = (try tool_dispatch.dispatchRunCommandCompatibility( typedDispatchContext(rt.context(), arena_state.allocator()), rt.tool_registry, .{ .command = "fx-compatibility-probe", .resolved_cwd = "/tmp", .environment = .legacy, }, )) orelse return error.TestExpectedEqual; const failure = result.failure; try expectToolErrorField(failure, "type", "tool_execution_failed"); try expectToolErrorField(failure, "tool_name", "terminal"); try expectToolErrorDetailString(failure, "error", "SkillInstallFailed"); } fn unexpectedTestPrompt( _: *anyopaque, _: Allocator, _: permission_request.PermissionRequest, _: ToolCall, _: ?*const diff_mod.FileReview, _: ?[]const types.PermissionGrant, ) anyerror!permission_request.OwnedPermissionResponse { return error.TestUnexpectedPrompt; } const TestAutoReview = struct { calls: usize = 0, decision: permission_auto_classifier.Decision = .allow, risk: permission_auto_classifier.Risk = .low, authorization: permission_auto_classifier.Authorization = .medium, rationale: []const u8 = "test automatic review", action_tag: ?std.meta.Tag(permission_auto_classifier.Action) = null, exact_command: ?[]const u8 = null, exact_arguments_json: ?[]const u8 = null, file_display_path: ?[]const u8 = null, file_additions: usize = 0, file_deletions: usize = 0, file_review_rows: usize = 0, escalation_reason: ?[]const u8 = null, target_path: ?[]const u8 = null, fn review( raw_ctx: *anyopaque, alloc: Allocator, request: permission_auto_classifier.ReviewRequest, ) anyerror!permission_auto_classifier.ParseOutcome { const self: *@This() = @ptrCast(@alignCast(raw_ctx)); if (request.workspace_root.len == 0 or request.escalation_reason.len == 0) { return error.TestExpectedReviewContext; } self.calls += 1; self.escalation_reason = request.escalation_reason; self.target_path = if (request.targets.len > 0) request.targets[0].path else null; self.action_tag = std.meta.activeTag(request.action); switch (request.action) { .command => |command| self.exact_command = command.command, .file_mutation => |file| { self.file_display_path = file.display_path; self.file_additions = file.additions; self.file_deletions = file.deletions; self.file_review_rows = file.review.rowCount(); }, .tool => |tool| { self.exact_arguments_json = tool.arguments_json; }, .sandbox_widening => {}, } return .{ .valid = .{ .risk = self.risk, .authorization = self.authorization, .decision = self.decision, .rationale = try alloc.dupe(u8, self.rationale), } }; } fn classifier(self: *@This()) permission_auto_classifier.Classifier { return .withOverride(self, review); } }; test "admission input represents prompt capability directly" { var rt = TestRuntime{ .interactive = false }; defer rt.deinit(std.testing.allocator); var ctx = rt.context(); try std.testing.expect(ctx.admissionInput().permission_prompter == null); ctx.permission_prompter = .{ .context = &rt, .request_fn = unexpectedTestPrompt }; const admission = ctx.admissionInput(); try std.testing.expect(admission.permission_prompter != null); } fn expectContains(haystack: []const u8, needle: []const u8) !void { try std.testing.expect(std.mem.find(u8, haystack, needle) != null); } fn expectNotContains(haystack: []const u8, needle: []const u8) !void { try std.testing.expect(std.mem.find(u8, haystack, needle) == null); } fn expectCommandResultField(json: []const u8, field: []const u8, expected: []const u8) !void { var parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, json, .{}); defer parsed.deinit(); const value = parsed.value.object.get(field) orelse return error.TestExpectedEqual; try std.testing.expect(value == .string); try std.testing.expectEqualStrings(expected, value.string); } fn expectCommandResultBool(json: []const u8, field: []const u8, expected: bool) !void { var parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, json, .{}); defer parsed.deinit(); const value = parsed.value.object.get(field) orelse return error.TestExpectedEqual; try std.testing.expect(value == .bool); try std.testing.expectEqual(expected, value.bool); } fn expectCommandResultInt(json: []const u8, field: []const u8, expected: anytype) !void { var parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, json, .{}); defer parsed.deinit(); const value = parsed.value.object.get(field) orelse return error.TestExpectedEqual; try std.testing.expect(value == .integer); try std.testing.expectEqual(@as(i64, @intCast(expected)), value.integer); } fn expectCommandResultNull(json: []const u8, field: []const u8) !void { var parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, json, .{}); defer parsed.deinit(); const value = parsed.value.object.get(field) orelse return error.TestExpectedEqual; try std.testing.expect(value == .null); } fn expectCommandResultStringPrefix(json: []const u8, field: []const u8, prefix: []const u8) !void { var parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, json, .{}); defer parsed.deinit(); const value = parsed.value.object.get(field) orelse return error.TestExpectedEqual; try std.testing.expect(value == .string); try std.testing.expect(std.mem.startsWith(u8, value.string, prefix)); } fn expectToolErrorField(json: []const u8, field: []const u8, expected: []const u8) !void { var parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, json, .{}); defer parsed.deinit(); const error_obj = parsed.value.object.get("error").?.object; const value = error_obj.get(field) orelse return error.TestExpectedEqual; try std.testing.expect(value == .string); try std.testing.expectEqualStrings(expected, value.string); } fn expectToolErrorDetailString(json: []const u8, field: []const u8, expected: []const u8) !void { var parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, json, .{}); defer parsed.deinit(); const details = parsed.value.object.get("error").?.object.get("details").?.object; const value = details.get(field) orelse return error.TestExpectedEqual; try std.testing.expect(value == .string); try std.testing.expectEqualStrings(expected, value.string); } fn expectToolErrorDetailInt(json: []const u8, field: []const u8, expected: i64) !void { var parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, json, .{}); defer parsed.deinit(); const details = parsed.value.object.get("error").?.object.get("details").?.object; const value = details.get(field) orelse return error.TestExpectedEqual; try std.testing.expect(value == .integer); try std.testing.expectEqual(expected, value.integer); } fn writeTestFile(dir: std.Io.Dir, path: []const u8, content: []const u8) !void { if (std.fs.path.dirname(path)) |parent| { try dir.createDirPath(io_mod.getIo(), parent); } var file = try dir.createFile(std.testing.io, path, .{ .truncate = true }); defer file.close(io_mod.getIo()); try file.writeStreamingAll(io_mod.getIo(), content); } fn writeLargeTestFile(dir: std.Io.Dir, path: []const u8, prefix: []const u8, fill_len: usize) !void { if (std.fs.path.dirname(path)) |parent| { try dir.createDirPath(io_mod.getIo(), parent); } var file = try dir.createFile(std.testing.io, path, .{ .truncate = true }); defer file.close(io_mod.getIo()); try file.writeStreamingAll(io_mod.getIo(), prefix); var chunk: [4096]u8 = undefined; @memset(&chunk, 'x'); var remaining = fill_len; while (remaining > 0) { const n = @min(remaining, chunk.len); try file.writeStreamingAll(io_mod.getIo(), chunk[0..n]); remaining -= n; } } fn readTraceFileForTest(alloc: Allocator, trace_path: []const u8) ![]u8 { var file = try std.Io.Dir.openFileAbsolute(std.testing.io, trace_path, .{}); defer file.close(io_mod.getIo()); var read_buf: [1024]u8 = undefined; var reader = file.reader(std.testing.io, &read_buf); return reader.interface.allocRemaining(alloc, std.Io.Limit.limited(64 * 1024)); } const WebSearchBackendFixture = struct { calls: usize = 0, fn search( raw_ctx: *anyopaque, ctx: tool_dispatch.DispatchContext, request: web_search_contract.Request, ) anyerror!web_search_contract.ExecutionOutput { const self: *@This() = @ptrCast(@alignCast(raw_ctx)); self.calls += 1; if (ctx.on_web_search_progress) |on_progress| { on_progress(ctx.web_search_progress_ctx.?, ctx.tool_call_id, .{ .query_started = request.query }); on_progress(ctx.web_search_progress_ctx.?, ctx.tool_call_id, .{ .results_received = .{ .query = request.query, .result_count = 3, } }); } return .{ .output = .{ .query = request.query, .results = &.{}, .duration_ms = 17, .web_search_requests = 2, }, .inner_usage = .{ .input_tokens = 12, .output_tokens = 34, .web_search_requests = 2 }, }; } }; const WebSearchProgressCapture = struct { searching_count: usize = 0, found_count: usize = 0, call_id: []const u8 = "", query_buf: [128]u8 = undefined, query_len: usize = 0, result_count: usize = 0, fn onProgress(raw_ctx: *anyopaque, call_id: []const u8, progress: types.WebSearchProgress) void { const self: *@This() = @ptrCast(@alignCast(raw_ctx)); self.call_id = call_id; switch (progress) { .query_started => |query| { self.searching_count += 1; self.setQuery(query); }, .results_received => |update| { self.found_count += 1; self.setQuery(update.query); self.result_count = update.result_count; }, } } fn setQuery(self: *@This(), query: []const u8) void { self.query_len = @min(self.query_buf.len, query.len); @memcpy(self.query_buf[0..self.query_len], query[0..self.query_len]); } fn queryView(self: *const @This()) []const u8 { return self.query_buf[0..self.query_len]; } }; fn expectToolOutput(ctx: Context, tool_name: []const u8, args_json: []const u8, expected: []const u8) !void { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); const result = try executeToolCall(ctx, arena, .{ .id = "1", .name = tool_name, .arguments_json = args_json, }); try std.testing.expectEqualStrings(expected, result.model_output); if (result.diff_entry) |payload| { diff_mod.freeDiffEntryPayload(std.heap.c_allocator, payload); } } fn absolutePathExists(path: []const u8) bool { _ = std.Io.Dir.cwd().statFile(io_mod.getIo(), path, .{}) catch return false; return true; } fn setTestHome(home: ?[]const u8) !void { const map = try std.heap.c_allocator.create(std.process.Environ.Map); map.* = std.process.Environ.Map.init(std.heap.c_allocator); if (home) |value| try map.put("HOME", value); io_mod.setEnvironMap(map); } test "background imports come from background and task modules" { try std.testing.expect(BackgroundRuntime == @import("../background/background_runtime.zig").BackgroundRuntime); try std.testing.expect(task_helpers.TaskState == @import("../tasks/task_helpers.zig").TaskState); const language = splitConversationLanguage(session_runtime.ConversationLanguage.literal("es")); try std.testing.expectEqualStrings("es", language.view()); } test "tool runtime explicit cancellation source overrides worker fallback" { var cancel_flag = std.atomic.Value(bool).init(false); var rt = TestRuntime{ .cancel_flag = &cancel_flag }; defer rt.deinit(std.testing.allocator); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); try std.testing.expect(typedDispatchContext(rt.context(), arena_state.allocator()).cancel_flag.? == &cancel_flag); } test "read-only local runtime tools are registered in built-in registry" { const cases = [_]struct { name: []const u8, kind: tool_specs.ExecutorKind, }{ .{ .name = "list_files", .kind = .list_files }, .{ .name = "glob_files", .kind = .glob_files }, .{ .name = "grep_files", .kind = .grep_files }, .{ .name = "read_file", .kind = .read_file }, .{ .name = "read_tool_result", .kind = .read_tool_result }, .{ .name = "file_info", .kind = .file_info }, }; var rt = TestRuntime{}; defer rt.deinit(std.testing.allocator); const registry = rt.context().tool_registry; for (cases) |case| { const found = registry.lookup(case.name) orelse return error.TestExpectedEqual; try std.testing.expectEqual(case.kind, found.executor_kind); } const terminal_tool = registry.lookup("terminal") orelse return error.TestExpectedEqual; try std.testing.expectEqual(tool_specs.ExecutorKind.terminal, terminal_tool.executor_kind); try std.testing.expect(registry.lookup("run_command") == null); } test "tool runtime validates and executes only tools from supplied registry" { const alloc = std.testing.allocator; var empty_rt = TestRuntime{ .tool_registry = .{} }; defer empty_rt.deinit(alloc); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); const call = ToolCall{ .id = "list", .name = "list_files", .arguments_json = "{}", }; try std.testing.expectEqual( tool_contracts.ToolCallValidationResult.not_registered, try validateToolCall(empty_rt.context(), arena, call), ); const missing = try executeToolCall(empty_rt.context(), arena, call); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.failure, missing.status); try std.testing.expectEqualStrings("Unsupported tool: list_files", missing.model_output); const list_only_registry = tool_dispatch.Registry{ .tools = &.{test_builtin_tools.list_files} }; var list_rt = TestRuntime{ .tool_registry = list_only_registry }; defer list_rt.deinit(alloc); try std.testing.expectEqual( tool_contracts.ToolCallValidationResult.valid, try validateToolCall(list_rt.context(), arena, call), ); } fn registryOwnedWebFetchCall( ctx: tool_dispatch.DispatchContext, input: tool_dispatch.ToolInput, ) tool_dispatch.DispatchError!tool_dispatch.ToolResult { _ = input; return .{ .success = try ctx.allocator.dupe(u8, "registry-owned web_fetch") }; } fn registryOwnedWebSearchCall( ctx: tool_dispatch.DispatchContext, input: tool_dispatch.ToolInput, ) tool_dispatch.DispatchError!tool_dispatch.ToolResult { _ = input; return .{ .success = try ctx.allocator.dupe(u8, "registry-owned web_search") }; } fn registryOwnedTerminalExecCall( ctx: tool_dispatch.DispatchContext, input: tool_dispatch.ToolInput, ) tool_dispatch.DispatchError!tool_dispatch.ToolResult { _ = input; return .{ .success = try ctx.allocator.dupe(u8, "registry-owned terminal exec") }; } fn registryOwnedFileInfoCall( ctx: tool_dispatch.DispatchContext, input: tool_dispatch.ToolInput, ) tool_dispatch.DispatchError!tool_dispatch.ToolResult { _ = input; return .{ .success = try ctx.allocator.dupe(u8, "registry-owned file_info") }; } fn registryOwnedCreateFolderCall( ctx: tool_dispatch.DispatchContext, input: tool_dispatch.ToolInput, ) tool_dispatch.DispatchError!tool_dispatch.ToolResult { _ = input; if (ctx.subagent_provider != null) return error.InvalidToolArguments; return .{ .success = try ctx.allocator.dupe(u8, "registry-owned create_folder") }; } fn registryOwnedAskQuestionCall( ctx: tool_dispatch.DispatchContext, input: tool_dispatch.ToolInput, ) tool_dispatch.DispatchError!tool_dispatch.ToolResult { _ = input; if (ctx.ask_question_ctx == null or ctx.ask_question_batch == null) { return error.InvalidToolArguments; } return .{ .success = try ctx.allocator.dupe(u8, "registry-owned ask_user_question") }; } fn registryOwnedSemanticSearchCall( ctx: tool_dispatch.DispatchContext, input: tool_dispatch.ToolInput, ) tool_dispatch.DispatchError!tool_dispatch.ToolResult { _ = input; return .{ .success = try ctx.allocator.dupe(u8, "registry-owned semantic_search") }; } const RegistryOwnedLauncherInput = struct {}; fn decodeRegistryOwnedLauncher( ctx: tool_dispatch.DispatchContext, _: []const u8, ) tool_dispatch.DispatchError!tool_dispatch.DecodeResult { const input = try ctx.allocator.create(RegistryOwnedLauncherInput); input.* = .{}; return .{ .input = .{ .ptr = input, .deinit_fn = deinitRegistryOwnedLauncherInput, } }; } fn deinitRegistryOwnedLauncherInput(ptr: *anyopaque, alloc: Allocator) void { const input: *RegistryOwnedLauncherInput = @ptrCast(@alignCast(ptr)); alloc.destroy(input); } fn registryOwnedLauncherCall( ctx: tool_dispatch.DispatchContext, input: tool_dispatch.ToolInput, ) tool_dispatch.DispatchError!tool_dispatch.ToolResult { _ = input; return .{ .success = try ctx.allocator.dupe(u8, "registry-owned launcher") }; } fn registryOwnedMemoryCall( ctx: tool_dispatch.DispatchContext, input: tool_dispatch.ToolInput, ) tool_dispatch.DispatchError!tool_dispatch.ToolResult { _ = input; return .{ .success = try ctx.allocator.dupe(u8, "registry-owned memory") }; } fn registryOwnedSkillCall( ctx: tool_dispatch.DispatchContext, input: tool_dispatch.ToolInput, ) tool_dispatch.DispatchError!tool_dispatch.ToolResult { _ = input; return .{ .success = try ctx.allocator.dupe(u8, "registry-owned skill") }; } fn registryOwnedInstallSkillCall( ctx: tool_dispatch.DispatchContext, input: tool_dispatch.ToolInput, ) tool_dispatch.DispatchError!tool_dispatch.ToolResult { _ = input; return .{ .success = try ctx.allocator.dupe(u8, "registry-owned install_skill") }; } fn registryOwnedMcpSearchCall( ctx: tool_dispatch.DispatchContext, input: tool_dispatch.ToolInput, ) tool_dispatch.DispatchError!tool_dispatch.ToolResult { _ = input; return .{ .success = try ctx.allocator.dupe(u8, "registry-owned mcp_search_tools") }; } fn registryOwnedMcpSelectCall( ctx: tool_dispatch.DispatchContext, input: tool_dispatch.ToolInput, ) tool_dispatch.DispatchError!tool_dispatch.ToolResult { _ = input; return .{ .success = try ctx.allocator.dupe(u8, "registry-owned mcp_select_tool") }; } const QuestionBridgeThreadState = struct { worker: *WorkerRuntime, entries: []const types.QuestionBatchEntry, answers: ?[][]u8 = null, err: ?Allocator.Error = null, }; fn runQuestionBridge(state: *QuestionBridgeThreadState) void { state.answers = requestQuestionBatchWithWorker( state.worker, std.testing.allocator, state.entries, ) catch |err| { state.err = err; return; }; } fn waitForQuestionBridgeSnapshot(worker: *WorkerRuntime) !worker_runtime.PendingQuestionBatchSnapshot { var attempts: usize = 0; while (attempts < 100) : (attempts += 1) { if (try worker.snapshotPendingQuestionBatch(std.testing.allocator)) |snapshot| return snapshot; io_mod.sleep(std.time.ns_per_ms); } return error.TestExpectedEqual; } test "ask_user_question worker bridge preserves allocator ownership" { const worker_alloc = std.heap.c_allocator; const options = [_]types.QuestionOption{ .{ .label = "Inspect repo state", .description = "Review local files first." }, .{ .label = "Run tests", .description = "Start with verification." }, }; const entries = [_]types.QuestionBatchEntry{.{ .question = "Choose next step?", .options = &options, }}; var worker = WorkerRuntime{}; defer worker.deinit(worker_alloc); var state = QuestionBridgeThreadState{ .worker = &worker, .entries = &entries, }; const thread = try std.Thread.spawn(.{}, runQuestionBridge, .{&state}); var thread_joined = false; defer if (!thread_joined) { worker.requestStop(); thread.join(); }; var snapshot = try waitForQuestionBridgeSnapshot(&worker); defer snapshot.deinit(std.testing.allocator); try std.testing.expectEqualStrings("Choose next step?", snapshot.entries[0].question); try worker.submitQuestionBatchAnswer(worker_alloc, &.{"Run tests"}); thread.join(); thread_joined = true; defer freeQuestionAnswers(std.testing.allocator, state.answers); try std.testing.expect(state.err == null); try std.testing.expectEqual(@as(usize, 1), state.answers.?.len); try std.testing.expectEqualStrings("Run tests", state.answers.?[0]); } test "ask_user_question execution uses supplied registry entry and interactive host capability" { const alloc = std.testing.allocator; var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); var registered_ask_question = test_builtin_tools.ask_user_question; registered_ask_question.call = registryOwnedAskQuestionCall; const tools = [_]tool_dispatch.Tool{registered_ask_question}; const registry = tool_dispatch.Registry{ .tools = tools[0..] }; const call = ToolCall{ .id = "ask-question-registry", .name = "ask_user_question", .arguments_json = "not-json", }; var rt = TestRuntime{ .tool_registry = registry }; defer rt.deinit(alloc); const result = try executeToolCallAuthorized(rt.context(), .{ .call_allocator = arena, .result_allocator = arena, .call = call, .authority = .ordinary, .session_grants = &.{}, .advertised_dynamic_tool_names = &.{}, .max_tool_result_bytes = rt.max_tool_result_bytes, }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.success, result.status); try std.testing.expectEqualStrings("registry-owned ask_user_question", result.model_output); rt.worker.requestCancel(); try std.testing.expectError(error.Cancelled, executeToolCallAuthorized(rt.context(), .{ .call_allocator = arena, .result_allocator = arena, .call = call, .authority = .ordinary, .session_grants = &.{}, .advertised_dynamic_tool_names = &.{}, .max_tool_result_bytes = rt.max_tool_result_bytes, })); } test "file_info execution uses supplied registry entry" { const alloc = std.testing.allocator; var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); var registered_file_info = test_builtin_tools.file_info; registered_file_info.call = registryOwnedFileInfoCall; const tools = [_]tool_dispatch.Tool{registered_file_info}; const registry = tool_dispatch.Registry{ .tools = tools[0..] }; const call = ToolCall{ .id = "info", .name = "file_info", .arguments_json = "{\"path\":\".\"}", }; var rt = TestRuntime{ .tool_registry = registry }; defer rt.deinit(alloc); const direct = try executeToolCall(rt.context(), arena, call); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.success, direct.status); try std.testing.expectEqualStrings("registry-owned file_info", direct.model_output); } test "create_folder supplied registry entry has no subagent capability" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const workspace = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(workspace); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); var registered_create_folder = test_builtin_tools.create_folder; registered_create_folder.call = registryOwnedCreateFolderCall; const tools = [_]tool_dispatch.Tool{registered_create_folder}; const registry = tool_dispatch.Registry{ .tools = tools[0..] }; const call = ToolCall{ .id = "create-folder-registry", .name = "create_folder", .arguments_json = "{\"path\":\"created\"}", }; var rt = TestRuntime{ .tool_registry = registry, .workspace_root = workspace, }; defer rt.deinit(alloc); const result = try executeToolCallAuthorized(rt.context(), .{ .call_allocator = arena, .result_allocator = arena, .call = call, .authority = .ordinary, .session_grants = &.{}, .advertised_dynamic_tool_names = &.{}, .max_tool_result_bytes = rt.max_tool_result_bytes, }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.success, result.status); try std.testing.expectEqualStrings("registry-owned create_folder", result.model_output); try std.testing.expectError( error.FileNotFound, tmp.dir.statFile(io_mod.getIo(), "created", .{}), ); } test "real tool runtime enforces refreshed live authority before filesystem effect" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const workspace = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(workspace); var rt = TestRuntime{ .workspace_root = workspace }; defer rt.deinit(alloc); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); const call = ToolCall{ .id = "live-create", .name = "create_folder", .arguments_json = "{\"path\":\"created\"}", }; const denied_tools = [_][]const u8{"read_file"}; try std.testing.expectError( error.LiveToolAuthorityUnavailable, executeToolCallAuthorized(rt.context(), .{ .call_allocator = arena, .result_allocator = arena, .call = call, .authority = .ordinary, .session_grants = &.{}, .live_authority = .{ .generation = 2, .root_id = "root", .tools = &denied_tools, .sandbox_backend = .none, .integrations = &.{}, .rules = .{}, .grants = &.{}, .permission_mode = .auto, }, .advertised_dynamic_tool_names = &.{}, .max_tool_result_bytes = rt.max_tool_result_bytes, }), ); try std.testing.expectError( error.FileNotFound, tmp.dir.statFile(io_mod.getIo(), "created", .{}), ); const allowed_tools = [_][]const u8{"create_folder"}; const result = try executeToolCallAuthorized(rt.context(), .{ .call_allocator = arena, .result_allocator = arena, .call = call, .authority = .ordinary, .session_grants = &.{}, .live_authority = .{ .generation = 3, .root_id = "root", .tools = &allowed_tools, .sandbox_backend = .none, .integrations = &.{}, .rules = .{}, .grants = &.{}, .permission_mode = .auto, }, .advertised_dynamic_tool_names = &.{}, .max_tool_result_bytes = rt.max_tool_result_bytes, }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.success, result.status); _ = try tmp.dir.statFile(io_mod.getIo(), "created", .{}); } test "semantic_search execution uses supplied registry entry" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const workspace = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(workspace); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); var registered_semantic_search = test_builtin_tools.semantic_search; registered_semantic_search.call = registryOwnedSemanticSearchCall; const tools = [_]tool_dispatch.Tool{registered_semantic_search}; const registry = tool_dispatch.Registry{ .tools = tools[0..] }; const call = ToolCall{ .id = "semantic-search", .name = "semantic_search", .arguments_json = "{\"query\":\"registry\"}", }; var rt = TestRuntime{ .tool_registry = registry, .workspace_root = workspace, }; defer rt.deinit(alloc); const result = try executeToolCall(rt.context(), arena, call); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.success, result.status); try std.testing.expectEqualStrings("registry-owned semantic_search", result.model_output); } test "launcher execution uses supplied registry entries after ordinary authorization" { const alloc = std.testing.allocator; var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); var registered_open_file = test_builtin_tools.open_file; registered_open_file.decode = decodeRegistryOwnedLauncher; registered_open_file.validate = null; registered_open_file.call = registryOwnedLauncherCall; const tools = [_]tool_dispatch.Tool{ registered_open_file, }; const registry = tool_dispatch.Registry{ .tools = tools[0..] }; var rt = TestRuntime{ .tool_registry = registry }; defer rt.deinit(alloc); for ([_][]const u8{"open_file"}) |name| { const result = try executeToolCallAuthorized(rt.context(), .{ .call_allocator = arena, .result_allocator = arena, .call = .{ .id = "launcher-registry", .name = name, .arguments_json = "{}", }, .authority = .ordinary, .session_grants = &.{}, .advertised_dynamic_tool_names = &.{}, .max_tool_result_bytes = rt.max_tool_result_bytes, }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.success, result.status); try std.testing.expectEqualStrings("registry-owned launcher", result.model_output); } } test "web_fetch execution uses supplied registry entry" { const alloc = std.testing.allocator; var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); var registered_web_fetch = test_builtin_tools.web_fetch; registered_web_fetch.call = registryOwnedWebFetchCall; const tools = [_]tool_dispatch.Tool{registered_web_fetch}; const registry = tool_dispatch.Registry{ .tools = tools[0..] }; const call = ToolCall{ .id = "fetch", .name = "web_fetch", .arguments_json = "{\"url\":\"https://example.com/docs\"}", }; var rt = TestRuntime{ .tool_registry = registry }; defer rt.deinit(alloc); const direct = try executeToolCall(rt.context(), arena, call); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.success, direct.status); try std.testing.expectEqualStrings("registry-owned web_fetch", direct.model_output); } test "web_search execution uses supplied registry entry" { const alloc = std.testing.allocator; var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); var registered_web_search = test_builtin_tools.web_search; registered_web_search.call = registryOwnedWebSearchCall; const tools = [_]tool_dispatch.Tool{registered_web_search}; const registry = tool_dispatch.Registry{ .tools = tools[0..] }; var backend = WebSearchBackendFixture{}; var rt = TestRuntime{ .tool_registry = registry, .web_search_runtime_ready = true, .web_search_backend = .{ .ctx = @ptrCast(&backend), .execute_fn = WebSearchBackendFixture.search }, }; defer rt.deinit(alloc); const result = try executeToolCall(rt.context(), arena, .{ .id = "web-search-registry", .name = "web_search", .arguments_json = "{\"query\":\"current news\"}", }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.success, result.status); try std.testing.expectEqualStrings("registry-owned web_search", result.model_output); try std.testing.expectEqual(@as(usize, 0), backend.calls); } test "terminal exec execution uses supplied registry entry" { const alloc = std.testing.allocator; var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); var registered_run_command = test_builtin_tools.terminal; registered_run_command.call = registryOwnedTerminalExecCall; const tools = [_]tool_dispatch.Tool{registered_run_command}; const registry = tool_dispatch.Registry{ .tools = tools[0..] }; const call = ToolCall{ .id = "run-command-registry", .name = "terminal", .arguments_json = "{\"action\":\"exec\",\"command\":\"printf bypassed\"}", }; var rt = TestRuntime{ .tool_registry = registry }; defer rt.deinit(alloc); const ctx = rt.context(); const command_ctx = try tool_admission.runCommandContext(ctx.admissionInput(), arena, call); const result = try executeToolCallAuthorized(ctx, .{ .call_allocator = arena, .result_allocator = arena, .call = call, .authority = .{ .run_command = .{ .shell_allowed = .{ .fingerprint = .init(command_ctx), .source = .configured_rule, } } }, .session_grants = &.{}, .advertised_dynamic_tool_names = &.{}, .max_tool_result_bytes = rt.max_tool_result_bytes, }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.success, result.status); try std.testing.expectEqualStrings("registry-owned terminal exec", result.model_output); } test "stateful local tool execution uses supplied registry entries" { const alloc = std.testing.allocator; var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); var registered_memory = test_builtin_tools.memory; registered_memory.call = registryOwnedMemoryCall; var registered_skill = test_builtin_tools.skill; registered_skill.call = registryOwnedSkillCall; var registered_install_skill = test_builtin_tools.install_skill; registered_install_skill.call = registryOwnedInstallSkillCall; const tools = [_]tool_dispatch.Tool{ registered_memory, registered_skill, registered_install_skill, }; const registry = tool_dispatch.Registry{ .tools = tools[0..] }; var rt = TestRuntime{ .tool_registry = registry }; defer rt.deinit(alloc); const cases = [_]struct { name: []const u8, args: []const u8, expected: []const u8, }{ .{ .name = "memory", .args = "{\"action\":\"save\",\"fact\":\"likes registries\"}", .expected = "registry-owned memory" }, .{ .name = "skill", .args = "{\"name\":\"workflow\"}", .expected = "registry-owned skill" }, .{ .name = "install_skill", .args = "{\"source\":\"/tmp/skills\",\"skill\":\"workflow\"}", .expected = "registry-owned install_skill" }, }; for (cases) |case| { const result = try executeToolCall(rt.context(), arena, .{ .id = "stateful", .name = case.name, .arguments_json = case.args, }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.success, result.status); try std.testing.expectEqualStrings(case.expected, result.model_output); } } test "MCP control tool execution preserves argument failures" { const alloc = std.testing.allocator; var rt = TestRuntime{}; defer rt.deinit(alloc); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); const cases = [_]struct { name: []const u8, args: []const u8, expected: []const u8, }{ .{ .name = "mcp_search_tools", .args = "{\"query\":1}", .expected = "mcp_search_tools requires a string query." }, .{ .name = "mcp_select_tool", .args = "{\"name\":1}", .expected = "mcp_select_tool requires an exact dynamic tool name." }, }; for (cases) |case| { const result = try executeToolCall(rt.context(), arena, .{ .id = "mcp_validation", .name = case.name, .arguments_json = case.args, }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.failure, result.status); try std.testing.expectEqualStrings(case.expected, result.model_output); } } test "MCP control tool execution uses supplied registry entries" { const alloc = std.testing.allocator; var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); var registered_search = test_builtin_tools.mcp_search_tools; registered_search.call = registryOwnedMcpSearchCall; var registered_select = test_builtin_tools.mcp_select_tool; registered_select.call = registryOwnedMcpSelectCall; const tools = [_]tool_dispatch.Tool{ registered_search, registered_select }; const registry = tool_dispatch.Registry{ .tools = tools[0..] }; var rt = TestRuntime{ .tool_registry = registry }; defer rt.deinit(alloc); const cases = [_]struct { name: []const u8, args: []const u8, expected: []const u8, }{ .{ .name = "mcp_search_tools", .args = "{\"query\":\"read\"}", .expected = "registry-owned mcp_search_tools" }, .{ .name = "mcp_select_tool", .args = "{\"name\":\"mcp_fs_read\"}", .expected = "registry-owned mcp_select_tool" }, }; for (cases) |case| { const result = try executeToolCall(rt.context(), arena, .{ .id = "mcp_registry", .name = case.name, .arguments_json = case.args, }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.success, result.status); try std.testing.expectEqualStrings(case.expected, result.model_output); } } test "validateToolCall rejects malformed registered input without claiming unknown calls" { const alloc = std.testing.allocator; var rt = TestRuntime{}; defer rt.deinit(alloc); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); const invalid = try validateToolCall(rt.context(), arena, .{ .id = "fetch", .name = "web_fetch", .arguments_json = "{\"url\":1}", }); try std.testing.expectEqualStrings("web_fetch field \"url\" must be a string", invalid.failure); try std.testing.expectEqual(tool_contracts.ToolCallValidationResult.not_registered, try validateToolCall(rt.context(), arena, .{ .id = "dynamic", .name = "dynamic_tool", .arguments_json = "{}", })); } test "validateToolCall preserves the registered captured command host" { var rt = TestRuntime{ .tool_registry = test_browser_workspace_tools.registry }; defer rt.deinit(std.testing.allocator); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); try std.testing.expectEqual( tool_contracts.ToolCallValidationResult.valid, try validateToolCall(rt.context(), arena_state.allocator(), .{ .id = "workspace-terminal", .name = "terminal", .arguments_json = "{\"action\":\"exec\",\"command\":\"printf ok\"}", }), ); } test "validateToolCall rejects selected MCP arguments through runtime capability" { const alloc = std.testing.allocator; var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); var fixture = McpFixture.CountingContext{}; const advertised = [_][]const u8{"mcp_fs_read"}; var rt = TestRuntime{ .mcp_ctx = @ptrCast(&fixture), .mcp_validate_tool = McpFixture.validateArguments, .advertised_dynamic_tool_names = &advertised, }; defer rt.deinit(alloc); const invalid = try validateToolCall(rt.context(), arena_state.allocator(), .{ .id = "mcp-invalid", .name = "mcp_fs_read", .arguments_json = "{\"path\":7}", }); try std.testing.expect(invalid == .failure); try std.testing.expectEqualStrings("path must be a string", invalid.failure); } test "checkToolAvailability rejects missing web_search runtime without catalog or network work" { const alloc = std.testing.allocator; var rt = TestRuntime{}; defer rt.deinit(alloc); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const reason = try checkToolAvailability(rt.context(), arena_state.allocator(), .{ .id = "search", .name = "web_search", .arguments_json = "{\"query\":\"current news\"}", }) orelse return error.TestExpectedEqual; try std.testing.expectEqualStrings(tool_dispatch.web_search_unavailable_message, reason); } test "validateToolCall rejects invalid private web_search before backend invocation" { const alloc = std.testing.allocator; var backend = WebSearchBackendFixture{}; var rt = TestRuntime{ .web_search_runtime_ready = true, .web_search_backend = .{ .ctx = @ptrCast(&backend), .execute_fn = WebSearchBackendFixture.search }, }; defer rt.deinit(alloc); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const invalid = try validateToolCall(rt.context(), arena_state.allocator(), .{ .id = "search", .name = "web_search", .arguments_json = "{\"query\":\"x\"}", }); try std.testing.expectEqualStrings("web_search field \"query\" must contain at least two characters", invalid.failure); try std.testing.expectEqual(@as(usize, 0), backend.calls); } test "executeToolCall invokes injected private web_search backend" { const alloc = std.testing.allocator; var backend = WebSearchBackendFixture{}; var rt = TestRuntime{ .web_search_runtime_ready = true, .web_search_backend = .{ .ctx = @ptrCast(&backend), .execute_fn = WebSearchBackendFixture.search }, }; defer rt.deinit(alloc); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const result = try executeToolCall(rt.context(), arena_state.allocator(), .{ .id = "search", .name = "web_search", .arguments_json = "{\"query\":\"current news\"}", }); try std.testing.expectEqual(@as(usize, 1), backend.calls); try expectContains(result.model_output, "Web search results for query: current news"); try std.testing.expectEqual(@as(u32, 2), result.inner_usage.?.web_search_requests); } test "valid allowed web_search emits searching query status" { const alloc = std.testing.allocator; var backend = WebSearchBackendFixture{}; var progress = WebSearchProgressCapture{}; var rt = TestRuntime{ .web_search_runtime_ready = true, .web_search_backend = .{ .ctx = @ptrCast(&backend), .execute_fn = WebSearchBackendFixture.search }, .web_search_progress_ctx = @ptrCast(&progress), .on_web_search_progress = WebSearchProgressCapture.onProgress, }; defer rt.deinit(alloc); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); _ = try executeToolCall(rt.context(), arena_state.allocator(), .{ .id = "call_search", .name = "web_search", .arguments_json = "{\"query\":\"current news\"}", }); try std.testing.expectEqual(@as(usize, 1), progress.searching_count); try std.testing.expectEqualStrings("call_search", progress.call_id); try std.testing.expectEqualStrings("current news", progress.queryView()); } test "invalid web_search emits no searching status" { const alloc = std.testing.allocator; var backend = WebSearchBackendFixture{}; var progress = WebSearchProgressCapture{}; var rt = TestRuntime{ .web_search_runtime_ready = true, .web_search_backend = .{ .ctx = @ptrCast(&backend), .execute_fn = WebSearchBackendFixture.search }, .web_search_progress_ctx = @ptrCast(&progress), .on_web_search_progress = WebSearchProgressCapture.onProgress, }; defer rt.deinit(alloc); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const invalid = try validateToolCall(rt.context(), arena_state.allocator(), .{ .id = "call_search", .name = "web_search", .arguments_json = "{\"query\":\"x\"}", }); try std.testing.expectEqualStrings("web_search field \"query\" must contain at least two characters", invalid.failure); try std.testing.expectEqual(@as(usize, 0), progress.searching_count); try std.testing.expectEqual(@as(usize, 0), backend.calls); } test "web_search emits found result count status" { const alloc = std.testing.allocator; var backend = WebSearchBackendFixture{}; var progress = WebSearchProgressCapture{}; var rt = TestRuntime{ .web_search_runtime_ready = true, .web_search_backend = .{ .ctx = @ptrCast(&backend), .execute_fn = WebSearchBackendFixture.search }, .web_search_progress_ctx = @ptrCast(&progress), .on_web_search_progress = WebSearchProgressCapture.onProgress, }; defer rt.deinit(alloc); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); _ = try executeToolCall(rt.context(), arena_state.allocator(), .{ .id = "call_search", .name = "web_search", .arguments_json = "{\"query\":\"current news\"}", }); try std.testing.expectEqual(@as(usize, 1), progress.found_count); try std.testing.expectEqual(@as(usize, 3), progress.result_count); } test "web_search completion includes searches and duration" { const alloc = std.testing.allocator; var backend = WebSearchBackendFixture{}; var rt = TestRuntime{ .web_search_runtime_ready = true, .web_search_backend = .{ .ctx = @ptrCast(&backend), .execute_fn = WebSearchBackendFixture.search }, }; defer rt.deinit(alloc); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const result = try executeToolCall(rt.context(), arena_state.allocator(), .{ .id = "call_search", .name = "web_search", .arguments_json = "{\"query\":\"current news\"}", }); const completion = result.web_search_completion orelse return error.TestExpectedEqual; try std.testing.expectEqual(@as(u32, 2), completion.searches); try std.testing.expectEqual(@as(u64, 17), completion.duration_ms); } test "web_fetch uses the configured shared runtime" { const alloc = std.testing.allocator; var fetch_runtime = web_fetch_runtime.Runtime.init(.{ .allocator = alloc }); defer fetch_runtime.deinit(alloc); try fetch_runtime.insert(alloc, .{ .submitted_url = "https://example.com/file.bin", .final_url = "https://example.com/file.bin", .status = .ok, .mime_type = "application/octet-stream", .content_kind = .binary, }); const builtin_tools = @import("../../builtins/tools.zig"); var rt = TestRuntime{ .tool_registry = builtin_tools.registry, .web_fetch_runtime = &fetch_runtime }; defer rt.deinit(alloc); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const ctx = rt.context(); try std.testing.expect(ctx.web_fetch_runtime.? == &fetch_runtime); try std.testing.expect(ctx.web_fetch_artifact_store == null); const call = ToolCall{ .id = "fetch", .name = "web_fetch", .arguments_json = "{\"url\":\"https://example.com/file.bin\"}" }; const direct = try executeToolCall(ctx, arena_state.allocator(), call); try std.testing.expect(direct.web_fetch_completion.?.cache_hit); try std.testing.expectEqualStrings("https://example.com/file.bin", direct.web_fetch_completion.?.url()); } test "parent web_fetch tool-call metric omits URL and fetched result content" { const alloc = std.testing.allocator; diagnostics.resetForTest(); defer diagnostics.resetForTest(); var fetch_runtime = web_fetch_runtime.Runtime.init(.{ .allocator = alloc }); defer fetch_runtime.deinit(alloc); try fetch_runtime.insert(alloc, .{ .submitted_url = "https://example.com/docs", .final_url = "https://example.com/docs", .status = .ok, .mime_type = "text/plain", .content_kind = .text, .converted_content = "FETCHED_PAGE_SECRET_RAW", }); const builtin_tools = @import("../../builtins/tools.zig"); var rt = TestRuntime{ .tool_registry = builtin_tools.registry, .web_fetch_runtime = &fetch_runtime }; defer rt.deinit(alloc); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const result = try executeToolCall(rt.context(), arena_state.allocator(), .{ .id = "fetch", .name = "web_fetch", .arguments_json = "{\"url\":\"https://example.com/docs\"}", }); try expectContains(result.model_output, "FETCHED_PAGE_SECRET_RAW"); var buf: [diagnostics.tool_call_ring_capacity]diagnostics.ToolCallMetric = undefined; const n = diagnostics.snapshotToolCalls(&buf); try std.testing.expectEqual(@as(usize, 1), n); try std.testing.expectEqualStrings("web_fetch", buf[0].name()); try std.testing.expect(buf[0].ok); try std.testing.expectEqualStrings("", buf[0].args()); try std.testing.expectEqualStrings("", buf[0].result()); try std.testing.expectEqual(@as(u32, 0), buf[0].args_total_bytes); try std.testing.expectEqual(@as(u32, 0), buf[0].result_total_bytes); try expectNotContains(buf[0].result(), "FETCHED_PAGE_SECRET_RAW"); } test "non-web_fetch tool-call metrics retain bounded args and result" { diagnostics.resetForTest(); defer diagnostics.resetForTest(); diagnostics.recordToolCallResult(.{ .name = "read_file", .arguments_json = "{\"path\":\"README.md\"}", .model_output = "README.md\n\n# fx\nnormal result\n", .ok = true, .started_at_ms = 1000, }); var buf: [diagnostics.tool_call_ring_capacity]diagnostics.ToolCallMetric = undefined; const n = diagnostics.snapshotToolCalls(&buf); try std.testing.expectEqual(@as(usize, 1), n); try std.testing.expectEqualStrings("read_file", buf[0].name()); try expectContains(buf[0].args(), "README.md"); try expectContains(buf[0].result(), "# fx"); try expectContains(buf[0].result(), "normal result"); } test "request tool permission keeps safe defaults while local writes bypass review" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "workspace"); defer alloc.free(root); var rt = TestRuntime{ .workspace_root = root, .permission_mode = .auto, .interactive = false, }; defer rt.deinit(std.testing.allocator); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); try std.testing.expectEqual(ToolPermissionDecision.once, (try tool_admission.requestPermissionOutcome(rt.context().admissionInput(), arena, .{ .id = "1", .name = "write_file", .arguments_json = "{\"path\":\"fx-permission-test.txt\",\"content\":\"hello\"}", }, .auto, &.{})).decision); try std.testing.expectEqual(ToolPermissionDecision.once, (try tool_admission.requestPermissionOutcome(rt.context().admissionInput(), arena, .{ .id = "1", .name = "list_files", .arguments_json = "{}", }, .ask, &.{})).decision); try std.testing.expectEqual(ToolPermissionDecision.once, (try tool_admission.requestPermissionOutcome(rt.context().admissionInput(), arena, .{ .id = "1", .name = "ask_user_question", .arguments_json = "{}", }, .ask, &.{})).decision); } test "CLI headless ordinary admission preserves multi-target deny precedence" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); { var source = try tmp.dir.createFile(io_mod.getIo(), "workspace/source.txt", .{ .truncate = true }); defer source.close(io_mod.getIo()); try source.writeStreamingAll(io_mod.getIo(), "source\n"); } const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "workspace"); defer alloc.free(root); var rt = TestRuntime{ .workspace_root = root, .interactive = false }; defer rt.deinit(alloc); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); const cases = [_]struct { tool_name: []const u8, arguments_json: []const u8, source_action: types.PermissionAction, destination_action: types.PermissionAction, }{ .{ .tool_name = "copy_file", .arguments_json = "{\"source\":\"source.txt\",\"destination\":\"destination.txt\"}", .source_action = .ask, .destination_action = .deny, }, .{ .tool_name = "copy_file", .arguments_json = "{\"source\":\"source.txt\",\"destination\":\"destination.txt\"}", .source_action = .deny, .destination_action = .ask, }, .{ .tool_name = "rename_file", .arguments_json = "{\"old_path\":\"source.txt\",\"new_path\":\"destination.txt\"}", .source_action = .ask, .destination_action = .deny, }, .{ .tool_name = "rename_file", .arguments_json = "{\"old_path\":\"source.txt\",\"new_path\":\"destination.txt\"}", .source_action = .deny, .destination_action = .ask, }, }; for (cases, 0..) |case, index| { var rules = [_]types.PermissionRule{ .{ .permission = @constCast(case.tool_name), .pattern = @constCast("source.txt"), .action = case.source_action, }, .{ .permission = @constCast(case.tool_name), .pattern = @constCast("destination.txt"), .action = case.destination_action, }, }; rt.permission_rules = .{ .rules = &rules }; const outcome = try tool_admission.requestPermissionOutcome(rt.context().admissionInput(), arena, .{ .id = try std.fmt.allocPrint(arena, "multi-target-{d}", .{index}), .name = case.tool_name, .arguments_json = case.arguments_json, }, .ask, &.{}); try std.testing.expectEqual(ToolPermissionDecision.policy_denied, outcome.decision); try std.testing.expect(outcome.execution_authority == null); } } test "run_command default user profile requires configured or reviewed shell authority" { const alloc = std.testing.allocator; var reviewer = TestAutoReview{}; var rt = TestRuntime{ .workspace_root = "/tmp/workspace", .interactive = false, .auto_classifier = reviewer.classifier(), }; defer rt.deinit(alloc); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); const direct = (try tool_admission.requestPermissionOutcome(rt.context().admissionInput(), arena, .{ .id = "direct", .name = "terminal", .arguments_json = "{\"action\":\"exec\",\"command\":\"pwd\"}", }, .ask, &.{})); try std.testing.expectEqual(ToolPermissionDecision.permission_required, direct.decision); try std.testing.expect(direct.execution_authority == null); const blocked = (try tool_admission.requestPermissionOutcome(rt.context().admissionInput(), arena, .{ .id = "blocked", .name = "terminal", .arguments_json = "{\"action\":\"exec\",\"command\":\"touch blocked.txt\"}", }, .ask, &.{})); try std.testing.expectEqual(ToolPermissionDecision.permission_required, blocked.decision); try std.testing.expect(blocked.execution_authority == null); var rules = [_]types.PermissionRule{ .{ .permission = @constCast("bash"), .pattern = @constCast("touch *"), .action = .allow }, }; rt.permission_rules = .{ .rules = &rules }; const configured = (try tool_admission.requestPermissionOutcome(rt.context().admissionInput(), arena, .{ .id = "configured", .name = "terminal", .arguments_json = "{\"action\":\"exec\",\"command\":\"touch configured.txt\"}", }, .ask, &.{})); switch ((configured.execution_authority orelse return error.TestExpectedEqual).run_command) { .direct_only => return error.TestExpectedShellAllowed, .shell_allowed => |authority| try std.testing.expectEqual(command_admission.ShellAuthorizationSource.configured_rule, authority.source), } try std.testing.expectEqual(@as(usize, 0), reviewer.calls); rt.permission_rules = .{}; const automatic = (try tool_admission.requestPermissionOutcome(rt.context().admissionInput(), arena, .{ .id = "automatic", .name = "terminal", .arguments_json = "{\"action\":\"exec\",\"command\":\"touch automatic.txt\"}", }, .auto, &.{})); switch ((automatic.execution_authority orelse return error.TestExpectedEqual).run_command) { .direct_only => return error.TestExpectedShellAllowed, .shell_allowed => |authority| try std.testing.expectEqual(command_admission.ShellAuthorizationSource.auto_classifier, authority.source), } try std.testing.expectEqual(@as(usize, 1), reviewer.calls); try std.testing.expectEqual( std.meta.Tag(permission_auto_classifier.Action).command, reviewer.action_tag.?, ); try std.testing.expectEqualStrings("touch automatic.txt", reviewer.exact_command.?); } test "tool context projects immutable session permission state into admission" { const alloc = std.testing.allocator; var rt = TestRuntime{ .workspace_root = "/tmp/workspace", .interactive = false }; defer rt.deinit(alloc); var rules = [_]types.PermissionRule{.{ .permission = @constCast("bash"), .pattern = @constCast("touch *"), .action = .allow, }}; rt.permission_rules = .{ .rules = &rules }; var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); const call = ToolCall{ .id = "runtime-session-deny", .name = "terminal", .arguments_json = "{\"action\":\"exec\",\"command\":\"touch configured.txt\"}", }; const key = try tool_admission.permissionStateKeyForCall( rt.context().admissionInput(), arena, call, ); try std.testing.expectEqual( session_permission_state.ApplyStatus.applied, try rt.session.applyPermissionEvent(alloc, .{ .set = .{ .key = key, .display_identity = "touch configured.txt", .decision = .deny, .expected_generation = null, } }), ); const outcome = try tool_admission.requestPermissionOutcome( rt.context().admissionInput(), arena, call, .auto, &.{}, ); try std.testing.expectEqual(ToolPermissionDecision.policy_denied, outcome.decision); try std.testing.expect(outcome.execution_authority == null); } test "local file mutations bypass review while external mutations use exact review" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); try tmp.dir.createDirPath(io_mod.getIo(), "workspace/x"); try tmp.dir.createDirPath(io_mod.getIo(), "external"); const workspace = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "workspace"); defer alloc.free(workspace); const external = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "external"); defer alloc.free(external); var reviewer = TestAutoReview{}; var rt = TestRuntime{ .workspace_root = workspace, .permission_mode = .auto, .interactive = false, .auto_classifier = reviewer.classifier(), }; defer rt.deinit(alloc); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); const targets = [_][]const u8{ try std.fs.path.join(arena, &.{ workspace, "approved-local.txt" }), try std.fs.path.join(arena, &.{ external, "approved-external.txt" }), }; for (targets, 0..) |target, index| { const args = try std.fmt.allocPrint( arena, "{{\"path\":\"{s}\",\"content\":\"alpha\\nbeta\\n\"}}", .{target}, ); const call: ToolCall = .{ .id = if (index == 0) "approved-local-write" else "approved-external-write", .name = "write_file", .arguments_json = args, }; const outcome = try tool_admission.requestPermissionOutcome( rt.context().admissionInput(), arena, call, .auto, &.{}, ); try std.testing.expectEqual(ToolPermissionDecision.once, outcome.decision); const authority = switch (outcome.execution_authority orelse return error.TestExpectedEqual) { .file_mutation => |value| value, else => return error.UnexpectedExecutionAuthority, }; const prepared = authority.prepared orelse return error.TestExpectedEqual; try std.testing.expectEqual(@as(usize, 2), prepared.review.additions); try std.testing.expectEqual(@as(usize, 0), prepared.review.deletions); if (index == 0) { try std.testing.expectEqual(@as(usize, 0), reviewer.calls); } else { try std.testing.expectEqual(@as(usize, 1), reviewer.calls); try std.testing.expectEqual( std.meta.Tag(permission_auto_classifier.Action).file_mutation, reviewer.action_tag.?, ); try std.testing.expectEqualStrings("tool_requires_approval", reviewer.escalation_reason.?); try std.testing.expectEqualStrings(target, reviewer.target_path.?); try std.testing.expectEqualStrings(target, reviewer.file_display_path.?); try std.testing.expectEqual(@as(usize, 2), reviewer.file_additions); try std.testing.expectEqual(@as(usize, 0), reviewer.file_deletions); try std.testing.expectEqual(@as(usize, 2), reviewer.file_review_rows); } try std.testing.expect(rt.worker.pending_permission_request_shared == null); try std.testing.expect(!absolutePathExists(target)); } } test "main file mutation producer rejects wildcard-bearing grant roots before prompt publication" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); const workspace = try io_mod.dirRealpathAlloc(arena, tmp.dir, "workspace"); const Scope = enum { workspace, external }; for ([_]u8{ '*', '?' }) |wildcard| { for ([_]Scope{ .workspace, .external }) |scope| { const prefix = if (scope == .workspace) "workspace" else "external"; const dir_name = try std.fmt.allocPrint( arena, "{s}{c}literal", .{ prefix, wildcard }, ); try tmp.dir.createDirPath(io_mod.getIo(), dir_name); const literal_root = try io_mod.dirRealpathAlloc(arena, tmp.dir, dir_name); const edit_path = try std.fs.path.join(arena, &.{ dir_name, "edit.txt" }); { var file = try tmp.dir.createFile( io_mod.getIo(), edit_path, .{ .truncate = true }, ); defer file.close(io_mod.getIo()); try file.writeStreamingAll(io_mod.getIo(), "before\n"); } for ([_]file_mutation_contract.Kind{ .write, .edit }) |kind| { const basename = if (kind == .write) "write.txt" else "edit.txt"; const target_path = try std.fs.path.join( arena, &.{ literal_root, basename }, ); const preflight = try tool_admission.preflightFileMutation( arena, .{ .id = "wildcard-main", .name = if (kind == .write) "write_file" else "edit_file", .arguments_json = try testFileMutationArgs( arena, kind, target_path, ), }, .{ .tool_registry = test_tool_registry, .workspace_root = if (scope == .workspace) literal_root else workspace, .permission_rules = .{}, .permission_mode = .ask, .can_prompt = true, }, ); switch (preflight) { .tool_failure => |reason| try std.testing.expectEqualStrings( "file mutation permission scope could not be represented safely", reason, ), else => return error.TestExpectedToolFailure, } } } } } test "main file mutation prompts project producer-backed workspace and external scopes" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); try tmp.dir.createDirPath(io_mod.getIo(), "external"); { var file = try tmp.dir.createFile( io_mod.getIo(), "workspace/edit.txt", .{ .truncate = true }, ); defer file.close(io_mod.getIo()); try file.writeStreamingAll(io_mod.getIo(), "before\n"); } { var file = try tmp.dir.createFile( io_mod.getIo(), "external/edit.txt", .{ .truncate = true }, ); defer file.close(io_mod.getIo()); try file.writeStreamingAll(io_mod.getIo(), "before\n"); } { var file = try tmp.dir.createFile( io_mod.getIo(), "workspace/max-edit.txt", .{ .truncate = true }, ); defer file.close(io_mod.getIo()); try file.writeStreamingAll(io_mod.getIo(), "before\n"); } const workspace = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "workspace"); defer alloc.free(workspace); const external = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "external"); defer alloc.free(external); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); const workspace_write = try std.fs.path.join( arena, &.{ workspace, "write.txt" }, ); const workspace_edit = try std.fs.path.join( arena, &.{ workspace, "edit.txt" }, ); const external_write = try std.fs.path.join( arena, &.{ external, "write.txt" }, ); const external_edit = try std.fs.path.join( arena, &.{ external, "edit.txt" }, ); const max_write_path = try testMaxAdmittedLexicalPath( arena, "max-write.txt", ); const max_edit_path = try testMaxAdmittedLexicalPath( arena, "max-edit.txt", ); try std.testing.expectEqual( std.Io.Dir.max_path_bytes, max_write_path.len, ); try std.testing.expectEqual( std.Io.Dir.max_path_bytes, max_edit_path.len, ); const workspace_pattern = try std.fmt.allocPrint( arena, "{s}/**", .{workspace}, ); const external_pattern = try std.fmt.allocPrint( arena, "{s}/**", .{external}, ); const cases = [_]struct { call: ToolCall, kind: file_mutation_contract.Kind, external_scope: ?[]const u8, grant_target: []const u8, basename: []const u8, }{ .{ .call = .{ .id = "workspace-write", .name = "write_file", .arguments_json = try testFileMutationArgs( arena, .write, workspace_write, ), }, .kind = .write, .external_scope = null, .grant_target = workspace_pattern, .basename = "write.txt", }, .{ .call = .{ .id = "workspace-edit", .name = "edit_file", .arguments_json = try testFileMutationArgs( arena, .edit, workspace_edit, ), }, .kind = .edit, .external_scope = null, .grant_target = workspace_pattern, .basename = "edit.txt", }, .{ .call = .{ .id = "external-write", .name = "write_file", .arguments_json = try testFileMutationArgs( arena, .write, external_write, ), }, .kind = .write, .external_scope = external, .grant_target = external_pattern, .basename = "write.txt", }, .{ .call = .{ .id = "external-edit", .name = "edit_file", .arguments_json = try testFileMutationArgs( arena, .edit, external_edit, ), }, .kind = .edit, .external_scope = external, .grant_target = external_pattern, .basename = "edit.txt", }, .{ .call = .{ .id = "max-workspace-write", .name = "write_file", .arguments_json = try testFileMutationArgs( arena, .write, max_write_path, ), }, .kind = .write, .external_scope = null, .grant_target = workspace_pattern, .basename = "max-write.txt", }, .{ .call = .{ .id = "max-workspace-edit", .name = "edit_file", .arguments_json = try testFileMutationArgs( arena, .edit, max_edit_path, ), }, .kind = .edit, .external_scope = null, .grant_target = workspace_pattern, .basename = "max-edit.txt", }, }; for (cases) |case| { var rt = TestRuntime{ .workspace_root = workspace, .permission_mode = .ask, }; defer rt.deinit(alloc); rt.worker.worker_processing = true; var state: FilePermissionThreadState = .{ .expected_grant_target = case.grant_target, .expected_workspace_offer = case.external_scope == null, }; const thread = try std.Thread.spawn( .{}, runFilePermissionOutcome, .{ &state, rt.context(), case.call, PermissionMode.ask }, ); var thread_joined = false; defer if (!thread_joined) { rt.worker.requestShutdown(); thread.join(); }; const request_id = try waitForPermissionPrompt( &rt.worker, "file_mutation", ); var snapshot = try rt.worker.snapshotState(alloc); defer snapshot.deinit(alloc); const request = snapshot.pending_permission_request orelse return error.TestExpectedEqual; const file_request = request.file orelse return error.TestExpectedEqual; try std.testing.expectEqualStrings("file_mutation", request.label); try std.testing.expectEqual(case.kind, file_request.kind); try std.testing.expectEqual( file_mutation_contract.FileApprovalIntent.mutation, file_request.intent, ); if (case.external_scope) |expected_root| { switch (file_request.scope) { .workspace_files => return error.TestExpectedExternalScope, .external_tree => |root| try std.testing.expectEqualStrings( expected_root, root, ), } } else { try std.testing.expect(file_request.scope == .workspace_files); } try std.testing.expect( std.mem.find(u8, request.label, file_request.preview.path) == null, ); try std.testing.expectEqualStrings( case.basename, file_request.preview.path[file_request.preview.path_basename_start..], ); try std.testing.expectEqual( worker_runtime.PermissionSubmissionResult.accepted, rt.worker.submitPermissionResponse( request_id, permission_request.OwnedPermissionResponse.init(std.testing.allocator, .once, null), ), ); thread.join(); thread_joined = true; try std.testing.expect(state.err == null); try std.testing.expect(state.file_authority); try std.testing.expect(state.prepared); try std.testing.expect(state.frozen_offer_matches); } } test "file mutation lifecycle decodes each call at most once" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); try tmp.dir.createDirPath(io_mod.getIo(), "external"); { var file = try tmp.dir.createFile( io_mod.getIo(), "workspace/same.txt", .{ .truncate = true }, ); defer file.close(io_mod.getIo()); try file.writeStreamingAll(io_mod.getIo(), "same\n"); } const workspace = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "workspace"); defer alloc.free(workspace); const external = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "external"); defer alloc.free(external); var call_arena_state = std.heap.ArenaAllocator.init(alloc); var call_arena_live = true; defer if (call_arena_live) call_arena_state.deinit(); const call_arena = call_arena_state.allocator(); var result_arena_state = std.heap.ArenaAllocator.init(alloc); defer result_arena_state.deinit(); const result_arena = result_arena_state.allocator(); var deny_rules = [_]types.PermissionRule{.{ .permission = @constCast("edit"), .pattern = @constCast("**"), .action = .deny, }}; const prompted_target = try std.fs.path.join( call_arena, &.{ external, "prompted.txt" }, ); const prompted_args = try std.fmt.allocPrint( call_arena, "{{\"path\":\"{s}\",\"content\":\"prompted\"}}", .{prompted_target}, ); const PreflightTag = std.meta.Tag(tool_admission.FileMutationPreflight); const cases = [_]struct { call: ToolCall, rules: types.PermissionRuleSet = .{}, can_prompt: bool = false, expected: PreflightTag, }{ .{ .call = .{ .id = "malformed", .name = "write_file", .arguments_json = "{" }, .expected = .tool_failure, }, .{ .call = .{ .id = "denied", .name = "write_file", .arguments_json = "{\"path\":\"denied.txt\",\"content\":\"blocked\"}", }, .rules = .{ .rules = &deny_rules }, .expected = .policy_denied, }, .{ .call = .{ .id = "prompted", .name = "write_file", .arguments_json = prompted_args, }, .can_prompt = true, .expected = .prompt, }, .{ .call = .{ .id = "no-op", .name = "write_file", .arguments_json = "{\"path\":\"same.txt\",\"content\":\"same\\n\"}", }, .expected = .allowed, }, .{ .call = .{ .id = "allowed", .name = "write_file", .arguments_json = "{\"path\":\"allowed.txt\",\"content\":\"allowed\"}", }, .expected = .permission_required, }, }; for (cases) |case| { const outcome = try tool_admission.preflightFileMutation(call_arena, case.call, .{ .tool_registry = test_tool_registry, .workspace_root = workspace, .permission_rules = case.rules, .permission_mode = .auto, .can_prompt = case.can_prompt, }); try std.testing.expectEqual(case.expected, std.meta.activeTag(outcome)); } try std.testing.expect(!absolutePathExists(prompted_target)); const committed_call: ToolCall = .{ .id = "committed", .name = "write_file", .arguments_json = "{\"path\":\"committed.txt\",\"content\":\"committed\\n\"}", }; var allow_rules = [_]types.PermissionRule{.{ .permission = @constCast("edit"), .pattern = @constCast("**"), .action = .allow, }}; const committed_authorization = switch (try tool_admission.preflightFileMutation( call_arena, committed_call, .{ .tool_registry = test_tool_registry, .workspace_root = workspace, .permission_rules = .{ .rules = &allow_rules }, .permission_mode = .auto, .can_prompt = false, }, )) { .allowed => |authorization| authorization, else => return error.TestExpectedAllowed, }; var cancel_flag = std.atomic.Value(bool).init(false); var rt = TestRuntime{ .workspace_root = workspace, .permission_mode = .auto, .cancel_flag = &cancel_flag, }; defer rt.deinit(alloc); const applied = try executeToolCallAuthorized(rt.context(), .{ .call_allocator = call_arena, .result_allocator = result_arena, .call = committed_call, .authority = .{ .file_mutation = committed_authorization }, .session_grants = &.{}, .advertised_dynamic_tool_names = &.{}, .max_tool_result_bytes = 64 * 1024, }); try std.testing.expectEqual( tool_contracts.ToolExecutionStatus.success, applied.status, ); try std.testing.expect(applied.committed_file_handoff != null); try std.testing.expect(applied.prepared_result_memory != null); call_arena_state.deinit(); call_arena_live = false; try std.testing.expectEqualStrings( "wrote committed.txt (10 bytes)", applied.model_output, ); try std.testing.expectEqualStrings( "committed.txt", applied.committed_file_handoff.?.preview.path, ); var committed_file = try tmp.dir.openFile( io_mod.getIo(), "workspace/committed.txt", .{}, ); defer committed_file.close(io_mod.getIo()); const committed_content = try io_mod.readFileToEnd( result_arena, &committed_file, 64, ); try std.testing.expectEqualStrings("committed\n", committed_content); } test "file mutation preflight separates edit approval from equality disclosure" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); try tmp.dir.createDirPath(io_mod.getIo(), "external"); { var file = try tmp.dir.createFile( io_mod.getIo(), "workspace/same.txt", .{ .truncate = true }, ); defer file.close(io_mod.getIo()); try file.writeStreamingAll(io_mod.getIo(), "same\n"); } { var file = try tmp.dir.createFile( io_mod.getIo(), "external/private.txt", .{ .truncate = true }, ); defer file.close(io_mod.getIo()); try file.writeStreamingAll(io_mod.getIo(), "private\n"); } const workspace = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "workspace"); defer alloc.free(workspace); const external = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "external"); defer alloc.free(external); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); const private_target = try std.fs.path.join( arena, &.{ external, "private.txt" }, ); const private_pattern = try std.fmt.allocPrint( arena, "{s}/**", .{external}, ); var equality_rules = [_]types.PermissionRule{ .{ .permission = @constCast("read"), .pattern = private_pattern, .action = .deny, }, .{ .permission = @constCast("edit"), .pattern = private_pattern, .action = .allow, }, }; const private_noop_args = try std.fmt.allocPrint( arena, "{{\"path\":\"{s}\",\"content\":\"private\\n\"}}", .{private_target}, ); const private_changed_args = try std.fmt.allocPrint( arena, "{{\"path\":\"{s}\",\"content\":\"changed\\n\"}}", .{private_target}, ); const PreflightTag = std.meta.Tag(tool_admission.FileMutationPreflight); const cases = [_]struct { call: ToolCall, rules: types.PermissionRuleSet = .{}, permission_mode: PermissionMode, can_prompt: bool, expected: PreflightTag, }{ .{ .call = .{ .id = "authorized-no-op", .name = "write_file", .arguments_json = "{\"path\":\"same.txt\",\"content\":\"same\\n\"}", }, .permission_mode = .ask, .can_prompt = true, .expected = .allowed, }, .{ .call = .{ .id = "read-denied-no-op", .name = "write_file", .arguments_json = private_noop_args, }, .rules = .{ .rules = &equality_rules }, .permission_mode = .auto, .can_prompt = true, .expected = .prompt, }, .{ .call = .{ .id = "read-denied-no-op-noninteractive", .name = "write_file", .arguments_json = private_noop_args, }, .rules = .{ .rules = &equality_rules }, .permission_mode = .auto, .can_prompt = false, .expected = .permission_required, }, .{ .call = .{ .id = "read-denied-changed", .name = "write_file", .arguments_json = private_changed_args, }, .rules = .{ .rules = &equality_rules }, .permission_mode = .auto, .can_prompt = false, .expected = .allowed, }, }; for (cases) |case| { const outcome = try tool_admission.preflightFileMutation(arena, case.call, .{ .tool_registry = test_tool_registry, .workspace_root = workspace, .permission_rules = case.rules, .permission_mode = case.permission_mode, .can_prompt = case.can_prompt, }); try std.testing.expectEqual(case.expected, std.meta.activeTag(outcome)); const authorization = switch (outcome) { .allowed => |value| value, .prompt => |value| value, .permission_required => |value| value, .policy_denied, .tool_failure => return error.TestExpectedAuthorization, }; try std.testing.expect(authorization.prepared != null); } } test "request tool permission combines configured external copy target with workspace automatic review" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); try tmp.dir.createDirPath(io_mod.getIo(), "external"); { var source = try tmp.dir.createFile(io_mod.getIo(), "workspace/source.txt", .{ .truncate = true }); defer source.close(io_mod.getIo()); try source.writeStreamingAll(io_mod.getIo(), "source\n"); } const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "workspace"); defer alloc.free(root); const external = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "external"); defer alloc.free(external); const pattern = try std.fmt.allocPrint(alloc, "{s}/**", .{external}); defer alloc.free(pattern); var rules = [_]types.PermissionRule{ .{ .permission = @constCast("copy_file"), .pattern = pattern, .action = .allow, }, }; var reviewer = TestAutoReview{}; var rt = TestRuntime{ .workspace_root = root, .permission_mode = .auto, .permission_rules = .{ .rules = &rules }, .interactive = false, .auto_classifier = reviewer.classifier(), }; defer rt.deinit(alloc); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); try std.testing.expectEqual(ToolPermissionDecision.once, (try tool_admission.requestPermissionOutcome(rt.context().admissionInput(), arena, .{ .id = "copy-external", .name = "copy_file", .arguments_json = "{\"source\":\"source.txt\",\"destination\":\"../external/copied.txt\"}", }, .auto, &.{})).decision); try std.testing.expectEqual(@as(usize, 1), reviewer.calls); try std.testing.expectEqualStrings("tool_requires_approval", reviewer.escalation_reason.?); try std.testing.expect(rt.worker.pending_permission_request_shared == null); } test "disabled automatic reviewer returns a recoverable denial without a human prompt" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); try tmp.dir.createDirPath(io_mod.getIo(), "external"); const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "workspace"); defer alloc.free(root); const external = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "external"); defer alloc.free(external); var rt = TestRuntime{ .workspace_root = root, .interactive = false }; defer rt.deinit(std.testing.allocator); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); const target = try std.fs.path.join(arena, &.{ external, "created.txt" }); const args = try std.fmt.allocPrint(arena, "{{\"path\":\"{s}\",\"content\":\"hello\"}}", .{target}); const outcome = try tool_admission.requestPermissionOutcome(rt.context().admissionInput(), arena, .{ .id = "external-write", .name = "write_file", .arguments_json = args, }, .auto, &.{}); try std.testing.expectEqual(ToolPermissionDecision.deny, outcome.decision); try std.testing.expectEqual(types.ToolPermissionDenialReason.auto_denied, outcome.denial_reason.?); try std.testing.expect(outcome.requirement == null); try std.testing.expect(outcome.execution_authority == null); try std.testing.expect(outcome.auto_review_result == null); try std.testing.expect(rt.worker.pending_permission_request_shared == null); } test "web_fetch permits valid public hosts by default" { var rt = TestRuntime{ .workspace_root = "/tmp/workspace", .permission_mode = .auto, .interactive = false }; defer rt.deinit(std.testing.allocator); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); try std.testing.expectEqual(ToolPermissionDecision.once, (try tool_admission.requestPermissionOutcome(rt.context().admissionInput(), arena, .{ .id = "fetch", .name = "web_fetch", .arguments_json = "{\"url\":\"https://docs.vercel.com/frameworks/nextjs\"}", }, .auto, &.{})).decision); } test "web_fetch exact session grant authorizes only matching canonical domain" { var rt = TestRuntime{ .workspace_root = "/tmp/workspace", .permission_mode = .auto, .interactive = false }; defer rt.deinit(std.testing.allocator); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); const grants = [_]PermissionGrant{ .{ .tool_name = @constCast("web_fetch"), .target_path = @constCast("domain:example.com") }, }; var ask_rules = [_]types.PermissionRule{ .{ .permission = @constCast("web_fetch"), .pattern = @constCast("domain:example.com"), .action = .ask }, .{ .permission = @constCast("web_fetch"), .pattern = @constCast("domain:example.org"), .action = .ask }, }; rt.permission_rules = .{ .rules = &ask_rules }; try std.testing.expectEqual(ToolPermissionDecision.once, (try tool_admission.requestPermissionOutcome(rt.context().admissionInput(), arena, .{ .id = "fetch", .name = "web_fetch", .arguments_json = "{\"url\":\"https://example.com/docs\"}", }, .auto, &grants)).decision); try std.testing.expectEqual(ToolPermissionDecision.permission_required, (try tool_admission.requestPermissionOutcome(rt.context().admissionInput(), arena, .{ .id = "fetch_other", .name = "web_fetch", .arguments_json = "{\"url\":\"https://example.org/docs\"}", }, .auto, &grants)).decision); } test "request tool permission honors configured deny and allow rules" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "workspace/src"); { var file = try tmp.dir.createFile(io_mod.getIo(), "workspace/src/app.zig", .{ .truncate = true }); defer file.close(io_mod.getIo()); try file.writeStreamingAll(io_mod.getIo(), "pub fn main() void {}\n"); } const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "workspace"); defer alloc.free(root); var rt = TestRuntime{ .workspace_root = root, .permission_mode = .auto, }; defer rt.deinit(std.testing.allocator); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); var deny_rules = [_]types.PermissionRule{ .{ .permission = @constCast("grep"), .pattern = @constCast("src"), .action = .deny }, }; rt.permission_rules = .{ .rules = &deny_rules }; try std.testing.expectEqual(ToolPermissionDecision.policy_denied, (try tool_admission.requestPermissionOutcome(rt.context().admissionInput(), arena, .{ .id = "1", .name = "grep_files", .arguments_json = "{\"pattern\":\"TODO\",\"path\":\"src\"}", }, .ask, &.{})).decision); var allow_rules = [_]types.PermissionRule{ .{ .permission = @constCast("*"), .pattern = @constCast("*"), .action = .ask }, .{ .permission = @constCast("read"), .pattern = @constCast("src/app.zig"), .action = .allow }, }; rt.permission_rules = .{ .rules = &allow_rules }; try std.testing.expectEqual(ToolPermissionDecision.once, (try tool_admission.requestPermissionOutcome(rt.context().admissionInput(), arena, .{ .id = "1", .name = "read_file", .arguments_json = "{\"path\":\"src/app.zig\"}", }, .ask, &.{})).decision); } test "request tool permission denies semantic_search outside workspace target" { const alloc = std.testing.allocator; var workspace_tmp = std.testing.tmpDir(.{}); defer workspace_tmp.cleanup(); try workspace_tmp.dir.createDirPath(io_mod.getIo(), "workspace"); var external_tmp = std.testing.tmpDir(.{}); defer external_tmp.cleanup(); { var file = try external_tmp.dir.createFile(io_mod.getIo(), "outside.txt", .{ .truncate = true }); defer file.close(io_mod.getIo()); try file.writeStreamingAll(io_mod.getIo(), "needle external\n"); } const root = try io_mod.dirRealpathAlloc(alloc, workspace_tmp.dir, "workspace"); defer alloc.free(root); const external = try io_mod.dirRealpathAlloc(alloc, external_tmp.dir, "."); defer alloc.free(external); var rt = TestRuntime{ .workspace_root = root, .permission_mode = .auto, }; defer rt.deinit(alloc); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); const args = try std.fmt.allocPrint(arena, "{{\"query\":\"needle\",\"path\":\"{s}\"}}", .{external}); try std.testing.expectEqual(ToolPermissionDecision.policy_denied, (try tool_admission.requestPermissionOutcome(rt.context().admissionInput(), arena, .{ .id = "semantic", .name = "semantic_search", .arguments_json = args, }, .auto, &.{})).decision); } test "executeToolCall rejects overlong glob pattern with failure status" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "workspace"); defer alloc.free(root); const pattern = try alloc.alloc(u8, glob_pattern.max_pattern_bytes + 1); defer alloc.free(pattern); @memset(pattern, 'a'); var rt = TestRuntime{ .workspace_root = root }; defer rt.deinit(alloc); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); const args = try std.fmt.allocPrint(arena, "{{\"pattern\":\"{s}\"}}", .{pattern}); const result = try executeToolCall(rt.context(), arena, .{ .id = "glob", .name = "glob_files", .arguments_json = args, }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.failure, result.status); const expected = try std.fmt.allocPrint(arena, "glob_files field \"pattern\" must be at most {d} bytes", .{glob_pattern.max_pattern_bytes}); try std.testing.expectEqualStrings(expected, result.model_output); } test "executeToolCall glob_files finds match beyond former traversal cap" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); var i: usize = 0; while (i < 2050) : (i += 1) { var name_buf: [64]u8 = undefined; const name = try std.fmt.bufPrint(&name_buf, "workspace/many/file-{d:0>4}.txt", .{i}); try writeTestFile(tmp.dir, name, "not here\n"); } try writeTestFile(tmp.dir, "workspace/zzzz/target.zig", "target\n"); const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "workspace"); defer alloc.free(root); var rt = TestRuntime{ .workspace_root = root }; defer rt.deinit(alloc); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const result = try executeToolCall(rt.context(), arena_state.allocator(), .{ .id = "glob", .name = "glob_files", .arguments_json = "{\"pattern\":\"**/*.zig\"}", }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.success, result.status); try expectContains(result.model_output, "zzzz/target.zig"); try std.testing.expect(std.mem.find(u8, result.model_output, "traversal cap") == null); } test "executeToolCall carries authoritative read coverage metadata" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); try writeTestFile(tmp.dir, "workspace/notes.txt", "one\ntwo\n"); const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "workspace"); defer alloc.free(root); var rt = TestRuntime{ .workspace_root = root }; defer rt.deinit(alloc); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); const full = try executeToolCall(rt.context(), arena, .{ .id = "full", .name = "read_file", .arguments_json = "{\"path\":\"notes.txt\"}", }); try std.testing.expect( full.tool_result_memory.?.model_view_covers_full_file.?, ); const partial = try executeToolCall(rt.context(), arena, .{ .id = "partial", .name = "read_file", .arguments_json = "{\"path\":\"notes.txt\",\"start_line\":2,\"line_count\":1}", }); try std.testing.expect( !partial.tool_result_memory.?.model_view_covers_full_file.?, ); } test "executeToolCall traces non-text grep skips in normal runtime" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeTestFile(tmp.dir, "workspace/binaryish/blob.bin", "binary\x00needle\x01\n"); const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "workspace"); defer alloc.free(root); const trace_path = try std.fs.path.join(alloc, &.{ root, "trace.log" }); defer alloc.free(trace_path); debug_trace.resetForTest(); try debug_trace.configureForTest(alloc, trace_path); defer debug_trace.resetForTest(); var rt = TestRuntime{ .workspace_root = root }; defer rt.deinit(alloc); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const result = try executeToolCall(rt.context(), arena_state.allocator(), .{ .id = "grep", .name = "grep_files", .arguments_json = "{\"pattern\":\"needle\",\"path\":\"binaryish\"}", }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.success, result.status); try std.testing.expectEqualStrings("[grep] no matches for needle\n", result.model_output); const trace = try readTraceFileForTest(alloc, trace_path); defer alloc.free(trace); try expectContains(trace, "grep_files skipped non-text file path="); try expectContains(trace, "binaryish/blob.bin"); } test "executeToolCall traces oversized grep skips in normal runtime" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeLargeTestFile(tmp.dir, "workspace/oversized/huge.txt", "needle hidden\n", 260 * 1024); const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "workspace"); defer alloc.free(root); const trace_path = try std.fs.path.join(alloc, &.{ root, "trace.log" }); defer alloc.free(trace_path); debug_trace.resetForTest(); try debug_trace.configureForTest(alloc, trace_path); defer debug_trace.resetForTest(); var rt = TestRuntime{ .workspace_root = root }; defer rt.deinit(alloc); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const result = try executeToolCall(rt.context(), arena_state.allocator(), .{ .id = "grep", .name = "grep_files", .arguments_json = "{\"pattern\":\"needle\",\"path\":\"oversized\"}", }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.success, result.status); try std.testing.expectEqualStrings("[grep] no matches for needle\n", result.model_output); const trace = try readTraceFileForTest(alloc, trace_path); defer alloc.free(trace); try expectContains(trace, "grep_files skipped oversized file path="); try expectContains(trace, "oversized/huge.txt"); } test "executeToolCall returns failure status for missing grep path" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "workspace"); defer alloc.free(root); var rt = TestRuntime{ .workspace_root = root }; defer rt.deinit(alloc); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const result = try executeToolCall(rt.context(), arena_state.allocator(), .{ .id = "grep", .name = "grep_files", .arguments_json = "{\"pattern\":\"needle\",\"path\":\"missing/nope.txt\"}", }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.failure, result.status); try expectContains(result.model_output, "Unable to resolve grep search root: missing/nope.txt (FileNotFound)"); } test "executeToolCall returns friendly failure for missing read_file path" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "workspace"); defer alloc.free(root); var rt = TestRuntime{ .workspace_root = root }; defer rt.deinit(alloc); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const result = try executeToolCall(rt.context(), arena_state.allocator(), .{ .id = "read", .name = "read_file", .arguments_json = "{\"path\":\"missing/nope.txt\"}", }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.failure, result.status); try std.testing.expect(!result.finish_turn); try expectToolErrorField(result.model_output, "type", "tool_execution_failed"); try expectToolErrorField(result.model_output, "tool_name", "read_file"); try expectToolErrorDetailString(result.model_output, "field", "path"); try expectToolErrorDetailString(result.model_output, "path", "missing/nope.txt"); try expectToolErrorDetailString(result.model_output, "error", "FileNotFound"); try expectContains(result.model_output, "glob_files"); try expectNotContains(result.model_output, "Tool read_file failed: FileNotFound"); } test "executeToolCall preserves explicit ignored directory grep roots" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeTestFile(tmp.dir, "workspace/node_modules/pkg/index.js", "needle module\n"); const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "workspace"); defer alloc.free(root); var rt = TestRuntime{ .workspace_root = root }; rt.ignored_list_entries = &.{"node_modules"}; defer rt.deinit(alloc); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const result = try executeToolCall(rt.context(), arena_state.allocator(), .{ .id = "grep", .name = "grep_files", .arguments_json = "{\"pattern\":\"needle\",\"path\":\"node_modules\"}", }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.success, result.status); try expectContains(result.model_output, "node_modules/pkg/index.js:1: needle module"); } test "request tool permission checks copy and rename destinations" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "workspace/src"); { var file = try tmp.dir.createFile(io_mod.getIo(), "workspace/src/source.txt", .{ .truncate = true }); defer file.close(io_mod.getIo()); try file.writeStreamingAll(io_mod.getIo(), "source\n"); } const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "workspace"); defer alloc.free(root); var rt = TestRuntime{ .workspace_root = root }; defer rt.deinit(std.testing.allocator); var rules = [_]types.PermissionRule{ .{ .permission = @constCast("copy_file"), .pattern = @constCast("blocked/*"), .action = .deny }, .{ .permission = @constCast("rename_file"), .pattern = @constCast("blocked/*"), .action = .deny }, }; rt.permission_rules = .{ .rules = &rules }; var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); try std.testing.expectEqual(ToolPermissionDecision.policy_denied, (try tool_admission.requestPermissionOutcome(rt.context().admissionInput(), arena, .{ .id = "copy", .name = "copy_file", .arguments_json = "{\"source\":\"src/source.txt\",\"destination\":\"blocked/copied.txt\"}", }, .auto, &.{})).decision); try std.testing.expectEqual(ToolPermissionDecision.policy_denied, (try tool_admission.requestPermissionOutcome(rt.context().admissionInput(), arena, .{ .id = "rename", .name = "rename_file", .arguments_json = "{\"old_path\":\"src/source.txt\",\"new_path\":\"blocked/renamed.txt\"}", }, .auto, &.{})).decision); try std.testing.expect(!absolutePathExists(try std.fs.path.join(arena, &.{ root, "blocked/copied.txt" }))); try std.testing.expect(!absolutePathExists(try std.fs.path.join(arena, &.{ root, "blocked/renamed.txt" }))); try std.testing.expect(absolutePathExists(try std.fs.path.join(arena, &.{ root, "src/source.txt" }))); } test "run_command timeout returns model-visible failure" { if (builtin.os.tag == .windows or builtin.os.tag == .wasi) return; const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDir( io_mod.getIo(), "session", std.Io.File.Permissions.fromMode(0o700), ); var session_dir = try tmp.dir.openDir(io_mod.getIo(), "session", .{ .iterate = true, .follow_symlinks = false, }); defer session_dir.close(io_mod.getIo()); const session_path = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "session"); defer alloc.free(session_path); var capability = try session_child_store.SessionChildCapability.initForTesting( alloc, session_dir, session_path, .writable, .{}, ); defer capability.deinit(); var rt = TestRuntime{ .command_timeout_ms = 1000, .session_child_capability = &capability, }; defer rt.deinit(std.testing.allocator); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); const tool_call: ToolCall = .{ .id = "cmd", .name = "terminal", .arguments_json = "{\"action\":\"exec\",\"command\":\"printf 'PRE-TIMEOUT-OUT\\n'; printf 'PRE-TIMEOUT-ERR\\n' >&2; sleep 5\",\"profile\":\"clean\"}", }; const result = try executeTestRunCommand(rt.context(), arena, tool_call); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.failure, result.status); try expectContains(result.model_output, "timeout=true\n"); try expectContains(result.model_output, "timeout_ms=1000\n"); try expectContains(result.model_output, "command timed out and was terminated\n"); try expectNotContains(result.model_output, "PRE-TIMEOUT-OUT"); try expectNotContains(result.model_output, "PRE-TIMEOUT-ERR"); const structured = result.command_result_json orelse return error.TestExpectedEqual; try expectCommandResultField(structured, "kind", "foreground"); try expectCommandResultField( structured, "command", "printf 'PRE-TIMEOUT-OUT\n'; printf 'PRE-TIMEOUT-ERR\n' >&2; sleep 5", ); try expectCommandResultBool(structured, "timed_out", true); try expectCommandResultInt(structured, "stdout_bytes", 0); try expectCommandResultInt(structured, "stderr_bytes", 0); const capture = result.command_replay_capture orelse return error.TestExpectedReplay; var canonical = (try capture.canonicalizeForComparison( arena_state.allocator(), )) orelse return error.TestExpectedReplay; defer canonical.deinit(arena_state.allocator()); var saw_stdout = false; var saw_stderr = false; for (canonical.records.items) |record| { if (record.stream == .stdout and std.mem.eql(u8, record.text.items, "PRE-TIMEOUT-OUT")) saw_stdout = true; if (record.stream == .stderr and std.mem.eql(u8, record.text.items, "PRE-TIMEOUT-ERR")) saw_stderr = true; } try std.testing.expect(saw_stdout); try std.testing.expect(saw_stderr); const runtime_execution_memory = @import("../agent/runtime/execution_memory.zig"); const runtime_config = @import("../agent/runtime/config.zig"); const execution_memory = @import("../agent/execution_memory.zig"); const session_codec = @import("../session/session_codec.zig"); var cancel = std.atomic.Value(bool).init(false); const config = runtime_config.Config{ .system_prompt = "", .gateway_retry_count = 0, .gateway_chat_url = "", .gateway_tools_json = "[]", .agent_step_limit = 1, .cancel_flag = &cancel, .session_child_capability = &capability, }; var prepared = try runtime_execution_memory.prepareToolModelOutput( arena, config, tool_call, result.model_output, ); runtime_execution_memory.applyToolResultMemory( &prepared.memory, result.tool_result_memory, ); var replay_finalized = false; defer if (!replay_finalized) capture.abort(arena); runtime_execution_memory.finalizeCommandReplay( arena, tool_call, &prepared, &capability, capture, ); replay_finalized = true; const terminal_replay = prepared.memory.command_output_replay orelse return error.TestExpectedReplay; const terminal_descriptor = switch (terminal_replay) { .available => |value| value, .unavailable => return error.TestExpectedReplay, }; var terminal_reader = try command_replay_store.Reader.open( alloc, &capability, terminal_descriptor, ); defer terminal_reader.deinit(); var terminal_stdout: std.ArrayList(u8) = .empty; defer terminal_stdout.deinit(alloc); var terminal_stderr: std.ArrayList(u8) = .empty; defer terminal_stderr.deinit(alloc); while (try terminal_reader.next(alloc)) |frame| { defer alloc.free(frame.payload); switch (frame.stream) { .stdout => try terminal_stdout.appendSlice(alloc, frame.payload), .stderr => try terminal_stderr.appendSlice(alloc, frame.payload), } } try expectContains(terminal_stdout.items, "PRE-TIMEOUT-OUT\n"); try expectContains(terminal_stderr.items, "PRE-TIMEOUT-ERR\n"); const persisted = try execution_memory.makePersistedToolResult( alloc, tool_call.id, tool_call.name, .failure, prepared.model_output, prepared.memory, ); defer execution_memory.freeTransientPersistedToolResult(alloc, persisted); var calls = [_]types.ToolCall{tool_call}; var results = [_]types.PersistedToolResult{persisted}; var steps = [_]types.ToolExecutionStep{.{ .tool_calls = calls[0..], .tool_results = results[0..], }}; const turn: types.HistoryTurn = .{ .assistant = .{ .user = .{ .text = @constCast("run the timeout fixture") }, .assistant = @constCast(""), .execution = .{ .tool_steps = steps[0..] }, } }; var encoded: std.Io.Writer.Allocating = .init(alloc); defer encoded.deinit(); try session_codec.writeHistoryTurn(&encoded.writer, turn); var parsed = try std.json.parseFromSlice( std.json.Value, alloc, encoded.written(), .{}, ); defer parsed.deinit(); const decoded = try session_codec.parseHistoryTurn(alloc, parsed.value); defer session_runtime.freeHistoryTurn(alloc, decoded); const decoded_result = decoded.assistant.execution.tool_steps[0].tool_results[0]; try expectNotContains(decoded_result.output, "PRE-TIMEOUT-OUT"); try expectNotContains(decoded_result.output, "PRE-TIMEOUT-ERR"); const decoded_replay = decoded_result.command_output_replay orelse return error.TestExpectedReplay; const decoded_descriptor = switch (decoded_replay) { .available => |value| value, .unavailable => return error.TestExpectedReplay, }; var resumed_reader = try command_replay_store.Reader.open( alloc, &capability, decoded_descriptor, ); defer resumed_reader.deinit(); var resumed_stdout: std.ArrayList(u8) = .empty; defer resumed_stdout.deinit(alloc); var resumed_stderr: std.ArrayList(u8) = .empty; defer resumed_stderr.deinit(alloc); while (try resumed_reader.next(alloc)) |frame| { defer alloc.free(frame.payload); switch (frame.stream) { .stdout => try resumed_stdout.appendSlice(alloc, frame.payload), .stderr => try resumed_stderr.appendSlice(alloc, frame.payload), } } try std.testing.expectEqualStrings(terminal_stdout.items, resumed_stdout.items); try std.testing.expectEqualStrings(terminal_stderr.items, resumed_stderr.items); } test "interactive command replay capture allocation fails open" { var failing = std.testing.FailingAllocator.init( std.testing.allocator, .{ .fail_index = 0 }, ); const init = initCommandReplayCapture( failing.allocator(), true, 64 * 1024, 64 * 1024, null, null, false, ); try std.testing.expect(init.capture == null); try std.testing.expect(init.unavailable); var callback = CommandReplayCaptureCallback{ .alloc = std.testing.allocator, .capture = null, }; try CommandReplayCaptureCallback.onChunk( @ptrCast(&callback), null, .stdout, "accepted output\n", ); try std.testing.expect(callback.had_accepted_output); var transferred = false; const result = try finishCommandToolResult( null, init.unavailable and callback.had_accepted_output, &transferred, null, .{ .model_output = "exit_code=0\n(no output)\n" }, ); try std.testing.expect(!transferred); switch (result.tool_result_memory.?.command_output_replay.?) { .unavailable => {}, .available => return error.TestExpectedUnavailableReplay, } } test "interactive devbox success emits raw provider output once" { const alloc = std.testing.allocator; var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); var rt = TestRuntime{ .workspace_root = "/tmp", .permission_mode = .auto, .sandbox_backend = .vercel, }; defer rt.deinit(alloc); var provider = TestDevboxProvider{}; var captured = TestCommandOutputCapture{ .alloc = alloc }; defer captured.deinit(); var ctx = rt.context(); ctx.devbox_provider = .{ .ctx = @ptrCast(&provider), .execute_fn = executeTestDevboxProvider, }; ctx.output_chunk_ctx = @ptrCast(&captured); ctx.on_output_chunk = TestCommandOutputCapture.onChunk; const result = try executeTestRunCommand(ctx, arena, .{ .id = "devbox-compact", .name = "terminal", .arguments_json = "{\"action\":\"exec\",\"command\":\"printf remote > provider-output.txt\"}", }); try std.testing.expectEqual(@as(usize, 1), provider.calls); try std.testing.expectEqualStrings( "DEVBOX_COMPACT_ONE\nDEVBOX_COMPACT_TWO\n", captured.bytes.items, ); try std.testing.expectEqual(@as(usize, 1), captured.stdout_chunks); try std.testing.expectEqual(@as(usize, 0), captured.stderr_chunks); try expectContains(result.model_output, "\nDEVBOX_COMPACT_ONE\nDEVBOX_COMPACT_TWO\n\n"); const replay = result.command_replay_capture orelse return error.TestExpectedReplay; try std.testing.expect(replay.hasOutput()); } test "run_command reactive sandbox retry timeout retains both attempts" { if (builtin.os.tag != .macos) return; const alloc = std.testing.allocator; const zio = io_mod.getIo(); const fixture_root = try std.fmt.allocPrint( alloc, "/var/tmp/fx-sandbox-retry-{d}-{d}", .{ std.c.getpid(), io_mod.nanoTimestamp() }, ); defer alloc.free(fixture_root); try std.Io.Dir.cwd().createDirPath(zio, fixture_root); defer std.Io.Dir.cwd().deleteTree(zio, fixture_root) catch {}; const home_path = try std.fs.path.join(alloc, &.{ fixture_root, "home" }); defer alloc.free(home_path); const cache_path = try std.fs.path.join(alloc, &.{ home_path, ".cache" }); defer alloc.free(cache_path); const workspace_path = try std.fs.path.join(alloc, &.{ fixture_root, "workspace" }); defer alloc.free(workspace_path); // The restricted profile permits /tmp and /private/tmp, so a fixture there // cannot prove that the retry widens access to the user's cache directory. try std.Io.Dir.cwd().createDirPath(zio, cache_path); try std.Io.Dir.cwd().createDirPath(zio, workspace_path); const home = try io_mod.realpathAlloc(alloc, home_path); defer alloc.free(home); const workspace = try io_mod.realpathAlloc(alloc, workspace_path); defer alloc.free(workspace); try setTestHome(home); defer setTestHome(null) catch {}; var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); const marker = try std.fmt.allocPrint( arena, "{s}/.cache/fx-replay-retry-{d}", .{ home, io_mod.nanoTimestamp() }, ); defer std.Io.Dir.deleteFileAbsolute(io_mod.getIo(), marker) catch {}; const command = try std.fmt.allocPrint( arena, "printf 'FIRST-ATTEMPT\\n'; printf x > '{s}' || exit 1; printf 'RETRY-ATTEMPT\\n'; sleep 5", .{marker}, ); const args = try runCommandArgsForTest(arena, command); var rt = TestRuntime{ .workspace_root = workspace, .permission_mode = .auto, .sandbox_backend = .macos, .command_timeout_ms = 750, }; defer rt.deinit(alloc); const call = ToolCall{ .id = "retry-timeout", .name = "terminal", .arguments_json = args, }; const restricted = try executeTestRunCommand(rt.context(), arena, call); const required = restricted.sandbox_scope_required orelse return error.TestExpectedSandboxScopeRequired; try std.testing.expectEqual( tool_contracts.SandboxWideningPhase.reactive, required.phase, ); var broader_context = try tool_admission.runCommandContext( rt.context().admissionInput(), arena, call, ); broader_context.scope = .broader; var restricted_capture_owned = required.command_replay_capture != null; defer if (restricted_capture_owned) { required.command_replay_capture.?.abort(arena); }; restricted_capture_owned = false; const result = try executeToolCallAuthorized(rt.context(), .{ .call_allocator = arena, .result_allocator = arena, .call = call, .authority = .{ .run_command = .{ .shell_allowed = .{ .fingerprint = .init(broader_context), .source = .auto_classifier, } } }, .session_grants = &.{}, .advertised_dynamic_tool_names = &.{}, .max_tool_result_bytes = rt.max_tool_result_bytes, .command_timeout_started_ms = required.command_timeout_started_ms, .command_replay_capture = required.command_replay_capture, .command_replay_unavailable = required.command_replay_unavailable, }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.failure, result.status); try expectContains(result.model_output, "timeout=true\n"); try expectNotContains(result.model_output, "FIRST-ATTEMPT"); try expectNotContains(result.model_output, "RETRY-ATTEMPT"); const capture = result.command_replay_capture orelse return error.TestExpectedReplay; defer capture.abort(arena); var canonical = (try capture.canonicalizeForComparison( arena, )) orelse return error.TestExpectedReplay; defer canonical.deinit(arena); var first_attempts: usize = 0; var retry_attempts: usize = 0; for (canonical.records.items) |record| { if (record.stream == .stdout and std.mem.eql(u8, record.text.items, "FIRST-ATTEMPT")) first_attempts += 1; if (record.stream == .stdout and std.mem.eql(u8, record.text.items, "RETRY-ATTEMPT")) retry_attempts += 1; } try std.testing.expectEqual(@as(usize, 2), first_attempts); try std.testing.expectEqual(@as(usize, 1), retry_attempts); } test "run_command post-spawn cancellation returns structured evidence in every mode" { if (builtin.os.tag == .windows or builtin.os.tag == .wasi) return; const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); try tmp.dir.createDirPath(io_mod.getIo(), "artifacts"); const workspace = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "workspace"); defer alloc.free(workspace); const artifact_dir = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "artifacts"); defer alloc.free(artifact_dir); var cancel = std.atomic.Value(bool).init(false); var rt = TestRuntime{ .workspace_root = workspace, .cancel_flag = &cancel, .permission_mode = .auto, .sandbox_backend = .none, .command_artifact_dir = artifact_dir, }; defer rt.deinit(alloc); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); const interactive_marker = try std.fs.path.join(arena, &.{ workspace, "interactive-ready" }); const interactive_command = try std.fmt.allocPrint( arena, "trap 'exit 0' TERM; printf ready > '{s}'; printf 'RUNTIME-READY\\n'; while :; do :; done", .{interactive_marker}, ); const interactive_args = try runCommandArgsForTest(arena, interactive_command); var interactive_trigger = CancelTestCommandOnOutput{ .flag = &cancel, .needle = "RUNTIME-READY", }; var interactive_ctx = rt.context(); interactive_ctx.output_chunk_ctx = @ptrCast(&interactive_trigger); interactive_ctx.on_output_chunk = CancelTestCommandOnOutput.onChunk; const interactive = try executeTestRunCommand(interactive_ctx, arena, .{ .id = "cancel-interactive", .name = "terminal", .arguments_json = interactive_args, }); try std.testing.expect(interactive_trigger.seen); try std.testing.expect(absolutePathExists(interactive_marker)); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.failure, interactive.status); try std.testing.expect(interactive.cancelled); try std.testing.expectEqualStrings("command cancelled\n", interactive.model_output); const structured = interactive.command_result_json orelse return error.TestExpectedEqual; try expectCommandResultField(structured, "kind", "foreground"); try expectCommandResultBool(structured, "truncated", false); try expectCommandResultStringPrefix(structured, "output_file", artifact_dir); cancel.store(false, .seq_cst); rt.interactive = false; const headless_marker = try std.fs.path.join(arena, &.{ workspace, "headless-ready" }); const headless_command = try std.fmt.allocPrint( arena, "trap 'exit 0' TERM; printf ready > '{s}'; printf 'HEADLESS-READY\\n'; while :; do :; done", .{headless_marker}, ); const headless_args = try runCommandArgsForTest(arena, headless_command); var headless_trigger = CancelTestCommandOnOutput{ .flag = &cancel, .needle = "HEADLESS-READY", }; var headless_ctx = rt.context(); headless_ctx.output_chunk_ctx = @ptrCast(&headless_trigger); headless_ctx.on_output_chunk = CancelTestCommandOnOutput.onChunk; const headless = try executeTestRunCommand(headless_ctx, arena, .{ .id = "cancel-headless", .name = "terminal", .arguments_json = headless_args, }); try std.testing.expect(headless_trigger.seen); try std.testing.expect(absolutePathExists(headless_marker)); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.failure, headless.status); try std.testing.expect(headless.cancelled); try std.testing.expectEqualStrings("command cancelled\n", headless.model_output); const headless_structured = headless.command_result_json orelse return error.TestExpectedEqual; try expectCommandResultField(headless_structured, "kind", "foreground"); try expectCommandResultField(headless_structured, "command", headless_command); try expectCommandResultBool(headless_structured, "truncated", false); try expectCommandResultStringPrefix(headless_structured, "output_file", artifact_dir); diagnostics.resetForTest(); defer diagnostics.resetForTest(); cancel.store(false, .seq_cst); headless_trigger.seen = false; var retry_ctx = rt.context(); retry_ctx.output_chunk_ctx = @ptrCast(&headless_trigger); retry_ctx.on_output_chunk = CancelTestCommandOnOutput.onChunk; var command_ctx = try tool_admission.runCommandContext( retry_ctx.admissionInput(), arena, .{ .id = "cancel-broader-retry", .name = "terminal", .arguments_json = headless_args, }, ); command_ctx.scope = .broader; const retry = try executeToolCallAuthorized(retry_ctx, .{ .call_allocator = arena, .result_allocator = arena, .call = .{ .id = "cancel-broader-retry", .name = "terminal", .arguments_json = headless_args, }, .authority = .{ .run_command = .{ .shell_allowed = .{ .fingerprint = .init(command_ctx), .source = .auto_classifier, } } }, .session_grants = &.{}, .advertised_dynamic_tool_names = &.{}, .max_tool_result_bytes = rt.max_tool_result_bytes, }); try std.testing.expect(headless_trigger.seen); try std.testing.expect(retry.cancelled); var metrics: [1]diagnostics.ToolCallMetric = undefined; try std.testing.expectEqual(@as(usize, 0), diagnostics.snapshotToolCalls(&metrics)); } test "broader sandbox retry consumes the original command timeout budget" { if (builtin.os.tag == .windows or builtin.os.tag == .wasi) return; var rt = TestRuntime{ .command_timeout_ms = 1, .sandbox_backend = .none, }; defer rt.deinit(std.testing.allocator); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); const call = ToolCall{ .id = "broader-timeout", .name = "terminal", .arguments_json = "{\"action\":\"exec\",\"command\":\"sleep 5\"}", }; var command_ctx = try tool_admission.runCommandContext( rt.context().admissionInput(), arena, call, ); command_ctx.scope = .broader; const original_start_ms = io_mod.milliTimestamp() - 50; const result = try executeToolCallAuthorized(rt.context(), .{ .call_allocator = arena, .result_allocator = arena, .call = call, .authority = .{ .run_command = .{ .shell_allowed = .{ .fingerprint = .init(command_ctx), .source = .auto_classifier, } } }, .session_grants = &.{}, .advertised_dynamic_tool_names = &.{}, .max_tool_result_bytes = rt.max_tool_result_bytes, .command_timeout_started_ms = original_start_ms, }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.failure, result.status); try expectContains(result.model_output, "timeout=true\n"); try expectContains(result.model_output, "timeout_ms=1\n"); } test "pre-spawn broader retry cancellation defers tool diagnostics" { if (builtin.os.tag == .windows or builtin.os.tag == .wasi) return; diagnostics.resetForTest(); defer diagnostics.resetForTest(); var cancel = std.atomic.Value(bool).init(true); var rt = TestRuntime{ .cancel_flag = &cancel, .permission_mode = .auto, .sandbox_backend = .none, }; defer rt.deinit(std.testing.allocator); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); const call = ToolCall{ .id = "cancel-before-broader-retry", .name = "terminal", .arguments_json = "{\"action\":\"exec\",\"command\":\"printf never > cancelled-before-retry\"}", }; var command_ctx = try tool_admission.runCommandContext( rt.context().admissionInput(), arena, call, ); command_ctx.scope = .broader; var request: tool_contracts.ToolExecutionRequest = .{ .call_allocator = arena, .result_allocator = arena, .call = call, .authority = .{ .run_command = .{ .shell_allowed = .{ .fingerprint = .init(command_ctx), .source = .auto_classifier, } } }, .session_grants = &.{}, .advertised_dynamic_tool_names = &.{}, .max_tool_result_bytes = rt.max_tool_result_bytes, }; try std.testing.expectError( error.CancelledBeforeExecution, executeToolCallAuthorized(rt.context(), request), ); var metrics: [1]diagnostics.ToolCallMetric = undefined; try std.testing.expectEqual(@as(usize, 0), diagnostics.snapshotToolCalls(&metrics)); command_ctx.scope = .restricted; request.authority = .{ .run_command = .{ .shell_allowed = .{ .fingerprint = .init(command_ctx), .source = .auto_classifier, } } }; try std.testing.expectError( error.Cancelled, executeToolCallAuthorized(rt.context(), request), ); try std.testing.expectEqual(@as(usize, 1), diagnostics.snapshotToolCalls(&metrics)); } test "run_command success exposes structured foreground metadata" { if (builtin.os.tag == .windows or builtin.os.tag == .wasi) return; var rt = TestRuntime{ .permission_mode = .auto, .sandbox_backend = .none, }; defer rt.deinit(std.testing.allocator); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const result = try executeTestRunCommand(rt.context(), arena_state.allocator(), .{ .id = "cmd", .name = "terminal", .arguments_json = "{\"action\":\"exec\",\"command\":\"printf '\\\\150\\\\145\\\\154\\\\154\\\\157'\"}", }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.success, result.status); const structured = result.command_result_json orelse return error.TestExpectedEqual; try expectCommandResultField(structured, "kind", "foreground"); try expectCommandResultField(structured, "command", "printf '\\150\\145\\154\\154\\157'"); try expectCommandResultField(structured, "cwd", "/tmp"); try expectCommandResultInt(structured, "exit_code", 0); try expectCommandResultNull(structured, "signal"); try expectCommandResultBool(structured, "timed_out", false); try expectCommandResultInt(structured, "stdout_bytes", 5); try expectCommandResultInt(structured, "stderr_bytes", 0); try expectCommandResultBool(structured, "truncated", false); try std.testing.expect(std.mem.find(u8, structured, "hello") == null); } fn fakeWorkspaceNonzero( alloc: Allocator, command: []const u8, cwd: []const u8, timeout_ms: u32, ) js_host_workspace.ExecuteError!command_contract.RunCommandResult { if (timeout_ms != js_host_workspace.max_timeout_ms) return error.InvalidWorkspaceResult; return command_contract.formatForegroundCommandResult(alloc, .{ .command = command, .cwd = cwd, .status = .{ .exit_code = 7 }, .stdout_display = "partial", .stderr_display = "failed", .stdout_bytes = 7, .stderr_bytes = 6, .duration_ms = 12, }); } fn fakeWorkspaceTruncated( alloc: Allocator, command: []const u8, cwd: []const u8, _: u32, ) js_host_workspace.ExecuteError!command_contract.RunCommandResult { var result = try command_contract.formatForegroundCommandResult(alloc, .{ .command = command, .cwd = cwd, .status = .{ .exit_code = 0 }, .stdout_display = "preview", .stderr_display = "", .stdout_bytes = 70_000, .stderr_bytes = 0, .duration_ms = 4, }); var metadata = result.command_result.?.foreground; metadata.truncated = true; result.command_result = .{ .foreground = metadata }; return result; } fn fakeWorkspaceCancelled( _: Allocator, command: []const u8, cwd: []const u8, _: u32, ) js_host_workspace.ExecuteError!command_contract.RunCommandResult { return .{ .output = "", .cancelled = true, .command_result = .{ .foreground = .{ .command = command, .cwd = cwd, .duration_ms = 3, } }, }; } fn fakeWorkspaceDeadline( _: Allocator, _: []const u8, _: []const u8, timeout_ms: u32, ) js_host_workspace.ExecuteError!command_contract.RunCommandResult { if (timeout_ms != js_host_workspace.max_timeout_ms) return error.InvalidWorkspaceResult; return error.WorkspaceDeadline; } test "browser run_command uses only the admitted host executor for nonzero and truncated results" { var rt = TestRuntime{ .workspace_root = "/virtual/workspace", .tool_registry = test_browser_workspace_tools.registry, .workspace_executor = .{ .execute_fn = fakeWorkspaceNonzero }, }; defer rt.deinit(std.testing.allocator); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); const failed = try executeTestRunCommand(rt.context(), arena, .{ .id = "browser-failed", .name = "terminal", .arguments_json = "{\"action\":\"exec\",\"command\":\"exit 7\"}", }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.failure, failed.status); try expectToolErrorDetailInt(failed.model_output, "exit_code", 7); const failed_json = failed.command_result_json orelse return error.TestExpectedEqual; try expectCommandResultField(failed_json, "cwd", "/virtual/workspace"); try expectCommandResultNull(failed_json, "signal"); try expectCommandResultNull(failed_json, "output_file"); rt.workspace_executor = .{ .execute_fn = fakeWorkspaceTruncated }; const truncated = try executeTestRunCommand(rt.context(), arena, .{ .id = "browser-truncated", .name = "terminal", .arguments_json = "{\"action\":\"exec\",\"command\":\"generate output\"}", }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.success, truncated.status); const truncated_json = truncated.command_result_json orelse return error.TestExpectedEqual; try expectCommandResultBool(truncated_json, "truncated", true); try expectCommandResultInt(truncated_json, "stdout_bytes", 70_000); try expectCommandResultNull(truncated_json, "output_file"); } test "browser run_command maps host cancellation and deadline without signal or fallback" { var rt = TestRuntime{ .workspace_root = "/virtual/workspace", .tool_registry = test_browser_workspace_tools.registry, .workspace_executor = .{ .execute_fn = fakeWorkspaceCancelled }, }; defer rt.deinit(std.testing.allocator); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); const cancelled = try executeTestRunCommand(rt.context(), arena, .{ .id = "browser-cancelled", .name = "terminal", .arguments_json = "{\"action\":\"exec\",\"command\":\"long command\"}", }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.failure, cancelled.status); try std.testing.expect(cancelled.cancelled); const cancelled_json = cancelled.command_result_json orelse return error.TestExpectedEqual; try expectCommandResultNull(cancelled_json, "signal"); try expectCommandResultBool(cancelled_json, "timed_out", false); rt.workspace_executor = .{ .execute_fn = fakeWorkspaceDeadline }; const timed_out = try executeTestRunCommand(rt.context(), arena, .{ .id = "browser-timeout", .name = "terminal", .arguments_json = "{\"action\":\"exec\",\"command\":\"long command\"}", }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.failure, timed_out.status); try expectContains(timed_out.model_output, "timeout=true\n"); try expectContains(timed_out.model_output, "timeout_ms=30000\n"); const timeout_json = timed_out.command_result_json orelse return error.TestExpectedEqual; try expectCommandResultBool(timeout_json, "timed_out", true); try expectCommandResultNull(timeout_json, "signal"); } test "run_command propagates output callback failure" { if (builtin.os.tag == .windows or builtin.os.tag == .wasi) return; const FailOutput = struct { fn write(_: *anyopaque, _: ?types.ToolLifecycleId, _: command_contract.CommandOutputStream, _: []const u8) error{OutOfMemory}!void { return error.OutOfMemory; } }; var rt = TestRuntime{ .permission_mode = .auto, .sandbox_backend = .none, }; defer rt.deinit(std.testing.allocator); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); var ctx = rt.context(); ctx.output_chunk_ctx = @ptrCast(&rt); ctx.on_output_chunk = FailOutput.write; try std.testing.expectError( error.OutOfMemory, executeTestRunCommand(ctx, arena_state.allocator(), .{ .id = "cmd", .name = "terminal", .arguments_json = "{\"action\":\"exec\",\"command\":\"printf 'handoff\\\\n'\"}", }), ); } test "run_command returns model output and structured metadata" { if (builtin.os.tag == .windows or builtin.os.tag == .wasi) return; var rt = TestRuntime{ .permission_mode = .auto, .sandbox_backend = .none, }; defer rt.deinit(std.testing.allocator); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const result = try executeTestRunCommand(rt.context(), arena_state.allocator(), .{ .id = "cmd", .name = "terminal", .arguments_json = "{\"action\":\"exec\",\"command\":\"printf 'quiet-stdout\\\\n'; printf 'quiet-stderr\\\\n' >&2\"}", }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.success, result.status); try expectContains(result.model_output, "exit_code=0\n"); try expectContains(result.model_output, "\nquiet-stdout\n\n"); try expectContains(result.model_output, "\nquiet-stderr\n\n"); const structured = result.command_result_json orelse return error.TestExpectedEqual; try expectCommandResultField(structured, "kind", "foreground"); try expectCommandResultInt(structured, "exit_code", 0); try expectCommandResultInt(structured, "stdout_bytes", 13); try expectCommandResultInt(structured, "stderr_bytes", 13); try expectCommandResultBool(structured, "timed_out", false); try expectCommandResultBool(structured, "truncated", false); } test "run_command nonzero exit returns structured masked failure" { if (builtin.os.tag == .windows or builtin.os.tag == .wasi) return; var rt = TestRuntime{ .permission_mode = .auto, .sandbox_backend = .none, }; defer rt.deinit(std.testing.allocator); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const result = try executeTestRunCommand(rt.context(), arena_state.allocator(), .{ .id = "cmd", .name = "terminal", .arguments_json = "{\"action\":\"exec\",\"command\":\"printf 'bad AKIA0123456789ABCDEF\\\\n' >&2; exit 7\"}", }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.failure, result.status); try expectToolErrorField(result.model_output, "type", "tool_execution_failed"); try expectToolErrorField(result.model_output, "tool_name", "terminal"); try expectToolErrorDetailString(result.model_output, "command", "printf 'bad [redacted]\\n' >&2; exit 7"); try expectToolErrorDetailString(result.model_output, "cwd", "/tmp"); try expectToolErrorDetailInt(result.model_output, "exit_code", 7); try expectToolErrorDetailString(result.model_output, "stderr", "bad [redacted]"); try expectContains(result.model_output, "Inspect stderr"); try expectNotContains(result.model_output, "AKIA0123456789ABCDEF"); const structured = result.command_result_json orelse return error.TestExpectedEqual; try expectCommandResultField(structured, "kind", "foreground"); try expectCommandResultInt(structured, "exit_code", 7); try expectCommandResultInt(structured, "stdout_bytes", 0); try expectCommandResultInt(structured, "stderr_bytes", 25); try std.testing.expect(std.mem.find(u8, structured, "\"stdout\":") == null); try std.testing.expect(std.mem.find(u8, structured, "\"stderr\":") == null); try std.testing.expectEqual( types.CommandProcessPresentation{ .exit_code = 7 }, result.tool_result_memory.?.command_process_presentation.?, ); } test "run_command huge output exposes truncation and artifact paths without stdout body" { if (builtin.os.tag == .windows or builtin.os.tag == .wasi) return; const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); try tmp.dir.createDirPath(io_mod.getIo(), "session/logs/commands"); const workspace = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "workspace"); defer alloc.free(workspace); const artifact_dir = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "session/logs/commands"); defer alloc.free(artifact_dir); var rt = TestRuntime{ .workspace_root = workspace, .permission_mode = .auto, .sandbox_backend = .none, }; rt.max_command_output_bytes = 16; rt.command_artifact_dir = artifact_dir; defer rt.deinit(alloc); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const result = try executeTestRunCommand(rt.context(), arena_state.allocator(), .{ .id = "cmd", .name = "terminal", .arguments_json = "{\"action\":\"exec\",\"command\":\"printf %026d 0\"}", }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.success, result.status); const structured = result.command_result_json orelse return error.TestExpectedEqual; try expectCommandResultBool(structured, "truncated", true); try expectCommandResultInt(structured, "stdout_bytes", 26); try expectCommandResultInt(structured, "stderr_bytes", 0); try expectCommandResultStringPrefix(structured, "output_file", artifact_dir); try expectCommandResultStringPrefix(structured, "stdout_file", artifact_dir); try expectCommandResultStringPrefix(structured, "stderr_file", artifact_dir); try std.testing.expect(std.mem.find(u8, structured, "00000000000000000000000000") == null); } test "run_command reactive sandbox widening preserves the restricted attempt" { if (builtin.os.tag != .macos) return; var rt = TestRuntime{ .permission_mode = .auto, .sandbox_backend = .macos, .interactive = false, }; defer rt.deinit(std.testing.allocator); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const result = try executeTestRunCommand(rt.context(), arena_state.allocator(), .{ .id = "cmd", .name = "terminal", .arguments_json = "{\"action\":\"exec\",\"command\":\"printf 'Operation not permitted\\\\n' >&2; exit 1\",\"profile\":\"clean\"}", }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.failure, result.status); try expectContains(result.model_output, "Operation not permitted"); const widening = result.sandbox_scope_required orelse return error.TestExpectedSandboxScopeRequired; try std.testing.expectEqual(tool_contracts.SandboxWideningPhase.reactive, widening.phase); try std.testing.expectEqualStrings( "printf 'Operation not permitted\\n' >&2; exit 1", widening.restricted_fingerprint.command, ); try std.testing.expectEqualStrings("/tmp", widening.restricted_fingerprint.resolved_cwd); try std.testing.expectEqualStrings( result.model_output, widening.restricted_model_output orelse return error.TestExpectedRestrictedResult, ); const structured = result.command_result_json orelse return error.TestExpectedEqual; try std.testing.expectEqualStrings( structured, widening.restricted_command_result_json orelse return error.TestExpectedRestrictedResult, ); try expectCommandResultField(structured, "kind", "foreground"); try expectCommandResultField(structured, "command", "printf 'Operation not permitted\\n' >&2; exit 1"); try expectCommandResultInt(structured, "exit_code", 1); try expectCommandResultInt(structured, "stdout_bytes", 0); try expectCommandResultInt(structured, "stderr_bytes", 24); try std.testing.expect(rt.worker.pending_permission_request_shared == null); } test "run_command preflight sandbox widening reports that no attempt ran" { if (builtin.os.tag != .macos) return; diagnostics.resetForTest(); defer diagnostics.resetForTest(); var rt = TestRuntime{ .permission_mode = .auto, .sandbox_backend = .macos, .interactive = false, }; defer rt.deinit(std.testing.allocator); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const result = try executeTestRunCommand(rt.context(), arena_state.allocator(), .{ .id = "cmd", .name = "terminal", .arguments_json = "{\"action\":\"exec\",\"command\":\"npm test\"}", }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.failure, result.status); try std.testing.expectEqualStrings("", result.model_output); try std.testing.expect(result.command_result_json == null); const widening = result.sandbox_scope_required orelse return error.TestExpectedSandboxScopeRequired; try std.testing.expectEqual(tool_contracts.SandboxWideningPhase.preflight, widening.phase); try std.testing.expectEqualStrings("npm test", widening.restricted_fingerprint.command); try std.testing.expectEqualStrings("/tmp", widening.restricted_fingerprint.resolved_cwd); try std.testing.expect(widening.restricted_model_output == null); try std.testing.expect(widening.restricted_command_result_json == null); try std.testing.expect(rt.worker.pending_permission_request_shared == null); var metrics: [1]diagnostics.ToolCallMetric = undefined; try std.testing.expectEqual(@as(usize, 0), diagnostics.snapshotToolCalls(&metrics)); } test "terminal exec rejects legacy background input without creating state" { var rt = TestRuntime{}; defer rt.deinit(std.testing.allocator); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const result = try executeToolCall(rt.context(), arena_state.allocator(), .{ .id = "cmd", .name = "terminal", .arguments_json = "{\"action\":\"exec\",\"command\":\"printf row07-headless-bg\",\"background\":true}", }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.failure, result.status); try expectContains(result.model_output, "\"code\":\"invalid_action_fields\""); try expectContains(result.model_output, "\"invalid_fields\":[\"background\"]"); var tasks = try rt.background.snapshotTasks(std.testing.allocator); defer tasks.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 0), tasks.items.len); } const PermissionThreadState = struct { decision: ?ToolPermissionDecision = null, err: ?anyerror = null, }; const FilePermissionThreadState = struct { decision: ?ToolPermissionDecision = null, file_authority: bool = false, prepared: bool = false, additions: usize = 0, expected_grant_target: []const u8 = "", expected_workspace_offer: bool = false, frozen_offer_matches: bool = false, err: ?anyerror = null, }; fn runFilePermissionOutcome( state: *FilePermissionThreadState, req_ctx: Context, call: ToolCall, mode: PermissionMode, ) void { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const outcome = tool_admission.requestPermissionOutcome( req_ctx.admissionInput(), arena_state.allocator(), call, mode, &.{}, ) catch |err| { state.err = err; return; }; if (outcome.tool_failure != null) { state.err = error.UnexpectedToolFailure; return; } state.decision = outcome.decision; const authority = outcome.execution_authority orelse return; switch (authority) { .file_mutation => |file_authority| { state.file_authority = true; state.prepared = file_authority.prepared != null; if (file_authority.prepared) |prepared| { state.additions = prepared.preview.additions; } if (file_authority.grant_offer) |offer| { state.frozen_offer_matches = fileGrantOfferMatchesExpectation( offer, state.expected_grant_target, state.expected_workspace_offer, ); } }, else => state.err = error.UnexpectedExecutionAuthority, } } fn fileGrantOfferMatchesExpectation( offer: file_mutation_contract.FileGrantOffer, expected_target: []const u8, expected_workspace: bool, ) bool { const workspace_permissions = [_][]const u8{ "edit", "create_folder", "open_file", "rename_file", "copy_file", "read", "list", "glob", "grep", }; for (offer.grants) |grant| { if (!std.mem.eql(u8, grant.target_path, expected_target)) return false; } if (expected_workspace) { if (offer.scope != .workspace_files) return false; if (offer.grants.len != workspace_permissions.len) return false; for (workspace_permissions) |permission| { var found = false; for (offer.grants) |grant| { found = found or std.mem.eql( u8, grant.tool_name, permission, ); } if (!found) return false; } return true; } return switch (offer.scope) { .workspace_files => false, .external_tree => |root| offer.grants.len == 1 and std.mem.eql(u8, offer.grants[0].tool_name, "edit") and std.mem.eql( u8, root, expected_target[0 .. expected_target.len - "/**".len], ), }; } fn testFileMutationArgs( alloc: Allocator, kind: file_mutation_contract.Kind, path: []const u8, ) ![]u8 { var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try out.writer.writeAll("{\"path\":"); try std.json.Stringify.value(path, .{}, &out.writer); switch (kind) { .write => { try out.writer.writeAll(",\"content\":\"after\\n\"}"); }, .edit => { try out.writer.writeAll( ",\"old_string\":\"before\\n\",\"new_string\":\"after\\n\"}", ); }, } return try out.toOwnedSlice(); } fn testMaxAdmittedLexicalPath( alloc: Allocator, basename: []const u8, ) ![]u8 { if (basename.len > std.Io.Dir.max_path_bytes) { return error.NameTooLong; } const path = try alloc.alloc(u8, std.Io.Dir.max_path_bytes); var cursor: usize = 0; var remaining = path.len - basename.len; if (remaining % 2 != 0) { if (remaining < "x/../".len) return error.NameTooLong; @memcpy(path[cursor..][0.."x/../".len], "x/../"); cursor += "x/../".len; remaining -= "x/../".len; } while (remaining > 0) : (remaining -= 2) { @memcpy(path[cursor..][0..2], "./"); cursor += 2; } @memcpy(path[cursor..][0..basename.len], basename); return path; } fn runPermissionRequest(state: *PermissionThreadState, req_ctx: Context, call: ToolCall, mode: PermissionMode) void { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const outcome = tool_admission.requestPermissionOutcome(req_ctx.admissionInput(), arena_state.allocator(), call, mode, &.{}) catch |err| { state.err = err; return; }; state.decision = outcome.decision; } fn waitForPermissionPrompt(worker: *WorkerRuntime, expected: []const u8) !u64 { var attempts: usize = 0; while (attempts < 100) : (attempts += 1) { var snapshot = try worker.snapshotState(std.testing.allocator); defer snapshot.deinit(std.testing.allocator); if (snapshot.pending_permission_request) |request| { try expectContains(request.label, expected); return request.id; } io_mod.sleep(1 * std.time.ns_per_ms); } return error.TestExpectedEqual; } test "configured ask rule prompts the worker" { var rules = [_]types.PermissionRule{ .{ .permission = @constCast("list"), .pattern = @constCast("."), .action = .ask }, }; var rt = TestRuntime{ .workspace_root = "/tmp/workspace", .permission_rules = .{ .rules = &rules }, .permission_mode = .auto, }; defer rt.deinit(std.testing.allocator); var state = PermissionThreadState{}; const thread = try std.Thread.spawn(.{}, runPermissionRequest, .{ &state, rt.context(), ToolCall{ .id = "1", .name = "list_files", .arguments_json = "{}" }, PermissionMode.auto }); var thread_joined = false; defer if (!thread_joined) { rt.worker.requestShutdown(); thread.join(); }; rt.worker.worker_processing = true; const request_id = try waitForPermissionPrompt(&rt.worker, "list_files"); try std.testing.expectEqual( worker_runtime.PermissionSubmissionResult.accepted, rt.worker.submitPermissionResponse( request_id, permission_request.OwnedPermissionResponse.init(std.testing.allocator, .once, null), ), ); thread.join(); thread_joined = true; try std.testing.expect(state.err == null); try std.testing.expectEqual(ToolPermissionDecision.once, state.decision.?); } const McpFixture = struct { const CountingContext = struct { calls: usize = 0, }; const PermissionContext = struct { search_rule_count: usize = 0, schema_rule_count: usize = 0, }; const AuthorityContext = struct { calls: usize = 0, admission_generation: u64 = 0, action_generation: u64 = 0, }; fn hasTrue(_: *anyopaque, _: []const u8, _: tool_mcp_runtime.Access) bool { return true; } fn hasFalse(_: *anyopaque, _: []const u8) bool { return false; } fn validateArguments(_: *anyopaque, arena: Allocator, _: []const u8, arguments_json: []const u8, _: tool_mcp_runtime.Access) anyerror!tool_mcp_runtime.ValidationResult { if (std.mem.eql(u8, arguments_json, "{\"path\":7}")) { return .{ .invalid = try arena.dupe(u8, "path must be a string") }; } return .valid; } fn callOk(_: *anyopaque, arena: Allocator, _: []const u8, _: []const u8, _: usize, _: tool_mcp_runtime.CallOptions) anyerror!?tool_mcp_runtime.CallResult { return .{ .model_output = try arena.dupe(u8, "mcp ok") }; } fn callCounting(raw_ctx: *anyopaque, arena: Allocator, _: []const u8, _: []const u8, _: usize, _: tool_mcp_runtime.CallOptions) anyerror!?tool_mcp_runtime.CallResult { const ctx: *CountingContext = @ptrCast(@alignCast(raw_ctx)); ctx.calls += 1; return .{ .model_output = try arena.dupe(u8, "mcp ok") }; } fn callRecordingAuthority(raw_ctx: *anyopaque, arena: Allocator, _: []const u8, _: []const u8, _: usize, options: tool_mcp_runtime.CallOptions) anyerror!?tool_mcp_runtime.CallResult { const ctx: *AuthorityContext = @ptrCast(@alignCast(raw_ctx)); const scope = switch (options.access) { .scoped => |value| value, else => return error.McpScopedAccessExpected, }; ctx.calls += 1; ctx.admission_generation = scope.admission_authority_generation; ctx.action_generation = scope.action_authority_generation; return .{ .model_output = try arena.dupe(u8, "mcp ok") }; } fn callNull(_: *anyopaque, _: Allocator, _: []const u8, _: []const u8, _: usize, _: tool_mcp_runtime.CallOptions) anyerror!?tool_mcp_runtime.CallResult { return null; } fn callFailure(_: *anyopaque, _: Allocator, _: []const u8, _: []const u8, _: usize, _: tool_mcp_runtime.CallOptions) anyerror!?tool_mcp_runtime.CallResult { return error.McpFixtureFailure; } fn search(_: *anyopaque, arena: Allocator, query: []const u8, _: usize, _: types.PermissionRuleSet, _: context_limits.Values) anyerror!tool_mcp_runtime.SearchResult { return .{ .model_output = try std.fmt.allocPrint(arena, "{{\"query\":\"{s}\",\"tools\":[{{\"name\":\"mcp_fs_read\",\"server\":\"fs\",\"description\":\"Read\",\"input_schema\":{{\"type\":\"object\"}},\"tags\":[\"fs\",\"read\"]}}],\"count\":1}}", .{query}) }; } fn schema(_: *anyopaque, arena: Allocator, name: []const u8, _: types.PermissionRuleSet, _: context_limits.Values, _: tool_mcp_runtime.Access) anyerror!?tool_mcp_runtime.ToolSchemaResult { if (!std.mem.eql(u8, name, "mcp_fs_read")) return null; return .{ .selected = .{ .model_output = try arena.dupe(u8, "{\"type\":\"function\",\"name\":\"mcp_fs_read\",\"description\":\"Read \",\"inputSchema\":{\"type\":\"object\",\"properties\":{\"context_limit_rejection\":{\"type\":\"string\"}}}}") } }; } fn searchRecordingRules(raw_ctx: *anyopaque, arena: Allocator, query: []const u8, _: usize, permission_rules: types.PermissionRuleSet, limits: context_limits.Values, _: tool_mcp_runtime.Access) anyerror!tool_mcp_runtime.SearchResult { const ctx: *PermissionContext = @ptrCast(@alignCast(raw_ctx)); ctx.search_rule_count = permission_rules.rules.len; return search(raw_ctx, arena, query, 0, permission_rules, limits); } fn schemaRecordingRules(raw_ctx: *anyopaque, arena: Allocator, name: []const u8, permission_rules: types.PermissionRuleSet, limits: context_limits.Values, access: tool_mcp_runtime.Access) anyerror!?tool_mcp_runtime.ToolSchemaResult { const ctx: *PermissionContext = @ptrCast(@alignCast(raw_ctx)); ctx.schema_rule_count = permission_rules.rules.len; return schema(raw_ctx, arena, name, permission_rules, limits, access); } }; test "MCP control tools forward permission rules through registered execution" { var permission_context = McpFixture.PermissionContext{}; var rules = [_]types.PermissionRule{ .{ .permission = @constCast("mcp_other"), .pattern = @constCast("*"), .action = .deny }, }; var rt = TestRuntime{ .mcp_ctx = @ptrCast(&permission_context), .mcp_search_tools = McpFixture.searchRecordingRules, .mcp_tool_schema = McpFixture.schemaRecordingRules, .permission_rules = .{ .rules = &rules }, }; defer rt.deinit(std.testing.allocator); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); _ = try executeToolCall(rt.context(), arena, .{ .id = "search", .name = "mcp_search_tools", .arguments_json = "{\"query\":\"read\"}", }); _ = try executeToolCall(rt.context(), arena, .{ .id = "select", .name = "mcp_select_tool", .arguments_json = "{\"name\":\"mcp_fs_read\"}", }); try std.testing.expectEqual(@as(usize, 1), permission_context.search_rule_count); try std.testing.expectEqual(@as(usize, 1), permission_context.schema_rule_count); rt.permission_mode = .yolo; permission_context = .{}; _ = try executeToolCall(rt.context(), arena, .{ .id = "yolo-search", .name = "mcp_search_tools", .arguments_json = "{\"query\":\"read\"}", }); _ = try executeToolCall(rt.context(), arena, .{ .id = "yolo-select", .name = "mcp_select_tool", .arguments_json = "{\"name\":\"mcp_fs_read\"}", }); try std.testing.expectEqual(@as(usize, 0), permission_context.search_rule_count); try std.testing.expectEqual(@as(usize, 0), permission_context.schema_rule_count); } test "MCP dynamic tool cannot execute before it is advertised" { var counting = McpFixture.CountingContext{}; var rt = TestRuntime{ .mcp_ctx = @ptrCast(&counting), .mcp_has_tool = McpFixture.hasTrue, .mcp_call_tool = McpFixture.callCounting, }; defer rt.deinit(std.testing.allocator); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); const result = try executeToolCall(rt.context(), arena, .{ .id = "dynamic", .name = "mcp_fs_read", .arguments_json = "{}", }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.failure, result.status); try expectContains(result.model_output, "not selected"); try std.testing.expectEqual(@as(usize, 0), counting.calls); } test "MCP select does not authorize same-response dynamic execution" { var counting = McpFixture.CountingContext{}; var rt = TestRuntime{ .mcp_ctx = @ptrCast(&counting), .mcp_has_tool = McpFixture.hasTrue, .mcp_call_tool = McpFixture.callCounting, .mcp_tool_schema = McpFixture.schema, }; defer rt.deinit(std.testing.allocator); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); const selected = try executeToolCall(rt.context(), arena, .{ .id = "select", .name = "mcp_select_tool", .arguments_json = "{\"name\":\"mcp_fs_read\"}", }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.success, selected.status); try std.testing.expectEqualStrings("mcp_fs_read", selected.selected_dynamic_tool_name.?); try expectContains(selected.selected_dynamic_tool_schema_json.?, "context_limit_rejection"); try expectContains(selected.selected_dynamic_tool_schema_json.?, ""); const result = try executeToolCall(rt.context(), arena, .{ .id = "dynamic", .name = "mcp_fs_read", .arguments_json = "{}", }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.failure, result.status); try expectContains(result.model_output, "not selected"); try std.testing.expectEqual(@as(usize, 0), counting.calls); } test "MCP unadvertised dynamic names stay unsupported without a runtime call" { var marker: u8 = 0; var null_rt = TestRuntime{ .mcp_ctx = @ptrCast(&marker), .mcp_has_tool = McpFixture.hasTrue, .mcp_call_tool = McpFixture.callNull, }; defer null_rt.deinit(std.testing.allocator); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); const result = try executeToolCall(null_rt.context(), arena, .{ .id = "dynamic", .name = "mcp_unknown", .arguments_json = "{}", }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.failure, result.status); try expectContains(result.model_output, "not selected"); var no_rt = TestRuntime{}; defer no_rt.deinit(std.testing.allocator); try expectToolOutput(no_rt.context(), "mcp_unknown", "{}", "Unsupported tool: mcp_unknown"); } test "MCP dynamic tool executes after its schema was advertised" { var counting = McpFixture.CountingContext{}; const advertised = [_][]const u8{"mcp_fs_read"}; var rt = TestRuntime{ .mcp_ctx = @ptrCast(&counting), .mcp_has_tool = McpFixture.hasTrue, .mcp_call_tool = McpFixture.callCounting, .advertised_dynamic_tool_names = &advertised, }; defer rt.deinit(std.testing.allocator); try expectToolOutput(rt.context(), "mcp_fs_read", "{}", "mcp ok"); try std.testing.expectEqual(@as(usize, 1), counting.calls); } test "MCP execution binds the last live action generation before transport" { const alloc = std.testing.allocator; var captured = mcp_access_policy.View{ .runtime_generation = 1, .owner_id = try alloc.dupe(u8, "child"), .parent_id = try alloc.dupe(u8, "parent"), .features_visible = false, .servers = try alloc.alloc(mcp_access_policy.ServerIdentity, 0), .tools = try alloc.alloc(mcp_access_policy.ToolIdentity, 0), }; defer captured.deinit(alloc); const UnusedLiveProvider = struct { fn resolve(_: *anyopaque, _: Allocator) !tool_mcp_runtime.ResolvedLiveView { return error.TestUnexpectedResult; } }; var marker: u8 = 0; var authority = McpFixture.AuthorityContext{}; var rt = TestRuntime{ .mcp_ctx = @ptrCast(&authority), .mcp_call_tool = McpFixture.callRecordingAuthority, }; defer rt.deinit(alloc); var tool_ctx = rt.context(); tool_ctx.mcp_access = .{ .scoped = .{ .captured = &captured, .admission_authority_generation = 17, .live = .{ .context = @ptrCast(&marker), .resolve_fn = UnusedLiveProvider.resolve, }, } }; const integrations = [_][]const u8{"mcp_fs_read"}; const result = try executeToolCallAuthorized(tool_ctx, .{ .call_allocator = alloc, .result_allocator = alloc, .call = .{ .id = "dynamic-authority", .name = "mcp_fs_read", .arguments_json = "{}", }, .authority = .ordinary, .session_grants = &.{}, .live_authority = .{ .generation = 41, .root_id = "root", .tools = &.{}, .sandbox_backend = .none, .integrations = &integrations, .rules = .{}, .grants = &.{}, .permission_mode = .auto, }, .advertised_dynamic_tool_names = &integrations, .max_tool_result_bytes = rt.max_tool_result_bytes, }); defer alloc.free(result.model_output); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.success, result.status); try std.testing.expectEqualStrings("mcp ok", result.model_output); try std.testing.expectEqual(@as(usize, 1), authority.calls); try std.testing.expectEqual(@as(u64, 17), authority.admission_generation); try std.testing.expectEqual(@as(u64, 41), authority.action_generation); const original_scope = switch (tool_ctx.mcp_access) { .scoped => |scope| scope, else => return error.McpScopedAccessExpected, }; try std.testing.expectEqual(@as(u64, 17), original_scope.admission_authority_generation); try std.testing.expectEqual(@as(u64, 0), original_scope.action_authority_generation); } test "MCP dynamic tool preserves provider errors without leaking dispatch output" { var marker: u8 = 0; const advertised = [_][]const u8{"mcp_fs_read"}; var rt = TestRuntime{ .mcp_ctx = @ptrCast(&marker), .mcp_has_tool = McpFixture.hasTrue, .mcp_call_tool = McpFixture.callFailure, .advertised_dynamic_tool_names = &advertised, }; defer rt.deinit(std.testing.allocator); try std.testing.expectError( error.McpFixtureFailure, executeToolCall(rt.context(), std.testing.allocator, .{ .id = "dynamic-error", .name = "mcp_fs_read", .arguments_json = "{}", }), ); } test "MCP availability controls permission targets and rules" { var marker: u8 = 0; const advertised = [_][]const u8{"mcp_fs_write"}; var rules = [_]types.PermissionRule{ .{ .permission = @constCast("mcp_fs_write"), .pattern = @constCast("mcp_fs_write"), .action = .deny }, }; var rt = TestRuntime{ .mcp_ctx = @ptrCast(&marker), .mcp_has_tool = McpFixture.hasTrue, .mcp_call_tool = McpFixture.callNull, .permission_rules = .{ .rules = &rules }, .advertised_dynamic_tool_names = &advertised, }; defer rt.deinit(std.testing.allocator); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); const ctx = rt.context(); const target = try tool_admission.permissionTargetForCall(ctx.admissionInput(), arena, .{ .id = "1", .name = "mcp_fs_write", .arguments_json = "{}" }); try std.testing.expectEqualStrings("mcp_fs_write", target); try std.testing.expectEqual(ToolPermissionDecision.policy_denied, (try tool_admission.requestPermissionOutcome(ctx.admissionInput(), arena, .{ .id = "1", .name = "mcp_fs_write", .arguments_json = "{}" }, .auto, &.{})).decision); } test "MCP unadvertised dynamic names do not receive permission targets" { var marker: u8 = 0; var rules = [_]types.PermissionRule{ .{ .permission = @constCast("mcp_fs_write"), .pattern = @constCast("mcp_fs_write"), .action = .deny }, }; var rt = TestRuntime{ .mcp_ctx = @ptrCast(&marker), .mcp_has_tool = McpFixture.hasTrue, .permission_rules = .{ .rules = &rules }, }; defer rt.deinit(std.testing.allocator); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); const ctx = rt.context(); try std.testing.expectEqual(ToolPermissionDecision.once, (try tool_admission.requestPermissionOutcome(ctx.admissionInput(), arena, .{ .id = "1", .name = "mcp_fs_write", .arguments_json = "{}" }, .auto, &.{})).decision); } test "terminal exec cannot reuse or replace a persisted legacy background task" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "background"); { var file = try tmp.dir.createFile(io_mod.getIo(), "server.log", .{ .truncate = true }); defer file.close(io_mod.getIo()); try file.writeStreamingAll(io_mod.getIo(), "ready on http://localhost:49123\n"); } const background_dir = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "background"); defer alloc.free(background_dir); const log_path = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "server.log"); defer alloc.free(log_path); const Stub = struct { fn match( pid_text: []const u8, _: process_supervisor.ProcessInstanceToken, ) process_supervisor.TokenMatch { return if (std.mem.eql(u8, pid_text, "12345")) .matched else .missing; } }; process_supervisor.process_token_match_for_test = Stub.match; defer process_supervisor.process_token_match_for_test = null; const token = try process_supervisor.ProcessInstanceToken.parse( "linux:00112233445566778899aabbccddeeff:12345", ); const pid_text = "12345"; var rt = TestRuntime{ .workspace_root = "/tmp/fx", .interactive = false, .background = BackgroundRuntime.init( background_process_provider.process_supervisor_test_provider, ), }; defer rt.deinit(alloc); try rt.background.enablePersistence(alloc, background_dir); const task_id = try rt.background.registerBackgroundDurably(alloc, .{ .pid = pid_text, .process_token = token, .command = "npm run dev", .cwd = "/tmp/fx", .log_path = log_path, .expect_url = true, .url = "http://localhost:49123", }); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const result = try executeToolCall(rt.context(), arena_state.allocator(), .{ .id = "1", .name = "terminal", .arguments_json = "{\"action\":\"exec\",\"command\":\"npm run dev\",\"background\":true}", }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.failure, result.status); try expectContains(result.model_output, "\"code\":\"invalid_action_fields\""); try expectContains(result.model_output, "\"invalid_fields\":[\"background\"]"); var tasks = try rt.background.snapshotTasks(alloc); defer tasks.deinit(alloc); try std.testing.expectEqual(@as(usize, 1), tasks.items.len); try std.testing.expectEqual(task_id, tasks.items[0].id); } test "external absolute non-write tracked mutations capture resolved paths" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); try tmp.dir.createDirPath(io_mod.getIo(), "external"); const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "workspace"); defer alloc.free(root); const external = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "external"); defer alloc.free(external); var tracker: change_tracker.ChangeTracker = .{}; defer tracker.deinit(std.heap.c_allocator); var allow_rules = [_]types.PermissionRule{ .{ .permission = @constCast("*"), .pattern = @constCast("*"), .action = .allow }, }; var rt = TestRuntime{ .workspace_root = root, .tracker = &tracker }; rt.permission_rules = .{ .rules = &allow_rules }; defer rt.deinit(alloc); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); try writeTestFile(tmp.dir, "external/delete.txt", "delete\n"); const delete_path = try std.fs.path.join(arena, &.{ external, "delete.txt" }); const delete_args = try std.fmt.allocPrint(arena, "{{\"path\":\"{s}\"}}", .{delete_path}); _ = try executeToolCall(rt.context(), arena, .{ .id = "external-delete", .name = "delete_file", .arguments_json = delete_args, }); try std.testing.expectEqual(@as(usize, 1), tracker.stack.items.len); try std.testing.expectEqual(change_tracker.OperationKind.delete, tracker.stack.items[0].kind); try std.testing.expectEqualStrings(delete_path, tracker.stack.items[0].path); try std.testing.expectEqualStrings("delete\n", tracker.stack.items[0].previous_content.?); try writeTestFile(tmp.dir, "external/rename.txt", "rename\n"); const rename_source = try std.fs.path.join(arena, &.{ external, "rename.txt" }); const rename_dest = try std.fs.path.join(arena, &.{ external, "renamed.txt" }); const rename_args = try std.fmt.allocPrint(arena, "{{\"old_path\":\"{s}\",\"new_path\":\"{s}\"}}", .{ rename_source, rename_dest }); _ = try executeToolCall(rt.context(), arena, .{ .id = "external-rename", .name = "rename_file", .arguments_json = rename_args, }); try std.testing.expectEqual(@as(usize, 2), tracker.stack.items.len); try std.testing.expectEqual(change_tracker.OperationKind.rename, tracker.stack.items[1].kind); try std.testing.expectEqualStrings(rename_source, tracker.stack.items[1].path); try std.testing.expectEqualStrings(rename_dest, tracker.stack.items[1].new_path.?); try writeTestFile(tmp.dir, "external/copy-source.txt", "copy\n"); const copy_source = try std.fs.path.join(arena, &.{ external, "copy-source.txt" }); const copy_dest = try std.fs.path.join(arena, &.{ external, "copy-dest.txt" }); const copy_args = try std.fmt.allocPrint(arena, "{{\"source\":\"{s}\",\"destination\":\"{s}\"}}", .{ copy_source, copy_dest }); _ = try executeToolCall(rt.context(), arena, .{ .id = "external-copy", .name = "copy_file", .arguments_json = copy_args, }); try std.testing.expectEqual(@as(usize, 3), tracker.stack.items.len); try std.testing.expectEqual(change_tracker.OperationKind.write, tracker.stack.items[2].kind); try std.testing.expectEqualStrings(copy_dest, tracker.stack.items[2].path); } test "invalid tracked mutations do not push tracker operations" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "workspace"); defer alloc.free(root); var tracker: change_tracker.ChangeTracker = .{}; defer tracker.deinit(std.heap.c_allocator); var rt = TestRuntime{ .workspace_root = root, .tracker = &tracker }; defer rt.deinit(alloc); const calls = [_]struct { name: []const u8, args: []const u8 }{ .{ .name = "delete_file", .args = "{}" }, .{ .name = "rename_file", .args = "{}" }, .{ .name = "copy_file", .args = "{}" }, }; for (calls) |call| { var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); try std.testing.expectError(error.InvalidToolArguments, executeToolCall(rt.context(), arena_state.allocator(), .{ .id = "1", .name = call.name, .arguments_json = call.args, })); try std.testing.expectEqual(@as(usize, 0), tracker.stack.items.len); } } test "failed tracked delete rename and copy skip tracker publication" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "workspace/non-empty"); try writeTestFile(tmp.dir, "workspace/non-empty/child.txt", "keep\n"); try writeTestFile(tmp.dir, "workspace/rename-source.txt", "rename\n"); try tmp.dir.createDirPath(io_mod.getIo(), "workspace/rename-destination"); try writeTestFile(tmp.dir, "workspace/rename-destination/child.txt", "keep\n"); try writeTestFile(tmp.dir, "workspace/copy-source.txt", "copy\n"); try tmp.dir.createDirPath(io_mod.getIo(), "workspace/copy-destination"); const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "workspace"); defer alloc.free(root); var tracker: change_tracker.ChangeTracker = .{}; defer tracker.deinit(std.heap.c_allocator); var rt = TestRuntime{ .workspace_root = root, .tracker = &tracker }; defer rt.deinit(alloc); const calls = [_]struct { name: []const u8, args: []const u8, failure_prefix: []const u8, }{ .{ .name = "delete_file", .args = "{\"path\":\"non-empty\"}", .failure_prefix = "delete_file failed:", }, .{ .name = "rename_file", .args = "{\"old_path\":\"rename-source.txt\",\"new_path\":\"rename-destination\"}", .failure_prefix = "rename_file failed:", }, .{ .name = "copy_file", .args = "{\"source\":\"copy-source.txt\",\"destination\":\"copy-destination\"}", .failure_prefix = "copy_file failed:", }, }; for (calls) |call| { var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const result = try executeToolCall(rt.context(), arena_state.allocator(), .{ .id = "tracked-failure", .name = call.name, .arguments_json = call.args, }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.failure, result.status); try std.testing.expect(std.mem.startsWith(u8, result.model_output, call.failure_prefix)); try std.testing.expectEqual(@as(usize, 0), tracker.stack.items.len); } } test "memory tool uses isolated HOME and preserves outputs" { const alloc = std.testing.allocator; var no_home_rt = TestRuntime{}; defer no_home_rt.deinit(alloc); try setTestHome(null); try expectToolOutput(no_home_rt.context(), "memory", "{\"action\":\"list\"}", "memory unavailable: HOME not set"); var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "home"); const home = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "home"); defer alloc.free(home); try setTestHome(home); var rt = TestRuntime{}; defer rt.deinit(alloc); const ctx = rt.context(); try expectToolOutput(ctx, "memory", "{\"action\":\"list\"}", "No saved memories"); var rejected_arena_state = std.heap.ArenaAllocator.init(alloc); defer rejected_arena_state.deinit(); const rejected = try executeToolCall(ctx, rejected_arena_state.allocator(), .{ .id = "invalid-memory-action", .name = "memory", .arguments_json = "{\"action\":\"replace\",\"fact\":\"new value\"}", }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.failure, rejected.status); try std.testing.expectEqualStrings( "memory field \"action\" must be one of: save, list, clear", rejected.model_output, ); try expectToolOutput(ctx, "memory", "{\"action\":\"save\",\"fact\":\"likes Zig\"}", "remembered"); try expectToolOutput(ctx, "memory", "{\"action\":\"save\",\"fact\":\"likes Zig\"}", "remembered"); try expectToolOutput(ctx, "memory", "{\"action\":\"list\"}", "- likes Zig\n"); const memories_path = try std.fs.path.join(alloc, &.{ home, ".fx", "memories.json" }); defer alloc.free(memories_path); var file = try std.Io.Dir.openFileAbsolute(io_mod.getIo(), memories_path, .{}); const content = blk: { defer file.close(io_mod.getIo()); break :blk try io_mod.readFileToEnd(alloc, &file, 4096); }; defer alloc.free(content); var parsed = try std.json.parseFromSlice(std.json.Value, alloc, content, .{}); defer parsed.deinit(); try std.testing.expectEqual(@as(usize, 1), parsed.value.array.items.len); try std.testing.expectEqualStrings("likes Zig", parsed.value.array.items[0].string); try expectToolOutput(ctx, "memory", "{\"action\":\"clear\"}", "memories cleared"); try expectToolOutput(ctx, "memory", "{\"action\":\"clear\"}", "memories cleared"); try expectToolOutput(ctx, "memory", "{\"action\":\"list\"}", "No saved memories"); try std.Io.Dir.createDirAbsolute(io_mod.getIo(), memories_path, .default_dir); const survivor_path = try std.fs.path.join(alloc, &.{ memories_path, "must-survive.txt" }); defer alloc.free(survivor_path); { var survivor = try std.Io.Dir.createFileAbsolute(io_mod.getIo(), survivor_path, .{}); survivor.close(io_mod.getIo()); } var failed_clear_arena_state = std.heap.ArenaAllocator.init(alloc); defer failed_clear_arena_state.deinit(); const failed_clear = try executeToolCall(ctx, failed_clear_arena_state.allocator(), .{ .id = "failed-memory-clear", .name = "memory", .arguments_json = "{\"action\":\"clear\"}", }); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.failure, failed_clear.status); try std.testing.expectEqualStrings( "memory clear failed: saved memories were not removed; ensure ~/.fx/memories.json is a removable file and retry", failed_clear.model_output, ); var survivor = try std.Io.Dir.openFileAbsolute(io_mod.getIo(), survivor_path, .{}); survivor.close(io_mod.getIo()); } test "install_skill explicit tool installs local skill source" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "repo/workflow"); try tmp.dir.createDirPath(io_mod.getIo(), "skills"); { var file = try tmp.dir.createFile(io_mod.getIo(), "repo/workflow/SKILL.md", .{}); defer file.close(io_mod.getIo()); try file.writeStreamingAll( io_mod.getIo(), "---\nname: workflow\"\ndescription: workflow helper\n---\n\nBODY SENTINEL\nkeep raw\n", ); } const repo_root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "repo"); defer alloc.free(repo_root); const skills_dir = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "skills"); defer alloc.free(skills_dir); var rt = TestRuntime{ .workspace_root = repo_root, .skills_dir = skills_dir }; defer rt.deinit(alloc); const args_json = try std.fmt.allocPrint(alloc, "{{\"source\":\"{s}\",\"skill\":\"workflow\"}}", .{repo_root}); defer alloc.free(args_json); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const result = try executeToolCall(rt.context(), arena_state.allocator(), .{ .id = "1", .name = "install_skill", .arguments_json = args_json }); try expectContains(result.model_output, "Installed 1 skill(s) into fx."); try expectContains(result.model_output, "- workflow"<injected>\n"); try expectNotContains(result.model_output, "") == null); const installed = try std.fs.path.join(alloc, &.{ skills_dir, "workflow", "SKILL.md" }); defer alloc.free(installed); var file = try std.Io.Dir.openFileAbsolute(io_mod.getIo(), installed, .{}); defer file.close(io_mod.getIo()); const content = try io_mod.readFileToEnd(alloc, &file, 1024 * 1024); defer alloc.free(content); try std.testing.expectEqualStrings( "---\nname: workflow\"\ndescription: workflow helper\n---\n\nBODY SENTINEL\nkeep raw\n", content, ); } test "skill tool preserves resource and discovery notices separately" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "home/workspace"); try tmp.dir.createDirPath(io_mod.getIo(), "home/.fx/skills/workflow/assets"); try tmp.dir.createDirPath(io_mod.getIo(), "home/.fx/skills/malformed"); { var file = try tmp.dir.createFile(io_mod.getIo(), "home/.fx/skills/workflow/SKILL.md", .{}); defer file.close(io_mod.getIo()); try file.writeStreamingAll( io_mod.getIo(), "---\nname: workflow\ndescription: workflow helper\n---\n\nuse the workflow skill\n" ++ ("bounded instruction line\n" ** 8), ); } { var file = try tmp.dir.createFile(io_mod.getIo(), "home/.fx/skills/workflow/assets/data.txt", .{}); defer file.close(io_mod.getIo()); try file.writeStreamingAll(io_mod.getIo(), "hello\n"); } { var file = try tmp.dir.createFile(io_mod.getIo(), "home/.fx/skills/malformed/SKILL.md", .{}); defer file.close(io_mod.getIo()); try file.writeStreamingAll(io_mod.getIo(), "---\ndescription: missing name\n---\nMALFORMED BODY"); } const workspace_root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "home/workspace"); defer alloc.free(workspace_root); const skills_dir = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "home/.fx/skills"); defer alloc.free(skills_dir); try setTestHome(null); var rt = TestRuntime{ .workspace_root = workspace_root, .skills_dir = skills_dir, .session_allocator = alloc }; defer rt.deinit(alloc); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); var ctx = rt.context(); ctx.context_limits.skill_chunk_bytes = .{ .value = .{ .bytes = 96 }, .source = .command_line }; const result = try executeToolCall(ctx, arena_state.allocator(), .{ .id = "1", .name = "skill", .arguments_json = "{\"name\":\"workflow\"}" }); try expectContains(result.model_output, "= self.responses.len) return error.UnexpectedVisionGatewayCall; const response = self.responses[response_index]; request.delivery.markPossiblySent(); return .{ .status = response.status, .completion = .{ .content = response.content, .generation_id = response.generation_id, .finish_reason = .stop, .usage = response.usage, }, .generation_origin = "https://ai-gateway.vercel.sh", }; } }; fn makeVisionCatalog( alloc: Allocator, dir: std.Io.Dir, count: usize, ) ![]types.ImageAttachment { const catalog = try alloc.alloc(types.ImageAttachment, count); errdefer alloc.free(catalog); var initialized: usize = 0; errdefer for (catalog[0..initialized]) |image| types.freeImageAttachment(alloc, image); const root = try io_mod.dirRealpathAlloc(alloc, dir, "."); defer alloc.free(root); const snapshot_dir = try std.fs.path.join(alloc, &.{ root, "snapshots" }); defer alloc.free(snapshot_dir); for (catalog, 0..) |*image, index| { const name = try std.fmt.allocPrint(alloc, "vision-{d}.png", .{index + 1}); defer alloc.free(name); try dir.writeFile(io_mod.getIo(), .{ .sub_path = name, .data = "\x89PNG\r\n\x1a\nimage bytes" }); const path = try io_mod.dirRealpathAlloc(alloc, dir, name); var path_owned = true; errdefer if (path_owned) alloc.free(path); const media_type = try alloc.dupe(u8, "image/png"); var media_type_owned = true; errdefer if (media_type_owned) alloc.free(media_type); image.* = .{ .id = index + 1, .path = path, .media_type = media_type }; path_owned = false; media_type_owned = false; var image_initialized = false; errdefer if (!image_initialized) types.freeImageAttachment(alloc, image.*); try image_attachments.captureImageSnapshot(alloc, image, snapshot_dir); initialized += 1; image_initialized = true; } return catalog; } fn visionArgs(alloc: Allocator, image_ids: []const usize, focus: []const u8) ![]u8 { var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try out.writer.writeAll("{\"image_ids\":"); try std.json.Stringify.value(image_ids, .{}, &out.writer); try out.writer.writeAll(",\"focus\":"); try std.json.Stringify.value(focus, .{}, &out.writer); try out.writer.writeByte('}'); return out.toOwnedSlice(); } fn visionProviderSuccess(alloc: Allocator, image_ids: []const usize) ![]u8 { var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try out.writer.writeAll("{\"images\":["); for (image_ids, 0..) |image_id, index| { if (index > 0) try out.writer.writeByte(','); try out.writer.print( "{{\"image_id\":{d},\"status\":\"ok\",\"summary\":\"evidence {d}\",\"visible_text\":[],\"details\":[]}}", .{ image_id, image_id }, ); } try out.writer.writeAll("]}"); return out.toOwnedSlice(); } fn visionProviderSuccessReversed(alloc: Allocator, image_ids: []const usize) ![]u8 { var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try out.writer.writeAll("{\"images\":["); var index = image_ids.len; while (index > 0) { index -= 1; if (index + 1 < image_ids.len) try out.writer.writeByte(','); const image_id = image_ids[index]; try out.writer.print( "{{\"image_id\":{d},\"status\":\"ok\",\"summary\":\"evidence {d}\",\"visible_text\":[],\"details\":[]}}", .{ image_id, image_id }, ); } try out.writer.writeAll("]}"); return out.toOwnedSlice(); } fn executeVisionForTest( rt: *TestRuntime, alloc: Allocator, args_json: []const u8, catalog: []const types.ImageAttachment, ) !ToolExecutionResult { return executeToolCallAuthorized(rt.context(), .{ .call_allocator = alloc, .result_allocator = alloc, .call = .{ .id = "vision-call", .name = "vision", .arguments_json = args_json }, .authority = .ordinary, .authorized_image_catalog = catalog, .session_grants = &.{}, .advertised_dynamic_tool_names = &.{}, .max_tool_result_bytes = rt.max_tool_result_bytes, }); } fn executeVisionPathForTest( rt: *TestRuntime, alloc: Allocator, args_json: []const u8, canonical_paths: []const []const u8, ) !ToolExecutionResult { const targets = try alloc.alloc( command_admission.VisionPathExecutionTarget, canonical_paths.len, ); var initialized: usize = 0; defer { for (targets[0..initialized]) |target| { alloc.free(@constCast(target.canonical_path)); } alloc.free(targets); } for (canonical_paths, targets) |path, *target| { var file = try std.Io.Dir.openFileAbsolute(io_mod.getIo(), path, .{ .allow_directory = false, .follow_symlinks = false, }); defer file.close(io_mod.getIo()); const stat = try file.stat(io_mod.getIo()); target.* = .{ .canonical_path = try alloc.dupe(u8, path), .identity = pathing.fileIdentity( try pathing.descriptorDevice(file.handle), stat, ), }; initialized += 1; } return executeVisionPathTargetsForTest(rt, alloc, args_json, targets); } fn executeVisionPathTargetsForTest( rt: *TestRuntime, alloc: Allocator, args_json: []const u8, targets: []const command_admission.VisionPathExecutionTarget, ) !ToolExecutionResult { return executeToolCallAuthorized(rt.context(), .{ .call_allocator = alloc, .result_allocator = alloc, .call = .{ .id = "vision-call", .name = "vision", .arguments_json = args_json }, .authority = .{ .vision_paths = .{ .targets = targets } }, .authorized_image_catalog = &.{}, .session_grants = &.{}, .advertised_dynamic_tool_names = &.{}, .max_tool_result_bytes = rt.max_tool_result_bytes, }); } fn visionRequestAllocationCount(args_json: []const u8) !usize { var probe = std.testing.FailingAllocator.init( std.testing.allocator, .{ .resize_fail_index = 0 }, ); const alloc = probe.allocator(); const request = try tool_contracts.vision.parse_vision_request(alloc, args_json); request.deinit(alloc); try std.testing.expectEqual(probe.allocated_bytes, probe.freed_bytes); return probe.alloc_index; } fn visionProviderParseAllocationCount(provider_json: []const u8) !usize { var probe = std.testing.FailingAllocator.init( std.testing.allocator, .{ .resize_fail_index = 0 }, ); const alloc = probe.allocator(); const parsed = try tool_contracts.vision.parse_vision_provider_result( alloc, provider_json, provider_json.len, ); parsed.deinit(alloc); try std.testing.expectEqual(probe.allocated_bytes, probe.freed_bytes); return probe.alloc_index; } fn visionAllocationIndexAtGateway( catalog: []const types.ImageAttachment, args_json: []const u8, provider_json: []const u8, ) !usize { const base = std.testing.allocator; var probe = std.testing.FailingAllocator.init( base, .{ .resize_fail_index = 0 }, ); const responses = [_]VisionGatewayResponse{.{ .content = provider_json }}; var fixture = VisionGatewayFixture{ .alloc = base, .responses = &responses, .allocation_probe = &probe, }; defer fixture.deinit(); var rt = TestRuntime{ .agent_stream_provider = fixture.provider(), .tool_registry = .{ .tools = vision_test_registry_tools[0..] }, .session_allocator = base, .context_limits = .{ .image_adapter_output_bytes = .{ .value = .{ .bytes = 64 * 1024 }, .source = .command_line, } }, }; defer rt.deinit(base); const alloc = probe.allocator(); const result = try executeVisionForTest(&rt, alloc, args_json, catalog); alloc.free(@constCast(result.model_output)); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.success, result.status); try std.testing.expectEqual(probe.allocated_bytes, probe.freed_bytes); return fixture.allocation_index_at_stream orelse error.TestExpectedEqual; } const OneShotFailingAllocator = struct { failing: std.testing.FailingAllocator, fn init(alloc: Allocator, fail_index: usize) OneShotFailingAllocator { return .{ .failing = .init(alloc, .{ .fail_index = fail_index, .resize_fail_index = 0, }) }; } fn allocator(self: *OneShotFailingAllocator) Allocator { return .{ .ptr = self, .vtable = &.{ .alloc = allocate, .resize = resize, .remap = remap, .free = free, } }; } fn allocate( raw: *anyopaque, len: usize, alignment: std.mem.Alignment, return_address: usize, ) ?[*]u8 { const self: *OneShotFailingAllocator = @ptrCast(@alignCast(raw)); const result = self.failing.allocator().rawAlloc( len, alignment, return_address, ); if (result == null and self.failing.has_induced_failure) { self.failing.fail_index = std.math.maxInt(usize); } return result; } fn resize( raw: *anyopaque, memory: []u8, alignment: std.mem.Alignment, new_len: usize, return_address: usize, ) bool { const self: *OneShotFailingAllocator = @ptrCast(@alignCast(raw)); return self.failing.allocator().rawResize( memory, alignment, new_len, return_address, ); } fn remap( raw: *anyopaque, memory: []u8, alignment: std.mem.Alignment, new_len: usize, return_address: usize, ) ?[*]u8 { const self: *OneShotFailingAllocator = @ptrCast(@alignCast(raw)); return self.failing.allocator().rawRemap( memory, alignment, new_len, return_address, ); } fn free( raw: *anyopaque, memory: []u8, alignment: std.mem.Alignment, return_address: usize, ) void { const self: *OneShotFailingAllocator = @ptrCast(@alignCast(raw)); self.failing.allocator().rawFree(memory, alignment, return_address); } }; fn expectVisionOutOfMemoryAt( fail_index: usize, expected_gateway_calls: usize, catalog: []const types.ImageAttachment, args_json: []const u8, provider_json: []const u8, ) !void { const base = std.testing.allocator; var failing = OneShotFailingAllocator.init(base, fail_index); const responses = [_]VisionGatewayResponse{.{ .content = provider_json }}; var fixture = VisionGatewayFixture{ .alloc = base, .responses = &responses }; defer fixture.deinit(); var rt = TestRuntime{ .agent_stream_provider = fixture.provider(), .tool_registry = .{ .tools = vision_test_registry_tools[0..] }, .session_allocator = base, .context_limits = .{ .image_adapter_output_bytes = .{ .value = .{ .bytes = 64 * 1024 }, .source = .command_line, } }, }; defer rt.deinit(base); const alloc = failing.allocator(); if (executeVisionForTest(&rt, alloc, args_json, catalog)) |result| { alloc.free(@constCast(result.model_output)); if (result.interactive_notice != null or result.system_notice != null) { return error.OutOfMemoryProducedVisionNotice; } return error.OutOfMemoryReturnedVisionResult; } else |err| { try std.testing.expectEqual(error.OutOfMemory, err); } try std.testing.expect(failing.failing.has_induced_failure); try std.testing.expectEqual(expected_gateway_calls, fixture.call_count); try std.testing.expectEqual( failing.failing.allocated_bytes, failing.failing.freed_bytes, ); } test "vision runtime propagates request and authorized catalog allocation failures" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const catalog = try makeVisionCatalog(alloc, tmp.dir, 1); defer types.freeImageAttachmentSlice(alloc, catalog); const args_json = "{\"image_ids\":[1],\"focus\":\"inspect\"}"; const provider_json = "{\"images\":[]}"; try expectVisionOutOfMemoryAt(0, 0, catalog, args_json, provider_json); try expectVisionOutOfMemoryAt( try visionRequestAllocationCount(args_json), 0, catalog, args_json, provider_json, ); } test "vision runtime propagates provider and parser allocation failures without outage notices" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const catalog = try makeVisionCatalog(alloc, tmp.dir, 1); defer types.freeImageAttachmentSlice(alloc, catalog); const args_json = "{\"image_ids\":[1],\"focus\":\"inspect\"}"; const provider_json = "{\"images\":[{\"image_id\":1,\"status\":\"ok\",\"summary\":\"read\",\"visible_text\":[],\"details\":[]}]}"; const stream_alloc_index = try visionAllocationIndexAtGateway( catalog, args_json, provider_json, ); try expectVisionOutOfMemoryAt( stream_alloc_index, 1, catalog, args_json, provider_json, ); try expectVisionOutOfMemoryAt( stream_alloc_index + 1, 1, catalog, args_json, provider_json, ); } test "vision parsed record transfer allocation failure releases the parsed owner" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const catalog = try makeVisionCatalog(alloc, tmp.dir, 1); defer types.freeImageAttachmentSlice(alloc, catalog); const args_json = "{\"image_ids\":[1],\"focus\":\"inspect\"}"; const provider_json = "{\"images\":[{\"image_id\":1,\"status\":\"ok\",\"summary\":\"read\",\"visible_text\":[],\"details\":[]}]}"; const transfer_capacity_fail_index = try visionAllocationIndexAtGateway(catalog, args_json, provider_json) + 1 + try visionProviderParseAllocationCount(provider_json); try expectVisionOutOfMemoryAt( transfer_capacity_fail_index, 1, catalog, args_json, provider_json, ); } test "vision authorized catalog retains historical access outside budgeted model history" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const source_images = try makeVisionCatalog(alloc, tmp.dir, 1); defer types.freeImageAttachmentSlice(alloc, source_images); source_images[0].id = 41; const history = [_]types.HistoryTurn{ .{ .assistant = .{ .user = .{ .text = @constCast("historical turn"), .images = source_images }, .assistant = @constCast("historical answer"), } }, .{ .assistant = .{ .user = .{ .text = @constCast("newer turn without images") }, .assistant = @constCast("newer answer"), } }, }; const catalog = try session_runtime.collect_image_catalog(alloc, &history, &.{}); defer types.freeImageAttachmentSlice(alloc, catalog); var budget_arena_state = std.heap.ArenaAllocator.init(alloc); defer budget_arena_state.deinit(); var budgeted_messages: std.ArrayList(ChatMessage) = .empty; try session_runtime.appendHistoryChatMessagesBudgeted( budget_arena_state.allocator(), &budgeted_messages, &history, .{ .max_tokens = 1 }, ); for (budgeted_messages.items) |message| { try std.testing.expectEqual(@as(usize, 0), message.images.len); } const provider_json = try visionProviderSuccess(alloc, &.{41}); defer alloc.free(provider_json); const responses = [_]VisionGatewayResponse{.{ .content = provider_json }}; var fixture = VisionGatewayFixture{ .alloc = alloc, .responses = &responses }; defer fixture.deinit(); var rt = TestRuntime{ .agent_stream_provider = fixture.provider(), .tool_registry = .{ .tools = vision_test_registry_tools[0..] }, .session_allocator = alloc, }; defer rt.deinit(alloc); const result = try executeVisionForTest( &rt, alloc, "{\"image_ids\":[41],\"focus\":\"inspect historical evidence\"}", catalog, ); defer alloc.free(@constCast(result.model_output)); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.success, result.status); try expectContains(result.model_output, "\"image_id\":41"); try std.testing.expectEqual(@as(usize, 1), fixture.call_count); } test "vision runtime resolves historical authorized images and batches twenty as eight eight four" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const catalog = try makeVisionCatalog(alloc, tmp.dir, 20); defer types.freeImageAttachmentSlice(alloc, catalog); const requested = [_]usize{ 20, 1, 19, 2, 18, 3, 17, 4, 16, 5, 15, 6, 14, 7, 13, 8, 12, 9, 11, 10 }; const first = try visionProviderSuccessReversed(alloc, requested[0..8]); defer alloc.free(first); const second = try visionProviderSuccessReversed(alloc, requested[8..16]); defer alloc.free(second); const third = try visionProviderSuccessReversed(alloc, requested[16..20]); defer alloc.free(third); const responses = [_]VisionGatewayResponse{ .{ .content = first, .usage = .{ .input_tokens = 2, .output_tokens = 3 } }, .{ .content = second, .usage = .{ .input_tokens = 5, .output_tokens = 7 }, .generation_id = "gen_01ARZ3NDEKTSV4RRFFQ69G5FAW", }, .{ .content = third, .usage = .{ .input_tokens = 11, .output_tokens = 13 }, .generation_id = "gen_01ARZ3NDEKTSV4RRFFQ69G5FAX", }, }; var fixture = VisionGatewayFixture{ .alloc = alloc, .responses = &responses }; defer fixture.deinit(); var rt = TestRuntime{ .agent_stream_provider = fixture.provider(), .tool_registry = .{ .tools = vision_test_registry_tools[0..] }, .api_key = "gateway-key", .gateway_team = "team_vision", .gateway_retry_count = 2, .gateway_chat_url = "https://gateway.invalid/chat", .session_allocator = alloc, .context_limits = .{ .image_adapter_output_bytes = .{ .value = .{ .bytes = 64 * 1024 }, .source = .command_line, } }, }; defer rt.deinit(alloc); const args = try visionArgs(alloc, &requested, "Read the build state"); defer alloc.free(args); const result = try executeVisionForTest(&rt, alloc, args, catalog); defer alloc.free(@constCast(result.model_output)); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.success, result.status); try std.testing.expectEqual(@as(usize, 3), fixture.call_count); try std.testing.expectEqual(@as(usize, 8), std.mem.count(u8, fixture.payloads.items[0], "\"type\":\"file\"")); try std.testing.expectEqual(@as(usize, 8), std.mem.count(u8, fixture.payloads.items[1], "\"type\":\"file\"")); try std.testing.expectEqual(@as(usize, 4), std.mem.count(u8, fixture.payloads.items[2], "\"type\":\"file\"")); try std.testing.expectEqualStrings("google/gemini-2.5-flash", fixture.last_model); try std.testing.expectEqualStrings("gateway-key", fixture.last_api_key); try std.testing.expectEqualStrings("team_vision", fixture.last_team.?); try std.testing.expectEqual(@as(usize, 2), fixture.last_retry_count); try std.testing.expectEqualStrings("https://gateway.invalid/chat", fixture.last_chat_url); try expectContains(fixture.payloads.items[0], "Read the build state"); try expectContains(fixture.payloads.items[0], "\"mediaType\":\"image/png\""); for (fixture.payloads.items) |payload| { for (catalog) |image| try expectNotContains(payload, image.path); } var previous_offset: usize = 0; for (requested) |image_id| { const needle = try std.fmt.allocPrint(alloc, "\"image_id\":{d}", .{image_id}); defer alloc.free(needle); const relative = std.mem.findPos(u8, result.model_output, previous_offset, needle) orelse return error.TestExpectedEqual; previous_offset = relative + needle.len; } try std.testing.expectEqual(@as(u64, 18), result.inner_usage.?.input_tokens); try std.testing.expectEqual(@as(u64, 23), result.inner_usage.?.output_tokens); var usage_snapshot = try rt.session.usage.snapshot(alloc); defer usage_snapshot.deinit(alloc); try std.testing.expectEqual(@as(usize, 3), usage_snapshot.pending.len); } test "vision runtime rejects unauthorized ids before filesystem or provider access" { const alloc = std.testing.allocator; var fixture = VisionGatewayFixture{ .alloc = alloc, .responses = &.{} }; defer fixture.deinit(); var rt = TestRuntime{ .agent_stream_provider = fixture.provider(), .tool_registry = .{ .tools = vision_test_registry_tools[0..] }, }; defer rt.deinit(alloc); const result = try executeVisionForTest(&rt, alloc, "{\"image_ids\":[91],\"focus\":\"inspect\"}", &.{}); defer alloc.free(@constCast(result.model_output)); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.failure, result.status); try expectContains(result.model_output, "UnauthorizedImageId"); try std.testing.expectEqual(@as(usize, 0), fixture.call_count); } test "vision runtime rejects a path source without canonical path authority" { const alloc = std.testing.allocator; var fixture = VisionGatewayFixture{ .alloc = alloc, .responses = &.{} }; defer fixture.deinit(); var rt = TestRuntime{ .agent_stream_provider = fixture.provider(), .tool_registry = .{ .tools = vision_test_registry_tools[0..] }, .workspace_root = "/tmp", }; defer rt.deinit(alloc); try std.testing.expectError( error.InvalidVisionExecutionAuthority, executeVisionForTest( &rt, alloc, "{\"paths\":[\"unapproved.png\"],\"focus\":\"inspect\"}", &.{}, ), ); try std.testing.expectEqual(@as(usize, 0), fixture.call_count); } test "vision runtime loads approved paths into a transient authorized catalog" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.writeFile(io_mod.getIo(), .{ .sub_path = "photo.png", .data = "\x89PNG\r\n\x1a\nimage bytes", }); const workspace = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(workspace); const source_path = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "photo.png"); defer alloc.free(source_path); const provider_json = try visionProviderSuccess(alloc, &.{1}); defer alloc.free(provider_json); const responses = [_]VisionGatewayResponse{.{ .content = provider_json }}; var fixture = VisionGatewayFixture{ .alloc = alloc, .responses = &responses }; defer fixture.deinit(); var rt = TestRuntime{ .agent_stream_provider = fixture.provider(), .tool_registry = .{ .tools = vision_test_registry_tools[0..] }, .workspace_root = workspace, .session_allocator = alloc, .context_limits = .{ .image_adapter_output_bytes = .{ .value = .{ .bytes = 64 * 1024 }, .source = .command_line, } }, }; defer rt.deinit(alloc); const result = try executeVisionPathForTest( &rt, alloc, "{\"paths\":[\"photo.png\"],\"focus\":\"inspect\"}", &.{source_path}, ); defer alloc.free(@constCast(result.model_output)); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.success, result.status); try expectContains(result.model_output, "\"image_id\":1"); try std.testing.expectEqual(@as(usize, 1), fixture.call_count); try expectContains(fixture.payloads.items[0], "\"type\":\"file\""); try expectNotContains(fixture.payloads.items[0], source_path); } test "vision runtime returns bounded failures for unavailable approved paths" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.writeFile(io_mod.getIo(), .{ .sub_path = "removed.png", .data = "\x89PNG\r\n\x1a\nimage bytes", }); const workspace = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(workspace); const removed_path = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "removed.png"); defer alloc.free(removed_path); var file = try std.Io.Dir.openFileAbsolute(io_mod.getIo(), removed_path, .{ .follow_symlinks = false, }); const stat = try file.stat(io_mod.getIo()); const target: command_admission.VisionPathExecutionTarget = .{ .canonical_path = removed_path, .identity = pathing.fileIdentity( try pathing.descriptorDevice(file.handle), stat, ), }; file.close(io_mod.getIo()); try tmp.dir.deleteFile(io_mod.getIo(), "removed.png"); var fixture = VisionGatewayFixture{ .alloc = alloc, .responses = &.{} }; defer fixture.deinit(); var rt = TestRuntime{ .agent_stream_provider = fixture.provider(), .tool_registry = .{ .tools = vision_test_registry_tools[0..] }, .workspace_root = workspace, }; defer rt.deinit(alloc); const result = try executeVisionPathTargetsForTest( &rt, alloc, "{\"paths\":[\"removed.png\"],\"focus\":\"inspect\"}", &.{target}, ); defer alloc.free(@constCast(result.model_output)); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.failure, result.status); try expectContains(result.model_output, "FileNotFound"); try std.testing.expectEqual(@as(usize, 0), fixture.call_count); } test "vision runtime propagates cancellation while preparing approved paths" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.writeFile(io_mod.getIo(), .{ .sub_path = "photo.png", .data = "\x89PNG\r\n\x1a\nimage bytes", }); const workspace = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(workspace); const source_path = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "photo.png"); defer alloc.free(source_path); var fixture = VisionGatewayFixture{ .alloc = alloc, .responses = &.{} }; defer fixture.deinit(); var cancel_flag = std.atomic.Value(bool).init(true); var rt = TestRuntime{ .agent_stream_provider = fixture.provider(), .tool_registry = .{ .tools = vision_test_registry_tools[0..] }, .workspace_root = workspace, .cancel_flag = &cancel_flag, }; defer rt.deinit(alloc); try std.testing.expectError( error.Cancelled, executeVisionPathForTest( &rt, alloc, "{\"paths\":[\"photo.png\"],\"focus\":\"inspect\"}", &.{source_path}, ), ); try std.testing.expectEqual(@as(usize, 0), fixture.call_count); } test "vision runtime preserves partial success and exact total outage notices" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const catalog = try makeVisionCatalog(alloc, tmp.dir, 2); defer types.freeImageAttachmentSlice(alloc, catalog); const partial_json = "{\"images\":[" ++ "{\"image_id\":1,\"status\":\"ok\",\"summary\":\"read\",\"visible_text\":[],\"details\":[]}]}"; // The malformed response is scripted twice because a structurally invalid // successful response is retried once. The outage before it is not retried. const responses = [_]VisionGatewayResponse{ .{ .content = partial_json }, .{ .status = .service_unavailable }, .{ .content = "not-json" }, .{ .content = "not-json" }, }; var fixture = VisionGatewayFixture{ .alloc = alloc, .responses = &responses }; defer fixture.deinit(); var rt = TestRuntime{ .agent_stream_provider = fixture.provider(), .tool_registry = .{ .tools = vision_test_registry_tools[0..] }, .session_allocator = alloc, }; defer rt.deinit(alloc); const partial = try executeVisionForTest(&rt, alloc, "{\"image_ids\":[1,2],\"focus\":\"inspect\"}", catalog); defer alloc.free(@constCast(partial.model_output)); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.success, partial.status); try std.testing.expect(partial.interactive_notice == null); try expectContains(partial.model_output, "\"status\":\"ok\""); try expectContains(partial.model_output, "\"code\":\"missing_provider_record\""); try expectContains(partial.model_output, "\"retryable\":true"); try std.testing.expect(partial.status_detail == null); try std.testing.expectEqual(@as(usize, 1), fixture.call_count); const outage = try executeVisionForTest(&rt, alloc, "{\"image_ids\":[1],\"focus\":\"inspect\"}", catalog); defer alloc.free(@constCast(outage.model_output)); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.failure, outage.status); try std.testing.expectEqualStrings(vision_executor.outage_tip, outage.interactive_notice.?.body); try std.testing.expectEqualStrings(vision_executor.outage_system_notice, outage.system_notice.?); try expectContains(outage.model_output, "\"code\":\"vision_unavailable\""); try std.testing.expectEqualStrings("Vision is unavailable right now", outage.status_detail.?); try expectContains(outage.system_notice.?, "None of the requested images were read"); try expectContains(outage.system_notice.?, "retry Vision"); try expectContains(outage.system_notice.?, "change strategy"); try expectContains(outage.system_notice.?, "without making visual claims"); try std.testing.expectEqual(@as(usize, 2), fixture.call_count); const malformed = try executeVisionForTest(&rt, alloc, "{\"image_ids\":[2],\"focus\":\"inspect\"}", catalog); defer alloc.free(@constCast(malformed.model_output)); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.failure, malformed.status); try expectContains(malformed.model_output, "\"code\":\"provider_response_invalid\""); try expectContains(malformed.model_output, "\"retryable\":true"); try std.testing.expectEqualStrings("provider response stayed invalid after one retry", malformed.status_detail.?); try std.testing.expect(malformed.interactive_notice == null); try std.testing.expect(malformed.system_notice == null); try std.testing.expectEqual(@as(usize, 4), fixture.call_count); } test "vision runtime classifies an empty successful provider result as invalid" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const catalog = try makeVisionCatalog(alloc, tmp.dir, 1); defer types.freeImageAttachmentSlice(alloc, catalog); const responses = [_]VisionGatewayResponse{ .{ .content = "{\"images\":[]}" }, .{ .content = "{\"images\":[]}" }, }; var fixture = VisionGatewayFixture{ .alloc = alloc, .responses = &responses }; defer fixture.deinit(); var rt = TestRuntime{ .agent_stream_provider = fixture.provider(), .tool_registry = .{ .tools = vision_test_registry_tools[0..] }, .session_allocator = alloc, }; defer rt.deinit(alloc); const result = try executeVisionForTest( &rt, alloc, "{\"image_ids\":[1],\"focus\":\"inspect\"}", catalog, ); defer alloc.free(@constCast(result.model_output)); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.failure, result.status); try expectContains(result.model_output, "\"code\":\"provider_response_invalid\""); try std.testing.expectEqualStrings("provider response stayed invalid after one retry", result.status_detail.?); try std.testing.expect( std.mem.find(u8, result.model_output, "\"image_id\":1") != null, ); try std.testing.expect(result.interactive_notice == null); try std.testing.expect(result.system_notice == null); // An empty successful response is structurally invalid, so it earns the single retry. try std.testing.expectEqual(@as(usize, 2), fixture.call_count); } test "vision runtime honors configured provider bounds without a hidden twenty KiB ceiling" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const catalog = try makeVisionCatalog(alloc, tmp.dir, 1); defer types.freeImageAttachmentSlice(alloc, catalog); const evidence = "x" ** 3500; var large_out: std.Io.Writer.Allocating = .init(alloc); defer large_out.deinit(); try large_out.writer.writeAll("{\"images\":[{\"image_id\":1,\"status\":\"ok\",\"summary\":\"large\",\"visible_text\":["); for (0..7) |index| { if (index > 0) try large_out.writer.writeByte(','); try std.json.Stringify.value(evidence, .{}, &large_out.writer); } try large_out.writer.writeAll("],\"details\":[]}]}"); const large_json = try large_out.toOwnedSlice(); defer alloc.free(large_json); try std.testing.expect(large_json.len > 20 * 1024); const responses = [_]VisionGatewayResponse{ .{ .content = large_json }, .{ .content = large_json }, .{ .content = large_json }, }; var fixture = VisionGatewayFixture{ .alloc = alloc, .responses = &responses }; defer fixture.deinit(); var rt = TestRuntime{ .agent_stream_provider = fixture.provider(), .tool_registry = .{ .tools = vision_test_registry_tools[0..] }, .session_allocator = alloc, .context_limits = .{ .image_adapter_output_bytes = .{ .value = .{ .bytes = large_json.len }, .source = .command_line, } }, }; defer rt.deinit(alloc); const accepted = try executeVisionForTest(&rt, alloc, "{\"image_ids\":[1],\"focus\":\"inspect\"}", catalog); defer alloc.free(@constCast(accepted.model_output)); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.success, accepted.status); try std.testing.expectEqual(@as(usize, 1), fixture.call_count); rt.context_limits.image_adapter_output_bytes.value = .{ .bytes = large_json.len - 1 }; const rejected = try executeVisionForTest(&rt, alloc, "{\"image_ids\":[1],\"focus\":\"inspect\"}", catalog); defer alloc.free(@constCast(rejected.model_output)); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.failure, rejected.status); try expectContains(rejected.model_output, "\"code\":\"output_limit_exceeded\""); try expectContains(rejected.model_output, "Call Vision again with a narrower focus or fewer images."); try std.testing.expectEqualStrings("response exceeded the configured output budget", rejected.status_detail.?); try std.testing.expectEqual(@as(usize, 2), fixture.call_count); rt.context_limits.image_adapter_output_bytes.value = .off; const unbounded = try executeVisionForTest(&rt, alloc, "{\"image_ids\":[1],\"focus\":\"inspect\"}", catalog); defer alloc.free(@constCast(unbounded.model_output)); try std.testing.expectEqual(tool_contracts.ToolExecutionStatus.success, unbounded.status); try std.testing.expectEqual(@as(usize, 3), fixture.call_count); } test "vision cancellation stops later batches and does not surface the outage tip" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const catalog = try makeVisionCatalog(alloc, tmp.dir, 9); defer types.freeImageAttachmentSlice(alloc, catalog); const first = try visionProviderSuccess(alloc, &.{ 1, 2, 3, 4, 5, 6, 7, 8 }); defer alloc.free(first); const responses = [_]VisionGatewayResponse{.{ .content = first }}; var fixture = VisionGatewayFixture{ .alloc = alloc, .responses = &responses, .cancel_after_call = 1, }; defer fixture.deinit(); var cancel_flag = std.atomic.Value(bool).init(false); var rt = TestRuntime{ .agent_stream_provider = fixture.provider(), .tool_registry = .{ .tools = vision_test_registry_tools[0..] }, .cancel_flag = &cancel_flag, .session_allocator = alloc, .context_limits = .{ .image_adapter_output_bytes = .{ .value = .{ .bytes = 64 * 1024 }, .source = .command_line, } }, }; defer rt.deinit(alloc); try std.testing.expectError( error.Cancelled, executeVisionForTest( &rt, alloc, "{\"image_ids\":[1,2,3,4,5,6,7,8,9],\"focus\":\"inspect\"}", catalog, ), ); try std.testing.expectEqual(@as(usize, 1), fixture.call_count); }