diff --git a/src/agent/auth/auth.zig b/src/agent/auth/auth.zig index c742408f8..5d18ecfa0 100644 --- a/src/agent/auth/auth.zig +++ b/src/agent/auth/auth.zig @@ -133,13 +133,15 @@ fn storeSaveAt(arena: std.mem.Allocator, dir: []const u8, id: []const u8, tokens .account_id = tokens.account_id, }); - // Secrets file: owner read/write only, from the moment it exists. - const file = try std.Io.Dir.cwd().createFile(lp.io, path, .{ .permissions = .fromMode(0o600) }); - defer file.close(lp.io); + // Secrets file: owner read/write only, from the moment it exists. Written + // to a temp file and renamed so a failed write can't corrupt the store. + var af = try std.Io.Dir.cwd().createFileAtomic(lp.io, path, .{ .permissions = .fromMode(0o600), .replace = true }); + defer af.deinit(lp.io); var buf: [1024]u8 = undefined; - var w = file.writer(lp.io, &buf); + var w = af.file.writer(lp.io, &buf); try std.json.Stringify.value(map, .{}, &w.interface); try w.end(); + try af.replace(lp.io); } fn storeDeleteAt(arena: std.mem.Allocator, dir: []const u8, id: []const u8) void { @@ -147,12 +149,13 @@ fn storeDeleteAt(arena: std.mem.Allocator, dir: []const u8, id: []const u8) void const data = std.Io.Dir.cwd().readFileAlloc(lp.io, path, arena, .limited(64 * 1024)) catch return; var parsed = std.json.parseFromSliceLeaky(std.json.ArrayHashMap(StoredToken), arena, data, .{ .ignore_unknown_fields = true }) catch return; _ = parsed.map.swapRemove(id); - const file = std.Io.Dir.cwd().createFile(lp.io, path, .{ .permissions = .fromMode(0o600) }) catch return; - defer file.close(lp.io); + var af = std.Io.Dir.cwd().createFileAtomic(lp.io, path, .{ .permissions = .fromMode(0o600), .replace = true }) catch return; + defer af.deinit(lp.io); var buf: [1024]u8 = undefined; - var w = file.writer(lp.io, &buf); + var w = af.file.writer(lp.io, &buf); std.json.Stringify.value(parsed, .{}, &w.interface) catch return; w.end() catch return; + af.replace(lp.io) catch return; } /// Load the stored token for `id`, or null when absent/unreadable/no data dir. diff --git a/src/agent/auth/models_dev.zig b/src/agent/auth/models_dev.zig index 51b3f5b5c..caf69e163 100644 --- a/src/agent/auth/models_dev.zig +++ b/src/agent/auth/models_dev.zig @@ -40,7 +40,7 @@ pub fn modelIds(arena: std.mem.Allocator, provider_id: []const u8, app_dir: ?[]c } const catalog = fetch(arena) catch return &.{}; const ids = parseProviderModels(arena, catalog, provider_id) catch return &.{}; - if (app_dir) |dir| writeCache(dir, provider_id, ids) catch {}; + if (app_dir) |dir| writeCache(arena, dir, provider_id, ids) catch {}; return ids; } @@ -63,15 +63,15 @@ fn readCache(arena: std.mem.Allocator, app_dir: []const u8, provider_id: []const return parsed.ids; } -fn writeCache(app_dir: []const u8, provider_id: []const u8, ids: []const []const u8) !void { - var arena: std.heap.ArenaAllocator = .init(std.heap.page_allocator); - defer arena.deinit(); - const a = arena.allocator(); - const path = try cachePath(a, app_dir, provider_id); - - var buf: std.Io.Writer.Allocating = .init(a); - try std.json.Stringify.value(Cache{ .fetched_ms = auth.nowMs(), .ids = ids }, .{}, &buf.writer); - try std.Io.Dir.cwd().writeFile(lp.io, .{ .sub_path = path, .data = buf.written() }); +fn writeCache(arena: std.mem.Allocator, app_dir: []const u8, provider_id: []const u8, ids: []const []const u8) !void { + const path = try cachePath(arena, app_dir, provider_id); + var af = try std.Io.Dir.cwd().createFileAtomic(lp.io, path, .{ .replace = true }); + defer af.deinit(lp.io); + var buf: [1024]u8 = undefined; + var w = af.file.writer(lp.io, &buf); + try std.json.Stringify.value(Cache{ .fetched_ms = auth.nowMs(), .ids = ids }, .{}, &w.interface); + try w.end(); + try af.replace(lp.io); } /// Model-id keys of `catalog[provider_id].models`, filtered to tool-call-capable