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.zig61
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;