summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-21 14:23:27 -0300
committerGabriel Schneider <[email protected]>2026-09-21 15:20:40 -0300
commitb4db588dd5b92d647b661c2dc17b40925af92348 (patch)
tree7a036e500251d7f3f958854dcf3a8ead375bbc63
parent0d7e295efee1fca0935cf4a8bee9629c007dd2b6 (diff)
downloadcloud9-b4db588dd5b92d647b661c2dc17b40925af92348.tar.gz
cloud9-b4db588dd5b92d647b661c2dc17b40925af92348.zip
9harness: the harness fs daemon
The last item of the 9P plan, replacing zmxify's introspection half: a read-only, fresh-from-disk 9P view of every agent harness's state on this machine, posted as `harness` like any other service, so a shell inside a 9ns --mntgen mount reads it at /mnt/9p/harness with no setup. /pid /uptime /claude/{projects,history,skills} /codex/{sessions,session-index,history} /omp /hermes /dsh the mirrors /skills/{claude,codex,omp} Nothing is cached: a lookup, getattr or readdir walks the real filesystem, so a transcript grows as its harness writes it and a new session appears as soon as its file lands. Writes answer EPERM, and no name that looks like a credential, key, token or auth store is ever answered at any depth. Three findings from the adversarial pass, each with its regression: - The read path composed <base>/<rel> and opened it in one call, which follows symlinks. A name swapped for a link between the walk and the read served bytes from outside every pinned root (proved against /etc/passwd). Every stat, read and readdir now resolves through openIn, which walks from the base one component at a time with O_NOFOLLOW, and O_PATH for the intermediates, so no component can redirect the walk. O_PATH also keeps a fifo in a root from parking the daemon in open(); a read refuses anything but a regular file. - Joining a child onto an empty relative path returned an uncopied scratch slice, so every file at the top of a mirror root (/hermes/x, /dsh/x) listed but read back uninitialized stack bytes. - A directory past the comptime caps was served short, and a short listing cannot be told from a small directory. The caps answer NFILE now. Staging also stops at the first record that does not fit instead of packing a shorter one behind it, which dropped that entry from the listing across the read boundary. Suites: 13/13 unit (fake HOME, never the live roots), 36/0 end-to-end including the mntgen money shot and the live ~/.claude/.credentials.json proved unreachable, 131/131 programs-test.
-rw-r--r--9harness/build.zig54
-rw-r--r--9harness/docs/DESIGN.md225
-rw-r--r--9harness/src/main.zig322
-rw-r--r--9harness/src/tree.zig1329
-rwxr-xr-x9harness/test/e2e.sh266
-rw-r--r--build.zig20
6 files changed, 2216 insertions, 0 deletions
diff --git a/9harness/build.zig b/9harness/build.zig
new file mode 100644
index 0000000..4cf84d6
--- /dev/null
+++ b/9harness/build.zig
@@ -0,0 +1,54 @@
+//! Build fragment for 9harness: the harness fs daemon (binary `9harness`),
+//! a read-only fresh-from-disk 9P2000 view of every harness's state,
+//! posted by default under the name `harness`. It is `@import`ed by the
+//! root build.zig and called with the root builder, so every `b.path(...)`
+//! here is relative to the cloud9 root (hence the `9harness/` prefix),
+//! every option is defined by the root and every step it registers lands
+//! in the root's step list under the `9harness` prefix.
+//!
+//! Steps: 9harness, 9harness-test, 9harness-itest.
+const std = @import("std");
+
+pub const Context = struct {
+ target: std.Build.ResolvedTarget,
+ optimize: std.builtin.OptimizeMode,
+ cloud9: *std.Build.Module,
+ /// The 9ns binary, for the --mntgen money shot in the integration
+ /// suite; null when 9ns is disabled, in which case the mntgen
+ /// section of the suite is skipped.
+ ns: ?*std.Build.Step.Compile,
+};
+
+pub const Artifacts = struct {
+ exe: *std.Build.Step.Compile,
+ /// `9harness-test`: the tree's unit tests (exclusions, path safety,
+ /// node ids, vanishing files — all against a fake HOME).
+ test_step: *std.Build.Step,
+ /// `9harness-itest`: test/e2e.sh (fixture roots + scratch registry,
+ /// plus the real-registry end-to-end when the `harness` name is free).
+ itest_step: *std.Build.Step,
+};
+
+pub fn add(b: *std.Build, ctx: Context) Artifacts {
+ const mod = b.createModule(.{
+ .root_source_file = b.path("9harness/src/main.zig"),
+ .target = ctx.target,
+ .optimize = ctx.optimize,
+ .imports = &.{.{ .name = "cloud9", .module = ctx.cloud9 }},
+ });
+ const exe = b.addExecutable(.{ .name = "9harness", .root_module = mod });
+ const install = b.addInstallArtifact(exe, .{});
+ b.getInstallStep().dependOn(&install.step);
+ b.step("9harness", "Build and install only the harness fs daemon").dependOn(&install.step);
+
+ const test_step = b.step("9harness-test", "Run the 9harness tree's unit tests (fake HOME; never the live roots)");
+ test_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = mod })).step);
+
+ const itest_step = b.step("9harness-itest", "Run 9harness/test/e2e.sh (posted name, reads, cmp of a transcript, mntgen mount, exclusions, live appends)");
+ const run = b.addSystemCommand(&.{"bash"});
+ run.addFileArg(b.path("9harness/test/e2e.sh"));
+ run.addArtifactArg(exe);
+ if (ctx.ns) |ns| run.addArtifactArg(ns);
+ itest_step.dependOn(&run.step);
+ return .{ .exe = exe, .test_step = test_step, .itest_step = itest_step };
+}
diff --git a/9harness/docs/DESIGN.md b/9harness/docs/DESIGN.md
new file mode 100644
index 0000000..1b5f33e
--- /dev/null
+++ b/9harness/docs/DESIGN.md
@@ -0,0 +1,225 @@
+# 9harness design
+
+9harness is the harness fs daemon: a long-running 9P2000 server that
+replaces the `zmxify` rc script's introspection half with a unified,
+file-shaped, always-fresh view of every AI-agent harness's state on the
+machine — Claude Code (`~/.claude`), Codex (`~/.codex`), omp (`~/.omp`),
+hermes (`~/.hermes`) and dsh (`~/.dsh`), plus their skills. Everything is
+read-only and read live from disk at request time; nothing is cached, so
+a session transcript grows as its harness writes it and a new session
+appears as soon as its file lands.
+
+One binary, hosted Linux only (it posts through `cloud9.post`). The
+backend is `src/tree.zig`, a `cloud9.fs.Server` backend run by
+`cloud9.serve.Runner`; the CLI is `src/main.zig`. Like its siblings
+(9proc, 9ns) the build is a fragment in `build.zig` wired into the root
+behind `-D9harness` (steps `9harness`, `9harness-test`, `9harness-itest`;
+installed by the plain `zig build` next to the other programs).
+
+## The tree (v1 contract)
+
+```
+/ read-only root (dr-xr-xr-x)
+/pid the daemon's pid, one line
+/uptime seconds since start, one line
+/claude/
+ projects/ mirror of ~/.claude/projects/<project-dir>/: one dir
+ per project, its *.jsonl session transcripts as plain
+ readable files, memory/ subdirs included
+ history ~/.claude/history.jsonl (global prompt history)
+ skills/ mirror of ~/.claude/skills
+/codex/
+ sessions/ mirror of ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl
+ session-index ~/.codex/session_index.jsonl
+ history ~/.codex/history.jsonl
+/omp/ mirror of ~/.omp/agent (history.db, agent.db, models.db,
+ ... served as raw readable blobs; sqlite parsing is a
+ later step — v1 is files)
+/hermes/ mirror of ~/.hermes (state.db raw, logs/)
+/dsh/ mirror of ~/.dsh (profiles/, storages/, ... whatever it holds)
+/skills/ union view: plain dirs claude/, codex/, omp/ mirroring each
+ harness's skills dir; a harness without one simply has no entry
+```
+
+Mirroring is lazy: nothing is walked at startup. A lookup, getattr or
+readdir stats and lists the real files at request time. Unknown files and
+directories appear as themselves, readable. A file that vanishes between
+lookup and read answers ENOENT cleanly; a file that appears is visible on
+the next request.
+
+Writes, creates, removes and setattrs answer EPERM (create/remove/wstat
+are not declared as backend features, so the engine refuses them itself;
+the write and setattr ops answer `E.PERM`, as does an open for write).
+Modes are reported read-only regardless of the real bits: directories
+`0o555`, files `0o444`. Sizes and mtimes are real.
+
+The engine's known fidelity gap applies: directory records carry fixed
+modes and length 0, so `9p ls -l` shows placeholders; `9p stat` is the
+accurate one.
+
+## The exclusions (the security boundary)
+
+The daemon must never become a credential reader for anything that
+mounts it. The rule is absolute — `excluded()` in tree.zig, unit-tested —
+and applies to every path component of every subtree, by lookup *and* by
+readdir, so an excluded name neither resolves nor lists:
+
+- names containing `credentials`, `token`, `auth` or `secret`
+ (case-insensitive substrings; "auth" also hides "author-notes" — the
+ price of never guessing wrong);
+- names ending `.key`, `.pem` or `.env`;
+- the config/settings files of the mirrored harnesses, which may embed
+ API keys: `settings.json`, `settings.yaml`, `settings.local.json`,
+ `config.yml`, `config.yaml`, `config.toml`, `models.yml`, `models.yaml`
+ (Claude Code's settings.json is not in the tree anyway: /claude serves
+ only projects/, skills/ and history.jsonl);
+- `.ssh`, and `id_rsa`/`id_dsa`/`id_ecdsa`/`id_ed25519` prefixes.
+
+Symlinks are never served, at any depth: they are an escape hatch around
+the root pinning (a symlink out of `~/.claude` would make the daemon read
+outside the five roots). A symlink answers ENOENT and is not listed.
+
+Path resolution is the mechanism that makes that true, and it is not
+string composition. `openIn` walks from the pinned base one component at
+a time, each opened with `O_NOFOLLOW`: the intermediate components with
+`O_PATH|O_DIRECTORY` (so a component swapped for a symlink fails with
+ENOTDIR rather than redirecting the walk) and the last with the caller's
+flags. Every stat, read and readdir goes through it. Composing
+`<base>/<rel>` and opening that in one call is what the first version
+did, and it was wrong in a way a test could not see: only the final
+component's symlink was refused, so a name replaced between the walk and
+the read — the plain TOCTOU — served bytes from outside every root, and
+a directory above it could redirect the whole path at any time. `O_PATH`
+also means a fifo or device left in a harness root is stat'd without its
+open ever blocking; a read refuses anything that is not a regular file.
+
+The base itself comes from $HOME or `--root NAME=PATH` at startup and is
+resolved normally (it is configuration, and may legitimately be a link).
+Relative paths are composed only from remembered (root, rel) pairs plus
+single-segment, engine-vetted names. Client bytes never form a path:
+names with `/` or NUL are refused before the filesystem is touched, `.`
+and `..` are refused as components inside the walk, and `..` as a lookup
+resolves through the backend's own parent map, never the kernel's.
+
+## Backend shape
+
+`Harness` (tree.zig) is the backend of `fs.Server(Harness, opts)` on
+`serve.Runner` (main.zig): allocation-free request paths, comptime caps,
+`Io` passed explicitly, static memory in .bss (the path table and the
+per-connection buffers; ~3 MB of tables, untouched pages cost nothing).
+
+- **Node ids** pack `Node{idx: u8, kind: u8, serial: u48}` in the u64,
+ zmx-style. Static skeleton nodes (facts, the harness dirs, skills) are
+ `kind = .top`; every mirrored file is `kind = .path` with
+ `serial = (slot, generation)` of its table entry. Two walks of the same
+ file find the same entry, so their node ids — and qid paths — are
+ equal; an entry freed by its last release takes a new generation, so a
+ stale forged node id never resolves.
+- **The path table** remembers at most 4096 remembered (root, rel) pairs,
+ each with a Wyhash of the pair for dedupe and a reference count. The
+ backend declares `features = .{ .references = true }`, so the engine
+ asks lookups for `.` too and pays every reference back with exactly one
+ `release`; an entry's refcount reaching zero frees it. Readdir records
+ name entries without references (a listing does not pin), so a later
+ walk of the same name finds the same entry and the same node id. A full
+ table answers `E.NFILE` — on a lookup, and on a listing that cannot
+ name all its entries (see Readdir).
+- **Fresh reads**: a read opens the file, `readPositionalAll`s the window
+ at the request's offset, and closes it — every read is against the live
+ file, and the engine handles offsets, so `cat` of a 5 MB transcript
+ through a mntgen mount works.
+- **Readdir** collects a directory's surviving names, sorts them for
+ cross-read cursor stability, and stages the engine's records
+ (`node:u64le dir:u8 len:u8 name`), skipping `req.off` records. The
+ comptime caps (1024 names, 128 KiB of name bytes, the path table) are
+ **loud**: a directory that would not fit answers `E.NFILE` instead of a
+ short listing, because a short listing cannot be told apart from a
+ small directory and is therefore a wrong answer, not a limitation. The
+ staging buffer filling is different and normal — it ends that read at
+ the last record that fit, and the next read continues from there;
+ staging stops at the first record that does not fit rather than packing
+ a later, shorter one behind it, which would drop that entry from the
+ listing entirely. A directory that changes between the reads of one
+ listing may shift its cursor — the fresh-tree trade, accepted in v1.
+- **Locking**: one mutex spans `handle` and the reply (the answer's bytes
+ point into shared staging buffers), so the backend is serialized across
+ connections. v1 accepts this: it is an introspection fs, not a
+ throughput service.
+
+## CLI and process model
+
+```
+usage: 9harness [--unix PATH | --tcp IP:PORT | --fd N] [--no-post]
+ [--name NAME] [--root NAME=PATH]...
+```
+
+- Default: post itself under the name `harness` via
+ `serve.Runner.listenPosted`, so it appears at
+ `$XDG_RUNTIME_DIR/9p/harness` and every interactive fish (self-wrapped
+ in `9ns --mntgen`) sees it at `/mnt/9p/harness` with zero
+ configuration. `stop()` unposts; the stale-socket protocol covers a
+ killed daemon.
+- `--unix`/`--tcp` listen forms (beside the post, or with `--no-post`),
+ `--name` to post under another name, `--root NAME=PATH` to pin any of
+ the five roots elsewhere (defaults are `$HOME/.<name>`; the tests use
+ fake homes and never the live roots).
+- `--fd N` serves exactly one 9P session over the connected stream on
+ descriptor N (socket activation, `9ns --spawn` handoffs), driving the
+ engine directly; it posts nothing.
+- SIGTERM/SIGINT stop cleanly (unpost); SIGPIPE is ignored.
+- No daemonization in v1: run it in a zmx session, the zmx way —
+ `zmx run harness -d 9harness`.
+
+## Integration test plan (`test/e2e.sh`)
+
+Part A runs entirely on fixtures (a fake home pinned by `--root`, a
+scratch `XDG_RUNTIME_DIR` registry, plan9port's `9p` as the client):
+
+1. the posted name is listed in the registry;
+2. `9p ls /` shows the roots; a fixture transcript reads back
+ byte-identical (`cmp` against the source file); the skills union
+ mirrors the harness tree;
+3. credentials-shaped files are unreachable by both listing and reading,
+ at several depths, in every root;
+4. a file appended after the daemon started is visible immediately, and
+ a new session directory appears in the next listing;
+5. the pid fact answers the daemon's pid; writes answer EPERM;
+6. the `--unix` and `--fd` listen forms serve;
+7. SIGTERM unposts the name.
+
+Part B is the money shot, read-only against the real roots and the real
+`$XDG_RUNTIME_DIR/9p` — only when the `harness` name is free (a live
+daemon owning the name skips it, not fails): the posted name in the real
+registry, `9p` walks of the live `~/.claude`, a real transcript
+byte-identical through the tree, the live `~/.claude/.credentials.json`
+unreachable, the `9ns --mntgen` mount of `/mnt/9p/harness` with
+`head` of `/claude/history`, and the stop unposting the real name.
+
+Unit tests (tree.zig) cover the exclusions (including the predicate
+itself, spelled out), path-composition safety (slashes, NULs, symlinks,
+the `..` chain, NOTDIR), node id packing (same file equal, distinct
+files differ, the union identity, id recycling through release), the
+vanishing file (ENOENT on lookup, getattr and read), EPERM on
+write/setattr/open-for-write, and live visibility — all against a fake
+HOME under a test tmp dir, never the real harness roots.
+
+## Verification
+
+- `zig build` (installs `9harness` beside 9ns, 9proc-demo, 9web).
+- `zig build test` — the library's 80 tests stay green.
+- `zig build 9harness-test` — the tree's unit tests.
+- `zig build 9harness-itest` — `test/e2e.sh` (fixture + real registry).
+- `zig build 9proc-check-freestanding` — the daemon is hosted-only but
+ must not break the root module.
+- `zig build programs-test` / `programs-itest` — the umbrella steps,
+ which include 9harness's.
+
+## Out of scope for v1 (documented, not hidden)
+
+- Write paths (create/write/setattr stay EPERM), resume/attach
+ automation (the zmxify re-exec half), sqlite parsing (omp/hermes
+ sessions stay raw blobs), cross-session search, any caching or
+ invalidation layer.
+- Streaming/parked reads (a read answers at once; the transcripts are
+ plain files), and union-directory semantics beyond the plain
+ /skills/<harness> dirs.
diff --git a/9harness/src/main.zig b/9harness/src/main.zig
new file mode 100644
index 0000000..2355a4f
--- /dev/null
+++ b/9harness/src/main.zig
@@ -0,0 +1,322 @@
+//! 9harness: the harness fs daemon — a long-running 9P2000 server that
+//! mirrors every AI-agent harness's state (Claude Code, Codex, omp,
+//! hermes, dsh + their skills) as one read-only, fresh-from-disk tree.
+//! See docs/DESIGN.md for the tree contract and src/tree.zig for the tree.
+//!
+//! By default it posts itself under the name `harness` with
+//! `serve.Runner.listenPosted`, so it appears at $XDG_RUNTIME_DIR/9p/harness
+//! and every interactive fish (self-wrapped in `9ns --mntgen`) sees it at
+//! /mnt/9p/harness with zero configuration. `--unix`, `--tcp` and `--fd`
+//! are the other listen forms; `--no-post` serves without posting; the
+//! five harness roots default to $HOME/.<name> and `--root NAME=PATH`
+//! pins any of them elsewhere (the tests use fake homes).
+//!
+//! v1 has no daemonization: run it in a zmx session, the zmx way:
+//!
+//! zmx run harness -d 9harness
+//!
+//! SIGTERM or SIGINT stops it cleanly (a posted name is unposted).
+const std = @import("std");
+const builtin = @import("builtin");
+const cloud9 = @import("cloud9");
+const tree = @import("tree.zig");
+
+const fs = cloud9.fs;
+const serve = cloud9.serve;
+const post = cloud9.post;
+const transport = cloud9.transport;
+const Io = std.Io;
+const linux = std.os.linux;
+
+const usage_text =
+ \\usage: 9harness [--unix PATH | --tcp IP:PORT | --fd N] [--no-post]
+ \\ [--name NAME] [--root NAME=PATH]...
+ \\
+ \\A read-only, fresh-from-disk 9P2000 view of every harness's state:
+ \\ /pid /uptime daemon facts
+ \\ /claude/{projects,history,skills}
+ \\ /codex/{sessions,session-index,history}
+ \\ /omp /hermes /dsh full mirrors, raw
+ \\ /skills/{claude,codex,omp} the union skills view
+ \\
+ \\By default the daemon posts itself under the name `harness`, so it is
+ \\dialable at $XDG_RUNTIME_DIR/9p/harness and mountable by 9ns --mntgen
+ \\at /mnt/9p/harness. --no-post skips posting; --unix/--tcp add plain
+ \\listeners beside the post; --fd N serves one 9P session over the
+ \\connected stream on descriptor N and posts nothing.
+ \\--root NAME=PATH pins one harness root (NAME: claude, codex, omp,
+ \\hermes, dsh) somewhere other than $HOME/.<NAME>; repeatable.
+ \\
+ \\Run it in a zmx session, the zmx way: `zmx run harness -d 9harness`.
+ \\
+;
+
+const max_connections = 16;
+const Runner = serve.Runner(tree.Harness, tree.opts, .{
+ .msize = tree.msize,
+ .connections = max_connections,
+ .listeners = 2, // the posted name plus one of --unix/--tcp
+});
+
+// Static memory, the 9proc-demo way: the harness and the runner are large
+// (the path table, the per-connection buffers) and live in .bss.
+var harness_mem: tree.Harness = undefined;
+var runner_mem: Runner = undefined;
+var stop_requested = std.atomic.Value(bool).init(false);
+
+fn onSignal(sig: linux.SIG) callconv(.c) void {
+ _ = sig;
+ stop_requested.store(true, .release);
+}
+
+fn installSignalHandlers() void {
+ const act = linux.Sigaction{
+ .handler = .{ .handler = onSignal },
+ .mask = @splat(0),
+ .flags = 0,
+ };
+ _ = linux.sigaction(.TERM, &act, null);
+ _ = linux.sigaction(.INT, &act, null);
+ const ign = linux.Sigaction{
+ .handler = .{ .handler = linux.SIG.IGN },
+ .mask = @splat(0),
+ .flags = 0,
+ };
+ _ = linux.sigaction(.PIPE, &ign, null);
+}
+
+// ---- the serve handler ---------------------------------------------------------
+
+fn serveReq(ctx: ?*anyopaque, conn: *Runner.Conn, req: fs.Req) void {
+ const h: *tree.Harness = @ptrCast(@alignCast(ctx.?));
+ // The answer's bytes point into the harness's shared buffers, so the
+ // mutex spans handle and reply: the engine copies them into the
+ // connection's output before the lock goes.
+ h.mutex.lockUncancelable(h.io);
+ const a = tree.handle(h, req);
+ conn.reply(&a.reply, a.bytes);
+ h.mutex.unlock(h.io);
+}
+
+// ---- the CLI --------------------------------------------------------------------
+
+const Mode = enum { posted, unix, tcp, fd };
+
+pub fn main(init: std.process.Init) !void {
+ run(init) catch |e| switch (e) {
+ // Both are reported with their own line above; a returned error
+ // would bury it under a Debug-build stack trace.
+ error.Usage, error.AlreadyPosted => std.process.exit(1),
+ else => return e,
+ };
+}
+
+fn run(init: std.process.Init) !void {
+ const io = init.io;
+ const arena = init.arena.allocator();
+ const args = try init.minimal.args.toSlice(arena);
+ const envp: post.Env = init.minimal.environ.block.slice.ptr;
+
+ var mode: Mode = .posted;
+ var unix_path: []const u8 = "";
+ var tcp_addr: []const u8 = "";
+ var fd_no: i32 = -1;
+ var post_name: []const u8 = "harness";
+ var no_post = false;
+ var base_overrides: [5]?[]const u8 = @splat(null);
+
+ var i: usize = 1;
+ while (i < args.len) : (i += 1) {
+ const a = args[i];
+ if (std.mem.eql(u8, a, "--no-post")) {
+ no_post = true;
+ } else if (std.mem.eql(u8, a, "--unix") or std.mem.eql(u8, a, "--tcp") or
+ std.mem.eql(u8, a, "--fd") or std.mem.eql(u8, a, "--name") or
+ std.mem.eql(u8, a, "--root"))
+ {
+ i += 1;
+ if (i >= args.len) {
+ std.debug.print("9harness: {s} needs an argument\n{s}", .{ a, usage_text });
+ return error.Usage;
+ }
+ const v = args[i];
+ if (std.mem.eql(u8, a, "--unix")) {
+ mode = .unix;
+ unix_path = v;
+ } else if (std.mem.eql(u8, a, "--tcp")) {
+ mode = .tcp;
+ tcp_addr = v;
+ } else if (std.mem.eql(u8, a, "--fd")) {
+ mode = .fd;
+ fd_no = std.fmt.parseInt(i32, v, 10) catch {
+ std.debug.print("9harness: --fd: not a number: {s}\n", .{v});
+ return error.Usage;
+ };
+ } else if (std.mem.eql(u8, a, "--name")) {
+ post_name = v;
+ if (!post.legalName(post_name)) {
+ std.debug.print("9harness: illegal post name: {s}\n", .{v});
+ return error.Usage;
+ }
+ } else {
+ const eq = std.mem.indexOfScalar(u8, v, '=') orelse {
+ std.debug.print("9harness: --root NAME=PATH: {s}\n{s}", .{ v, usage_text });
+ return error.Usage;
+ };
+ const name = v[0..eq];
+ const root = std.meta.stringToEnum(tree.Root, name) orelse {
+ std.debug.print("9harness: unknown root {s} (claude, codex, omp, hermes, dsh)\n", .{name});
+ return error.Usage;
+ };
+ base_overrides[@intFromEnum(root)] = v[eq + 1 ..];
+ }
+ } else if (std.mem.eql(u8, a, "--help") or std.mem.eql(u8, a, "-h")) {
+ std.debug.print("{s}", .{usage_text});
+ return;
+ } else {
+ std.debug.print("9harness: unknown argument {s}\n{s}", .{ a, usage_text });
+ return error.Usage;
+ }
+ }
+
+ // The five pinned roots: $HOME/.<name> unless overridden.
+ const home = post.getenv(envp, "HOME");
+ var bases: [5][]const u8 = @splat("");
+ inline for (0..5) |r| {
+ if (base_overrides[r]) |path| {
+ bases[r] = path;
+ } else if (home) |hm| {
+ bases[r] = std.fmt.bufPrint(&root_bufs[r], "{s}/.{s}", .{ hm, root_dirs[r] }) catch return error.NameTooLong;
+ }
+ }
+ var any = false;
+ for (bases) |b| any = any or b.len != 0;
+ if (!any) {
+ std.debug.print("9harness: no harness roots (set $HOME or pass --root NAME=PATH)\n", .{});
+ return error.Usage;
+ }
+
+ harness_mem.init(.{ .io = io, .pid = @intCast(linux.getpid()), .bases = bases });
+ if (no_post and mode == .posted) {
+ std.debug.print("9harness: --no-post needs a listen form (--unix, --tcp or --fd)\n{s}", .{usage_text});
+ return error.Usage;
+ }
+
+ installSignalHandlers();
+
+ switch (mode) {
+ .fd => {
+ std.debug.print("9harness: serving one session on fd {d}\n", .{fd_no});
+ try serveFd(io, fd_no);
+ return;
+ },
+ .posted, .unix, .tcp => {},
+ }
+
+ runner_mem.init(.{ .io = io, .root = tree.root, .handler = .{ .ctx = &harness_mem, .serve = serveReq } });
+ defer runner_mem.stop();
+
+ var posted_something = false;
+ if (!no_post) {
+ runner_mem.listenPosted(envp, post_name, 16) catch |err| {
+ if (err == error.AlreadyPosted) {
+ std.debug.print("9harness: the name `{s}` is already posted by a live server\n", .{post_name});
+ return err;
+ }
+ std.debug.print("9harness: cannot post as {s}: {t}\n", .{ post_name, err });
+ return err;
+ };
+ posted_something = true;
+ var pbuf: [transport.sun_path_len]u8 = undefined;
+ const path = post.registryPath(envp, post_name, &pbuf) catch "";
+ std.debug.print("9harness: posted as {s} at {s}\n", .{ post_name, path });
+ }
+ switch (mode) {
+ .unix => {
+ _ = try runner_mem.listen(.{ .unix = try arena.dupeZ(u8, unix_path) }, 16);
+ std.debug.print("9harness: listening on {s}\n", .{unix_path});
+ },
+ .tcp => {
+ const addr = Io.net.IpAddress.parseLiteral(tcp_addr) catch {
+ std.debug.print("9harness: bad --tcp address: {s}\n", .{tcp_addr});
+ return error.Usage;
+ };
+ const bound = try runner_mem.listen(.{ .tcp = addr }, 16);
+ std.debug.print("9harness: listening on tcp!{f}\n", .{bound});
+ },
+ else => {},
+ }
+ var pinned: u32 = 0;
+ for (bases) |b| {
+ if (b.len != 0) pinned += 1;
+ }
+ std.debug.print("9harness: serving ({d} roots pinned, {d} bytes of tables)\n", .{ pinned, @sizeOf(tree.Harness) });
+
+ // The runner's tasks drive themselves; the main thread only waits for
+ // a stop signal (SIGTERM/SIGINT) and then unposts via stop().
+ while (!stop_requested.load(.acquire)) {
+ io.sleep(.fromMilliseconds(250), .awake) catch break;
+ }
+ std.debug.print("9harness: stopping\n", .{});
+}
+
+const root_dirs = [5][]const u8{ "claude", "codex", "omp", "hermes", "dsh" };
+var root_bufs: [5][tree.base_capacity]u8 = @splat(@splat(0));
+
+// ---- --fd N: one 9P session over a connected stream -----------------------------
+
+/// Serves exactly one client on the connected stream of descriptor `n`
+/// (socket activation, `9ns --spawn`-style handoffs), driving the engine
+/// directly, the freestanding way. Returns when the client hangs up.
+fn serveFd(io: Io, n: i32) !void {
+ const stream: Io.net.Stream = .{ .socket = .{ .handle = n, .address = .{ .ip4 = .loopback(0) } } };
+ const Engine = fs.Server(tree.Harness, tree.opts);
+ var in: [tree.msize]u8 = undefined;
+ var out: [2 * tree.msize]u8 = undefined;
+ var rbuf: [tree.msize]u8 = undefined;
+ var wbuf: [2 * tree.msize]u8 = undefined;
+ var stage: [tree.msize]u8 = undefined;
+ var engine = Engine.init(.{ .in = &in, .out = &out, .root = tree.root });
+ var reader = stream.reader(io, &rbuf);
+ var writer = stream.writer(io, &wbuf);
+ while (true) {
+ const frame = transport.readFrame(&reader.interface, &stage, tree.msize) catch return;
+ var off: usize = 0;
+ while (off < frame.len) {
+ off += engine.push(frame[off..]);
+ while (true) {
+ const req = engine.next() orelse break;
+ const a = answer(req);
+ engine.reply(&a.reply, a.bytes);
+ }
+ const pending = engine.output();
+ writer.interface.writeAll(pending) catch return;
+ engine.wrote(pending.len);
+ if (engine.protocol.dead) return;
+ }
+ writer.interface.flush() catch return;
+ }
+}
+
+/// One request for the --fd loop: same locking discipline as the runner's
+/// handler (the reply bytes live in the harness's shared buffers).
+fn answer(req: fs.Req) tree.Answer {
+ harness_mem.mutex.lockUncancelable(harness_mem.io);
+ defer harness_mem.mutex.unlock(harness_mem.io);
+ return tree.handle(&harness_mem, req);
+}
+
+test {
+ _ = @import("tree.zig"); // the tree's unit tests (exclusions, ids, ...)
+}
+
+test "main: the root override parser pins each named root" {
+ // Parsing is inline in run(); the mapping itself is what the tests
+ // rely on, and it is exercised end to end in test/e2e.sh.
+ _ = std.meta.stringToEnum(tree.Root, "claude").?;
+ _ = std.meta.stringToEnum(tree.Root, "codex").?;
+ _ = std.meta.stringToEnum(tree.Root, "omp").?;
+ _ = std.meta.stringToEnum(tree.Root, "hermes").?;
+ _ = std.meta.stringToEnum(tree.Root, "dsh").?;
+ try std.testing.expect(std.meta.stringToEnum(tree.Root, "zmx") == null);
+}
diff --git a/9harness/src/tree.zig b/9harness/src/tree.zig
new file mode 100644
index 0000000..6525e58
--- /dev/null
+++ b/9harness/src/tree.zig
@@ -0,0 +1,1329 @@
+//! 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.
+//!
+//! /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;
+
+// ---- 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,
+
+ /// Directory entries of the root, in listing order.
+ pub const listed = [_]Top{ .pid, .uptime, .claude, .codex, .omp, .hermes, .dsh, .skills };
+
+ pub fn fileName(t: Top) []const u8 {
+ return @tagName(t);
+ }
+
+ pub fn dir(t: Top) bool {
+ return switch (t) {
+ .root, .claude, .codex, .omp, .hermes, .dsh, .skills => true,
+ .pid, .uptime => 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 };
+
+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) };
+}
+
+/// 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,
+
+ pub const InitOptions = struct {
+ io: Io,
+ pid: u32,
+ /// Base path of each root, or "" when the root is not pinned.
+ bases: [5][]const u8,
+ };
+
+ pub fn init(h: *Harness, o: InitOptions) void {
+ h.* = .{ .io = o.io, .pid = o.pid, .started_sec = Io.Timestamp.now(o.io, .real).toSeconds() };
+ 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 };
+ },
+ }
+ }
+
+ fn entryNode(h: *Harness, e: *Entry) u64 {
+ return nodeOf(e, h.slotOf(e));
+ }
+};
+
+pub const Target = union(enum) {
+ top: Top,
+ path: *Entry,
+};
+
+// ---- 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.
+fn statIn(h: *Harness, r: Root, rel_path: []const u8) ?Io.Dir.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;
+}
+
+fn statAttr(h: *Harness, e: *Entry) ?fs.Attr {
+ const st = statIn(h, e.root, e.rel()) orelse return null;
+ return .{
+ .name = basename(e.rel()),
+ .node = h.entryNode(e),
+ .dir = st.kind == .directory,
+ .size = if (st.kind == .directory) 0 else st.size,
+ .mode = if (st.kind == .directory) 0o555 else 0o444,
+ .mtime = @truncate(@as(u64, @bitCast(st.mtime.toSeconds()))),
+ };
+}
+
+fn basename(rel_path: []const u8) []const u8 {
+ if (std.mem.lastIndexOfScalar(u8, rel_path, '/')) |i| return rel_path[i + 1 ..];
+ return rel_path;
+}
+
+/// A fact's text, rendered on demand. `buf` should be a small stack buffer.
+fn factText(h: *Harness, fact: Top, buf: []u8) []const u8 {
+ 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 },
+ .pid, .uptime => 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 st = statIn(h, m.root, m.rel) orelse return null;
+ if (st.kind != .directory) return null;
+ return .{ .name = top.fileName(), .node = topNode(top), .dir = true, .mode = 0o555 };
+ },
+ }
+ },
+ .path => |e| return statAttr(h, e),
+ }
+}
+
+/// 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);
+}
+
+// ---- 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 => fail(req.tag, E.PERM),
+ .open => open(h, req, t),
+ .release => release(h, req, t),
+ .readdir => readdir(h, req, t),
+ .read => read(h, req, t),
+ .write => fail(req.tag, E.PERM),
+ };
+}
+
+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 => 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);
+ },
+ .pid, .uptime => 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);
+ },
+ }
+}
+
+/// 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) };
+ },
+ };
+ 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: any open that would write or truncate is refused.
+ const rw = req.omode & 3;
+ if (rw == cloud9.owrite or rw == cloud9.ordwr) return fail(req.tag, E.PERM);
+ if (req.omode & cloud9.otrunc != 0) return fail(req.tag, E.PERM);
+ return .{ .reply = .{ .tag = req.tag, .handle = 1 } };
+}
+
+fn release(h: *Harness, req: fs.Req, t: Target) Answer {
+ _ = h;
+ switch (t) {
+ .top => {},
+ .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 => {
+ const text = factText(h, top, &fact_buf);
+ return window(req, text);
+ },
+ else => return fail(req.tag, E.ISDIR),
+ },
+ .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 => return fail(req.tag, E.NOTDIR),
+ .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 fail(req.tag, E.NFILE),
+ }
+ },
+ },
+ .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 fail(req.tag, E.NFILE),
+ }
+ },
+ }
+ 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.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,
+ 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;
+ rig.harness_mem.init(.{ .io = io, .pid = 4242, .bases = bases });
+ 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 });
+ }
+};
+
+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{ "pid", "uptime", "claude", "codex", "omp", "hermes", "dsh", "skills" }) |name| {
+ try testing.expect(stageHas(listing.bytes, name));
+ }
+}
+
+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: 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 9harness 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);
+}
diff --git a/9harness/test/e2e.sh b/9harness/test/e2e.sh
new file mode 100755
index 0000000..6e57727
--- /dev/null
+++ b/9harness/test/e2e.sh
@@ -0,0 +1,266 @@
+#!/usr/bin/env bash
+# End-to-end suite for 9harness: the read-only, fresh-from-disk 9P view of
+# every harness's state.
+#
+# Usage: bash 9harness/test/e2e.sh <9harness> [<9ns>] (zig build 9harness-itest)
+#
+# Part A always runs, entirely on fixtures: a fake home with fixture roots
+# pinned by --root, a scratch XDG_RUNTIME_DIR registry, plan9port's 9p as
+# the client. It proves: the posted name is listed, the roots walk and
+# read, a transcript comes back byte-identical (cmp), credentials-shaped
+# files are unreachable, a file appended after the daemon started is
+# visible at once, writes answer EPERM, and --unix/--fd listen forms work.
+#
+# Part B (the money shot) runs against the REAL roots and the REAL
+# registry only when the `harness` name is free, and only reads: the posted
+# name in /run/user/<uid>/9p, 9p walks of the live ~/.claude, a real
+# transcript byte-identical through the tree, the 9ns --mntgen mount of
+# /mnt/9p/harness, and the live ~/.claude/.credentials.json unreachable.
+# It is skipped (not failed) when a live daemon already owns the name.
+#
+# Exit 0 on success (or when the machine cannot run a part), 1 on failure.
+set -u
+
+H9=$(realpath "${1:?path to 9harness}")
+HARNESS_TMP_XDG="${XDG_RUNTIME_DIR:-}"
+[ $# -ge 2 ] && [ -n "$2" ] && NS=$(realpath "$2")
+P9P=/usr/lib/plan9/bin/9p
+TMP=$(mktemp -d "${TMPDIR:-/tmp}/9harness.XXXXXX")
+PIDS=()
+FAILED=0
+PASSED=0
+
+cleanup() {
+ for p in "${PIDS[@]:-}"; do [ -n "$p" ] && kill "$p" 2>/dev/null; done
+ rm -rf "$TMP"
+}
+trap cleanup EXIT
+
+pass() { PASSED=$((PASSED + 1)); echo "ok - $1"; }
+fail() { FAILED=$((FAILED + 1)); echo "FAIL - $1"; shift; [ $# -gt 0 ] && printf ' %s\n' "$@"; }
+expect_eq() { # name expected actual
+ if [ "$2" = "$3" ]; then pass "$1"; else fail "$1" "expected: $(printf %q "$2")" "actual: $(printf %q "$3")"; fi
+}
+expect_contains() { # name needle haystack
+ case "$3" in *"$2"*) pass "$1" ;; *) fail "$1" "missing: $(printf %q "$2")" "in: $(printf %q "$3")" ;; esac
+}
+expect_missing() { # name needle haystack
+ case "$3" in *"$2"*) fail "$1" "found: $(printf %q "$2")" "in: $(printf %q "$3")" ;; *) pass "$1" ;; esac
+}
+
+if [ ! -x "$P9P" ]; then
+ echo "SKIP: plan9port 9p not at $P9P"; exit 0
+fi
+if ! unshare -Urm true 2>/dev/null; then
+ echo "SKIP: unprivileged user namespaces unavailable"; exit 0
+fi
+
+start_daemon() { # args... -> sets DAEMON_PID, logs to $TMP/daemon.log
+ "$H9" "$@" >"$TMP/daemon.log" 2>&1 &
+ DAEMON_PID=$!
+ PIDS+=("$DAEMON_PID")
+}
+wait_posted() { # socket path
+ for _ in $(seq 1 100); do [ -S "$1" ] && return 0; sleep 0.05; done
+ return 1
+}
+p9() { "$P9P" -a "unix!$1" "${@:2}"; }
+
+# ============================================================================
+echo "# part A: fixture roots, scratch registry"
+# ============================================================================
+export XDG_RUNTIME_DIR="$TMP"
+REG="$TMP/9p"
+FX="$TMP/home"
+mkdir -p "$FX"
+
+# The fixture home: five roots, a real-shaped claude project with a
+# transcript, credentials-shaped files at several depths, codex sessions,
+# an omp agent dir, hermes logs and dsh profiles.
+mkfile() { mkdir -p "$(dirname "$1")"; printf '%s' "$2" > "$1"; }
+mkfile "$FX/claude/projects/-tmp-proj/session-abc.jsonl" '{"type":"user","message":"first line"}
+{"type":"assistant","message":"second line"}
+'
+mkfile "$FX/claude/projects/-tmp-proj/.credentials.json" 'STAY OUT'
+mkfile "$FX/claude/projects/-tmp-proj/auth.json" 'STAY OUT'
+mkfile "$FX/claude/projects/-tmp-proj/token.bin" 'STAY OUT'
+mkfile "$FX/claude/history.jsonl" '{"display":"claude prompt one"}
+{"display":"claude prompt two"}
+'
+mkdir -p "$FX/claude/skills/revu"
+mkfile "$FX/claude/skills/revu/SKILL.md" '# revu skill'
+mkfile "$FX/codex/sessions/2026/09/21/rollout-x.jsonl" '{"session":"codex one"}
+'
+mkfile "$FX/codex/session_index.jsonl" '{"id":"x"}
+'
+mkfile "$FX/codex/history.jsonl" '{"text":"codex prompt"}
+'
+mkfile "$FX/omp/agent/history.db" 'fake-history-db'
+mkfile "$FX/omp/agent/config.yml" 'STAY OUT'
+mkfile "$FX/hermes/auth.json" 'STAY OUT'
+mkfile "$FX/hermes/logs/app.log" 'hermes log line
+'
+mkfile "$FX/hermes/state.db" 'fake-hermes-state'
+mkfile "$FX/dsh/profiles/p1.yaml" 'name: p1'
+mkfile "$FX/dsh/.credentials.yaml" 'STAY OUT'
+
+start_daemon --root "claude=$FX/claude" --root "codex=$FX/codex" \
+ --root "omp=$FX/omp" --root "hermes=$FX/hermes" --root "dsh=$FX/dsh" \
+ --name harness
+wait_posted "$REG/harness" || { fail "the daemon posts as harness" "$(cat "$TMP/daemon.log")"; exit 1; }
+
+# 1. The posted name is listed.
+expect_contains "posted name listed in the registry" harness "$(ls "$REG")"
+
+# 2. The roots walk; a real transcript reads back byte-identical.
+expect_contains "ls / shows the roots" claude "$(p9 "$REG/harness" ls /)"
+expect_contains "ls / shows codex" codex "$(p9 "$REG/harness" ls /)"
+expect_contains "ls / shows skills" skills "$(p9 "$REG/harness" ls /)"
+p9 "$REG/harness" read /claude/projects/-tmp-proj/session-abc.jsonl > "$TMP/via-9p.jsonl" 2>"$TMP/read.err" \
+ || fail "transcript read through the tree" "$(cat "$TMP/read.err")"
+cmp -s "$TMP/via-9p.jsonl" "$FX/claude/projects/-tmp-proj/session-abc.jsonl" \
+ && pass "transcript byte-identical through the tree (cmp)" \
+ || fail "transcript byte-identical through the tree (cmp)" "differs"
+expect_eq "history file reads" "$(cat "$FX/claude/history.jsonl")" "$(p9 "$REG/harness" read /claude/history)"
+expect_eq "skills union mirrors the harness tree" "$(cat "$FX/claude/skills/revu/SKILL.md")" \
+ "$(p9 "$REG/harness" read /skills/claude/revu/SKILL.md)"
+
+# 3. A credentials-shaped file is unreachable anywhere in the tree.
+PROJ_LS=$(p9 "$REG/harness" ls /claude/projects/-tmp-proj)
+for bad in .credentials.json auth.json token.bin; do
+ expect_missing "credentials-shaped $bad not listed" "$bad" "$PROJ_LS"
+ p9 "$REG/harness" read "/claude/projects/-tmp-proj/$bad" >/dev/null 2>&1 \
+ && fail "credentials-shaped $bad unreachable" "read succeeded" \
+ || pass "credentials-shaped $bad unreachable"
+done
+expect_missing "hermes auth.json not listed" auth.json "$(p9 "$REG/harness" ls /hermes)"
+expect_missing "omp config.yml not listed" config.yml "$(p9 "$REG/harness" ls /omp)"
+expect_missing "dsh .credentials.yaml not listed" credentials.yaml "$(p9 "$REG/harness" ls /dsh)"
+sleep 0.3
+expect_contains "hermes logs still served" logs "$(p9 "$REG/harness" ls /hermes)"
+
+# 3b. A file at the very top of a mirror root: its relative path is the
+# bare name, the one case a join onto an empty directory path gets wrong.
+expect_contains "a file at the top of a mirror root is listed" state.db "$(p9 "$REG/harness" ls /hermes)"
+expect_eq "a file at the top of a mirror root reads back" "$(cat "$FX/hermes/state.db")" \
+ "$(p9 "$REG/harness" read /hermes/state.db)"
+
+# 3c. A directory bigger than the listing caps fails loudly. A short
+# listing is indistinguishable from a small directory, so the daemon must
+# never answer one: the read errors and the client sees it.
+mkdir -p "$FX/dsh/wide"
+seq 1 1100 | while read -r i; do : > "$FX/dsh/wide/f$(printf %05d "$i")"; done
+if p9 "$REG/harness" ls /dsh/wide > "$TMP/wide.out" 2>"$TMP/wide.err"; then
+ fail "an over-cap directory fails instead of truncating" "listed $(wc -l < "$TMP/wide.out") entries"
+else
+ pass "an over-cap directory fails instead of truncating ($(head -c 80 "$TMP/wide.err"))"
+fi
+rm -rf "$FX/dsh/wide"
+
+# 4. Live visibility: a file appended after the daemon started.
+printf '{"type":"assistant","message":"third line"}\n' >> "$FX/claude/projects/-tmp-proj/session-abc.jsonl"
+expect_eq "append after start is visible immediately" \
+ "$(cat "$FX/claude/projects/-tmp-proj/session-abc.jsonl")" \
+ "$(p9 "$REG/harness" read /claude/projects/-tmp-proj/session-abc.jsonl)"
+mkdir -p "$FX/claude/projects/-tmp-proj2"
+mkfile "$FX/claude/projects/-tmp-proj2/session-new.jsonl" '{"new":true}
+'
+expect_contains "new session dir appears at once" -tmp-proj2 "$(p9 "$REG/harness" ls /claude/projects)"
+
+# 5. The facts and the write refusal.
+DAEMON_PID_TEXT=$(p9 "$REG/harness" read /pid)
+expect_eq "pid fact answers the daemon's pid" "$DAEMON_PID" "$DAEMON_PID_TEXT"
+[ -n "$(p9 "$REG/harness" read /uptime)" ] && pass "uptime fact answers" || fail "uptime fact answers" "empty"
+printf 'x' | p9 "$REG/harness" write /pid >/dev/null 2>&1 \
+ && fail "write answers EPERM" "write succeeded" \
+ || pass "write answers EPERM"
+
+# 6. The --unix listen form beside the post.
+"$H9" --unix "$TMP/plain.sock" --no-post --root "claude=$FX/claude" >"$TMP/d2.log" 2>&1 &
+PIDS+=($!)
+for _ in $(seq 1 100); do [ -S "$TMP/plain.sock" ] && break; sleep 0.05; done
+expect_eq "--unix listen form serves" "$(cat "$FX/claude/history.jsonl")" "$(p9 "$TMP/plain.sock" read /claude/history)"
+
+# 7. The --fd listen form: one 9P session over a connected stream fd
+# (the 9ns --spawn / socket-activation shape), driven through a
+# socketpair: the daemon sees EOF when both ends close and exits.
+if command -v python3 >/dev/null; then
+ python3 - "$H9" "$FX" <<'EOF'
+import os, socket, subprocess, sys
+h9, fx = sys.argv[1], sys.argv[2]
+a, b = socket.socketpair()
+pid = os.fork()
+if pid == 0:
+ a.close()
+ os.dup2(b.fileno(), 7)
+ os.execv(h9, [h9, "--fd", "7", "--root", f"claude={fx}/claude"])
+b.close()
+a.close()
+os.waitpid(pid, 0)
+EOF
+ pass "--fd listen form runs a session and exits on hangup"
+else
+ echo "skip - --fd form: python3 not available"
+fi
+
+kill "$DAEMON_PID" 2>/dev/null
+wait "$DAEMON_PID" 2>/dev/null
+sleep 0.2
+[ -S "$REG/harness" ] && fail "SIGTERM unposts the name" "socket still there" || pass "SIGTERM unposts the name"
+
+# ============================================================================
+echo "# part B: the real roots, the real registry (read-only)"
+# ============================================================================
+export XDG_RUNTIME_DIR="${HARNESS_TMP_XDG:-/run/user/$(id -u)}"
+REAL_REG="$XDG_RUNTIME_DIR/9p"
+REAL_SOCKET="$REAL_REG/harness"
+if [ -e "$REAL_SOCKET" ]; then
+ echo "SKIP: a live daemon already owns the posted name `harness`"
+else
+ HOME_DIR=$(getent passwd "$(id -u)" | cut -d: -f6)
+ start_daemon --name harness
+ if wait_posted "$REAL_SOCKET"; then
+ expect_contains "posted name listed in the real registry" harness "$(ls "$REAL_REG")"
+ expect_contains "ls / shows the claude root" claude "$(p9 "$REAL_SOCKET" ls /)"
+ # A real transcript, byte-identical through the tree.
+ REAL_T=$(ls "$HOME_DIR/.claude/projects"/*/*.jsonl 2>/dev/null | head -n 1 || true)
+ if [ -n "$REAL_T" ]; then
+ REL="${REAL_T#"$HOME_DIR/.claude/projects/"}"
+ p9 "$REAL_SOCKET" read "/claude/projects/$REL" > "$TMP/real-via-9p" 2>/dev/null \
+ && cmp -s "$TMP/real-via-9p" "$REAL_T" \
+ && pass "real transcript byte-identical through the tree" \
+ || fail "real transcript byte-identical through the tree" "$REAL_T"
+ else
+ echo "skip - no real claude transcript found"
+ fi
+ # The credentials at ~/.claude are unreachable: no mount serves the
+ # root dir, and the exclusion rule holds everywhere else.
+ p9 "$REAL_SOCKET" read /claude/.credentials.json >/dev/null 2>&1 \
+ && fail "live .credentials.json unreachable" "read succeeded" \
+ || pass "live .credentials.json unreachable"
+ CLAUDE_LS=$(p9 "$REAL_SOCKET" ls /claude)
+ expect_missing "no credentials leaked into /claude" credentials "$CLAUDE_LS"
+ # The money shot: the mntgen mount every interactive fish sees.
+ if [ -n "$NS" ]; then
+ OUT=$(timeout 60 "$NS" --mntgen -- sh -c 'ls /mnt/9p/harness && head -c 200 /mnt/9p/harness/claude/history' 2>"$TMP/mntgen.err")
+ RC=$?
+ if [ $RC -eq 0 ]; then
+ expect_contains "mntgen mount lists the harness tree" claude "$OUT"
+ expect_contains "mntgen mount reads claude/history" claude "$OUT"
+ else
+ fail "mntgen money shot (exit $RC)" "$(cat "$TMP/mntgen.err")"
+ fi
+ else
+ echo "skip - mntgen money shot: 9ns not provided"
+ fi
+ kill "$DAEMON_PID" 2>/dev/null
+ wait "$DAEMON_PID" 2>/dev/null
+ sleep 0.2
+ [ -S "$REAL_SOCKET" ] && fail "stop unposts the real name" "socket still there" || pass "stop unposts the real name"
+ else
+ fail "daemon posts into the real registry" "$(cat "$TMP/daemon.log")"
+ fi
+fi
+
+echo "# $PASSED passed, $FAILED failed"
+[ "$FAILED" -eq 0 ]
diff --git a/build.zig b/build.zig
index 334da9f..795d8e2 100644
--- a/build.zig
+++ b/build.zig
@@ -156,6 +156,7 @@ pub fn build(b: *std.Build) void {
const is_linux = target.result.os.tag == .linux;
const want_9proc = b.option(bool, "9proc", "Build the 9proc library module and demo (default: target is Linux)") orelse is_linux;
const want_9ns = b.option(bool, "9ns", "Build the 9ns FUSE mount CLI (default: target is Linux; Linux only)") orelse is_linux;
+ const want_9harness = b.option(bool, "9harness", "Build the harness fs daemon (default: target is Linux; Linux only)") orelse is_linux;
if (want_9ns and !is_linux) {
std.debug.print("error: -D9ns=true needs a Linux target (got {s})\n", .{@tagName(target.result.os.tag)});
std.process.exit(1);
@@ -206,6 +207,25 @@ pub fn build(b: *std.Build) void {
programs_itest.dependOn(p.itest_step);
}
+ // The harness fs daemon: a read-only 9P view of every harness's
+ // state, posted by default as `harness` (Linux only: it posts into
+ // $XDG_RUNTIME_DIR/9p through cloud9.post).
+ if (want_9harness and !is_linux) {
+ std.debug.print("error: -D9harness=true needs a Linux target (got {s})\n", .{@tagName(target.result.os.tag)});
+ std.process.exit(1);
+ }
+ const harness_build = @import("9harness/build.zig");
+ const harness: ?harness_build.Artifacts = if (want_9harness) harness_build.add(b, .{
+ .target = target,
+ .optimize = optimize,
+ .cloud9 = module,
+ .ns = if (ns) |p| p.exe else null,
+ }) else null;
+ if (harness) |p| {
+ programs_test.dependOn(p.test_step);
+ programs_itest.dependOn(p.itest_step);
+ }
+
// 9proc's end-to-end suites drive the demo through a 9ns mount,
// so they are wired once both fragments have run.
if (proc) |i| {