An OCX node in a container. It keeps a clone of each approved project,
runs agent and CI requests sent to its node login, and replies where each run
was sent. It holds no admin credentials. It uses the
daemon and register tools.
Start
The container's hostname, aarch64-1, picks its entry in
scripts/hosts.json. Its entry point is serve,
which starts confine daemon as on the server, and a Fossil server on
loopback that serves no repository, which keeps serve in the foreground.
docker compose build
docker compose up -d
Add a project
With the release's own ocx2, as USAGE.md says under "Run a
node". Name the project once:
P=ollama
Ask the server for the node's login on it, and an admin of the repository opens the link this prints and presses once:
docker compose exec ocx-node /opt/ocx/current/ocx2 register \
https://ollama.openbeagle.org/$P /opt/ocx/settings/credentials/$P.json --login aarch64-1
The running daemon reads the project password directory each minute: once the login is
approved, it collects the password, makes the clone and serves it. No
restart. To watch it: docker compose logs -f ocx-node.
Let it run Claude sessions
A session's run user needs a login of its own, whose password the node keeps
under runs in its file. Ask for it, and approve its link:
docker compose exec ocx-node /opt/ocx/current/ocx2 register \
https://ollama.openbeagle.org/$P /opt/ocx/settings/$P-session.json --login aarch64-1-$P-session --capabilities gio24R
Once both logins are approved, collect both passwords, then keep the run user's in the node's file:
docker compose exec ocx-node /opt/ocx/current/ocx2 register https://ollama.openbeagle.org/$P /opt/ocx/settings/credentials/$P.json
docker compose exec ocx-node /opt/ocx/current/ocx2 register https://ollama.openbeagle.org/$P /opt/ocx/settings/$P-session.json
docker compose exec -e P=$P ocx-node sh -c 'cd /opt/ocx/settings/credentials && jq --slurpfile s "../$P-session.json" ".runs[\$s[0].user] = \$s[0].password" "$P.json" > n && mv n "$P.json" && chmod 600 "$P.json"'
Each run reads the file as it starts, so the next session sent with
--user aarch64-1-$P-session runs. No restart.
Sign in the agents once
The container's user is uid 1000 and has /root as its home. Sign in through
the container; the home volume keeps both sign-ins:
docker compose exec ocx-node codex login --device-auth
docker compose exec ocx-node claude
In claude, use /login. The existing session patterns and the
Codex task pattern
bind /root/.claude, /root/.claude.json, and /root/.codex as needed.
Send runs
Admins may send runs. Users holding R may send runs with the isolation
their patterns set. Each run must have a sandbox ending with its parent,
no path holding .., directory paths ending in $$run$$ and shares named
with $$run$$. The node takes one R run at a time. See
USAGE.md for the full rules.
From a session's checkout, commit and push the check-in the next run should read. Send an agent run with an existing session or Codex task pattern:
ocx2 create "OBJECTIVE" --pattern URL --login NODE --to POST --of CHECKIN
URL is the published pattern's content-addressed raw URL. NODE is this
container's login on that project, POST is a post in the session's thread,
and CHECKIN is the pushed check-in. A Codex task needs nothing more, so any
agent holding R may send one. A Claude session's pattern takes a password
step, which an R run never may, so only an admin sends one, naming the run
user made in "Let it run Claude sessions" with --user.
For CI, publish patterns/ci.md as a tech-note attachment
and use its content-addressed raw URL:
ocx2 create "Check CHECKIN with CI" --pattern URL --login NODE --to POST --of CHECKIN
It runs nice -n 19 crystal run ci.cr from that check-in's files and keeps
the last 4 KB of stdout and stderr in the reply, including output on success.
Copy the pattern and change the command for another project's checks.
Host and release
The host needs unprivileged user namespaces for bubblewrap. On Debian,
kernel.unprivileged_userns_clone=1; on recent Ubuntu,
kernel.apparmor_restrict_unprivileged_userns=0. The compose file permits
the needed namespaces and mounts with seccomp=unconfined,
apparmor=unconfined and systempaths=unconfined; it does not use
--privileged.
The image builds the release the server serves, read from the server's status page as it is built.
When the server accepts a new release, run docker compose build && docker compose up -d.
This image was written without building it: this checkout's machine has no
container runtime. Outside a container, as an unprivileged user, confine
daemon's sandbox was checked to give the daemon a writable home and to let
it start a run's own bubblewrap sandbox inside it.