Login
ocx-rootfs
Login

Two root folders built from a BQ2 seed, so that no agent OCX runs shares a sandbox with the repositories: the public server, which runs Fossil and the CGI programs and starts no agent, and the client, which runs the ocx2 daemon and reaches the server only over HTTPS. Both start with serve, and carry the same hosts.json; the hostname each is started with picks its entry. The server keeps the machine's name. The client is x86-1.

Repositories, certificates, Git mirrors, passwords and sign-ins are data, not build output: no build puts them in a folder.

Build

Each input is checked against its pin, and everything compiles inside the seed with no network. A step that finished is not repeated.

crystal run build-rootfs.cr -- server OUTPUT BQ2 DOWNLOADS
crystal run build-rootfs.cr -- client OUTPUT BQ2 DOWNLOADS

OUTPUT is the new folder, BQ2 a bq2 built from the seed's own version, and DOWNLOADS where inputs are kept. It must already hold clones, made over HTTPS, named ollama.fossil, gp-crystal.fossil and crystal-robots.fossil.

Compare two builds, by kind, mode, link target and file hash:

crystal run compare-rootfs.cr -- FOLDER FOLDER

Start

The server:

bq2 sysroot-namespace --rootfs=SERVER --bind=REPOSITORIES:/var/lib/fossil --bind=MIRRORS:/var/lib/ollama-codex/git-mirror -- /usr/local/libexec/ocx/serve

The client holds its name in its /etc/hostname. A folder can hold the name but not set it: the hostname belongs to the kernel, and the machine's cannot be changed from inside. So the start command makes a hostname space of its own and applies the name there:

bq2 sysroot-namespace --rootfs=CLIENT -- /usr/bin/unshare --uts /bin/sh -c 'hostname -F /etc/hostname && exec /usr/local/libexec/ocx/serve'

The client's daemon ends when the server cannot be reached, and nothing restarts it (failures, item 20). Running sessions go on. Start it again with confine daemon in place of serve above, not by restarting a service whose cgroup holds the sessions.

Test

test-rootfs.cr runs both folders together on copies of repositories, in a sealed network and process list, with the real settings and certificate. It checks each page against pages.txt, a push, an approved login, a run sent from the server and reported back from the client, and a session across a server restart. Then it rehearses the release route on two stage runs, as "What shows it works", below, says.

The rehearsal, on the host

A Claude session on the host, outside both root folders, can run it; the maintainer only starts that session. Nothing here touches the live server or client: every write is to copies.

  1. Two stage runs. From a session on the branch, send the stage pattern for two pushed check-ins, each --of the check-in's full 64-digit hash (fossil info prints only 40, and import then refuses the run). Both must be at or after 0388e725f3, whose build does not hold CI's pace limit; earlier ones crossed it on x86-1. Wait for both runs' ended, exit 0. Their shared files stay on the server until someone imports or removes them, so the copies below hold them.
  2. The paths. ps -eo args | grep [s]ysroot-namespace shows the two start commands of "Start", above: the program before sysroot-namespace is BQ2, the server's --rootfs is SERVER and its two --bind sources are the repositories and the git exports; the other --rootfs is CLIENT.
  3. The copies, into a new folder COPY on the same disk: cp -a SERVER COPY/server, cp -a CLIENT COPY/client, cp -a MIRRORS COPY/data/mirror, and each repository with sqlite3 REPOSITORIES/NAME.fossil ".backup COPY/data/repos/NAME.fossil", so a repository being written is still copied whole.
  4. The run, from a checkout of the branch, in ocx-rootfs/. The maintainer has no sudo, so the namespace maps the account to root:

    crystal build test-rootfs.cr -o COPY/test-rootfs
    unshare --map-root-user --net --pid --fork --mount --mount-proc COPY/test-rootfs COPY/server COPY/client BQ2 COPY/data COPY/work FIRST NEXT
    

FIRST and NEXT are the two stage runs' full hashes, FIRST the older check-in. It prints one line for each check, and ends with ALL PASSED or the checks that failed. Post that output on ticket 8986816c0b.

  1. Afterwards, remove COPY. The stage runs' shared files stay on the live server until the real import.

It fails one check on copies of today's repositories: a push is not exported when Fossil's background process follows a request under /ext. That process holds the CGI environment, so %F git export runs Fossil as a CGI, which answers "no such file: git". With the hook %F git export --nocgi --if-mirrored -R %R, that check passes.

