const std = @import("std"); const context_mod = @import("context.zig"); const session_mod = @import("session.zig"); // Tiny local session protocol for trusted same-user tools. // req: session/002, session/003, governance/003 test { _ = handleLine; } 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); if (std.mem.eql(u8, trimmed, "context")) return context_mod.sessionContextAlloc(allocator, session, null, null, .{}); if (std.mem.startsWith(u8, trimmed, "context ")) return contextResponse(allocator, session, trimmed[8..]); if (std.mem.startsWith(u8, trimmed, "open ")) { const bytes = trimmed[5..]; if (!std.unicode.utf8ValidateSlice(bytes)) return allocator.dupe(u8, "err invalid utf8\n"); try session.openFixture(bytes); return stateResponse(allocator, session); } 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"); } fn contextResponse(allocator: std.mem.Allocator, session: *session_mod.Session, payload: []const u8) ![]u8 { const split = std.mem.indexOfScalar(u8, payload, ' '); if (split) |index| return context_mod.sessionContextAlloc(allocator, session, payload[0..index], payload[index + 1 ..], .{}); 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); if (std.mem.eql(u8, command, "move_up")) return dispatchAndRespond(allocator, session, .move_up); if (std.mem.eql(u8, command, "move_down")) return dispatchAndRespond(allocator, session, .move_down); if (std.mem.eql(u8, command, "move_word_forward")) return dispatchAndRespond(allocator, session, .move_word_forward); if (std.mem.eql(u8, command, "move_word_back")) return dispatchAndRespond(allocator, session, .move_word_back); if (std.mem.eql(u8, command, "move_word_end")) return dispatchAndRespond(allocator, session, .move_word_end); if (std.mem.eql(u8, command, "move_line_start")) return dispatchAndRespond(allocator, session, .move_line_start); if (std.mem.eql(u8, command, "move_line_end")) return dispatchAndRespond(allocator, session, .move_line_end); if (std.mem.eql(u8, command, "delete_backward")) return dispatchAndRespond(allocator, session, .delete_backward); if (std.mem.eql(u8, command, "delete_forward")) return dispatchAndRespond(allocator, session, .delete_forward); if (std.mem.eql(u8, command, "delete_line")) return dispatchAndRespond(allocator, session, .delete_line); if (std.mem.eql(u8, command, "change_line")) return dispatchAndRespond(allocator, session, .change_line); if (std.mem.eql(u8, command, "open_line_below")) return dispatchAndRespond(allocator, session, .open_line_below); if (std.mem.eql(u8, command, "open_line_above")) return dispatchAndRespond(allocator, session, .open_line_above); if (std.mem.startsWith(u8, command, "replace_char ")) return dispatchAndRespond(allocator, session, .{ .replace_char = command[13..] }); if (std.mem.startsWith(u8, command, "insert ")) return dispatchAndRespond(allocator, session, .{ .insert = command[7..] }); if (std.mem.startsWith(u8, command, "panel_open ")) return panelRespond(allocator, session, .{ .open = command[11..] }); if (std.mem.startsWith(u8, command, "list_open ")) return listOpenRespond(allocator, session, command[10..]); if (std.mem.startsWith(u8, command, "list_filter ")) return listRespond(allocator, session, .{ .filter = command[12..] }); if (std.mem.eql(u8, command, "list_down")) return listRespond(allocator, session, .down); if (std.mem.eql(u8, command, "list_up")) return listRespond(allocator, session, .up); if (std.mem.eql(u8, command, "list_select")) return listRespond(allocator, session, .select); if (std.mem.eql(u8, command, "list_cancel")) return listRespond(allocator, session, .cancel); if (std.mem.eql(u8, command, "panel_close")) return panelRespond(allocator, session, .close); if (std.mem.eql(u8, command, "panel_next")) return panelRespond(allocator, session, .next); if (std.mem.eql(u8, command, "panel_prev")) return panelRespond(allocator, session, .previous); if (std.mem.startsWith(u8, command, "pair ")) { const pair = pairByName(command[5..]) orelse return allocator.dupe(u8, "err unknown pair\n"); return dispatchAndRespond(allocator, session, .{ .insert_pair = pair }); } return allocator.dupe(u8, "err unknown command\n"); } fn pairByName(name: []const u8) ?session_mod.Pair { if (std.mem.eql(u8, name, "parens")) return .{ .open = "(", .close = ")" }; if (std.mem.eql(u8, name, "brackets")) return .{ .open = "[", .close = "]" }; if (std.mem.eql(u8, name, "braces")) return .{ .open = "{", .close = "}" }; if (std.mem.eql(u8, name, "double_quote")) return .{ .open = "\"", .close = "\"" }; if (std.mem.eql(u8, name, "single_quote")) return .{ .open = "'", .close = "'" }; if (std.mem.eql(u8, name, "backtick")) return .{ .open = "`", .close = "`" }; return null; } fn dispatchAndRespond(allocator: std.mem.Allocator, session: *session_mod.Session, command: session_mod.Command) ![]u8 { session.dispatch(command) catch |err| switch (err) { error.InvalidUtf8Insertion => return allocator.dupe(u8, "err invalid utf8\n"), error.NoBufferOpen => return allocator.dupe(u8, "err no buffer open\n"), else => return err, }; return stateResponse(allocator, session); } const PanelCommand = union(enum) { open: []const u8, close, next, previous, }; fn panelRespond(allocator: std.mem.Allocator, session: *session_mod.Session, command: PanelCommand) ![]u8 { switch (command) { .open => |title| session.openPanel(title) catch |err| switch (err) { error.InvalidPanelTitle => return allocator.dupe(u8, "err invalid panel title\n"), else => return err, }, .close => session.closePanel() catch |err| switch (err) { error.NoPanelOpen => return allocator.dupe(u8, "err no panel open\n"), else => return err, }, .next => session.nextPanel() catch |err| switch (err) { error.NoPanelOpen => return allocator.dupe(u8, "err no panel open\n"), else => return err, }, .previous => session.previousPanel() catch |err| switch (err) { error.NoPanelOpen => return allocator.dupe(u8, "err no panel open\n"), else => return err, }, } return stateResponse(allocator, session); } const ListCommand = union(enum) { filter: []const u8, down, up, select, cancel, }; fn listOpenRespond(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 list open\n"); const title = payload[0..split]; const items_payload = payload[split + 1 ..]; var items = std.ArrayList([]const u8).empty; defer items.deinit(allocator); var iter = std.mem.splitScalar(u8, items_payload, '|'); while (iter.next()) |item| try items.append(allocator, item); session.openListPanel(title, items.items) catch |err| switch (err) { error.InvalidPanelTitle => return allocator.dupe(u8, "err invalid panel title\n"), error.InvalidListItem => return allocator.dupe(u8, "err invalid list item\n"), error.EmptyList => return allocator.dupe(u8, "err empty list\n"), else => return err, }; return stateResponse(allocator, session); } fn listRespond(allocator: std.mem.Allocator, session: *session_mod.Session, command: ListCommand) ![]u8 { switch (command) { .filter => |filter| session.filterListPanel(filter) catch |err| return listError(allocator, err), .down => session.listPanelDown() catch |err| return listError(allocator, err), .up => session.listPanelUp() catch |err| return listError(allocator, err), .select => _ = session.selectListPanel() catch |err| return listError(allocator, err), .cancel => session.cancelListPanel() catch |err| return listError(allocator, err), } return stateResponse(allocator, session); } fn listError(allocator: std.mem.Allocator, err: anyerror) ![]u8 { return switch (err) { error.NoPanelOpen => allocator.dupe(u8, "err no panel open\n"), error.ActivePanelIsNotList => allocator.dupe(u8, "err active panel is not list\n"), error.InvalidListFilter => allocator.dupe(u8, "err invalid list filter\n"), error.EmptyList => allocator.dupe(u8, "err empty list\n"), else => err, }; } fn stateResponse(allocator: std.mem.Allocator, session: *session_mod.Session) ![]u8 { const snap = try session.snapshot(); const panel_path = try session.panelPathAlloc(allocator); defer allocator.free(panel_path); const panel_summary = try session.panelSummaryAlloc(allocator); defer allocator.free(panel_summary); return std.fmt.allocPrint( allocator, "ok state cursor_byte={d} cursor_cell={d} bytes_len={d} panel_depth={d} active_panel={s} panel_path={s} panel_summary={s}\n", .{ snap.cursor_byte, snap.cursor_cell, snap.bytes.len, snap.panel_depth, if (snap.active_panel_title) |title| title else "-", panel_path, panel_summary, }, ); } 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(); try session.openFixture("abc"); try session.openListPanel("diag", &.{ "one", "two" }); const response = try handleLine(std.testing.allocator, &session, "context fix_bug inspect\n"); defer std.testing.allocator.free(response); try std.testing.expect(std.mem.indexOf(u8, response, "context:v1\n") != null); try std.testing.expect(std.mem.indexOf(u8, response, "active_panel=diag") != null); try std.testing.expect(std.mem.indexOf(u8, response, "panel_summary=") != null); try std.testing.expect(std.mem.indexOf(u8, response, "task=fix_bug") != null); try std.testing.expect(std.mem.indexOf(u8, response, "intent=inspect") != null); } test "regular: protocol opens bytes, reports state, and dispatches commands" { var session = session_mod.Session.init(std.testing.allocator); defer session.deinit(); const open = try handleLine(std.testing.allocator, &session, "open café\n"); defer std.testing.allocator.free(open); try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=5 panel_depth=0 active_panel=- panel_path=- panel_summary=-\n", open); const moved = try handleLine(std.testing.allocator, &session, "command move_right\n"); defer std.testing.allocator.free(moved); try std.testing.expectEqualStrings("ok state cursor_byte=1 cursor_cell=1 bytes_len=5 panel_depth=0 active_panel=- panel_path=- panel_summary=-\n", moved); const inserted = try handleLine(std.testing.allocator, &session, "command insert 🔥\n"); defer std.testing.allocator.free(inserted); try std.testing.expectEqualStrings("ok state cursor_byte=5 cursor_cell=3 bytes_len=9 panel_depth=0 active_panel=- panel_path=- panel_summary=-\n", inserted); } test "regular: protocol delete command removes a whole UTF-8 codepoint" { var session = session_mod.Session.init(std.testing.allocator); defer session.deinit(); var response = try handleLine(std.testing.allocator, &session, "open aé\n"); std.testing.allocator.free(response); response = try handleLine(std.testing.allocator, &session, "command move_right\n"); std.testing.allocator.free(response); response = try handleLine(std.testing.allocator, &session, "command move_right\n"); std.testing.allocator.free(response); response = try handleLine(std.testing.allocator, &session, "command delete_backward\n"); defer std.testing.allocator.free(response); try std.testing.expectEqualStrings("ok state cursor_byte=1 cursor_cell=1 bytes_len=1 panel_depth=0 active_panel=- panel_path=- panel_summary=-\n", response); } test "adversarial: protocol rejects unknown requests and commands" { var session = session_mod.Session.init(std.testing.allocator); defer session.deinit(); const request = try handleLine(std.testing.allocator, &session, "subscribe everything\n"); defer std.testing.allocator.free(request); try std.testing.expectEqualStrings("err unknown request\n", request); try session.openFixture("abc"); const command = try handleLine(std.testing.allocator, &session, "command plugin.load\n"); defer std.testing.allocator.free(command); try std.testing.expectEqualStrings("err unknown command\n", command); } test "adversarial: protocol reports no buffer and invalid utf8 without corrupting state" { var session = session_mod.Session.init(std.testing.allocator); defer session.deinit(); const no_buffer = try handleLine(std.testing.allocator, &session, "state\n"); defer std.testing.allocator.free(no_buffer); try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=0 active_panel=- panel_path=- panel_summary=-\n", no_buffer); try session.openFixture("safe"); const before = try session.snapshot(); const bad = [_]u8{ 'c', 'o', 'm', 'm', 'a', 'n', 'd', ' ', 'i', 'n', 's', 'e', 'r', 't', ' ', 0xc3, 0x28 }; const invalid = try handleLine(std.testing.allocator, &session, &bad); defer std.testing.allocator.free(invalid); try std.testing.expectEqualStrings("err invalid utf8\n", invalid); const after = try session.snapshot(); try std.testing.expectEqualStrings(before.bytes, after.bytes); } test "regular: protocol pair commands insert delimiters with cursor between them" { var session = session_mod.Session.init(std.testing.allocator); defer session.deinit(); var response = try handleLine(std.testing.allocator, &session, "open call\n"); std.testing.allocator.free(response); response = try handleLine(std.testing.allocator, &session, "command move_right\n"); std.testing.allocator.free(response); response = try handleLine(std.testing.allocator, &session, "command move_right\n"); std.testing.allocator.free(response); response = try handleLine(std.testing.allocator, &session, "command move_right\n"); std.testing.allocator.free(response); response = try handleLine(std.testing.allocator, &session, "command move_right\n"); std.testing.allocator.free(response); response = try handleLine(std.testing.allocator, &session, "command pair parens\n"); defer std.testing.allocator.free(response); try std.testing.expectEqualStrings("ok state cursor_byte=5 cursor_cell=5 bytes_len=6 panel_depth=0 active_panel=- panel_path=- panel_summary=-\n", response); try std.testing.expectEqualStrings("call()", (try session.snapshot()).bytes); } test "adversarial: protocol rejects unknown pair names" { var session = session_mod.Session.init(std.testing.allocator); defer session.deinit(); try session.openFixture("abc"); const response = try handleLine(std.testing.allocator, &session, "command pair snippet_everything\n"); defer std.testing.allocator.free(response); try std.testing.expectEqualStrings("err unknown pair\n", response); try std.testing.expectEqualStrings("abc", (try session.snapshot()).bytes); } test "regular: protocol opens switches and closes generic panels" { var session = session_mod.Session.init(std.testing.allocator); defer session.deinit(); { const response = try handleLine(std.testing.allocator, &session, "command panel_open files\n"); defer std.testing.allocator.free(response); try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=1 active_panel=files panel_path=[files] panel_summary=empty\n", response); } { const response = try handleLine(std.testing.allocator, &session, "command panel_open diagnostics\n"); defer std.testing.allocator.free(response); try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=2 active_panel=diagnostics panel_path=files>[diagnostics] panel_summary=empty\n", response); } { const response = try handleLine(std.testing.allocator, &session, "command panel_prev\n"); defer std.testing.allocator.free(response); try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=2 active_panel=files panel_path=[files]>diagnostics panel_summary=empty\n", response); } { const response = try handleLine(std.testing.allocator, &session, "command panel_close\n"); defer std.testing.allocator.free(response); try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=1 active_panel=diagnostics panel_path=[diagnostics] panel_summary=empty\n", response); } } test "adversarial: protocol rejects invalid panel titles and empty panel actions" { var session = session_mod.Session.init(std.testing.allocator); defer session.deinit(); { const response = try handleLine(std.testing.allocator, &session, "command panel_open bad title\n"); defer std.testing.allocator.free(response); try std.testing.expectEqualStrings("err invalid panel title\n", response); } { const response = try handleLine(std.testing.allocator, &session, "command panel_close\n"); defer std.testing.allocator.free(response); try std.testing.expectEqualStrings("err no panel open\n", response); } } test "regular: protocol list panel filters moves selects and remains inspectable" { var session = session_mod.Session.init(std.testing.allocator); defer session.deinit(); { const response = try handleLine(std.testing.allocator, &session, "command list_open files src/main.zig|src/panel.zig|README.md\n"); defer std.testing.allocator.free(response); try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=1 active_panel=files panel_path=[files] panel_summary=list:filter=-,cursor=0,visible=3,selected=-\n", response); } { const response = try handleLine(std.testing.allocator, &session, "command list_filter src\n"); defer std.testing.allocator.free(response); try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=1 active_panel=files panel_path=[files] panel_summary=list:filter=src,cursor=0,visible=2,selected=-\n", response); } { const response = try handleLine(std.testing.allocator, &session, "command list_down\n"); defer std.testing.allocator.free(response); try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=1 active_panel=files panel_path=[files] panel_summary=list:filter=src,cursor=1,visible=2,selected=-\n", response); } { const response = try handleLine(std.testing.allocator, &session, "command list_select\n"); defer std.testing.allocator.free(response); try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=1 active_panel=files panel_path=[files] panel_summary=list:filter=src,cursor=1,visible=2,selected=src/panel.zig\n", response); } } test "regular: protocol list cancel closes list panel" { var session = session_mod.Session.init(std.testing.allocator); defer session.deinit(); var response = try handleLine(std.testing.allocator, &session, "command list_open files a.zig|b.zig\n"); std.testing.allocator.free(response); response = try handleLine(std.testing.allocator, &session, "command list_cancel\n"); defer std.testing.allocator.free(response); try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=0 active_panel=- panel_path=- panel_summary=-\n", response); } test "adversarial: protocol list errors are explicit" { var session = session_mod.Session.init(std.testing.allocator); defer session.deinit(); { const response = try handleLine(std.testing.allocator, &session, "command list_open files bad item\n"); defer std.testing.allocator.free(response); try std.testing.expectEqualStrings("err invalid list item\n", response); } { const response = try handleLine(std.testing.allocator, &session, "command list_filter src\n"); defer std.testing.allocator.free(response); try std.testing.expectEqualStrings("err no panel open\n", response); } }