summaryrefslogtreecommitdiff
path: root/9harness/src/tree.zig
Commit message (Collapse)AuthorAge
* Rename 9harness to 9agents; README, file-backed qids, worded errorsGabriel Schneider6 days
| | | | | | - 9harness/ becomes 9agents/ (build option -D9agents, package paths). - 9agents serves /README, reports qid paths from the file's (dev, ino) and qid versions that move with the file, and names its refusals.
* 9harness: /active, and zmxify as a write to a fileGabriel Schneider10 days
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The mirror answers what files exist. /active answers what is running: one directory per live agent, normalized across harnesses, fields as small text files, synthesized per request. /active/claude/345104/{pid,cwd,session,via,name,status,title, model,started,zmx,transcript,agents/} This is /proc's shape, and deliberately: a directory per object named by pid under a directory per harness, rather than a compound `claude-345104` that would make you parse a name to recover a field that is already the directory above it. There is no `updated` file — that is the mtime of `transcript`, which stat already carries. Each harness is asked in its own terms, and the route is reported in `via` so a wrong guess is visible rather than silent. Claude Code publishes sessions/<pid>.json itself, with procStart as a pid-reuse guard, so nothing there is guessed. omp and dsh are found by the transcript they hold open, omp falling back to the store named after its cwd. codex's rollout file carries the session id in its *name*, so its sqlite is never opened. hermes is the one gap and needs none: its sessions live only in sqlite, and the only hermes processes that run are the gateway and the dashboard, which are not sessions. Liveness is /proc/<pid> plus a matching start time: a pid alone is not an identity. The daemon never lists its own ancestry, so it cannot show or act on the tree serving the request. The write path, and why it is a file and not a ctl: writing a zmx session name into an agent's `zmx` moves it there. The file means which zmx session this agent lives in, and writing makes that true. A ctl taking verbs is the ordinary Plan 9 spelling, and an executable script served in the tree is the spelling zmx's own `attach` uses, but a script that shells out to a local binary lies over a remote mount — it would run against a session that is not on the client's machine. A write is served where the authority is. Every refusal comes before anything is destroyed: the name must be zmx's label charset, unused by a live session, and the agent's session must have resolved, because nothing is killed that has nowhere to come back to. The command is fixed per harness and no client byte reaches exec. It is off unless --allow-move: this is the one place the tree is not read-only, and anything that can mount it could otherwise kill an agent. 9harness/zmxify replaces the 307-line rc script. It parses no /proc, opens no fd table and queries no database; it lists /active, offers the rows to fzf and writes the chosen name. It no longer excludes the caller's own session, which the old one had to: that script did the killing itself, so killing its own parent lost the session it was rescuing. The daemon completes the kill and the re-exec whether or not the client is still connected — verified by hanging up immediately after sending the write — so zmxifying the terminal you are sitting in now works, which is the common case. --proc DIR is the fixture seam: the scan, the liveness guard, the exclusions and the ancestry rule are unit-tested against a fake process tree, never the live one. Suites: 87/87 root, 48/48 9ns, 26/26 9harness (+5 for the view), 60/60 9proc, 144/144 programs-test, 51+88 9ns integration, 44/0 9harness end-to-end (+16, including a real move against a fake harness and a fake zmx), 29/0 9proc debug, 213/0 9ns adversarial, freestanding green.
* 9harness: the harness fs daemonGabriel Schneider11 days
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.