Something went wrong. Try again.
My version of https://github.com/vercel-labs/fx
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550const std = @import("std");const skill_contract = @import("../skills/skill_contract.zig");const command_admission = @import("../permissions/command_admission.zig");const core_permissions = @import("../permissions/permissions.zig");const core_types = @import("../shared/types.zig");const debug_trace = @import("../shared/debug_trace.zig");const command_environment = @import("../execution/command_environment.zig");const file_mutation_contract = @import("file_mutation_contract.zig");const io_mod = @import("../shared/io.zig");const message = @import("../shared/message.zig");const mod_registry = @import("../mods/registry.zig");const permission_gate = @import("../permissions/permission_gate.zig");const change_tracker = @import("../workspace/change_tracker.zig");const read_tracker_mod = @import("../workspace/read_tracker.zig");const session_child_store = @import("../session/session_child_store.zig");const command_replay_store = @import("../session/command_replay_store.zig");const command_runner = @import("../execution/command_runner.zig");const managed_execution = @import("../execution/managed_execution.zig");const subagent_tool_provider = @import("../subagent/tool_provider.zig");const text_utils = @import("../shared/text_utils.zig");const web_fetch_runtime = @import("web_fetch_runtime.zig");const web_fetch_artifacts = @import("../session/web_fetch_artifacts.zig");const model_tool_schema = @import("model_tool_schema.zig");const host = @import("../hosts/host.zig");const tool_result_errors = @import("tool_result_errors.zig");const tool_result_limits = @import("tool_result_limits.zig");const tool_mcp_runtime = @import("tool_mcp_runtime.zig");const web_search_contract = @import("web_search_contract.zig");const context_limits = @import("../config/context_limits.zig");const workspace_access = @import("../workspace/workspace_access.zig");const terminal_client_runtime = @import("../terminal/client.zig");const terminal_contracts = @import("../terminal/contracts.zig");const tool_args = @import("tool_args.zig");const result_commit = @import("result_commit.zig");
const Allocator = std.mem.Allocator;
pub const default_ignored_list_entries = [_][]const u8{ ".git", ".zig-cache", "zig-out", "node_modules", ".next", "dist", "build", "coverage",};pub const default_max_list_entries: usize = 100;pub const default_max_read_file_bytes: usize = 50 * 1024;pub const default_max_read_file_lines: usize = 400;pub const default_max_read_file_line_len: usize = 2000;
pub const web_search_unavailable_message = "web_search is unavailable: no local runtime with a configured Gateway transport policy is installed";pub const terminal_unavailable_message = "{\"error\":{\"tool\":\"shell\",\"code\":\"unsupported_host\",\"retryable\":false}}";const terminal_saved_session_required_message = "TTY shell actions require a saved fx session.";const terminal_saved_session_required_suggestion = "Use shell.run with tty=false, or rerun without --no-save.";
pub const ToolCapabilities = struct { web_search_runtime_ready: bool = false, terminal: host.TerminalSupport = .unsupported,
pub fn for_host(host_capabilities: host.Capabilities) ToolCapabilities { return .{ .terminal = host_capabilities.terminal }; }
pub fn terminalAvailable(self: ToolCapabilities) bool { return self.terminal.isSupported(); }};
pub const WebSearchProgressFn = *const fn (*anyopaque, []const u8, core_types.WebSearchProgress) void;pub const WebFetchProgressFn = *const fn (*anyopaque, []const u8, core_types.WebFetchProgress) void;
pub const WebSearchBackendFn = *const fn ( *anyopaque, DispatchContext, web_search_contract.Request,) anyerror!web_search_contract.ExecutionOutput;
pub const WebSearchBackend = struct { ctx: *anyopaque, execute_fn: WebSearchBackendFn,
pub fn execute( self: WebSearchBackend, ctx: DispatchContext, request: web_search_contract.Request, ) !web_search_contract.ExecutionOutput { return self.execute_fn(self.ctx, ctx, request); }};
pub const VisionProviderFn = *const fn ( ?*anyopaque, DispatchContext, ToolInput,) DispatchError!ToolResult;
pub const VisionProvider = struct { ctx: ?*anyopaque = null, execute_fn: VisionProviderFn,
pub fn execute( self: VisionProvider, ctx: DispatchContext, input: ToolInput, ) DispatchError!ToolResult { return self.execute_fn(self.ctx, ctx, input); }};
pub const SelectedDynamicToolSinkFn = *const fn ( ?*anyopaque, []const u8, []const u8, ?tool_mcp_runtime.Binding,) error{OutOfMemory}!void;
pub const ContextNoticeSinkFn = *const fn (?*anyopaque, []const u8) error{OutOfMemory}!void;
/// Erased, owned typed input decoded by a concrete tool.pub const ToolInput = struct { ptr: *anyopaque, deinit_fn: *const fn (ptr: *anyopaque, alloc: Allocator) void,
/// Casts the erased input back to the tool's concrete input type. pub fn as(self: ToolInput, comptime T: type) *T { return @ptrCast(@alignCast(self.ptr)); }
/// Frees the concrete input through the tool-provided destructor. pub fn deinit(self: ToolInput, alloc: Allocator) void { self.deinit_fn(self.ptr, alloc); }};
/// Owned success or failure text returned by a tool call.pub const ToolResult = union(enum) { success: []u8, failure: []u8, rich: struct { text: []u8, images: []core_types.ToolImage, is_error: bool = false },
/// Frees the owned result text and media. pub fn deinit(self: ToolResult, alloc: Allocator) void { switch (self) { .success => |body| alloc.free(body), .failure => |body| alloc.free(body), .rich => |content| { alloc.free(content.text); core_types.freeToolImages(alloc, content.images); }, } }
pub fn intoAuthorized(self: ToolResult) AuthorizedDispatchResult { return switch (self) { .success => |body| .{ .status = .success, .body = body }, .failure => |body| .{ .status = .failure, .body = body }, .rich => |content| .{ .status = if (content.is_error) .failure else .success, .body = content.text, .images = content.images }, }; }};
pub const ModelContentKind = enum { ordinary, complete_skill };
pub const HostToolProviderFn = *const fn ( *anyopaque, Allocator, []const u8, []const u8, usize, ?*std.atomic.Value(bool),) DispatchError!ToolResult;
pub const HostToolProvider = struct { context: *anyopaque, call_fn: HostToolProviderFn,
pub fn call( self: HostToolProvider, alloc: Allocator, name: []const u8, arguments_json: []const u8, max_result_bytes: usize, cancel_flag: ?*std.atomic.Value(bool), ) DispatchError!ToolResult { return self.call_fn( self.context, alloc, name, arguments_json, max_result_bytes, cancel_flag, ); }};
/// Result of decoding a raw JSON argument buffer into a typed input.pub const DecodeResult = union(enum) { input: ToolInput, failure: []u8,};
/// Errors that stop local dispatch before a model-recoverable result is built.pub const DispatchError = std.json.ParseError(std.json.Scanner) || error{ OutOfMemory, InvalidToolArguments, InvalidPath, FileNotFound, HomeNotSet, PathOutsideWorkspace, WorkspaceUnavailable, Cancelled,};
/// Core-owned execution backend used by the registered run_command callback.pub const RunCommandRequest = struct { command: []const u8, resolved_cwd: []const u8, environment: command_environment.Environment, timeout_ms: u64,};
pub const RunCommandBackendFn = *const fn ( ?*anyopaque, DispatchContext, RunCommandRequest,) DispatchError!ToolResult;
pub const RunCommandBackend = struct { ctx: ?*anyopaque = null, execute_fn: RunCommandBackendFn,
pub fn execute( self: RunCommandBackend, ctx: DispatchContext, request: RunCommandRequest, ) DispatchError!ToolResult { return self.execute_fn(self.ctx, ctx, request); }};
/// Context shared by core tool dispatch, validation, and execution.pub const DispatchContext = struct { allocator: Allocator, permission_mode: permission_gate.PermissionMode = .ask, permission_decider: ?PermissionDecider = null, execution_authority: ?command_admission.ToolExecutionAuthority = null, workspace_root: []const u8 = "", access_scope: ?workspace_access.AccessScope = null, ignored_list_entries: []const []const u8 = &default_ignored_list_entries, max_list_entries: usize = default_max_list_entries, max_read_file_bytes: usize = default_max_read_file_bytes, max_read_file_lines: usize = default_max_read_file_lines, max_read_file_line_len: usize = default_max_read_file_line_len, max_tool_result_bytes: usize = tool_result_limits.default_max_tool_result_bytes, max_command_output_bytes: usize = tool_result_limits.default_max_tool_result_bytes, skills_dir: []const u8 = "", skill_locations: ?*const skill_contract.Locations = null, resolved_skill: ?*const skill_contract.PreparedSkill = null, context_limits: context_limits.Values = .{}, read_tracker: ?*read_tracker_mod.ReadTracker = null, change_tracker: ?*change_tracker.ChangeTracker = null, cancel_flag: ?*std.atomic.Value(bool) = null, output_chunk_lifecycle_id: ?core_types.ToolLifecycleId = null, output_chunk_ctx: ?*anyopaque = null, on_output_chunk: ?command_runner.CommandOutputCallback = null, command_artifact_dir: ?[]const u8 = null, managed_executions: ?*managed_execution.Runtime = null, tool_result_dir: ?[]const u8 = null, session_child_capability: ?*session_child_store.SessionChildCapability = null, ephemeral_command_replay: ?*command_replay_store.EphemeralStore = null, terminal_client: ?*terminal_client_runtime.Runtime = null, terminal_owner_session_id: ?[]const u8 = null, terminal_transport_role: terminal_contracts.TransportRole = .interactive, lifecycle_allocator: Allocator = std.heap.c_allocator, command_timeout_ms: ?usize = null, captured_command_host: command_environment.Host = .native, run_command_backend: ?RunCommandBackend = null, subagent_provider: ?subagent_tool_provider.Provider = null, vision_provider: ?VisionProvider = null, host_tool_provider: ?HostToolProvider = null, ask_question_ctx: ?*anyopaque = null, ask_question_batch: ?AskQuestionBatchFn = null, tool_capabilities: ToolCapabilities = .{}, 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_backend: ?WebSearchBackend = null, tool_call_id: []const u8 = "", tool_call_name: []const u8 = "", web_search_progress_ctx: ?*anyopaque = null, on_web_search_progress: ?WebSearchProgressFn = null, web_fetch_progress_ctx: ?*anyopaque = null, on_web_fetch_progress: ?WebFetchProgressFn = null, mcp_ctx: ?*anyopaque = 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_call_options: tool_mcp_runtime.CallOptions = .{}, mcp_call_status_sink: ?*?tool_mcp_runtime.CallStatus = null, mcp_execution_error_sink: ?*?anyerror = null, mcp_permission_rules: core_types.PermissionRuleSet = .{}, selected_dynamic_tool_ctx: ?*anyopaque = null, on_selected_dynamic_tool: ?SelectedDynamicToolSinkFn = null, context_notice_ctx: ?*anyopaque = null, on_context_notice: ?ContextNoticeSinkFn = null, inner_usage_sink: ?*?core_types.ToolUsage = null, web_search_completion_sink: ?*?core_types.WebSearchCompletion = null, web_fetch_completion_sink: ?*?core_types.WebFetchCompletion = null, tool_result_memory_sink: ?*?core_types.ToolResultMemory = null, model_content_kind_sink: ?*ModelContentKind = null, command_result_json_sink: ?*?[]const u8 = null, result_commit_sink: ?*?result_commit.Token = null,};
/// Function pointer used by ask_user_question to request live user answers.pub const AskQuestionBatchFn = *const fn ( ?*anyopaque, Allocator, []const core_types.QuestionBatchEntry,) anyerror!?[][]u8;
/// Function pointer used to override permission decisions in tests and callers.pub const PermissionDecider = *const fn (*const Tool, ToolInput, DispatchContext) permission_gate.Decision;
/// Function pointer that decodes JSON arguments into a concrete input.pub const DecodeFn = *const fn (DispatchContext, []const u8) DispatchError!DecodeResult;
/// Optional pure validation step run before the permission gate.pub const ValidateFn = *const fn (DispatchContext, ToolInput) DispatchError!?[]u8;
/// Function pointer that executes a validated and allowed tool input.pub const CallFn = *const fn (DispatchContext, ToolInput) DispatchError!ToolResult;
/// Optional Core adapter selected by a registered tool descriptor when the/// ordinary decode/validate/call path needs a focused lifecycle wrapper.pub const AuthorizedCallAdapterFn = *const fn ( DispatchContext, Registry, message.ToolCall,) DispatchError!AuthorizedDispatchResult;
/// Optional Core mapper selected by a registered tool descriptor after an/// authorized call has produced its structured dispatch result.pub const AuthorizedResultMapperFn = *const fn ( Allocator, AuthorizedDispatchResult, *?[]u8,) Allocator.Error!AuthorizedDispatchResult;
pub const RunCommandCompatibility = struct { matches: *const fn ([]const u8) bool, /// Returns success or failure text owned by `ctx.allocator`. execute: *const fn (DispatchContext, []const u8) DispatchError!ToolResult,};
/// Consumes a decoded tool input and transfers its owned fields into Core's/// canonical file-mutation contract. The caller owns the returned input and/// must not deinitialize the consumed ToolInput.pub const TakeFileMutationInputFn = *const fn ( ToolInput, Allocator,) file_mutation_contract.FileMutationInput;
/// Function pointer classifying whether an input is read-only.pub const ReadsOnlyFn = *const fn (ToolInput) bool;
/// Function pointer classifying whether an input is irreversible.pub const IrreversibleFn = *const fn (ToolInput) bool;
pub const ProviderAdvertisementError = std.mem.Allocator.Error || std.Io.Writer.Error || error{ InvalidWebSearchBackend, ConflictingDomainFilters, InvalidGatewayAdvertisement,};
pub const WriteProviderAdvertisementFn = *const fn ( std.mem.Allocator, *std.Io.Writer,) ProviderAdvertisementError!void;
pub const LabelArgKind = enum { none, name, path, pattern, url, command, description, source, resource, action, query, server, selector, session_id,};
pub const PermissionTargetKind = core_permissions.PermissionTargetKind;
pub const ExecutorKind = enum { glob_files, grep_files, read_file, read_tool_result, write_file, edit_file, web_fetch, web_search, run_command, terminal, skill, install_skill, subagent, capability_search, mcp_select_tool, mcp_features, ask_user_question, vision, host,};
pub const ApprovalPolicy = enum { standard, ask_only, auto_deny_on_ask,};
pub const RuntimeProviderKind = enum { none, run_command, subagent, vision,};
pub const CapturedCommandFn = *const fn (ToolInput) bool;pub const ProcessLocalFn = *const fn (ToolInput) bool;
pub const CallPresentation = struct { activity_kind: core_types.ToolActivityKind, action_label: []const u8, completed_action_label: []const u8, label_arg_kind: LabelArgKind, label_arg_default: []const u8,};
/// Optional call-aware presentation override. Returned strings must outlive the/// parsed arguments supplied to the callback.pub const PresentationFn = *const fn (std.json.ObjectMap) ?CallPresentation;
/// Descriptor for a core tool's model-facing metadata and runtime callbacks.pub const Tool = struct { name: []const u8, description: []const u8, model_schema: model_tool_schema.FunctionSchema, model_visible: bool = true, write_provider_advertisement_fn: ?WriteProviderAdvertisementFn = null, /// Set when the provider runs the tool instead of fx dispatch. Such a tool /// never reaches a call-time permission check, so advertisement is its only /// enforcement point and requires an already-settled allow. provider_executed: bool = false, executor_kind: ExecutorKind = .read_file, activity_kind: core_types.ToolActivityKind = .read, requires_approval: bool = false, approval_policy: ApprovalPolicy = .standard, action_label: []const u8 = "Working", completed_action_label: []const u8 = "Completed", label_arg_kind: LabelArgKind = .none, label_arg_default: []const u8 = "", presentation_fn: ?PresentationFn = null, permission_target_kind: PermissionTargetKind = .none, decode: DecodeFn, validate: ?ValidateFn = null, call: CallFn, runtime_provider: RuntimeProviderKind = .none, captured_command_host: command_environment.Host = .native, captured_command_action: ?[]const u8 = null, captured_command_fn: ?CapturedCommandFn = null, process_local_fn: ?ProcessLocalFn = null, authorized_call_adapter: ?AuthorizedCallAdapterFn = null, authorized_result_mapper: ?AuthorizedResultMapperFn = null, cancel_if_requested_after_call: bool = false, run_command_compatibility: ?RunCommandCompatibility = null, take_file_mutation_input_fn: ?TakeFileMutationInputFn = null, prepare_skill_call_fn: ?*const fn (DispatchContext, []const u8) DispatchError!skill_contract.CallPreparation = null, reads_only_fn: ReadsOnlyFn, irreversible_fn: IrreversibleFn,};
pub const Registry = mod_registry.ToolRegistry(Tool);
pub const ProgressLabelKind = enum { none, started, completed,};
/// Classifies rendered tool progress through the registered presentation labels.pub fn classifyProgressLabel(registry: Registry, text: []const u8) ProgressLabelKind { for (registry.tools) |tool| { if (startsWithProgressLabel(text, tool.action_label)) return .started; if (startsWithProgressLabel(text, tool.completed_action_label)) return .completed; } return .none;}
fn startsWithProgressLabel(text: []const u8, label: []const u8) bool { if (!std.mem.startsWith(u8, text, label)) return false; if (text.len == label.len) return true; return text[label.len] == ' ';}
pub const RunCommandCompatibilityMatch = struct { tool: *const Tool, compatibility: RunCommandCompatibility,};
pub const RunCommandCompatibilityMatchError = error{AmbiguousRunCommandCompatibility};
pub fn matchRunCommandCompatibility( registry: Registry, command: []const u8,) RunCommandCompatibilityMatchError!?RunCommandCompatibilityMatch { var matched: ?RunCommandCompatibilityMatch = null; for (registry.tools) |*tool| { const compatibility = tool.run_command_compatibility orelse continue; if (!compatibility.matches(command)) continue; if (matched != null) return error.AmbiguousRunCommandCompatibility; matched = .{ .tool = tool, .compatibility = compatibility, }; } return matched;}
pub fn dispatchRunCommandCompatibility( ctx: DispatchContext, registry: Registry, request: RunCommandRequest,) (DispatchError || RunCommandCompatibilityMatchError)!?ToolResult { if (std.meta.activeTag(request.environment) != .legacy) return null; const matched = (try matchRunCommandCompatibility( registry, request.command, )) orelse return null; if (std.mem.indexOfAny(u8, request.command, "|;&\n") != null) return null; return try matched.compatibility.execute(ctx, request.command);}
pub fn toolActivityKind(registry: Registry, tool_name: []const u8) core_types.ToolActivityKind { return if (registry.lookup(tool_name)) |tool| tool.activity_kind else .command;}
pub fn staticPresentation(tool: Tool) CallPresentation { return .{ .activity_kind = tool.activity_kind, .action_label = tool.action_label, .completed_action_label = tool.completed_action_label, .label_arg_kind = tool.label_arg_kind, .label_arg_default = tool.label_arg_default, };}
pub fn presentationForArgs(tool: Tool, args: std.json.ObjectMap) CallPresentation { if (tool.presentation_fn) |resolve| { if (resolve(args)) |presentation| return presentation; } return staticPresentation(tool);}
pub fn toolCallPresentation( alloc: Allocator, registry: Registry, call: core_types.ToolCall,) ?CallPresentation { const tool = registry.lookup(call.name) orelse return null; var scratch_state = std.heap.ArenaAllocator.init(alloc); defer scratch_state.deinit(); const args = tool_args.parseToolArgsObject( scratch_state.allocator(), call.arguments_json, ) catch return staticPresentation(tool.*); return presentationForArgs(tool.*, args);}
pub fn toolActivityKindForCall( alloc: Allocator, registry: Registry, call: core_types.ToolCall,) core_types.ToolActivityKind { const presentation = toolCallPresentation(alloc, registry, call) orelse return .command; return presentation.activity_kind;}
pub fn toolLabelValue(tool: Tool, args: std.json.ObjectMap) ?[]const u8 { return labelValueForKind(tool.label_arg_kind, args);}
pub fn presentationLabelValue(presentation: CallPresentation, args: std.json.ObjectMap) ?[]const u8 { return labelValueForKind(presentation.label_arg_kind, args);}
fn labelValueForKind(kind: LabelArgKind, args: std.json.ObjectMap) ?[]const u8 { return switch (kind) { .none => null, .name => optionalStringArg(args, "name"), .path => optionalStringArg(args, "path"), .pattern => optionalStringArg(args, "pattern"), .url => optionalStringArg(args, "url"), .command => optionalStringArg(args, "command"), .description => optionalStringArg(args, "description"), .source => optionalStringArg(args, "source"), .resource => optionalStringArg(args, "resource"), .action => optionalStringArg(args, "action"), .query => optionalStringArg(args, "query"), .server => optionalStringArg(args, "server"), .selector => optionalStringArg(args, "selector"), .session_id => optionalStringArg(args, "session_id"), };}
fn optionalStringArg(args: std.json.ObjectMap, key: []const u8) ?[]const u8 { const value = args.get(key) orelse return null; if (value != .string) return null; return value.string;}
/// Side-effect-free admission result for a registered tool call.pub const ValidationResult = union(enum) { not_registered, valid, failure: []u8,};
const ValidatedToolCall = union(enum) { not_registered, failure: []u8, input: struct { tool: *const Tool, value: ToolInput, },};
/// Decodes and validates registered input without resolving permission or executing the tool.pub fn validateRegisteredToolCall(ctx: DispatchContext, registry: Registry, call: message.ToolCall) DispatchError!ValidationResult { const validated = try decodeAndValidateRegisteredToolCall(ctx, registry, call); return switch (validated) { .not_registered => .not_registered, .failure => |reason| .{ .failure = reason }, .input => |input| blk: { input.value.deinit(ctx.allocator); break :blk .valid; }, };}
fn decodeAndValidateRegisteredToolCall(ctx: DispatchContext, registry: Registry, call: message.ToolCall) DispatchError!ValidatedToolCall { const tool = registry.lookup(call.name) orelse return .not_registered; return decodeAndValidateToolCall(ctx, tool, call.arguments_json);}
fn decodeAndValidateToolCall( ctx: DispatchContext, tool: *const Tool, arguments_json: []const u8,) DispatchError!ValidatedToolCall { const decoded = try tool.decode(ctx, arguments_json); return switch (decoded) { .failure => |reason| .{ .failure = reason }, .input => |input| blk: { errdefer input.deinit(ctx.allocator); if (tool.validate) |validate| { if (try validate(ctx, input)) |reason| { input.deinit(ctx.allocator); break :blk .{ .failure = reason }; } } break :blk .{ .input = .{ .tool = tool, .value = input } }; }, };}
/// A registered call that passed local availability and permission admission.pub const AdmittedToolCall = struct { tool: *const Tool, input: ToolInput, context: DispatchContext, permission_decision: permission_gate.Decision,
pub fn deinit(self: AdmittedToolCall) void { var permission_decision = self.permission_decision; permission_decision.deinit(self.context.allocator); self.input.deinit(self.context.allocator); }};
/// Result of generic tool lookup, validation, availability, and permission admission.pub const AdmissionResult = union(enum) { not_registered, failure: []u8, admitted: AdmittedToolCall,};
/// Admits one model-requested registered call without executing it.pub fn admitToolCall(ctx: DispatchContext, registry: Registry, call: message.ToolCall) DispatchError!AdmissionResult { var call_ctx = ctx; const validated = try decodeAndValidateRegisteredToolCall(call_ctx, registry, call); switch (validated) { .not_registered => return .not_registered, .failure => |reason| return .{ .failure = reason }, .input => |input| { var input_transferred = false; defer if (!input_transferred) input.value.deinit(call_ctx.allocator);
if (try localToolAvailabilityFailure(call_ctx, input.tool, input.value)) |reason| { return .{ .failure = reason }; }
var decision = permission_gate.decideOrdinary(input.tool, input.value, call_ctx); var decision_transferred = false; defer if (!decision_transferred) decision.deinit(call_ctx.allocator);
switch (decision.action) { .allow => {}, .deny => { traceDeniedWebSearch(.{}, call, decision.denial_reason orelse .policy_denied); return .{ .failure = try deniedToolResultBody(call_ctx.allocator, input.tool.name, decision) }; }, .ask => { traceDeniedWebSearch(.{}, call, .permission_required); return .{ .failure = try tool_result_errors.toolPermissionDeniedJson(call_ctx.allocator, input.tool.name, .permission_required) }; }, }
const captured_command = input.tool.executor_kind == .run_command or if (input.tool.captured_command_fn) |classify| classify(input.value) else false; if (captured_command) { const authority = decision.execution_authority orelse { return .{ .failure = try call_ctx.allocator.dupe(u8, "captured command permission allowed without execution authority") }; }; call_ctx.execution_authority = switch (authority) { .run_command => |command_authority| .{ .run_command = command_authority }, else => { return .{ .failure = try call_ctx.allocator.dupe(u8, "captured command permission returned invalid execution authority") }; }, }; } else { if (decision.execution_authority != null) { return .{ .failure = try call_ctx.allocator.dupe(u8, "non-command permission returned command execution authority") }; } call_ctx.execution_authority = .ordinary; }
if (core_permissions.isFilesystemMutationTool(input.tool.name)) { var execution_decision = permission_gate.decideOrdinary(input.tool, input.value, call_ctx); defer execution_decision.deinit(call_ctx.allocator); switch (execution_decision.action) { .allow => {}, .deny => { return .{ .failure = try deniedToolResultBody(call_ctx.allocator, input.tool.name, execution_decision) }; }, .ask => { return .{ .failure = try tool_result_errors.toolPermissionDeniedJson(call_ctx.allocator, input.tool.name, .permission_required) }; }, } }
const admitted = AdmissionResult{ .admitted = .{ .tool = input.tool, .input = input.value, .context = call_ctx, .permission_decision = decision, } }; input_transferred = true; decision_transferred = true; return admitted; }, }}
/// Owned tool-result message body produced by dispatch.pub const DispatchResult = struct { model_content_kind: ModelContentKind = .ordinary, status: Status, body: []u8, images: []core_types.ToolImage = &.{}, status_detail: ?[]u8 = null, inner_usage: ?core_types.ToolUsage = null, web_search_completion: ?core_types.WebSearchCompletion = null, web_fetch_completion: ?core_types.WebFetchCompletion = null, tool_result_memory: ?core_types.ToolResultMemory = null, command_result_json: ?[]const u8 = null,
/// Tool-result status used only by tests and diagnostics. pub const Status = enum { success, failure, };
/// Frees the owned tool-result body and optional diagnostic detail. pub fn deinit(self: DispatchResult, alloc: Allocator) void { alloc.free(self.body); core_types.freeToolImages(alloc, self.images); if (self.status_detail) |detail| alloc.free(detail); if (self.command_result_json) |json| alloc.free(@constCast(json)); if (self.tool_result_memory) |memory| { if (memory.command_output_replay) |replay| switch (replay) { .available => |descriptor| alloc.free(@constCast(descriptor.handle)), .unavailable => {}, }; } }};
/// Owned authorized-dispatch result. Runtime metadata is published through/// the caller-provided sinks in `DispatchContext` instead of being copied into/// this transport result.pub const AuthorizedDispatchResult = struct { status: DispatchResult.Status, body: []u8, images: []core_types.ToolImage = &.{},
pub fn deinit(self: AuthorizedDispatchResult, alloc: Allocator) void { alloc.free(self.body); core_types.freeToolImages(alloc, self.images); }};
/// Runs one model-requested tool call through lookup, validation, gate, and call.pub fn dispatchToolCall(ctx: DispatchContext, registry: Registry, call: message.ToolCall) DispatchError!DispatchResult { var captured_usage: ?core_types.ToolUsage = null; var captured_web_search_completion: ?core_types.WebSearchCompletion = null; var captured_web_fetch_completion: ?core_types.WebFetchCompletion = null; var captured_tool_result_memory: ?core_types.ToolResultMemory = null; var captured_model_content_kind: ModelContentKind = .ordinary; var captured_command_result_json: ?[]const u8 = null; var call_ctx = ctx; if (call_ctx.inner_usage_sink == null) call_ctx.inner_usage_sink = &captured_usage; if (call_ctx.web_search_completion_sink == null) call_ctx.web_search_completion_sink = &captured_web_search_completion; if (call_ctx.web_fetch_completion_sink == null) call_ctx.web_fetch_completion_sink = &captured_web_fetch_completion; if (call_ctx.tool_result_memory_sink == null) call_ctx.tool_result_memory_sink = &captured_tool_result_memory; if (call_ctx.model_content_kind_sink == null) call_ctx.model_content_kind_sink = &captured_model_content_kind; call_ctx.model_content_kind_sink.?.* = .ordinary; if (call_ctx.command_result_json_sink == null) call_ctx.command_result_json_sink = &captured_command_result_json;
const admission = try admitToolCall(call_ctx, registry, call); switch (admission) { .not_registered => return failure(try std.fmt.allocPrint(call_ctx.allocator, "unknown tool: {s}", .{call.name})), .failure => |reason| return failure(reason), .admitted => |admitted| { defer admitted.deinit();
const result = try admitted.tool.call(admitted.context, admitted.input); const inner_usage = if (admitted.context.inner_usage_sink) |slot| slot.* else captured_usage; const web_search_completion = if (admitted.context.web_search_completion_sink) |slot| slot.* else captured_web_search_completion; const web_fetch_completion = if (admitted.context.web_fetch_completion_sink) |slot| slot.* else captured_web_fetch_completion; const tool_result_memory = if (admitted.context.tool_result_memory_sink) |slot| slot.* else captured_tool_result_memory; const command_result_json = if (admitted.context.command_result_json_sink) |slot| slot.* else captured_command_result_json; const output = result.intoAuthorized(); return .{ .status = output.status, .model_content_kind = if (output.status == .success) if (admitted.context.model_content_kind_sink) |kind| kind.* else .ordinary else .ordinary, .body = output.body, .images = output.images, .inner_usage = inner_usage, .web_search_completion = web_search_completion, .web_fetch_completion = web_fetch_completion, .tool_result_memory = tool_result_memory, .command_result_json = command_result_json, }; }, }}
/// Runs one already-authorized registered tool call through lookup, validation, and call.pub fn dispatchAuthorizedToolCall( ctx: DispatchContext, registry: Registry, call: message.ToolCall, status_detail: *?[]u8,) DispatchError!AuthorizedDispatchResult { const tool = registry.lookup(call.name) orelse return .{ .status = .failure, .body = try std.fmt.allocPrint(ctx.allocator, "unknown tool: {s}", .{call.name}), }; const result = if (tool.authorized_call_adapter) |adapter| try adapter(ctx, registry, call) else try dispatchAuthorizedToolCallDefault(ctx, registry, call); if (tool.authorized_result_mapper) |mapper| { return mapper(ctx.allocator, result, status_detail) catch |err| { result.deinit(ctx.allocator); return err; }; } return result;}
/// Runs the ordinary authorized decode/validate/call path without consulting a/// descriptor's lifecycle adapter. Focused adapters use this to wrap one call/// without recursively selecting themselves again.pub fn dispatchAuthorizedToolCallDefault(ctx: DispatchContext, registry: Registry, call: message.ToolCall) DispatchError!AuthorizedDispatchResult { const validated = try decodeAndValidateRegisteredToolCall(ctx, registry, call); switch (validated) { .not_registered => return .{ .status = .failure, .body = try std.fmt.allocPrint(ctx.allocator, "unknown tool: {s}", .{call.name}) }, .failure => |reason| return .{ .status = .failure, .body = reason }, .input => |input| { defer input.value.deinit(ctx.allocator);
const result = try input.tool.call(ctx, input.value); if (input.tool.cancel_if_requested_after_call and ctx.cancel_flag != null and ctx.cancel_flag.?.load(.seq_cst)) { result.deinit(ctx.allocator); return error.Cancelled; } return result.intoAuthorized(); }, }}
pub fn reportInnerUsage(ctx: DispatchContext, usage: core_types.ToolUsage) void { const sink = ctx.inner_usage_sink orelse return; sink.* = usage;}
pub fn reportWebSearchCompletion(ctx: DispatchContext, completion: core_types.WebSearchCompletion) void { const sink = ctx.web_search_completion_sink orelse return; sink.* = completion;}
pub fn reportWebFetchCompletion(ctx: DispatchContext, completion: core_types.WebFetchCompletion) void { const sink = ctx.web_fetch_completion_sink orelse return; sink.* = completion;}
pub fn reportToolResultMemory(ctx: DispatchContext, memory: core_types.ToolResultMemory) void { const sink = ctx.tool_result_memory_sink orelse return; sink.* = memory;}
pub fn reportCommandResultJson(ctx: DispatchContext, json: []const u8) void { const sink = ctx.command_result_json_sink orelse return; sink.* = json;}
pub fn reportResultCommit(ctx: DispatchContext, token: result_commit.Token) void { const sink = ctx.result_commit_sink orelse return; sink.* = token;}
pub fn reportSelectedDynamicTool( ctx: DispatchContext, name: []const u8, schema_json: []const u8, binding: ?tool_mcp_runtime.Binding,) error{OutOfMemory}!void { const sink = ctx.on_selected_dynamic_tool orelse return; try sink(ctx.selected_dynamic_tool_ctx, name, schema_json, binding);}
pub fn reportContextNotice(ctx: DispatchContext, notice: []const u8) error{OutOfMemory}!void { const sink = ctx.on_context_notice orelse return; try sink(ctx.context_notice_ctx, notice);}
pub fn localToolAvailabilityFailure( ctx: DispatchContext, tool: *const Tool, input: ToolInput,) DispatchError!?[]u8 { return switch (tool.executor_kind) { .web_search => if (ctx.tool_capabilities.web_search_runtime_ready) null else try ctx.allocator.dupe(u8, web_search_unavailable_message), .terminal => if (tool.captured_command_fn != null and tool.captured_command_fn.?(input)) null else if (tool.process_local_fn != null and tool.process_local_fn.?(input) and ctx.managed_executions != null) null else if (!ctx.tool_capabilities.terminalAvailable()) try ctx.allocator.dupe(u8, terminal_unavailable_message) else if (ctx.session_child_capability != null) null else try tool_result_errors.toolExecutionFailureJson(ctx.allocator, .{ .tool_name = tool.name, .message = terminal_saved_session_required_message, .suggestion = terminal_saved_session_required_suggestion, }), else => null, };}
pub fn localToolAvailabilityFailureForCall( ctx: DispatchContext, registry: Registry, call: message.ToolCall,) DispatchError!?[]u8 { const validated = try decodeAndValidateRegisteredToolCall(ctx, registry, call); return switch (validated) { .not_registered, .failure => null, .input => |input| blk: { defer input.value.deinit(ctx.allocator); break :blk try localToolAvailabilityFailure(ctx, input.tool, input.value); }, };}
fn failure(body: []u8) DispatchResult { return .{ .status = .failure, .body = body };}
fn deniedToolResultBody(alloc: Allocator, tool_name: []const u8, decision: permission_gate.Decision) ![]u8 { if (decision.structured_model_output) return alloc.dupe(u8, decision.reason); return tool_result_errors.toolPermissionDeniedJson(alloc, tool_name, decision.denial_reason orelse .policy_denied);}
pub fn traceDeniedWebSearch(ctx: debug_trace.TraceContext, call: message.ToolCall, reason: core_types.ToolPermissionDenialReason) void { if (!core_permissions.isWebSearchToolName(call.name)) return;
var arena_state = std.heap.ArenaAllocator.init(std.heap.c_allocator); defer arena_state.deinit(); const arena = arena_state.allocator();
var parsed = std.json.parseFromSlice(std.json.Value, arena, call.arguments_json, .{}) catch { debug_trace.eventf("permission", "web_search_denied", ctx, "call_id={s} tool_name={s} query=<invalid-json> reason={s}", .{ call.id, call.name, @tagName(reason) }); return; }; defer parsed.deinit(); if (parsed.value != .object) return; const query_value = parsed.value.object.get("query") orelse return; if (query_value != .string) return;
const masked = text_utils.maskSecrets(arena, query_value.string) catch "<redaction-error>"; var preview_buf: [160]u8 = undefined; const query_preview = debug_trace.terminalPreview(&preview_buf, masked); debug_trace.eventf("permission", "web_search_denied", ctx, "call_id={s} tool_name={s} query={s} query_bytes={d} reason={s}", .{ call.id, call.name, query_preview, query_value.string.len, @tagName(reason) });}
const MockInput = struct { read_only: bool = true, irreversible: bool = false,};
fn mockInputDeinit(ptr: *anyopaque, alloc: Allocator) void { const input: *MockInput = @ptrCast(@alignCast(ptr)); alloc.destroy(input);}
fn decodeMock(ctx: DispatchContext, args_json: []const u8) DispatchError!DecodeResult { if (std.mem.eql(u8, args_json, "invalid_args_error")) { return error.InvalidToolArguments; }
if (std.mem.eql(u8, args_json, "decode_error")) { return .{ .failure = try ctx.allocator.dupe(u8, "invalid mock arguments") }; }
const input = try ctx.allocator.create(MockInput); input.* = .{ .read_only = !std.mem.eql(u8, args_json, "deny"), .irreversible = std.mem.eql(u8, args_json, "irreversible"), }; return .{ .input = .{ .ptr = input, .deinit_fn = mockInputDeinit } };}
fn validateMock(ctx: DispatchContext, input: ToolInput) DispatchError!?[]u8 { _ = input; _ = ctx; return null;}
fn rejectMock(ctx: DispatchContext, input: ToolInput) DispatchError!?[]u8 { _ = input; return try ctx.allocator.dupe(u8, "validation rejected input");}
fn callMock(ctx: DispatchContext, input: ToolInput) DispatchError!ToolResult { _ = input; return .{ .success = try ctx.allocator.dupe(u8, "mock ok") };}
fn mockReadsOnly(input: ToolInput) bool { return input.as(MockInput).read_only;}
fn mockIrreversible(input: ToolInput) bool { return input.as(MockInput).irreversible;}
fn askDecision(_: *const Tool, _: ToolInput, _: DispatchContext) permission_gate.Decision { return .{ .action = .ask, .reason = "side effect requires confirmation" };}
var unavailable_web_search_permission_count: usize = 0;var unavailable_web_search_execution_count: usize = 0;
fn countWebSearchPermission(_: *const Tool, _: ToolInput, _: DispatchContext) permission_gate.Decision { unavailable_web_search_permission_count += 1; return .{ .action = .allow, .reason = "test allow" };}
fn denyWebSearchPermission(_: *const Tool, _: ToolInput, _: DispatchContext) permission_gate.Decision { unavailable_web_search_permission_count += 1; return .{ .action = .deny, .reason = "test deny", .denial_reason = .user_denied };}
fn countWebSearchExecution(ctx: DispatchContext, _: ToolInput) DispatchError!ToolResult { unavailable_web_search_execution_count += 1; return .{ .success = try ctx.allocator.dupe(u8, "unexpected execution") };}
const mock_tool = Tool{ .name = "mock_tool", .description = "Mock tool used by dispatch tests.", .model_schema = .{ .name = "mock_tool", .description = "Mock tool used by dispatch tests.", }, .decode = decodeMock, .validate = validateMock, .call = callMock, .reads_only_fn = mockReadsOnly, .irreversible_fn = mockIrreversible,};
fn matchesCompatibilityFixture(_: []const u8) bool { return true;}
fn executeCompatibilityFixture(ctx: DispatchContext, _: []const u8) DispatchError!ToolResult { return .{ .success = try ctx.allocator.dupe(u8, "compatibility fixture") };}
const first_compatibility_tool = blk: { var tool = mock_tool; tool.name = "first_compatibility"; tool.run_command_compatibility = .{ .matches = matchesCompatibilityFixture, .execute = executeCompatibilityFixture, }; break :blk tool;};
const second_compatibility_tool = blk: { var tool = mock_tool; tool.name = "second_compatibility"; tool.run_command_compatibility = .{ .matches = matchesCompatibilityFixture, .execute = executeCompatibilityFixture, }; break :blk tool;};
test "run command compatibility rejects overlapping matches" { const registry = Registry{ .tools = &.{ first_compatibility_tool, second_compatibility_tool } };
try std.testing.expectError( error.AmbiguousRunCommandCompatibility, matchRunCommandCompatibility(registry, "matching command"), );}
test "Registry.lookup returns hit and miss" { const registry = Registry{ .tools = &.{mock_tool} };
try std.testing.expect(registry.lookup("mock_tool") != null); try std.testing.expect(registry.lookup("missing") == null);}
test "classifyProgressLabel preserves registered order and label boundaries" { const first_tool = blk: { var tool = mock_tool; tool.action_label = "Running mock"; tool.completed_action_label = "Shared label"; break :blk tool; }; const second_tool = blk: { var tool = mock_tool; tool.name = "second_mock_tool"; tool.action_label = "Shared label"; tool.completed_action_label = "Ran mock"; break :blk tool; }; const registry = Registry{ .tools = &.{ first_tool, second_tool } };
try std.testing.expectEqual(ProgressLabelKind.started, classifyProgressLabel(registry, "Running mock")); try std.testing.expectEqual(ProgressLabelKind.started, classifyProgressLabel(registry, "Running mock value")); try std.testing.expectEqual(ProgressLabelKind.completed, classifyProgressLabel(registry, "Shared label value")); try std.testing.expectEqual(ProgressLabelKind.completed, classifyProgressLabel(registry, "Ran mock value")); try std.testing.expectEqual(ProgressLabelKind.none, classifyProgressLabel(registry, "Running mockery")); try std.testing.expectEqual(ProgressLabelKind.none, classifyProgressLabel(registry, "Unknown tool"));}
test "toolLabelValue reads the label field named by registered metadata" { const labeled_tool = Tool{ .name = "labeled_tool", .description = "Labeled mock tool.", .model_schema = .{ .name = "labeled_tool", .description = "Labeled mock tool.", }, .label_arg_kind = .command, .decode = decodeMock, .call = callMock, .reads_only_fn = mockReadsOnly, .irreversible_fn = mockIrreversible, };
var parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, "{\"command\":\"zig build\",\"name\":\"ignored\"}", .{}); defer parsed.deinit(); try std.testing.expectEqualStrings("zig build", toolLabelValue(labeled_tool, parsed.value.object).?); try std.testing.expect(toolLabelValue(mock_tool, parsed.value.object) == null);}
test "validateRegisteredToolCall distinguishes unregistered valid and rejected calls without execution" { const rejecting_tool = Tool{ .name = "rejecting_tool", .description = "Rejecting mock tool.", .model_schema = .{ .name = "rejecting_tool", .description = "Rejecting mock tool.", }, .decode = decodeMock, .validate = rejectMock, .call = callMock, .reads_only_fn = mockReadsOnly, .irreversible_fn = mockIrreversible, }; const registry = Registry{ .tools = &.{ mock_tool, rejecting_tool } };
try std.testing.expectEqual(.not_registered, try validateRegisteredToolCall(.{ .allocator = std.testing.allocator }, registry, .{ .id = "c1", .name = "missing", .arguments_json = "{}", })); try std.testing.expectEqual(.valid, try validateRegisteredToolCall(.{ .allocator = std.testing.allocator }, registry, .{ .id = "c2", .name = "mock_tool", .arguments_json = "{}", }));
const rejected = try validateRegisteredToolCall(.{ .allocator = std.testing.allocator }, registry, .{ .id = "c3", .name = "rejecting_tool", .arguments_json = "{}", }); defer switch (rejected) { .failure => |reason| std.testing.allocator.free(reason), else => {}, }; try std.testing.expectEqualStrings("validation rejected input", rejected.failure);}
test "dispatchToolCall materializes unknown tool failure" { const result = try dispatchToolCall(.{ .allocator = std.testing.allocator }, .{}, .{ .id = "c1", .name = "missing", .arguments_json = "{}", }); defer result.deinit(std.testing.allocator);
try std.testing.expectEqual(.failure, result.status); try std.testing.expectEqualStrings("unknown tool: missing", result.body);}
test "dispatchToolCall materializes decode failure" { const registry = Registry{ .tools = &.{mock_tool} }; const result = try dispatchToolCall(.{ .allocator = std.testing.allocator }, registry, .{ .id = "c1", .name = "mock_tool", .arguments_json = "decode_error", }); defer result.deinit(std.testing.allocator);
try std.testing.expectEqual(.failure, result.status); try std.testing.expectEqualStrings("invalid mock arguments", result.body);}
test "dispatchToolCall propagates decode errors" { const registry = Registry{ .tools = &.{mock_tool} }; try std.testing.expectError(error.InvalidToolArguments, dispatchToolCall(.{ .allocator = std.testing.allocator }, registry, .{ .id = "c1", .name = "mock_tool", .arguments_json = "invalid_args_error", }));}
test "dispatchToolCall materializes validate failure" { const rejecting_tool = Tool{ .name = "rejecting_tool", .description = "Rejecting mock tool.", .model_schema = .{ .name = "rejecting_tool", .description = "Rejecting mock tool.", }, .decode = decodeMock, .validate = rejectMock, .call = callMock, .reads_only_fn = mockReadsOnly, .irreversible_fn = mockIrreversible, }; const registry = Registry{ .tools = &.{rejecting_tool} }; const result = try dispatchToolCall(.{ .allocator = std.testing.allocator }, registry, .{ .id = "c1", .name = "rejecting_tool", .arguments_json = "{}", }); defer result.deinit(std.testing.allocator);
try std.testing.expectEqual(.failure, result.status); try std.testing.expectEqualStrings("validation rejected input", result.body);}
test "dispatchToolCall allows and calls read-only tool" { const registry = Registry{ .tools = &.{mock_tool} }; const result = try dispatchToolCall(.{ .allocator = std.testing.allocator }, registry, .{ .id = "c1", .name = "mock_tool", .arguments_json = "{}", }); defer result.deinit(std.testing.allocator);
try std.testing.expectEqual(.success, result.status); try std.testing.expectEqualStrings("mock ok", result.body);}
test "dispatchToolCall denies non-read-only tool through gate" { const registry = Registry{ .tools = &.{mock_tool} }; const result = try dispatchToolCall(.{ .allocator = std.testing.allocator }, registry, .{ .id = "c1", .name = "mock_tool", .arguments_json = "deny", }); defer result.deinit(std.testing.allocator);
try std.testing.expectEqual(.failure, result.status); try expectPermissionDeniedBody(result.body, "mock_tool", .permission_required);}
test "dispatchToolCall downgrades ask decision to deny result" { const registry = Registry{ .tools = &.{mock_tool} }; const result = try dispatchToolCall(.{ .allocator = std.testing.allocator, .permission_decider = askDecision, }, registry, .{ .id = "c1", .name = "mock_tool", .arguments_json = "{}", }); defer result.deinit(std.testing.allocator);
try std.testing.expectEqual(.failure, result.status); try expectPermissionDeniedBody(result.body, "mock_tool", .permission_required);}
fn checkAdmitToolCallAskFailureAllocationFailures(alloc: Allocator) !void { const registry = Registry{ .tools = &.{mock_tool} }; const admission = try admitToolCall(.{ .allocator = alloc, .permission_decider = askDecision, }, registry, .{ .id = "c1", .name = "mock_tool", .arguments_json = "{}", });
switch (admission) { .failure => |body| { defer alloc.free(body); try expectPermissionDeniedBody(body, "mock_tool", .permission_required); }, else => return error.TestUnexpectedResult, }}
test "admitToolCall cleans decoded input across ask failure-body allocation failures" { try std.testing.checkAllAllocationFailures( std.testing.allocator, checkAdmitToolCallAskFailureAllocationFailures, .{}, );}
test "dispatchToolCall rejects unavailable web_search before permission or execution" { unavailable_web_search_permission_count = 0; unavailable_web_search_execution_count = 0; const web_search = Tool{ .name = "web_search", .description = "Web search dispatch fixture.", .model_schema = .{ .name = "web_search", .description = "Web search dispatch fixture.", }, .executor_kind = .web_search, .decode = decodeMock, .call = countWebSearchExecution, .reads_only_fn = mockReadsOnly, .irreversible_fn = mockIrreversible, }; const registry = Registry{ .tools = &.{web_search} };
const result = try dispatchToolCall(.{ .allocator = std.testing.allocator, .permission_decider = countWebSearchPermission, }, registry, .{ .id = "c1", .name = "web_search", .arguments_json = "{}", }); defer result.deinit(std.testing.allocator);
try std.testing.expectEqual(.failure, result.status); try std.testing.expectEqualStrings(web_search_unavailable_message, result.body); try std.testing.expectEqual(@as(usize, 0), unavailable_web_search_permission_count); try std.testing.expectEqual(@as(usize, 0), unavailable_web_search_execution_count);}
test "dispatchToolCall traces denied web_search query without secrets or execution" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(root); const trace_path = try std.fs.path.join(alloc, &.{ root, "web-search-denied-trace.log" }); defer alloc.free(trace_path);
debug_trace.resetForTest(); defer debug_trace.resetForTest(); try debug_trace.configureForTestWithScopes(alloc, trace_path, "permission");
unavailable_web_search_permission_count = 0; unavailable_web_search_execution_count = 0; const web_search = Tool{ .name = "web_search", .description = "Web search dispatch fixture.", .model_schema = .{ .name = "web_search", .description = "Web search dispatch fixture.", }, .executor_kind = .web_search, .decode = decodeMock, .call = countWebSearchExecution, .reads_only_fn = mockReadsOnly, .irreversible_fn = mockIrreversible, }; const registry = Registry{ .tools = &.{web_search} };
const result = try dispatchToolCall(.{ .allocator = alloc, .permission_decider = denyWebSearchPermission, .tool_capabilities = .{ .web_search_runtime_ready = true }, }, registry, .{ .id = "c1", .name = "web_search", .arguments_json = "{\"query\":\"latest AI_GATEWAY_API_KEY=secret-value news\"}", }); defer result.deinit(alloc); debug_trace.shutdown();
try expectPermissionDeniedBody(result.body, "web_search", .user_denied); try std.testing.expectEqual(@as(usize, 1), unavailable_web_search_permission_count); try std.testing.expectEqual(@as(usize, 0), unavailable_web_search_execution_count);
var file = try std.Io.Dir.openFileAbsolute(std.testing.io, trace_path, .{}); defer file.close(std.testing.io); const trace = try io_mod.readFileToEnd(alloc, &file, 8192); defer alloc.free(trace); try std.testing.expect(std.mem.find(u8, trace, "event=web_search_denied") != null); try std.testing.expect(std.mem.find(u8, trace, "tool_name=web_search") != null); try std.testing.expect(std.mem.find(u8, trace, "query=latest AI_GATEWAY_API_KEY=[redacted] news") != null); try std.testing.expect(std.mem.find(u8, trace, "secret-value") == null); try std.testing.expect(std.mem.find(u8, trace, "result body") == null);}
fn expectPermissionDeniedBody(body: []const u8, tool_name: []const u8, reason: core_types.ToolPermissionDenialReason) !void { try std.testing.expect(tool_result_errors.isToolPermissionDeniedOutput(body)); var parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, body, .{}); defer parsed.deinit();
const error_obj = parsed.value.object.get("error").?.object; try std.testing.expectEqualStrings("tool_permission_denied", error_obj.get("type").?.string); try std.testing.expectEqualStrings(tool_name, error_obj.get("tool_name").?.string); try std.testing.expectEqualStrings(@tagName(reason), error_obj.get("reason").?.string); try std.testing.expect(error_obj.get("denied").?.bool);}
test "ToolResult deinit frees owned success and failure bodies" { var ok = ToolResult{ .success = try std.testing.allocator.dupe(u8, "ok") }; ok.deinit(std.testing.allocator);
var err = ToolResult{ .failure = try std.testing.allocator.dupe(u8, "err") }; err.deinit(std.testing.allocator);}test "DispatchContext command runner fields default to inactive values" { const ctx = DispatchContext{ .allocator = std.testing.allocator };
try std.testing.expectEqual(permission_gate.PermissionMode.ask, ctx.permission_mode); try std.testing.expect(ctx.cancel_flag == null); try std.testing.expect(ctx.output_chunk_ctx == null); try std.testing.expect(ctx.on_output_chunk == null); try std.testing.expect(ctx.command_artifact_dir == null); try std.testing.expect(ctx.command_timeout_ms == null); try std.testing.expect(ctx.run_command_backend == null); try std.testing.expect(ctx.subagent_provider == null); try std.testing.expect(ctx.ask_question_ctx == null); try std.testing.expect(ctx.ask_question_batch == null); try std.testing.expect(!ctx.tool_capabilities.web_search_runtime_ready); try std.testing.expectEqual(host.TerminalSupport.unsupported, ctx.tool_capabilities.terminal);}
test "terminal tool capability facts follow the host support matrix" { const os_tags = [_]std.Target.Os.Tag{ .macos, .linux, .windows, .wasi, .freebsd, .emscripten, }; for (os_tags) |os_tag| { const expected = host.terminalSupportForOs(os_tag); const capabilities = ToolCapabilities.for_host(host.nativeForOs(os_tag)); try std.testing.expectEqual(expected, capabilities.terminal); try std.testing.expectEqual( expected.isSupported(), capabilities.terminalAvailable(), ); }}