The uid-boundary fence — what is built, and the three root steps left
The uid-boundary fence — what is built, and the three root steps left
Min's record proved the hole: opencode authentication is per-uid, so any
process running as lab could call any paid model with no gate ($22.38 lifetime
spend through it). This record is the fix, built and committed.
1. New measurements that changed the design (all proven, in the session)
- A config-file
apiKeybypasses the db. Config-with-key + empty opencode.db →rc=0on a live model call. So a runner config carrying a literal key would re-open the hole; the runner's config is keyless by construction and the bootstrap refuses to install one that leaks (_literal_key_paths).{env:...}references are kept — they resolve to nothing under the unit'sUnsetEnvironment=and fail closed. - The
envvar alone does NOT authenticate without the db:OPENCODE_GO_API_KEYset, no db →Error: Upstream request failed: Go requires a credential, rc=1. The db is the credential; the env var is not (this also re-proves E5: an env key cannot rescue a fenced process). - The no-credential error is loud, not silent: empty db + paid model →
Error: Model unavailable: <model>, rc=1. That is the criterion-5 shape. - A systemd USER unit cannot switch uid:
systemd-run --user -p User=rootexitsstatus=216/GROUP. The fence therefore needs a system unit (root) — a user-level fix was never available. - The default opencode service port (49374) is global, not per-uid: a
second uid's child collided (
Managed service port 49374 ... already in use). The runner gets its own port (49375).
2. What is built (commit e1eb316 on origin/main)
| File | Role |
|---|---|
operation/runtime/ocrunner_bootstrap.py | root bootstrap: creates ocrunner, builds /var/lib/ocrunner 0700, installs the keyless config, pipes the credential across uids (auth export → auth import, in-memory, secret never on disk or in output), sets the runner's port, shares operation/state/ with lab via setgid group. --check reports the fence state without root; --de-scope-lab is the deliberate final step. |
etc/systemd/solos-v3-worker-fenced.service | the system unit: User=ocrunner, SupplementaryGroups=lab, UMask=0002, E5's UnsetEnvironment= carried over, ProtectSystem=strict + explicit ReadWritePaths. systemd-analyze verify passes. |
tests/live_proof_uid_fence.py | all five acceptance criteria as rows. Without root it reports NOT-RUN, never a substitute PASS; currently green: suite 457 passed, red check loud-fails as measured. |
Design choices recorded in the code, not silently made: the equality-canary in the scrub was strengthened to a path-listed leak check; the cutover order is disable the lab worker BEFORE enabling the fenced unit (two workers on one store race for the next task); the proof sets umask 0002 because root creating files in a setgid dir umask-0022 would lock the runner out of its own db.
3. The three root steps that remain (Young, in this exact order)
# 1. stop the lab user-unit worker (as lab)
systemctl --user disable --now solos-v3-worker.service
# 2. bootstrap + install + enable
sudo python3 /home/lab/.solos/operation/runtime/ocrunner_bootstrap.py
sudo install -m 644 /home/lab/.solos/etc/systemd/solos-v3-worker-fenced.service \
/etc/systemd/system/ && sudo systemctl daemon-reload && \
sudo systemctl enable --now solos-v3-worker-fenced.service
# 3. prove all five criteria
sudo python3 /home/lab/.solos/tests/live_proof_uid_fence.py
# 4. only when the proof is green — close the lab side (deliberate):
sudo python3 /home/lab/.solos/operation/runtime/ocrunner_bootstrap.py --de-scope-lab
Why --de-scope-lab is not part of apply: it changes what a human shell
can spend (lab re-auths with opencode auth login on demand; the key stays
in /home/lab/.config/opencode/lab-go.env, mode 0600, for that). Making it an
action keeps the fence from silently breaking a human workflow.
Rolled back = the steps in reverse; nothing else to undo (the fence adds a user, a unit and a config; it changes nothing existing until step 4).
4. Verification state, honestly
- Offline suite: 457 passed, 2 skipped, 2 xfailed — under the interpreter
whose site-packages carry the deps. (Measured on the way: the suite needs
the A2A SDK on PYTHONPATH plus
json-rpc; 7 "failures" in one earlier run were an environment gap — pydantic_core compiled for 3.12 vs 3.14 — not code. The proof discovers a working interpreter rather than assuming.) - Red check: PASS (loud
Model unavailable, rc=1, no spend). - Criteria 1, 2-real, 3a, 4-runtime: NOT-RUN — they need the root steps.
--checkcurrently reports NOT APPLIED, which is the true state until sudo runs. - Not done and deliberate: no change to
route()/invoke.py(the fence is a uid, not a code check — criterion 4); no second launcher; E5 intact.
5. For Mira, afterwards (the ledger ask stands)
Once criterion 2 holds, opencode stats --cost --json read under each uid
becomes the truth about WHO spent: lab's total freezes (humans only by
then), ocrunner's grows (the fleet). Difference = fleet spend, per day,
without any attribution code. That is the cheapest ledger available, and it
becomes possible only after the fence.
The three-unit topology — and what it corrects in our own design
Date: 2026-10-10 Author: Sol Answers: Young's unit/agent/topology statement Status: RECORD — with a correction to my own earlier claim
The Brain↔Pulse A2A bridge — measured, and one defect in your config
Young, 2026-10-12: "Might need to enable A2A to establish bridge between Pulse and Brain Unit."

