diff options
Diffstat (limited to '9harness/src/main.zig')
| -rw-r--r-- | 9harness/src/main.zig | 61 |
1 files changed, 59 insertions, 2 deletions
diff --git a/9harness/src/main.zig b/9harness/src/main.zig index 2355a4f..fe0a07b 100644 --- a/9harness/src/main.zig +++ b/9harness/src/main.zig @@ -30,7 +30,8 @@ const linux = std.os.linux; const usage_text = \\usage: 9harness [--unix PATH | --tcp IP:PORT | --fd N] [--no-post] - \\ [--name NAME] [--root NAME=PATH]... + \\ [--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 @@ -38,6 +39,7 @@ const usage_text = \\ /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 @@ -46,6 +48,14 @@ const usage_text = \\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`. \\ @@ -98,6 +108,19 @@ fn serveReq(ctx: ?*anyopaque, conn: *Runner.Conn, req: fs.Req) void { 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 }; @@ -124,14 +147,20 @@ fn run(init: std.process.Init) !void { 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; @@ -152,6 +181,10 @@ fn run(init: std.process.Init) !void { 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)) { @@ -196,7 +229,31 @@ fn run(init: std.process.Init) !void { return error.Usage; } - harness_mem.init(.{ .io = io, .pid = @intCast(linux.getpid()), .bases = bases }); + // 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; |
