Convert 'do not', 'cannot', 'never', 'avoid', 'don't' patterns across AGENTS.md, README.md, and 11 docs/*.md files into positive, actionable instructions that tell the reader what TO do. Preserved: hard safety constraints (MUST NOT agent boundaries, vault credential confinement intent) — these are enforceable guardrails where the prohibition IS the instruction.
5.7 KiB
Vault Provision — First-Proof Runbook (osa)
Status. The spawn → vault-provision → .env chain is wired and unit-tested in
code (colibri-daemon spawn hook → colibri-vault::provision), but it is not yet
drivable from the CLI. Two gaps make the live proof a manual procedure today:
- #101 — no
register-tenantsocket command / CLI verb; tenants are only insertable via raw SQLite. - #102 —
colibri spawn-agent/spawn-localhardcodejail: None; a jailed spawn (the only kind that triggers provisioning) must be sent as raw socket JSON.
This runbook proves the chain using that interim path. When #101/#102 land, steps 3–4
collapse into colibri register-tenant … + colibri spawn-agent … --jail-name ….
First-proof policy (see layered-soul/docs/HIVE-ONBOARDING.md): use a scratch jail +
throwaway test collection only — no real tenant data until the path hardening (#92) lands.
How the chain actually resolves (so the setup is correct)
- The hook is
provision_tenant_env(jail_name, jail_root_path). It looks upstore.get_tenant(jail_name); if no tenant row matches, it no-ops. - It then requires
tenant.jail_root_path == spawned root(trailing-slash-normalized) — a mismatch refuses provisioning. - It calls
colibri_vault::provision(&tenant.tenant_id, jail_root_path)— so the Vaultwarden collection must be named exactlytenant_id, andtenant_id= jail name = collection name (the 1:1:1 contract). - On success it writes
<jail_root>/.envat0600and flips tenant status →active.
Paths (FreeBSD daemon)
- Socket:
/var/run/colibri/colibri.sock - DB:
/var/db/colibri/colibri.sqlite - Provider env (bootstrap creds):
/usr/local/etc/colibri/provider.env
Prerequisites
colibri-daemonrunning on osa./usr/local/etc/colibri/provider.env(mode 600) hasBW_SERVERplus the three bootstrap secretsBW_CLIENTID/BW_CLIENTSECRET/BW_PASSWORD(PR #69), and the daemon has them in its environment (the rc.d loads provider.env).bwCLI on the daemon's PATH.- Pick a scratch tenant id, e.g.
T=proof0.
Step 1 — scratch jail + bootstrap
T=proof0
sudo bastille create "$T" 15.0-RELEASE-p10 <ip> # your standard Bastille create
sudo agent-jail-bootstrap.sh "$T" # runtime pkgs + colibri binaries
# jail root is /usr/local/bastille/jails/$T/root
Step 2 — test collection in Vaultwarden
In the web UI, create a Collection named exactly $T, and add one Login item:
- Name = a harmless env var, e.g.
FIRST_PROOF_KEY - Password field = a throwaway value (this validates the name-based contract)
The bootstrap account must have read access to that collection.
Step 3 — register the tenant (interim: manual SQLite — gap #101)
T=proof0
DB=/var/db/colibri/colibri.sqlite
sudo sqlite3 "$DB" "INSERT INTO tenants
(tenant_id, jail_root_path, collection_id, status, created_at, updated_at)
VALUES ('$T', '/usr/local/bastille/jails/$T/root', '$T', 'provisioned',
datetime('now'), datetime('now'));"
jail_root_pathmust exactly match the spawned root (the hook compares them).collection_idisNOT NULL UNIQUE, but the hook resolves the collection bytenant_id(name) — so setcollection_id = tenant_idto satisfy the constraint (it's vestigial; see #88/#93).- SQLite runs in WAL, so this insert is safe while the daemon is up.
Step 4 — trigger a jailed spawn (interim: raw socket JSON — gap #102)
T=proof0
SOCK=/var/run/colibri/colibri.sock
printf '%s\n' \
'{"cmd":"spawn-agent","provider":"local","model":"/usr/local/bin/colibri-test-agent","local_args":["--session-id","'"$T"'-proof","--step-ms","10","--hold-secs","1"],"jail":{"name":"'"$T"'","root_path":"/usr/local/bastille/jails/'"$T"'/root"}}' \
| sudo nc -U "$SOCK"
provider: "local"uses thecolibri-test-agentbinary copied into the jail byagent-jail-bootstrap.sh, so the proof does not depend on provider API keys or a separateCOLIBRI_AGENT_BINARYbeing present in the jail.jail.name→jexecinto the existing jail;jail.root_path→ where the hook writes.env. The provision hook fires after the local test agent spawns successfully.- (Equivalent: send the same JSON line with the Python raw-socket helper used by the poller.)
Step 5 — verify
T=proof0; DB=/var/db/colibri/colibri.sqlite; R=/usr/local/bastille/jails/$T/root
# daemon log shows: "provisioning tenant env from vault" then "vault provision complete"
sudo stat -f '%Sp %N' "$R/.env" # expect -rw------- (0600)
sudo grep -c '^FIRST_PROOF_KEY=' "$R/.env" # expect 1 (value not printed)
sudo sqlite3 "$DB" "SELECT tenant_id,status FROM tenants WHERE tenant_id='$T';" # expect active
Pass = .env at 0600, key present, tenant status=active.
Cleanup (scratch proof)
T=proof0; DB=/var/db/colibri/colibri.sqlite
sudo sqlite3 "$DB" "DELETE FROM tenants WHERE tenant_id='$T';"
sudo rm -f /usr/local/bastille/jails/$T/root/.env
sudo bastille destroy "$T"
# delete the test Collection + item in Vaultwarden
Security notes
- Scratch jail + test collection only (first-proof policy) — no real tenant secrets.
Bootstrap creds (
BW_*) remain confined to the daemon'sprovider.env(0600); only the resolved.enventers the jail.
Follow-ups that retire the manual steps
- #101 register-tenant socket command + CLI → replaces step 3.
- #102
--jailflags oncolibri spawn-agent→ replaces step 4. - #92 path canonicalization/containment; #100 crate
bwhardening (server-match, serialize) — land before promoting beyond scratch.