# cloud9 integration The published `cloud9` package owns the base 9P2000 wire format, client and server connections, and TCP/Unix/QUIC transports. `build.zig.zon` pins a commit from `git@git.sr.ht:~gbrls/cloud9`, so `zig build` fetches it into `zig-pkg/` like every other dependency; no sibling checkout is required. Re-pin with `zig fetch --save=cloud9 git+https://git.sr.ht/~gbrls/cloud9#`, and swap in `.cloud9 = .{ .path = "../cloud9" }` while editing both packages at once. The file-server engine (fids, jobs, parking, flush, hangup) is cloud9's `fs.Server`; the control tree in `src/ninep/` is its backend, using cloud9's `fs.Req`, `fs.Op`, `fs.Status`, `fs.E` and `fs.ReplyWith` (extended with the editor's reply payload locator). `src/9p.zig` names the editor's and the board's `fs.Options` and re-exports the wire names the transports use. Mounting, Unix namespace discovery and permissions, the editor event loop, connection limits, and exported tree policy remain here. The Unix and TCP listeners run on cloud9's `serve.Runner` (`std.Io`: an accept task per listener, a reader and a writer task per connection, four slots); its handler answers every backend request on the connection's task, taking the editor's turn with the core (`pardes.turn`) while the editor waits for input or is out in a syscall. A request that would change a pane while the editor is mid-step is parked with `Status.again` -- the engine parks reads, writes, opens, truncations, clunks and removes -- and retried by `Runner.wakeAll` when the editor next rests. A read with nothing yet parks the same way, but is never retried for news: the core holds it (`ctlfs.hold`) and `answerHeld` answers it on its connection through `Conn.reply`'s engine, under the connection's lock and only while the engine still holds that tag parked, since cloud9 does not tell the backend about a Tflush. `src/9p_quic.zig` selects the existing `pardes-9p` ALPN for cloud9's optional OpenSSL transport; QUIC still runs on the poll loop in `src/9p_io.zig` on the editor's thread, since cloud9's QUIC adapter is nonblocking-descriptor based rather than `std.Io` based. The standalone protocol/GPIO tests import the same module. The separate `05-zig-p4` build also supplies cloud9 for the GPIO firmware entry. Run `zig build 9p-test` for the engine configurations and `zig build 9p-io-test -Dquic=true` for native transport/client integration. Run cloud9's `zig build test`, `transport-test`, `quic-test -Dquic=true`, `fuzz`, and `differential` steps for the shared implementation. See cloud9's `docs/validation.md` for recorded runs and known test-environment limits. Invalid framing now terminates a server connection. Cloud9 also checks reply counts and reserves tags until flush completion. Client metadata is slightly larger to track those reservations, and its bounds tests reflect that fixed cost. ## The posted-9P registry `$XDG_RUNTIME_DIR/9p` is this machine's `/srv`: a server posts itself in it under a name, and clients dial names rather than paths. cloud9 owns both sides (`cloud9.post`), and `9ns --mntgen` mounts the whole registry at `/mnt/9p` for programs that want it as a filesystem. pardes's only part in it is to put itself there. **Serving.** A listening editor advertises itself at `$XDG_RUNTIME_DIR/9p/pardes/`, a symlink to the socket it already binds. One directory for the program, one entry per editor, so several editors group instead of crowding the registry root — the layout zmx posts its sessions under. The socket itself does not move: adopting the registry only advertises. Only the runtime-directory socket posts; an instance that fell back to `~/.local/state/pardes` stays out of the user's registry, the way a private `ZMX_DIR` does for zmx. Stopping unposts, and only while the entry is still ours, so a name another editor has since claimed is never unlinked. An exit that cannot run any code of its own — an aborted test, a kill, a crash — leaves its entry and its socket behind, so posting first sweeps the group: every entry that is a symlink and whose socket answers a connect with a definite ECONNREFUSED is unlinked, along with the socket it points at when a `stat` agrees that is a socket of ours. Anything that is not a symlink is somebody else's, and any other answer — connected, busy, refused permission, a surprise — counts as live, because uncertainty belongs to the server that owns the socket rather than to the sweeper. That is `cloud9.post.Probe`'s classification, repeated in `src/9p_io.zig` only because `post.probe` is raw Linux syscalls and pardes also builds for darwin. **Consuming.** Nothing. `9ns --mntgen` mounts the whole registry at `/mnt/9p`, and an interactive fish already self-wraps in one, so a pardes started from a terminal sees every posted service as ordinary files — `/mnt/9p/harness/active/...` is read with the same code that reads any other path. Teaching pardes to dial the registry itself would put discovery in a second place for no gain: mounting is the client's job and 9ns is the client. `--mount==` keeps meaning exactly what it always did, and a dial keeps resolving exactly as it always did — a bare name is another pardes session, and anything with a slash is a path, relative ones included. The one case that is not free: a pardes started outside a mntgen mount has no `/mnt/9p`. That is 9ns's problem to solve — by being in the namespace — not a reason for pardes to carry its own registry client. ### What this diverges from, deliberately * **pardes binds its own socket; it does not post through `cloud9.post`.** `post` would give us its hardened claim protocol (temp-bind plus atomic rename under a lock) instead of the stale-socket retry in `listen`, but it claims *flat* names only: `legalName` rejects `/`, and `claimName` derives its lock directory by stripping `/9p` from the registry path, so a name inside a subdirectory cannot go through it. zmx hand-rolls the same symlink for the same reason. Unifying them means teaching `post` a group — passing the lock directory in rather than deriving it — and that is a change to adversarially-hardened code, not a rename. * **The registry entry is a symlink, not the socket.** A reader that expects every registry entry to be a socket must `stat` following symlinks. `9ns --mntgen` and `cloud9.post.dial` both do. * **Dialing is untouched.** An earlier draft taught `resolve` to fall back to the registry for a bare name and to read `/` as a subdirectory entry. Both were reverted: the second reinterpreted relative dials, which are a feature, and the first duplicated what 9ns already does. `src/9p_io.zig`'s `resolve` is byte-identical to what it was.