diff --git a/src/agent/Agent.zig b/src/agent/Agent.zig index 7ba1ca12e..aae856e32 100644 --- a/src/agent/Agent.zig +++ b/src/agent/Agent.zig @@ -294,12 +294,12 @@ pub fn init(allocator: std.mem.Allocator, app: *App, opts: Config.Agent) !*Agent }); return error.ConflictingFlags; } - save.validateFilename(save_path) catch { + if (save_path.len == 0) { log.fatal(.app, "invalid --save filename", .{ - .hint = "--save takes a local file name, not a path (e.g. script.js)", + .hint = "--save needs a non-empty file name", }); return error.InvalidFilename; - }; + } } if (opts.no_llm and opts.provider != null) { log.warn(.app, "ignoring --provider", .{ .reason = "--no-llm takes precedence" }); @@ -1013,7 +1013,6 @@ fn handleSave(self: *Agent, arena: std.mem.Allocator, rest: []const u8) void { const msg: []const u8 = switch (err) { error.UnterminatedQuote => "unterminated filename quote", error.EmptyFilename => "filename cannot be empty", - error.InvalidFilename => "filename must be a local file name, not a path", error.OutOfMemory => "out of memory", }; self.terminal.printError("{s}", .{msg}); diff --git a/src/agent/save.zig b/src/agent/save.zig index 48649c0f5..ab3d23822 100644 --- a/src/agent/save.zig +++ b/src/agent/save.zig @@ -52,7 +52,7 @@ pub fn parseCommand(arena: std.mem.Allocator, rest: []const u8) !Command { name = trimmed[0..tok_end]; after = trimmed[tok_end..]; } - try validateFilename(name); + if (name.len == 0) return error.EmptyFilename; if (!std.mem.endsWith(u8, name, ".js")) { name = try std.mem.concat(arena, u8, &.{ name, ".js" }); } @@ -60,14 +60,6 @@ pub fn parseCommand(arena: std.mem.Allocator, rest: []const u8) !Command { return .{ .filename = name, .prompt = if (prompt.len == 0) null else prompt }; } -pub fn validateFilename(name: []const u8) !void { - if (name.len == 0) return error.EmptyFilename; - if (std.fs.path.isAbsolute(name)) return error.InvalidFilename; - if (std.mem.indexOfScalar(u8, name, '/') != null) return error.InvalidFilename; - if (std.mem.indexOfScalar(u8, name, '\\') != null) return error.InvalidFilename; - if (std.mem.eql(u8, name, ".") or std.mem.eql(u8, name, "..")) return error.InvalidFilename; -} - pub fn randomFilename(arena: std.mem.Allocator) ![]const u8 { for (0..100) |_| { const n = std.crypto.random.int(u64); @@ -175,12 +167,13 @@ test "parseCommand: empty is all null" { try std.testing.expect(r.prompt == null); } -test "parseCommand: rejects path-like filenames" { +test "parseCommand: accepts path-like filenames" { var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); defer arena.deinit(); const aa = arena.allocator(); - try std.testing.expectError(error.InvalidFilename, parseCommand(aa, "../evil.js")); - try std.testing.expectError(error.InvalidFilename, parseCommand(aa, "/tmp/x.js")); + try std.testing.expectEqualStrings("../evil.js", (try parseCommand(aa, "../evil.js")).filename.?); + try std.testing.expectEqualStrings("/tmp/x.js", (try parseCommand(aa, "/tmp/x.js")).filename.?); + try std.testing.expectEqualStrings("out/run.js", (try parseCommand(aa, "out/run")).filename.?); try std.testing.expectError(error.UnterminatedQuote, parseCommand(aa, "\"unclosed.js")); } diff --git a/src/help.zon b/src/help.zon index 3891afaf7..e3758f395 100644 --- a/src/help.zon +++ b/src/help.zon @@ -208,8 +208,8 @@ \\--save Synthesize a replayable .js script from the --task \\ run and write it to PATH, instead of printing the \\ answer. Replay it later with `agent PATH` (no LLM - \\ calls). Overwrites PATH if it exists. PATH must be a - \\ local file name, not a path. Requires --task. + \\ calls). Overwrites PATH if it exists. Requires + \\ --task. \\ \\-a, --attach Feed a local file to the model alongside --task. \\ Repeatable, one file per flag. Text files are