const std = @import("std"); // Foreground Pi/local assistant bridge over compact context. // req: session/002, session/004, governance/002, governance/003, testing/001, testing/002 test { _ = rowsAlloc; } pub const Error = error{ InvalidCommand, InvalidContext, }; pub fn rowsAlloc(allocator: std.mem.Allocator, io: std.Io, argv: []const []const u8, context: []const u8) ![][]const u8 { try validateArgv(argv); try validateContext(context); const full_argv = try allocator.alloc([]const u8, argv.len + 1); defer allocator.free(full_argv); @memcpy(full_argv[0..argv.len], argv); full_argv[argv.len] = context; var rows = std.ArrayList([]const u8).empty; errdefer { for (rows.items) |row| allocator.free(row); rows.deinit(allocator); } const preview = try commandPreviewAlloc(allocator, argv); defer allocator.free(preview); try rows.append(allocator, try std.fmt.allocPrint(allocator, "pi:spawned:{s}", .{preview})); try rows.append(allocator, try std.fmt.allocPrint(allocator, "pi:context:bytes_{d}", .{context.len})); const result = std.process.run(allocator, io, .{ .argv = full_argv, .stdout_limit = .limited(128 * 1024), .stderr_limit = .limited(64 * 1024), }) catch |err| { try rows.append(allocator, try std.fmt.allocPrint(allocator, "pi:status:spawn_error_{s}", .{@errorName(err)})); return rows.toOwnedSlice(allocator); }; defer allocator.free(result.stdout); defer allocator.free(result.stderr); try appendTermRow(allocator, &rows, result.term); try appendOutputRows(allocator, &rows, "stdout", result.stdout); try appendOutputRows(allocator, &rows, "stderr", result.stderr); if (rows.items.len == 3) try rows.append(allocator, try allocator.dupe(u8, "pi:output_empty")); return rows.toOwnedSlice(allocator); } fn validateArgv(argv: []const []const u8) !void { if (argv.len == 0) return Error.InvalidCommand; for (argv) |arg| { if (arg.len == 0 or !std.unicode.utf8ValidateSlice(arg)) return Error.InvalidCommand; for (arg) |byte| if (byte == 0 or byte == '\n' or byte == '\r') return Error.InvalidCommand; } } fn validateContext(context: []const u8) !void { if (context.len == 0 or !std.unicode.utf8ValidateSlice(context)) return Error.InvalidContext; for (context) |byte| if (byte == 0) return Error.InvalidContext; } fn appendTermRow(allocator: std.mem.Allocator, rows: *std.ArrayList([]const u8), term: std.process.Child.Term) !void { const row = switch (term) { .exited => |code| try std.fmt.allocPrint(allocator, "pi:status:exit_{d}", .{code}), .signal => |sig| try std.fmt.allocPrint(allocator, "pi:status:signal_{d}", .{@intFromEnum(sig)}), .stopped => |sig| try std.fmt.allocPrint(allocator, "pi:status:stopped_{d}", .{@intFromEnum(sig)}), .unknown => |code| try std.fmt.allocPrint(allocator, "pi:status:unknown_{d}", .{code}), }; try rows.append(allocator, row); } fn appendOutputRows(allocator: std.mem.Allocator, rows: *std.ArrayList([]const u8), prefix: []const u8, output: []const u8) !void { var lines = std.mem.splitScalar(u8, output, '\n'); while (lines.next()) |raw_line| { const trimmed = std.mem.trim(u8, raw_line, "\r"); if (trimmed.len == 0) continue; const sanitized = try sanitizeAlloc(allocator, trimmed, 120); defer allocator.free(sanitized); try rows.append(allocator, try std.fmt.allocPrint(allocator, "{s}:{s}", .{ prefix, sanitized })); } } fn commandPreviewAlloc(allocator: std.mem.Allocator, argv: []const []const u8) ![]u8 { var out = std.ArrayList(u8).empty; errdefer out.deinit(allocator); for (argv, 0..) |arg, index| { if (index != 0) try out.append(allocator, '_'); const clean = try sanitizeAlloc(allocator, arg, 40); defer allocator.free(clean); try out.appendSlice(allocator, clean); if (out.items.len >= 120) break; } return out.toOwnedSlice(allocator); } fn sanitizeAlloc(allocator: std.mem.Allocator, value: []const u8, max_len: usize) ![]u8 { var out = std.ArrayList(u8).empty; errdefer out.deinit(allocator); var i: usize = 0; while (i < value.len and out.items.len < max_len) { const len = std.unicode.utf8ByteSequenceLength(value[i]) catch 1; const end = @min(value.len, i + len); const slice = value[i..end]; if (slice.len == 1 and (slice[0] <= 0x20 or slice[0] == '|')) { try out.append(allocator, '_'); } else { try out.appendSlice(allocator, slice); } i = end; } if (i < value.len) try out.appendSlice(allocator, "..."); if (out.items.len == 0) try out.append(allocator, '_'); return out.toOwnedSlice(allocator); } fn freeRows(allocator: std.mem.Allocator, rows: []const []const u8) void { for (rows) |row| allocator.free(row); allocator.free(rows); } test "regular: fake pi command receives context and emits output rows" { const context = "context:v1\ntask=fix_bug\n"; const rows = try rowsAlloc(std.testing.allocator, std.testing.io, &.{ "sh", "-c", "case \"$1\" in *context:v1*) echo pi_ok;; *) echo missing; exit 2;; esac", "fake-pi" }, context); defer freeRows(std.testing.allocator, rows); try std.testing.expectEqualStrings("pi:status:exit_0", rows[2]); try std.testing.expectEqualStrings("stdout:pi_ok", rows[3]); } test "adversarial: missing pi command and invalid input are explicit" { try std.testing.expectError(Error.InvalidCommand, rowsAlloc(std.testing.allocator, std.testing.io, &.{}, "context:v1\n")); try std.testing.expectError(Error.InvalidContext, rowsAlloc(std.testing.allocator, std.testing.io, &.{"true"}, "")); const rows = try rowsAlloc(std.testing.allocator, std.testing.io, &.{"definitely-not-a-mim-pi"}, "context:v1\n"); defer freeRows(std.testing.allocator, rows); try std.testing.expect(std.mem.startsWith(u8, rows[2], "pi:status:spawn_error_")); }