Something went wrong. Try again.
My version of https://github.com/vercel-labs/fx
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787const std = @import("std");const builtin = @import("builtin");const command_admission = @import("../../core/permissions/command_admission.zig");const command_contract = @import("../../core/execution/command_contract.zig");const command_environment = @import("../../core/execution/command_environment.zig");const debug_trace = @import("../../core/shared/debug_trace.zig");const managed_execution = @import("../../core/execution/managed_execution.zig");const managed_contract = @import("../../core/execution/managed_execution_contract.zig");const io_mod = @import("../../core/shared/io.zig");const mem_utils = @import("../../core/shared/mem_utils.zig");const pathing = @import("../../core/workspace/pathing.zig");const terminal_identity = @import("../../core/terminal/identity.zig");const terminal_action_executor = @import("../../core/terminal/action_executor.zig");const terminal_managed_observer = @import("../../core/terminal/managed_observer.zig");const terminal_operation = @import("../../core/terminal/operation.zig");const terminal_store = @import("../../core/terminal/store.zig");const shell_resolver = @import("../../core/terminal/shell_resolver.zig");const sort_utils = @import("../../core/shared/sort_utils.zig");const terminal_contracts = @import("../../core/terminal/contracts.zig");const tool_args = @import("../../core/tooling/tool_args.zig");const tool_dispatch = @import("../../core/tooling/tool_dispatch.zig");const tool_result_limits = @import("../../core/tooling/tool_result_limits.zig");const tool_result_errors = @import("../../core/tooling/tool_result_errors.zig");const text_utils = @import("../../core/shared/text_utils.zig");const result_commit = @import("../../core/tooling/result_commit.zig");const result_store = @import("../../core/session/result_store.zig");const types = @import("../../core/shared/types.zig");const workspace_access = @import("../../core/workspace/workspace_access.zig");
const Allocator = std.mem.Allocator;
pub const Action = enum { run, interact, stop,};
const ShellKind = enum { executable };
pub const ShellInput = struct { kind: ShellKind, path: []const u8, clean_start: bool = false,};
pub const Input = struct { action: Action, command: ?[]const u8 = null, cwd: ?[]const u8 = null, profile: ?command_environment.Profile = null, shell: ?ShellInput = null, tty: bool = false, yield_time_ms: u32 = managed_contract.default_yield_time_ms, timeout_ms: ?u64 = null, session_id: ?[]const u8 = null, chars: ?[]const u8 = null, force: bool = false,};
pub const public_field_names = blk: { const fields = @typeInfo(Input).@"struct".fields; var names: [fields.len][]const u8 = undefined; for (fields, 0..) |field, index| names[index] = field.name; break :blk names;};
pub const ActionFieldContract = struct { allowed: []const []const u8, required: []const []const u8, conflicts: []const tool_result_errors.TerminalActionFieldConflict = &.{},};
pub fn actionFieldContract(action: Action) ActionFieldContract { return switch (action) { .run => .{ .allowed = &.{ "action", "command", "cwd", "profile", "shell", "tty", "yield_time_ms", "timeout_ms" }, .required = &.{ "action", "command" }, .conflicts = &.{.{ "profile", "shell" }}, }, .interact => .{ .allowed = &.{ "action", "session_id", "chars", "yield_time_ms" }, .required = &.{ "action", "session_id" }, }, .stop => .{ .allowed = &.{ "action", "session_id", "force" }, .required = &.{ "action", "session_id" }, }, };}
const OwnedInput = struct { arena_state: std.heap.ArenaAllocator.State, value: Input,
fn deinit(self: *OwnedInput, alloc: Allocator) void { self.arena_state.promote(alloc).deinit(); self.* = undefined; }};
pub fn decode( ctx: tool_dispatch.DispatchContext, args_json: []const u8,) tool_dispatch.DispatchError!tool_dispatch.DecodeResult { if (try decode_input(ctx, args_json)) |input| return .{ .input = input }; return .{ .failure = try request_correction(ctx.allocator, args_json, ctx.session_child_capability != null) };}
fn decode_input( ctx: tool_dispatch.DispatchContext, args_json: []const u8,) tool_dispatch.DispatchError!?tool_dispatch.ToolInput { var arena_state = std.heap.ArenaAllocator.init(ctx.allocator); defer mem_utils.deinit_arena(arena_state); const arena = arena_state.allocator(); var raw = std.json.parseFromSliceLeaky( std.json.Value, arena, args_json, .{ .allocate = .alloc_always }, ) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => return null, }; if (raw != .object) return null; const raw_action = raw.object.get("action") orelse return null; if (raw_action != .string) return null; const action = std.meta.stringToEnum(Action, raw_action.string) orelse return null; elideKnownNullFields(&raw.object);
var correction_scratch: ActionFieldCorrectionScratch = .{}; defer correction_scratch.deinit(ctx.allocator); if (try actionFieldCorrection( ctx.allocator, action, raw.object, &correction_scratch, ) != null) return null; normalizeCompositeArgument(arena, &raw, "shell") catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => return null, }; var input = std.json.parseFromValueLeaky(Input, arena, raw, .{}) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => return null, }; if (raw.object.get("yield_time_ms") == null) { input.yield_time_ms = defaultYieldTime(action); } if (argument_problem(input) != null) return null; const owned = try ctx.allocator.create(OwnedInput); owned.* = .{ .arena_state = arena_state.state, .value = input, }; arena_state.state = .init; return .{ .ptr = owned, .deinit_fn = inputDeinit, };}
fn defaultYieldTime(action: Action) u32 { return switch (action) { .run => managed_contract.default_yield_time_ms, .interact => managed_contract.default_wait_ceiling_ms, .stop => 0, };}
fn effective_interact_yield_time(has_input: bool, requested_ms: u32) u32 { if (!has_input) { return @min( @max(requested_ms, managed_contract.default_wait_ceiling_ms), managed_contract.max_wait_ceiling_ms, ); } return @min(requested_ms, managed_contract.max_yield_time_ms);}
// Advisory only: none of these values enters the executable decode path.fn request_correction(alloc: Allocator, args_json: []const u8, supports_tty: bool) Allocator.Error![]u8 { var arena_state = std.heap.ArenaAllocator.init(alloc); defer mem_utils.deinit_arena(arena_state); const arena = arena_state.allocator(); if (args_json.len > 16 * 1024) { return correction_json(alloc, &.{"Request is too large to suggest a repair; submit the intended action with only its required fields."}, null); } const raw = std.json.parseFromSliceLeaky(std.json.Value, arena, args_json, .{}) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => return correction_json(alloc, &.{"Shell arguments must be a JSON object."}, null), }; if (raw != .object or raw.object.count() > 32) { return correction_json(alloc, &.{"Shell arguments must be one bounded request object."}, null); }
var problems: std.ArrayList([]const u8) = .empty; var repairable = true; var object = raw.object; if (raw.object.get("request")) |wrapper| { var request = wrapper; if (request == .string) { try problems.append(arena, "request must be an object, not a JSON string."); request = std.json.parseFromSliceLeaky(std.json.Value, arena, request.string, .{}) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => return correction_json(alloc, problems.items, null), }; } if (request != .object or request.object.count() > 32) { return correction_json(alloc, &.{"request must be one object containing the intended action."}, null); } object = request.object; if (raw.object.count() > 1) { try problems.append(arena, "Only request is allowed at the top level; put action fields inside request."); var outer = raw.object.iterator(); while (outer.next()) |entry| { const name = entry.key_ptr.*; if (std.mem.eql(u8, name, "request")) continue; if (object.contains(name)) { repairable = false; } else { try object.put(arena, name, entry.value_ptr.*); } } } } elideKnownNullFields(&object); const action_value = object.get("action"); const action: Action = if (action_value) |value| blk: { if (value == .string) { if (std.meta.stringToEnum(Action, value.string)) |action| break :blk action; } try problems.append(arena, "request.action must be run, interact, or stop."); return correction_json(alloc, problems.items, null); } else blk: { try problems.append(arena, "request.action is required."); const command = object.get("command") orelse return correction_json(alloc, problems.items, null); if (command != .string or object.contains("session_id") or object.contains("chars") or object.contains("force")) { return correction_json(alloc, problems.items, null); } try object.put(arena, "action", .{ .string = "run" }); break :blk .run; };
var scratch: ActionFieldCorrectionScratch = .{}; if (try actionFieldCorrection(arena, action, object, &scratch)) |correction| { for (correction.invalid_fields) |name| { try problems.append(arena, try std.fmt.allocPrint( arena, "request.{s} is not accepted for {s}.", .{ text_utils.utf8PrefixByBytes(name, 64), @tagName(action) }, )); // A non-null unknown field can express intent that cannot be reconstructed. if (object.get(name).? != .null) repairable = false; _ = object.orderedRemove(name); } for (correction.missing_fields) |name| { try problems.append(arena, try std.fmt.allocPrint(arena, "request.{s} is required.", .{name})); repairable = false; } for (correction.conflicts) |conflict| { try problems.append(arena, try std.fmt.allocPrint(arena, "Choose either request.{s} or request.{s}.", .{ conflict[0], conflict[1] })); repairable = false; } }
var canonical: std.json.ObjectMap = .empty; inline for (@typeInfo(Input).@"struct".fields) |field| { if (object.get(field.name)) |original| { var value = original; const T = if (@typeInfo(field.type) == .optional) @typeInfo(field.type).optional.child else field.type; const expected = comptime switch (@typeInfo(T)) { .int => "an integer", .bool => "a boolean", .pointer => "a string", .@"enum" => "an advertised value", else => "an object matching its schema", }; var type_reported = false; if (comptime @typeInfo(T) == .int) { if (value == .string) { try problems.append(arena, "request." ++ field.name ++ " must be an integer."); type_reported = true; if (std.fmt.parseInt(T, value.string, 10)) |number| { value = if (std.math.cast(i64, number)) |integer| .{ .integer = integer } else .{ .number_string = try std.fmt.allocPrint(arena, "{d}", .{number}) }; } else |_| { repairable = false; } } } if (std.json.parseFromValueLeaky(field.type, arena, value, .{})) |_| { if (comptime T == ShellInput) { var shell: std.json.ObjectMap = .empty; inline for (@typeInfo(ShellInput).@"struct".fields) |member| { if (value.object.get(member.name)) |supplied| { try shell.put(arena, member.name, supplied); } } value = .{ .object = shell }; } } else |err| { if (err == error.OutOfMemory) return error.OutOfMemory; if (!type_reported) try problems.append(arena, "request." ++ field.name ++ " must be " ++ expected ++ "."); repairable = false; } try canonical.put(arena, field.name, value); } } const candidate = std.json.parseFromValueLeaky(Input, arena, .{ .object = canonical }, .{}) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => return correction_json(alloc, problems.items, null), }; if (argument_problem(candidate)) |problem| { try problems.append(arena, problem); repairable = false; } if (!supports_tty and (canonical.contains("tty") or canonical.contains("shell") or canonical.contains("chars"))) { try problems.append(arena, "Interactive Shell fields require a saved session."); repairable = false; } if (problems.items.len == 0) { try problems.append(arena, "Submit one Shell action inside request."); } return correction_json(alloc, problems.items, if (repairable) canonical else null);}
fn correction_json( alloc: Allocator, problems: []const []const u8, candidate: ?std.json.ObjectMap,) Allocator.Error![]u8 { const Retry = struct { request: std.json.Value }; var out: std.Io.Writer.Allocating = .init(alloc); errdefer out.deinit(); std.json.Stringify.value(.{ .@"error" = .{ .code = "invalid_shell_request", .executed = false, .problems = problems, .instruction = if (candidate != null) @as(?[]const u8, "Call shell once using retry_with exactly.") else null, .retry_with = if (candidate) |object| @as(?Retry, .{ .request = .{ .object = object } }) else null, } }, .{ .emit_null_optional_fields = false }, &out.writer) catch return error.OutOfMemory; return try out.toOwnedSlice();}
fn argument_problem(input: Input) ?[]const u8 { switch (input.action) { .run => { const command = input.command orelse return "request.command is required."; if (command.len == 0 or command.len > terminal_contracts.max_command_bytes) return "request.command must contain 1-65536 bytes."; if (input.timeout_ms == 0) return "request.timeout_ms must be at least 1; choose the intended deadline."; if (input.profile != null and input.shell != null) return "Choose either request.profile or request.shell."; if (!input.tty and input.shell != null) return "request.shell requires tty=true; choose the intended execution mode."; if (input.yield_time_ms > managed_contract.max_yield_time_ms) return "request.yield_time_ms must be between 0 and 30000."; }, .interact => { if (input.yield_time_ms > managed_contract.max_wait_ceiling_ms) return "request.yield_time_ms must be between 0 and 300000."; if (input.chars) |chars| { if (chars.len > terminal_contracts.max_write_bytes) return "request.chars exceed 65536 bytes."; } }, .stop => {}, } return null;}
fn normalizeCompositeArgument( alloc: Allocator, root: *std.json.Value, field_name: []const u8,) !void { const value = root.object.getPtr(field_name) orelse return; try tool_args.normalizeCompositeObjectValue(alloc, value);}
fn inputDeinit(ptr: *anyopaque, alloc: Allocator) void { const input: *OwnedInput = @ptrCast(@alignCast(ptr)); input.deinit(alloc); alloc.destroy(input);}
fn elideKnownNullFields(object: *std.json.ObjectMap) void { for (public_field_names[1..]) |field_name| { const value = object.get(field_name) orelse continue; if (value == .null or (value == .string and tool_args.isNullPlaceholderText(value.string))) { _ = object.orderedRemove(field_name); } }}
const ActionFieldCorrectionScratch = struct { invalid_fields: std.ArrayList([]const u8) = .empty, missing_fields: [public_field_names.len][]const u8 = undefined, conflicts: [public_field_names.len]tool_result_errors.TerminalActionFieldConflict = undefined,
fn deinit(self: *ActionFieldCorrectionScratch, alloc: Allocator) void { self.invalid_fields.deinit(alloc); self.* = undefined; }};
fn actionFieldCorrection( alloc: Allocator, action: Action, object: std.json.ObjectMap, scratch: *ActionFieldCorrectionScratch,) Allocator.Error!?tool_result_errors.TerminalActionFieldCorrection { const field_contract = actionFieldContract(action); try scratch.invalid_fields.ensureTotalCapacity(alloc, object.count()); var fields = object.iterator(); while (fields.next()) |entry| { var allowed = false; for (field_contract.allowed) |name| { if (std.mem.eql(u8, entry.key_ptr.*, name)) { allowed = true; break; } } if (!allowed) scratch.invalid_fields.appendAssumeCapacity(entry.key_ptr.*); } sort_utils.sort( []const u8, scratch.invalid_fields.items, {}, struct { fn lessThan(_: void, left: []const u8, right: []const u8) bool { return std.mem.order(u8, left, right) == .lt; } }.lessThan, ); var missing_count: usize = 0; for (field_contract.required) |name| { if (object.get(name) != null) continue; scratch.missing_fields[missing_count] = name; missing_count += 1; } var conflict_count: usize = 0; for (field_contract.conflicts) |conflict| { if (object.get(conflict[0]) == null or object.get(conflict[1]) == null) continue; scratch.conflicts[conflict_count] = conflict; conflict_count += 1; } if (scratch.invalid_fields.items.len == 0 and missing_count == 0 and conflict_count == 0) { return null; } return .{ .action = @tagName(action), .invalid_fields = scratch.invalid_fields.items, .missing_fields = scratch.missing_fields[0..missing_count], .allowed_fields = field_contract.allowed, .conflicts = scratch.conflicts[0..conflict_count], };}
pub fn validate( ctx: tool_dispatch.DispatchContext, erased: tool_dispatch.ToolInput,) tool_dispatch.DispatchError!?[]u8 { const input = erased.as(OwnedInput).value; var arena_state = std.heap.ArenaAllocator.init(ctx.allocator); defer mem_utils.deinit_arena(arena_state); const arena = arena_state.allocator(); return switch (input.action) { .run => validateRun(ctx, arena, input), .interact => validateInteract(ctx, input), .stop => null, };}
fn validateInteract( ctx: tool_dispatch.DispatchContext, input: Input,) tool_dispatch.DispatchError!?[]u8 { if (argument_problem(input)) |problem| return try ctx.allocator.dupe(u8, problem); return null;}
fn validateRun( ctx: tool_dispatch.DispatchContext, arena: Allocator, input: Input,) tool_dispatch.DispatchError!?[]u8 { if (argument_problem(input)) |problem| return try ctx.allocator.dupe(u8, problem); _ = resolveCwd(arena, ctx, input.cwd) catch |err| { return try std.fmt.allocPrint( ctx.allocator, "shell run cwd is invalid: {s}", .{@errorName(err)}, ); }; if (!input.tty) { _ = commandEnvironment(arena, ctx, input.profile) catch |err| { return try std.fmt.allocPrint( ctx.allocator, "shell run profile is invalid: {s}", .{@errorName(err)}, ); }; } return null;}
pub fn call( ctx: tool_dispatch.DispatchContext, erased: tool_dispatch.ToolInput,) tool_dispatch.DispatchError!tool_dispatch.ToolResult { const input = erased.as(OwnedInput).value; return switch (input.action) { .run => callRun(ctx, input), .interact => callInteract(ctx, input), .stop => callStop(ctx, input), };}
fn callRun( ctx: tool_dispatch.DispatchContext, input: Input,) tool_dispatch.DispatchError!tool_dispatch.ToolResult { if (input.tty) { return callTtyRun(ctx, input); } const runtime = ctx.managed_executions orelse return unavailable(ctx); const execution_authority = ctx.execution_authority orelse return unavailable(ctx); const authority = switch (execution_authority) { .run_command => |value| value, else => return unavailable(ctx), }; const command = input.command orelse return unavailable(ctx); var request_arena_state = std.heap.ArenaAllocator.init(ctx.allocator); defer mem_utils.deinit_arena(request_arena_state); const request_arena = request_arena_state.allocator(); const cwd = resolveCwd(request_arena, ctx, input.cwd) catch |err| { if (err == error.OutOfMemory) return error.OutOfMemory; return .{ .failure = try std.fmt.allocPrint( ctx.allocator, "shell run cwd is invalid: {s}", .{@errorName(err)}, ) }; }; const environment = commandEnvironment( request_arena, ctx, input.profile, ) catch |err| { return .{ .failure = try std.fmt.allocPrint( ctx.allocator, "shell run profile is invalid: {s}", .{@errorName(err)}, ) }; }; var execution_id_buffer: [64]u8 = undefined; const execution_id = runtime.generatedId(&execution_id_buffer) catch |err| return runtimeFailure(ctx, err); var prepared = runtime.startCaptured(ctx.allocator, .{ .execution_id = execution_id, .command = command, .cwd = cwd, .environment = environment, .authority = authority, .max_output_bytes = ctx.max_command_output_bytes, .timeout_ms = if (input.timeout_ms) |value| std.math.cast(usize, value) orelse return unavailable(ctx) else ctx.command_timeout_ms, .command_artifact_dir = ctx.command_artifact_dir, .replay_capability = ctx.session_child_capability, .output_chunk_lifecycle_id = ctx.output_chunk_lifecycle_id, .output_chunk_ctx = ctx.output_chunk_ctx, .on_output_chunk = ctx.on_output_chunk, .yield_time_ms = input.yield_time_ms, .cancel_flag = ctx.cancel_flag, }) catch |err| { if (err == error.Cancelled and ctx.cancel_flag != null and ctx.cancel_flag.?.load(.seq_cst)) { return error.Cancelled; } return runtimeFailure(ctx, err); }; defer prepared.deinit(ctx.allocator); return finishPrepared(ctx, runtime, &prepared, .command);}
fn callInteract( ctx: tool_dispatch.DispatchContext, input: Input,) tool_dispatch.DispatchError!tool_dispatch.ToolResult { const runtime = ctx.managed_executions orelse return unavailable(ctx); const session_id = input.session_id orelse return unavailable(ctx); ensureOwnedTtyIndexed(ctx, runtime, session_id) catch |err| return runtimeFailure(ctx, err); const chars = input.chars orelse ""; const yield_time_ms = effective_interact_yield_time(chars.len != 0, input.yield_time_ms); if (runtime.isTombstone(session_id)) { if (chars.len != 0) return runtimeFailure(ctx, error.ExecutionTerminal); if (runtime.retainedTerminalSnapshot(ctx.allocator, session_id) catch |err| return runtimeFailure(ctx, err)) |retained| { var prepared = retained; defer prepared.deinit(ctx.allocator); return finishPrepared(ctx, runtime, &prepared, .command); } } if (runtime.backendFor(session_id) == .tty) { return callTtyInteract(ctx, input, yield_time_ms); } if (chars.len != 0) return runtimeFailure(ctx, error.InvalidBackend); var prepared = runtime.wait( ctx.allocator, session_id, yield_time_ms, ctx.cancel_flag, ) catch |err| return runtimeFailure(ctx, err); defer prepared.deinit(ctx.allocator); return finishPrepared(ctx, runtime, &prepared, .command);}
fn callStop( ctx: tool_dispatch.DispatchContext, input: Input,) tool_dispatch.DispatchError!tool_dispatch.ToolResult { const runtime = ctx.managed_executions orelse return unavailable(ctx); const session_id = input.session_id orelse return unavailable(ctx); ensureOwnedTtyIndexed(ctx, runtime, session_id) catch |err| return runtimeFailure(ctx, err); if (runtime.isTombstone(session_id)) { if (runtime.retainedTerminalSnapshot(ctx.allocator, session_id) catch |err| return runtimeFailure(ctx, err)) |retained| { var prepared = retained; defer prepared.deinit(ctx.allocator); return finishPrepared(ctx, runtime, &prepared, .stop); } } if (runtime.backendFor(session_id) == .tty) { if (runtime.stateFor(session_id)) |state| { if (state != .running) { return finishTerminalTtyStop(ctx, runtime, session_id, state); } } refreshTtyExecution(ctx, runtime, session_id, "") catch |err| return runtimeFailure(ctx, err); if (runtime.stateFor(session_id)) |state| { if (state != .running) { return finishTerminalTtyStop(ctx, runtime, session_id, state); } } return callTtyStop(ctx, input); } var prepared = runtime.stop( ctx.allocator, session_id, input.force, ) catch |err| return runtimeFailure(ctx, err); defer prepared.deinit(ctx.allocator); return finishPrepared(ctx, runtime, &prepared, .stop);}
const ParsedTerminalExecution = struct { result: terminal_contracts.OwnedResult,
fn deinit(self: *ParsedTerminalExecution, alloc: Allocator) void { self.result.deinit(alloc); self.* = undefined; }};
fn callTtyRun( ctx: tool_dispatch.DispatchContext, input: Input,) tool_dispatch.DispatchError!tool_dispatch.ToolResult { const runtime = ctx.managed_executions orelse return unavailable(ctx); const owner = ctx.session_child_capability orelse return unavailable(ctx); const durable_session_id = ctx.terminal_owner_session_id orelse return unavailable(ctx); const command = input.command orelse return unavailable(ctx); const cwd = resolveCwd(ctx.allocator, ctx, input.cwd) catch |err| { if (err == error.OutOfMemory) return error.OutOfMemory; return runtimeFailure(ctx, err); }; defer ctx.allocator.free(@constCast(cwd)); var shell_arena_state = std.heap.ArenaAllocator.init(ctx.allocator); defer mem_utils.deinit_arena(shell_arena_state); var login_shell_buffer: [4096]u8 = undefined; const configured = shell_resolver.configuredLoginShellInto(&login_shell_buffer); const shell = ttyShell(shell_arena_state.allocator(), input, configured) catch |err| return runtimeFailure(ctx, err); const environment = shell_resolver.environmentForShellSpec( shell_arena_state.allocator(), configured, shell, ) catch |err| return runtimeFailure(ctx, err); requireTtyShellAuthority(ctx, .{ .command = command, .resolved_cwd = cwd, .target_os = builtin.os.tag, .environment = environment, .execution_mode = .tty, }) catch |err| return runtimeFailure(ctx, err); runtime.reserveTtyCapacity() catch |err| return runtimeFailure(ctx, err); var capacity_reserved = true; defer if (capacity_reserved) runtime.releaseTtyCapacity(); var profile_user_buffer: [64]u8 = undefined; const profile_user = terminal_identity.profileUser(&profile_user_buffer) orelse return unavailable(ctx); var persistence = terminal_operation.prepareStartPersistence(ctx.allocator, .{ .profile_user = profile_user, .durable_session_id = durable_session_id, .workspace_root = ctx.workspace_root, .cwd = cwd, .transport_role = ctx.terminal_transport_role, .backend = .native, .actor = .agent, .controls = .full(), .lifetime = .session, }) catch |err| return runtimeFailure(ctx, err); defer persistence.deinit(); const request = terminal_contracts.ActionRequest{ .start = .{ .cwd = cwd, .command = command, .shell = shell, .backend = .native, .return_when = if (input.yield_time_ms == 0) .started else .exit, .wait_ceiling_ms = @max(@as(u64, 1), input.yield_time_ms), .timeout_ms = input.timeout_ms, .persistence = persistence.view(), } }; var executed = executeTerminal(ctx, request) catch |err| return runtimeFailure(ctx, err); defer executed.deinit(ctx.allocator); const started = switch (executed.result.view()) { .failure => return cloneTerminalFailure(ctx, executed.result.view()), .success => |success| switch (success) { .start => |value| value, else => return runtimeFailure(ctx, error.InvalidTerminalResult), }, }; var session_owned = true; defer if (session_owned) closeTtyBestEffort(ctx, started.session.session_id); const initial_state = terminal_managed_observer.snapshotState(started.session, started.outcome); var observed = terminal_managed_observer.observe( ttyObserverContext(ctx, runtime) orelse return unavailable(ctx), started.session.session_id, initial_state, null, ) catch |err| return runtimeFailure(ctx, err); defer observed.deinit(ctx.allocator); finalizeCompletedTty(ctx, started.session.session_id, observed.state) catch |err| return runtimeFailure(ctx, err); var prepared = runtime.registerTty(ctx.allocator, .{ .execution_id = started.session.session_id, .command = command, .cwd = cwd, .state = observed.state, .output = observed.output, .replay_output = observed.replay_output, .next_cursor = observed.next_cursor, .output_incomplete = observed.output_incomplete, .error_name = if (observed.timed_out) "TimeoutExpired" else null, .max_output_bytes = ctx.max_command_output_bytes, .published_running = observed.state == .running, .capacity_reserved = true, .replay_capability = ctx.session_child_capability, }) catch |err| return runtimeFailure(ctx, err); capacity_reserved = false; session_owned = false; defer prepared.deinit(ctx.allocator); _ = owner; return finishPrepared(ctx, runtime, &prepared, .command);}
fn callTtyInteract( ctx: tool_dispatch.DispatchContext, input: Input, yield_time_ms: u32,) tool_dispatch.DispatchError!tool_dispatch.ToolResult { const runtime = ctx.managed_executions orelse return unavailable(ctx); const session_id = input.session_id orelse return unavailable(ctx); if (runtime.isTombstone(session_id)) { return runtimeFailure(ctx, error.ExecutionTerminal); } var state: managed_execution.SnapshotState = .running; var accepted_bytes: ?u32 = null; const chars = input.chars orelse ""; if (chars.len != 0) { if (runtime.backendFor(session_id) != .tty) { return runtimeFailure(ctx, error.InvalidBackend); } var ready = executeAuthorizedTerminal(ctx, session_id, .{ .wait = .{ .session_id = session_id, .return_when = .started, .safety_ceiling_ms = 20_000, .authority = null, } }) catch |err| return runtimeFailure(ctx, err); defer ready.deinit(ctx.allocator); const ready_result = switch (ready.result.view()) { .failure => return cloneTerminalFailure(ctx, ready.result.view()), .success => |success| switch (success) { .wait => |value| value, else => return runtimeFailure(ctx, error.InvalidTerminalResult), }, }; if (ready_result.session.lifecycle != .running) { return runtimeFailure(ctx, error.TerminalNotReady); }
var acquired = executeAuthorizedTerminal(ctx, session_id, .{ .write = .{ .session_id = session_id, .lease = .acquire, .authority = null, } }) catch |err| return runtimeFailure(ctx, err); defer acquired.deinit(ctx.allocator); switch (acquired.result.view()) { .failure => return cloneTerminalFailure(ctx, acquired.result.view()), .success => |success| switch (success) { .write => {}, else => return runtimeFailure(ctx, error.InvalidTerminalResult), }, } var release_needed = true; defer if (release_needed) releaseTtyLease(ctx, session_id);
var used = executeAuthorizedTerminal(ctx, session_id, .{ .write = .{ .session_id = session_id, .payload = .{ .text = chars }, .lease = .use, .authority = null, } }) catch |err| return runtimeFailure(ctx, err); defer used.deinit(ctx.allocator); accepted_bytes = switch (used.result.view()) { .failure => return cloneTerminalFailure(ctx, used.result.view()), .success => |success| switch (success) { .write => |value| value.accepted_bytes, else => return runtimeFailure(ctx, error.InvalidTerminalResult), }, };
var released = executeAuthorizedTerminal(ctx, session_id, .{ .write = .{ .session_id = session_id, .lease = .release, .authority = null, } }) catch |err| return runtimeFailure(ctx, err); defer released.deinit(ctx.allocator); const facts = switch (released.result.view()) { .failure => return cloneTerminalFailure(ctx, released.result.view()), .success => |success| switch (success) { .write => |value| value.session, else => return runtimeFailure(ctx, error.InvalidTerminalResult), }, }; release_needed = false; state = terminal_managed_observer.snapshotState(facts, null); } const waiter_id = runtime.reserveExternalWait(session_id) catch |err| return runtimeFailure(ctx, err); defer runtime.releaseExternalWait(session_id, waiter_id); if (state == .running and yield_time_ms != 0) { var waited = executeAuthorizedTerminal(ctx, session_id, .{ .wait = .{ .session_id = session_id, .return_when = .exit, .safety_ceiling_ms = yield_time_ms, .authority = null, } }) catch |err| return runtimeFailure(ctx, err); defer waited.deinit(ctx.allocator); const result = switch (waited.result.view()) { .failure => return cloneTerminalFailure(ctx, waited.result.view()), .success => |success| switch (success) { .wait => |value| value, else => return runtimeFailure(ctx, error.InvalidTerminalResult), }, }; state = terminal_managed_observer.snapshotState(result.session, result.outcome); } if (accepted_bytes != null and yield_time_ms == 0) { io_mod.sleep(100 * std.time.ns_per_ms); } var observed = terminal_managed_observer.observe( ttyObserverContext(ctx, runtime) orelse return unavailable(ctx), session_id, state, runtime.ttyCursorFor(session_id), ) catch |err| return runtimeFailure(ctx, err); defer observed.deinit(ctx.allocator); if (runtime.externalWaitPreempted(session_id, waiter_id)) { return runtimeFailure(ctx, error.WaitPreempted); } finalizeCompletedTty(ctx, session_id, observed.state) catch |err| return runtimeFailure(ctx, err); var prepared = runtime.updateTty(ctx.allocator, .{ .execution_id = session_id, .command = "", .state = observed.state, .output = observed.output, .replay_output = observed.replay_output, .next_cursor = observed.next_cursor, .output_incomplete = observed.output_incomplete, .error_name = if (observed.timed_out) "TimeoutExpired" else null, .max_output_bytes = ctx.max_command_output_bytes, .published_running = true, }) catch |err| return runtimeFailure(ctx, err); defer prepared.deinit(ctx.allocator); return if (accepted_bytes) |count| finishPreparedWithAccepted(ctx, runtime, &prepared, count) else finishPrepared(ctx, runtime, &prepared, .command);}
fn callTtyStop( ctx: tool_dispatch.DispatchContext, input: Input,) tool_dispatch.DispatchError!tool_dispatch.ToolResult { const runtime = ctx.managed_executions orelse return unavailable(ctx); const session_id = input.session_id orelse return unavailable(ctx); runtime.preemptWait(session_id); var signaled = executeAuthorizedTerminal(ctx, session_id, .{ .signal = .{ .session_id = session_id, .signal = if (input.force) .kill else .terminate, .authority = null, } }) catch |err| return runtimeFailure(ctx, err); defer signaled.deinit(ctx.allocator); switch (signaled.result.view()) { .failure => return cloneTerminalFailure(ctx, signaled.result.view()), .success => |success| switch (success) { .signal => {}, else => return runtimeFailure(ctx, error.InvalidTerminalResult), }, }
var stopped_status: ?command_contract.CommandStatus = null; var waited = executeAuthorizedTerminal(ctx, session_id, .{ .wait = .{ .session_id = session_id, .return_when = .exit, .safety_ceiling_ms = 2_000, .authority = null, } }) catch |err| return runtimeFailure(ctx, err); defer waited.deinit(ctx.allocator); const wait_result = switch (waited.result.view()) { .failure => return cloneTerminalFailure(ctx, waited.result.view()), .success => |success| switch (success) { .wait => |value| value, else => return runtimeFailure(ctx, error.InvalidTerminalResult), }, }; stopped_status = statusFromOutcome(wait_result.outcome); var observed = terminal_managed_observer.observe( ttyObserverContext(ctx, runtime) orelse return unavailable(ctx), session_id, terminal_managed_observer.snapshotState(wait_result.session, wait_result.outcome), runtime.ttyCursorFor(session_id), ) catch |err| return runtimeFailure(ctx, err); defer observed.deinit(ctx.allocator);
var closed = executeAuthorizedTerminal(ctx, session_id, .{ .close = .{ .session_id = session_id, .policy = if (input.force) .force else .graceful, .authority = null, } }) catch |err| return runtimeFailure(ctx, err); defer closed.deinit(ctx.allocator); switch (closed.result.view()) { .failure => return cloneTerminalFailure(ctx, closed.result.view()), .success => |success| switch (success) { .close => {}, else => return runtimeFailure(ctx, error.InvalidTerminalResult), }, } var prepared = runtime.updateTty(ctx.allocator, .{ .execution_id = session_id, .command = "", .state = .{ .stopped = stopped_status }, .output = observed.output, .replay_output = observed.replay_output, .next_cursor = observed.next_cursor, .output_incomplete = observed.output_incomplete, .error_name = if (observed.timed_out) "TimeoutExpired" else null, .max_output_bytes = ctx.max_command_output_bytes, .published_running = true, }) catch |err| return runtimeFailure(ctx, err); defer prepared.deinit(ctx.allocator); return finishPrepared(ctx, runtime, &prepared, .stop);}
fn ttyShell( alloc: Allocator, input: Input, configured_login_shell: ?[]const u8,) !terminal_contracts.ShellSpec { if (input.shell) |shell| return .{ .executable = .{ .path = shell.path, .clean_start = shell.clean_start, } }; return shell_resolver.profileShell( alloc, configured_login_shell, input.profile orelse .user, );}
fn requireTtyShellAuthority( ctx: tool_dispatch.DispatchContext, command_ctx: command_admission.CommandContext,) !void { const execution_authority = ctx.execution_authority orelse return error.CommandAuthorityContextMismatch; const command_authority = switch (execution_authority) { .run_command => |value| value, .ordinary, .file_mutation, .vision_paths => return error.CommandAuthorityContextMismatch, }; const shell_allowed = switch (command_authority) { .shell_allowed => |value| value, .direct_only => return error.CommandAdmissionChanged, }; if (!shell_allowed.fingerprint.matches(command_ctx)) { return error.CommandAuthorityContextMismatch; }}
fn executeTerminal( ctx: tool_dispatch.DispatchContext, request: terminal_contracts.ActionRequest,) !ParsedTerminalExecution { return .{ .result = try terminal_action_executor.execute(.{ .alloc = ctx.allocator, .lifecycle_allocator = ctx.lifecycle_allocator, .runtime = ctx.terminal_client orelse return error.TerminalUnavailable, .cancel_flag = ctx.cancel_flag, }, request) };}
fn executeAuthorizedTerminal( ctx: tool_dispatch.DispatchContext, session_id: []const u8, request: terminal_contracts.ActionRequest,) !ParsedTerminalExecution { var authority = try reloadTerminalAuthority(ctx, session_id); defer authority.deinit(); const authorized: terminal_contracts.ActionRequest = switch (request) { .read => |value| .{ .read = blk: { var owned = value; owned.authority = authority.view(); break :blk owned; } }, .write => |value| .{ .write = blk: { var owned = value; owned.authority = authority.view(); break :blk owned; } }, .wait => |value| .{ .wait = blk: { var owned = value; owned.authority = authority.view(); break :blk owned; } }, .signal => |value| .{ .signal = blk: { var owned = value; owned.authority = authority.view(); break :blk owned; } }, .close => |value| .{ .close = blk: { var owned = value; owned.authority = authority.view(); break :blk owned; } }, .screen => |value| .{ .screen = blk: { var owned = value; owned.authority = authority.view(); break :blk owned; } }, .start, .inspect, .list, .resize => return error.InvalidTerminalRequest, }; return executeTerminal(ctx, authorized);}
fn reloadTerminalAuthority( ctx: tool_dispatch.DispatchContext, session_id: []const u8,) !terminal_operation.OwnedAuthorityClaim { const owner = ctx.session_child_capability orelse return error.TerminalAuthorityUnavailable; const durable_session_id = ctx.terminal_owner_session_id orelse return error.TerminalAuthorityUnavailable; var profile_user_buffer: [64]u8 = undefined; const profile_user = terminal_identity.profileUser(&profile_user_buffer) orelse return error.TerminalAuthorityUnavailable; return terminal_store.reloadOwnerAuthorityClaim(ctx.allocator, owner, .{ .terminal_session_id = session_id, .profile_user = profile_user, .durable_session_id = durable_session_id, .workspace_root = ctx.workspace_root, .transport_role = ctx.terminal_transport_role, .actor = .agent, });}
fn cloneTerminalFailure( ctx: tool_dispatch.DispatchContext, result: terminal_contracts.Result,) tool_dispatch.DispatchError!tool_dispatch.ToolResult { if (result == .success) return runtimeFailure(ctx, error.InvalidTerminalResult); var out: std.Io.Writer.Allocating = .init(ctx.allocator); errdefer out.deinit(); std.json.Stringify.value(result, .{}, &out.writer) catch return error.OutOfMemory; return .{ .failure = try out.toOwnedSlice() };}
fn statusFromOutcome( outcome: terminal_contracts.ReturnOutcome,) ?command_contract.CommandStatus { return switch (outcome) { .exited => |code| .{ .exit_code = code }, .signal => |signal| .{ .signal = signal }, .started, .condition_met, .safety_ceiling, .cancelled => null, };}
fn releaseTtyLease( ctx: tool_dispatch.DispatchContext, session_id: []const u8,) void { var released = executeAuthorizedTerminal(ctx, session_id, .{ .write = .{ .session_id = session_id, .lease = .release, .authority = null, } }) catch |err| { debug_trace.logf( "shell", "TTY write lease release failed session_id={s} err={s}", .{ session_id, @errorName(err) }, ); return; }; released.deinit(ctx.allocator);}
pub fn releaseAgentWriteLease( ctx: tool_dispatch.DispatchContext, session_id: []const u8,) !void { var released = try executeAuthorizedTerminal(ctx, session_id, .{ .write = .{ .session_id = session_id, .lease = .release, .authority = null, } }); defer released.deinit(ctx.allocator); switch (released.result.view()) { .success => |success| switch (success) { .write => {}, else => return error.InvalidTerminalLeaseCleanupResult, }, .failure => |failure| switch (failure.code) { .session_not_found, .lease_conflict => {}, else => return error.TerminalLeaseCleanupFailed, }, }}
fn finalizeCompletedTty( ctx: tool_dispatch.DispatchContext, session_id: []const u8, state: managed_execution.SnapshotState,) !void { switch (state) { .completed => {}, .running, .stopped, .lost => return, } var closed = try executeAuthorizedTerminal(ctx, session_id, .{ .close = .{ .session_id = session_id, .policy = .graceful, .authority = null, } }); defer closed.deinit(ctx.allocator); switch (closed.result.view()) { .failure => return error.TerminalCloseFailed, .success => |success| switch (success) { .close => {}, else => return error.InvalidTerminalResult, }, }}
fn closeTtyBestEffort( ctx: tool_dispatch.DispatchContext, session_id: []const u8,) void { var closed = executeAuthorizedTerminal(ctx, session_id, .{ .close = .{ .session_id = session_id, .policy = .force, .authority = null, } }) catch |err| { debug_trace.logf( "shell", "unpublished TTY cleanup failed session_id={s} err={s}", .{ session_id, @errorName(err) }, ); return; }; closed.deinit(ctx.allocator);}
fn finishPreparedWithAccepted( ctx: tool_dispatch.DispatchContext, runtime: *managed_execution.Runtime, prepared: *managed_execution.PreparedSnapshot, accepted_bytes: u32,) tool_dispatch.DispatchError!tool_dispatch.ToolResult { const body = formatSnapshotWithLimit( ctx.allocator, prepared.snapshot, accepted_bytes, ctx.max_tool_result_bytes, ) catch |err| { runtime.cancelDelivery( prepared.snapshot.execution_id, prepared.reservation_id, ) catch {}; if (err == error.OutOfMemory) return error.OutOfMemory; return runtimeFailure(ctx, err); }; errdefer ctx.allocator.free(body); publishSnapshotMetadata(ctx, prepared.snapshot) catch |err| { runtime.cancelDelivery( prepared.snapshot.execution_id, prepared.reservation_id, ) catch {}; if (err == error.OutOfMemory) return error.OutOfMemory; return runtimeFailure(ctx, err); }; handoffPreparedDelivery(ctx, runtime, prepared.reservation_id) catch return runtimeFailure(ctx, error.ResultCommitFailed); return .{ .success = body };}
fn ensureOwnedTtyIndexed( ctx: tool_dispatch.DispatchContext, runtime: *managed_execution.Runtime, session_id: []const u8,) !void { if (runtime.stateFor(session_id) != null) return; const observer = ttyObserverContext(ctx, runtime) orelse return; try terminal_managed_observer.syncOwned(observer);}
fn refreshTtyExecution( ctx: tool_dispatch.DispatchContext, runtime: *managed_execution.Runtime, session_id: []const u8, command: []const u8,) !void { return terminal_managed_observer.refresh( ttyObserverContext(ctx, runtime) orelse return error.TerminalAuthorityUnavailable, session_id, command, );}
fn ttyObserverContext( ctx: tool_dispatch.DispatchContext, runtime: *managed_execution.Runtime,) ?terminal_managed_observer.Context { return .{ .alloc = ctx.allocator, .lifecycle_allocator = ctx.lifecycle_allocator, .terminal_client = ctx.terminal_client orelse return null, .managed_runtime = runtime, .owner = ctx.session_child_capability orelse return null, .durable_session_id = ctx.terminal_owner_session_id orelse return null, .workspace_root = ctx.workspace_root, .transport_role = ctx.terminal_transport_role, .max_output_bytes = ctx.max_command_output_bytes, .cancel_flag = ctx.cancel_flag, };}
fn finishTerminalTtyStop( ctx: tool_dispatch.DispatchContext, runtime: *managed_execution.Runtime, session_id: []const u8, state: managed_execution.SnapshotState,) tool_dispatch.DispatchError!tool_dispatch.ToolResult { finalizeCompletedTty(ctx, session_id, state) catch |err| return runtimeFailure(ctx, err); var prepared = runtime.updateTty(ctx.allocator, .{ .execution_id = session_id, .command = "", .state = state, .max_output_bytes = ctx.max_command_output_bytes, .published_running = true, }) catch |err| return runtimeFailure(ctx, err); defer prepared.deinit(ctx.allocator); return finishPrepared(ctx, runtime, &prepared, .stop);}
fn finishPrepared( ctx: tool_dispatch.DispatchContext, runtime: *managed_execution.Runtime, prepared: *managed_execution.PreparedSnapshot, action: enum { command, stop },) tool_dispatch.DispatchError!tool_dispatch.ToolResult { const body = formatSnapshotWithLimit( ctx.allocator, prepared.snapshot, null, ctx.max_tool_result_bytes, ) catch |err| { runtime.cancelDelivery( prepared.snapshot.execution_id, prepared.reservation_id, ) catch {}; if (err == error.OutOfMemory) return error.OutOfMemory; return .{ .failure = try ctx.allocator.dupe(u8, "shell result is unavailable") }; }; errdefer ctx.allocator.free(body); publishSnapshotMetadata(ctx, prepared.snapshot) catch |err| { runtime.cancelDelivery( prepared.snapshot.execution_id, prepared.reservation_id, ) catch {}; if (err == error.OutOfMemory) return error.OutOfMemory; return runtimeFailure(ctx, err); }; handoffPreparedDelivery(ctx, runtime, prepared.reservation_id) catch { return .{ .failure = try ctx.allocator.dupe(u8, "shell result commit failed") }; }; const failed = switch (action) { .command => snapshotFailed(prepared.snapshot), .stop => stop_result_failed(prepared.snapshot.state), }; return if (failed) .{ .failure = body } else .{ .success = body };}
fn publishSnapshotMetadata( ctx: tool_dispatch.DispatchContext, snapshot: managed_execution.Snapshot,) !void { if (ctx.command_result_json_sink == null and ctx.tool_result_memory_sink == null) return; const status: ?command_contract.CommandStatus = switch (snapshot.state) { .completed => |value| value, .stopped => |value| value, .lost => .indeterminate, .running => return, }; const projection: command_contract.StatusProjection = if (status) |value| command_contract.projectStatus(value) else .{ .exit_code = null, .signal = null, .termination_indeterminate = false, }; const timed_out = if (snapshot.error_name) |name| std.mem.eql(u8, name, "TimeoutExpired") else false; var memory = types.ToolResultMemory{ .output_bytes = snapshot.stdout_bytes +| snapshot.stderr_bytes, .stored_output_bytes = snapshot.stdout_bytes +| snapshot.stderr_bytes, .truncated = snapshot.output_truncated, }; if (ctx.tool_result_memory_sink != null) { if (snapshot.output_file) |handle| { memory.command_output_replay = .{ .available = .{ .handle = try ctx.allocator.dupe(u8, handle), .framed_bytes = snapshot.output_framed_bytes, } }; } } errdefer if (memory.command_output_replay) |replay| switch (replay) { .available => |descriptor| ctx.allocator.free(@constCast(descriptor.handle)), .unavailable => {}, }; const completed = switch (snapshot.state) { .completed => true, .running, .stopped, .lost => false, }; if (timed_out) { memory.command_process_presentation = .timed_out; } else if (completed) { if (projection.signal) |signal| { memory.command_process_presentation = .{ .signal = signal }; } else if (projection.exit_code) |exit_code| { if (exit_code != 0) { memory.command_process_presentation = .{ .exit_code = exit_code }; } } } if (ctx.command_result_json_sink != null) { const command_result = command_contract.CommandResult{ .command = snapshot.command, .cwd = snapshot.cwd, .exit_code = projection.exit_code, .signal = projection.signal, .timed_out = timed_out, .termination_indeterminate = projection.termination_indeterminate, .output_incomplete = snapshot.output_incomplete, .duration_ms = snapshot.duration_ms, .stdout_bytes = snapshot.stdout_bytes, .stderr_bytes = snapshot.stderr_bytes, .truncated = snapshot.output_truncated, .output_file = snapshot.output_file, }; const command_result_json = try command_result.toJson(ctx.allocator); tool_dispatch.reportCommandResultJson(ctx, command_result_json); } if (ctx.tool_result_memory_sink != null) { tool_dispatch.reportToolResultMemory(ctx, memory); }}
fn handoffPreparedDelivery( ctx: tool_dispatch.DispatchContext, runtime: *managed_execution.Runtime, reservation_id: u64,) !void { if (ctx.result_commit_sink == null) { return runtime.commitReservation(reservation_id); } tool_dispatch.reportResultCommit(ctx, result_commit.Token{ .context = runtime, .identity = reservation_id, .commit_fn = commitManagedDelivery, .cancel_fn = cancelManagedDelivery, });}
fn commitManagedDelivery(raw: *anyopaque, reservation_id: u64) !void { const runtime: *managed_execution.Runtime = @ptrCast(@alignCast(raw)); return runtime.commitReservation(reservation_id);}
fn cancelManagedDelivery(raw: *anyopaque, reservation_id: u64) void { const runtime: *managed_execution.Runtime = @ptrCast(@alignCast(raw)); runtime.cancelReservation(reservation_id) catch |err| { debug_trace.logf( "shell", "managed delivery cancellation failed reservation={d} err={s}", .{ reservation_id, @errorName(err) }, ); };}
fn formatSnapshot( alloc: Allocator, snapshot: managed_execution.Snapshot, accepted_bytes: ?u32,) ![]u8 { return formatSnapshotWithLimit( alloc, snapshot, accepted_bytes, tool_result_limits.default_max_tool_result_bytes, );}
fn formatSnapshotWithLimit( alloc: Allocator, snapshot: managed_execution.Snapshot, accepted_bytes: ?u32, max_bytes: usize,) ![]u8 { const inline_max_bytes = @min( max_bytes, result_store.large_result_threshold_bytes, ); if (try formatModelSafeSnapshotRaw( alloc, snapshot, accepted_bytes, snapshot.output_delta, snapshot.output_truncated, inline_max_bytes, )) |full| { if (full.len <= inline_max_bytes) return full; alloc.free(full); }
var minimum: usize = 0; var maximum: usize = @min(snapshot.output_delta.len, inline_max_bytes); var best: ?[]u8 = null; errdefer if (best) |value| alloc.free(value); while (minimum <= maximum) { const content_budget = minimum + (maximum - minimum) / 2; const marker = "\n... bytes omitted; use full_output_handle for exact output ...\n"; var projected_writer: std.Io.Writer.Allocating = .init(alloc); defer projected_writer.deinit(); try text_utils.writeHeadTailBounded( &projected_writer.writer, snapshot.output_delta, content_budget, marker, .up, ); const projected = try projected_writer.toOwnedSlice(); defer alloc.free(projected); const candidate = (try formatModelSafeSnapshotRaw( alloc, snapshot, accepted_bytes, projected, true, inline_max_bytes, )) orelse { if (content_budget == 0) break; maximum = content_budget - 1; continue; }; if (candidate.len <= inline_max_bytes) { if (best) |value| alloc.free(value); best = candidate; minimum = content_budget + 1; } else { alloc.free(candidate); if (content_budget == 0) break; maximum = content_budget - 1; } } if (best) |value| return value; return formatSnapshotRaw( alloc, snapshot, accepted_bytes, "", true, );}
fn formatModelSafeSnapshotRaw( alloc: Allocator, snapshot: managed_execution.Snapshot, accepted_bytes: ?u32, output_delta: []const u8, output_truncated: bool, max_encoded_bytes: usize,) !?[]u8 { if (text_utils.isModelSafeText(output_delta)) { return try formatSnapshotRaw( alloc, snapshot, accepted_bytes, output_delta, output_truncated, ); } var encoded = try text_utils.encodeTerminalSafe( alloc, output_delta, max_encoded_bytes, ); defer encoded.deinit(alloc); if (encoded.truncated) return null; return try formatSnapshotRaw( alloc, snapshot, accepted_bytes, encoded.bytes, output_truncated, );}
const shell_parse_retry_guidance = "The shell could not parse this command (unmatched quote or syntax error), so nothing executed. Rewrite the command with corrected quoting or escaping and submit the full corrected command instead of rerunning the same text.";const usage_error_retry_guidance = "The command exited with a usage error (missing or invalid arguments). Rebuild the command with the required arguments explicitly set, then submit the corrected command instead of rerunning it unchanged.";
fn failureRetryGuidance(exit_code: ?i64, output: []const u8) ?[]const u8 { const code = exit_code orelse return null; if (code == 0) return null; if (isShellParseErrorOutput(output)) return shell_parse_retry_guidance; if (hasUsageBannerOutput(output)) return usage_error_retry_guidance; return null;}
fn isShellParseErrorOutput(output: []const u8) bool { var lines = std.mem.splitScalar(u8, output, '\n'); while (lines.next()) |line| { const trimmed = std.mem.trim(u8, line, " \t\r"); if (!hasShellErrorPrefix(trimmed)) continue; if (text_utils.containsIgnoreCase(trimmed, "unmatched") or text_utils.containsIgnoreCase(trimmed, "syntax error") or text_utils.containsIgnoreCase(trimmed, "parse error") or text_utils.containsIgnoreCase(trimmed, "unterminated quoted string")) return true; } return false;}
// Shell error prefixes may carry an absolute argv[0] path (fx spawns the// resolved login shell as /bin/bash, /bin/zsh, ...), so match the basename// of the token before the first colon rather than a raw line prefix.fn hasShellErrorPrefix(line: []const u8) bool { const colon = std.mem.findScalar(u8, line, ':') orelse return false; const token = line[0..colon]; if (token.len == 0 or std.mem.findScalar(u8, token, ' ') != null) return false; const base = if (std.mem.findLast(u8, token, "/")) |slash| token[slash + 1 ..] else token; const names = [_][]const u8{ "zsh", "bash", "sh", "dash" }; for (names) |name| { if (std.mem.eql(u8, base, name)) return true; } return false;}
fn hasUsageBannerOutput(output: []const u8) bool { var lines = std.mem.splitScalar(u8, output, '\n'); var scanned: usize = 0; while (lines.next()) |line| { const trimmed = std.mem.trimStart(u8, line, " \t\r"); if (trimmed.len == 0) continue; if (scanned >= 4) return false; scanned += 1; if (std.ascii.startsWithIgnoreCase(trimmed, "usage:")) return true; } return false;}
fn formatSnapshotRaw( alloc: Allocator, snapshot: managed_execution.Snapshot, accepted_bytes: ?u32, output_delta: []const u8, output_truncated: bool,) ![]u8 { const status = switch (snapshot.state) { .completed => |value| value, .stopped => |value| value, .lost => .indeterminate, .running => null, }; const projection: command_contract.StatusProjection = if (status) |value| command_contract.projectStatus(value) else .{ .exit_code = null, .signal = null, .termination_indeterminate = false, }; const retry_guidance: ?[]const u8 = if (snapshot.output_incomplete) "Command output is incomplete. Inspect external state and available output before retrying; do not blindly rerun a command that may have changed state." else switch (snapshot.state) { .lost => "Execution status is indeterminate. Inspect external state before retrying; do not blindly rerun a command that may have changed state.", .completed => failureRetryGuidance(projection.exit_code, snapshot.output_delta), .running, .stopped => null, }; var out: std.Io.Writer.Allocating = .init(alloc); errdefer out.deinit(); try std.json.Stringify.value(.{ .session_id = if (snapshot.retained) snapshot.execution_id else null, .state = snapshotStateName(snapshot.state), .backend = @tagName(snapshot.backend), .persistence = @tagName(snapshot.persistence), .output_truncated = output_truncated, .output_incomplete = snapshot.output_incomplete, .output_terminal_safe = true, .full_output_handle = snapshot.output_file, .exit_code = projection.exit_code, .signal = projection.signal, .termination_indeterminate = projection.termination_indeterminate, .duration_ms = snapshot.duration_ms, .accepted_bytes = accepted_bytes, .@"error" = snapshot.error_name, .retry_guidance = retry_guidance, .output_delta = output_delta, }, .{}, &out.writer); return try out.toOwnedSlice();}
fn snapshotStateName(state: managed_execution.SnapshotState) []const u8 { return switch (state) { .running => "running", .completed => "completed", .stopped => "stopped", .lost => "lost", };}
fn snapshotFailed(snapshot: managed_execution.Snapshot) bool { if (snapshot.output_incomplete) return true; return switch (snapshot.state) { .running => false, .completed => |status| switch (status) { .exit_code => |code| code != 0, .signal, .indeterminate => true, .finished => false, }, .stopped, .lost => true, };}
fn stop_result_failed(state: managed_execution.SnapshotState) bool { return switch (state) { .lost => true, .stopped => |status| if (status) |value| switch (value) { .indeterminate => true, .exit_code, .signal, .finished => false, } else false, .running, .completed => false, };}
test "shell stop fails closed only for lost or indeterminate outcomes" { try std.testing.expect(stop_result_failed(.lost)); try std.testing.expect(stop_result_failed(.{ .stopped = .indeterminate })); try std.testing.expect(!stop_result_failed(.{ .stopped = .{ .signal = 9 } })); try std.testing.expect(!stop_result_failed(.{ .completed = .{ .exit_code = 0 } }));}
fn runtimeFailure( ctx: tool_dispatch.DispatchContext, err: anyerror,) tool_dispatch.DispatchError!tool_dispatch.ToolResult { if (err == error.OutOfMemory) return error.OutOfMemory; return .{ .failure = try std.fmt.allocPrint( ctx.allocator, "{{\"error\":{{\"tool\":\"shell\",\"code\":\"{s}\",\"retryable\":false}}}}", .{@errorName(err)}, ) };}
fn unavailable( ctx: tool_dispatch.DispatchContext,) tool_dispatch.DispatchError!tool_dispatch.ToolResult { return .{ .failure = try ctx.allocator.dupe( u8, "{\"error\":{\"tool\":\"shell\",\"code\":\"unavailable\",\"retryable\":false}}", ) };}
fn resolveCwd( arena: Allocator, ctx: tool_dispatch.DispatchContext, requested: ?[]const u8,) ![]const u8 { const scope = ctx.access_scope orelse workspace_access.AccessScope.primaryOnly(ctx.workspace_root); const value = requested orelse return arena.dupe(u8, scope.primary_directory); if (std.mem.eql(u8, value, ".")) { return arena.dupe(u8, scope.primary_directory); } return pathing.resolveWorkspaceOrExternalPath( arena, scope.primary_directory, value, );}
fn commandEnvironment( alloc: Allocator, ctx: tool_dispatch.DispatchContext, profile: ?command_environment.Profile,) !command_environment.Environment { if (ctx.captured_command_host == .workspace_clean) { if (profile != null) return error.InvalidWorkspaceInput; return .workspace_clean; } var login_shell_buffer: [4096]u8 = undefined; const configured = shell_resolver.configuredLoginShellInto(&login_shell_buffer); return shell_resolver.environment(alloc, configured, profile);}
pub fn isCapturedCommand(erased: tool_dispatch.ToolInput) bool { const input = erased.as(OwnedInput).value; return input.action == .run and !input.tty;}
pub fn isProcessLocal(erased: tool_dispatch.ToolInput) bool { const input = erased.as(OwnedInput).value; return switch (input.action) { .run => !input.tty, .interact => input.chars == null or input.chars.?.len == 0, .stop => true, };}
pub fn readsOnly(erased: tool_dispatch.ToolInput) bool { const input = erased.as(OwnedInput).value; return switch (input.action) { .interact => input.chars == null or input.chars.?.len == 0, .run, .stop => false, };}
pub fn presentation(args: std.json.ObjectMap) ?tool_dispatch.CallPresentation { const action = std.meta.stringToEnum( Action, tool_args.optionalStringArg(args, "action") orelse return null, ) orelse return null; return switch (action) { .run => .{ .activity_kind = .command, .action_label = "Running", .completed_action_label = "Ran", .label_arg_kind = .command, .label_arg_default = "command", }, .interact => if (tool_args.optionalStringArg(args, "chars")) |chars| if (chars.len == 0) sessionPresentation("Waiting for", "Observed") else sessionPresentation("Sending input to", "Sent input to") else sessionPresentation("Waiting for", "Observed"), .stop => sessionPresentation("Stopping", "Stopped"), };}
fn sessionPresentation( action_label: []const u8, completed_action_label: []const u8,) tool_dispatch.CallPresentation { return .{ .activity_kind = .command, .action_label = action_label, .completed_action_label = completed_action_label, .label_arg_kind = .session_id, .label_arg_default = "shell execution", };}
pub fn isIrreversible(_: tool_dispatch.ToolInput) bool { return false;}
test "shell action fields are closed and command authority covers every run" { try std.testing.expectEqualSlices( []const u8, &.{ "action", "command", "cwd", "profile", "shell", "tty", "yield_time_ms", "timeout_ms" }, actionFieldContract(.run).allowed, ); try std.testing.expectEqualSlices( []const u8, &.{ "action", "session_id", "chars", "yield_time_ms" }, actionFieldContract(.interact).allowed, );}
test "shell timeout minimum is enforced before correction and execution" { const alloc = std.testing.allocator; const zero = try decode(.{ .allocator = alloc }, "{\"action\":\"run\",\"command\":\"true\",\"timeout_ms\":0}"); switch (zero) { .input => |input| { input.deinit(alloc); return error.TestUnexpectedResult; }, .failure => |failure| { defer alloc.free(failure); var parsed = try std.json.parseFromSlice(std.json.Value, alloc, failure, .{}); defer parsed.deinit(); const detail = parsed.value.object.get("error").?.object; try std.testing.expectEqualStrings("invalid_shell_request", detail.get("code").?.string); try std.testing.expect(!detail.get("executed").?.bool); try std.testing.expect(detail.get("retry_with") == null); }, } for ([_][]const u8{ "{\"action\":\"run\",\"command\":\"true\"}", "{\"action\":\"run\",\"command\":\"true\",\"timeout_ms\":1}", }) |args| { const decoded = try decode(.{ .allocator = alloc }, args); switch (decoded) { .input => |input| input.deinit(alloc), .failure => |failure| { alloc.free(failure); return error.TestUnexpectedResult; }, } }}
test "shell request correction suggests only unambiguous repairs without executing" { const alloc = std.testing.allocator; const cases = [_]struct { input: []const u8, retry: ?[]const u8 }{ .{ .input = "{\"command\":\"sleep 30\",\"timeout_ms\":\"40000\",\"yield_time_ms\":\"30000\"}", .retry = "{\"action\":\"run\",\"command\":\"sleep 30\",\"yield_time_ms\":30000,\"timeout_ms\":40000}" }, .{ .input = "{\"request\":{\"command\":\"sleep 30\"},\"yield_time_ms\":\"30000\"}", .retry = "{\"action\":\"run\",\"command\":\"sleep 30\",\"yield_time_ms\":30000}" }, .{ .input = "{\"action\":\"run\",\"command\":\"true\",\"background\":null}", .retry = "{\"action\":\"run\",\"command\":\"true\"}" }, .{ .input = "{\"request\":\"{\\\"action\\\":\\\"run\\\",\\\"command\\\":\\\"true\\\"}\"}", .retry = "{\"action\":\"run\",\"command\":\"true\"}" }, .{ .input = "{\"action\":\"interact\",\"session_id\":\"shell-3\",\"yield_time_ms\":\"1000\",\"unused\":null}", .retry = "{\"action\":\"interact\",\"yield_time_ms\":1000,\"session_id\":\"shell-3\"}" }, .{ .input = "{\"command\":\"true\",\"tty\":true}", .retry = null }, .{ .input = "{}", .retry = null }, .{ .input = "{\"action\":null,\"command\":\"true\"}", .retry = null }, .{ .input = "{\"session_id\":\"shell-3\"}", .retry = null }, .{ .input = "{\"command\":\"true\",\"session_id\":\"shell-3\"}", .retry = null }, .{ .input = "{\"action\":\"run\",\"command\":\"true\",\"background\":true}", .retry = null }, .{ .input = "{\"action\":\"run\",\"command\":\"true\",\"profile\":\"clean\",\"shell\":{\"kind\":\"executable\",\"path\":\"/bin/bash\"}}", .retry = null }, .{ .input = "{\"action\":\"run\",\"command\":\"true\",\"yield_time_ms\":30001}", .retry = null }, .{ .input = "{\"request\":{\"action\":\"run\",\"command\":\"true\",\"yield_time_ms\":\"1000\",\"timeout_ms\":0}}", .retry = null }, .{ .input = "{\"action\":\"run\",\"command\":\"true\",\"yield_time_ms\":4294967296}", .retry = null }, .{ .input = "{\"action\":\"stop\",\"session_id\":\"shell-3\",\"force\":\"true\"}", .retry = null }, .{ .input = "{\"request\":{\"action\":\"run\",\"command\":\"true\"},\"command\":\"false\"}", .retry = null }, .{ .input = "{", .retry = null }, .{ .input = "[]", .retry = null }, }; for (cases) |case| { const result = try decode(.{ .allocator = alloc }, case.input); const failure = switch (result) { .failure => |failure| failure, .input => |input| { input.deinit(alloc); return error.TestUnexpectedResult; }, }; defer alloc.free(failure); var parsed = try std.json.parseFromSlice(std.json.Value, alloc, failure, .{}); defer parsed.deinit(); const detail = parsed.value.object.get("error").?.object; try std.testing.expectEqualStrings("invalid_shell_request", detail.get("code").?.string); try std.testing.expect(!detail.get("executed").?.bool); try std.testing.expect(detail.get("problems").?.array.items.len > 0); try std.testing.expect((try tool_result_errors.inspectTerminalActionFieldCorrection(alloc, failure)) != null); if (case.retry) |expected| { const request = detail.get("retry_with").?.object.get("request").?; const json = try std.json.Stringify.valueAlloc(alloc, request, .{}); defer alloc.free(json); try std.testing.expectEqualStrings(expected, json); const decoded = try decode(.{ .allocator = alloc }, json); switch (decoded) { .input => |input| input.deinit(alloc), .failure => |reason| { alloc.free(reason); return error.TestUnexpectedResult; }, } } else { try std.testing.expect(detail.get("retry_with") == null); } }}
fn check_request_correction_allocations(alloc: Allocator, args_json: []const u8) !void { const result = try decode(.{ .allocator = alloc }, args_json); switch (result) { .failure => |failure| alloc.free(failure), .input => |input| { input.deinit(alloc); return error.TestUnexpectedResult; }, }}
test "shell request correction releases partial allocations" { try std.testing.checkAllAllocationFailures(std.testing.allocator, check_request_correction_allocations, .{ "{\"request\":{\"command\":\"true\"},\"yield_time_ms\":\"30000\"}", }); try std.testing.checkAllAllocationFailures(std.testing.allocator, check_request_correction_allocations, .{ "{\"command\":\"true\",\"tty\":true,\"shell\":{\"path\":\"/bin/bash\",\"kind\":\"executable\"}}", });}
test "shell request correction canonicalizes nested shell members" { const alloc = std.testing.allocator; const first = try request_correction(alloc, \\{"command":"true","tty":true,"shell":{"kind":"executable","path":"/bin/bash","clean_start":false}} , true); defer alloc.free(first); const reordered = try request_correction(alloc, \\{"shell":{"clean_start":false,"path":"/bin/bash","kind":"executable"},"tty":true,"command":"true"} , true); defer alloc.free(reordered); try std.testing.expectEqualStrings(first, reordered); const omitted = try request_correction(alloc, \\{"command":"true","tty":true,"shell":{"path":"/bin/bash","kind":"executable"}} , true); defer alloc.free(omitted); try std.testing.expect(std.mem.find(u8, omitted, "clean_start") == null);}
test "shell request correction bounds feedback and preserves input bytes" { const alloc = std.testing.allocator; const command = "printf '\u{1f308}\\n'; echo \"$VALUE\""; const key = "x" ** 63 ++ "\u{1f308}"; const source = try std.json.Stringify.valueAlloc(alloc, .{ .command = command, .x = null, }, .{}); defer alloc.free(source); const correction = try request_correction(alloc, source, true); defer alloc.free(correction); var parsed = try std.json.parseFromSlice(std.json.Value, alloc, correction, .{}); defer parsed.deinit(); const request = parsed.value.object.get("error").?.object.get("retry_with").?.object.get("request").?.object; try std.testing.expectEqualStrings(command, request.get("command").?.string);
const long_key = try std.fmt.allocPrint(alloc, "{{\"command\":\"true\",\"{s}\":null}}", .{key}); defer alloc.free(long_key); const bounded = try request_correction(alloc, long_key, true); defer alloc.free(bounded); try std.testing.expect(std.unicode.utf8ValidateSlice(bounded)); const too_large = try alloc.alloc(u8, 16 * 1024 + 1); defer alloc.free(too_large); @memset(too_large, ' '); const failure = try request_correction(alloc, too_large, true); defer alloc.free(failure); try std.testing.expect(failure.len < 512); try std.testing.expect(std.mem.find(u8, failure, "retry_with") == null);
const tty = try request_correction(alloc, "{\"command\":\"true\",\"tty\":true}", true); defer alloc.free(tty); try std.testing.expect(std.mem.find(u8, tty, "retry_with") != null);}
test "shell interact classification follows optional input" { const alloc = std.testing.allocator; const ctx = tool_dispatch.DispatchContext{ .allocator = alloc }; const cases = [_]struct { json: []const u8, reads_only: bool, process_local: bool, }{ .{ .json = "{\"action\":\"interact\",\"session_id\":\"shell-1\"}", .reads_only = true, .process_local = true, }, .{ .json = "{\"action\":\"interact\",\"session_id\":\"shell-1\",\"chars\":\"\"}", .reads_only = true, .process_local = true, }, .{ .json = "{\"action\":\"interact\",\"session_id\":\"shell-1\",\"chars\":\"hello\\n\"}", .reads_only = false, .process_local = false, }, }; for (cases) |case| { const decoded = try decode(ctx, case.json); switch (decoded) { .failure => |failure| { defer alloc.free(failure); return error.TestUnexpectedResult; }, .input => |input| { defer input.deinit(alloc); try std.testing.expectEqual(case.reads_only, readsOnly(input)); try std.testing.expectEqual(case.process_local, isProcessLocal(input)); }, } }
const oversized = try alloc.alloc(u8, terminal_contracts.max_write_bytes + 1); defer alloc.free(oversized); const failure = try validateInteract(ctx, .{ .action = .interact, .session_id = "shell-1", .chars = oversized, }) orelse return error.TestUnexpectedResult; defer alloc.free(failure); try std.testing.expect(std.mem.find(u8, failure, "exceed") != null);}
test "TTY execution requires matching shell authority" { const command_ctx = command_admission.CommandContext{ .command = "pwd", .resolved_cwd = "/workspace", .target_os = builtin.os.tag, .environment = .{ .clean = "/bin/bash" }, .execution_mode = .tty, }; try std.testing.expectError( error.CommandAdmissionChanged, requireTtyShellAuthority(.{ .allocator = std.testing.allocator, .execution_authority = .{ .run_command = .{ .direct_only = .init(command_ctx), } }, }, command_ctx), );
try requireTtyShellAuthority(.{ .allocator = std.testing.allocator, .execution_authority = .{ .run_command = .{ .shell_allowed = .{ .fingerprint = .init(command_ctx), .source = .auto_classifier, } } }, }, command_ctx);
var changed = command_ctx; changed.environment = .{ .user = "/bin/bash" }; try std.testing.expectError( error.CommandAuthorityContextMismatch, requireTtyShellAuthority(.{ .allocator = std.testing.allocator, .execution_authority = .{ .run_command = .{ .shell_allowed = .{ .fingerprint = .init(command_ctx), .source = .auto_classifier, } } }, }, changed), );}
test "shell decoder preserves null omission and rejects cross action fields" { const alloc = std.testing.allocator; const ctx = tool_dispatch.DispatchContext{ .allocator = alloc }; const decoded = try decode( ctx, "{\"action\":\"run\",\"command\":\"true\",\"cwd\":null,\"profile\":null,\"tty\":false,\"yield_time_ms\":0,\"timeout_ms\":null}", ); switch (decoded) { .failure => |failure| { defer alloc.free(failure); return error.TestUnexpectedResult; }, .input => |input| { defer input.deinit(alloc); try std.testing.expect(isCapturedCommand(input)); }, } const invalid = try decode( ctx, "{\"action\":\"interact\",\"session_id\":\"shell-session\",\"command\":\"true\"}", ); switch (invalid) { .input => |input| { defer input.deinit(alloc); return error.TestUnexpectedResult; }, .failure => |failure| { defer alloc.free(failure); try std.testing.expect(std.mem.find(u8, failure, "invalid_shell_request") != null); }, }}
test "shell decoder applies action specific observation defaults" { const alloc = std.testing.allocator; const ctx = tool_dispatch.DispatchContext{ .allocator = alloc }; const run_decoded = try decode(ctx, "{\"action\":\"run\",\"command\":\"true\"}"); switch (run_decoded) { .failure => |failure| { defer alloc.free(failure); return error.TestUnexpectedResult; }, .input => |input| { defer input.deinit(alloc); try std.testing.expectEqual( @as(u32, 30_000), input.as(OwnedInput).value.yield_time_ms, ); }, } const interact_decoded = try decode( ctx, "{\"action\":\"interact\",\"session_id\":\"shell-session\"}", ); switch (interact_decoded) { .failure => |failure| { defer alloc.free(failure); return error.TestUnexpectedResult; }, .input => |input| { defer input.deinit(alloc); try std.testing.expectEqual( @as(u32, 5_000), input.as(OwnedInput).value.yield_time_ms, ); }, }}
test "shell interaction wait bounds empty observations without delaying writes" { const cases = [_]struct { has_input: bool, requested_ms: u32, expected_ms: u32, }{ .{ .has_input = false, .requested_ms = 0, .expected_ms = 5_000 }, .{ .has_input = false, .requested_ms = 1_000, .expected_ms = 5_000 }, .{ .has_input = false, .requested_ms = 5_000, .expected_ms = 5_000 }, .{ .has_input = false, .requested_ms = 45_000, .expected_ms = 45_000 }, .{ .has_input = false, .requested_ms = 300_000, .expected_ms = 300_000 }, .{ .has_input = true, .requested_ms = 0, .expected_ms = 0 }, .{ .has_input = true, .requested_ms = 1_000, .expected_ms = 1_000 }, .{ .has_input = true, .requested_ms = 30_000, .expected_ms = 30_000 }, .{ .has_input = true, .requested_ms = 300_000, .expected_ms = 30_000 }, }; for (cases) |case| { try std.testing.expectEqual( case.expected_ms, effective_interact_yield_time(case.has_input, case.requested_ms), ); }
const failure = try validateInteract( .{ .allocator = std.testing.allocator }, .{ .action = .interact, .session_id = "shell-1", .yield_time_ms = managed_contract.max_wait_ceiling_ms + 1, }, ) orelse return error.TestUnexpectedResult; defer std.testing.allocator.free(failure); try std.testing.expect(std.mem.find(u8, failure, "between 0 and 300000") != null);
const overflow = try decode( .{ .allocator = std.testing.allocator }, "{\"action\":\"interact\",\"session_id\":\"shell-1\",\"yield_time_ms\":4294967296}", ); switch (overflow) { .input => |input| { defer input.deinit(std.testing.allocator); return error.TestUnexpectedResult; }, .failure => |message| std.testing.allocator.free(message), }}
test "shell decoder rejects removed handoff and legacy actions" { const alloc = std.testing.allocator; const ctx = tool_dispatch.DispatchContext{ .allocator = alloc }; const run_decoded = try decode( ctx, "{\"action\":\"run\",\"command\":\"sleep 30\",\"yield_time_ms\":0,\"handoff\":\"next_turn\"}", ); switch (run_decoded) { .failure => |failure| alloc.free(failure), .input => |input| { defer input.deinit(alloc); return error.TestUnexpectedResult; }, }
const wait_decoded = try decode( ctx, "{\"action\":\"wait\",\"session_id\":\"shell-session\"}", ); switch (wait_decoded) { .input => |input| { defer input.deinit(alloc); return error.TestUnexpectedResult; }, .failure => |failure| { defer alloc.free(failure); }, }}
test "stopped execution is a successful shell observation without command failure metadata" { const alloc = std.testing.allocator; const statuses = [_]command_contract.CommandStatus{ .{ .signal = 15 }, .{ .exit_code = 143 }, }; for (statuses) |status| { var memory: ?types.ToolResultMemory = null; try publishSnapshotMetadata(.{ .allocator = alloc, .tool_result_memory_sink = &memory, }, .{ .execution_id = @constCast("shell-stopped"), .command = @constCast("sleep 60"), .cwd = @constCast("/tmp"), .retained = true, .state = .{ .stopped = status }, .output_delta = @constCast(""), .output_truncated = false, }); try std.testing.expect(memory != null); try std.testing.expect(memory.?.command_process_presentation == null); }}
test "lost shell snapshot preserves indeterminate execution guidance" { const alloc = std.testing.allocator; const body = try formatSnapshot(alloc, .{ .execution_id = @constCast("shell-lost"), .command = @constCast("mutating-command"), .cwd = @constCast("/tmp"), .retained = true, .state = .lost, .output_delta = @constCast(""), .output_truncated = false, }, null); defer alloc.free(body);
var parsed = try std.json.parseFromSlice(std.json.Value, alloc, body, .{}); defer parsed.deinit(); const object = parsed.value.object; try std.testing.expect(object.get("termination_indeterminate").?.bool); try std.testing.expect(std.mem.find( u8, object.get("retry_guidance").?.string, "do not blindly rerun", ) != null);}
test "completed shell snapshot reports incomplete output without losing status" { const alloc = std.testing.allocator; const body = try formatSnapshot(alloc, .{ .execution_id = @constCast("shell-incomplete"), .command = @constCast("mutating-command"), .cwd = @constCast("/tmp"), .retained = false, .state = .{ .completed = .{ .exit_code = 0 } }, .output_delta = @constCast("partial output"), .output_truncated = false, .output_incomplete = true, }, null); defer alloc.free(body);
var parsed = try std.json.parseFromSlice(std.json.Value, alloc, body, .{}); defer parsed.deinit(); const object = parsed.value.object; try std.testing.expectEqualStrings("completed", object.get("state").?.string); try std.testing.expectEqual(@as(i64, 0), object.get("exit_code").?.integer); try std.testing.expect(object.get("output_incomplete").?.bool); try std.testing.expect(std.mem.find( u8, object.get("retry_guidance").?.string, "do not blindly rerun", ) != null);}
test "completed shell snapshot with shell parse error carries rewrite guidance" { const alloc = std.testing.allocator; const body = try formatSnapshot(alloc, .{ .execution_id = @constCast("shell-parse-error"), .command = @constCast("grep -rn '"), .cwd = @constCast("/tmp"), .retained = false, .state = .{ .completed = .{ .exit_code = 1 } }, .output_delta = @constCast("zsh:1: unmatched '\n"), .output_truncated = false, }, null); defer alloc.free(body);
var parsed = try std.json.parseFromSlice(std.json.Value, alloc, body, .{}); defer parsed.deinit(); const object = parsed.value.object; try std.testing.expectEqualStrings("completed", object.get("state").?.string); try std.testing.expectEqual(@as(i64, 1), object.get("exit_code").?.integer); try std.testing.expectEqualStrings( shell_parse_retry_guidance, object.get("retry_guidance").?.string, );}
test "completed shell snapshot with usage error carries argument guidance" { const alloc = std.testing.allocator; const body = try formatSnapshot(alloc, .{ .execution_id = @constCast("shell-usage-error"), .command = @constCast("grep -n"), .cwd = @constCast("/tmp"), .retained = false, .state = .{ .completed = .{ .exit_code = 2 } }, .output_delta = @constCast("usage: grep [-abcdDEFGHhIiJLlMmnOopqRSsUVvwXxZz] [-A num] [-B num]\n\t[-e pattern] [-f file] [--binary-files=value]\n"), .output_truncated = false, }, null); defer alloc.free(body);
var parsed = try std.json.parseFromSlice(std.json.Value, alloc, body, .{}); defer parsed.deinit(); const object = parsed.value.object; try std.testing.expectEqual(@as(i64, 2), object.get("exit_code").?.integer); try std.testing.expectEqualStrings( usage_error_retry_guidance, object.get("retry_guidance").?.string, );}
test "completed shell snapshot with ordinary failure has no retry guidance" { const alloc = std.testing.allocator; const body = try formatSnapshot(alloc, .{ .execution_id = @constCast("shell-ordinary-failure"), .command = @constCast("grep needle /nonexistent"), .cwd = @constCast("/tmp"), .retained = false, .state = .{ .completed = .{ .exit_code = 2 } }, .output_delta = @constCast("grep: /nonexistent: No such file or directory\n"), .output_truncated = false, }, null); defer alloc.free(body);
var parsed = try std.json.parseFromSlice(std.json.Value, alloc, body, .{}); defer parsed.deinit(); try std.testing.expect(parsed.value.object.get("retry_guidance").? == .null);}
test "failure guidance detects signatures in raw output when emitted text is encoded" { const alloc = std.testing.allocator; const body = try formatSnapshot(alloc, .{ .execution_id = @constCast("shell-usage-encoded"), .command = @constCast("grep -n"), .cwd = @constCast("/tmp"), .retained = false, .state = .{ .completed = .{ .exit_code = 2 } }, .output_delta = @constCast("usage: grep [-abcdDEFGHhIiJLlMmnOopqRSsUVvwXxZz]\npartial \x00 binary\n"), .output_truncated = false, }, null); defer alloc.free(body);
var parsed = try std.json.parseFromSlice(std.json.Value, alloc, body, .{}); defer parsed.deinit(); const object = parsed.value.object; try std.testing.expectEqualStrings( usage_error_retry_guidance, object.get("retry_guidance").?.string, );}
test "incomplete output guidance takes precedence over failure guidance" { const alloc = std.testing.allocator; const body = try formatSnapshot(alloc, .{ .execution_id = @constCast("shell-incomplete-usage"), .command = @constCast("grep -n"), .cwd = @constCast("/tmp"), .retained = false, .state = .{ .completed = .{ .exit_code = 2 } }, .output_delta = @constCast("usage: grep [-abcdDEFGHhIiJLlMmnOopqRSsUVvwXxZz]\n"), .output_truncated = false, .output_incomplete = true, }, null); defer alloc.free(body);
var parsed = try std.json.parseFromSlice(std.json.Value, alloc, body, .{}); defer parsed.deinit(); try std.testing.expect(std.mem.find( u8, parsed.value.object.get("retry_guidance").?.string, "do not blindly rerun", ) != null);}
test "failure guidance detectors match only their signatures" { try std.testing.expectEqual( shell_parse_retry_guidance, failureRetryGuidance(1, "zsh:1: unmatched '\n"), ); // fx spawns the resolved login shell with an absolute argv[0], so bash // errors carry a /bin/bash prefix; bare-basename forms also occur when a // command spawns a shell subprocess itself. try std.testing.expectEqual( shell_parse_retry_guidance, failureRetryGuidance(2, "/bin/bash: -c: line 1: syntax error: unexpected end of file\n"), ); try std.testing.expectEqual( shell_parse_retry_guidance, failureRetryGuidance(2, "bash: -c: line 1: syntax error near unexpected token `)'\n"), ); try std.testing.expectEqual( shell_parse_retry_guidance, failureRetryGuidance(2, "/bin/dash: 1: Syntax error: Unterminated quoted string\n"), ); try std.testing.expectEqual( shell_parse_retry_guidance, failureRetryGuidance(1, "zsh: parse error near '\\n'\n"), ); try std.testing.expectEqual( usage_error_retry_guidance, failureRetryGuidance(2, "usage: grep [-abcdDEFGHhIiJLlMmnOopqRSsUVvwXxZz]\n"), ); try std.testing.expectEqual( usage_error_retry_guidance, failureRetryGuidance(64, "Usage: ls [-ABCFGHabcdfghiklmnopqrstuvwx1] [file ...]\n"), ); try std.testing.expectEqual( usage_error_retry_guidance, failureRetryGuidance(2, "grep: unknown option\nusage: grep [-abcdDEFGHhIiJLlMmnOopqRSsUVvwXxZz]\n"), );
// Exit zero never carries failure guidance. try std.testing.expectEqual(@as(?[]const u8, null), failureRetryGuidance(0, "usage: grep\n")); try std.testing.expectEqual(@as(?[]const u8, null), failureRetryGuidance(null, "zsh:1: unmatched '\n")); // A compiler-style syntax error without a shell prefix is not a shell parse error. try std.testing.expectEqual(@as(?[]const u8, null), failureRetryGuidance(1, "main.c:4:5: error: expected ';' after expression (syntax error)\n")); // A non-shell token before the colon is not a shell prefix even when a shell name appears later. try std.testing.expectEqual(@as(?[]const u8, null), failureRetryGuidance(1, "error: bash: syntax error\n")); // Ordinary failures keep null guidance. try std.testing.expectEqual(@as(?[]const u8, null), failureRetryGuidance(2, "grep: /nonexistent: No such file or directory\n")); try std.testing.expectEqual(@as(?[]const u8, null), failureRetryGuidance(1, "")); // A usage-looking line deep in output is not an argument rejection banner. try std.testing.expectEqual(@as(?[]const u8, null), failureRetryGuidance(1, "line one\nline two\nline three\nline four\nusage: not a banner\n"));}
test "shell snapshot keeps bounded head tail and control metadata" { const alloc = std.testing.allocator; const output = "HEAD_SENTINEL\n" ++ ("x" ** (70 * 1024)) ++ "\nTAIL_SENTINEL"; const body = try formatSnapshot(alloc, .{ .execution_id = @constCast("shell-large"), .command = @constCast("large-output"), .cwd = @constCast("/tmp"), .retained = true, .state = .{ .completed = .{ .exit_code = 0 } }, .output_delta = @constCast(output), .output_truncated = false, .output_file = @constCast("fx-command-replay-large.bin"), }, null); defer alloc.free(body);
try std.testing.expect(body.len <= 16 * 1024); var parsed = try std.json.parseFromSlice(std.json.Value, alloc, body, .{}); defer parsed.deinit(); const object = parsed.value.object; try std.testing.expectEqualStrings( "fx-command-replay-large.bin", object.get("full_output_handle").?.string, ); try std.testing.expect(object.get("output_truncated").?.bool); const projected = object.get("output_delta").?.string; try std.testing.expect(std.mem.find(u8, projected, "HEAD_SENTINEL") != null); try std.testing.expect(std.mem.find(u8, projected, "TAIL_SENTINEL") != null); try std.testing.expect(std.mem.find(u8, projected, "bytes omitted") != null);}
test "shell snapshot projects hostile bytes as readable terminal-safe text" { const alloc = std.testing.allocator; const raw = "\x1b[31mRED\x1b[0m\rREWRITE\t\x00\xff\nCONTROL_TAIL\n"; const body = try formatSnapshot(alloc, .{ .execution_id = @constCast("shell-hostile"), .command = @constCast("hostile-output"), .cwd = @constCast("/tmp"), .retained = true, .state = .{ .completed = .{ .exit_code = 0 } }, .output_delta = @constCast(raw), .output_truncated = false, .output_file = @constCast("fx-command-replay-hostile.bin"), }, null); defer alloc.free(body);
var parsed = try std.json.parseFromSlice(std.json.Value, alloc, body, .{}); defer parsed.deinit(); const output_value = parsed.value.object.get("output_delta") orelse return error.TestExpectedEqual; if (output_value != .string) return error.TestUnexpectedResult; const terminal_safe = parsed.value.object.get("output_terminal_safe") orelse return error.TestExpectedEqual; try std.testing.expect(terminal_safe.bool); try std.testing.expect(std.mem.find(u8, output_value.string, "\\x1b") != null); try std.testing.expect(std.mem.find(u8, output_value.string, "\\x00") != null); try std.testing.expect(std.mem.find(u8, output_value.string, "\\xff") != null); try std.testing.expect(std.mem.find(u8, output_value.string, "CONTROL_TAIL") != null); try std.testing.expectEqualStrings( "fx-command-replay-hostile.bin", parsed.value.object.get("full_output_handle").?.string, );}
test "shell snapshot keeps a hostile output tail within the result limit" { const alloc = std.testing.allocator; const raw = ("\xff" ** (70 * 1024)) ++ "\nCONTROL_TAIL"; const body = try formatSnapshot(alloc, .{ .execution_id = @constCast("shell-hostile-large"), .command = @constCast("hostile-large-output"), .cwd = @constCast("/tmp"), .retained = true, .state = .{ .completed = .{ .exit_code = 0 } }, .output_delta = @constCast(raw), .output_truncated = false, .output_file = @constCast("fx-command-replay-hostile-large.bin"), }, null); defer alloc.free(body);
try std.testing.expect(body.len <= 16 * 1024); var parsed = try std.json.parseFromSlice(std.json.Value, alloc, body, .{}); defer parsed.deinit(); const output_value = parsed.value.object.get("output_delta") orelse return error.TestExpectedEqual; if (output_value != .string) return error.TestUnexpectedResult; try std.testing.expect(std.mem.find(u8, output_value.string, "\\xff") != null); try std.testing.expect(std.mem.find(u8, output_value.string, "bytes omitted") != null); try std.testing.expect(std.mem.find(u8, output_value.string, "CONTROL_TAIL") != null); try std.testing.expect(parsed.value.object.get("output_truncated").?.bool);}
test "running shell snapshot leaves continuation intent to the caller" { const alloc = std.testing.allocator; const body = try formatSnapshot(alloc, .{ .execution_id = @constCast("shell-running"), .command = @constCast("long-command"), .cwd = @constCast("/tmp"), .retained = true, .state = .running, .output_delta = @constCast(""), .output_truncated = false, }, null); defer alloc.free(body);
var parsed = try std.json.parseFromSlice(std.json.Value, alloc, body, .{}); defer parsed.deinit(); try std.testing.expect(parsed.value.object.get("next_action") == null); try std.testing.expectEqualStrings( "shell-running", parsed.value.object.get("session_id").?.string, ); try std.testing.expectEqualStrings( "running", parsed.value.object.get("state").?.string, );}
test "registered shell empty observation waits through one managed execution" { if (comptime @import("builtin").os.tag == .wasi) return; const alloc = std.testing.allocator; var runtime = managed_execution.Runtime.init(alloc); defer runtime.deinit(); var streamed_bytes = std.atomic.Value(usize).init(0); const StreamCapture = struct { fn append( raw: *anyopaque, _: ?types.ToolLifecycleId, _: command_contract.CommandOutputStream, chunk: []const u8, ) !void { const count: *std.atomic.Value(usize) = @ptrCast(@alignCast(raw)); _ = count.fetchAdd(chunk.len, .seq_cst); } }; const spec = tool_dispatch.Tool{ .name = "shell", .description = "shell", .model_schema = .{ .name = "shell", .description = "shell" }, .executor_kind = .terminal, .activity_kind = .command, .requires_approval = true, .decode = decode, .validate = validate, .call = call, .captured_command_action = "run", .captured_command_fn = isCapturedCommand, .process_local_fn = isProcessLocal, .reads_only_fn = readsOnly, .irreversible_fn = isIrreversible, }; const registry = tool_dispatch.Registry{ .tools = &.{spec} }; var environment_arena_state = std.heap.ArenaAllocator.init(alloc); defer environment_arena_state.deinit(); const environment = try commandEnvironment( environment_arena_state.allocator(), .{ .allocator = alloc, .workspace_root = "/tmp" }, .clean, ); const command_ctx = command_admission.CommandContext{ .command = "sleep 2; printf done", .resolved_cwd = "/tmp", .target_os = @import("builtin").os.tag, .environment = environment, }; const authority = command_admission.CommandExecutionAuthority{ .shell_allowed = .{ .fingerprint = .init(command_ctx), .source = .yolo, }, }; var start_status_detail: ?[]u8 = null; defer if (start_status_detail) |detail| alloc.free(detail); const started = try tool_dispatch.dispatchAuthorizedToolCall( .{ .allocator = alloc, .workspace_root = "/tmp", .tool_call_id = "shell-integration", .managed_executions = &runtime, .execution_authority = .{ .run_command = authority }, .max_command_output_bytes = 4096, .output_chunk_lifecycle_id = .{ .turn_id = 1, .call_id = "shell-integration", }, .output_chunk_ctx = &streamed_bytes, .on_output_chunk = StreamCapture.append, }, registry, .{ .id = "shell-integration", .name = "shell", .arguments_json = "{\"action\":\"run\",\"command\":\"sleep 2; printf done\",\"cwd\":\"/tmp\",\"profile\":\"clean\",\"yield_time_ms\":0}", }, &start_status_detail, ); defer started.deinit(alloc); try std.testing.expectEqual(tool_dispatch.DispatchResult.Status.success, started.status); try std.testing.expect(std.mem.find(u8, started.body, "\"state\":\"running\"") != null); var started_json = try std.json.parseFromSlice(std.json.Value, alloc, started.body, .{}); defer started_json.deinit(); const execution_id = started_json.value.object.get("session_id") orelse return error.TestExpectedEqual; const interact_arguments = try std.fmt.allocPrint( alloc, "{{\"action\":\"interact\",\"session_id\":\"{s}\",\"yield_time_ms\":1000}}", .{execution_id.string}, ); defer alloc.free(interact_arguments);
var wait_status_detail: ?[]u8 = null; defer if (wait_status_detail) |detail| alloc.free(detail); var command_result_json: ?[]const u8 = null; defer if (command_result_json) |json| alloc.free(@constCast(json)); var tool_result_memory: ?types.ToolResultMemory = null; defer if (tool_result_memory) |memory| { if (memory.command_output_replay) |replay| switch (replay) { .available => |descriptor| alloc.free(@constCast(descriptor.handle)), .unavailable => {}, }; }; const waited = try tool_dispatch.dispatchAuthorizedToolCall( .{ .allocator = alloc, .workspace_root = "/tmp", .tool_call_id = "shell-wait", .managed_executions = &runtime, .max_command_output_bytes = 4096, .command_result_json_sink = &command_result_json, .tool_result_memory_sink = &tool_result_memory, }, registry, .{ .id = "shell-wait", .name = "shell", .arguments_json = interact_arguments, }, &wait_status_detail, ); defer waited.deinit(alloc); try std.testing.expectEqual(tool_dispatch.DispatchResult.Status.success, waited.status); try std.testing.expect(std.mem.find(u8, waited.body, "\"state\":\"completed\"") != null); try std.testing.expect(std.mem.find(u8, waited.body, "done") != null); try std.testing.expectEqual(@as(usize, "done".len), streamed_bytes.load(.seq_cst)); try std.testing.expect(std.mem.find( u8, command_result_json orelse return error.TestExpectedEqual, "\"kind\":\"command\"", ) != null); const replay = tool_result_memory.?.command_output_replay orelse return error.TestExpectedEqual; try std.testing.expect(replay == .available);}
test "shell delivery advances only after result commit" { if (comptime @import("builtin").os.tag == .wasi) return; const alloc = std.testing.allocator; var runtime = managed_execution.Runtime.init(alloc); defer runtime.deinit(); const command_ctx = command_admission.CommandContext{ .command = "printf commit-token", .resolved_cwd = "/tmp", .target_os = @import("builtin").os.tag, .environment = .legacy, }; var prepared = try runtime.startCaptured(alloc, .{ .execution_id = "delivery-commit", .command = command_ctx.command, .cwd = command_ctx.resolved_cwd, .environment = command_ctx.environment, .authority = .{ .shell_allowed = .{ .fingerprint = .init(command_ctx), .source = .yolo, } }, .max_output_bytes = 4096, .timeout_ms = 2000, .command_artifact_dir = null, .yield_time_ms = 0, }); defer prepared.deinit(alloc); var commit_token: ?result_commit.Token = null; var result = try finishPrepared( .{ .allocator = alloc, .result_commit_sink = &commit_token, }, &runtime, &prepared, .command, ); defer result.deinit(alloc); try std.testing.expect(commit_token != null); try std.testing.expectError( error.ExecutionBusy, runtime.wait(alloc, "delivery-commit", 0, null), ); commit_token.?.cancel(); var replayed = try runtime.wait(alloc, "delivery-commit", 2000, null); defer replayed.deinit(alloc); try std.testing.expect(std.mem.find( u8, replayed.snapshot.output_delta, "commit-token", ) != null); try runtime.commitDelivery( replayed.snapshot.execution_id, replayed.reservation_id, );}