PranitInitial commit439f83c3 months agoHistory
237 lines · 7.6 KB · Zig
RawDownload
const std = @import("std");const file_mutation_contract = @import("../../core/tooling/file_mutation_contract.zig");const tool_dispatch = @import("../../core/tooling/tool_dispatch.zig"); const Allocator = std.mem.Allocator;const max_content_bytes: usize = 4 * 1024 * 1024; /// Typed input for the built-in write_file tool.pub const Input = struct {    path: []u8,    content: []u8,     pub fn deinit(self: *Input, alloc: Allocator) void {        alloc.free(self.path);        alloc.free(self.content);        self.* = .{ .path = &.{}, .content = &.{} };    }}; /// Decodes write_file JSON into an owned Input released by ToolInput.deinit.pub fn decode(    ctx: tool_dispatch.DispatchContext,    args_json: []const u8,) tool_dispatch.DispatchError!tool_dispatch.DecodeResult {    var parsed = std.json.parseFromSlice(        std.json.Value,        ctx.allocator,        args_json,        .{},    ) catch {        return .{ .failure = try ctx.allocator.dupe(            u8,            "write_file arguments must be valid JSON",        ) };    };    defer parsed.deinit();     if (parsed.value != .object) {        return .{ .failure = try ctx.allocator.dupe(            u8,            "write_file arguments must be an object",        ) };    }     const path_value = parsed.value.object.get("path") orelse {        return .{ .failure = try ctx.allocator.dupe(            u8,            "write_file requires string field \"path\"",        ) };    };    if (path_value != .string) {        return .{ .failure = try ctx.allocator.dupe(            u8,            "write_file field \"path\" must be a string",        ) };    }     const content_value = parsed.value.object.get("content") orelse {        return .{ .failure = try ctx.allocator.dupe(            u8,            "write_file requires string field \"content\"",        ) };    };    if (content_value != .string) {        return .{ .failure = try ctx.allocator.dupe(            u8,            "write_file field \"content\" must be a string",        ) };    }     const input = try ctx.allocator.create(Input);    errdefer ctx.allocator.destroy(input);    const owned_path = try ctx.allocator.dupe(u8, path_value.string);    errdefer ctx.allocator.free(owned_path);    const owned_content = try ctx.allocator.dupe(u8, content_value.string);    errdefer ctx.allocator.free(owned_content);    input.* = .{        .path = owned_path,        .content = owned_content,    };    return .{ .input = .{ .ptr = input, .deinit_fn = inputDeinit } };} fn inputDeinit(ptr: *anyopaque, alloc: Allocator) void {    const input: *Input = @ptrCast(@alignCast(ptr));    input.deinit(alloc);    alloc.destroy(input);} pub fn takeFileMutationInput(    tool_input: tool_dispatch.ToolInput,    alloc: Allocator,) file_mutation_contract.FileMutationInput {    const input = tool_input.as(Input);    const moved = file_mutation_contract.WriteInput{        .path = input.path,        .content = input.content,    };    alloc.destroy(input);    return .{ .write = moved };} pub fn validate(    ctx: tool_dispatch.DispatchContext,    tool_input: tool_dispatch.ToolInput,) tool_dispatch.DispatchError!?[]u8 {    const input = tool_input.as(Input);    if (input.path.len > std.Io.Dir.max_path_bytes) {        return try ctx.allocator.dupe(            u8,            "file mutation preparation failed: path exceeds the preparation limit",        );    }    if (input.content.len > max_content_bytes) {        return try ctx.allocator.dupe(            u8,            "write_file failed: content exceeds the 4 MiB preparation limit",        );    }    return null;} pub fn call(    ctx: tool_dispatch.DispatchContext,    _: tool_dispatch.ToolInput,) tool_dispatch.DispatchError!tool_dispatch.ToolResult {    return .{ .failure = try ctx.allocator.dupe(        u8,        "write_file execution requires canonical tool runtime authorization",    ) };} /// Reports that write_file mutates filesystem state.pub fn readsOnly(_: tool_dispatch.ToolInput) bool {    return false;} /// Reports that write_file can destroy prior file content.pub fn isIrreversible(_: tool_dispatch.ToolInput) bool {    return true;} fn noopInputDeinit(_: *anyopaque, _: Allocator) void {} test "write_file decodes invalid argument shapes as failures" {    const cases = [_]struct {        json: []const u8,        reason: []const u8,    }{        .{ .json = "{", .reason = "write_file arguments must be valid JSON" },        .{ .json = "[]", .reason = "write_file arguments must be an object" },        .{ .json = "{\"content\":\"x\"}", .reason = "write_file requires string field \"path\"" },        .{ .json = "{\"path\":1,\"content\":\"x\"}", .reason = "write_file field \"path\" must be a string" },        .{ .json = "{\"path\":\"/tmp/x\"}", .reason = "write_file requires string field \"content\"" },        .{ .json = "{\"path\":\"/tmp/x\",\"content\":1}", .reason = "write_file field \"content\" must be a string" },    };     for (cases) |case| {        const decoded = try decode(            .{ .allocator = std.testing.allocator },            case.json,        );        switch (decoded) {            .failure => |reason| {                defer std.testing.allocator.free(reason);                try std.testing.expectEqualStrings(case.reason, reason);            },            .input => |input| {                input.deinit(std.testing.allocator);                return error.TestExpectedDecodeFailure;            },        }    }} test "write_file decodes owned typed input" {    const decoded = try decode(        .{ .allocator = std.testing.allocator },        "{\"path\":\"file.txt\",\"content\":\"hello\\n\"}",    );    const erased = switch (decoded) {        .input => |input| input,        .failure => |reason| {            defer std.testing.allocator.free(reason);            return error.TestExpectedDecodedInput;        },    };    defer erased.deinit(std.testing.allocator);    const input = erased.as(Input);    try std.testing.expectEqualStrings("file.txt", input.path);    try std.testing.expectEqualStrings("hello\n", input.content);    try std.testing.expect(        try validate(.{ .allocator = std.testing.allocator }, erased) == null,    );} test "write_file validation enforces path and content preparation limits" {    const alloc = std.testing.allocator;    const oversized_path = try alloc.alloc(u8, std.Io.Dir.max_path_bytes + 1);    defer alloc.free(oversized_path);    @memset(oversized_path, 'p');    const oversized_content = try alloc.alloc(u8, 4 * 1024 * 1024 + 1);    defer alloc.free(oversized_content);    @memset(oversized_content, 'x');     const cases = [_]struct {        input: Input,        reason: []const u8,    }{        .{            .input = .{                .path = oversized_path,                .content = @constCast("content"),            },            .reason = "file mutation preparation failed: path exceeds the preparation limit",        },        .{            .input = .{                .path = @constCast("file.txt"),                .content = oversized_content,            },            .reason = "write_file failed: content exceeds the 4 MiB preparation limit",        },    };     for (cases) |case| {        var input = case.input;        const erased = tool_dispatch.ToolInput{            .ptr = &input,            .deinit_fn = noopInputDeinit,        };        const reason = try validate(.{ .allocator = alloc }, erased) orelse            return error.TestExpectedValidationFailure;        defer alloc.free(reason);        try std.testing.expectEqualStrings(case.reason, reason);    }}