Node bootstrap and worker onboarding
2 target(s) · 15 steps · kit adoptedThe bootstrap kit is the shortest path from 'a machine exists' to 'the fleet is using it'. It is declared here and executed by scripts/bootstrap-node.sh; the console renders this file so the plan an operator reads is the plan that runs. Every step states how re-running it is safe, how to prove it worked, and how to undo it — a bootstrap step you cannot verify or reverse is a one-way door onto somebody's home network.
Sign the Claude CLI in as the jarvis service account on home1
Claude Code is the engineering worker. Its sign-in is an interactive OAuth device flow: it prints a URL, waits for a browser, and writes the credential itself. Nothing headless can complete it, and until it is done home1 can be dispatched planning work but no code.
Blocks: All engineering dispatch to home1. The node will onboard, heartbeat and advertise capacity without it — and every coding task will still land on VIN's single slot.
Run this
ssh root@uk-james-home1.tail99ba7.ts.net sudo -u jarvis -H npm install -g @anthropic-ai/claude-code # only if `sudo -u jarvis -H command -v claude` is empty sudo -u jarvis -H claude # then, at the prompt: /login — it prints a URL, open it, approve, come back
Values you will be asked for
- Account
-
jkkec23@gmail.comThe same account the CLIs on home1 already use under the 'james' user. Using a different one is fine, but write down which. - Verify
-
sudo -u jarvis -H test -f /home/jarvis/.claude.json && echo signed-in - Then
-
sudo /opt/jarvisos/bin/jarvisos-bootstrap.sh advertiseRe-advertises immediately; this card clears on the next heartbeat.
Open https://claude.ai/login
Onboarding detail →
Clears itself when home1 advertises
worker-claude.
Fleet capacity, as the governor should see it
Derived from measurements, never declaredThe dispatch supervisor derives worker fan-out from available memory alone. That is right on a 1.9 GiB edge node and wrong on a 34 GiB home node that is already 15 GiB into swap and stalling for half of every five minutes. These numbers add the two constraints memory cannot see — core count and pressure — and are served at /api/v1/bootstrap so admission control can read them rather than re-derive them.
| Node | Slots | Bound by | Memory-only would give | Source |
|---|---|---|---|---|
| home1 | 3 | cpu + pressure | 8 | evidence 2026-08-16 |
| vin | 1 | memory | 1 | evidence 2026-08-16 |
Knobs: 300 MiB held back, 350 MiB per worker, 2 cores per worker, halved above 25% CPU pressure or 1% memory pressure.
Open gaps
What is between here and a fully onboarded fleet-
Open gap: home1 has not reported in home1No live heartbeat with capabilities from "home1". Its 3 worker slot(s) are a measurement from 2026-08-16, not a signal. Run scripts/bootstrap-node.sh install on it, then advertise.
-
Open gap: Memory-only admission would over-dispatch home1 home1The supervisor's current formula gives 8 worker slot(s) here; deriving from cores and pressure as well gives 3. Bound by cpu: 12 core(s) at 2 per worker. Halved for pressure — CPU stalled 38.8% of the last five minutes (threshold 25%); memory fully stalled 6.55% of the last ten seconds (threshold 1%). Set JARVISOS_MAX_PARALLEL=3 in /etc/jarvisos/config.env on home1 until the supervisor reads this endpoint.
-
Open gap: vin has not reported in vinNo live heartbeat with capabilities from "vin". Its 1 worker slot(s) are a measurement from 2026-08-16, not a signal. Run scripts/bootstrap-node.sh install on it, then advertise.
-
Open gap: Sign the Claude CLI in as the jarvis service account on home1 home1Claude Code is the engineering worker. Its sign-in is an interactive OAuth device flow: it prints a URL, waits for a browser, and writes the credential itself. Nothing headless can complete it, and until it is done home1 can be dispatched planning work but no code. Blocks: All engineering dispatch to home1. The node will onboard, heartbeat and advertise capacity without it — and every coding task will still land on VIN's single slot.
-
Open gap: Sign the Codex CLI in as the jarvis service account on home1 home1Codex is the reviewer in the dispatch model, so without it home1's work is written and merged by the same worker. Its sign-in is the same interactive OAuth flow and equally cannot be automated. Blocks: Independent review of work produced on home1.
Targets
home1 not onboarded
home1 · primary worker and always-on computeBound by cpu: 12 core(s) at 2 per worker. Halved for pressure — CPU stalled 38.8% of the last five minutes (threshold 25%); memory fully stalled 6.55% of the last ten seconds (threshold 1%).
Memory allows 46 · cores allow 6 · declared ceiling 6 · halved for pressure · from a measurement on 2026-08-16
- Reached at
- uk-james-home1.tail99ba7.ts.net
- Admin path
- tailscale-ssh as root
- Service account
- jarvis
- Last heartbeat
- never
- Ceiling
- 6 — 12 cores at two cores per worker. Deliberately below what memory alone would allow (46), because this node already runs 83 containers and a legacy service estate that were here first.
Governor: memory-only admission would dispatch 8 here. This node is already stalling; the derived answer is 3.
vin not onboarded
vin · public edge and dispatch nodeBound by memory: 773 MiB available, 300 MiB held back, 350 MiB per worker.
Memory allows 1 · cores allow 1 · declared ceiling 2 · from a measurement on 2026-08-16
- Reached at
- vin.tail99ba7.ts.net
- Admin path
- local as jarvis
- Service account
- jarvis
- Last heartbeat
- never
- Ceiling
- 2 — 1 vCPU and 1.9 GiB of RAM. Memory is the binding constraint here and the existing governor already models it correctly.
Human-only steps
One is surfaced on the overview at a time, by priorityEverything not on this list is the installer's job. These three are interactive OAuth flows and a network enrolment: no automation can complete them, and pretending otherwise would mean an installer that can add machines to a private network and sign in as its owner.
-
Sign the Claude CLI in as the jarvis service account on home1
surfaced now
Claude Code is the engineering worker. Its sign-in is an interactive OAuth device flow: it prints a URL, waits for a browser, and writes the credential itself. Nothing headless can complete it, and until it is done home1 can be dispatched planning work but no code.
-
Sign the Codex CLI in as the jarvis service account on home1
queued
Codex is the reviewer in the dispatch model, so without it home1's work is written and merged by the same worker. Its sign-in is the same interactive OAuth flow and equally cannot be automated.
-
Enrol the node on the tailnet
done — observed, not advertised
Joining a private network is an act of enrolment. It needs an auth key or an interactive login from the admin console, and this kit deliberately has neither: an installer that can add machines to your private network is an installer that can add anyone's.
The installer, step by step
scripts/bootstrap-node.sh · plan · install · verify · advertise · rollback
Every step states what re-running it does, how to prove it worked and how to undo it. A step that changes
a host and cannot name its undo fails npm run check rather than reaching a machine.
Preflight
Prove the machine is the kind of machine this kit can bootstrap, and that we are already on the private plane, before anything is written.
| Step | Re-running it | Proven by | Undone by |
|---|---|---|---|
preflight-host
safe
Linux with systemd, and we are root
|
read-only | uname -s is Linux, /run/systemd/system exists, effective uid is 0 (or sudo -n works) | Reads only; there is nothing to undo. |
preflight-mesh
safe
Tailscale is installed, logged in and carrying this node
|
read-only | BackendState is Running and Self.DNSName is non-empty | Reads only. The kit never runs `tailscale up`, never edits the tailnet ACL and never touches a firewall: joining a private network is an act of enrolment that belongs to a human with the admin console open. |
preflight-console
safe
The status console answers from this node
|
read-only | HTTP 200 with console:"ok" | Reads only. |
Service account
Give fleet work an identity of its own, so what it can reach is a decision rather than an accident of whose shell it borrowed.
| Step | Re-running it | Proven by | Undone by |
|---|---|---|---|
service-account
disruptive
Create the jarvis system account
|
exact — an existing jarvis account is left completely untouched, including its shell, home and groups | getent passwd jarvis returns a line and the home directory exists | userdel jarvis (only with --purge, and only if this run created it; the account is otherwise left alone) |
Roots
Create the four roots everything else assumes: code, state, logs and transcripts. Nothing here is on the NAS — a worker root that lives on the thing we may be recovering cannot be used to recover it.
| Step | Re-running it | Proven by | Undone by |
|---|---|---|---|
roots-code
safe
Code root /opt/jarvisos and its bin/
|
exact — mkdir -p and an explicit chmod; existing content is not touched | /opt/jarvisos/bin is a directory, group jarvis, mode 0755 | rmdir /opt/jarvisos/bin /opt/jarvisos if empty; never recursive |
roots-state
safe
State, workspace, report and transcript roots under /var/lib/jarvisos
|
exact — mkdir -p; existing task state, workspaces and transcripts are never rewritten | all five directories exist, owned by jarvis, mode 0750 | Left in place. These hold task history and worktrees; removing them on rollback would delete work the rollback is trying to protect. --purge removes them only after printing what it is about to lose. |
roots-log
safe
Log root /var/log/jarvisos
|
exact | /var/log/jarvisos exists, owned by jarvis, mode 0750 | Left in place; logs are evidence about the run that is being rolled back. |
Command line tools
Prove the tools the runner and the workers need are present. The kit reports what is missing; it installs only from the distribution's own package manager, and only when told to.
| Step | Re-running it | Proven by | Undone by |
|---|---|---|---|
tooling-base
disruptive
Base CLIs: git, curl, node, python3, rsync, jq
|
exact when everything is present (the common case); otherwise apt-get install of the missing set only, and only with --allow-apt | every one of the six resolves on PATH | Packages installed by this step are listed in the run log and can be removed with apt-get remove. Nothing is removed automatically: another workload may have started depending on them in the meantime. |
tooling-workers
safe
Worker CLIs and their sign-in state for the service account
|
read-only | each binary resolves for the jarvis account and its credential file exists — presence only; the kit never reads the contents | Reads only. Credentials are never written, copied or moved by this kit: a token copied between accounts is a token whose custody nobody can describe afterwards. |
Secret references
Put the node's configuration file in place with the right ownership and the right keys, and leave the values to whoever owns them.
| Step | Re-running it | Proven by | Undone by |
|---|---|---|---|
config-env
disruptive
/etc/jarvisos/config.env, root:jarvis 0640
|
additive — an existing file is never rewritten; only missing keys are appended, and an existing key keeps its value even when empty | the file exists, is mode 0640 root:jarvis, and contains every key in paths.configKeys | A backup is written to /etc/jarvisos/config.env.bak-bootstrap-<stamp> before any append; rollback restores it. A file this run created from nothing is removed. |
ingest-token
disruptive
Install or enroll this node's scoped ingest identity
|
exact — an identical credential is a no-op; a different one is refused unless --force-token | POST /api/v1/heartbeat from this node returns 202, not 401 | The pre-change credential backup is restored. Revocation in the console registry is a separate operator action. |
Node agent
Install the heartbeat and capability agent, so the node stops being something a human has to remember and starts being something the fleet can see.
| Step | Re-running it | Proven by | Undone by |
|---|---|---|---|
agent-install
safe
Copy the kit to /opt/jarvisos/bin/jarvisos-bootstrap.sh
|
exact — byte-identical copy is skipped; a differing copy is replaced and the old one kept as .prev | /opt/jarvisos/bin/jarvisos-bootstrap.sh --version matches the kit version | Remove the file, restoring .prev if this run replaced one. |
agent-unit
disruptive
systemd unit and timer jarvisos-node-agent
|
exact — the unit text is generated deterministically and written only when it differs; the timer is enabled only if not already enabled | systemctl is-enabled jarvisos-node-agent.timer is 'enabled' and one manual `systemctl start jarvisos-node-agent.service` exits 0 | systemctl disable --now jarvisos-node-agent.timer, then remove both unit files and daemon-reload. |
Host-substrate deployment runner
Provide a break-glass deployment path only for workloads explicitly classified as host substrate. Ordinary applications use the Coolify templates in config/deploy-templates.json.
| Step | Re-running it | Proven by | Undone by |
|---|---|---|---|
deploy-runner
disruptive
Exception-gated template unit jarvisos-deploy@.service
|
exact — deterministic unit text, written only when it differs | systemctl cat jarvisos-deploy@.service resolves and a dry-run instance exits 0 | Remove the template unit and daemon-reload. Instances are transient and hold no state. |
Advertise capacity
Tell the fleet what this node can actually take, measured rather than declared, within seconds of it being connected.
| Step | Re-running it | Proven by | Undone by |
|---|---|---|---|
advertise-capacity
safe
Measure and POST the capability heartbeat
|
exact — a heartbeat is an observation; sending it twice reports the same node twice | the console returns 202 and the node appears with a fresh heartbeat and a non-empty capability list | Nothing is written on either side beyond an in-memory observation that expires on its own. Stopping the timer stops the advertisement. |
Paths and secret references
Names and locations only — this console never holds a value| Path | Purpose | Owner | Mode | Backup |
|---|---|---|---|---|
/opt/jarvisos |
code root | root:jarvis | 0755 | t0-crown-jewels |
/opt/jarvisos/bin/jarvisos-bootstrap.sh |
installer and node agent, one file | root:jarvis | 0755 | t0-crown-jewels |
/var/lib/jarvisos/state |
task store and locks | jarvis:jarvis | 0750 | t0-crown-jewels |
/var/lib/jarvisos/workspaces |
repository and worktree root, one directory per project | jarvis:jarvis | 0750 | t1-reproducible |
/var/lib/jarvisos/reports |
worker run reports | jarvis:jarvis | 0750 | t1-reproducible |
/var/lib/jarvisos/transcripts |
worker transcripts; may contain quoted source and must never leave the node unredacted | jarvis:jarvis | 0750 | t1-reproducible |
/var/log/jarvisos |
agent and deployment logs | jarvis:jarvis | 0750 | none |
/etc/jarvisos/config.env |
node configuration and non-secret settings | root:jarvis | 0640 | t0-crown-jewels |
/etc/jarvisos/node-credential |
this node's scoped ingest credential | root:jarvis | 0640 | t0-crown-jewels |
-
node credential— /etc/jarvisos/node-credential · custody node, supplied by offline issue/rotation, or short-lived bootstrap enrollment -
tailscaled node state— /var/lib/tailscale/tailscaled.state · custody node, supplied by tailscale up, run by a human at enrolment -
worker CLI credentials— /home/jarvis/.claude.json, /home/jarvis/.codex/auth.json, /home/jarvis/.gemini/ · custody node, supplied by interactive OAuth sign-in by a human, as the service account
Current-state evidence
What was actually observed, and how-
E1 · home1 — home1 is on the tailnet and reachable: uk-james-home1.tail99ba7.ts.net / 100.96.106.91, tag:homelab-jc, online, 33-39 ms via DERP(lhr) with a direct path also established.
2026-08-16 · tailscale status --json and tailscale ping, from VIN · closes G2
-
E2 · home1 — Administrative access already exists and needs no new credential: home1 runs Tailscale SSH, and the tailnet policy permits root and james from VIN. It denies jarvis and ubuntu, so the service account will not be reachable over SSH — which is correct, and is why the agent is a timer that reports outward rather than a door that is knocked on.
2026-08-16 · ssh -o BatchMode=yes, from VIN
-
E3 · home1 — Ubuntu 26.04 LTS, kernel 7.0.0-29, AMD Ryzen 5 PRO 5650U, 12 cores. MemTotal 34.2 GiB, MemAvailable 16.4 GiB, swap 24 GiB with 15.2 GiB already in use. Root filesystem 3.6 TB with 2.9 TB free; the NAS is mounted at /mnt/nas/homelab (63 TB, 87% used). Load average 5.88 / 14.65 / 18.94 across 12 cores, 83 running containers.
2026-08-16 · read-only enumeration over Tailscale SSH · closes G1
-
E4 · home1 — CPU pressure some avg300 = 38.80%, memory pressure full avg10 = 6.55%. The node is not idle: it is losing a measurable share of every five minutes to contention, while MemAvailable still reads 16 GiB. This is the number that makes memory-only admission control wrong here.
2026-08-16 · /proc/pressure, over Tailscale SSH
-
E5 · home1 — Present: node v22.22.2, npm 10.9.7, git 2.53.0, curl, jq 1.8.1, python3 3.14.4, docker 29.4.1, rsync, tailscale 1.102.2, gemini 0.47.0. Absent: no jarvis account, no /opt/jarvisos, no /etc/jarvisos, no /var/lib/jarvisos, no /var/log/jarvisos, no JarvisOS unit. claude and codex exist and are signed in only for the human user 'james' under /home/james/.local/bin.
2026-08-16 · read-only enumeration over Tailscale SSH
-
E6 · home1 — 10.10.0.10 — the address the inventory declares for home1 — answered nothing. Every reachable path to this node in this run was the MagicDNS name.
2026-08-16 · ping, from VIN
-
E7 · vin — 1 vCPU, MemTotal 1.9 GiB, MemAvailable 773 MiB, 525 MiB of swap in use, load 0.12. jarvisos-dispatch.timer is active and the supervisor derives its fan-out from MemAvailable alone, which yields exactly one worker slot here today.
2026-08-16 · systemctl and /proc, on VIN
-
E8 · home1 — home1 carries a pre-existing estate this kit did not create and does not manage: jarvis-bridge, jarvis-canary, jarvis-fleet, jarvis-opencode-sync, jarvis-proxy-autofix and jarvis-proxy-backup units, plus 83 containers. The kit manages the names jarvisos-node-agent.service, jarvisos-node-agent.timer and jarvisos-deploy@.service, and nothing else.
2026-08-16 · read-only enumeration over Tailscale SSH
Notes
-
Note: home1 capacity is from evidence, not a live agent home1Derived from read-only enumeration over Tailscale SSH — see evidence E3 and E4 on 2026-08-16. It is the best number available and it will not update on its own.
-
Note: vin capacity is from evidence, not a live agent vinDerived from local /proc — see evidence E7 on 2026-08-16. It is the best number available and it will not update on its own.
How to read this page
- One installer, five verbs: plan, install, verify, advertise, rollback. Anything that cannot be expressed as one of those does not belong in the bootstrap path.
- Idempotent means re-running changes nothing, not that re-running is merely harmless. Every step declares which of the two it achieves and how.
- The installer never invents a secret it cannot prove the console shares. A node that heartbeats with a token the console does not hold looks alive and is not.
- The installer touches exactly the unit names it declares. A home node that has been in service for years carries other people's daemons, and the kit is not entitled to any of them.
- Capacity is advertised from measurements, never from the spec sheet. A node with 34 GiB of RAM and 15 GiB of it already in swap is not a 34 GiB node.
- Human-only means no automation could do it — an interactive OAuth flow, a physical act, a custody decision. Everything else is the installer's job, and putting it on a card is how a dashboard teaches people to ignore cards.
Source: config/bootstrap.json · installer scripts/bootstrap-node.sh ·
machine-readable at /api/v1/bootstrap · narrative in
docs/NODE-BOOTSTRAP.md.