Releasing from clients to servers (planned, not built)

Planned on ticket 8986816c0b, for the maintainer's approval, and revised after Codex's review there. Today a release moves from where it is built to where it runs only by hand. The aim: a client builds each next ordinary release of OCX, the public server takes it, tests it, accepts it and can roll it back, and the client then runs what the server accepted.

The first round is OCX alone, on x86_64: the server and x86-1. Other projects, and aarch64, come after that round trip works (below).

What a project says it releases

A project opts in through its ci.cr. One that says nothing gets nothing.

ci.cr never publishes. The project does not say where it runs: when a project is added on a server, project setup asks whether that server serves its CGI.

Who counts as a builder

A release counts only from a build that ran the known stage pattern, on a clone the server trusts to build:

This trusts the reviewed source and the builder's host; no hash can show that hostile source ran honest tests.

The steps

A person runs each step after the build, and can run one again or fix a link by hand if it is cut short; the steps add nothing to make that automatic.

  1. A client builds. A stage run, sent to a builder, runs ci.cr -- build on its check-in in its sandbox, which holds no password, and names the built files in unversioned_files as releases/$$run$$/FILE. The builder's watcher posts ended, recording each file's SHA-256 in outputs, then sends the files as the builder's login, which holds y.
  2. A person imports it. A person with setup runs import RUN, RUN the stage run named where its ended was posted, on the page (a POST) or at the command line. The files are in the server's own repository, so it reads them there, with no sync. It counts the release only when, by its receipts and never by tags or user cards: the run is the stage pattern, compared field by field, addressed to a listed builder and its clone, sent by another user allowed to send it; the builder's first counted started names the check-in; its first counted ended for that run, in its thread, is exited with exit 0; and every output is present and matches its hash. Otherwise it refuses and changes nothing. An unwanted release's shared files are removed by hand.
  3. The server tests it. Import copies the files into its own staged/NAME, keeping the run and hashes it counted beside them, runs each program with no arguments, as staging does today, requiring its usage line, offers it as next on /ext/ocx2-cgi-next, and removes the shared files. Only import writes the server's staged/; the server starts no run. The person tries the release there.
  4. The server accepts it, as it does today: accept copies next into the server's root and moves current, keeping the one before as previous; rolling back moves it back. The release tools take one lock, so two never change the root at once.
  5. The server serves it. The server's page /ext/ocx2-cgi/release answers what is current and previous: each one's check-in and each file with its SHA-256, and serves those files by name, over HTTPS, to a signed-in builder or setup login.

The client follows

A person on the client runs ocx2 follow SERVER --root ROOT, inside the client's root, as the root's owner. It asks the server's /release what is current. If that is not the client's own current, it downloads each file it lacks from the server by name, checks every hash, puts the release in its root, and moves current, keeping the one before as previous. A rollback is the same: the server names its previous, and the client takes that release, from its own root if it has it. The daemon ends when current moves, and the person starts it again with confine daemon, as under "Start" above. Nothing follows on a timer.

When the server cannot be reached, the client keeps what it runs; to move it back meanwhile, a person links its current to its previous by hand.

The client downloads only its own architecture's files, once per release, over a plain HTTPS request; it needs no fossil uv sync, which today sends every shared file a clone lacks, even for unversioned_get of one file.

Before the first ordinary release

Neither root's installed release can import, serve /release or follow, and the client's installed confine locks ROOT/node/daemon.lock where this tree locks ROOT/state/daemon.lock. So the first release that can is installed once by hand on both roots, with install-first.cr, on the host, outside both roots, as their owner, in the maintainer's checkout (decision 22):

  1. crystal build ocx-rootfs/install-first.cr -o install-first, then ./install-first SERVER CLIENT REPOSITORIES RUN, the folders as the start commands under "Start" name them, RUN the full hash of an ended stage run. It gathers the release's files from the server's ollama.fossil, builds the host programs, and says what it would change.
  2. Stop the client's daemon and wait until idle (idle ROOT, below).
  3. Run it again with --apply. It refuses while the client's daemon holds ROOT/node/daemon.lock, then removes it; it installs the host programs, keeping each one it replaces as PROGRAM.previous, puts the release in both roots and links it current, the one before previous, and writes the server's settings/builders.json.
  4. Restart the server if serve was replaced, and start the client's daemon.

