summaryrefslogtreecommitdiff
path: root/9harness/src/main.zig
diff options
context:
space:
mode:
Diffstat (limited to '9harness/src/main.zig')
-rw-r--r--9harness/src/main.zig379
1 files changed, 0 insertions, 379 deletions
diff --git a/9harness/src/main.zig b/9harness/src/main.zig
deleted file mode 100644
index fe0a07b..0000000
--- a/9harness/src/main.zig
+++ /dev/null
@@ -1,379 +0,0 @@
-//! 9harness: the harness fs daemon — a long-running 9P2000 server that
-//! mirrors every AI-agent harness's state (Claude Code, Codex, omp,
-//! hermes, dsh + their skills) as one read-only, fresh-from-disk tree.
-//! See docs/DESIGN.md for the tree contract and src/tree.zig for the tree.
-//!
-//! By default it posts itself under the name `harness` with
-//! `serve.Runner.listenPosted`, so it appears at $XDG_RUNTIME_DIR/9p/harness
-//! and every interactive fish (self-wrapped in `9ns --mntgen`) sees it at
-//! /mnt/9p/harness with zero configuration. `--unix`, `--tcp` and `--fd`
-//! are the other listen forms; `--no-post` serves without posting; the
-//! five harness roots default to $HOME/.<name> and `--root NAME=PATH`
-//! pins any of them elsewhere (the tests use fake homes).
-//!
-//! v1 has no daemonization: run it in a zmx session, the zmx way:
-//!
-//! zmx run harness -d 9harness
-//!
-//! SIGTERM or SIGINT stops it cleanly (a posted name is unposted).
-const std = @import("std");
-const builtin = @import("builtin");
-const cloud9 = @import("cloud9");
-const tree = @import("tree.zig");
-
-const fs = cloud9.fs;
-const serve = cloud9.serve;
-const post = cloud9.post;
-const transport = cloud9.transport;
-const Io = std.Io;
-const linux = std.os.linux;
-
-const usage_text =
- \\usage: 9harness [--unix PATH | --tcp IP:PORT | --fd N] [--no-post]
- \\ [--name NAME] [--root NAME=PATH]... [--proc DIR]
- \\ [--allow-move] [--zmx PATH]
- \\
- \\A read-only, fresh-from-disk 9P2000 view of every harness's state:
- \\ /pid /uptime daemon facts
- \\ /claude/{projects,history,skills}
- \\ /codex/{sessions,session-index,history}
- \\ /omp /hermes /dsh full mirrors, raw
- \\ /skills/{claude,codex,omp} the union skills view
- \\ /active/<harness>/<pid>/ what is running right now
- \\
- \\By default the daemon posts itself under the name `harness`, so it is
- \\dialable at $XDG_RUNTIME_DIR/9p/harness and mountable by 9ns --mntgen
- \\at /mnt/9p/harness. --no-post skips posting; --unix/--tcp add plain
- \\listeners beside the post; --fd N serves one 9P session over the
- \\connected stream on descriptor N and posts nothing.
- \\--root NAME=PATH pins one harness root (NAME: claude, codex, omp,
- \\hermes, dsh) somewhere other than $HOME/.<NAME>; repeatable.
- \\--proc DIR reads the process tree somewhere other than /proc (for
- \\tests); --proc "" leaves /active out of the tree entirely.
- \\--allow-move lets a write to /active/<h>/<pid>/zmx move that agent
- \\into a zmx session of that name: the daemon kills it and re-execs
- \\the harness under zmx with its session resumed. Off by default,
- \\because it is the one place the tree is not read-only, and anything
- \\that can mount it can then kill an agent. --zmx PATH names the
- \\binary a move runs (default: zmx, found on $PATH).
- \\
- \\Run it in a zmx session, the zmx way: `zmx run harness -d 9harness`.
- \\
-;
-
-const max_connections = 16;
-const Runner = serve.Runner(tree.Harness, tree.opts, .{
- .msize = tree.msize,
- .connections = max_connections,
- .listeners = 2, // the posted name plus one of --unix/--tcp
-});
-
-// Static memory, the 9proc-demo way: the harness and the runner are large
-// (the path table, the per-connection buffers) and live in .bss.
-var harness_mem: tree.Harness = undefined;
-var runner_mem: Runner = undefined;
-var stop_requested = std.atomic.Value(bool).init(false);
-
-fn onSignal(sig: linux.SIG) callconv(.c) void {
- _ = sig;
- stop_requested.store(true, .release);
-}
-
-fn installSignalHandlers() void {
- const act = linux.Sigaction{
- .handler = .{ .handler = onSignal },
- .mask = @splat(0),
- .flags = 0,
- };
- _ = linux.sigaction(.TERM, &act, null);
- _ = linux.sigaction(.INT, &act, null);
- const ign = linux.Sigaction{
- .handler = .{ .handler = linux.SIG.IGN },
- .mask = @splat(0),
- .flags = 0,
- };
- _ = linux.sigaction(.PIPE, &ign, null);
-}
-
-// ---- the serve handler ---------------------------------------------------------
-
-fn serveReq(ctx: ?*anyopaque, conn: *Runner.Conn, req: fs.Req) void {
- const h: *tree.Harness = @ptrCast(@alignCast(ctx.?));
- // The answer's bytes point into the harness's shared buffers, so the
- // mutex spans handle and reply: the engine copies them into the
- // connection's output before the lock goes.
- h.mutex.lockUncancelable(h.io);
- const a = tree.handle(h, req);
- conn.reply(&a.reply, a.bytes);
- h.mutex.unlock(h.io);
-}
-
-/// `name` found on `path_env`, as an absolute path in the arena.
-fn onPath(io: Io, arena: std.mem.Allocator, path_env: []const u8, name: []const u8) ![]const u8 {
- var it = std.mem.splitScalar(u8, path_env, ':');
- while (it.next()) |dir_path| {
- if (dir_path.len == 0) continue;
- const candidate = try std.fmt.allocPrint(arena, "{s}/{s}", .{ dir_path, name });
- const st = Io.Dir.statFile(.cwd(), io, candidate, .{ .follow_symlinks = true }) catch continue;
- if (st.kind != .file) continue;
- return candidate;
- }
- return error.NotFound;
-}
-
-// ---- the CLI --------------------------------------------------------------------
-
-const Mode = enum { posted, unix, tcp, fd };
-
-pub fn main(init: std.process.Init) !void {
- run(init) catch |e| switch (e) {
- // Both are reported with their own line above; a returned error
- // would bury it under a Debug-build stack trace.
- error.Usage, error.AlreadyPosted => std.process.exit(1),
- else => return e,
- };
-}
-
-fn run(init: std.process.Init) !void {
- const io = init.io;
- const arena = init.arena.allocator();
- const args = try init.minimal.args.toSlice(arena);
- const envp: post.Env = init.minimal.environ.block.slice.ptr;
-
- var mode: Mode = .posted;
- var unix_path: []const u8 = "";
- var tcp_addr: []const u8 = "";
- var fd_no: i32 = -1;
- var post_name: []const u8 = "harness";
- var no_post = false;
- var base_overrides: [5]?[]const u8 = @splat(null);
- var proc_root: []const u8 = "/proc";
- var zmx_path: []const u8 = "zmx";
- var allow_move = false;
-
- var i: usize = 1;
- while (i < args.len) : (i += 1) {
- const a = args[i];
- if (std.mem.eql(u8, a, "--no-post")) {
- no_post = true;
- } else if (std.mem.eql(u8, a, "--allow-move")) {
- allow_move = true;
- } else if (std.mem.eql(u8, a, "--unix") or std.mem.eql(u8, a, "--tcp") or
- std.mem.eql(u8, a, "--fd") or std.mem.eql(u8, a, "--name") or
- std.mem.eql(u8, a, "--proc") or std.mem.eql(u8, a, "--zmx") or
- std.mem.eql(u8, a, "--root"))
- {
- i += 1;
- if (i >= args.len) {
- std.debug.print("9harness: {s} needs an argument\n{s}", .{ a, usage_text });
- return error.Usage;
- }
- const v = args[i];
- if (std.mem.eql(u8, a, "--unix")) {
- mode = .unix;
- unix_path = v;
- } else if (std.mem.eql(u8, a, "--tcp")) {
- mode = .tcp;
- tcp_addr = v;
- } else if (std.mem.eql(u8, a, "--fd")) {
- mode = .fd;
- fd_no = std.fmt.parseInt(i32, v, 10) catch {
- std.debug.print("9harness: --fd: not a number: {s}\n", .{v});
- return error.Usage;
- };
- } else if (std.mem.eql(u8, a, "--proc")) {
- proc_root = v;
- } else if (std.mem.eql(u8, a, "--zmx")) {
- zmx_path = v;
- } else if (std.mem.eql(u8, a, "--name")) {
- post_name = v;
- if (!post.legalName(post_name)) {
- std.debug.print("9harness: illegal post name: {s}\n", .{v});
- return error.Usage;
- }
- } else {
- const eq = std.mem.indexOfScalar(u8, v, '=') orelse {
- std.debug.print("9harness: --root NAME=PATH: {s}\n{s}", .{ v, usage_text });
- return error.Usage;
- };
- const name = v[0..eq];
- const root = std.meta.stringToEnum(tree.Root, name) orelse {
- std.debug.print("9harness: unknown root {s} (claude, codex, omp, hermes, dsh)\n", .{name});
- return error.Usage;
- };
- base_overrides[@intFromEnum(root)] = v[eq + 1 ..];
- }
- } else if (std.mem.eql(u8, a, "--help") or std.mem.eql(u8, a, "-h")) {
- std.debug.print("{s}", .{usage_text});
- return;
- } else {
- std.debug.print("9harness: unknown argument {s}\n{s}", .{ a, usage_text });
- return error.Usage;
- }
- }
-
- // The five pinned roots: $HOME/.<name> unless overridden.
- const home = post.getenv(envp, "HOME");
- var bases: [5][]const u8 = @splat("");
- inline for (0..5) |r| {
- if (base_overrides[r]) |path| {
- bases[r] = path;
- } else if (home) |hm| {
- bases[r] = std.fmt.bufPrint(&root_bufs[r], "{s}/.{s}", .{ hm, root_dirs[r] }) catch return error.NameTooLong;
- }
- }
- var any = false;
- for (bases) |b| any = any or b.len != 0;
- if (!any) {
- std.debug.print("9harness: no harness roots (set $HOME or pass --root NAME=PATH)\n", .{});
- return error.Usage;
- }
-
- // A move execs zmx by absolute path: the daemon resolves it once,
- // at startup, so nothing about $PATH matters when the write lands.
- const zmx_abs = if (std.mem.indexOfScalar(u8, zmx_path, '/') != null)
- zmx_path
- else
- onPath(io, arena, post.getenv(envp, "PATH") orelse "", zmx_path) catch zmx_path;
- if (allow_move and std.mem.indexOfScalar(u8, zmx_abs, '/') == null) {
- std.debug.print("9harness: --allow-move needs zmx on $PATH (or --zmx PATH)\n", .{});
- return error.Usage;
- }
-
- harness_mem.init(.{
- .io = io,
- .pid = @intCast(linux.getpid()),
- .bases = bases,
- .proc = proc_root,
- .home = home orelse "",
- .zmx = zmx_abs,
- .runtime = post.getenv(envp, "XDG_RUNTIME_DIR") orelse "",
- .allow_move = allow_move,
- .envp = envp,
- });
- if (allow_move) {
- std.debug.print("9harness: moves allowed — a write to /active/<h>/<pid>/zmx re-execs that agent under {s}\n", .{zmx_abs});
- }
- if (no_post and mode == .posted) {
- std.debug.print("9harness: --no-post needs a listen form (--unix, --tcp or --fd)\n{s}", .{usage_text});
- return error.Usage;
- }
-
- installSignalHandlers();
-
- switch (mode) {
- .fd => {
- std.debug.print("9harness: serving one session on fd {d}\n", .{fd_no});
- try serveFd(io, fd_no);
- return;
- },
- .posted, .unix, .tcp => {},
- }
-
- runner_mem.init(.{ .io = io, .root = tree.root, .handler = .{ .ctx = &harness_mem, .serve = serveReq } });
- defer runner_mem.stop();
-
- var posted_something = false;
- if (!no_post) {
- runner_mem.listenPosted(envp, post_name, 16) catch |err| {
- if (err == error.AlreadyPosted) {
- std.debug.print("9harness: the name `{s}` is already posted by a live server\n", .{post_name});
- return err;
- }
- std.debug.print("9harness: cannot post as {s}: {t}\n", .{ post_name, err });
- return err;
- };
- posted_something = true;
- var pbuf: [transport.sun_path_len]u8 = undefined;
- const path = post.registryPath(envp, post_name, &pbuf) catch "";
- std.debug.print("9harness: posted as {s} at {s}\n", .{ post_name, path });
- }
- switch (mode) {
- .unix => {
- _ = try runner_mem.listen(.{ .unix = try arena.dupeZ(u8, unix_path) }, 16);
- std.debug.print("9harness: listening on {s}\n", .{unix_path});
- },
- .tcp => {
- const addr = Io.net.IpAddress.parseLiteral(tcp_addr) catch {
- std.debug.print("9harness: bad --tcp address: {s}\n", .{tcp_addr});
- return error.Usage;
- };
- const bound = try runner_mem.listen(.{ .tcp = addr }, 16);
- std.debug.print("9harness: listening on tcp!{f}\n", .{bound});
- },
- else => {},
- }
- var pinned: u32 = 0;
- for (bases) |b| {
- if (b.len != 0) pinned += 1;
- }
- std.debug.print("9harness: serving ({d} roots pinned, {d} bytes of tables)\n", .{ pinned, @sizeOf(tree.Harness) });
-
- // The runner's tasks drive themselves; the main thread only waits for
- // a stop signal (SIGTERM/SIGINT) and then unposts via stop().
- while (!stop_requested.load(.acquire)) {
- io.sleep(.fromMilliseconds(250), .awake) catch break;
- }
- std.debug.print("9harness: stopping\n", .{});
-}
-
-const root_dirs = [5][]const u8{ "claude", "codex", "omp", "hermes", "dsh" };
-var root_bufs: [5][tree.base_capacity]u8 = @splat(@splat(0));
-
-// ---- --fd N: one 9P session over a connected stream -----------------------------
-
-/// Serves exactly one client on the connected stream of descriptor `n`
-/// (socket activation, `9ns --spawn`-style handoffs), driving the engine
-/// directly, the freestanding way. Returns when the client hangs up.
-fn serveFd(io: Io, n: i32) !void {
- const stream: Io.net.Stream = .{ .socket = .{ .handle = n, .address = .{ .ip4 = .loopback(0) } } };
- const Engine = fs.Server(tree.Harness, tree.opts);
- var in: [tree.msize]u8 = undefined;
- var out: [2 * tree.msize]u8 = undefined;
- var rbuf: [tree.msize]u8 = undefined;
- var wbuf: [2 * tree.msize]u8 = undefined;
- var stage: [tree.msize]u8 = undefined;
- var engine = Engine.init(.{ .in = &in, .out = &out, .root = tree.root });
- var reader = stream.reader(io, &rbuf);
- var writer = stream.writer(io, &wbuf);
- while (true) {
- const frame = transport.readFrame(&reader.interface, &stage, tree.msize) catch return;
- var off: usize = 0;
- while (off < frame.len) {
- off += engine.push(frame[off..]);
- while (true) {
- const req = engine.next() orelse break;
- const a = answer(req);
- engine.reply(&a.reply, a.bytes);
- }
- const pending = engine.output();
- writer.interface.writeAll(pending) catch return;
- engine.wrote(pending.len);
- if (engine.protocol.dead) return;
- }
- writer.interface.flush() catch return;
- }
-}
-
-/// One request for the --fd loop: same locking discipline as the runner's
-/// handler (the reply bytes live in the harness's shared buffers).
-fn answer(req: fs.Req) tree.Answer {
- harness_mem.mutex.lockUncancelable(harness_mem.io);
- defer harness_mem.mutex.unlock(harness_mem.io);
- return tree.handle(&harness_mem, req);
-}
-
-test {
- _ = @import("tree.zig"); // the tree's unit tests (exclusions, ids, ...)
-}
-
-test "main: the root override parser pins each named root" {
- // Parsing is inline in run(); the mapping itself is what the tests
- // rely on, and it is exercised end to end in test/e2e.sh.
- _ = std.meta.stringToEnum(tree.Root, "claude").?;
- _ = std.meta.stringToEnum(tree.Root, "codex").?;
- _ = std.meta.stringToEnum(tree.Root, "omp").?;
- _ = std.meta.stringToEnum(tree.Root, "hermes").?;
- _ = std.meta.stringToEnum(tree.Root, "dsh").?;
- try std.testing.expect(std.meta.stringToEnum(tree.Root, "zmx") == null);
-}