Add replayable tool protocol logs
This commit is contained in:
@@ -11,6 +11,12 @@ test {
|
||||
|
||||
pub const max_request_bytes = 4096;
|
||||
|
||||
pub fn replayToolLogLine(allocator: std.mem.Allocator, session: *session_mod.Session, log_line: []const u8) ![]u8 {
|
||||
const parsed = parseToolLog(log_line) orelse return allocator.dupe(u8, "err invalid tool log\n");
|
||||
if (!std.mem.eql(u8, parsed.status, "ok")) return allocator.dupe(u8, "err logged command failed\n");
|
||||
return commandResponse(allocator, session, parsed.command);
|
||||
}
|
||||
|
||||
pub fn handleLine(allocator: std.mem.Allocator, session: *session_mod.Session, line: []const u8) ![]u8 {
|
||||
const trimmed = std.mem.trimEnd(u8, line, "\r\n");
|
||||
if (std.mem.eql(u8, trimmed, "state")) return stateResponse(allocator, session);
|
||||
@@ -25,6 +31,9 @@ pub fn handleLine(allocator: std.mem.Allocator, session: *session_mod.Session, l
|
||||
if (std.mem.startsWith(u8, trimmed, "command ")) {
|
||||
return commandResponse(allocator, session, trimmed[8..]);
|
||||
}
|
||||
if (std.mem.startsWith(u8, trimmed, "tool ")) {
|
||||
return toolResponse(allocator, session, trimmed[5..]);
|
||||
}
|
||||
return allocator.dupe(u8, "err unknown request\n");
|
||||
}
|
||||
|
||||
@@ -34,6 +43,51 @@ fn contextResponse(allocator: std.mem.Allocator, session: *session_mod.Session,
|
||||
return context_mod.sessionContextAlloc(allocator, session, payload, null, .{});
|
||||
}
|
||||
|
||||
fn toolResponse(allocator: std.mem.Allocator, session: *session_mod.Session, payload: []const u8) ![]u8 {
|
||||
const split = std.mem.indexOfScalar(u8, payload, ' ') orelse return allocator.dupe(u8, "err invalid tool request\n");
|
||||
const name = payload[0..split];
|
||||
const command = payload[split + 1 ..];
|
||||
if (!validToolName(name) or command.len == 0 or std.mem.startsWith(u8, command, "tool ")) return allocator.dupe(u8, "err invalid tool request\n");
|
||||
if (!validToolPayload(command)) return allocator.dupe(u8, "err invalid tool request\n");
|
||||
|
||||
const command_response = try commandResponse(allocator, session, command);
|
||||
defer allocator.free(command_response);
|
||||
const status: []const u8 = if (std.mem.startsWith(u8, command_response, "ok ")) "ok" else "err";
|
||||
return std.fmt.allocPrint(
|
||||
allocator,
|
||||
"tool-log\t{s}\t{s}\t{s}\n{s}",
|
||||
.{ name, command, status, command_response },
|
||||
);
|
||||
}
|
||||
|
||||
fn validToolName(name: []const u8) bool {
|
||||
if (name.len == 0 or name.len > 32) return false;
|
||||
for (name) |byte| {
|
||||
if (!((byte >= 'a' and byte <= 'z') or (byte >= 'A' and byte <= 'Z') or (byte >= '0' and byte <= '9') or byte == '_' or byte == '-')) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
fn validToolPayload(command: []const u8) bool {
|
||||
for (command) |byte| {
|
||||
if (byte == 0 or byte == '\t' or byte == '\n' or byte == '\r') return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
fn parseToolLog(log_line: []const u8) ?struct { name: []const u8, command: []const u8, status: []const u8 } {
|
||||
const trimmed = std.mem.trimEnd(u8, log_line, "\r\n");
|
||||
if (!std.mem.startsWith(u8, trimmed, "tool-log\t")) return null;
|
||||
var fields = std.mem.splitScalar(u8, trimmed, '\t');
|
||||
_ = fields.next() orelse return null;
|
||||
const name = fields.next() orelse return null;
|
||||
const command = fields.next() orelse return null;
|
||||
const status = fields.next() orelse return null;
|
||||
if (fields.next() != null) return null;
|
||||
if (!validToolName(name) or !validToolPayload(command)) return null;
|
||||
return .{ .name = name, .command = command, .status = status };
|
||||
}
|
||||
|
||||
fn commandResponse(allocator: std.mem.Allocator, session: *session_mod.Session, command: []const u8) ![]u8 {
|
||||
if (std.mem.eql(u8, command, "move_left")) return dispatchAndRespond(allocator, session, .move_left);
|
||||
if (std.mem.eql(u8, command, "move_right")) return dispatchAndRespond(allocator, session, .move_right);
|
||||
@@ -171,6 +225,49 @@ fn stateResponse(allocator: std.mem.Allocator, session: *session_mod.Session) ![
|
||||
);
|
||||
}
|
||||
|
||||
test "regular: trusted tool command logs and replays deterministic state" {
|
||||
var session = session_mod.Session.init(std.testing.allocator);
|
||||
defer session.deinit();
|
||||
try session.openFixture("ab");
|
||||
|
||||
const response = try handleLine(std.testing.allocator, &session, "tool pi insert c\n");
|
||||
defer std.testing.allocator.free(response);
|
||||
try std.testing.expect(std.mem.startsWith(u8, response, "tool-log\tpi\tinsert c\tok\n"));
|
||||
try std.testing.expect(std.mem.indexOf(u8, response, "bytes_len=3") != null);
|
||||
|
||||
const log_end = std.mem.indexOfScalar(u8, response, '\n').?;
|
||||
var replayed = session_mod.Session.init(std.testing.allocator);
|
||||
defer replayed.deinit();
|
||||
try replayed.openFixture("ab");
|
||||
const replay_response = try replayToolLogLine(std.testing.allocator, &replayed, response[0..log_end]);
|
||||
defer std.testing.allocator.free(replay_response);
|
||||
try std.testing.expect(std.mem.indexOf(u8, replay_response, "bytes_len=3") != null);
|
||||
}
|
||||
|
||||
test "adversarial: tool path rejects hidden or malformed mutations" {
|
||||
var session = session_mod.Session.init(std.testing.allocator);
|
||||
defer session.deinit();
|
||||
try session.openFixture("ab");
|
||||
|
||||
{
|
||||
const response = try handleLine(std.testing.allocator, &session, "tool bad* insert c\n");
|
||||
defer std.testing.allocator.free(response);
|
||||
try std.testing.expectEqualStrings("err invalid tool request\n", response);
|
||||
}
|
||||
|
||||
{
|
||||
const response = try handleLine(std.testing.allocator, &session, "tool pi tool nested\n");
|
||||
defer std.testing.allocator.free(response);
|
||||
try std.testing.expectEqualStrings("err invalid tool request\n", response);
|
||||
}
|
||||
|
||||
{
|
||||
const response = try replayToolLogLine(std.testing.allocator, &session, "tool-log\tpi\tinsert c\terr");
|
||||
defer std.testing.allocator.free(response);
|
||||
try std.testing.expectEqualStrings("err logged command failed\n", response);
|
||||
}
|
||||
}
|
||||
|
||||
test "regular: protocol context request returns compact agent context" {
|
||||
var session = session_mod.Session.init(std.testing.allocator);
|
||||
defer session.deinit();
|
||||
|
||||
Reference in New Issue
Block a user