The way back: link current to previous on both roots and rename each PROGRAM.previous back. test-rootfs.cr rehearses this, and the ordinary release after it, on copies, when a rehearsal is wanted.

Other projects, after the first round

Then other projects move to the same steps, and build-rootfs.cr stops building theirs (gp-crystal, crystal-robots, swk-ai), each project's first install tested as OCX's was. serve and confine do not change, except that confine learns once to run a project's CGI program named by an entry in hosts.json, from that project's current, in a sandbox with no access to the repository file. swk-ai's page reads its repository file and writes runs and attachments with it, so it moves unsandboxed, as it runs today, its entry stating that exception, and is sandboxed once its page works through Fossil's web pages and a clone of its own; that change is swk-ai's, on a ticket of its own. aarch64 comes with them, for interactor and social-badge on a BeagleBadge. A server accepts once its own architecture's build passes; a build of another architecture for the same check-in is accepted on its own, and /release lists each architecture's files with the run that built them. Clients compare by file hashes, never by check-in alone.

What shows it works

test-rootfs.cr, given two stage runs x86-1 built (FIRST and NEXT), first installs FIRST by hand on both roots with this checkout's host programs, as "Before the first ordinary release" says, and writes the server's settings/builders.json. Then it shows that import refuses a report from a login not listed, a run that is not the stage pattern, and a changed file, changing nothing; that import offers NEXT and removes its shared files; that the server accepts it and its page lists NEXT as current and FIRST as previous; that follow moves the client to NEXT, its daemon ends, and once started again exactly one daemon runs, on NEXT; and that both roll back to FIRST the same way. It does not show a client that missed a release following straight to the current one, which needs a third release.

Moving sessions to protocol version 2

migrate-sessions.cr carries each open session across the switch, by its own handoff on the forum. It reads, writes and prints no password.

  1. On the client, before the switch, crystal run migrate-sessions.cr -- list > sessions.json lists each run a watcher holds, from the watchers' command lines and their clones, with each acting clone's login (its default user) and server-code.
  2. On the server, crystal run migrate-sessions.cr -- ask sessions.json /var/lib/fossil LOGIN MINUTES > handoffs.json replies to each session's run, as LOGIN, asking it to commit, push and answer with a handoff by the deadline. It stops each session that answered. One that did not answer, and a run that is not a session, is left running. It names the request, the handoff and the stop by their full hashes.
  3. After the switch, crystal run migrate-sessions.cr -- links handoffs.json PATTERN prints a link to version 2's create page for each session that handed off and was stopped: a session from PATTERN, replying to its handoff, addressed to the acting clone's login and clone (its server-code), as the same process user, whose password the link resets for the new clone to collect. A run that ended lost after its stop was posted still counts as stopped. Each session it gives no link names what it lacks.

Bringing a root to rest, and clearing it

tidy-root.cr runs on a host, inside its namespaces, as the root's own account. Without --apply it changes nothing and says what it would do.

The switch, with both, on each machine in turn:

  1. Hand off the sessions (above, steps 1 and 2).
  2. Client: stop the daemon, then idle ROOT 60 until the last task ends.
  3. Server, then client: accept the version 2 release; on the client, to-v2 ROOT --apply, then start the daemon.
  4. Publish the patterns in version 2's words: run-claude.md and run-codex.md as the wiki pages run/claude and run/codex, written as a user holding admin or setup; session.md, codex-task.md and stage.md as attachments of a tech-note, each then named by its content-addressed raw URL. session.md names codex-task.md by that URL, so publish them as they are.
  5. Start the sessions again from their links (above, step 3), giving session.md's URL as PATTERN.

pattern-v2.cr rewrites a version 1 pattern's keys and values in version 2's words, keeping its prose, and lists each line still naming version 1's words for a person to rewrite: crystal run pattern-v2.cr -- PATTERN.md > PATTERN-v2.md. The patterns in ../patterns/ were made with it, then their prose rewritten; each passes the branch's create --dry-run, which refuses a version 1 key.

  1. Once both run well: sweep ROOT --apply on each, and later sweep ROOT --apply --archives.

The way back is the same in reverse: stop the daemon, idle, rollback, to-v1 ROOT --apply, start the daemon.

What it leaves out