summaryrefslogtreecommitdiff
path: root/9agents/src/tree.zig
diff options
context:
space:
mode:
Diffstat (limited to '9agents/src/tree.zig')
-rw-r--r--9agents/src/tree.zig2413
1 files changed, 2413 insertions, 0 deletions
diff --git a/9agents/src/tree.zig b/9agents/src/tree.zig
new file mode 100644
index 0000000..ed0699c
--- /dev/null
+++ b/9agents/src/tree.zig
@@ -0,0 +1,2413 @@
+//! The harness file tree served over 9P2000: a unified, read-only,
+//! fresh-from-disk view of every AI-agent harness's state on the machine.
+//!
+//! /README what the tree is and how to read it
+//! /pid /uptime daemon facts, one line each
+//! /claude/ projects/ (mirror of ~/.claude/projects),
+//! history (~/.claude/history.jsonl),
+//! skills/ (mirror of ~/.claude/skills)
+//! /codex/ sessions/ (~/.codex/sessions),
+//! session-index (~/.codex/session_index.jsonl),
+//! history (~/.codex/history.jsonl)
+//! /omp/ mirror of ~/.omp/agent, raw blobs
+//! /hermes/ mirror of ~/.hermes, raw
+//! /dsh/ mirror of ~/.dsh
+//! /skills/{claude,codex,omp}/ the same skills trees the harnesses own
+//!
+//! Everything below the named mount points is a lazy mirror: a lookup,
+//! getattr or readdir walks the real filesystem at request time, so a
+//! session transcript grows as its harness writes it and a new session
+//! appears as soon as its file lands. No cache, no invalidation.
+//!
+//! Security boundary — the exclusion rule is absolute and unit-tested:
+//! no name that looks like a credential, key, token or auth store is ever
+//! answered, at any depth (`excluded`); symlinks are never served (they
+//! are an escape hatch around the pinned roots). Every path is resolved
+//! from its pinned root one component at a time with `O_NOFOLLOW`
+//! (`openIn`), so no name below a root — swapped mid-session or not —
+//! can point the daemon at a file outside it. The daemon must never
+//! become a credential reader for anything that mounts it. Writes,
+//! creates and setattrs answer EPERM.
+//!
+//! This module is the backend of `cloud9.fs.Server` (main.zig hands it to
+//! `serve.Runner`). It declares `features = .{ .references = true }`: every
+//! lookup result is a reference, so table entries below (the mirrored
+//! files) are refcounted and freed when the last fid lets go. Node ids
+//! pack (kind, root index, serial) in a u64, zmx-style: a mirrored file's
+//! serial is its table slot and generation, so two walks of the same file
+//! answer the same qid path while it is held.
+const std = @import("std");
+const cloud9 = @import("cloud9");
+const fs = cloud9.fs;
+const E = fs.E;
+const Io = std.Io;
+const linux = std.os.linux;
+pub const active = @import("active.zig");
+
+// ---- comptime bounds ---------------------------------------------------------
+
+/// Longest relative path a mirrored file may carry (bytes below a root).
+pub const rel_capacity: usize = 640;
+/// Mirrored files remembered at once; each holds one reference per fid.
+pub const path_capacity: usize = 4096;
+comptime {
+ if (path_capacity > 1 << 12) @compileError("path table slots must fit the 12 serial bits");
+}
+/// Largest frame the daemon negotiates; read and readdir staging buffers
+/// are this big, so a single Rread can carry one full frame of bytes.
+pub const msize: u32 = 32 * 1024;
+/// Names one directory listing may stage (sorted for cursor stability).
+pub const list_capacity: usize = 1024;
+/// Bytes of name storage one listing may use (255 per name, packed).
+pub const list_name_bytes: usize = 128 * 1024;
+/// Longest base path a pinned root may carry.
+pub const base_capacity: usize = 512;
+
+pub const opts: fs.Options = .{
+ .fid_capacity = 512,
+ .slot_capacity = 8,
+ .name_capacity = 255,
+ .username_capacity = 28,
+ .fid_index = true,
+};
+
+// ---- the tree skeleton ------------------------------------------------------
+
+pub const Root = enum(u8) { claude, codex, omp, hermes, dsh };
+
+/// Static nodes: the facts, the harness dirs and the union skills dir.
+pub const Top = enum(u8) {
+ root = 1,
+ pid,
+ uptime,
+ claude,
+ codex,
+ omp,
+ hermes,
+ dsh,
+ skills,
+ active,
+ readme,
+
+ /// Directory entries of the root, in listing order.
+ pub const listed = [_]Top{ .readme, .pid, .uptime, .claude, .codex, .omp, .hermes, .dsh, .skills, .active };
+
+ pub fn fileName(t: Top) []const u8 {
+ // The only name that is not its tag: a README announces itself in
+ // the listing, and lowercase would bury it among the harness dirs.
+ return if (t == .readme) "README" else @tagName(t);
+ }
+
+ pub fn dir(t: Top) bool {
+ return switch (t) {
+ .root, .claude, .codex, .omp, .hermes, .dsh, .skills, .active => true,
+ .pid, .uptime, .readme => false,
+ };
+ }
+
+ fn parent(t: Top) Top {
+ _ = t;
+ return .root; // the root is its own parent; every top hangs off it
+ }
+
+ /// The mirror a harness dir serves: its root and the relative path
+ /// below it. The claude and codex dirs are *virtual* (their children
+ /// are named mounts); omp, hermes and dsh mirror one subtree each.
+ fn mirror(t: Top) ?Mount {
+ return switch (t) {
+ .omp => .{ .root = .omp, .rel = "agent" },
+ .hermes => .{ .root = .hermes, .rel = "" },
+ .dsh => .{ .root = .dsh, .rel = "" },
+ else => null,
+ };
+ }
+};
+
+/// Where a mirrored subtree sits: `rel` below the root's pinned base path.
+pub const Mount = struct {
+ root: Root,
+ rel: []const u8,
+};
+
+const NamedMount = struct {
+ /// The name as it appears in its virtual directory.
+ name: []const u8,
+ owner: Top,
+ mount: Mount,
+};
+
+/// The children of the virtual directories (claude, codex, skills). The
+/// same target may appear twice (/claude/skills and /skills/claude): a
+/// lookup of either hands out the same table entry, so both paths share
+/// qid paths and identity.
+const named_mounts = [_]NamedMount{
+ .{ .name = "projects", .owner = .claude, .mount = .{ .root = .claude, .rel = "projects" } },
+ .{ .name = "history", .owner = .claude, .mount = .{ .root = .claude, .rel = "history.jsonl" } },
+ .{ .name = "skills", .owner = .claude, .mount = .{ .root = .claude, .rel = "skills" } },
+ .{ .name = "sessions", .owner = .codex, .mount = .{ .root = .codex, .rel = "sessions" } },
+ .{ .name = "session-index", .owner = .codex, .mount = .{ .root = .codex, .rel = "session_index.jsonl" } },
+ .{ .name = "history", .owner = .codex, .mount = .{ .root = .codex, .rel = "history.jsonl" } },
+ .{ .name = "claude", .owner = .skills, .mount = .{ .root = .claude, .rel = "skills" } },
+ .{ .name = "codex", .owner = .skills, .mount = .{ .root = .codex, .rel = "skills" } },
+ .{ .name = "omp", .owner = .skills, .mount = .{ .root = .omp, .rel = "skills" } },
+};
+
+fn mountNamed(owner: Top, name: []const u8) ?Mount {
+ for (named_mounts) |m| {
+ if (m.owner == owner and std.mem.eql(u8, m.name, name)) return m.mount;
+ }
+ return null;
+}
+
+/// The Top that owns the mount whose target is exactly (root, rel) — the
+/// canonical parent a ".." walk answers. Falls back to the root's harness
+/// dir for targets no named mount covers (deep paths, mirrors).
+fn mountOwner(r: Root, rel_path: []const u8) Top {
+ for (named_mounts) |m| {
+ if (m.mount.root == r and std.mem.eql(u8, m.mount.rel, rel_path)) return m.owner;
+ }
+ return switch (r) {
+ .claude => .claude,
+ .codex => .codex,
+ .omp => .omp,
+ .hermes => .hermes,
+ .dsh => .dsh,
+ };
+}
+
+// ---- node ids and the path table ---------------------------------------------
+
+pub const Kind = enum(u8) { top = 0, path = 1, active = 2 };
+
+comptime {
+ // The derived view indexes the same pinned roots the mirror does.
+ for (std.enums.values(Root)) |r| {
+ if (!std.mem.eql(u8, @tagName(r), @tagName(@as(active.Kind, @enumFromInt(@intFromEnum(r))))))
+ @compileError("tree.Root and active.Kind must agree, index by index");
+ }
+}
+
+/// A node under `/active`. Everything but a harness directory names a
+/// slot of the live table and the generation it had when the id was
+/// minted, so an id outlives its process only long enough to answer
+/// ENOENT.
+pub const AFile = enum(u8) {
+ /// `/active/<harness>`
+ harness_dir,
+ /// `/active/<harness>/<pid>`
+ entry,
+ pid,
+ ppid,
+ started,
+ cwd,
+ name,
+ title,
+ session,
+ model,
+ via,
+ zmx,
+ status,
+ transcript,
+ /// `/active/<harness>/<pid>/agents`
+ agents,
+ /// `/active/<harness>/<pid>/agents/<name>`
+ agent,
+ agent_model,
+ agent_transcript,
+
+ /// The fields of one entry, in listing order. A field with no value
+ /// is not listed and does not resolve.
+ pub const entry_files = [_]AFile{
+ .pid, .ppid, .started, .cwd, .name, .title,
+ .session, .model, .via, .zmx, .status, .transcript,
+ };
+ pub const agent_files = [_]AFile{ .agent_model, .agent_transcript };
+
+ pub fn fileName(f: AFile) []const u8 {
+ return switch (f) {
+ .agent_model => "model",
+ .agent_transcript => "transcript",
+ else => @tagName(f),
+ };
+ }
+};
+
+/// A resolved `/active` node.
+pub const Act = struct {
+ file: AFile,
+ /// Only for `harness_dir`.
+ kind: active.Kind = .claude,
+ slot: u8 = 0,
+ gen: u8 = 0,
+ agent: u8 = 0,
+};
+
+pub fn activeNode(a: Act) u64 {
+ const serial: u48 = switch (a.file) {
+ .harness_dir => @intFromEnum(a.kind),
+ else => @as(u48, a.slot) | (@as(u48, a.gen) << 8) | (@as(u48, a.agent) << 16),
+ };
+ return @bitCast(Node{
+ .idx = @intFromEnum(a.file),
+ .kind = @intFromEnum(Kind.active),
+ .serial = serial,
+ });
+}
+
+pub const Node = packed struct(u64) {
+ /// A Top index (kind == .top) or a Root index (kind == .path).
+ idx: u8 = 0,
+ kind: u8 = 0,
+ serial: u48 = 0,
+};
+
+pub fn topNode(t: Top) u64 {
+ return @bitCast(Node{ .idx = @intFromEnum(t), .kind = @intFromEnum(Kind.top) });
+}
+
+pub const root: u64 = topNode(Top.root);
+
+/// One remembered mirrored file: its root, its relative path, the hash
+/// that short-circuits dedupe lookups, and its reference count (one per
+/// fid holding it, handed out by lookup, paid back by release).
+const Entry = struct {
+ used: bool = false,
+ root: Root = .claude,
+ rel_buf: [rel_capacity]u8 = undefined,
+ rel_len: u16 = 0,
+ hash: u64 = 0,
+ refs: u32 = 0,
+ gen: u32 = 0,
+
+ fn rel(e: *const Entry) []const u8 {
+ return e.rel_buf[0..e.rel_len];
+ }
+};
+
+fn serialOf(slot: u12, gen: u32) u48 {
+ return (@as(u48, gen) << 12) | slot;
+}
+
+fn nodeOf(e: *const Entry, slot: u12) u64 {
+ return @bitCast(Node{
+ .idx = @intFromEnum(e.root),
+ .kind = @intFromEnum(Kind.path),
+ .serial = serialOf(slot, e.gen),
+ });
+}
+
+// ---- the exclusions (the security boundary) -----------------------------------
+
+fn containsFold(name: []const u8, needle: []const u8) bool {
+ if (name.len < needle.len) return false;
+ var i: usize = 0;
+ while (i + needle.len <= name.len) : (i += 1) {
+ if (std.ascii.eqlIgnoreCase(name[i .. i + needle.len], needle)) return true;
+ }
+ return false;
+}
+
+fn endsWithFold(name: []const u8, suffix: []const u8) bool {
+ return name.len >= suffix.len and std.ascii.eqlIgnoreCase(name[name.len - suffix.len ..], suffix);
+}
+
+fn isOneOf(name: []const u8, comptime names: []const []const u8) bool {
+ inline for (names) |n| if (std.ascii.eqlIgnoreCase(name, n)) return true;
+ return false;
+}
+
+/// Absolute rule: a name that may hold a credential, key, token or auth
+/// material is never served, at any depth, by lookup or readdir. The
+/// substrings are folded (case-insensitive) and deliberately broad —
+/// "auth" also hides "author-notes", the price of never guessing wrong.
+/// When unsure, exclude and document (docs/DESIGN.md).
+pub fn excluded(name: []const u8) bool {
+ @setEvalBranchQuota(20000);
+ if (name.len == 0) return true;
+ for ([_][]const u8{ "credentials", "token", "auth", "secret" }) |needle| {
+ if (containsFold(name, needle)) return true;
+ }
+ if (endsWithFold(name, ".key")) return true;
+ if (endsWithFold(name, ".pem")) return true;
+ if (endsWithFold(name, ".env")) return true;
+ // Config and settings files of the other harnesses may embed API keys
+ // (hermes config.yaml, omp config.yml, dsh settings.yaml). Claude
+ // Code's settings.json is not in the tree at all: /claude serves only
+ // projects/, skills/ and history.jsonl.
+ if (isOneOf(name, &.{ "settings.json", "settings.yaml", "settings.local.json", "config.yml", "config.yaml", "config.toml", "models.yml", "models.yaml", ".ssh" })) return true;
+ for ([_][]const u8{ "id_rsa", "id_dsa", "id_ecdsa", "id_ed25519" }) |prefix| {
+ if (name.len >= prefix.len and std.ascii.eqlIgnoreCase(name[0..prefix.len], prefix)) return true;
+ }
+ return false;
+}
+
+/// Every component of a relative path must pass `excluded`; used on the
+/// comptime mount targets and the unit-test fixtures alike.
+pub fn excludedPath(rel_path: []const u8) bool {
+ @setEvalBranchQuota(4000);
+ var it = std.mem.splitScalar(u8, rel_path, '/');
+ while (it.next()) |comp| {
+ if (excluded(comp)) return true;
+ }
+ return false;
+}
+
+// ---- the backend --------------------------------------------------------------
+
+pub const Answer = struct {
+ reply: fs.Reply,
+ bytes: []const u8 = "",
+};
+
+fn fail(tag: u64, e: u16) Answer {
+ return .{ .reply = fs.Reply.fail(tag, e) };
+}
+
+/// A refusal that says why. 9P2000 has no numeric error — error(5) is
+/// `Rerror tag[2] ename[s]`, a string — and acme names its refusals in words
+/// ("permission denied", "file does not exist", fsys.c). A server that only
+/// answers bare errnos throws away the one channel the protocol gives it for
+/// explaining itself, which matters most here: a move has seven different
+/// reasons to say no, and EPERM tells the user none of them.
+///
+/// `errno` is still carried, because the reader may be a Linux mount rather
+/// than a person: 9ns maps the *text* back to an errno by case-insensitive
+/// substring (enameToErrno in 9ns/src/nine.zig, first match wins). So each
+/// message below is worded to keep the phrase that carries its errno —
+/// "not permitted" for EPERM, "invalid" for EINVAL, "exists" for EEXIST,
+/// "no such" for ENOENT — and to avoid phrases that would silently retarget
+/// it ("permission" and "denied" map to EACCES, "read-only" to EROFS, and
+/// the bare substring "fid" to EBADF).
+fn failWhy(tag: u64, e: u16, ename: []const u8) Answer {
+ return .{ .reply = .{ .tag = tag, .status = .err, .errno = e, .ename = ename } };
+}
+
+/// The daemon's state. One instance serves every connection; `handle` is
+/// called with `mutex` held (the serve handler in main.zig holds it across
+/// handle and reply, because `bytes` point into the shared buffers).
+pub const Harness = struct {
+ pub const Req = fs.Req;
+ pub const Reply = fs.Reply;
+ pub const features: fs.Features = .{ .references = true };
+
+ io: Io,
+ pid: u32 = 0,
+ started_sec: i64 = 0,
+ /// The five pinned roots; a root without a base is unreachable.
+ base_buf: [5][base_capacity]u8 = @splat(@splat(0)),
+ base_len: [5]u16 = @splat(0),
+ base_set: [5]bool = @splat(false),
+ table: [path_capacity]Entry = @splat(.{}),
+ mutex: Io.Mutex = .init,
+ // Shared staging (guarded by `mutex`).
+ data_buf: [msize]u8 = undefined,
+ stage_buf: [msize]u8 = undefined,
+ list_names: [list_name_bytes]u8 = undefined,
+ list_dirs: [list_capacity]bool = undefined,
+ list_offs: [list_capacity]u32 = undefined,
+
+ // The derived view (`/active`). An empty `proc` base turns it off,
+ // which is the default: a test must opt in to reading a process
+ // tree, and never the live one.
+ live: active.Table = .{},
+ proc_buf: [base_capacity]u8 = @splat(0),
+ proc_len: u16 = 0,
+ home_buf: [base_capacity]u8 = @splat(0),
+ home_len: u16 = 0,
+ zmx_buf: [base_capacity]u8 = @splat(0),
+ zmx_len: u16 = 0,
+ runtime_buf: [base_capacity]u8 = @splat(0),
+ runtime_len: u16 = 0,
+ envp: ?cloud9.post.Env = null,
+ self_pid: u32 = 0,
+ btime: i64 = 0,
+ allow_move: bool = false,
+ act_text: [1024]u8 = undefined,
+ act_name: [64]u8 = undefined,
+ act_path: [active.path_capacity]u8 = undefined,
+
+ pub const InitOptions = struct {
+ io: Io,
+ pid: u32,
+ /// Base path of each root, or "" when the root is not pinned.
+ bases: [5][]const u8,
+ /// The proc filesystem `/active` reads. Empty — the default —
+ /// leaves the derived view out of the tree entirely.
+ proc: []const u8 = "",
+ /// $HOME, for the harnesses' store-slug spellings.
+ home: []const u8 = "",
+ /// The zmx binary a move execs, resolved to an absolute path by
+ /// the caller. Never client bytes.
+ zmx: []const u8 = "zmx",
+ /// $XDG_RUNTIME_DIR, where a move looks for the zmx session it
+ /// is about to create.
+ runtime: []const u8 = "",
+ /// The daemon's own environment block, handed to the harness a
+ /// move re-execs. Null leaves a moved agent with an empty one.
+ envp: ?cloud9.post.Env = null,
+ /// Whether writing `/active/<h>/<pid>/zmx` may move a session.
+ allow_move: bool = false,
+ };
+
+ pub fn init(h: *Harness, o: InitOptions) void {
+ h.* = .{ .io = o.io, .pid = o.pid, .started_sec = Io.Timestamp.now(o.io, .real).toSeconds() };
+ if (o.proc.len > 0 and o.proc.len <= base_capacity) {
+ @memcpy(h.proc_buf[0..o.proc.len], o.proc);
+ h.proc_len = @intCast(o.proc.len);
+ h.self_pid = o.pid;
+ h.btime = active.bootTime(o.io, o.proc);
+ }
+ if (o.home.len <= base_capacity) {
+ @memcpy(h.home_buf[0..o.home.len], o.home);
+ h.home_len = @intCast(o.home.len);
+ }
+ if (o.zmx.len > 0 and o.zmx.len <= base_capacity) {
+ @memcpy(h.zmx_buf[0..o.zmx.len], o.zmx);
+ h.zmx_len = @intCast(o.zmx.len);
+ }
+ if (o.runtime.len <= base_capacity) {
+ @memcpy(h.runtime_buf[0..o.runtime.len], o.runtime);
+ h.runtime_len = @intCast(o.runtime.len);
+ }
+ h.allow_move = o.allow_move;
+ h.envp = o.envp;
+ inline for (0..5) |i| {
+ const path = o.bases[i];
+ h.base_set[i] = path.len > 0 and path.len <= base_capacity;
+ if (h.base_set[i]) {
+ @memcpy(h.base_buf[i][0..path.len], path);
+ h.base_len[i] = @intCast(path.len);
+ }
+ }
+ }
+
+ pub fn base(h: *const Harness, r: Root) ?[]const u8 {
+ if (!h.base_set[@intFromEnum(r)]) return null;
+ return h.base_buf[@intFromEnum(r)][0..h.base_len[@intFromEnum(r)]];
+ }
+
+ // -- path table --
+
+ fn hashOf(r: Root, rel_path: []const u8) u64 {
+ var hh = std.hash.Wyhash.init(0x6861_726e_6573_7300); // "harness"
+ hh.update(&.{@intFromEnum(r)});
+ hh.update(rel_path);
+ return hh.final();
+ }
+
+ /// Finds or creates the entry for (root, rel). `ref` says whether the
+ /// caller hands out a reference (a lookup) or only names the file (a
+ /// readdir record). Two walks of the same file find the same entry,
+ /// so their node ids are equal; a freed entry's generation changes.
+ fn entryFor(h: *Harness, r: Root, rel_path: []const u8, ref: bool) ?*Entry {
+ if (rel_path.len == 0 or rel_path.len > rel_capacity) return null;
+ const hash = hashOf(r, rel_path);
+ var free: ?*Entry = null;
+ for (&h.table) |*e| {
+ if (!e.used) {
+ if (free == null) free = e;
+ continue;
+ }
+ if (e.hash == hash and e.root == r and e.rel_len == rel_path.len and
+ std.mem.eql(u8, e.rel(), rel_path))
+ {
+ if (ref) e.refs += 1;
+ return e;
+ }
+ }
+ const e = free orelse return null;
+ e.used = true;
+ e.root = r;
+ e.rel_len = @intCast(rel_path.len);
+ @memcpy(e.rel_buf[0..rel_path.len], rel_path);
+ e.hash = hash;
+ e.refs = @intFromBool(ref);
+ e.gen +%= 1;
+ return e;
+ }
+
+ fn slotOf(h: *Harness, e: *Entry) u12 {
+ const off = (@intFromPtr(e) - @intFromPtr(&h.table)) / @sizeOf(Entry);
+ return @intCast(off);
+ }
+
+ /// The target a node names; a freed or forged serial answers null.
+ fn resolve(h: *Harness, node: u64) ?Target {
+ const n: Node = @bitCast(node);
+ switch (std.enums.fromInt(Kind, n.kind) orelse return null) {
+ .top => return .{ .top = std.enums.fromInt(Top, n.idx) orelse return null },
+ .path => {
+ const r = std.enums.fromInt(Root, n.idx) orelse return null;
+ const slot: u12 = @truncate(n.serial);
+ const e = &h.table[slot];
+ if (!e.used or e.root != r) return null;
+ if (serialOf(slot, e.gen) != n.serial) return null;
+ return .{ .path = e };
+ },
+ .active => {
+ const f = std.enums.fromInt(AFile, n.idx) orelse return null;
+ if (f == .harness_dir) {
+ const k = std.enums.fromInt(active.Kind, @as(u8, @truncate(n.serial))) orelse return null;
+ return .{ .act = .{ .file = f, .kind = k } };
+ }
+ const slot: u8 = @truncate(n.serial);
+ const gen: u8 = @truncate(n.serial >> 8);
+ const agent: u8 = @truncate(n.serial >> 16);
+ const l = h.live.at(slot, gen) orelse return null;
+ return .{ .act = .{ .file = f, .kind = l.kind, .slot = slot, .gen = gen, .agent = agent } };
+ },
+ }
+ }
+
+ fn entryNode(h: *Harness, e: *Entry) u64 {
+ return nodeOf(e, h.slotOf(e));
+ }
+};
+
+pub const Target = union(enum) {
+ top: Top,
+ path: *Entry,
+ act: Act,
+};
+
+// ---- attributes ---------------------------------------------------------------
+
+/// Opens `<base(root)>/<rel>` without following a symlink at any step
+/// below the base: every component is opened with `O_NOFOLLOW`, so a
+/// name swapped for a symlink between the walk and the read fails
+/// instead of reaching out of the root. Composing the whole path and
+/// opening it in one call cannot do that — only the last component's
+/// symlink is refused then, and every directory above it is followed
+/// wherever it points. The base itself is configuration (`--root` or
+/// $HOME), not client bytes, so it resolves normally. The caller owns
+/// the descriptor.
+fn openIn(h: *Harness, r: Root, rel_path: []const u8, final: linux.O) ?i32 {
+ const b = h.base(r) orelse return null;
+ if (rel_path.len > rel_capacity or b.len > base_capacity) return null;
+ const walk: linux.O = .{ .PATH = true, .DIRECTORY = true, .CLOEXEC = true, .NOFOLLOW = true };
+
+ var base_z: [base_capacity + 1]u8 = @splat(0);
+ @memcpy(base_z[0..b.len], b);
+ var top_flags = if (rel_path.len == 0) final else walk;
+ top_flags.NOFOLLOW = false; // the pinned root may legitimately be a link
+ top_flags.CLOEXEC = true;
+ var fd = fdOf(linux.open(@ptrCast(&base_z), top_flags, 0)) orelse return null;
+ if (rel_path.len == 0) return fd;
+
+ var rest = rel_path;
+ while (true) {
+ const slash = std.mem.indexOfScalar(u8, rest, '/');
+ const comp = if (slash) |i| rest[0..i] else rest;
+ const last = slash == null;
+ // Nothing below composes these; a component that could re-enter
+ // the walk is refused rather than opened.
+ if (comp.len == 0 or comp.len > 255 or
+ std.mem.eql(u8, comp, ".") or std.mem.eql(u8, comp, ".."))
+ {
+ _ = linux.close(fd);
+ return null;
+ }
+ var name_z: [256]u8 = @splat(0);
+ @memcpy(name_z[0..comp.len], comp);
+ var flags = if (last) final else walk;
+ flags.NOFOLLOW = true;
+ flags.CLOEXEC = true;
+ const next = fdOf(linux.openat(fd, @ptrCast(&name_z), flags, 0));
+ _ = linux.close(fd);
+ fd = next orelse return null;
+ if (last) return fd;
+ rest = rest[comp.len + 1 ..];
+ }
+}
+
+fn fdOf(rc: usize) ?i32 {
+ if (linux.errno(rc) != .SUCCESS) return null;
+ return @intCast(rc);
+}
+
+/// Stat of `<base(root)>/<rel>`, resolved the no-follow way. A symlink
+/// is never served: it is the one escape hatch around the pinned roots.
+/// `O_PATH` opens the name itself, so a fifo or device never blocks and
+/// never has its open side effects run.
+/// A stat plus the file's identity on disk.
+const Stat = struct {
+ st: Io.Dir.Stat,
+ /// The qid path to report, or 0 when the kernel would not say.
+ ///
+ /// intro(5) is strict about what a qid path means: "The path is an
+ /// integer unique among all files in the hierarchy. If a file is deleted
+ /// and recreated with the same name in the same directory, the old and
+ /// new path components of the qids should be different" — and two files
+ /// are the same file if and only if their qids match. So the path has to
+ /// be a property of the *file*, stable for as long as it exists.
+ ///
+ /// The backend's own node id cannot supply that: it is a slot in the path
+ /// table, and a slot freed by its last release comes back with a new
+ /// generation (the guard that stops a forged node id resolving). A client
+ /// that walks, stats and clunks — `9p stat` does exactly this — saw a
+ /// different qid path for the same unchanged file every single time, and
+ /// by the protocol's own rule that means a different file.
+ ///
+ /// So this is u9fs's answer instead, from the same fstat the attr already
+ /// needs:
+ ///
+ /// memmove(p, &st->st_ino, ...); *(dev_t*)q ^= st->st_dev; -- u9fs.c
+ ///
+ /// The device is mixed in because an inode number is only unique within
+ /// its filesystem, and the five pinned roots need not share one. The
+ /// engine keeps resolving requests by node id, which still recycles; only
+ /// the qid the client sees is stabilised (fs.Attr.path exists for exactly
+ /// this split, and the engine uses it for nothing else).
+ qid_path: u64,
+};
+
+fn statIn(h: *Harness, r: Root, rel_path: []const u8) ?Stat {
+ const fd = openIn(h, r, rel_path, .{ .PATH = true, .CLOEXEC = true }) orelse return null;
+ const f: Io.File = .{ .handle = fd, .flags = .{ .nonblocking = false } };
+ defer f.close(h.io);
+ const st = f.stat(h.io) catch return null;
+ if (st.kind == .sym_link) return null; // never served: an escape hatch
+ return .{ .st = st, .qid_path = identityOf(fd) };
+}
+
+/// (dev, ino) of an open descriptor, folded into one integer. 0 when statx
+/// declines to answer, which the caller reads as "fall back to the node id".
+fn identityOf(fd: i32) u64 {
+ var sx: linux.Statx = undefined;
+ const want: linux.STATX = .{ .INO = true };
+ const rc = linux.statx(fd, "", linux.AT.EMPTY_PATH, want, &sx);
+ if (linux.errno(rc) != .SUCCESS or !sx.mask.INO) return 0;
+ const dev = (@as(u64, sx.dev_major) << 32) | sx.dev_minor;
+ // The inode carries the entropy, so it stays unshifted and the device is
+ // folded over its top bits: two roots on one filesystem keep distinct
+ // inodes, and the same inode on two filesystems stops colliding.
+ return sx.ino ^ (dev *% 0x9E37_79B9_7F4A_7C15);
+}
+
+/// The qid version of a file on disk, the way u9fs derives it:
+///
+/// qid.vers = st->st_mtime ^ (st->st_size << 8); -- u9fs.c
+///
+/// intro(5) makes the qid the server's identity for a file — "two files on
+/// the same server hierarchy are the same if and only if their qids are the
+/// same" — and the version the part that "is incremented every time the file
+/// is modified". A server that leaves it 0 is telling every client that
+/// nothing it serves ever changes, which is exactly wrong for this tree: its
+/// whole point is transcripts that grow while you read them. The size shift
+/// is what makes an append within the same second still change the version,
+/// and a rewrite that keeps the size change it through the mtime.
+fn qidVers(mtime_sec: i64, size: u64) u32 {
+ return @truncate(@as(u64, @bitCast(mtime_sec)) ^ (size << 8));
+}
+
+/// The qid version of text this server derives rather than reads: there is no
+/// mtime behind it, so the content itself is the only honest version.
+fn textVers(text: []const u8) u32 {
+ return @truncate(std.hash.Wyhash.hash(0, text));
+}
+
+fn statAttr(h: *Harness, e: *Entry) ?fs.Attr {
+ const got = statIn(h, e.root, e.rel()) orelse return null;
+ const st = got.st;
+ const mtime = st.mtime.toSeconds();
+ const size: u64 = if (st.kind == .directory) 0 else st.size;
+ return .{
+ .name = basename(e.rel()),
+ .node = h.entryNode(e),
+ .dir = st.kind == .directory,
+ // stat(5): "Directories and most files representing devices have a
+ // conventional length of 0."
+ .size = size,
+ .mode = if (st.kind == .directory) 0o555 else 0o444,
+ .mtime = @truncate(@as(u64, @bitCast(mtime))),
+ .version = qidVers(mtime, size),
+ .path = if (got.qid_path != 0) got.qid_path else null,
+ };
+}
+
+fn basename(rel_path: []const u8) []const u8 {
+ if (std.mem.lastIndexOfScalar(u8, rel_path, '/')) |i| return rel_path[i + 1 ..];
+ return rel_path;
+}
+
+/// Served as `/README`: what the tree is and how to read it. Static, so it
+/// is the one "fact" that needs no buffer — `factText` returns it directly.
+const readme_text =
+ \\9agents - every coding agent's state on this machine, as files.
+ \\Read-only: writes answer EPERM. Nothing is cached, so a transcript
+ \\grows while you cat it and a new session appears as soon as its
+ \\file lands.
+ \\
+ \\ README this file
+ \\ pid uptime this daemon's pid; seconds since it started
+ \\ active/ what is running right now
+ \\ claude/ projects/ history skills/
+ \\ codex/ sessions/ session-index history
+ \\ omp/ hermes/ dsh/ each harness's own directory, raw
+ \\ skills/ claude/ codex/ omp/, in one place
+ \\
+ \\active/<harness>/<pid>/ holds pid ppid started cwd name title
+ \\session via status transcript zmx.
+ \\ session its session id, empty when it did not resolve
+ \\ via how that id was found: registry, fd, dir or none
+ \\ status only harnesses that publish one have it
+ \\ transcript the live .jsonl; its mtime is the last activity
+ \\ zmx the zmx session it runs in, empty outside zmx
+ \\
+ \\ ls active/*/* what is running
+ \\ cat active/claude/345104/session what it is
+ \\ tail -f active/claude/345104/transcript
+ \\
+ \\Credentials are excluded by name and never listed, at any depth;
+ \\symlinks are never served. Writing a name into an agent's zmx file
+ \\moves it into that zmx session, and only when the daemon was
+ \\started with --allow-move; otherwise that write answers EPERM too.
+ \\
+;
+
+/// A fact's text, rendered on demand. `buf` should be a small stack buffer.
+fn factText(h: *Harness, fact: Top, buf: []u8) []const u8 {
+ if (fact == .readme) return readme_text; // static: outlives any buffer
+ var w = Io.Writer.fixed(buf);
+ switch (fact) {
+ .pid => w.print("{d}\n", .{h.pid}) catch {},
+ .uptime => {
+ const now = Io.Timestamp.now(h.io, .real).toSeconds();
+ const up: u64 = if (now > h.started_sec) @intCast(now - h.started_sec) else 0;
+ w.print("{d}\n", .{up}) catch {};
+ },
+ else => {},
+ }
+ return w.buffered();
+}
+
+var fact_buf: [64]u8 = undefined;
+
+fn attrFor(h: *Harness, t: Target) ?fs.Attr {
+ switch (t) {
+ .top => |top| {
+ switch (top) {
+ .root => return .{ .name = "/", .node = root, .dir = true, .mode = 0o555 },
+ .active => return .{ .name = "active", .node = topNode(.active), .dir = true, .mode = 0o555 },
+ .pid, .uptime, .readme => return .{
+ .name = top.fileName(),
+ .node = topNode(top),
+ .size = factText(h, top, &fact_buf).len,
+ .mode = 0o444,
+ },
+ .claude, .codex, .skills => return .{
+ .name = top.fileName(),
+ .node = topNode(top),
+ .dir = true,
+ .mode = 0o555,
+ },
+ .omp, .hermes, .dsh => {
+ // The harness dir IS its mirror: stat the target.
+ const m = top.mirror().?;
+ const got = statIn(h, m.root, m.rel) orelse return null;
+ if (got.st.kind != .directory) return null;
+ return .{
+ .name = top.fileName(),
+ .node = topNode(top),
+ .dir = true,
+ .mode = 0o555,
+ .path = if (got.qid_path != 0) got.qid_path else null,
+ };
+ },
+ }
+ },
+ .path => |e| return statAttr(h, e),
+ .act => |a| return activeAttr(h, a),
+ }
+}
+
+/// Attr of the mount target as a table entry (fresh from disk, or null
+/// when the target is missing, a symlink, or excluded by name).
+fn attrOfMount(h: *Harness, r: Root, rel_path: []const u8) ?fs.Attr {
+ const e = h.entryFor(r, rel_path, false) orelse return null;
+ defer if (e.refs == 0) {
+ e.used = false;
+ e.gen +%= 1;
+ };
+ return statAttr(h, e);
+}
+
+// ---- /active: the derived view -------------------------------------------------
+
+fn sourcesOf(h: *Harness) active.Sources {
+ var roots: [5][]const u8 = @splat("");
+ for (0..5) |i| {
+ if (h.base_set[i]) roots[i] = h.base_buf[i][0..h.base_len[i]];
+ }
+ return .{
+ .io = h.io,
+ .proc = h.proc_buf[0..h.proc_len],
+ .home = h.home_buf[0..h.home_len],
+ .roots = roots,
+ .self_pid = h.self_pid,
+ .btime = h.btime,
+ };
+}
+
+/// Rescans the process tree. Done on a listing of `/active` and on a
+/// lookup that misses, which is what makes an agent visible the moment
+/// it starts and gone the moment it exits.
+fn rescan(h: *Harness) void {
+ if (h.proc_len == 0) return;
+ h.live.scan(sourcesOf(h));
+}
+
+fn hasLive(h: *Harness, k: active.Kind) bool {
+ for (&h.live.slots) |*sl| {
+ if (sl.used and sl.live.kind == k) return true;
+ }
+ return false;
+}
+
+/// `<text>\n` staged where an answer can point at it.
+fn line(h: *Harness, text: []const u8) ?[]const u8 {
+ return std.fmt.bufPrint(&h.act_text, "{s}\n", .{text}) catch null;
+}
+
+/// The value of a field file, or null when this agent has none — an
+/// absent field is not listed and does not resolve, so the tree never
+/// answers a blank where it does not know.
+fn activeText(h: *Harness, a: Act) ?[]const u8 {
+ const l = h.live.at(a.slot, a.gen) orelse return null;
+ const src = sourcesOf(h);
+ var scratch: [active.text_capacity]u8 = undefined;
+ return switch (a.file) {
+ .pid => std.fmt.bufPrint(&h.act_text, "{d}\n", .{l.pid}) catch null,
+ .ppid => if (l.ppid == 0) null else std.fmt.bufPrint(&h.act_text, "{d}\n", .{l.ppid}) catch null,
+ .started => if (l.started == 0) null else std.fmt.bufPrint(&h.act_text, "{d}\n", .{l.started}) catch null,
+ .cwd => if (l.cwd.len == 0) null else line(h, l.cwd.slice()),
+ .session => if (l.session.len == 0) null else line(h, l.session.slice()),
+ .via => line(h, l.via.text()),
+ .name => line(h, active.registryField(src, l, "name", &scratch) orelse return null),
+ .status => line(h, active.registryField(src, l, "status", &scratch) orelse return null),
+ .title => line(h, active.headField(h.io, l.kind, l.transcript.slice(), .title, &scratch) orelse return null),
+ .model => line(h, active.headField(h.io, l.kind, l.transcript.slice(), .model, &scratch) orelse return null),
+ .agent_model => blk: {
+ const path = activePath(h, a) orelse break :blk null;
+ break :blk line(h, active.headField(h.io, l.kind, path, .model, &scratch) orelse return null);
+ },
+ // `zmx` is always there, so it can always be written to; empty
+ // means the agent runs outside zmx.
+ .zmx => if (active.zmxOf(src, l.pid, &scratch)) |v| line(h, v) else h.act_text[0..0],
+ else => null,
+ };
+}
+
+/// The absolute path behind a transcript-shaped file.
+fn activePath(h: *Harness, a: Act) ?[]const u8 {
+ const l = h.live.at(a.slot, a.gen) orelse return null;
+ switch (a.file) {
+ .transcript => return if (l.transcript.len == 0) null else l.transcript.slice(),
+ .agent_transcript, .agent_model => {
+ var ag: active.Agents = .{};
+ active.agentsOf(h.io, l, &ag);
+ return active.agentPath(l, &ag, a.agent, &h.act_path);
+ },
+ else => return null,
+ }
+}
+
+/// Opens an absolute path without following a symlink at the last
+/// component. These paths are derived from a pinned root or from the
+/// fd the harness itself holds, never from client bytes, but a name
+/// swapped underneath must still fail rather than redirect.
+fn openAbsNoFollow(path: []const u8) ?i32 {
+ if (path.len == 0 or path.len >= active.path_capacity) return null;
+ var z: [active.path_capacity]u8 = @splat(0);
+ @memcpy(z[0..path.len], path);
+ const rc = linux.open(@ptrCast(&z), .{
+ .ACCMODE = .RDONLY,
+ .NOFOLLOW = true,
+ .CLOEXEC = true,
+ .NONBLOCK = true,
+ }, 0);
+ if (linux.errno(rc) != .SUCCESS) return null;
+ return @intCast(rc);
+}
+
+fn activeAttr(h: *Harness, a: Act) ?fs.Attr {
+ switch (a.file) {
+ .harness_dir => {
+ if (!hasLive(h, a.kind)) return null;
+ return .{ .name = a.kind.text(), .node = activeNode(a), .dir = true, .mode = 0o555 };
+ },
+ .entry => {
+ const l = h.live.at(a.slot, a.gen) orelse return null;
+ const nm = std.fmt.bufPrint(&h.act_name, "{d}", .{l.pid}) catch return null;
+ return .{ .name = nm, .node = activeNode(a), .dir = true, .mode = 0o555 };
+ },
+ .agents => {
+ const l = h.live.at(a.slot, a.gen) orelse return null;
+ if (l.agent_dir.len == 0) return null;
+ return .{ .name = "agents", .node = activeNode(a), .dir = true, .mode = 0o555 };
+ },
+ .agent => {
+ const l = h.live.at(a.slot, a.gen) orelse return null;
+ var ag: active.Agents = .{};
+ active.agentsOf(h.io, l, &ag);
+ if (a.agent >= ag.count) return null;
+ const nm = ag.name(a.agent);
+ @memcpy(h.act_name[0..nm.len], nm);
+ return .{ .name = h.act_name[0..nm.len], .node = activeNode(a), .dir = true, .mode = 0o555 };
+ },
+ .transcript, .agent_transcript => {
+ const path = activePath(h, a) orelse return null;
+ const st = Io.Dir.statFile(.cwd(), h.io, path, .{ .follow_symlinks = false }) catch return null;
+ if (st.kind != .file) return null;
+ const mtime = st.mtime.toSeconds();
+ return .{
+ .name = a.file.fileName(),
+ .node = activeNode(a),
+ .size = st.size,
+ .mode = 0o444,
+ .mtime = @truncate(@as(u64, @bitCast(mtime))),
+ .version = qidVers(mtime, st.size),
+ };
+ },
+ else => {
+ const text = activeText(h, a) orelse return null;
+ // Only `zmx` is ever writable, and only when a move is allowed.
+ const mode: u16 = if (a.file == .zmx and h.allow_move) 0o644 else 0o444;
+ return .{
+ .name = a.file.fileName(),
+ .node = activeNode(a),
+ .size = text.len,
+ .mode = mode,
+ // Derived, not read: `status` flipping busy->idle keeps its
+ // size, so only the text can carry the change.
+ .version = textVers(text),
+ };
+ },
+ }
+}
+
+/// The slot holding `pid` for this harness, if any.
+fn slotOfPid(h: *Harness, k: active.Kind, pid: u32) ?Act {
+ for (&h.live.slots, 0..) |*sl, i| {
+ if (!sl.used or sl.live.kind != k or sl.live.pid != pid) continue;
+ return .{ .file = .entry, .kind = k, .slot = @intCast(i), .gen = sl.gen };
+ }
+ return null;
+}
+
+fn activeLookup(h: *Harness, req: fs.Req, a: Act, name: []const u8) Answer {
+ switch (a.file) {
+ .harness_dir => {
+ const pid = std.fmt.parseInt(u32, name, 10) catch return fail(req.tag, E.NOENT);
+ var found = slotOfPid(h, a.kind, pid);
+ if (found == null) {
+ rescan(h);
+ found = slotOfPid(h, a.kind, pid);
+ }
+ const act = found orelse return fail(req.tag, E.NOENT);
+ return activeReply(h, req, act, name);
+ },
+ .entry => {
+ if (std.mem.eql(u8, name, "agents")) {
+ return activeReply(h, req, .{ .file = .agents, .kind = a.kind, .slot = a.slot, .gen = a.gen }, name);
+ }
+ for (AFile.entry_files) |f| {
+ if (!std.mem.eql(u8, f.fileName(), name)) continue;
+ return activeReply(h, req, .{ .file = f, .kind = a.kind, .slot = a.slot, .gen = a.gen }, name);
+ }
+ return fail(req.tag, E.NOENT);
+ },
+ .agents => {
+ const l = h.live.at(a.slot, a.gen) orelse return fail(req.tag, E.NOENT);
+ var ag: active.Agents = .{};
+ active.agentsOf(h.io, l, &ag);
+ const i = ag.indexOf(name) orelse return fail(req.tag, E.NOENT);
+ return activeReply(h, req, .{
+ .file = .agent,
+ .kind = a.kind,
+ .slot = a.slot,
+ .gen = a.gen,
+ .agent = @intCast(i),
+ }, name);
+ },
+ .agent => {
+ for (AFile.agent_files) |f| {
+ if (!std.mem.eql(u8, f.fileName(), name)) continue;
+ return activeReply(h, req, .{
+ .file = f,
+ .kind = a.kind,
+ .slot = a.slot,
+ .gen = a.gen,
+ .agent = a.agent,
+ }, name);
+ }
+ return fail(req.tag, E.NOENT);
+ },
+ else => return fail(req.tag, E.NOTDIR),
+ }
+}
+
+fn activeReply(h: *Harness, req: fs.Req, a: Act, name: []const u8) Answer {
+ const attr = activeAttr(h, a) orelse return fail(req.tag, E.NOENT);
+ var with_name = attr;
+ with_name.name = name;
+ return .{ .reply = .{ .tag = req.tag, .attr = with_name } };
+}
+
+fn activeReaddir(h: *Harness, req: fs.Req, a: Act) Answer {
+ var st: Staging = .{ .buf = &h.stage_buf, .skip = req.off };
+ switch (a.file) {
+ .harness_dir => {
+ for (&h.live.slots, 0..) |*sl, i| {
+ if (!sl.used or sl.live.kind != a.kind) continue;
+ var num: [24]u8 = undefined;
+ const nm = std.fmt.bufPrint(&num, "{d}", .{sl.live.pid}) catch continue;
+ st.add(activeNode(.{
+ .file = .entry,
+ .kind = a.kind,
+ .slot = @intCast(i),
+ .gen = sl.gen,
+ }), true, nm);
+ }
+ },
+ .entry => {
+ const l = h.live.at(a.slot, a.gen) orelse return fail(req.tag, E.NOENT);
+ for (AFile.entry_files) |f| {
+ const child: Act = .{ .file = f, .kind = a.kind, .slot = a.slot, .gen = a.gen };
+ if (activeAttr(h, child) == null) continue; // no value: not listed
+ st.add(activeNode(child), false, f.fileName());
+ }
+ if (l.agent_dir.len > 0) {
+ st.add(activeNode(.{ .file = .agents, .kind = a.kind, .slot = a.slot, .gen = a.gen }), true, "agents");
+ }
+ },
+ .agents => {
+ const l = h.live.at(a.slot, a.gen) orelse return fail(req.tag, E.NOENT);
+ var ag: active.Agents = .{};
+ active.agentsOf(h.io, l, &ag);
+ for (0..ag.count) |i| {
+ st.add(activeNode(.{
+ .file = .agent,
+ .kind = a.kind,
+ .slot = a.slot,
+ .gen = a.gen,
+ .agent = @intCast(i),
+ }), true, ag.name(i));
+ }
+ },
+ .agent => {
+ for (AFile.agent_files) |f| {
+ const child: Act = .{ .file = f, .kind = a.kind, .slot = a.slot, .gen = a.gen, .agent = a.agent };
+ if (activeAttr(h, child) == null) continue;
+ st.add(activeNode(child), false, f.fileName());
+ }
+ },
+ else => return fail(req.tag, E.NOTDIR),
+ }
+ return .{ .reply = .{ .tag = req.tag }, .bytes = h.stage_buf[0..st.len] };
+}
+
+fn activeRead(h: *Harness, req: fs.Req, a: Act) Answer {
+ switch (a.file) {
+ .harness_dir, .entry, .agents, .agent => return fail(req.tag, E.ISDIR),
+ .transcript, .agent_transcript => {
+ const path = activePath(h, a) orelse return fail(req.tag, E.NOENT);
+ const fd = openAbsNoFollow(path) orelse return fail(req.tag, E.NOENT);
+ const file: Io.File = .{ .handle = fd, .flags = .{ .nonblocking = false } };
+ defer file.close(h.io);
+ const st = file.stat(h.io) catch return fail(req.tag, E.IO);
+ if (st.kind == .directory) return fail(req.tag, E.ISDIR);
+ if (st.kind != .file) return fail(req.tag, E.PERM);
+ _ = linux.fcntl(fd, linux.F.SETFL, 0);
+ const want = @min(req.size, h.data_buf.len);
+ const n = file.readPositionalAll(h.io, h.data_buf[0..want], req.off) catch
+ return fail(req.tag, E.IO);
+ return .{ .reply = .{ .tag = req.tag }, .bytes = h.data_buf[0..n] };
+ },
+ else => {
+ const text = activeText(h, a) orelse return fail(req.tag, E.NOENT);
+ return window(req, text);
+ },
+ }
+}
+
+// ---- the move: writing a zmx session name into an agent's `zmx` ----------------
+
+/// How long a move waits, in 100ms ticks: for the agent to take the
+/// hangup, then to die outright, then for the new zmx session to post.
+const term_ticks: usize = 50;
+const kill_ticks: usize = 30;
+const post_ticks: usize = 100;
+
+fn napOneTick(h: *Harness) void {
+ h.io.sleep(.fromMilliseconds(100), .awake) catch {};
+}
+
+/// Is this name already a live zmx session? zmx posts each session into
+/// the registry, so the registry is the answer — no process scanning.
+fn zmxPosted(h: *Harness, name: []const u8) bool {
+ if (h.runtime_len == 0) return false;
+ var buf: [active.path_capacity]u8 = undefined;
+ const path = std.fmt.bufPrint(&buf, "{s}/9p/zmx/{s}", .{ h.runtime_buf[0..h.runtime_len], name }) catch return true;
+ return Io.Dir.statFile(.cwd(), h.io, path, .{ .follow_symlinks = true }) != error.FileNotFound;
+}
+
+fn procGone(h: *Harness, pid: u32) bool {
+ var buf: [active.path_capacity]u8 = undefined;
+ const path = std.fmt.bufPrint(&buf, "{s}/{d}", .{ h.proc_buf[0..h.proc_len], pid }) catch return false;
+ _ = Io.Dir.statFile(.cwd(), h.io, path, .{ .follow_symlinks = true }) catch return true;
+ return false;
+}
+
+/// SIGTERM, then SIGKILL, then give up. The agent's session was
+/// resolved before this ran, so whatever happens it has somewhere to
+/// come back to.
+fn killAndWait(h: *Harness, pid: u32) bool {
+ _ = linux.kill(@intCast(pid), .TERM);
+ for (0..term_ticks) |_| {
+ if (procGone(h, pid)) return true;
+ napOneTick(h);
+ }
+ _ = linux.kill(@intCast(pid), .KILL);
+ for (0..kill_ticks) |_| {
+ if (procGone(h, pid)) return true;
+ napOneTick(h);
+ }
+ return false;
+}
+
+/// `zmx run <name> -d <harness> <resume flag> <resume value>`, in the
+/// agent's own directory. Every word but `<name>` is fixed by the
+/// harness; `<name>` was checked against zmx's label charset before
+/// anything was killed. No byte a client wrote reaches `exec` as a
+/// command.
+fn spawnZmx(h: *Harness, l: *const active.Live, name: []const u8, resume_value: []const u8) bool {
+ if (h.zmx_len == 0) return false;
+ const harness_argv0: []const u8 = @tagName(l.kind);
+ const resume_flag: []const u8 = switch (l.kind) {
+ .codex => "resume",
+ else => "--resume",
+ };
+
+ // One buffer holds every NUL-terminated word; `argv` points into it.
+ var words: [8 * active.path_capacity]u8 = undefined;
+ var used: usize = 0;
+ var argv: [9]?[*:0]const u8 = @splat(null);
+ var n: usize = 0;
+ const parts = [_][]const u8{
+ h.zmx_buf[0..h.zmx_len], "run", name, "-d",
+ harness_argv0, resume_flag, resume_value,
+ };
+ for (parts) |w| {
+ if (used + w.len + 1 > words.len or n + 1 >= argv.len) return false;
+ @memcpy(words[used..][0..w.len], w);
+ words[used + w.len] = 0;
+ argv[n] = @ptrCast(&words[used]);
+ n += 1;
+ used += w.len + 1;
+ }
+
+ var cwd_z: [active.cwd_capacity + 1]u8 = @splat(0);
+ if (l.cwd.len >= cwd_z.len) return false;
+ @memcpy(cwd_z[0..l.cwd.len], l.cwd.slice());
+
+ const rc = linux.fork();
+ if (linux.errno(rc) != .SUCCESS) return false;
+ if (rc == 0) {
+ // The child: only async-signal-safe calls from here.
+ _ = linux.chdir(@ptrCast(&cwd_z));
+ const empty = [_:null]?[*:0]const u8{};
+ const envp: cloud9.post.Env = h.envp orelse &empty;
+ _ = linux.execve(argv[0].?, @ptrCast(&argv), envp);
+ linux.exit(127);
+ }
+ // Reap the forked `zmx`, which returns as soon as the session is up.
+ const child: i32 = @intCast(rc);
+ var status: u32 = 0;
+ for (0..post_ticks) |_| {
+ const w = linux.waitpid(child, &status, 1); // WNOHANG
+ if (w == @as(usize, @intCast(child))) break;
+ napOneTick(h);
+ }
+ return true;
+}
+
+/// A move: kill the agent and bring it back inside a zmx session of the
+/// name written. Refusals come before anything is destroyed.
+fn moveToZmx(h: *Harness, req: fs.Req, a: Act) Answer {
+ if (!h.allow_move) return failWhy(req.tag, E.PERM, "moves are not permitted: start 9agents with --allow-move");
+ const name = std.mem.trim(u8, req.data, " \t\r\n");
+ if (!active.legalZmxName(name)) return failWhy(req.tag, E.INVAL, "invalid zmx name: letters, digits, dot, dash and underscore only");
+
+ const l = h.live.at(a.slot, a.gen) orelse return fail(req.tag, E.NOENT);
+ // Nothing is killed that has nowhere to come back to.
+ if (l.via == .none or l.session.len == 0) return failWhy(req.tag, E.PERM, "no session resolved: not permitted, there would be nothing to resume");
+ if (l.cwd.len == 0) return failWhy(req.tag, E.PERM, "no working directory known for this agent: not permitted");
+ const resume_value: []const u8 = switch (l.kind) {
+ // omp resumes by the transcript it wrote, the others by id.
+ .omp => l.transcript.slice(),
+ .claude, .codex, .hermes => l.session.slice(),
+ // dsh has no resume form worth guessing at.
+ .dsh => return failWhy(req.tag, E.PERM, "dsh has no resume form: moving it is not permitted"),
+ };
+ if (resume_value.len == 0) return failWhy(req.tag, E.PERM, "nothing to resume from: not permitted");
+
+ // Already there: setting a value it already has changes nothing.
+ var have: [active.text_capacity]u8 = undefined;
+ if (active.zmxOf(sourcesOf(h), l.pid, &have)) |current| {
+ if (std.mem.eql(u8, current, name)) {
+ return .{ .reply = .{ .tag = req.tag, .written = @intCast(req.data.len) } };
+ }
+ }
+ if (zmxPosted(h, name)) return failWhy(req.tag, E.EXIST, "a live zmx session of that name already exists");
+ // The slot could have gone stale between the scan and this write.
+ if (!active.stillAlive(sourcesOf(h), l)) return failWhy(req.tag, E.NOENT, "the agent exited before the move began: no such process");
+
+ // Everything below this line destroys something.
+ var snapshot = l.*;
+ if (!killAndWait(h, snapshot.pid)) return failWhy(req.tag, E.IO, "the agent did not exit when signalled; nothing was started");
+ if (!spawnZmx(h, &snapshot, name, resume_value)) return failWhy(req.tag, E.IO, "could not start zmx; the agent has already been stopped");
+ for (0..post_ticks) |_| {
+ if (zmxPosted(h, name)) {
+ rescan(h);
+ return .{ .reply = .{ .tag = req.tag, .written = @intCast(req.data.len) } };
+ }
+ napOneTick(h);
+ }
+ rescan(h);
+ return failWhy(req.tag, E.IO, "zmx did not post the session in time; the agent may still return");
+}
+
+// ---- dispatch ------------------------------------------------------------------
+
+/// Answers one engine request. The caller holds `h.mutex` and replies
+/// before letting go of it (answer bytes point into `h`).
+pub fn handle(h: *Harness, req: fs.Req) Answer {
+ const t = h.resolve(req.node) orelse return fail(req.tag, E.NOENT);
+ return switch (req.op) {
+ .lookup => lookup(h, req, t),
+ .getattr => attrReply(h, req.tag, t),
+ .setattr => setattrReq(h, req, t),
+ .open => open(h, req, t),
+ .release => release(h, req, t),
+ .readdir => readdir(h, req, t),
+ .read => read(h, req, t),
+ .write => writeReq(h, req, t),
+ };
+}
+
+/// Truncation is how a shell's `>` opens a file before writing it. The
+/// `zmx` file has no length of its own — a write replaces the value —
+/// so on the one writable file a zero-length truncate is a no-op
+/// rather than a refusal. Everything else still answers EPERM.
+fn setattrReq(h: *Harness, req: fs.Req, t: Target) Answer {
+ switch (t) {
+ .act => |a| if (a.file == .zmx and h.allow_move and req.truncate) {
+ return .{ .reply = .{ .tag = req.tag } };
+ },
+ else => {},
+ }
+ return failWhy(req.tag, E.PERM, "9agents serves a view, not a store: writing is not permitted");
+}
+
+/// The tree answers EPERM to every write but one: a zmx session name
+/// into a live agent's `zmx`, which moves it there.
+fn writeReq(h: *Harness, req: fs.Req, t: Target) Answer {
+ switch (t) {
+ .act => |a| if (a.file == .zmx) return moveToZmx(h, req, a),
+ else => {},
+ }
+ return failWhy(req.tag, E.PERM, "9agents serves a view, not a store: writing is not permitted");
+}
+
+fn attrReply(h: *Harness, tag: u64, t: Target) Answer {
+ const a = attrFor(h, t) orelse return fail(tag, E.NOENT);
+ return .{ .reply = .{ .tag = tag, .attr = a } };
+}
+
+/// A lookup's attr names the file as the client asked for it (the engine
+/// keeps that name for the fid); refs for path targets were already taken
+/// by `entryFor`.
+fn lookupAttr(h: *Harness, req: fs.Req, t: Target, name: []const u8) Answer {
+ const a = attrFor(h, t) orelse return fail(req.tag, E.NOENT);
+ var with_name = a;
+ with_name.name = name;
+ return .{ .reply = .{ .tag = req.tag, .attr = with_name } };
+}
+
+fn lookup(h: *Harness, req: fs.Req, t: Target) Answer {
+ const name = req.data;
+ if (name.len == 0 or name.len > 255) return fail(req.tag, E.NOENT);
+ if (std.mem.indexOfAny(u8, name, "/\x00") != null) return fail(req.tag, E.NOENT);
+
+ if (std.mem.eql(u8, name, ".")) {
+ // The engine asks for "." when cloning a fid; the reference is
+ // paid for path targets here like any other lookup result.
+ switch (t) {
+ .top, .act => return attrReply(h, req.tag, t),
+ .path => |e| {
+ e.refs += 1;
+ return lookupAttr(h, req, t, name);
+ },
+ }
+ }
+ if (std.mem.eql(u8, name, "..")) return lookupParent(h, req, t);
+
+ switch (t) {
+ .top => |top| switch (top) {
+ .root => {
+ const found: ?Top = blk: for (Top.listed) |e| {
+ if (std.mem.eql(u8, e.fileName(), name)) break :blk e;
+ } else break :blk null;
+ return attrReply(h, req.tag, .{ .top = found orelse return fail(req.tag, E.NOENT) });
+ },
+ .claude, .codex, .skills => {
+ const m = mountNamed(top, name) orelse return fail(req.tag, E.NOENT);
+ const e = h.entryFor(m.root, m.rel, true) orelse return fail(req.tag, E.NFILE);
+ const a = statAttr(h, e) orelse {
+ unref(e);
+ return fail(req.tag, E.NOENT);
+ };
+ var with_name = a;
+ with_name.name = name;
+ return .{ .reply = .{ .tag = req.tag, .attr = with_name } };
+ },
+ .omp, .hermes, .dsh => {
+ const m = top.mirror().?;
+ return lookupBelow(h, req, m.root, m.rel, name);
+ },
+ .active => {
+ const k = blk: for (std.enums.values(active.Kind)) |k| {
+ if (std.mem.eql(u8, k.text(), name)) break :blk k;
+ } else return fail(req.tag, E.NOENT);
+ if (!hasLive(h, k)) {
+ rescan(h);
+ if (!hasLive(h, k)) return fail(req.tag, E.NOENT);
+ }
+ return activeReply(h, req, .{ .file = .harness_dir, .kind = k }, name);
+ },
+ .pid, .uptime, .readme => return fail(req.tag, E.NOTDIR),
+ },
+ .path => |e| {
+ const rel_path = e.rel();
+ if (attrFor(h, t)) |a| {
+ if (!a.dir) return fail(req.tag, E.NOTDIR);
+ } else return fail(req.tag, E.NOENT);
+ return lookupBelow(h, req, e.root, rel_path, name);
+ },
+ .act => |a| return activeLookup(h, req, a, name),
+ }
+}
+
+/// A lookup of `name` in the directory (root, dir_rel): the child's rel
+/// path is composed only from the remembered pair and the (single-segment,
+/// engine-vetted) name.
+fn lookupBelow(h: *Harness, req: fs.Req, r: Root, dir_rel: []const u8, name: []const u8) Answer {
+ if (excluded(name)) return fail(req.tag, E.NOENT);
+ var scratch: [rel_capacity + 256]u8 = undefined;
+ const child_rel = joinRel(&scratch, dir_rel, name) orelse return fail(req.tag, E.NOENT);
+ const e = h.entryFor(r, child_rel, true) orelse return fail(req.tag, E.NFILE);
+ const a = statAttr(h, e) orelse {
+ unref(e);
+ return fail(req.tag, E.NOENT);
+ };
+ var with_name = a;
+ with_name.name = name;
+ return .{ .reply = .{ .tag = req.tag, .attr = with_name } };
+}
+
+fn joinRel(scratch: []u8, dir_rel: []const u8, name: []const u8) ?[]const u8 {
+ if (dir_rel.len == 0) {
+ if (name.len > scratch.len) return null;
+ @memcpy(scratch[0..name.len], name);
+ return scratch[0..name.len];
+ }
+ const total = dir_rel.len + 1 + name.len;
+ if (total > scratch.len) return null;
+ @memcpy(scratch[0..dir_rel.len], dir_rel);
+ scratch[dir_rel.len] = '/';
+ @memcpy(scratch[dir_rel.len + 1 .. total], name);
+ return scratch[0..total];
+}
+
+fn lookupParent(h: *Harness, req: fs.Req, t: Target) Answer {
+ const parent: Target = switch (t) {
+ .top => |top| .{ .top = top.parent() },
+ .path => |e| blk: {
+ const rel_path = e.rel();
+ if (std.mem.lastIndexOfScalar(u8, rel_path, '/')) |i| {
+ const up = rel_path[0..i];
+ const pe = h.entryFor(e.root, up, true) orelse return fail(req.tag, E.NFILE);
+ break :blk .{ .path = pe };
+ }
+ break :blk .{ .top = mountOwner(e.root, rel_path) };
+ },
+ .act => |a| switch (a.file) {
+ .harness_dir => .{ .top = .active },
+ .entry => .{ .act = .{ .file = .harness_dir, .kind = a.kind } },
+ .agents => .{ .act = .{ .file = .entry, .kind = a.kind, .slot = a.slot, .gen = a.gen } },
+ .agent => .{ .act = .{ .file = .agents, .kind = a.kind, .slot = a.slot, .gen = a.gen } },
+ .agent_model, .agent_transcript => .{ .act = .{
+ .file = .agent,
+ .kind = a.kind,
+ .slot = a.slot,
+ .gen = a.gen,
+ .agent = a.agent,
+ } },
+ // Every other file hangs directly off its entry.
+ else => .{ .act = .{ .file = .entry, .kind = a.kind, .slot = a.slot, .gen = a.gen } },
+ },
+ };
+ return attrReply(h, req.tag, parent);
+}
+
+fn unref(e: *Entry) void {
+ if (e.refs > 0) e.refs -= 1;
+ if (e.refs == 0) {
+ e.used = false;
+ e.gen +%= 1;
+ }
+}
+
+fn open(h: *Harness, req: fs.Req, t: Target) Answer {
+ _ = attrFor(h, t) orelse return fail(req.tag, E.NOENT); // still there?
+ // Read-only tree, with exactly one exception: the `zmx` file of a
+ // live agent, and only when the daemon was started to allow moves.
+ // Truncation is meaningless there (a write replaces the value), so
+ // it is accepted rather than refused, which is what `>` needs.
+ const writable = switch (t) {
+ .act => |a| a.file == .zmx and h.allow_move,
+ else => false,
+ };
+ const rw = req.omode & 3;
+ if (!writable) {
+ if (rw == cloud9.owrite or rw == cloud9.ordwr) return failWhy(req.tag, E.PERM, "9agents serves a view, not a store: writing is not permitted");
+ if (req.omode & cloud9.otrunc != 0) return failWhy(req.tag, E.PERM, "truncation is not permitted here");
+ }
+ return .{ .reply = .{ .tag = req.tag, .handle = 1 } };
+}
+
+fn release(h: *Harness, req: fs.Req, t: Target) Answer {
+ _ = h;
+ switch (t) {
+ .top, .act => {},
+ .path => |e| unref(e),
+ }
+ return .{ .reply = .{ .tag = req.tag } };
+}
+
+// ---- reads ----------------------------------------------------------------------
+
+fn read(h: *Harness, req: fs.Req, t: Target) Answer {
+ switch (t) {
+ .top => |top| switch (top) {
+ .pid, .uptime, .readme => {
+ const text = factText(h, top, &fact_buf);
+ return window(req, text);
+ },
+ else => return fail(req.tag, E.ISDIR),
+ },
+ .act => |a| return activeRead(h, req, a),
+ .path => |e| {
+ // `O_NONBLOCK` so a fifo left in a harness root cannot park the
+ // daemon in `open`; the kind check below refuses it anyway.
+ const fd = openIn(h, e.root, e.rel(), .{
+ .ACCMODE = .RDONLY,
+ .NONBLOCK = true,
+ .CLOEXEC = true,
+ }) orelse return fail(req.tag, E.NOENT);
+ const file: Io.File = .{ .handle = fd, .flags = .{ .nonblocking = false } };
+ defer file.close(h.io);
+ const st = file.stat(h.io) catch return fail(req.tag, E.IO);
+ if (st.kind == .directory) return fail(req.tag, E.ISDIR);
+ // Only regular files have bytes this tree promises to serve.
+ if (st.kind != .file) return fail(req.tag, E.PERM);
+ _ = linux.fcntl(fd, linux.F.SETFL, 0); // pread wants no O_NONBLOCK
+ const want = @min(req.size, h.data_buf.len);
+ const n = file.readPositionalAll(h.io, h.data_buf[0..want], req.off) catch
+ return fail(req.tag, E.IO);
+ return .{ .reply = .{ .tag = req.tag }, .bytes = h.data_buf[0..n] };
+ },
+ }
+}
+
+/// `text` windowed by the request's offset and size.
+fn window(req: fs.Req, text: []const u8) Answer {
+ const off: usize = @intCast(@min(req.off, text.len));
+ const n = @min(text.len - off, req.size);
+ return .{ .reply = .{ .tag = req.tag }, .bytes = text[off..][0..n] };
+}
+
+// ---- readdir --------------------------------------------------------------------
+
+/// Directory records in the engine's shape: `node:u64le dir:u8 len:u8 name`.
+const Staging = struct {
+ buf: []u8,
+ len: usize = 0,
+ skip: u64,
+ /// Set once a record did not fit. Everything after it is left for the
+ /// next read: dropping one record and staging a shorter one behind it
+ /// would lose that entry, because the client's next offset counts the
+ /// records it received.
+ full: bool = false,
+
+ fn add(s: *Staging, node: u64, dir: bool, name: []const u8) void {
+ if (s.full) return;
+ if (s.skip > 0) {
+ s.skip -= 1;
+ return;
+ }
+ if (name.len == 0 or name.len > 255) return;
+ if (s.len + 10 + name.len > s.buf.len) {
+ s.full = true;
+ return;
+ }
+ std.mem.writeInt(u64, s.buf[s.len..][0..8], node, .little);
+ s.buf[s.len + 8] = @intFromBool(dir);
+ s.buf[s.len + 9] = @intCast(name.len);
+ @memcpy(s.buf[s.len + 10 ..][0..name.len], name);
+ s.len += 10 + name.len;
+ }
+};
+
+fn readdir(h: *Harness, req: fs.Req, t: Target) Answer {
+ var st: Staging = .{ .buf = &h.stage_buf, .skip = req.off };
+ switch (t) {
+ .top => |top| switch (top) {
+ .root => for (Top.listed) |e| st.add(topNode(e), e.dir(), e.fileName()),
+ .claude, .codex, .skills => {
+ for (named_mounts) |m| {
+ if (m.owner != top) continue;
+ // Only mounts whose target exists are listed (a harness
+ // without skills simply has no skills/ entry).
+ const a = attrOfMount(h, m.mount.root, m.mount.rel) orelse continue;
+ const e = h.entryFor(m.mount.root, m.mount.rel, false) orelse continue;
+ st.add(h.entryNode(e), a.dir, m.name);
+ }
+ },
+ .pid, .uptime, .readme => return fail(req.tag, E.NOTDIR),
+ .active => {
+ rescan(h);
+ for (std.enums.values(active.Kind)) |k| {
+ if (!hasLive(h, k)) continue;
+ st.add(activeNode(.{ .file = .harness_dir, .kind = k }), true, k.text());
+ }
+ },
+ .omp, .hermes, .dsh => {
+ const m = top.mirror().?;
+ if (h.base(m.root) == null) return fail(req.tag, E.NOENT);
+ switch (listDir(h, &st, m.root, m.rel)) {
+ .ok => {},
+ .gone => return fail(req.tag, E.NOENT),
+ .failed => return fail(req.tag, E.IO),
+ .overflow => return failWhy(req.tag, E.NFILE, "directory has more entries than this server can list at once"),
+ }
+ },
+ },
+ .path => |e| {
+ if (attrFor(h, t)) |a| {
+ if (!a.dir) return fail(req.tag, E.NOTDIR);
+ } else return fail(req.tag, E.NOENT);
+ switch (listDir(h, &st, e.root, e.rel())) {
+ .ok => {},
+ .gone => return fail(req.tag, E.NOENT),
+ .failed => return fail(req.tag, E.IO),
+ .overflow => return failWhy(req.tag, E.NFILE, "directory has more entries than this server can list at once"),
+ }
+ },
+ .act => |a| return activeReaddir(h, req, a),
+ }
+ return .{ .reply = .{ .tag = req.tag }, .bytes = h.stage_buf[0..st.len] };
+}
+
+/// How a listing attempt ended. `overflow` is deliberate: a comptime cap
+/// that would silently drop entries answers an error instead, because a
+/// short listing is indistinguishable from a small directory.
+const Listing = enum { ok, gone, failed, overflow };
+
+/// Stages the contents of the directory (root, dir_rel): every name the
+/// walker yields that survives the exclusions, sorted so a listing that
+/// spans several reads stays consistent.
+fn listDir(h: *Harness, st: *Staging, r: Root, dir_rel: []const u8) Listing {
+ const fd = openIn(h, r, dir_rel, .{
+ .ACCMODE = .RDONLY,
+ .DIRECTORY = true,
+ .CLOEXEC = true,
+ }) orelse return .gone;
+ const dir: Io.Dir = .{ .handle = fd };
+ defer Io.Dir.close(dir, h.io);
+ var read_buf: [Io.Dir.Iterator.reader_buffer_len]u8 align(@alignOf(usize)) = undefined;
+ var reader = Io.Dir.Reader.init(dir, &read_buf);
+ var names_len: usize = 0;
+ var count: usize = 0;
+ while (true) {
+ const entry = (reader.next(h.io) catch return .failed) orelse break;
+ if (excluded(entry.name)) continue;
+ var kind = entry.kind;
+ if (kind == .unknown or kind == .sym_link) {
+ // The walker's word is not proof: stat without following.
+ var child_buf: [rel_capacity + 256]u8 = undefined;
+ const child_rel = joinRel(&child_buf, dir_rel, entry.name) orelse continue;
+ const cst = statIn(h, r, child_rel) orelse continue;
+ kind = cst.st.kind;
+ }
+ if (kind == .sym_link) continue; // never served
+ // A cap reached is an error, never a short listing: a directory
+ // that quietly loses entries is a wrong answer, and a caller
+ // cannot tell it from a small directory.
+ if (count == list_capacity or names_len + entry.name.len > h.list_names.len) return .overflow;
+ @memcpy(h.list_names[names_len..][0..entry.name.len], entry.name);
+ h.list_offs[count] = @intCast(names_len);
+ h.list_dirs[count] = kind == .directory;
+ names_len += entry.name.len;
+ count += 1;
+ }
+ // Sort the (offset, length) pairs by name for cursor stability.
+ const SortCtx = struct {
+ names: []const u8,
+ offs: []const u32,
+ lens: [list_capacity]u32,
+
+ fn lessThan(ctx: @This(), a: usize, b: usize) bool {
+ return std.mem.order(u8, ctx.nameAt(a), ctx.nameAt(b)) == .lt;
+ }
+ fn nameAt(ctx: @This(), i: usize) []const u8 {
+ const start = ctx.offs[i];
+ return ctx.names[start..][0..ctx.lens[i]];
+ }
+ };
+ var lens: [list_capacity]u32 = @splat(0);
+ var order: [list_capacity]usize = @splat(0);
+ {
+ var end: usize = 0;
+ for (0..count) |i| {
+ end = if (i + 1 < count) h.list_offs[i + 1] else names_len;
+ lens[i] = @intCast(end - h.list_offs[i]);
+ order[i] = i;
+ }
+ }
+ const ctx: SortCtx = .{ .names = h.list_names[0..names_len], .offs = &h.list_offs, .lens = lens };
+ std.mem.sort(usize, order[0..count], ctx, SortCtx.lessThan);
+ for (order[0..count]) |i| {
+ const name = ctx.nameAt(i);
+ var child_buf: [rel_capacity + 256]u8 = undefined;
+ const child_rel = joinRel(&child_buf, dir_rel, name) orelse continue;
+ const e = h.entryFor(r, child_rel, false) orelse return .overflow; // path table full
+ st.add(h.entryNode(e), h.list_dirs[i], name);
+ }
+ return .ok;
+}
+
+// ---- unit tests -------------------------------------------------------------------
+
+const testing = std.testing;
+
+/// A rig with a fake HOME: every root under one temp dir, never the real
+/// ~/.claude or any other live harness root.
+const Rig = struct {
+ dir: testing.TmpDir,
+ /// Build a fixture process tree under <home>/proc and point
+ /// `/active` at it. Off by default: a unit test must opt in to
+ /// reading a process tree, and it is never the live one.
+ with_proc: bool = false,
+ /// The pid the daemon believes it is, for the ancestry exclusion.
+ self_pid: u32 = 4242,
+ zmx: []const u8 = "zmx",
+ allow_move: bool = false,
+ path_buf: [std.fs.max_path_bytes]u8 = undefined,
+ home: []const u8 = undefined,
+ h: *Harness = undefined,
+ harness_mem: Harness = undefined,
+
+ fn start(rig: *Rig) !void {
+ const io = testing.io;
+ rig.dir = testing.tmpDir(.{});
+ errdefer rig.dir.cleanup();
+ const len = try rig.dir.dir.realPath(io, &rig.path_buf);
+ rig.home = try testing.allocator.dupe(u8, rig.path_buf[0..len]);
+ var mk: [std.fs.max_path_bytes]u8 = undefined;
+ // The five roots, pinned to the fake home.
+ inline for ([_][]const u8{
+ ".claude/projects/p1", ".claude/skills/revu",
+ ".codex/sessions/2026/09/21",
+ ".omp/agent", ".hermes/logs",
+ ".dsh/profiles",
+ }) |sub| {
+ try Io.Dir.cwd().createDirPath(io, try std.fmt.bufPrint(&mk, "{s}/{s}", .{ rig.home, sub }));
+ }
+ var base_buf: [5][std.fs.max_path_bytes]u8 = @splat(@splat(0));
+ var bases: [5][]const u8 = @splat("");
+ inline for (0..5) |i| {
+ bases[i] = try std.fmt.bufPrint(&base_buf[i], "{s}/{s}", .{ rig.home, home_dirs[i] });
+ }
+ rig.harness_mem = undefined;
+ var proc_buf: [std.fs.max_path_bytes]u8 = undefined;
+ const proc_root: []const u8 = if (rig.with_proc)
+ try std.fmt.bufPrint(&proc_buf, "{s}/proc", .{rig.home})
+ else
+ "";
+ if (rig.with_proc) {
+ try Io.Dir.cwd().createDirPath(io, proc_root);
+ try rig.put("proc/stat", "cpu 1 2 3\nbtime 1000000\nprocesses 7\n");
+ }
+ rig.harness_mem.init(.{
+ .io = io,
+ .pid = rig.self_pid,
+ .bases = bases,
+ .proc = proc_root,
+ .home = rig.home,
+ .zmx = rig.zmx,
+ .allow_move = rig.allow_move,
+ });
+ rig.h = &rig.harness_mem;
+ }
+
+ fn end(rig: *Rig) void {
+ rig.dir.cleanup();
+ testing.allocator.free(rig.home);
+ }
+
+ fn put(rig: *Rig, rel_path: []const u8, bytes: []const u8) !void {
+ var buf: [std.fs.max_path_bytes]u8 = undefined;
+ const path = try std.fmt.bufPrint(&buf, "{s}/{s}", .{ rig.home, rel_path });
+ if (std.mem.lastIndexOfScalar(u8, rel_path, '/')) |i| {
+ var dbuf: [std.fs.max_path_bytes]u8 = undefined;
+ const dir_path = try std.fmt.bufPrint(&dbuf, "{s}/{s}", .{ rig.home, rel_path[0..i] });
+ try Io.Dir.cwd().createDirPath(testing.io, dir_path);
+ }
+ var file = try Io.Dir.createFileAbsolute(testing.io, path, .{});
+ defer file.close(testing.io);
+ try file.writeStreamingAll(testing.io, bytes);
+ }
+
+
+ fn del(rig: *Rig, rel_path: []const u8) !void {
+ var buf: [std.fs.max_path_bytes]u8 = undefined;
+ const path = try std.fmt.bufPrint(&buf, "{s}/{s}", .{ rig.home, rel_path });
+ try Io.Dir.deleteFileAbsolute(testing.io, path);
+ }
+
+ /// lookup of `name` in `dir_node`, answering the child's attr.
+ fn lookupName(rig: *Rig, tag: u64, dir_node: u64, name: []const u8) Answer {
+ return handle(rig.h, .{ .tag = tag, .op = .lookup, .node = dir_node, .data = name });
+ }
+
+ /// Writes one fake process into the fixture `/proc`: the `stat`
+ /// line (with the start time in field 22, after a name that holds
+ /// the spaces and parentheses a real one can), the NUL-separated
+ /// `cmdline`, and a `cwd` symlink.
+ fn fakeProc(rig: *Rig, o: struct {
+ pid: u32,
+ ppid: u32 = 1,
+ comm: []const u8 = "x",
+ starttime: u64 = 5000,
+ argv: []const u8,
+ cwd: []const u8 = "",
+ }) !void {
+ var rel: [128]u8 = undefined;
+ var stat_text: [512]u8 = undefined;
+ var w = Io.Writer.fixed(&stat_text);
+ try w.print("{d} ({s}) S {d}", .{ o.pid, o.comm, o.ppid });
+ for (5..22) |i| try w.print(" {d}", .{i});
+ try w.print(" {d} 0 0\n", .{o.starttime});
+ try rig.put(try std.fmt.bufPrint(&rel, "proc/{d}/stat", .{o.pid}), w.buffered());
+ try rig.put(try std.fmt.bufPrint(&rel, "proc/{d}/cmdline", .{o.pid}), o.argv);
+
+ const target = if (o.cwd.len > 0) o.cwd else rig.home;
+ var link_buf: [std.fs.max_path_bytes]u8 = undefined;
+ const link = try std.fmt.bufPrintZ(&link_buf, "{s}/proc/{d}/cwd", .{ rig.home, o.pid });
+ Io.Dir.cwd().symLink(testing.io, target, link, .{}) catch {};
+ }
+
+ /// Removes a fake process, as an exit would.
+ fn reapProc(rig: *Rig, pid: u32) !void {
+ var buf: [std.fs.max_path_bytes]u8 = undefined;
+ const dir_path = try std.fmt.bufPrint(&buf, "{s}/proc/{d}", .{ rig.home, pid });
+ const d = try Io.Dir.openDirAbsolute(testing.io, dir_path, .{ .iterate = true });
+ var rb: [Io.Dir.Iterator.reader_buffer_len]u8 align(@alignOf(usize)) = undefined;
+ var it = Io.Dir.Reader.init(d, &rb);
+ var names: [8][64]u8 = undefined;
+ var lens: [8]usize = @splat(0);
+ var n: usize = 0;
+ while (n < names.len) {
+ const e = (it.next(testing.io) catch break) orelse break;
+ @memcpy(names[n][0..e.name.len], e.name);
+ lens[n] = e.name.len;
+ n += 1;
+ }
+ Io.Dir.close(d, testing.io);
+ for (0..n) |i| {
+ var one: [std.fs.max_path_bytes]u8 = undefined;
+ const path = try std.fmt.bufPrint(&one, "{s}/{s}", .{ dir_path, names[i][0..lens[i]] });
+ Io.Dir.deleteFileAbsolute(testing.io, path) catch {};
+ }
+ try Io.Dir.cwd().deleteDir(testing.io, dir_path);
+ }
+};
+
+const home_dirs = [5][]const u8{ ".claude", ".codex", ".omp", ".hermes", ".dsh" };
+
+fn expectNoent(a: Answer) !void {
+ try testing.expect(a.reply.status == .err);
+ try testing.expectEqual(E.NOENT, a.reply.errno);
+}
+
+fn expectEperm(a: Answer) !void {
+ try testing.expect(a.reply.status == .err);
+ try testing.expectEqual(E.PERM, a.reply.errno);
+}
+
+test "tree: facts at the root" {
+ var rig: Rig = .{ .dir = undefined };
+ try rig.start();
+ defer rig.end();
+ const a = rig.lookupName(1, root, "pid");
+ try testing.expect(a.reply.status == .ok);
+ try testing.expect(a.reply.attr.dir == false);
+ var read_a = handle(rig.h, .{ .tag = 2, .op = .read, .node = a.reply.attr.node, .off = 0, .size = 64 });
+ try testing.expectEqualStrings("4242\n", read_a.bytes);
+ read_a = handle(rig.h, .{ .tag = 3, .op = .read, .node = a.reply.attr.node, .off = 0, .size = 2 });
+ try testing.expectEqualStrings("42", read_a.bytes);
+ const up = rig.lookupName(4, root, "uptime");
+ try testing.expect(up.reply.status == .ok);
+ const up_read = handle(rig.h, .{ .tag = 5, .op = .read, .node = up.reply.attr.node, .off = 0, .size = 64 });
+ try testing.expect(up_read.bytes.len > 0 and up_read.bytes[up_read.bytes.len - 1] == '\n');
+ // The root lists the facts and the five harness dirs.
+ const listing = handle(rig.h, .{ .tag = 6, .op = .readdir, .node = root, .off = 0, .size = msize });
+ try testing.expect(listing.reply.status == .ok);
+ for ([_][]const u8{ "README", "pid", "uptime", "claude", "codex", "omp", "hermes", "dsh", "skills" }) |name| {
+ try testing.expect(stageHas(listing.bytes, name));
+ }
+}
+
+test "tree: README is served at the root, whole and read-only" {
+ var rig: Rig = .{ .dir = undefined };
+ try rig.start();
+ defer rig.end();
+
+ const rd = rig.lookupName(1, root, "README");
+ try testing.expect(rd.reply.status == .ok);
+ try testing.expect(!rd.reply.attr.dir);
+ try testing.expectEqual(readme_text.len, rd.reply.attr.size);
+
+ // Read it back whole and compare: a truncated or empty README would
+ // otherwise pass every check above.
+ const got = handle(rig.h, .{ .tag = 2, .op = .read, .node = rd.reply.attr.node, .off = 0, .size = msize });
+ try testing.expect(got.reply.status == .ok);
+ try testing.expectEqualStrings(readme_text, got.bytes);
+
+ // It is a file, and it is read-only like everything else here.
+ const as_dir = handle(rig.h, .{ .tag = 3, .op = .readdir, .node = rd.reply.attr.node, .off = 0, .size = msize });
+ try testing.expectEqual(E.NOTDIR, as_dir.reply.errno);
+ const wr = handle(rig.h, .{ .tag = 4, .op = .write, .node = rd.reply.attr.node, .off = 0, .size = 1 });
+ try testing.expectEqual(E.PERM, wr.reply.errno);
+}
+
+fn stageHas(staged: []const u8, name: []const u8) bool {
+ var i: usize = 0;
+ while (i + 10 <= staged.len) {
+ const len: usize = staged[i + 9];
+ const entry = staged[i + 10 ..][0..len];
+ if (std.mem.eql(u8, entry, name)) return true;
+ i += 10 + len;
+ }
+ return false;
+}
+
+test "tree: exclusions never serve a credentials-shaped name" {
+ var rig: Rig = .{ .dir = undefined };
+ try rig.start();
+ defer rig.end();
+ // The fixture: a real transcript and a pile of credentials-shaped names
+ // at several depths, including inside the mirrored subtrees.
+ try rig.put(".claude/projects/p1/session-x.jsonl", "{\"a\":1}\n");
+ try rig.put(".claude/projects/p1/.credentials.json", "STAY OUT\n");
+ try rig.put(".claude/projects/p1/auth.json", "STAY OUT\n");
+ try rig.put(".claude/projects/p1/settings.json", "STAY OUT\n");
+ try rig.put(".claude/projects/p1/token.txt", "STAY OUT\n");
+ try rig.put(".claude/projects/p1/api.key", "STAY OUT\n");
+ try rig.put(".claude/projects/p1/models.yml", "STAY OUT\n");
+ try rig.put(".claude/projects/p1/deep/.env", "STAY OUT\n");
+ try rig.put(".claude/history.jsonl", "{\"h\":1}\n");
+ try rig.put(".hermes/auth.json", "STAY OUT\n");
+ try rig.put(".hermes/.env", "STAY OUT\n");
+ try rig.put(".hermes/config.yaml", "STAY OUT\n");
+ try rig.put(".hermes/logs/app.log", "log line\n");
+ try rig.put(".dsh/.credentials.yaml", "STAY OUT\n");
+ try rig.put(".dsh/settings.yaml", "STAY OUT\n");
+
+ // /claude lists only its mounts; the credentials at ~/.claude root are
+ // not in the tree at all (no mount serves them).
+ const claude = handle(rig.h, .{ .tag = 1, .op = .readdir, .node = topNode(.claude), .off = 0, .size = msize });
+ try testing.expect(claude.reply.status == .ok);
+ for ([_][]const u8{ "projects", "history", "skills" }) |name| try testing.expect(stageHas(claude.bytes, name));
+ try testing.expect(!stageHas(claude.bytes, "credentials"));
+
+ // A project's listing shows the transcript and nothing else.
+ const projects = rig.lookupName(2, topNode(.claude), "projects");
+ try testing.expect(projects.reply.status == .ok);
+ const p1 = rig.lookupName(3, projects.reply.attr.node, "p1");
+ try testing.expect(p1.reply.status == .ok);
+ const listed = handle(rig.h, .{ .tag = 4, .op = .readdir, .node = p1.reply.attr.node, .off = 0, .size = msize });
+ try testing.expect(listed.reply.status == .ok);
+ try testing.expect(stageHas(listed.bytes, "session-x.jsonl"));
+ for ([_][]const u8{ ".credentials.json", "auth.json", "settings.json", "token.txt", "api.key", "models.yml" }) |name| {
+ try testing.expect(!stageHas(listed.bytes, name));
+ }
+ // The plain directory that holds a .env is served; the .env is not.
+ try testing.expect(stageHas(listed.bytes, "deep"));
+
+ // Every credentials-shaped name is unreachable by lookup too.
+ for ([_][]const u8{ ".credentials.json", "auth.json", "settings.json", "token.txt", "api.key", "models.yml" }) |name| {
+ try expectNoent(rig.lookupName(5, p1.reply.attr.node, name));
+ }
+ const deep = rig.lookupName(6, p1.reply.attr.node, "deep");
+ try testing.expect(deep.reply.status == .ok);
+ try expectNoent(rig.lookupName(7, deep.reply.attr.node, ".env"));
+
+ // The fully mirrored roots hide theirs as well.
+ const hermes = rig.lookupName(8, root, "hermes");
+ try testing.expect(hermes.reply.status == .ok);
+ const hlist = handle(rig.h, .{ .tag = 9, .op = .readdir, .node = hermes.reply.attr.node, .off = 0, .size = msize });
+ try testing.expect(stageHas(hlist.bytes, "logs"));
+ for ([_][]const u8{ "auth.json", ".env", "config.yaml" }) |name| {
+ try testing.expect(!stageHas(hlist.bytes, name));
+ try expectNoent(rig.lookupName(10, hermes.reply.attr.node, name));
+ }
+ const dsh = rig.lookupName(11, root, "dsh");
+ try testing.expect(dsh.reply.status == .ok);
+ const dlist = handle(rig.h, .{ .tag = 12, .op = .readdir, .node = dsh.reply.attr.node, .off = 0, .size = msize });
+ try testing.expect(!stageHas(dlist.bytes, ".credentials.yaml"));
+ try testing.expect(!stageHas(dlist.bytes, "settings.yaml"));
+ try testing.expect(stageHas(dlist.bytes, "profiles"));
+
+ // The exclusion predicate itself, spelled out.
+ try testing.expect(excluded(".credentials.json"));
+ try testing.expect(excluded("AUTH.JSON"));
+ try testing.expect(excluded("session-token.bin"));
+ try testing.expect(excluded("id_rsa_backup"));
+ try testing.expect(excluded("prod.pem"));
+ try testing.expect(excluded("config.yaml"));
+ try testing.expect(!excluded("session-x.jsonl"));
+ try testing.expect(!excluded("SKILL.md"));
+ try testing.expect(!excluded("logs"));
+}
+
+test "tree: paths compose only from the pinned roots" {
+ var rig: Rig = .{ .dir = undefined };
+ try rig.start();
+ defer rig.end();
+ try rig.put(".claude/projects/p1/session-x.jsonl", "{}\n");
+ // A slash, a NUL or an overlong name never reaches the filesystem.
+ try expectNoent(rig.lookupName(1, topNode(.claude), "projects/../p1"));
+ try expectNoent(rig.lookupName(2, topNode(.claude), "projects/\x00"));
+ // A symlink inside a mirror is not served, whatever it points at.
+ var buf: [std.fs.max_path_bytes]u8 = undefined;
+ const link = try std.fmt.bufPrintZ(&buf, "{s}/.claude/projects/p1/escape", .{rig.home});
+ try Io.Dir.cwd().symLink(testing.io, rig.home, link, .{});
+ const projects = rig.lookupName(3, topNode(.claude), "projects");
+ const p1 = rig.lookupName(4, projects.reply.attr.node, "p1");
+ try expectNoent(rig.lookupName(5, p1.reply.attr.node, "escape"));
+ // ".." from a mirrored file lands on its canonical parent, and walking
+ // ".." repeatedly terminates at the root.
+ const session = rig.lookupName(6, p1.reply.attr.node, "session-x.jsonl");
+ try testing.expect(session.reply.status == .ok);
+ var cur = session.reply.attr.node;
+ var hops: usize = 0;
+ while (hops < 8) : (hops += 1) {
+ const up = rig.lookupName(7, cur, "..");
+ try testing.expect(up.reply.status == .ok);
+ if (up.reply.attr.node == root) break;
+ cur = up.reply.attr.node;
+ }
+ try testing.expect(cur == root or hops < 8);
+ // The facts refuse lookups with NOTDIR.
+ try testing.expect(rig.lookupName(8, topNode(.pid), "x").reply.errno == E.NOTDIR);
+}
+
+test "tree: a file's qid path survives release, and its version tracks content" {
+ var rig: Rig = .{ .dir = undefined };
+ try rig.start();
+ defer rig.end();
+ try rig.put(".claude/projects/p1/session-x.jsonl", "{}\n");
+
+ const projects = rig.lookupName(1, topNode(.claude), "projects");
+ const dir = rig.lookupName(2, projects.reply.attr.node, "p1");
+ const first = rig.lookupName(3, dir.reply.attr.node, "session-x.jsonl");
+ try testing.expect(first.reply.status == .ok);
+ const qid_path = first.reply.attr.path.?;
+ const version = first.reply.attr.version;
+
+ // Give every reference back, so the table slot is free to recycle.
+ for ([_]u64{ first.reply.attr.node, dir.reply.attr.node, projects.reply.attr.node }) |n| {
+ _ = handle(rig.h, .{ .tag = 4, .op = .release, .node = n });
+ }
+
+ // Walk to it again from scratch. intro(5): two files are the same file if
+ // and only if their qids are the same — so the same unchanged file must
+ // come back with the same qid path, whatever the node id does. Before the
+ // qid path came from the file itself, the recycled slot's new generation
+ // made this a different file on every walk.
+ const p2 = rig.lookupName(5, topNode(.claude), "projects");
+ const d2 = rig.lookupName(6, p2.reply.attr.node, "p1");
+ const again = rig.lookupName(7, d2.reply.attr.node, "session-x.jsonl");
+ try testing.expect(again.reply.status == .ok);
+ try testing.expectEqual(qid_path, again.reply.attr.path.?);
+ try testing.expectEqual(version, again.reply.attr.version);
+
+ // Two different files never share a qid path.
+ try rig.put(".claude/projects/p1/session-y.jsonl", "{}\n");
+ const other = rig.lookupName(8, d2.reply.attr.node, "session-y.jsonl");
+ try testing.expect(other.reply.attr.path.? != qid_path);
+
+ // And a change to the content moves the version, which is what tells a
+ // caching client the transcript it is holding has grown.
+ try rig.put(".claude/projects/p1/session-x.jsonl", "{}\n{\"more\":1}\n");
+ const grown = rig.lookupName(9, d2.reply.attr.node, "session-x.jsonl");
+ try testing.expectEqual(qid_path, grown.reply.attr.path.?);
+ try testing.expect(grown.reply.attr.version != version);
+}
+
+test "tree: node ids — same file equal, distinct files differ, ids recycle" {
+ var rig: Rig = .{ .dir = undefined };
+ try rig.start();
+ defer rig.end();
+ try rig.put(".claude/projects/p1/session-x.jsonl", "{}\n");
+ try rig.put(".claude/projects/p1/session-y.jsonl", "{}\n");
+ const projects = rig.lookupName(1, topNode(.claude), "projects");
+ const a1 = rig.lookupName(2, projects.reply.attr.node, "p1");
+ // Two walks of the same file: the same node id (the dedupe table).
+ const x1 = rig.lookupName(3, a1.reply.attr.node, "session-x.jsonl");
+ const x2 = rig.lookupName(4, a1.reply.attr.node, "session-x.jsonl");
+ try testing.expect(x1.reply.status == .ok);
+ try testing.expectEqual(x1.reply.attr.node, x2.reply.attr.node);
+ const y1 = rig.lookupName(5, a1.reply.attr.node, "session-y.jsonl");
+ try testing.expect(y1.reply.attr.node != x1.reply.attr.node);
+ // The union skills view and the harness's own skills share identity.
+ const via_harness = rig.lookupName(6, topNode(.claude), "skills");
+ const via_union = rig.lookupName(7, topNode(.skills), "claude");
+ try testing.expectEqual(via_harness.reply.attr.node, via_union.reply.attr.node);
+ // References are paid back by release: both slots go, ids recycle.
+ for ([_]u64{ x1.reply.attr.node, x2.reply.attr.node, y1.reply.attr.node, a1.reply.attr.node }) |n| {
+ const rel = handle(rig.h, .{ .tag = 8, .op = .release, .node = n });
+ try testing.expect(rel.reply.status == .ok);
+ }
+ const x3 = rig.lookupName(9, projects.reply.attr.node, "p1");
+ const x4 = rig.lookupName(10, x3.reply.attr.node, "session-x.jsonl");
+ // The entry was freed and re-created: a fresh generation, a new id.
+ try testing.expect(x4.reply.attr.node != x1.reply.attr.node);
+ // Node packing round-trips through the struct.
+ const n: Node = .{ .idx = 3, .kind = 1, .serial = 0x1234_5678_9abc };
+ const bits: u64 = @bitCast(n);
+ const back: Node = @bitCast(bits);
+ try testing.expect(back.idx == 3 and back.kind == 1 and back.serial == 0x1234_5678_9abc);
+}
+
+test "tree: a file that vanishes between lookup and read answers ENOENT" {
+ var rig: Rig = .{ .dir = undefined };
+ try rig.start();
+ defer rig.end();
+ try rig.put(".claude/projects/p1/session-x.jsonl", "{\"a\":1}\n");
+ const projects = rig.lookupName(1, topNode(.claude), "projects");
+ const p1 = rig.lookupName(2, projects.reply.attr.node, "p1");
+ const session = rig.lookupName(3, p1.reply.attr.node, "session-x.jsonl");
+ try testing.expect(session.reply.status == .ok);
+ // The transcript reads back whole, fresh from disk.
+ const whole = handle(rig.h, .{ .tag = 4, .op = .read, .node = session.reply.attr.node, .off = 0, .size = 1024 });
+ try testing.expectEqualStrings("{\"a\":1}\n", whole.bytes);
+ // It vanishes: lookup, getattr and read all answer ENOENT cleanly.
+ try rig.del(".claude/projects/p1/session-x.jsonl");
+ try expectNoent(rig.lookupName(5, p1.reply.attr.node, "session-x.jsonl"));
+ const gone = handle(rig.h, .{ .tag = 6, .op = .getattr, .node = session.reply.attr.node });
+ try expectNoent(gone);
+ const read_gone = handle(rig.h, .{ .tag = 7, .op = .read, .node = session.reply.attr.node, .off = 0, .size = 64 });
+ try expectNoent(read_gone);
+}
+
+test "tree: writes and setattrs answer EPERM" {
+ var rig: Rig = .{ .dir = undefined };
+ try rig.start();
+ defer rig.end();
+ try rig.put(".claude/projects/p1/session-x.jsonl", "{}\n");
+ const projects = rig.lookupName(1, topNode(.claude), "projects");
+ const p1 = rig.lookupName(2, projects.reply.attr.node, "p1");
+ const session = rig.lookupName(3, p1.reply.attr.node, "session-x.jsonl");
+ try testing.expect(session.reply.status == .ok);
+ const write = handle(rig.h, .{ .tag = 4, .op = .write, .node = session.reply.attr.node, .data = "x" });
+ try expectEperm(write);
+ const set = handle(rig.h, .{ .tag = 5, .op = .setattr, .node = session.reply.attr.node, .set = .{ .mtime = true }, .mtime = 1 });
+ try expectEperm(set);
+ // An open for write is refused at open time.
+ const wopen = handle(rig.h, .{ .tag = 6, .op = .open, .node = session.reply.attr.node, .omode = cloud9.owrite });
+ try expectEperm(wopen);
+ const ropen = handle(rig.h, .{ .tag = 7, .op = .open, .node = session.reply.attr.node, .omode = cloud9.oread });
+ try testing.expect(ropen.reply.status == .ok);
+}
+
+test "tree: a transcript written while serving is visible at once" {
+ var rig: Rig = .{ .dir = undefined };
+ try rig.start();
+ defer rig.end();
+ try rig.put(".claude/projects/p1/session-x.jsonl", "{\"n\":1}\n");
+ const projects = rig.lookupName(1, topNode(.claude), "projects");
+ const p1 = rig.lookupName(2, projects.reply.attr.node, "p1");
+ // The harness appends; the next read sees it — no cache in between.
+ try rig.put(".claude/projects/p1/session-x.jsonl", "{\"n\":1}\n{\"n\":2}\n");
+ const session = rig.lookupName(3, p1.reply.attr.node, "session-x.jsonl");
+ const fresh = handle(rig.h, .{ .tag = 4, .op = .read, .node = session.reply.attr.node, .off = 0, .size = 1024 });
+ try testing.expectEqualStrings("{\"n\":1}\n{\"n\":2}\n", fresh.bytes);
+ // A brand-new session file appears on the next listing.
+ try rig.put(".claude/projects/p1/session-new.jsonl", "{\"n\":9}\n");
+ const listed = handle(rig.h, .{ .tag = 5, .op = .readdir, .node = p1.reply.attr.node, .off = 0, .size = msize });
+ try testing.expect(stageHas(listed.bytes, "session-new.jsonl"));
+}
+
+comptime {
+ // Every static mount target must survive the exclusion rules; the
+ // daemon's own skeleton may never be filtered out from under it.
+ for (named_mounts) |m| {
+ if (excludedPath(m.mount.rel)) @compileError("a 9agents mount target is excluded by name");
+ }
+}
+
+fn stageCount(staged: []const u8) usize {
+ var i: usize = 0;
+ var n: usize = 0;
+ while (i + 10 <= staged.len) : (n += 1) i += 10 + @as(usize, staged[i + 9]);
+ return n;
+}
+
+test "tree: a file directly under a mirror root reads back" {
+ // Regression: joining a child onto an empty relative path returned an
+ // uncopied scratch slice, so every file at the top of a mirror root
+ // (/hermes/<name>, /dsh/<name>) listed but resolved to garbage bytes.
+ var rig: Rig = .{ .dir = undefined };
+ try rig.start();
+ defer rig.end();
+ try rig.put(".hermes/note.txt", "hello-hermes\n");
+ const listing = handle(rig.h, .{ .tag = 1, .op = .readdir, .node = topNode(.hermes), .off = 0, .size = msize });
+ try testing.expect(stageHas(listing.bytes, "note.txt"));
+ const note = rig.lookupName(2, topNode(.hermes), "note.txt");
+ try testing.expect(note.reply.status == .ok);
+ const bytes = handle(rig.h, .{ .tag = 3, .op = .read, .node = note.reply.attr.node, .off = 0, .size = 64 });
+ try testing.expectEqualStrings("hello-hermes\n", bytes.bytes);
+}
+
+test "tree: a name swapped for a symlink under an open handle serves nothing" {
+ // Regression: the read path composed the whole path and opened it in
+ // one call, following symlinks. A name replaced between the walk and
+ // the read handed the client bytes from outside every pinned root.
+ var rig: Rig = .{ .dir = undefined };
+ try rig.start();
+ defer rig.end();
+ try rig.put("outside.txt", "OUTSIDE-THE-ROOTS\n"); // beside the roots, not in one
+ try rig.put(".claude/projects/p1/swap.txt", "safe\n");
+ const projects = rig.lookupName(1, topNode(.claude), "projects");
+ const p1 = rig.lookupName(2, projects.reply.attr.node, "p1");
+ const swap = rig.lookupName(3, p1.reply.attr.node, "swap.txt");
+ try testing.expect(swap.reply.status == .ok);
+ const opened = handle(rig.h, .{ .tag = 4, .op = .open, .node = swap.reply.attr.node, .omode = cloud9.oread });
+ try testing.expect(opened.reply.status == .ok);
+ // The file becomes a symlink out of the tree while the handle is open.
+ var link_buf: [std.fs.max_path_bytes]u8 = undefined;
+ var target_buf: [std.fs.max_path_bytes]u8 = undefined;
+ const link = try std.fmt.bufPrintZ(&link_buf, "{s}/.claude/projects/p1/swap.txt", .{rig.home});
+ const target = try std.fmt.bufPrint(&target_buf, "{s}/outside.txt", .{rig.home});
+ try rig.del(".claude/projects/p1/swap.txt");
+ try Io.Dir.cwd().symLink(testing.io, target, link, .{});
+ const after = handle(rig.h, .{ .tag = 5, .op = .read, .node = swap.reply.attr.node, .off = 0, .size = 64 });
+ try expectNoent(after);
+ try testing.expectEqual(@as(usize, 0), after.bytes.len);
+}
+
+test "tree: a listing past the cap fails loudly instead of truncating" {
+ // Regression: a directory with more entries (or longer names) than the
+ // comptime caps was served short, and a short listing is
+ // indistinguishable from a small directory.
+ var rig: Rig = .{ .dir = undefined };
+ try rig.start();
+ defer rig.end();
+ var name_buf: [64]u8 = undefined;
+ for (0..list_capacity + 1) |i| {
+ try rig.put(try std.fmt.bufPrint(&name_buf, ".dsh/f{d:0>5}", .{i}), "");
+ }
+ const listed = handle(rig.h, .{ .tag = 1, .op = .readdir, .node = topNode(.dsh), .off = 0, .size = msize });
+ try testing.expect(listed.reply.status == .err);
+ try testing.expectEqual(E.NFILE, listed.reply.errno);
+}
+
+test "tree: a listing spanning several reads loses no entry" {
+ // The staging buffer fills long before a big directory ends. Staging
+ // must stop at the first record that does not fit: dropping it and
+ // packing a shorter one behind it would lose that entry, because the
+ // next read's offset counts the records already delivered.
+ var rig: Rig = .{ .dir = undefined };
+ try rig.start();
+ defer rig.end();
+ const count = 200;
+ var name_buf: [320]u8 = undefined;
+ for (0..count) |i| {
+ const name = try std.fmt.bufPrint(&name_buf, ".dsh/{d:0>3}{s}", .{ i, "n" ** 200 });
+ try rig.put(name, "");
+ }
+ var seen: [count]bool = @splat(false);
+ var off: u64 = 0;
+ var reads: usize = 0;
+ while (reads < 16) : (reads += 1) {
+ const page = handle(rig.h, .{ .tag = 1, .op = .readdir, .node = topNode(.dsh), .off = off, .size = msize });
+ try testing.expect(page.reply.status == .ok);
+ if (page.bytes.len == 0) break;
+ var i: usize = 0;
+ while (i + 10 <= page.bytes.len) {
+ const len: usize = page.bytes[i + 9];
+ const name = page.bytes[i + 10 ..][0..len];
+ i += 10 + len;
+ // The fixture root also holds `profiles`, created by the rig.
+ const idx = std.fmt.parseInt(usize, name[0..@min(3, name.len)], 10) catch continue;
+ try testing.expect(!seen[idx]); // never served twice
+ seen[idx] = true;
+ }
+ off += stageCount(page.bytes);
+ }
+ try testing.expect(reads > 1); // the listing really did span several reads
+ for (seen) |s| try testing.expect(s);
+}
+
+test {
+ _ = active; // the derived view's own tests run with the tree's
+}
+
+// ---- /active: the derived view ------------------------------------------------
+
+/// Walks `/active` down to one agent's directory, answering its node.
+fn activeEntryNode(rig: *Rig, harness: []const u8, pid: []const u8) !u64 {
+ const act = rig.lookupName(90, root, "active");
+ try testing.expect(act.reply.status == .ok);
+ // A listing is what rescans, so it comes before the walk.
+ _ = handle(rig.h, .{ .tag = 91, .op = .readdir, .node = act.reply.attr.node, .off = 0, .size = msize });
+ const h_dir = rig.lookupName(92, act.reply.attr.node, harness);
+ try testing.expect(h_dir.reply.status == .ok);
+ const entry = rig.lookupName(93, h_dir.reply.attr.node, pid);
+ try testing.expect(entry.reply.status == .ok);
+ return entry.reply.attr.node;
+}
+
+fn activeField(rig: *Rig, entry: u64, name: []const u8) ![]const u8 {
+ const f = rig.lookupName(94, entry, name);
+ try testing.expect(f.reply.status == .ok);
+ const r = handle(rig.h, .{ .tag = 95, .op = .read, .node = f.reply.attr.node, .off = 0, .size = msize });
+ try testing.expect(r.reply.status == .ok);
+ return r.bytes;
+}
+
+test "active: a fixture process tree lists by harness and by pid" {
+ var rig: Rig = .{ .dir = undefined, .with_proc = true };
+ try rig.start();
+ defer rig.end();
+
+ // A Claude Code session, resolved the way the harness publishes it.
+ try rig.fakeProc(.{ .pid = 1001, .comm = "claude", .starttime = 5000, .argv = "claude\x00--print\x00" });
+ var rec: [512]u8 = undefined;
+ try rig.put(".claude/sessions/1001.json", try std.fmt.bufPrint(&rec,
+ \\{{"pid":1001,"sessionId":"sess-abc","cwd":"{s}",
+ \\ "name":"fixture-one","status":"busy","procStart":"5000"}}
+ , .{rig.home}));
+ var slug_buf: [512]u8 = undefined;
+ const slug = active.slugOf(.claude, rig.home, rig.home, &slug_buf).?;
+ var tr: [640]u8 = undefined;
+ try rig.put(
+ try std.fmt.bufPrint(&tr, ".claude/projects/{s}/sess-abc.jsonl", .{slug}),
+ "{\"type\":\"summary\",\"aiTitle\":\"Fixture Title\"}\n",
+ );
+
+ const entry = try activeEntryNode(&rig, "claude", "1001");
+ try testing.expectEqualStrings("1001\n", try activeField(&rig, entry, "pid"));
+ try testing.expectEqualStrings("sess-abc\n", try activeField(&rig, entry, "session"));
+ try testing.expectEqualStrings("registry\n", try activeField(&rig, entry, "via"));
+ try testing.expectEqualStrings("fixture-one\n", try activeField(&rig, entry, "name"));
+ try testing.expectEqualStrings("busy\n", try activeField(&rig, entry, "status"));
+ try testing.expectEqualStrings("Fixture Title\n", try activeField(&rig, entry, "title"));
+ // started is the boot time plus the process's own, in seconds.
+ try testing.expectEqualStrings("1000050\n", try activeField(&rig, entry, "started"));
+ // The transcript is served as the file it is, not as a field.
+ const t = rig.lookupName(96, entry, "transcript");
+ try testing.expect(t.reply.status == .ok);
+ const bytes = handle(rig.h, .{ .tag = 97, .op = .read, .node = t.reply.attr.node, .off = 0, .size = msize });
+ try testing.expect(std.mem.indexOf(u8, bytes.bytes, "Fixture Title") != null);
+ // A field this harness does not publish is absent, not blank.
+ try expectNoent(rig.lookupName(98, entry, "model"));
+}
+
+test "active: a daemon or helper is never listed as an agent" {
+ var rig: Rig = .{ .dir = undefined, .with_proc = true };
+ try rig.start();
+ defer rig.end();
+ // The shapes actually running on this machine.
+ try rig.fakeProc(.{ .pid = 1010, .comm = "python3", .argv = "python3\x00-m\x00hermes_cli.main\x00gateway\x00run\x00" });
+ try rig.fakeProc(.{ .pid = 1011, .comm = "omp", .argv = "omp\x00__omp_worker_daemon_broker\x00" });
+ try rig.fakeProc(.{ .pid = 1012, .comm = "claude", .argv = "claude\x00mcp-server\x00" });
+ // ...and one real session, so an empty answer cannot pass by default.
+ try rig.fakeProc(.{ .pid = 1013, .comm = "claude", .argv = "claude\x00" });
+
+ const act = rig.lookupName(1, root, "active");
+ const listing = handle(rig.h, .{ .tag = 2, .op = .readdir, .node = act.reply.attr.node, .off = 0, .size = msize });
+ try testing.expect(stageHas(listing.bytes, "claude"));
+ try testing.expect(!stageHas(listing.bytes, "hermes"));
+ try testing.expect(!stageHas(listing.bytes, "omp"));
+ const claude = rig.lookupName(3, act.reply.attr.node, "claude");
+ const pids = handle(rig.h, .{ .tag = 4, .op = .readdir, .node = claude.reply.attr.node, .off = 0, .size = msize });
+ try testing.expect(stageHas(pids.bytes, "1013"));
+ try testing.expect(!stageHas(pids.bytes, "1012")); // the mcp server
+}
+
+test "active: a pid reused by another process stops resolving" {
+ var rig: Rig = .{ .dir = undefined, .with_proc = true };
+ try rig.start();
+ defer rig.end();
+ try rig.fakeProc(.{ .pid = 1020, .comm = "claude", .starttime = 5000, .argv = "claude\x00" });
+ const entry = try activeEntryNode(&rig, "claude", "1020");
+ try testing.expect(handle(rig.h, .{ .tag = 5, .op = .getattr, .node = entry }).reply.status == .ok);
+
+ // The same pid, a different process: only the start time says so.
+ try rig.fakeProc(.{ .pid = 1020, .comm = "claude", .starttime = 9999, .argv = "claude\x00" });
+ const act = rig.lookupName(6, root, "active");
+ _ = handle(rig.h, .{ .tag = 7, .op = .readdir, .node = act.reply.attr.node, .off = 0, .size = msize });
+ try expectNoent(handle(rig.h, .{ .tag = 8, .op = .getattr, .node = entry }));
+ // The pid is still there — as the new process, under a new node.
+ const fresh = try activeEntryNode(&rig, "claude", "1020");
+ try testing.expect(fresh != entry);
+}
+
+test "active: a process that exits leaves the tree" {
+ var rig: Rig = .{ .dir = undefined, .with_proc = true };
+ try rig.start();
+ defer rig.end();
+ try rig.fakeProc(.{ .pid = 1030, .comm = "claude", .argv = "claude\x00" });
+ const entry = try activeEntryNode(&rig, "claude", "1030");
+ try testing.expect(handle(rig.h, .{ .tag = 9, .op = .getattr, .node = entry }).reply.status == .ok);
+
+ try rig.reapProc(1030);
+ const act = rig.lookupName(10, root, "active");
+ const listing = handle(rig.h, .{ .tag = 11, .op = .readdir, .node = act.reply.attr.node, .off = 0, .size = msize });
+ try testing.expect(!stageHas(listing.bytes, "claude"));
+ try expectNoent(handle(rig.h, .{ .tag = 12, .op = .getattr, .node = entry }));
+ try expectNoent(rig.lookupName(13, act.reply.attr.node, "claude"));
+}
+
+test "active: the daemon never lists the process tree it lives in" {
+ // 1041 is the daemon, its parent 1040 is a harness: showing it would
+ // let a client act on the tree serving it.
+ var rig: Rig = .{ .dir = undefined, .with_proc = true, .self_pid = 1041 };
+ try rig.start();
+ defer rig.end();
+ try rig.fakeProc(.{ .pid = 1040, .comm = "claude", .argv = "claude\x00" });
+ try rig.fakeProc(.{ .pid = 1041, .comm = "9agents", .ppid = 1040, .argv = "9agents\x00" });
+ try rig.fakeProc(.{ .pid = 1042, .comm = "claude", .argv = "claude\x00" });
+
+ const act = rig.lookupName(14, root, "active");
+ const claude = rig.lookupName(15, act.reply.attr.node, "claude");
+ try testing.expect(claude.reply.status == .ok);
+ const pids = handle(rig.h, .{ .tag = 16, .op = .readdir, .node = claude.reply.attr.node, .off = 0, .size = msize });
+ try testing.expect(stageHas(pids.bytes, "1042")); // an unrelated session lists
+ try testing.expect(!stageHas(pids.bytes, "1040")); // its own parent does not
+}