diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-21 16:49:20 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-21 16:49:20 -0300 |
| commit | dddd556accea6b6ea7802cd3f622f8b3cf8eb43f (patch) | |
| tree | 64a1cc34d44f9be6ec266d852fdef6f5b842d004 /9harness/test/e2e.sh | |
| parent | b4db588dd5b92d647b661c2dc17b40925af92348 (diff) | |
| download | cloud9-dddd556accea6b6ea7802cd3f622f8b3cf8eb43f.tar.gz cloud9-dddd556accea6b6ea7802cd3f622f8b3cf8eb43f.zip | |
9harness: /active, and zmxify as a write to a file
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.
Diffstat (limited to '9harness/test/e2e.sh')
| -rwxr-xr-x | 9harness/test/e2e.sh | 81 |
1 files changed, 81 insertions, 0 deletions
diff --git a/9harness/test/e2e.sh b/9harness/test/e2e.sh index 6e57727..254d999 100755 --- a/9harness/test/e2e.sh +++ b/9harness/test/e2e.sh @@ -209,6 +209,87 @@ sleep 0.2 [ -S "$REG/harness" ] && fail "SIGTERM unposts the name" "socket still there" || pass "SIGTERM unposts the name" # ============================================================================ +echo "# part C: /active, the derived view" +# ============================================================================ +# A fake agent: a process whose argv[0] basename is a harness name, with a +# session record Claude Code's own shape. The proc root is the real /proc +# (the fixture seam is unit-tested); nothing real is ever killed, because +# the only process this part touches is the sleep it started itself. +ACT="$TMP/act" +mkdir -p "$ACT/bin" "$ACT/home/.claude/sessions" "$ACT/home/.claude/projects" "$ACT/rt/9p/zmx" +cp /bin/sleep "$ACT/bin/claude" +# A zmx that records how it was called and posts the name, as the real one +# does; a move must never be tested against the user's live sessions. +cat > "$ACT/bin/zmx" <<ZEOF +#!/bin/sh +echo "\$@" > "$ACT/zmx-argv" +: > "$ACT/rt/9p/zmx/\$2" +exit 0 +ZEOF +chmod +x "$ACT/bin/zmx" + +"$ACT/bin/claude" 600 & +AGENT=$! +PIDS+=("$AGENT") +sleep 0.3 +# Field 22 of /proc/<pid>/stat, counted from the last ')' — the executable +# name can hold spaces and parentheses, so the line is never just split. +AGENT_START=$(sed 's/.*) //' "/proc/$AGENT/stat" | awk '{print $20}') +AGENT_CWD=$(readlink "/proc/$AGENT/cwd") +printf '{"pid":%d,"sessionId":"sess-e2e","cwd":"%s","name":"e2e-agent","status":"idle","procStart":"%s"}\n' \ + "$AGENT" "$AGENT_CWD" "$AGENT_START" > "$ACT/home/.claude/sessions/$AGENT.json" + +act_roots() { + echo --root "claude=$ACT/home/.claude" --root "codex=$ACT/home/.codex" \ + --root "omp=$ACT/home/.omp" --root "hermes=$ACT/home/.hermes" --root "dsh=$ACT/home/.dsh" +} + +# First: the read-only default. No --allow-move, so zmx cannot be written. +XDG_RUNTIME_DIR="$ACT/rt" "$H9" --no-post --unix "$ACT/ro.sock" $(act_roots) >"$ACT/ro.log" 2>&1 & +PIDS+=("$!") +wait_posted "$ACT/ro.sock" || { fail "read-only daemon listens" "$(cat "$ACT/ro.log")"; } +expect_contains "the agent lists under its harness" "$AGENT" "$(p9 "$ACT/ro.sock" ls /active/claude)" +expect_eq "its session comes from the harness's own record" "sess-e2e" "$(p9 "$ACT/ro.sock" read "/active/claude/$AGENT/session")" +expect_eq "the route taken is named" "registry" "$(p9 "$ACT/ro.sock" read "/active/claude/$AGENT/via")" +expect_eq "the harness's own name is served" "e2e-agent" "$(p9 "$ACT/ro.sock" read "/active/claude/$AGENT/name")" +expect_eq "the harness's own status is served" "idle" "$(p9 "$ACT/ro.sock" read "/active/claude/$AGENT/status")" +if echo "nope" | p9 "$ACT/ro.sock" write "/active/claude/$AGENT/zmx" 2>/dev/null; then + fail "without --allow-move the tree stays read-only" "the write was accepted" +else + pass "without --allow-move the tree stays read-only" +fi +expect_eq "a refused move leaves the agent running" "yes" "$([ -d "/proc/$AGENT" ] && echo yes)" + +# Now a daemon that allows moves. +XDG_RUNTIME_DIR="$ACT/rt" "$H9" --no-post --unix "$ACT/rw.sock" $(act_roots) \ + --allow-move --zmx "$ACT/bin/zmx" >"$ACT/rw.log" 2>&1 & +PIDS+=("$!") +wait_posted "$ACT/rw.sock" || { fail "move-allowing daemon listens" "$(cat "$ACT/rw.log")"; } + +# Every refusal must come before anything is destroyed. +for bad in "has space" "../escape" "semi;colon"; do + echo "$bad" | p9 "$ACT/rw.sock" write "/active/claude/$AGENT/zmx" 2>/dev/null + if [ $? -eq 0 ]; then fail "an illegal zmx name is refused ($bad)"; else pass "an illegal zmx name is refused ($bad)"; fi +done +: > "$ACT/rt/9p/zmx/occupied" +echo "occupied" | p9 "$ACT/rw.sock" write "/active/claude/$AGENT/zmx" 2>/dev/null \ + && fail "a name a live session holds is refused" || pass "a name a live session holds is refused" +expect_eq "no refusal killed the agent" "yes" "$([ -d "/proc/$AGENT" ] && echo yes)" + +# The move itself. +if echo "e2e-moved" | p9 "$ACT/rw.sock" write "/active/claude/$AGENT/zmx" 2>"$ACT/move.err"; then + pass "a legal name moves the agent" +else + fail "a legal name moves the agent" "$(cat "$ACT/move.err")" +fi +sleep 0.4 +expect_eq "the agent it replaced is gone" "gone" "$([ -d "/proc/$AGENT" ] || echo gone)" +# The command is fixed by the harness: the client supplied only the name. +expect_eq "zmx ran the harness with its session resumed" \ + "run e2e-moved -d claude --resume sess-e2e" "$(cat "$ACT/zmx-argv")" +expect_missing "no client byte reached the command" "e2e-moved -d claude --resume sess-e2e;" "$(cat "$ACT/zmx-argv")" + +# ============================================================================ echo "# part B: the real roots, the real registry (read-only)" # ============================================================================ export XDG_RUNTIME_DIR="${HARNESS_TMP_XDG:-/run/user/$(id -u)}" |
