PranitInitial commit439f83c3 months agoHistory
284 lines · 9.9 KB · Zig
RawDownload
const std = @import("std");const common = @import("common.zig");const json_number = @import("../json_number.zig"); const Allocator = std.mem.Allocator; pub const Protocol = common.Protocol; pub const Limits = struct {    max_values: usize = 100,    max_total_value_bytes: usize = 64 * 1024,    max_value_bytes: usize = 4096,    max_name_bytes: usize = 256,    max_ref_bytes: usize = 64 * 1024,    max_context_arguments: usize = 128,    max_context_bytes: usize = 128 * 1024,    common: common.Limits = .{},}; pub const Error = error{    InvalidReference,    InvalidArgument,    InvalidContext,    InvalidResult,    CompletionLimitExceeded,    CompletionByteLimitExceeded,} || common.Error || Allocator.Error || std.Io.Writer.Error; pub const Reference = union(enum) {    prompt: []const u8,    resource_template: []const u8,}; pub const Argument = struct {    name: []const u8,    value: []const u8,}; pub const Result = struct {    values: []const []u8,    total: ?u64 = null,    has_more: ?bool = null,     pub fn deinit(self: *Result, alloc: Allocator) void {        for (self.values) |value| alloc.free(value);        alloc.free(self.values);        self.* = undefined;    }}; pub fn parseResult(alloc: Allocator, response: []const u8, protocol: Protocol, limits: Limits) Error!Result {    var parsed = try common.parseEnvelope(alloc, response, limits.common);    defer parsed.deinit();    const result = common.completeResult(parsed.value, protocol) catch |err| return mapCommonResultError(err);    const completion = result.object.get("completion") orelse return error.InvalidResult;    if (completion != .object) return error.InvalidResult;    const values_value = completion.object.get("values") orelse return error.InvalidResult;    if (values_value != .array or values_value.array.items.len > limits.max_values) {        return error.CompletionLimitExceeded;    }    const values = try alloc.alloc([]u8, values_value.array.items.len);    var count: usize = 0;    var total_bytes: usize = 0;    errdefer {        for (values[0..count]) |value| alloc.free(value);        alloc.free(values);    }    for (values_value.array.items, 0..) |value, index| {        if (value != .string or value.string.len > limits.max_value_bytes) {            return error.CompletionByteLimitExceeded;        }        total_bytes = std.math.add(usize, total_bytes, value.string.len) catch            return error.CompletionByteLimitExceeded;        if (total_bytes > limits.max_total_value_bytes) return error.CompletionByteLimitExceeded;        values[index] = try alloc.dupe(u8, value.string);        count += 1;    }    const total = if (completion.object.get("total")) |value|        (try nonNegativeU64(alloc, value)) orelse return error.InvalidResult    else        null;    const has_more = if (completion.object.get("hasMore")) |value| blk: {        if (value != .bool) return error.InvalidResult;        break :blk value.bool;    } else null;    return .{ .values = values, .total = total, .has_more = has_more };} pub fn buildRequest(    alloc: Allocator,    request_id: u64,    protocol: Protocol,    reference: Reference,    argument: Argument,    context_arguments: []const Argument,    limits: Limits,    write_modern_metadata: *const fn (*std.Io.Writer) anyerror!void,) ![]u8 {    try validateReference(reference, limits);    try validateArgument(argument, limits);    if (context_arguments.len > limits.max_context_arguments) return error.InvalidContext;    var context_bytes: usize = 0;    for (context_arguments, 0..) |item, index| {        try validateArgument(item, limits);        context_bytes = std.math.add(usize, context_bytes, item.name.len) catch            return error.InvalidContext;        context_bytes = std.math.add(usize, context_bytes, item.value.len) catch            return error.InvalidContext;        if (context_bytes > limits.max_context_bytes) return error.InvalidContext;        for (context_arguments[0..index]) |previous| {            if (std.mem.eql(u8, previous.name, item.name)) return error.InvalidContext;        }    }     var out: std.Io.Writer.Allocating = .init(alloc);    defer out.deinit();    try out.writer.print("{{\"jsonrpc\":\"2.0\",\"id\":{d},\"method\":\"completion/complete\",\"params\":{{", .{request_id});    if (protocol == .modern) {        try out.writer.writeAll("\"_meta\":");        try write_modern_metadata(&out.writer);        try out.writer.writeByte(',');    }    try out.writer.writeAll("\"ref\":{");    switch (reference) {        .prompt => |name| {            try out.writer.writeAll("\"type\":\"ref/prompt\",\"name\":");            try std.json.Stringify.value(name, .{}, &out.writer);        },        .resource_template => |uri| {            try out.writer.writeAll("\"type\":\"ref/resource\",\"uri\":");            try std.json.Stringify.value(uri, .{}, &out.writer);        },    }    try out.writer.writeAll("},\"argument\":{\"name\":");    try std.json.Stringify.value(argument.name, .{}, &out.writer);    try out.writer.writeAll(",\"value\":");    try std.json.Stringify.value(argument.value, .{}, &out.writer);    try out.writer.writeAll("}");    if (context_arguments.len > 0) {        try out.writer.writeAll(",\"context\":{\"arguments\":{");        for (context_arguments, 0..) |item, index| {            if (index > 0) try out.writer.writeByte(',');            try std.json.Stringify.value(item.name, .{}, &out.writer);            try out.writer.writeByte(':');            try std.json.Stringify.value(item.value, .{}, &out.writer);        }        try out.writer.writeAll("}}");    }    try out.writer.writeAll("}}");    return try out.toOwnedSlice();} fn validateReference(reference: Reference, limits: Limits) Error!void {    const value = switch (reference) {        .prompt => |name| name,        .resource_template => |uri| uri,    };    if (value.len == 0 or value.len > limits.max_ref_bytes) return error.InvalidReference;} fn validateArgument(argument: Argument, limits: Limits) Error!void {    if (argument.name.len == 0 or argument.name.len > limits.max_name_bytes or        argument.value.len > limits.max_value_bytes)    {        return error.InvalidArgument;    }} fn mapCommonResultError(err: common.Error) Error {    return switch (err) {        error.ProtocolFailure => error.ProtocolFailure,        error.OutOfMemory => error.OutOfMemory,        else => error.InvalidResult,    };} fn nonNegativeU64(alloc: Allocator, value: std.json.Value) Error!?u64 {    const parsed = json_number.fromValue(alloc, value, .{}) catch |err| switch (err) {        error.OutOfMemory => return error.OutOfMemory,        else => return null,    };    if (parsed == null) return null;    var number = parsed.?;    defer number.deinit(alloc);    const result = number.toNonNegativeUsize() orelse return null;    return std.math.cast(u64, result);} fn writeMetadata(writer: *std.Io.Writer) !void {    try writer.writeAll("{\"io.modelcontextprotocol/related-task\":{\"taskId\":\"fx-test\"}}");} test "completion result enforces count bytes total and hasMore" {    const alloc = std.testing.allocator;    var result = try parseResult(        alloc,        "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"completion\":{\"values\":[\"alpha\",\"beta\"],\"total\":3,\"hasMore\":true}}}",        .modern,        .{},    );    defer result.deinit(alloc);    try std.testing.expectEqual(@as(usize, 2), result.values.len);    try std.testing.expectEqual(@as(?u64, 3), result.total);    try std.testing.expectEqual(@as(?bool, true), result.has_more);     try std.testing.expectError(        error.CompletionLimitExceeded,        parseResult(            alloc,            "{\"jsonrpc\":\"2.0\",\"id\":2,\"result\":{\"completion\":{\"values\":[\"a\",\"b\"]}}}",            .modern,            .{ .max_values = 1 },        ),    );    try std.testing.expectError(        error.CompletionByteLimitExceeded,        parseResult(            alloc,            "{\"jsonrpc\":\"2.0\",\"id\":3,\"result\":{\"completion\":{\"values\":[\"alpha\",\"beta\"]}}}",            .modern,            .{ .max_total_value_bytes = 4 },        ),    );} test "completion requests encode prompt and resource template references" {    const alloc = std.testing.allocator;    const prompt_request = try buildRequest(        alloc,        1,        .modern,        .{ .prompt = "review" },        .{ .name = "focus", .value = "sec" },        &.{.{ .name = "language", .value = "zig" }},        .{},        writeMetadata,    );    defer alloc.free(prompt_request);    try std.testing.expect(std.mem.find(u8, prompt_request, "\"type\":\"ref/prompt\"") != null);     const resource_request = try buildRequest(        alloc,        2,        .modern,        .{ .resource_template = "db:///{table}/{id}" },        .{ .name = "table", .value = "us" },        &.{},        .{},        writeMetadata,    );    defer alloc.free(resource_request);    try std.testing.expect(std.mem.find(u8, resource_request, "\"type\":\"ref/resource\"") != null);    try std.testing.expectError(        error.InvalidContext,        buildRequest(            alloc,            3,            .modern,            .{ .prompt = "review" },            .{ .name = "focus", .value = "sec" },            &.{.{ .name = "language", .value = "zig" }},            .{ .max_context_bytes = 2 },            writeMetadata,        ),    );} fn checkCompletionAllocationFailures(alloc: Allocator) !void {    var result = try parseResult(        alloc,        "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"completion\":{\"values\":[\"alpha\",\"beta\",\"gamma\"],\"total\":3,\"hasMore\":false}}}",        .modern,        .{},    );    defer result.deinit(alloc);} test "completion result construction is allocation failure safe" {    try std.testing.checkAllAllocationFailures(        std.testing.allocator,        checkCompletionAllocationFailures,        .{},    );}