Something went wrong. Try again.
My version of https://github.com/vercel-labs/fx
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404const std = @import("std");const builtin = @import("builtin");const diff_mod = @import("../output/diff.zig");const file_mutation_contract = @import("file_mutation_contract.zig");const io_mod = @import("../shared/io.zig");const pathing = @import("../workspace/pathing.zig");const text_utils = @import("../shared/text_utils.zig");const types = @import("../shared/types.zig");
const testing_permissions = if (builtin.is_test) @import("../permissions/permissions.zig")else struct {};const testing_builtin_tools = if (builtin.is_test) @import("../../builtins/tools.zig")else struct {};const testing_write_file = if (builtin.is_test) @import("../../tools/filesystem/write_file.zig")else struct {};const testing_edit_file = if (builtin.is_test) @import("../../tools/filesystem/edit_file.zig")else struct {};
const Allocator = std.mem.Allocator;const PreparedFileMutation = file_mutation_contract.PreparedFileMutation;const PolicyEvaluatedFileTargets = file_mutation_contract.PolicyEvaluatedFileTargets;
const max_content_bytes: usize = 4 * 1024 * 1024;const max_total_content_bytes: usize = 8 * 1024 * 1024;
pub const ParentCleanupResidueReason = enum { not_empty, identity_changed, remove_failed,};
pub const CreatedParentResidue = struct { component_index: usize, path_end: usize, permission_target_index: usize, observed_identity: ?file_mutation_contract.FileIdentity, reason: ParentCleanupResidueReason,};
pub const ApplyRejectionReason = enum { binding_mismatch, stale_preimage, cancelled, traversal_changed, staged_source_changed, io_failure,};
pub const ApplyRejection = struct { reason: ApplyRejectionReason, created_parent_residue: []const CreatedParentResidue,};
pub const ChangeTrackerHandoff = struct { kind: file_mutation_contract.Kind, raw_path: []const u8, previous_content: ?[]const u8, committed_at_ms: i64,};
pub const FullViewSnapshot = struct { after_content: []const u8, lifecycle_id: types.ToolLifecycleId,};
pub const CommittedFileHandoff = struct { preview: diff_mod.FileChangePreview, tracker: ChangeTrackerHandoff, full_view: ?FullViewSnapshot = null,
pub fn init( preview: diff_mod.FileChangePreview, tracker: ChangeTrackerHandoff, ) CommittedFileHandoff { return .{ .preview = preview, .tracker = tracker, }; }
pub fn deinit(self: CommittedFileHandoff, alloc: Allocator) void { alloc.free(@constCast(self.preview.path)); for (self.preview.lines) |line| alloc.free(@constCast(line.text)); alloc.free(@constCast(self.preview.lines)); alloc.free(@constCast(self.tracker.raw_path)); if (self.tracker.previous_content) |content| { alloc.free(@constCast(content)); } if (self.full_view) |full_view| { alloc.free(@constCast(full_view.after_content)); alloc.free(@constCast(full_view.lifecycle_id.call_id)); } }
pub fn attachFullView( self: *CommittedFileHandoff, alloc: Allocator, after_content: []const u8, lifecycle_id: types.ToolLifecycleId, ) Allocator.Error!void { const owned_after_content = try alloc.dupe(u8, after_content); errdefer alloc.free(owned_after_content); const owned_call_id = try alloc.dupe(u8, lifecycle_id.call_id); errdefer alloc.free(owned_call_id);
self.full_view = .{ .after_content = owned_after_content, .lifecycle_id = .{ .turn_id = lifecycle_id.turn_id, .call_id = owned_call_id, }, }; }};
pub const ApplyResult = union(enum) { rejected: ApplyRejection, committed: CommittedFileHandoff,};
const target_mismatch_message = "file mutation preparation failed: approved target no longer matches the call";const identity_changed_message = "file mutation preparation failed: approved filesystem identity changed";const preview_too_large_message = "file mutation preparation failed: diff preview exceeds preparation limits";const total_content_too_large_message = "file mutation preparation failed: content proof exceeds the 8 MiB preparation limit";
const FileMutationPrepareError = error{ OutOfMemory, Canceled, Unexpected, SystemResources, ProcessFdQuotaExceeded, SystemFdQuotaExceeded,};
const FileMutationApplyError = error{ OutOfMemory, ResultContractViolation,};
const PreimageResult = union(enum) { preimage: file_mutation_contract.Preimage, semantic_failure: []const u8,};
const PostimageResult = union(enum) { content: []const u8, semantic_failure: []const u8,};
const PreviewResult = union(enum) { preview: diff_mod.FileChangePreview, semantic_failure: []const u8,};
const CommitClaim = struct { claimed: std.atomic.Value(bool) = .init(false),};
var target_resolution_check_count: usize = 0;
pub fn resetTargetResolutionCheckCountForTest() void { if (builtin.is_test) target_resolution_check_count = 0;}
pub fn targetResolutionCheckCountForTest() usize { return if (builtin.is_test) target_resolution_check_count else 0;}
pub fn prepare( call_alloc: std.mem.Allocator, call: types.ToolCall, input: file_mutation_contract.FileMutationInput, policy_targets: file_mutation_contract.PolicyEvaluatedFileTargets,) FileMutationPrepareError!file_mutation_contract.PrepareResult { return prepareInternal(call_alloc, call, input, policy_targets, true);}
/// Prepares a mutation whose decoded input and canonical target proof were/// produced together by tool admission. Identity, proof, preimage, and diff/// validation remain unchanged; only duplicate canonical resolution is skipped.pub fn prepareResolvedTarget( call_alloc: std.mem.Allocator, call: types.ToolCall, input: file_mutation_contract.FileMutationInput, policy_targets: file_mutation_contract.PolicyEvaluatedFileTargets,) FileMutationPrepareError!file_mutation_contract.PrepareResult { return prepareInternal(call_alloc, call, input, policy_targets, false);}
fn prepareInternal( call_alloc: std.mem.Allocator, call: types.ToolCall, input: file_mutation_contract.FileMutationInput, policy_targets: file_mutation_contract.PolicyEvaluatedFileTargets, verify_target_resolution: bool,) FileMutationPrepareError!file_mutation_contract.PrepareResult { if (policyIdentityBindingMismatch(policy_targets)) { return .{ .semantic_failure = identity_changed_message }; } if (!policy_targets.proofValid()) { return .{ .semantic_failure = target_mismatch_message }; } if (verify_target_resolution and !try resolvedTargetMatches(input, policy_targets)) { return .{ .semantic_failure = target_mismatch_message }; }
const preimage_result = try verifyAndReadPreimage(call_alloc, input, policy_targets); const preimage = switch (preimage_result) { .preimage => |preimage| preimage, .semantic_failure => |message| return .{ .semantic_failure = message }, };
const postimage_result = try derivePostimage(call_alloc, input, preimage); const after_content = switch (postimage_result) { .content => |content| content, .semantic_failure => |message| return .{ .semantic_failure = message }, }; const before_content = switch (preimage) { .absent => "", .present => |present| present.content, }; const total_content_bytes = std.math.add( usize, before_content.len, after_content.len, ) catch return .{ .semantic_failure = total_content_too_large_message }; if (total_content_bytes > max_total_content_bytes) { return .{ .semantic_failure = total_content_too_large_message }; }
// External anchors show the resolved target so a symlink redirect cannot look local. const approval_path = switch (policy_targets.anchor.scope) { .external => policy_targets.canonical_target_path, .workspace => input.path(), }; const encoded_path = text_utils.encodeTerminalSafePathTail( call_alloc, approval_path, diff_mod.max_encoded_path_bytes, ) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, error.PathBasenameTooLong => return .{ .semantic_failure = preview_too_large_message, }, }; const preview_result = try buildPreview( call_alloc, encoded_path, preimage, before_content, after_content, ); const preview = switch (preview_result) { .preview => |preview| preview, .semantic_failure => |message| return .{ .semantic_failure = message }, }; const review = diff_mod.FileReview.init( call_alloc, before_content, after_content, ) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, error.ArithmeticOverflow => return .{ .semantic_failure = preview_too_large_message, }, };
var arguments_hasher = std.crypto.hash.sha2.Sha256.init(.{}); arguments_hasher.update(call.arguments_json); const commit_claim = try call_alloc.create(CommitClaim); commit_claim.* = .{}; return .{ .prepared = .{ .call_id = call.id, .tool_name = call.name, .arguments_hash = arguments_hasher.finalResult(), .target_path = policy_targets.canonical_target_path, .display_path = encoded_path.bytes, .policy_targets = policy_targets, .kind = std.meta.activeTag(input), .preimage = preimage, .after_content = after_content, .preview = preview, .review = review, .commit_token = commit_claim, } };}
const ApplyTestControls = struct { ctx: ?*anyopaque = null, after_write_chunk: ?*const fn (?*anyopaque, usize) anyerror!void = null, after_stage: ?*const fn (?*anyopaque, std.Io.Dir, []const u8) anyerror!void = null, after_final_preimage_read: ?*const fn (?*anyopaque, std.Io.Dir, []const u8) anyerror!void = null, after_final_validation: ?*const fn (?*anyopaque) anyerror!void = null,};
const CreatedParentRecord = struct { component_index: usize, path_end: usize, permission_target_index: usize, identity: file_mutation_contract.FileIdentity,};
const PreimageValidation = union(enum) { valid: ?std.Io.File.Permissions, stale, io_failure,};
const TraversalValidation = union(enum) { parent: std.Io.Dir, changed, io_failure,};
const StagedSourceValidation = enum { valid, changed, io_failure,};
const TargetEntryValidation = enum { valid, changed, io_failure,};
const StagedWriteError = error{ Canceled, IoFailure,};
const ApplyResources = struct { policy: PolicyEvaluatedFileTargets, created: *[file_mutation_contract.max_file_target_components]CreatedParentRecord, realized: *[file_mutation_contract.max_file_target_components]file_mutation_contract.FileIdentity, residue_storage: []CreatedParentResidue, created_count: usize = 0, parent: ?std.Io.Dir = null, temp_name: ?[]const u8 = null, temp_identity: ?file_mutation_contract.FileIdentity = null,
fn closeParent(self: *ApplyResources) void { if (self.parent) |parent| parent.close(io_mod.getIo()); self.parent = null; }
fn reject( self: *ApplyResources, reason: ApplyRejectionReason, ) ApplyResult { self.cleanupTemp(); self.closeParent(); const residues = self.cleanupCreatedParents(); return .{ .rejected = .{ .reason = reason, .created_parent_residue = residues, } }; }
fn cleanupTemp(self: *ApplyResources) void { const parent = self.parent orelse return; const temp_name = self.temp_name orelse return; const expected_identity = self.temp_identity orelse return; const actual_identity = observedFileIdentity(parent, temp_name) orelse return; if (!identityEql(actual_identity, expected_identity)) return; parent.deleteFile(io_mod.getIo(), temp_name) catch return; self.temp_name = null; self.temp_identity = null; }
fn cleanupCreatedParents( self: *ApplyResources, ) []const CreatedParentResidue { var residue_count: usize = 0; var index = self.created_count; while (index > 0) { index -= 1; const created = self.created[index]; if (cleanupCreatedParent(self.policy, self.realized, created)) |residue| { self.residue_storage[residue_count] = residue; residue_count += 1; } } return self.residue_storage[0..residue_count]; }};
pub fn apply( call_alloc: std.mem.Allocator, result_alloc: std.mem.Allocator, call: types.ToolCall, authorization: file_mutation_contract.FileExecutionAuthorization, cancel_flag: *std.atomic.Value(bool),) FileMutationApplyError!ApplyResult { return applyWithTestControls( call_alloc, result_alloc, call, authorization, cancel_flag, .{}, );}
fn applyWithTestControls( call_alloc: std.mem.Allocator, result_alloc: std.mem.Allocator, call: types.ToolCall, authorization: file_mutation_contract.FileExecutionAuthorization, cancel_flag: *std.atomic.Value(bool), test_controls: ApplyTestControls,) FileMutationApplyError!ApplyResult { const prepared = authorization.prepared orelse { return rejectedWithoutResidue(.binding_mismatch); }; if (!applyBindingValid(call, authorization.policy_targets, prepared)) { return rejectedWithoutResidue(.binding_mismatch); } const commit_claim = preparedCommitClaim(prepared); if (commit_claim.claimed.load(.seq_cst)) { return error.ResultContractViolation; } if (cancel_flag.load(.seq_cst)) return rejectedWithoutResidue(.cancelled);
const residue_storage = try call_alloc.alloc( CreatedParentResidue, prepared.policy_targets.traversal_directories.len, ); const previous_content = switch (prepared.preimage) { .absent => null, .present => |present| present.content, }; var handoff = try dupeCommittedFileHandoff( result_alloc, prepared.preview, prepared.kind, prepared.target_path, previous_content, ); var handoff_owned = true; defer if (handoff_owned) handoff.deinit(result_alloc);
var created: [file_mutation_contract.max_file_target_components]CreatedParentRecord = undefined; var realized: [file_mutation_contract.max_file_target_components]file_mutation_contract.FileIdentity = undefined; var resources: ApplyResources = .{ .policy = prepared.policy_targets, .created = &created, .realized = &realized, .residue_storage = residue_storage, }; defer resources.closeParent();
io_mod.e2eFailIfDurableMutationAttempted(); if (realizeTraversal(&resources)) |reason| return resources.reject(reason); const parent = resources.parent.?; const target_name = prepared.policy_targets.component( prepared.policy_targets.anchor.relative_components[ prepared.policy_targets.anchor.relative_components.len - 1 ], );
const initial_preimage = validatePreimage( parent, target_name, prepared, null, null, null, ); const destination_permissions = switch (initial_preimage) { .valid => |permissions| permissions, .stale => return resources.reject(.stale_preimage), .io_failure => return resources.reject(.io_failure), };
var random_bytes: [16]u8 = undefined; io_mod.getIo().random(&random_bytes); const suffix = std.fmt.bytesToHex(random_bytes, .lower); var temp_name_buffer: [".fx-stage-".len + suffix.len]u8 = undefined; @memcpy(temp_name_buffer[0..".fx-stage-".len], ".fx-stage-"); @memcpy(temp_name_buffer[".fx-stage-".len..], suffix[0..]); const temp_name = temp_name_buffer[0..];
var stage = parent.createFile(io_mod.getIo(), temp_name, .{ .read = true, .truncate = false, .exclusive = true, .permissions = destination_permissions orelse .default_file, .resolve_beneath = true, }) catch return resources.reject(.io_failure); resources.temp_name = temp_name; defer stage.close(io_mod.getIo());
const initial_stage_stat = stage.stat(io_mod.getIo()) catch return resources.reject(.io_failure); if (initial_stage_stat.kind != .file or initial_stage_stat.nlink != 1) { return resources.reject(.staged_source_changed); } resources.temp_identity = pathing.fileIdentity( pathing.descriptorDevice(stage.handle) catch return resources.reject(.io_failure), initial_stage_stat, ); if (destination_permissions) |permissions| { stage.setPermissions(io_mod.getIo(), permissions) catch return resources.reject(.io_failure); } writeStagedContent( &stage, prepared.after_content, cancel_flag, test_controls, ) catch |err| { return resources.reject(switch (err) { error.Canceled => .cancelled, error.IoFailure => .io_failure, }); }; stage.sync(io_mod.getIo()) catch return resources.reject(.io_failure); if (cancel_flag.load(.seq_cst)) return resources.reject(.cancelled);
if (test_controls.after_stage) |after_stage| { after_stage(test_controls.ctx, parent, temp_name) catch return resources.reject(.io_failure); } if (!applyBindingValid(call, authorization.policy_targets, prepared)) { return resources.reject(.binding_mismatch); } if (cancel_flag.load(.seq_cst)) return resources.reject(.cancelled);
switch (validatePreimage( parent, target_name, prepared, destination_permissions, test_controls.ctx, test_controls.after_final_preimage_read, )) { .valid => {}, .stale => return resources.reject(.stale_preimage), .io_failure => return resources.reject(.io_failure), } switch (validateStagedSource( parent, temp_name, &stage, resources.temp_identity.?, prepared.after_content, )) { .valid => {}, .changed => return resources.reject(.staged_source_changed), .io_failure => return resources.reject(.io_failure), } if (cancel_flag.load(.seq_cst)) return resources.reject(.cancelled);
const revalidated = revalidateTraversal(&resources); var commit_parent = switch (revalidated) { .parent => |revalidated_parent| revalidated_parent, .changed => return resources.reject(.traversal_changed), .io_failure => return resources.reject(.io_failure), }; defer commit_parent.close(io_mod.getIo());
switch (validateTargetEntry( commit_parent, target_name, prepared.policy_targets.items[0].expected_identity, )) { .valid => {}, .changed => return resources.reject(.stale_preimage), .io_failure => return resources.reject(.io_failure), } switch (validateStagedSourceIdentity( commit_parent, temp_name, &stage, resources.temp_identity.?, prepared.after_content.len, )) { .valid => {}, .changed => return resources.reject(.staged_source_changed), .io_failure => return resources.reject(.io_failure), } if (cancel_flag.load(.seq_cst)) return resources.reject(.cancelled); if (test_controls.after_final_validation) |after_final_validation| { after_final_validation(test_controls.ctx) catch return resources.reject(.io_failure); } if (commit_claim.claimed.cmpxchgStrong( false, true, .seq_cst, .seq_cst, ) != null) { resources.cleanupTemp(); resources.closeParent(); _ = resources.cleanupCreatedParents(); return error.ResultContractViolation; }
// The fresh descriptor binds the final entry checks and rename to the // revalidated parent. The platform cannot freeze its namespace after // these checks, so a competing move can still win the remaining window. commit_parent.rename( temp_name, commit_parent, target_name, io_mod.getIo(), ) catch return resources.reject(.io_failure); resources.temp_name = null; resources.temp_identity = null; resources.closeParent();
handoff.tracker.committed_at_ms = io_mod.milliTimestamp(); handoff_owned = false; return .{ .committed = handoff };}
fn preparedCommitClaim(prepared: PreparedFileMutation) *CommitClaim { return @ptrCast(@alignCast(prepared.commit_token));}
fn dupeCommittedFileHandoff( alloc: Allocator, preview: diff_mod.FileChangePreview, kind: file_mutation_contract.Kind, raw_path: []const u8, previous_content: ?[]const u8,) Allocator.Error!CommittedFileHandoff { const owned_path = try alloc.dupe(u8, preview.path); errdefer alloc.free(owned_path);
const owned_lines = try alloc.alloc( diff_mod.PreviewLine, preview.lines.len, ); errdefer alloc.free(owned_lines); var initialized_lines: usize = 0; errdefer for (owned_lines[0..initialized_lines]) |line| { alloc.free(@constCast(line.text)); }; for (preview.lines, owned_lines) |source, *destination| { destination.* = source; destination.text = try alloc.dupe(u8, source.text); initialized_lines += 1; }
const owned_raw_path = try alloc.dupe(u8, raw_path); errdefer alloc.free(owned_raw_path); const owned_previous_content = if (previous_content) |content| try alloc.dupe(u8, content) else null; errdefer if (owned_previous_content) |content| alloc.free(content);
return CommittedFileHandoff.init( .{ .path = owned_path, .lines = owned_lines, .additions = preview.additions, .deletions = preview.deletions, .truncated = preview.truncated, }, .{ .kind = kind, .raw_path = owned_raw_path, .previous_content = owned_previous_content, .committed_at_ms = 0, }, );}
fn writeStagedContent( stage: *std.Io.File, content: []const u8, cancel_flag: *std.atomic.Value(bool), test_controls: ApplyTestControls,) StagedWriteError!void { const chunk_bytes: usize = 64 * 1024; var offset: usize = 0; while (offset < content.len) { if (cancel_flag.load(.seq_cst)) return error.Canceled; const end = @min(offset + chunk_bytes, content.len); stage.writeStreamingAll(io_mod.getIo(), content[offset..end]) catch return error.IoFailure; offset = end; if (test_controls.after_write_chunk) |after_write_chunk| { after_write_chunk(test_controls.ctx, offset) catch return error.IoFailure; } } if (cancel_flag.load(.seq_cst)) return error.Canceled;}
fn rejectedWithoutResidue( reason: ApplyRejectionReason,) ApplyResult { return .{ .rejected = .{ .reason = reason, .created_parent_residue = &.{}, } };}
fn validateStagedSource( parent: std.Io.Dir, temp_name: []const u8, stage: *std.Io.File, expected_identity: file_mutation_contract.FileIdentity, expected_content: []const u8,) StagedSourceValidation { switch (validateStagedSourceIdentity( parent, temp_name, stage, expected_identity, expected_content.len, )) { .valid => {}, .changed => return .changed, .io_failure => return .io_failure, }
var hasher = std.crypto.hash.sha2.Sha256.init(.{}); var reader_buffer: [8192]u8 = undefined; var reader = stage.reader(io_mod.getIo(), &reader_buffer); reader.seekTo(0) catch return .io_failure; var chunk: [8192]u8 = undefined; while (true) { const bytes_read = reader.interface.readSliceShort(&chunk) catch return .io_failure; if (bytes_read == 0) break; hasher.update(chunk[0..bytes_read]); }
var expected_hasher = std.crypto.hash.sha2.Sha256.init(.{}); expected_hasher.update(expected_content); if (!std.mem.eql( u8, &hasher.finalResult(), &expected_hasher.finalResult(), )) return .changed; return .valid;}
fn validateStagedSourceIdentity( parent: std.Io.Dir, temp_name: []const u8, stage: *std.Io.File, expected_identity: file_mutation_contract.FileIdentity, expected_size: usize,) StagedSourceValidation { const zio = io_mod.getIo(); const stat = stage.stat(zio) catch return .io_failure; if (stat.kind != .file or stat.nlink != 1 or stat.size != expected_size) { return .changed; } const handle_identity = pathing.fileIdentity( pathing.descriptorDevice(stage.handle) catch return .io_failure, stat, ); if (!identityEql(handle_identity, expected_identity)) return .changed;
const named_identity = observedFileIdentity(parent, temp_name) orelse return .changed; if (!identityEql(named_identity, expected_identity)) return .changed; return .valid;}
fn applyBindingValid( call: types.ToolCall, policy: PolicyEvaluatedFileTargets, prepared: PreparedFileMutation,) bool { if (!std.mem.eql(u8, call.id, prepared.call_id)) return false; if (!std.mem.eql(u8, call.name, prepared.tool_name)) return false; const expected_kind: file_mutation_contract.Kind = if (std.mem.eql(u8, call.name, "write_file")) .write else if (std.mem.eql(u8, call.name, "edit_file")) .edit else return false; if (prepared.kind != expected_kind) return false;
var hasher = std.crypto.hash.sha2.Sha256.init(.{}); hasher.update(call.arguments_json); if (!std.mem.eql(u8, &hasher.finalResult(), &prepared.arguments_hash)) { return false; } if (!PolicyEvaluatedFileTargets.authorizationEql(policy, prepared.policy_targets)) { return false; } if (!policy.proofValid()) return false; if (!std.mem.eql(u8, prepared.target_path, policy.canonical_target_path)) { return false; } if (!std.mem.eql(u8, prepared.display_path, prepared.preview.path)) return false; prepared.preview.validate() catch return false; if (prepared.after_content.len > max_content_bytes) return false; switch (prepared.preimage) { .absent => {}, .present => |present| { if (present.content.len > max_content_bytes) return false; var content_hasher = std.crypto.hash.sha2.Sha256.init(.{}); content_hasher.update(present.content); if (!std.mem.eql( u8, &content_hasher.finalResult(), &present.content_hash, )) return false; }, } return true;}
fn realizeTraversal( resources: *ApplyResources,) ?ApplyRejectionReason { const zio = io_mod.getIo(); var current = std.Io.Dir.openDirAbsolute( zio, resources.policy.anchorPath(), .{ .follow_symlinks = false }, ) catch |err| return if (pathStateChanged(err)) .traversal_changed else .io_failure;
const anchor_identity = pathing.directoryIdentity(current) catch { current.close(zio); return .io_failure; }; if (!identityEql(anchor_identity, resources.policy.anchor.identity)) { current.close(zio); return .traversal_changed; }
for (resources.policy.traversal_directories, 0..) |entry, index| { const component = resources.policy.component( resources.policy.anchor.relative_components[entry.component_index], ); switch (entry.state) { .existing => |expected_identity| { const next = current.openDir( zio, component, .{ .follow_symlinks = false }, ) catch |err| { current.close(zio); return if (pathStateChanged(err)) .traversal_changed else .io_failure; }; const actual_identity = pathing.directoryIdentity(next) catch { next.close(zio); current.close(zio); return .io_failure; }; if (!identityEql(actual_identity, expected_identity)) { next.close(zio); current.close(zio); return .traversal_changed; } resources.realized[index] = actual_identity; current.close(zio); current = next; }, .create => |create| { current.createDir(zio, component, .default_dir) catch |err| { current.close(zio); return if (pathStateChanged(err) or err == error.PathAlreadyExists) .traversal_changed else .io_failure; }; const next = current.openDir( zio, component, .{ .follow_symlinks = false }, ) catch { current.deleteDir(zio, component) catch {}; current.close(zio); return .io_failure; }; const actual_identity = pathing.directoryIdentity(next) catch { next.close(zio); current.deleteDir(zio, component) catch {}; current.close(zio); return .io_failure; }; resources.realized[index] = actual_identity; resources.created[resources.created_count] = .{ .component_index = entry.component_index, .path_end = entry.path_end, .permission_target_index = create.permission_target_index, .identity = actual_identity, }; resources.created_count += 1; current.close(zio); current = next; }, } } resources.parent = current; return null;}
fn revalidateTraversal(resources: *ApplyResources) TraversalValidation { const zio = io_mod.getIo(); var current = std.Io.Dir.openDirAbsolute( zio, resources.policy.anchorPath(), .{ .follow_symlinks = false }, ) catch |err| return if (pathStateChanged(err)) .changed else .io_failure;
const anchor_identity = pathing.directoryIdentity(current) catch { current.close(zio); return .io_failure; }; if (!identityEql(anchor_identity, resources.policy.anchor.identity)) { current.close(zio); return .changed; }
for (resources.policy.traversal_directories, 0..) |entry, index| { const component = resources.policy.component( resources.policy.anchor.relative_components[entry.component_index], ); const next = current.openDir( zio, component, .{ .follow_symlinks = false }, ) catch |err| { current.close(zio); return if (pathStateChanged(err)) .changed else .io_failure; }; const actual_identity = pathing.directoryIdentity(next) catch { next.close(zio); current.close(zio); return .io_failure; }; if (!identityEql(actual_identity, resources.realized[index])) { next.close(zio); current.close(zio); return .changed; } current.close(zio); current = next; } return .{ .parent = current };}
fn validatePreimage( parent: std.Io.Dir, target_name: []const u8, prepared: PreparedFileMutation, expected_permissions: ?std.Io.File.Permissions, test_control_ctx: ?*anyopaque, after_present_read: ?*const fn (?*anyopaque, std.Io.Dir, []const u8) anyerror!void,) PreimageValidation { const zio = io_mod.getIo(); return switch (prepared.preimage) { .absent => blk: { _ = parent.statFile( zio, target_name, .{ .follow_symlinks = false }, ) catch |err| switch (err) { error.FileNotFound => break :blk .{ .valid = null }, else => break :blk if (pathStateChanged(err)) .stale else .io_failure, }; break :blk .stale; }, .present => |expected| blk: { var file = parent.openFile(zio, target_name, .{ .allow_directory = false, .follow_symlinks = false, .resolve_beneath = true, }) catch |err| break :blk if (pathStateChanged(err)) .stale else .io_failure; defer file.close(zio);
const stat = file.stat(zio) catch break :blk .io_failure; if (stat.kind != .file or stat.size != expected.content.len) { break :blk .stale; } const actual_identity = pathing.fileIdentity( pathing.descriptorDevice(file.handle) catch break :blk .io_failure, stat, ); const expected_identity = prepared.policy_targets.items[0].expected_identity orelse break :blk .stale; if (!identityEql(actual_identity, expected_identity)) break :blk .stale; if (stat.permissions.toMode() & 0o222 == 0) break :blk .io_failure; if (expected_permissions) |permissions| { if (stat.permissions.toMode() != permissions.toMode()) break :blk .stale; }
var hasher = std.crypto.hash.sha2.Sha256.init(.{}); var read_buffer: [8192]u8 = undefined; var reader = file.reader(zio, &read_buffer); var chunk: [8192]u8 = undefined; while (true) { const bytes_read = reader.interface.readSliceShort(&chunk) catch break :blk .io_failure; if (bytes_read == 0) break; hasher.update(chunk[0..bytes_read]); } if (!std.mem.eql(u8, &hasher.finalResult(), &expected.content_hash)) { break :blk .stale; } if (after_present_read) |after_read| { after_read(test_control_ctx, parent, target_name) catch break :blk .io_failure; } switch (validateTargetEntry(parent, target_name, actual_identity)) { .valid => {}, .changed => break :blk .stale, .io_failure => break :blk .io_failure, } break :blk .{ .valid = stat.permissions }; }, };}
fn validateTargetEntry( parent: std.Io.Dir, name: []const u8, expected_identity: ?file_mutation_contract.FileIdentity,) TargetEntryValidation { const zio = io_mod.getIo(); if (expected_identity == null) { _ = parent.statFile( zio, name, .{ .follow_symlinks = false }, ) catch |err| return switch (err) { error.FileNotFound => .valid, else => if (pathStateChanged(err)) .changed else .io_failure, }; return .changed; }
var file = parent.openFile(zio, name, .{ .allow_directory = false, .follow_symlinks = false, .path_only = true, .resolve_beneath = true, }) catch |err| return if (pathStateChanged(err)) .changed else .io_failure; defer file.close(zio); const stat = file.stat(zio) catch return .io_failure; if (stat.kind != .file) return .changed; const actual_identity = pathing.fileIdentity( pathing.descriptorDevice(file.handle) catch return .io_failure, stat, ); return if (identityEql(actual_identity, expected_identity.?)) .valid else .changed;}
fn observedFileIdentity( parent: std.Io.Dir, name: []const u8,) ?file_mutation_contract.FileIdentity { var file = parent.openFile(io_mod.getIo(), name, .{ .allow_directory = false, .follow_symlinks = false, .path_only = true, .resolve_beneath = true, }) catch return null; defer file.close(io_mod.getIo()); const stat = file.stat(io_mod.getIo()) catch return null; if (stat.kind != .file or stat.nlink != 1) return null; return pathing.fileIdentity( pathing.descriptorDevice(file.handle) catch return null, stat, );}
fn cleanupCreatedParent( policy: PolicyEvaluatedFileTargets, realized: *const [file_mutation_contract.max_file_target_components]file_mutation_contract.FileIdentity, created: CreatedParentRecord,) ?CreatedParentResidue { const zio = io_mod.getIo(); var current = std.Io.Dir.openDirAbsolute( zio, policy.anchorPath(), .{ .follow_symlinks = false }, ) catch return createdParentResidue(created, null, .identity_changed);
const anchor_identity = pathing.directoryIdentity(current) catch { current.close(zio); return createdParentResidue(created, null, .identity_changed); }; if (!identityEql(anchor_identity, policy.anchor.identity)) { current.close(zio); return createdParentResidue(created, null, .identity_changed); }
for (policy.traversal_directories[0..created.component_index], 0..) |entry, index| { const component = policy.component( policy.anchor.relative_components[entry.component_index], ); const next = current.openDir( zio, component, .{ .follow_symlinks = false }, ) catch { current.close(zio); return createdParentResidue(created, null, .identity_changed); }; const actual_identity = pathing.directoryIdentity(next) catch { next.close(zio); current.close(zio); return createdParentResidue(created, null, .identity_changed); }; if (!identityEql(actual_identity, realized[index])) { next.close(zio); current.close(zio); return createdParentResidue(created, null, .identity_changed); } current.close(zio); current = next; } defer current.close(zio);
const component = policy.component( policy.anchor.relative_components[created.component_index], ); const child = current.openDir( zio, component, .{ .follow_symlinks = false }, ) catch |err| switch (err) { error.FileNotFound => return null, else => return createdParentResidue(created, null, .identity_changed), }; const observed = pathing.directoryIdentity(child) catch { child.close(zio); return createdParentResidue(created, null, .identity_changed); }; child.close(zio); if (!identityEql(observed, created.identity)) { return createdParentResidue(created, observed, .identity_changed); }
current.deleteDir(zio, component) catch |err| return createdParentResidue( created, observed, if (err == error.DirNotEmpty) .not_empty else .remove_failed, ); return null;}
fn createdParentResidue( created: CreatedParentRecord, observed_identity: ?file_mutation_contract.FileIdentity, reason: ParentCleanupResidueReason,) CreatedParentResidue { return .{ .component_index = created.component_index, .path_end = created.path_end, .permission_target_index = created.permission_target_index, .observed_identity = observed_identity, .reason = reason, };}
fn pathStateChanged(err: anyerror) bool { return switch (err) { error.FileNotFound, error.NotDir, error.SymLinkLoop, error.PathAlreadyExists, error.IsDir, => true, else => false, };}
fn policyIdentityBindingMismatch(policy: PolicyEvaluatedFileTargets) bool { if (policy.items.len < 2) return false; const parent_item = policy.items[1]; if (parent_item.kind != .parent) return false;
if (policy.traversal_directories.len == 0) { if (parent_item.disposition != .existing_parent or parent_item.path_end != policy.anchor.path_end) { return false; } return !optionalIdentityEql( parent_item.expected_identity, policy.anchor.identity, ); }
const parent = policy.traversal_directories[ policy.traversal_directories.len - 1 ]; return switch (parent.state) { .existing => |identity| parent_item.disposition == .existing_parent and parent_item.path_end == parent.path_end and !optionalIdentityEql(parent_item.expected_identity, identity), .create => parent_item.disposition == .create_parent and parent_item.path_end == parent.path_end and parent_item.expected_identity != null, };}
fn checkedCharge(base: usize, count: usize, item_size: usize) ?usize { const bytes = std.math.mul(usize, count, item_size) catch return null; return std.math.add(usize, base, bytes) catch null;}
fn resolvedTargetMatches( input: file_mutation_contract.FileMutationInput, policy: PolicyEvaluatedFileTargets,) FileMutationPrepareError!bool { if (builtin.is_test) target_resolution_check_count += 1; var primary_path_scratch: [std.Io.Dir.max_path_bytes]u8 = undefined; var secondary_path_scratch: [std.Io.Dir.max_path_bytes]u8 = undefined; var component_scratch: [file_mutation_contract.max_file_target_components]pathing.BoundedFileTargetComponent = undefined; const resolved = pathing.resolveFileMutationTargetBounded( policy.anchorPath(), input.path(), switch (input) { .write => .create, .edit => .existing, }, &primary_path_scratch, &secondary_path_scratch, &component_scratch, ) catch |err| { if (operationalError(err)) |operational| return operational; return false; };
if (!std.mem.eql( u8, resolved.canonical_target_path, policy.canonical_target_path, )) return false; if (resolved.anchor_path_end != policy.anchor.path_end) return false; if (resolved.relative_components.len != policy.anchor.relative_components.len) { return false; } for (resolved.relative_components, policy.anchor.relative_components) |actual, expected| { if (actual.start != expected.start or actual.end != expected.end) return false; } return true;}
fn verifyAndReadPreimage( alloc: Allocator, input: file_mutation_contract.FileMutationInput, policy: PolicyEvaluatedFileTargets,) FileMutationPrepareError!PreimageResult { const zio = io_mod.getIo(); var current_dir = std.Io.Dir.openDirAbsolute( zio, policy.anchorPath(), .{ .follow_symlinks = false }, ) catch |err| { if (operationalError(err)) |operational| return operational; return .{ .semantic_failure = identity_changed_message }; }; defer current_dir.close(zio);
const anchor_identity = pathing.directoryIdentity(current_dir) catch |err| { if (operationalError(err)) |operational| return operational; return .{ .semantic_failure = identity_changed_message }; }; if (!identityEql(anchor_identity, policy.anchor.identity)) { return .{ .semantic_failure = identity_changed_message }; }
var missing_parent_seen = false; for (policy.traversal_directories) |entry| { const component = policy.component( policy.anchor.relative_components[entry.component_index], ); switch (entry.state) { .existing => |expected_identity| { if (missing_parent_seen) { return .{ .semantic_failure = identity_changed_message }; } const next_dir = current_dir.openDir( zio, component, .{ .follow_symlinks = false }, ) catch |err| { if (operationalError(err)) |operational| return operational; return .{ .semantic_failure = identity_changed_message }; }; const actual_identity = pathing.directoryIdentity(next_dir) catch |err| { next_dir.close(zio); if (operationalError(err)) |operational| return operational; return .{ .semantic_failure = identity_changed_message }; }; if (!identityEql(actual_identity, expected_identity)) { next_dir.close(zio); return .{ .semantic_failure = identity_changed_message }; } current_dir.close(zio); current_dir = next_dir; }, .create => { if (missing_parent_seen) continue; _ = current_dir.statFile( zio, component, .{ .follow_symlinks = false }, ) catch |err| switch (err) { error.FileNotFound => { missing_parent_seen = true; continue; }, else => { if (operationalError(err)) |operational| return operational; return .{ .semantic_failure = identity_changed_message }; }, }; return .{ .semantic_failure = identity_changed_message }; }, } }
const target_span = policy.anchor.relative_components[ policy.anchor.relative_components.len - 1 ]; const target_name = policy.component(target_span); const expected_target_identity = policy.items[0].expected_identity; if (missing_parent_seen) { if (expected_target_identity != null or input != .write) { return .{ .semantic_failure = identity_changed_message }; } return .{ .preimage = .absent }; }
const path_stat = current_dir.statFile( zio, target_name, .{ .follow_symlinks = false }, ) catch |err| switch (err) { error.FileNotFound => { if (expected_target_identity != null or input != .write) { return .{ .semantic_failure = identity_changed_message }; } return .{ .preimage = .absent }; }, else => { if (operationalError(err)) |operational| return operational; return .{ .semantic_failure = identity_changed_message }; }, }; const expected_identity = expected_target_identity orelse { return .{ .semantic_failure = identity_changed_message }; }; if (path_stat.kind != .file or expected_identity.kind != .file) { return .{ .semantic_failure = "file mutation preparation failed: target is not a regular file" }; }
var file = current_dir.openFile(zio, target_name, .{ .allow_directory = false, .follow_symlinks = false, }) catch |err| { if (operationalError(err)) |operational| return operational; return .{ .semantic_failure = identity_changed_message }; }; defer file.close(zio);
const file_stat = file.stat(zio) catch |err| { if (operationalError(err)) |operational| return operational; return .{ .semantic_failure = identity_changed_message }; }; const actual_identity: file_mutation_contract.FileIdentity = .{ .device = try pathing.descriptorDevice(file.handle), .inode = @intCast(file_stat.inode), .kind = file_stat.kind, }; if (!identityEql(actual_identity, expected_identity)) { return .{ .semantic_failure = identity_changed_message }; }
var read_buf: [8192]u8 = undefined; var reader = file.reader(zio, &read_buf); const content = reader.interface.allocRemaining( alloc, .limited(max_content_bytes + 1), ) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, error.StreamTooLong => return .{ .semantic_failure = "file mutation preparation failed: preimage exceeds the 4 MiB preparation limit" }, error.ReadFailed => { const root = reader.err orelse return error.Unexpected; if (operationalError(root)) |operational| return operational; return .{ .semantic_failure = "file mutation preparation failed: unable to read the approved preimage" }; }, }; if (content.len > max_content_bytes) { return .{ .semantic_failure = "file mutation preparation failed: preimage exceeds the 4 MiB preparation limit" }; }
var content_hasher = std.crypto.hash.sha2.Sha256.init(.{}); content_hasher.update(content); return .{ .preimage = .{ .present = .{ .content = content, .content_hash = content_hasher.finalResult(), } } };}
fn derivePostimage( alloc: Allocator, input: file_mutation_contract.FileMutationInput, preimage: file_mutation_contract.Preimage,) error{OutOfMemory}!PostimageResult { return switch (input) { .write => |write| .{ .content = try alloc.dupe(u8, write.content), }, .edit => |edit| blk: { if (std.mem.eql(u8, edit.old_string, edit.new_string)) { break :blk .{ .semantic_failure = "edit_file failed: old_string and new_string are identical" }; } const before = switch (preimage) { .absent => break :blk .{ .semantic_failure = identity_changed_message }, .present => |present| present.content, }; const occurrence_count = countOccurrences(before, edit.old_string); if (occurrence_count == 0) { break :blk .{ .semantic_failure = "edit_file failed: old_string not found in file. Re-read the file to see its current contents; if the change is already applied, do not retry this edit." }; } if (occurrence_count > 1) { break :blk .{ .semantic_failure = try std.fmt.allocPrint( alloc, "edit_file failed: old_string is not unique (found {d} occurrences), provide more context", .{occurrence_count}, ) }; }
const match_start = std.mem.find( u8, before, edit.old_string, ).?; const prefix_len = match_start; const suffix_start = match_start + edit.old_string.len; var after_len = std.math.add( usize, prefix_len, edit.new_string.len, ) catch break :blk .{ .semantic_failure = "edit_file failed: postimage exceeds the 4 MiB preparation limit" }; after_len = std.math.add( usize, after_len, before.len - suffix_start, ) catch break :blk .{ .semantic_failure = "edit_file failed: postimage exceeds the 4 MiB preparation limit" }; if (after_len > max_content_bytes) { break :blk .{ .semantic_failure = "edit_file failed: postimage exceeds the 4 MiB preparation limit" }; }
const after = try alloc.alloc(u8, after_len); @memcpy(after[0..prefix_len], before[0..prefix_len]); const replacement_end = prefix_len + edit.new_string.len; @memcpy(after[prefix_len..replacement_end], edit.new_string); @memcpy(after[replacement_end..], before[suffix_start..]); break :blk .{ .content = after }; }, };}
fn countOccurrences(haystack: []const u8, needle: []const u8) usize { if (needle.len == 0) return 0; var count: usize = 0; var index: usize = 0; while (std.mem.find(u8, haystack[index..], needle)) |relative| { count += 1; index += relative + needle.len; } return count;}
fn buildPreview( alloc: Allocator, display_path: text_utils.EncodedPathTail, preimage: file_mutation_contract.Preimage, before_content: []const u8, after_content: []const u8,) error{OutOfMemory}!PreviewResult { var projection = diff_mod.buildBoundedPreview( alloc, before_content, after_content, .{}, ) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, error.InvalidPreviewOptions, error.PreviewBudgetExceeded, error.ArithmeticOverflow, => return .{ .semantic_failure = preview_too_large_message }, };
const lines = if (projection.additions == 0 and projection.deletions == 0) blk: { projection.deinit(alloc); const notice = switch (preimage) { .absent => if (after_content.len == 0) "empty file" else "no content changes", .present => "no content changes", }; const notice_text = try alloc.dupe(u8, notice); errdefer alloc.free(notice_text); const notice_lines = try alloc.alloc(diff_mod.PreviewLine, 1); notice_lines[0] = .{ .op = .notice, .text = notice_text }; break :blk notice_lines; } else blk: { const preview_lines = alloc.alloc( diff_mod.PreviewLine, projection.lines.len, ) catch |err| { projection.deinit(alloc); return err; }; for (projection.lines, preview_lines) |source, *destination| { destination.* = .{ .op = switch (source.op) { .context => .context, .addition => .addition, .deletion => .deletion, .elision => .elision, .notice => .notice, }, .old_line = source.old_line, .new_line = source.new_line, .text = source.text, }; } alloc.free(projection.lines); break :blk preview_lines; };
const preview: diff_mod.FileChangePreview = .{ .path = display_path.bytes, .path_basename_start = display_path.basename_start, .path_source_truncated = display_path.source_truncated, .lines = lines, .additions = projection.additions, .deletions = projection.deletions, .truncated = projection.truncated, }; preview.validate() catch return .{ .semantic_failure = preview_too_large_message };
var projection_charge = display_path.bytes.len; projection_charge = checkedCharge( projection_charge, lines.len, @sizeOf(diff_mod.PreviewLine), ) orelse return .{ .semantic_failure = preview_too_large_message }; for (lines) |line| { projection_charge = std.math.add( usize, projection_charge, line.text.len, ) catch return .{ .semantic_failure = preview_too_large_message }; } if (projection_charge > diff_mod.max_request_projection_bytes) { return .{ .semantic_failure = preview_too_large_message }; } return .{ .preview = preview };}
fn identityEql( a: file_mutation_contract.FileIdentity, b: file_mutation_contract.FileIdentity,) bool { return a.device == b.device and a.inode == b.inode and a.kind == b.kind;}
fn optionalIdentityEql( a: ?file_mutation_contract.FileIdentity, b: ?file_mutation_contract.FileIdentity,) bool { if (a) |actual| { const expected = b orelse return false; return identityEql(actual, expected); } return b == null;}
fn operationalError(err: anyerror) ?FileMutationPrepareError { return switch (err) { error.OutOfMemory => error.OutOfMemory, error.Canceled => error.Canceled, error.Unexpected => error.Unexpected, error.SystemResources => error.SystemResources, error.ProcessFdQuotaExceeded => error.ProcessFdQuotaExceeded, error.SystemFdQuotaExceeded => error.SystemFdQuotaExceeded, else => null, };}
fn writeArgumentsJson( alloc: Allocator, path: []const u8, content: []const u8,) ![]u8 { var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit();
try out.writer.writeAll("{\"path\":"); try std.json.Stringify.value(path, .{}, &out.writer); try out.writer.writeAll(",\"content\":"); try std.json.Stringify.value(content, .{}, &out.writer); try out.writer.writeByte('}'); return out.toOwnedSlice();}
fn editArgumentsJson( alloc: Allocator, path: []const u8, old_string: []const u8, new_string: []const u8,) ![]u8 { var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit();
try out.writer.writeAll("{\"path\":"); try std.json.Stringify.value(path, .{}, &out.writer); try out.writer.writeAll(",\"old_string\":"); try std.json.Stringify.value(old_string, .{}, &out.writer); try out.writer.writeAll(",\"new_string\":"); try std.json.Stringify.value(new_string, .{}, &out.writer); try out.writer.writeByte('}'); return out.toOwnedSlice();}
fn workspaceRoot(alloc: Allocator, tmp: std.testing.TmpDir) ![]u8 { return io_mod.dirRealpathAlloc(alloc, tmp.dir, ".");}
fn createFile( tmp: *std.testing.TmpDir, sub_path: []const u8, content: []const u8,) !void { var file = try tmp.dir.createFile(std.testing.io, sub_path, .{}); defer file.close(std.testing.io); try file.writeStreamingAll(std.testing.io, content);}
fn evaluatePolicy( alloc: Allocator, workspace_root: []const u8, call: types.ToolCall,) !PolicyEvaluatedFileTargets { const input = try decodeTestMutationInput(alloc, call); const result = try testing_permissions.evaluateFileMutationTargets( alloc, workspace_root, input, .ask, .{}, &.{}, &.{}, ); return switch (result) { .evaluated => |evaluated| evaluated, .target_resolution_failure, .policy_denied => error.UnexpectedPolicyFailure, };}
fn expectPrepared( alloc: Allocator, call: types.ToolCall, policy: PolicyEvaluatedFileTargets,) !PreparedFileMutation { const input = try decodeTestMutationInput(alloc, call); return switch (try prepare(alloc, call, input, policy)) { .prepared => |prepared| prepared, .semantic_failure => error.UnexpectedSemanticFailure, };}
fn expectSemanticFailure( alloc: Allocator, call: types.ToolCall, policy: PolicyEvaluatedFileTargets,) ![]const u8 { const input = try decodeTestMutationInput(alloc, call); return switch (try prepare(alloc, call, input, policy)) { .prepared => error.UnexpectedPreparedMutation, .semantic_failure => |message| message, };}
fn decodeTestMutationInput( alloc: Allocator, call: types.ToolCall,) !file_mutation_contract.FileMutationInput { const kind: file_mutation_contract.Kind = if (std.mem.eql(u8, call.name, "write_file")) .write else if (std.mem.eql(u8, call.name, "edit_file")) .edit else return error.InvalidToolArguments; const tool = switch (kind) { .write => &testing_builtin_tools.write_file, .edit => &testing_builtin_tools.edit_file, }; const decoded = try tool.decode(.{ .allocator = alloc }, call.arguments_json); return switch (decoded) { .failure => error.InvalidToolArguments, .input => |tool_input| blk: { break :blk switch (kind) { .write => testing_write_file.takeFileMutationInput( tool_input, alloc, ), .edit => testing_edit_file.takeFileMutationInput( tool_input, alloc, ), }; }, };}
fn expectTerminalSafe(text: []const u8) !void { try std.testing.expect(std.unicode.utf8ValidateSlice(text)); try std.testing.expect(std.mem.findScalar(u8, text, 0x1b) == null); try std.testing.expect(std.mem.findScalar(u8, text, '\n') == null); try std.testing.expect(std.mem.findScalar(u8, text, '\r') == null);}
fn expectNoStageFiles(root: []const u8) !void { var dir = try std.Io.Dir.openDirAbsolute(std.testing.io, root, .{ .iterate = true }); defer dir.close(std.testing.io); var walker = try dir.walk(std.testing.allocator); defer walker.deinit(); while (try walker.next(std.testing.io)) |entry| { try std.testing.expect(std.mem.find(u8, entry.path, ".fx-stage-") == null); }}
test "prepare derives a missing write without creating the target" { var tmp = std.testing.tmpDir(.{ .iterate = true }); defer tmp.cleanup(); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); const root = try workspaceRoot(arena, tmp); const arguments = try writeArgumentsJson( arena, "missing/parents/new.txt", "hello\n", ); const call: types.ToolCall = .{ .id = "write-missing", .name = "write_file", .arguments_json = arguments, }; const policy = try evaluatePolicy(arena, root, call);
var input: file_mutation_contract.FileMutationInput = .{ .write = .{ .path = try arena.dupe(u8, "missing/parents/new.txt"), .content = try arena.dupe(u8, "hello\n"), } }; defer input.deinit(arena); const prepared = switch (try prepare(arena, call, input, policy)) { .prepared => |value| value, .semantic_failure => return error.UnexpectedSemanticFailure, };
try std.testing.expectEqual(file_mutation_contract.Kind.write, prepared.kind); try std.testing.expect(prepared.preimage == .absent); try std.testing.expectEqualStrings("hello\n", prepared.after_content); try std.testing.expectEqualStrings("missing/parents/new.txt", prepared.display_path); try std.testing.expect(prepared.target_path.ptr == policy.canonical_target_path.ptr); try std.testing.expect(PolicyEvaluatedFileTargets.authorizationEql(policy, prepared.policy_targets)); try std.testing.expectEqual(@as(usize, 1), prepared.preview.additions); try std.testing.expectEqual(@as(usize, 0), prepared.preview.deletions); try std.testing.expectError( error.FileNotFound, tmp.dir.statFile(std.testing.io, "missing", .{}), );}
test "prepare shows the canonical external target when a symlink redirects outside the workspace" { if (builtin.os.tag == .windows) return error.SkipZigTest;
var tmp = std.testing.tmpDir(.{ .iterate = true }); defer tmp.cleanup(); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator();
try tmp.dir.createDir(std.testing.io, "workspace", .default_dir); try tmp.dir.createDir(std.testing.io, "outside", .default_dir); tmp.dir.symLink(io_mod.getIo(), "../outside", "workspace/link", .{ .is_directory = true }) catch |err| switch (err) { error.AccessDenied, error.PermissionDenied => return error.SkipZigTest, else => return err, };
const workspace = try io_mod.dirRealpathAlloc(arena, tmp.dir, "workspace"); const misleading_path = try std.fs.path.join(arena, &.{ workspace, "link/new.txt" }); const call: types.ToolCall = .{ .id = "write-symlink-redirect", .name = "write_file", .arguments_json = try writeArgumentsJson(arena, misleading_path, "hello\n"), }; const policy = try evaluatePolicy(arena, workspace, call); const prepared = try expectPrepared(arena, call, policy);
try std.testing.expectEqual(.external, prepared.policy_targets.anchor.scope); try std.testing.expectEqualStrings(policy.canonical_target_path, prepared.display_path); try std.testing.expectEqualStrings(prepared.display_path, prepared.preview.path);}
test "prepare retains full review separately from the bounded approval preview" { var tmp = std.testing.tmpDir(.{ .iterate = true }); defer tmp.cleanup(); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); const root = try workspaceRoot(arena, tmp);
var content: std.ArrayList(u8) = .empty; defer content.deinit(std.testing.allocator); for (1..31) |line_number| { var line: [32]u8 = undefined; try content.appendSlice( std.testing.allocator, try std.fmt.bufPrint(&line, "line {d}\n", .{line_number}), ); }
const call: types.ToolCall = .{ .id = "write-full-review", .name = "write_file", .arguments_json = try writeArgumentsJson( arena, "full-review.txt", content.items, ), }; const policy = try evaluatePolicy(arena, root, call); var prepared = try expectPrepared(arena, call, policy);
try std.testing.expect(prepared.preview.lines.len <= diff_mod.max_preview_lines); var viewport = try prepared.review.viewport(arena, 24, 6); defer viewport.deinit(arena); try std.testing.expectEqual(@as(usize, 30), viewport.total_rows); try std.testing.expectEqualStrings("line 25", viewport.lines[0].text); try std.testing.expectEqualStrings("line 30", viewport.lines[5].text);}
test "prepare derives an existing write from the exact reviewed preimage" { var tmp = std.testing.tmpDir(.{ .iterate = true }); defer tmp.cleanup(); try createFile(&tmp, "note.txt", "old\n"); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); const root = try workspaceRoot(arena, tmp); const arguments = try writeArgumentsJson(arena, "note.txt", "new\n"); const call: types.ToolCall = .{ .id = "write-existing", .name = "write_file", .arguments_json = arguments, }; const policy = try evaluatePolicy(arena, root, call);
const prepared = try expectPrepared(arena, call, policy);
const present = prepared.preimage.present; try std.testing.expectEqualStrings("old\n", present.content); var expected_hasher = std.crypto.hash.sha2.Sha256.init(.{}); expected_hasher.update("old\n"); try std.testing.expectEqualSlices(u8, &expected_hasher.finalResult(), &present.content_hash); try std.testing.expectEqualStrings("new\n", prepared.after_content); try std.testing.expectEqual(@as(usize, 1), prepared.preview.additions); try std.testing.expectEqual(@as(usize, 1), prepared.preview.deletions);
var file = try tmp.dir.openFile(std.testing.io, "note.txt", .{}); defer file.close(std.testing.io); const unchanged = try io_mod.readFileToEnd(arena, &file, 64); try std.testing.expectEqualStrings("old\n", unchanged);}
test "prepare computes one exact edit occurrence" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try createFile(&tmp, "note.txt", "alpha\nbeta\ngamma\n"); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); const root = try workspaceRoot(arena, tmp); const arguments = try editArgumentsJson(arena, "note.txt", "beta", "BETA"); const call: types.ToolCall = .{ .id = "edit-exact", .name = "edit_file", .arguments_json = arguments, }; const policy = try evaluatePolicy(arena, root, call);
const prepared = try expectPrepared(arena, call, policy);
try std.testing.expectEqual(file_mutation_contract.Kind.edit, prepared.kind); try std.testing.expectEqualStrings("alpha\nbeta\ngamma\n", prepared.preimage.present.content); try std.testing.expectEqualStrings("alpha\nBETA\ngamma\n", prepared.after_content); try std.testing.expectEqual(@as(usize, 1), prepared.preview.additions); try std.testing.expectEqual(@as(usize, 1), prepared.preview.deletions);}
test "prepare preserves exact edit semantic failures" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try createFile(&tmp, "note.txt", "same twice same\n"); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); const root = try workspaceRoot(arena, tmp);
const identical_args = try editArgumentsJson(arena, "note.txt", "same", "same"); const identical_call: types.ToolCall = .{ .id = "edit-identical", .name = "edit_file", .arguments_json = identical_args, }; const identical_policy = try evaluatePolicy(arena, root, identical_call); try std.testing.expectEqualStrings( "edit_file failed: old_string and new_string are identical", try expectSemanticFailure(arena, identical_call, identical_policy), );
const missing_args = try editArgumentsJson(arena, "note.txt", "missing", "new"); const missing_call: types.ToolCall = .{ .id = "edit-missing", .name = "edit_file", .arguments_json = missing_args, }; const missing_policy = try evaluatePolicy(arena, root, missing_call); try std.testing.expectEqualStrings( "edit_file failed: old_string not found in file. Re-read the file to see its current contents; if the change is already applied, do not retry this edit.", try expectSemanticFailure(arena, missing_call, missing_policy), );
const duplicate_args = try editArgumentsJson(arena, "note.txt", "same", "new"); const duplicate_call: types.ToolCall = .{ .id = "edit-duplicate", .name = "edit_file", .arguments_json = duplicate_args, }; const duplicate_policy = try evaluatePolicy(arena, root, duplicate_call); try std.testing.expectEqualStrings( "edit_file failed: old_string is not unique (found 2 occurrences), provide more context", try expectSemanticFailure(arena, duplicate_call, duplicate_policy), );}
test "zero-occurrence edit failure explains recovery and leaves the file untouched" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); // The field is already gone, so a repeated removal must fail with // guidance rather than a bare mismatch. try createFile(&tmp, "strategy.ts", "export interface RacePlan {\n laps: number;\n}\n"); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); const root = try workspaceRoot(arena, tmp); const arguments = try editArgumentsJson(arena, "strategy.ts", " trajectory?: Trajectory;\n}", "}"); const call: types.ToolCall = .{ .id = "edit-already-applied", .name = "edit_file", .arguments_json = arguments, }; const policy = try evaluatePolicy(arena, root, call);
const failure = try expectSemanticFailure(arena, call, policy); try std.testing.expect(std.mem.find(u8, failure, "old_string not found in file") != null); try std.testing.expect(std.mem.find(u8, failure, "Re-read the file") != null); try std.testing.expect(std.mem.find(u8, failure, "if the change is already applied, do not retry") != null);
var file = try tmp.dir.openFile(std.testing.io, "strategy.ts", .{}); defer file.close(std.testing.io); const unchanged = try io_mod.readFileToEnd(arena, &file, 4096); try std.testing.expectEqualStrings("export interface RacePlan {\n laps: number;\n}\n", unchanged);}
test "prepare emits explicit empty and no-change notices" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try createFile(&tmp, "same.txt", "same\n"); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); const root = try workspaceRoot(arena, tmp);
const empty_args = try writeArgumentsJson(arena, "empty.txt", ""); const empty_call: types.ToolCall = .{ .id = "write-empty", .name = "write_file", .arguments_json = empty_args, }; const empty_policy = try evaluatePolicy(arena, root, empty_call); const empty = try expectPrepared(arena, empty_call, empty_policy); try std.testing.expectEqual(@as(usize, 0), empty.preview.additions); try std.testing.expectEqual(@as(usize, 0), empty.preview.deletions); try std.testing.expectEqual(@as(usize, 1), empty.preview.lines.len); try std.testing.expectEqual(diff_mod.PreviewOp.notice, empty.preview.lines[0].op); try std.testing.expectEqualStrings("empty file", empty.preview.lines[0].text);
const same_args = try writeArgumentsJson(arena, "same.txt", "same\n"); const same_call: types.ToolCall = .{ .id = "write-same", .name = "write_file", .arguments_json = same_args, }; const same_policy = try evaluatePolicy(arena, root, same_call); const same = try expectPrepared(arena, same_call, same_policy); try std.testing.expectEqual(@as(usize, 0), same.preview.additions); try std.testing.expectEqual(@as(usize, 0), same.preview.deletions); try std.testing.expectEqualStrings("no content changes", same.preview.lines[0].text);}
test "prepare terminal-encodes hostile path and preview content" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); const root = try workspaceRoot(arena, tmp); const hostile_path = "name\x1b[31m\nfile.txt"; const hostile_content = "line\x1b[31mred\x1b[0m\n"; const arguments = try writeArgumentsJson(arena, hostile_path, hostile_content); const call: types.ToolCall = .{ .id = "write-hostile", .name = "write_file", .arguments_json = arguments, }; const policy = try evaluatePolicy(arena, root, call);
const prepared = try expectPrepared(arena, call, policy);
try expectTerminalSafe(prepared.display_path); try std.testing.expect(!std.mem.eql(u8, hostile_path, prepared.display_path)); for (prepared.preview.lines) |line| try expectTerminalSafe(line.text); try std.testing.expectError( error.FileNotFound, tmp.dir.statFile(std.testing.io, hostile_path, .{}), );}
test "prepare enforces independent preimage and postimage caps" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); const root = try workspaceRoot(arena, tmp);
const oversized_preimage = try arena.alloc(u8, max_content_bytes + 1); @memset(oversized_preimage, 'x'); try createFile(&tmp, "oversized.txt", oversized_preimage); const preimage_call: types.ToolCall = .{ .id = "oversized-preimage", .name = "write_file", .arguments_json = try writeArgumentsJson(arena, "oversized.txt", "new"), }; const preimage_policy = try evaluatePolicy(arena, root, preimage_call); try std.testing.expectEqualStrings( "file mutation preparation failed: preimage exceeds the 4 MiB preparation limit", try expectSemanticFailure(arena, preimage_call, preimage_policy), );
const maximum_preimage = try arena.alloc(u8, max_content_bytes); @memset(maximum_preimage, 'x'); maximum_preimage[0] = 'a'; try createFile(&tmp, "maximum.txt", maximum_preimage); const postimage_call: types.ToolCall = .{ .id = "oversized-postimage", .name = "edit_file", .arguments_json = try editArgumentsJson(arena, "maximum.txt", "a", "aa"), }; const postimage_policy = try evaluatePolicy(arena, root, postimage_call); try std.testing.expectEqualStrings( "edit_file failed: postimage exceeds the 4 MiB preparation limit", try expectSemanticFailure(arena, postimage_call, postimage_policy), );}
test "prepare hashes exact raw JSON bytes" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); const root = try workspaceRoot(arena, tmp); const call_a: types.ToolCall = .{ .id = "hash-a", .name = "write_file", .arguments_json = "{\"path\":\"note.txt\",\"content\":\"same\"}", }; const call_b: types.ToolCall = .{ .id = "hash-b", .name = "write_file", .arguments_json = "{ \"content\": \"same\", \"path\": \"note.txt\" }", }; const policy_a = try evaluatePolicy(arena, root, call_a); const policy_b = try evaluatePolicy(arena, root, call_b); const prepared_a = try expectPrepared(arena, call_a, policy_a); const prepared_b = try expectPrepared(arena, call_b, policy_b);
var hasher = std.crypto.hash.sha2.Sha256.init(.{}); hasher.update(call_a.arguments_json); try std.testing.expectEqualSlices(u8, &hasher.finalResult(), &prepared_a.arguments_hash); try std.testing.expect(!std.mem.eql( u8, &prepared_a.arguments_hash, &prepared_b.arguments_hash, ));}
test "prepare rejects call target and authority identity mismatches" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try createFile(&tmp, "a.txt", "a"); try createFile(&tmp, "b.txt", "b"); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); const root = try workspaceRoot(arena, tmp); const call_a: types.ToolCall = .{ .id = "target-a", .name = "write_file", .arguments_json = try writeArgumentsJson(arena, "a.txt", "new"), }; const policy_a = try evaluatePolicy(arena, root, call_a); const call_b: types.ToolCall = .{ .id = "target-b", .name = "write_file", .arguments_json = try writeArgumentsJson(arena, "b.txt", "new"), }; try std.testing.expectEqualStrings( "file mutation preparation failed: approved target no longer matches the call", try expectSemanticFailure(arena, call_b, policy_a), );
var changed_anchor = policy_a; changed_anchor.anchor.identity.inode +%= 1; try std.testing.expectEqualStrings( "file mutation preparation failed: approved filesystem identity changed", try expectSemanticFailure(arena, call_a, changed_anchor), );
try createFile(&tmp, "replacement.txt", "replacement"); try tmp.dir.rename("replacement.txt", tmp.dir, "a.txt", std.testing.io); try std.testing.expectEqualStrings( "file mutation preparation failed: approved filesystem identity changed", try expectSemanticFailure(arena, call_a, policy_a), );}
test "prepare rejects an intermediate-directory retarget" { if (builtin.os.tag == .windows) return error.SkipZigTest;
var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDir(std.testing.io, "approved", .default_dir); try tmp.dir.createDir(std.testing.io, "other", .default_dir); { var approved = try tmp.dir.openDir(std.testing.io, "approved", .{}); defer approved.close(std.testing.io); var file = try approved.createFile(std.testing.io, "note.txt", .{}); defer file.close(std.testing.io); try file.writeStreamingAll(std.testing.io, "approved"); } { var other = try tmp.dir.openDir(std.testing.io, "other", .{}); defer other.close(std.testing.io); var file = try other.createFile(std.testing.io, "note.txt", .{}); defer file.close(std.testing.io); try file.writeStreamingAll(std.testing.io, "other"); }
var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); const root = try workspaceRoot(arena, tmp); const call: types.ToolCall = .{ .id = "retarget", .name = "edit_file", .arguments_json = try editArgumentsJson(arena, "approved/note.txt", "approved", "new"), }; const policy = try evaluatePolicy(arena, root, call);
try tmp.dir.rename("approved", tmp.dir, "approved-old", std.testing.io); try tmp.dir.symLink(std.testing.io, "other", "approved", .{ .is_directory = true });
try std.testing.expectEqualStrings( "file mutation preparation failed: approved target no longer matches the call", try expectSemanticFailure(arena, call, policy), );}
test "prepare rejects a corrupted permission target proof" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); const root = try workspaceRoot(arena, tmp); const call: types.ToolCall = .{ .id = "corrupted-proof-prepare", .name = "write_file", .arguments_json = try writeArgumentsJson(arena, "new.txt", "new"), }; const policy = try evaluatePolicy(arena, root, call);
var corrupted_components = try arena.dupe( file_mutation_contract.PathSpan, policy.anchor.relative_components, ); corrupted_components[0].start = policy.anchor.path_end; var corrupted_policy = policy; corrupted_policy.anchor.relative_components = corrupted_components;
try std.testing.expectEqualStrings( target_mismatch_message, try expectSemanticFailure(arena, call, corrupted_policy), ); try std.testing.expectError( error.FileNotFound, tmp.dir.statFile(std.testing.io, "new.txt", .{}), );}
const TestPreparedMutation = struct { input: file_mutation_contract.FileMutationInput, policy: PolicyEvaluatedFileTargets, prepared: PreparedFileMutation,};
fn prepareForApply( alloc: Allocator, workspace_root: []const u8, call: types.ToolCall,) !TestPreparedMutation { const input = try decodeTestMutationInput(alloc, call); const result = try testing_permissions.evaluateFileMutationTargets( alloc, workspace_root, input, .ask, .{}, &.{}, &.{}, ); const policy = switch (result) { .evaluated => |evaluated| evaluated, .target_resolution_failure, .policy_denied => return error.UnexpectedPolicyFailure, }; return .{ .input = input, .policy = policy, .prepared = switch (try prepare(alloc, call, input, policy)) { .prepared => |prepared| prepared, .semantic_failure => return error.UnexpectedSemanticFailure, }, };}
fn executionAuthorization( prepared: TestPreparedMutation,) file_mutation_contract.FileExecutionAuthorization { return .{ .input = prepared.input, .policy_targets = prepared.policy, .prepared = prepared.prepared, };}
fn expectApplyRejected( result: ApplyResult, reason: ApplyRejectionReason,) !ApplyRejection { return switch (result) { .rejected => |rejected| blk: { try std.testing.expectEqual(reason, rejected.reason); break :blk rejected; }, .committed => error.UnexpectedCommittedMutation, };}
fn readTestFile( alloc: Allocator, tmp: *std.testing.TmpDir, sub_path: []const u8,) ![]u8 { var file = try tmp.dir.openFile(std.testing.io, sub_path, .{}); defer file.close(std.testing.io); return io_mod.readFileToEnd(alloc, &file, max_content_bytes + 1);}
test "apply atomically installs reviewed bytes and returns typed commit metadata" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var call_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer call_arena_state.deinit(); var result_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer result_arena_state.deinit(); const call_alloc = call_arena_state.allocator(); const result_alloc = result_arena_state.allocator(); const root = try workspaceRoot(call_alloc, tmp); const call: types.ToolCall = .{ .id = "apply-write", .name = "write_file", .arguments_json = try writeArgumentsJson(call_alloc, "nested/new.txt", "reviewed\n"), }; const prepared = try prepareForApply(call_alloc, root, call); var cancel_flag = std.atomic.Value(bool).init(false);
const result = try apply( call_alloc, result_alloc, call, executionAuthorization(prepared), &cancel_flag, );
const committed_handoff = switch (result) { .committed => |handoff| handoff, .rejected => return error.UnexpectedRejectedMutation, }; try std.testing.expectEqualStrings( prepared.prepared.target_path, committed_handoff.tracker.raw_path, ); try std.testing.expectEqual( file_mutation_contract.Kind.write, committed_handoff.tracker.kind, ); try std.testing.expectEqual( @as(?[]const u8, null), committed_handoff.tracker.previous_content, ); try std.testing.expectEqual( prepared.prepared.preview.additions, committed_handoff.preview.additions, );
const installed = try readTestFile(call_alloc, &tmp, "nested/new.txt"); try std.testing.expectEqualStrings("reviewed\n", installed);}
test "apply installs reviewed bytes beneath an authorized external anchor" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDir(std.testing.io, "workspace", .default_dir); try tmp.dir.createDir(std.testing.io, "external", .default_dir); var call_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer call_arena_state.deinit(); var result_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer result_arena_state.deinit(); const call_alloc = call_arena_state.allocator(); const result_alloc = result_arena_state.allocator(); const workspace = try io_mod.dirRealpathAlloc( call_alloc, tmp.dir, "workspace", ); const external = try io_mod.dirRealpathAlloc( call_alloc, tmp.dir, "external", ); const target_path = try std.fs.path.join( call_alloc, &.{ external, "nested/new.txt" }, ); const call: types.ToolCall = .{ .id = "apply-external", .name = "write_file", .arguments_json = try writeArgumentsJson( call_alloc, target_path, "reviewed\n", ), }; const prepared = try prepareForApply(call_alloc, workspace, call); try std.testing.expectEqual( .external, prepared.policy.anchor.scope, ); var cancel_flag = std.atomic.Value(bool).init(false);
const result = try apply( call_alloc, result_alloc, call, executionAuthorization(prepared), &cancel_flag, );
try std.testing.expect(result == .committed); const installed = try readTestFile( call_alloc, &tmp, "external/nested/new.txt", ); try std.testing.expectEqualStrings("reviewed\n", installed);}
test "apply rejects binding mismatches before allocation or filesystem access" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var owner = std.heap.ArenaAllocator.init(std.testing.allocator); defer owner.deinit(); const owner_alloc = owner.allocator(); const root = try workspaceRoot(owner_alloc, tmp); const call: types.ToolCall = .{ .id = "binding", .name = "write_file", .arguments_json = try writeArgumentsJson(owner_alloc, "new.txt", "new"), }; const prepared = try prepareForApply(owner_alloc, root, call);
var failing_call = std.testing.FailingAllocator.init( std.testing.allocator, .{ .fail_index = 0 }, ); var failing_result = std.testing.FailingAllocator.init( std.testing.allocator, .{ .fail_index = 0 }, ); const call_alloc = failing_call.allocator(); const result_alloc = failing_result.allocator(); var cancel_flag = std.atomic.Value(bool).init(false); const authorization: file_mutation_contract.FileExecutionAuthorization = .{ .input = prepared.input, .policy_targets = prepared.policy, .prepared = null, };
const rejected = try expectApplyRejected( try apply( call_alloc, result_alloc, call, authorization, &cancel_flag, ), .binding_mismatch, ); try std.testing.expectEqual(@as(usize, 0), rejected.created_parent_residue.len); try std.testing.expectEqual(@as(usize, 0), failing_call.allocated_bytes); try std.testing.expectEqual(@as(usize, 0), failing_result.allocated_bytes); try std.testing.expectError( error.FileNotFound, tmp.dir.statFile(std.testing.io, "new.txt", .{}), );}
test "apply rejects a corrupted prepared policy proof before mutation" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var call_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer call_arena_state.deinit(); var result_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer result_arena_state.deinit(); const call_alloc = call_arena_state.allocator(); const result_alloc = result_arena_state.allocator(); const root = try workspaceRoot(call_alloc, tmp); const call: types.ToolCall = .{ .id = "corrupted-proof-apply", .name = "write_file", .arguments_json = try writeArgumentsJson(call_alloc, "new.txt", "new"), }; var prepared = try prepareForApply(call_alloc, root, call); var corrupted_components = try call_alloc.dupe( file_mutation_contract.PathSpan, prepared.policy.anchor.relative_components, ); corrupted_components[0].start = prepared.policy.anchor.path_end; var corrupted_policy = prepared.policy; corrupted_policy.anchor.relative_components = corrupted_components; prepared.policy = corrupted_policy; prepared.prepared.policy_targets = corrupted_policy;
var cancel_flag = std.atomic.Value(bool).init(false); const rejected = try expectApplyRejected( try apply( call_alloc, result_alloc, call, executionAuthorization(prepared), &cancel_flag, ), .binding_mismatch, ); try std.testing.expectEqual(@as(usize, 0), rejected.created_parent_residue.len); try std.testing.expectError( error.FileNotFound, tmp.dir.statFile(std.testing.io, "new.txt", .{}), );}
test "apply allocation failure before rename leaves target unchanged" { var tmp = std.testing.tmpDir(.{ .iterate = true }); defer tmp.cleanup(); try createFile(&tmp, "note.txt", "old"); var owner = std.heap.ArenaAllocator.init(std.testing.allocator); defer owner.deinit(); const call_alloc = owner.allocator(); const root = try workspaceRoot(call_alloc, tmp); const call: types.ToolCall = .{ .id = "allocation-failure", .name = "write_file", .arguments_json = try writeArgumentsJson(call_alloc, "note.txt", "new"), }; const prepared = try prepareForApply(call_alloc, root, call); var failing = std.testing.FailingAllocator.init( std.testing.allocator, .{ .fail_index = 3 }, ); var test_state = StageTestState{}; var cancel_flag = std.atomic.Value(bool).init(false);
try std.testing.expectError( error.OutOfMemory, applyWithTestControls( call_alloc, failing.allocator(), call, executionAuthorization(prepared), &cancel_flag, .{ .ctx = &test_state, .after_stage = mutateAfterStage }, ), ); try std.testing.expect(!test_state.reached); try std.testing.expectEqual(failing.allocated_bytes, failing.freed_bytes); const unchanged = try readTestFile(call_alloc, &tmp, "note.txt"); try std.testing.expectEqualStrings("old", unchanged); try expectNoStageFiles(root);}
test "apply rejects stale absent targets and stale existing preimages" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try createFile(&tmp, "existing.txt", "old"); var call_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer call_arena_state.deinit(); var result_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer result_arena_state.deinit(); const call_alloc = call_arena_state.allocator(); const result_alloc = result_arena_state.allocator(); const root = try workspaceRoot(call_alloc, tmp);
const missing_call: types.ToolCall = .{ .id = "stale-absent", .name = "write_file", .arguments_json = try writeArgumentsJson(call_alloc, "missing.txt", "new"), }; const missing_prepared = try prepareForApply(call_alloc, root, missing_call); try createFile(&tmp, "missing.txt", "raced"); var cancel_flag = std.atomic.Value(bool).init(false); _ = try expectApplyRejected( try apply( call_alloc, result_alloc, missing_call, executionAuthorization(missing_prepared), &cancel_flag, ), .stale_preimage, ); const raced = try readTestFile(call_alloc, &tmp, "missing.txt"); try std.testing.expectEqualStrings("raced", raced);
const existing_call: types.ToolCall = .{ .id = "stale-existing", .name = "write_file", .arguments_json = try writeArgumentsJson(call_alloc, "existing.txt", "new"), }; const existing_prepared = try prepareForApply(call_alloc, root, existing_call); { var file = try tmp.dir.openFile(std.testing.io, "existing.txt", .{ .mode = .write_only }); defer file.close(std.testing.io); try file.setLength(std.testing.io, 0); try file.writeStreamingAll(std.testing.io, "changed"); } _ = try expectApplyRejected( try apply( call_alloc, result_alloc, existing_call, executionAuthorization(existing_prepared), &cancel_flag, ), .stale_preimage, ); const changed = try readTestFile(call_alloc, &tmp, "existing.txt"); try std.testing.expectEqualStrings("changed", changed);}
test "apply rejects replaced anchors and existing parents" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDir(std.testing.io, "workspace", .default_dir); try tmp.dir.createDir(std.testing.io, "workspace/parent", .default_dir); try createFile(&tmp, "workspace/parent/note.txt", "old"); var call_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer call_arena_state.deinit(); var result_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer result_arena_state.deinit(); const call_alloc = call_arena_state.allocator(); const result_alloc = result_arena_state.allocator(); const workspace = try io_mod.dirRealpathAlloc(call_alloc, tmp.dir, "workspace"); const call: types.ToolCall = .{ .id = "replace-anchor", .name = "write_file", .arguments_json = try writeArgumentsJson(call_alloc, "parent/note.txt", "new"), }; const prepared = try prepareForApply(call_alloc, workspace, call);
try tmp.dir.rename("workspace", tmp.dir, "workspace-old", std.testing.io); try tmp.dir.createDir(std.testing.io, "workspace", .default_dir); var cancel_flag = std.atomic.Value(bool).init(false); _ = try expectApplyRejected( try apply( call_alloc, result_alloc, call, executionAuthorization(prepared), &cancel_flag, ), .traversal_changed, ); const anchor_unchanged = try readTestFile(call_alloc, &tmp, "workspace-old/parent/note.txt"); try std.testing.expectEqualStrings("old", anchor_unchanged);
try tmp.dir.rename("workspace", tmp.dir, "workspace-empty", std.testing.io); try tmp.dir.rename("workspace-old", tmp.dir, "workspace", std.testing.io); const parent_call: types.ToolCall = .{ .id = "replace-parent", .name = "write_file", .arguments_json = try writeArgumentsJson(call_alloc, "parent/note.txt", "new"), }; const parent_prepared = try prepareForApply(call_alloc, workspace, parent_call); try tmp.dir.rename("workspace/parent", tmp.dir, "workspace/parent-old", std.testing.io); try tmp.dir.createDir(std.testing.io, "workspace/parent", .default_dir); _ = try expectApplyRejected( try apply( call_alloc, result_alloc, parent_call, executionAuthorization(parent_prepared), &cancel_flag, ), .traversal_changed, ); const parent_unchanged = try readTestFile(call_alloc, &tmp, "workspace/parent-old/note.txt"); try std.testing.expectEqualStrings("old", parent_unchanged);}
const CreatedParentReplacement = enum { none, shallow, deep,};
const StageTestState = struct { reached: bool = false, cancel_flag: ?*std.atomic.Value(bool) = null, workspace_root: ?[]const u8 = null, replace_created_parent: CreatedParentReplacement = .none, replace_temp: bool = false, mutate_temp: bool = false, add_blocker: bool = false,};
fn mutateAfterStage( ctx: ?*anyopaque, parent: std.Io.Dir, temp_name: []const u8,) anyerror!void { const state: *StageTestState = @ptrCast(@alignCast(ctx.?)); state.reached = true; if (state.replace_created_parent != .none) { var root = std.Io.Dir.openDirAbsolute( std.testing.io, state.workspace_root.?, .{}, ) catch return error.TestControlFailed; defer root.close(std.testing.io); switch (state.replace_created_parent) { .none => unreachable, .shallow => { root.rename("one", root, "one-original", std.testing.io) catch return error.TestControlFailed; root.createDir(std.testing.io, "one", .default_dir) catch return error.TestControlFailed; }, .deep => { root.rename("one/two", root, "one/two-original", std.testing.io) catch return error.TestControlFailed; root.createDir(std.testing.io, "one/two", .default_dir) catch return error.TestControlFailed; }, } } if (state.replace_temp) { parent.rename(temp_name, parent, ".removed-stage", std.testing.io) catch return error.TestControlFailed; parent.deleteFile(std.testing.io, ".removed-stage") catch return error.TestControlFailed; var replacement = parent.createFile(std.testing.io, temp_name, .{ .truncate = false, .exclusive = true, }) catch return error.TestControlFailed; defer replacement.close(std.testing.io); replacement.writeStreamingAll(std.testing.io, "foreign") catch return error.TestControlFailed; } if (state.mutate_temp) { var staged = parent.openFile(std.testing.io, temp_name, .{ .mode = .write_only, .follow_symlinks = false, .resolve_beneath = true, }) catch return error.TestControlFailed; defer staged.close(std.testing.io); staged.writeStreamingAll(std.testing.io, "tampered") catch return error.TestControlFailed; staged.sync(std.testing.io) catch return error.TestControlFailed; } if (state.add_blocker) { var blocker = parent.createFile(std.testing.io, "blocker", .{ .truncate = false, .exclusive = true, }) catch return error.TestControlFailed; blocker.close(std.testing.io); } if (state.cancel_flag) |cancel_flag| cancel_flag.store(true, .seq_cst);}
const StageChunkTestState = struct { cancel_flag: *std.atomic.Value(bool), chunk_count: usize = 0,};
fn cancelAfterFirstStageChunk( ctx: ?*anyopaque, _: usize,) anyerror!void { const state: *StageChunkTestState = @ptrCast(@alignCast(ctx.?)); state.chunk_count += 1; if (state.chunk_count == 1) state.cancel_flag.store(true, .seq_cst);}
fn replaceTargetAfterFinalPreimageRead( _: ?*anyopaque, parent: std.Io.Dir, target_name: []const u8,) anyerror!void { parent.rename( target_name, parent, "reviewed-target", std.testing.io, ) catch return error.TestControlFailed; var replacement = parent.createFile(std.testing.io, target_name, .{ .truncate = false, .exclusive = true, }) catch return error.TestControlFailed; defer replacement.close(std.testing.io); replacement.writeStreamingAll(std.testing.io, "old") catch return error.TestControlFailed;}
const MoveParentTestState = struct { tmp: *std.testing.TmpDir, reached: bool = false,};
fn moveParentForTest(state: *MoveParentTestState) !void { state.reached = true; state.tmp.dir.rename( "parent", state.tmp.dir, "moved-parent", std.testing.io, ) catch return error.TestControlFailed; state.tmp.dir.createDir(std.testing.io, "parent", .default_dir) catch return error.TestControlFailed; createFile(state.tmp, "parent/existing.txt", "replacement") catch return error.TestControlFailed;}
fn moveParentAfterFinalPreimageRead( ctx: ?*anyopaque, _: std.Io.Dir, _: []const u8,) anyerror!void { const state: *MoveParentTestState = @ptrCast(@alignCast(ctx.?)); try moveParentForTest(state);}
fn moveParentAfterFinalValidation(ctx: ?*anyopaque) anyerror!void { const state: *MoveParentTestState = @ptrCast(@alignCast(ctx.?)); try moveParentForTest(state);}
test "apply rejects replaced staged sources without deleting foreign replacements" { var tmp = std.testing.tmpDir(.{ .iterate = true }); defer tmp.cleanup(); var call_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer call_arena_state.deinit(); var result_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer result_arena_state.deinit(); const call_alloc = call_arena_state.allocator(); const result_alloc = result_arena_state.allocator(); const root = try workspaceRoot(call_alloc, tmp); const call: types.ToolCall = .{ .id = "replace-stage", .name = "write_file", .arguments_json = try writeArgumentsJson(call_alloc, "new.txt", "reviewed"), }; const prepared = try prepareForApply(call_alloc, root, call); var cancel_flag = std.atomic.Value(bool).init(false); var test_state = StageTestState{ .replace_temp = true };
_ = try expectApplyRejected( try applyWithTestControls( call_alloc, result_alloc, call, executionAuthorization(prepared), &cancel_flag, .{ .ctx = &test_state, .after_stage = mutateAfterStage }, ), .staged_source_changed, ); try std.testing.expectError( error.FileNotFound, tmp.dir.statFile(std.testing.io, "new.txt", .{}), );
var root_dir = try tmp.dir.openDir(std.testing.io, ".", .{ .iterate = true }); defer root_dir.close(std.testing.io); var iterator = root_dir.iterate(); var found_foreign = false; while (try iterator.next(std.testing.io)) |entry| { if (std.mem.startsWith(u8, entry.name, ".fx-stage-")) { const foreign = try readTestFile(call_alloc, &tmp, entry.name); try std.testing.expectEqualStrings("foreign", foreign); found_foreign = true; } } try std.testing.expect(found_foreign);}
test "apply rejects in-place staged content changes before rename" { var tmp = std.testing.tmpDir(.{ .iterate = true }); defer tmp.cleanup(); var call_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer call_arena_state.deinit(); var result_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer result_arena_state.deinit(); const call_alloc = call_arena_state.allocator(); const result_alloc = result_arena_state.allocator(); const root = try workspaceRoot(call_alloc, tmp); const call: types.ToolCall = .{ .id = "mutate-stage", .name = "write_file", .arguments_json = try writeArgumentsJson(call_alloc, "new.txt", "reviewed"), }; const prepared = try prepareForApply(call_alloc, root, call); var cancel_flag = std.atomic.Value(bool).init(false); var test_state = StageTestState{ .mutate_temp = true };
_ = try expectApplyRejected( try applyWithTestControls( call_alloc, result_alloc, call, executionAuthorization(prepared), &cancel_flag, .{ .ctx = &test_state, .after_stage = mutateAfterStage }, ), .staged_source_changed, ); try std.testing.expectError( error.FileNotFound, tmp.dir.statFile(std.testing.io, "new.txt", .{}), );
var root_dir = try tmp.dir.openDir(std.testing.io, ".", .{ .iterate = true }); defer root_dir.close(std.testing.io); var iterator = root_dir.iterate(); while (try iterator.next(std.testing.io)) |entry| { try std.testing.expect(!std.mem.startsWith(u8, entry.name, ".fx-stage-")); }}
test "apply rejects a target pathname replaced during final preimage validation" { var tmp = std.testing.tmpDir(.{ .iterate = true }); defer tmp.cleanup(); try createFile(&tmp, "existing.txt", "old"); var call_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer call_arena_state.deinit(); var result_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer result_arena_state.deinit(); const call_alloc = call_arena_state.allocator(); const result_alloc = result_arena_state.allocator(); const root = try workspaceRoot(call_alloc, tmp); const call: types.ToolCall = .{ .id = "replace-target-during-read", .name = "write_file", .arguments_json = try writeArgumentsJson( call_alloc, "existing.txt", "new", ), }; const prepared = try prepareForApply(call_alloc, root, call); var cancel_flag = std.atomic.Value(bool).init(false);
_ = try expectApplyRejected( try applyWithTestControls( call_alloc, result_alloc, call, executionAuthorization(prepared), &cancel_flag, .{ .after_final_preimage_read = replaceTargetAfterFinalPreimageRead }, ), .stale_preimage, );
const replacement = try readTestFile(call_alloc, &tmp, "existing.txt"); try std.testing.expectEqualStrings("old", replacement); const reviewed = try readTestFile(call_alloc, &tmp, "reviewed-target"); try std.testing.expectEqualStrings("old", reviewed); try expectNoStageFiles(root);}
test "apply rejects a parent moved during final preimage validation" { var tmp = std.testing.tmpDir(.{ .iterate = true }); defer tmp.cleanup(); try tmp.dir.createDir(std.testing.io, "parent", .default_dir); try createFile(&tmp, "parent/existing.txt", "old"); var call_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer call_arena_state.deinit(); var result_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer result_arena_state.deinit(); const call_alloc = call_arena_state.allocator(); const result_alloc = result_arena_state.allocator(); const root = try workspaceRoot(call_alloc, tmp); const call: types.ToolCall = .{ .id = "move-parent-during-final-read", .name = "write_file", .arguments_json = try writeArgumentsJson( call_alloc, "parent/existing.txt", "new", ), }; const prepared = try prepareForApply(call_alloc, root, call); var cancel_flag = std.atomic.Value(bool).init(false); var test_state = MoveParentTestState{ .tmp = &tmp };
_ = try expectApplyRejected( try applyWithTestControls( call_alloc, result_alloc, call, executionAuthorization(prepared), &cancel_flag, .{ .ctx = &test_state, .after_final_preimage_read = moveParentAfterFinalPreimageRead, }, ), .traversal_changed, );
try std.testing.expect(test_state.reached); const canonical = try readTestFile( call_alloc, &tmp, "parent/existing.txt", ); try std.testing.expectEqualStrings("replacement", canonical); const moved = try readTestFile( call_alloc, &tmp, "moved-parent/existing.txt", ); try std.testing.expectEqualStrings("old", moved); try expectNoStageFiles(root);}
test "apply commits through a parent moved after final validation" { var tmp = std.testing.tmpDir(.{ .iterate = true }); defer tmp.cleanup(); try tmp.dir.createDir(std.testing.io, "parent", .default_dir); try createFile(&tmp, "parent/existing.txt", "old"); var call_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer call_arena_state.deinit(); var result_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer result_arena_state.deinit(); const call_alloc = call_arena_state.allocator(); const result_alloc = result_arena_state.allocator(); const root = try workspaceRoot(call_alloc, tmp); const call: types.ToolCall = .{ .id = "move-parent-after-final-validation", .name = "write_file", .arguments_json = try writeArgumentsJson( call_alloc, "parent/existing.txt", "new", ), }; const prepared = try prepareForApply(call_alloc, root, call); var cancel_flag = std.atomic.Value(bool).init(false); var test_state = MoveParentTestState{ .tmp = &tmp };
const result = try applyWithTestControls( call_alloc, result_alloc, call, executionAuthorization(prepared), &cancel_flag, .{ .ctx = &test_state, .after_final_validation = moveParentAfterFinalValidation, }, );
const committed = switch (result) { .committed => |handoff| handoff, .rejected => return error.UnexpectedRejectedMutation, }; try std.testing.expect(test_state.reached); try std.testing.expectEqualStrings( "parent/existing.txt", committed.preview.path, ); const canonical = try readTestFile( call_alloc, &tmp, "parent/existing.txt", ); try std.testing.expectEqualStrings("replacement", canonical); const moved = try readTestFile( call_alloc, &tmp, "moved-parent/existing.txt", ); try std.testing.expectEqualStrings("new", moved); try expectNoStageFiles(root);}
test "apply observes cancellation between bounded stage writes" { var tmp = std.testing.tmpDir(.{ .iterate = true }); defer tmp.cleanup(); var call_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer call_arena_state.deinit(); var result_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer result_arena_state.deinit(); const call_alloc = call_arena_state.allocator(); const result_alloc = result_arena_state.allocator(); const root = try workspaceRoot(call_alloc, tmp); const content = try call_alloc.alloc(u8, 128 * 1024); @memset(content, 'x'); const call: types.ToolCall = .{ .id = "cancel-stage-chunk", .name = "write_file", .arguments_json = try writeArgumentsJson( call_alloc, "one/two/new.txt", content, ), }; const prepared = try prepareForApply(call_alloc, root, call); var cancel_flag = std.atomic.Value(bool).init(false); var test_state = StageChunkTestState{ .cancel_flag = &cancel_flag };
const rejected = try expectApplyRejected( try applyWithTestControls( call_alloc, result_alloc, call, executionAuthorization(prepared), &cancel_flag, .{ .ctx = &test_state, .after_write_chunk = cancelAfterFirstStageChunk, }, ), .cancelled, );
try std.testing.expectEqual(@as(usize, 1), test_state.chunk_count); try std.testing.expectEqual(@as(usize, 0), rejected.created_parent_residue.len); try std.testing.expectError( error.FileNotFound, tmp.dir.statFile(std.testing.io, "one", .{}), ); try expectNoStageFiles(root);}
test "apply rejects replacement of transaction-created parent directories" { inline for ([_]CreatedParentReplacement{ .shallow, .deep }) |replacement| { var tmp = std.testing.tmpDir(.{ .iterate = true }); defer tmp.cleanup(); var call_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer call_arena_state.deinit(); var result_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer result_arena_state.deinit(); const call_alloc = call_arena_state.allocator(); const result_alloc = result_arena_state.allocator(); const root = try workspaceRoot(call_alloc, tmp); const call: types.ToolCall = .{ .id = switch (replacement) { .none => unreachable, .shallow => "replace-created-shallow", .deep => "replace-created-deep", }, .name = "write_file", .arguments_json = try writeArgumentsJson( call_alloc, "one/two/new.txt", "reviewed", ), }; const prepared = try prepareForApply(call_alloc, root, call); var cancel_flag = std.atomic.Value(bool).init(false); var test_state = StageTestState{ .workspace_root = root, .replace_created_parent = replacement, };
const rejected = try expectApplyRejected( try applyWithTestControls( call_alloc, result_alloc, call, executionAuthorization(prepared), &cancel_flag, .{ .ctx = &test_state, .after_stage = mutateAfterStage }, ), .traversal_changed, );
try std.testing.expectEqual(@as(usize, 2), rejected.created_parent_residue.len); try std.testing.expect( rejected.created_parent_residue[0].component_index > rejected.created_parent_residue[1].component_index, ); try std.testing.expectError( error.FileNotFound, tmp.dir.statFile(std.testing.io, "one/two/new.txt", .{}), ); const moved_target = switch (replacement) { .none => unreachable, .shallow => "one-original/two/new.txt", .deep => "one/two-original/new.txt", }; try std.testing.expectError( error.FileNotFound, tmp.dir.statFile(std.testing.io, moved_target, .{}), ); try expectNoStageFiles(root); }}
test "apply cancellation removes its temp and newly created parents" { var tmp = std.testing.tmpDir(.{ .iterate = true }); defer tmp.cleanup(); try createFile(&tmp, "keep.txt", "keep"); var call_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer call_arena_state.deinit(); var result_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer result_arena_state.deinit(); const call_alloc = call_arena_state.allocator(); const result_alloc = result_arena_state.allocator(); const root = try workspaceRoot(call_alloc, tmp); const call: types.ToolCall = .{ .id = "cancel-clean", .name = "write_file", .arguments_json = try writeArgumentsJson(call_alloc, "one/two/new.txt", "reviewed"), }; const prepared = try prepareForApply(call_alloc, root, call); var cancel_flag = std.atomic.Value(bool).init(false); var test_state = StageTestState{ .cancel_flag = &cancel_flag };
const rejected = try expectApplyRejected( try applyWithTestControls( call_alloc, result_alloc, call, executionAuthorization(prepared), &cancel_flag, .{ .ctx = &test_state, .after_stage = mutateAfterStage }, ), .cancelled, ); try std.testing.expectEqual(@as(usize, 0), rejected.created_parent_residue.len); const keep = try readTestFile(call_alloc, &tmp, "keep.txt"); try std.testing.expectEqualStrings("keep", keep); try std.testing.expectError( error.FileNotFound, tmp.dir.statFile(std.testing.io, "one", .{}), ); try expectNoStageFiles(root);}
test "apply returns deepest-first bounded residue when created parents are not empty" { var tmp = std.testing.tmpDir(.{ .iterate = true }); defer tmp.cleanup(); var call_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer call_arena_state.deinit(); var result_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer result_arena_state.deinit(); const call_alloc = call_arena_state.allocator(); const result_alloc = result_arena_state.allocator(); const root = try workspaceRoot(call_alloc, tmp); const call: types.ToolCall = .{ .id = "cancel-residue", .name = "write_file", .arguments_json = try writeArgumentsJson(call_alloc, "one/two/new.txt", "reviewed"), }; const prepared = try prepareForApply(call_alloc, root, call); var cancel_flag = std.atomic.Value(bool).init(false); var test_state = StageTestState{ .cancel_flag = &cancel_flag, .add_blocker = true, };
const rejected = try expectApplyRejected( try applyWithTestControls( call_alloc, result_alloc, call, executionAuthorization(prepared), &cancel_flag, .{ .ctx = &test_state, .after_stage = mutateAfterStage }, ), .cancelled, ); try std.testing.expectEqual(@as(usize, 2), rejected.created_parent_residue.len); try std.testing.expect( rejected.created_parent_residue[0].component_index > rejected.created_parent_residue[1].component_index, ); for (rejected.created_parent_residue) |residue| { try std.testing.expectEqual( ParentCleanupResidueReason.not_empty, residue.reason, ); try std.testing.expect(residue.observed_identity != null); } const blocker = try readTestFile(call_alloc, &tmp, "one/two/blocker"); try std.testing.expectEqualStrings("", blocker);}
test "apply preserves the existing destination mode" { if (builtin.os.tag == .windows) return error.SkipZigTest;
var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try createFile(&tmp, "mode.txt", "old"); { var file = try tmp.dir.openFile(std.testing.io, "mode.txt", .{}); defer file.close(std.testing.io); try file.setPermissions( std.testing.io, std.Io.File.Permissions.fromMode(0o640), ); } var call_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer call_arena_state.deinit(); var result_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer result_arena_state.deinit(); const call_alloc = call_arena_state.allocator(); const result_alloc = result_arena_state.allocator(); const root = try workspaceRoot(call_alloc, tmp); const call: types.ToolCall = .{ .id = "mode", .name = "write_file", .arguments_json = try writeArgumentsJson(call_alloc, "mode.txt", "new"), }; const prepared = try prepareForApply(call_alloc, root, call); var cancel_flag = std.atomic.Value(bool).init(false);
const result = try apply( call_alloc, result_alloc, call, executionAuthorization(prepared), &cancel_flag, ); try std.testing.expect(result == .committed); const stat = try tmp.dir.statFile( std.testing.io, "mode.txt", .{ .follow_symlinks = false }, ); try std.testing.expectEqual( @as(std.posix.mode_t, 0o640), stat.permissions.toMode() & 0o777, );}
test "approved mutation cannot apply twice" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var call_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer call_arena_state.deinit(); var result_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer result_arena_state.deinit(); const call_alloc = call_arena_state.allocator(); const result_alloc = result_arena_state.allocator(); const root = try workspaceRoot(call_alloc, tmp); const call: types.ToolCall = .{ .id = "single-use-proof", .name = "write_file", .arguments_json = try writeArgumentsJson(call_alloc, "new.txt", "reviewed"), }; const prepared = try prepareForApply(call_alloc, root, call); var cancel_flag = std.atomic.Value(bool).init(false);
const first = try apply( call_alloc, result_alloc, call, executionAuthorization(prepared), &cancel_flag, ); try std.testing.expect(first == .committed); try std.testing.expectError( error.ResultContractViolation, apply( call_alloc, result_alloc, call, executionAuthorization(prepared), &cancel_flag, ), ); const installed = try readTestFile(call_alloc, &tmp, "new.txt"); try std.testing.expectEqualStrings("reviewed", installed);}
test "apply commit handoff remains valid after the call owner is destroyed" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var result_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer result_arena_state.deinit(); const result_alloc = result_arena_state.allocator(); var call_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); const call_alloc = call_arena_state.allocator(); const root = try workspaceRoot(call_alloc, tmp); const call: types.ToolCall = .{ .id = "result-lifetime", .name = "write_file", .arguments_json = try writeArgumentsJson(call_alloc, "new.txt", "reviewed"), }; const prepared = try prepareForApply(call_alloc, root, call); var cancel_flag = std.atomic.Value(bool).init(false); const result = try apply( call_alloc, result_alloc, call, executionAuthorization(prepared), &cancel_flag, ); const committed_handoff = switch (result) { .committed => |handoff| handoff, .rejected => return error.UnexpectedRejectedMutation, };
call_arena_state.deinit();
try std.testing.expectEqualStrings("new.txt", committed_handoff.preview.path); try std.testing.expect( std.mem.endsWith(u8, committed_handoff.tracker.raw_path, "new.txt"), ); try std.testing.expectEqual( file_mutation_contract.Kind.write, committed_handoff.tracker.kind, );}