Add replayable tool protocol logs

This commit is contained in:
slhx agent
2026-06-21 05:03:38 +02:00
parent ffbff6e4b8
commit 7170b7d14c
+97
View File
@@ -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();