diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-21 14:23:27 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-21 15:20:40 -0300 |
| commit | b4db588dd5b92d647b661c2dc17b40925af92348 (patch) | |
| tree | 7a036e500251d7f3f958854dcf3a8ead375bbc63 /9harness/src | |
| parent | 0d7e295efee1fca0935cf4a8bee9629c007dd2b6 (diff) | |
| download | cloud9-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.
Diffstat (limited to '9harness/src')
| -rw-r--r-- | 9harness/src/main.zig | 322 | ||||
| -rw-r--r-- | 9harness/src/tree.zig | 1329 |
2 files changed, 1651 insertions, 0 deletions
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); +} |
