feat(seed): zero-touch boot from a personalized seed (provider keys → provider.env) #110

Merged
clawdie merged 1 commit from seed-zero-touch-provisioning into main 2026-06-22 08:57:50 +02:00
4 changed files with 61 additions and 18 deletions

View file

@ -42,23 +42,35 @@ just double-clicks "Join Hive" and the fetch runs automatically.
| Join Hive | `live/operator-session/clawdie-join-hive.sh` | Desktop launcher: checks for creds → prompts or auto-fetches |
| Seed README | `live/operator-session/clawdie-live-seed.README.txt` | Operator-facing seed partition instructions |
## Remaining work: delete the click
## Zero-touch: direct keys on the seed (implemented)
The seed partition path reduces onboarding to ONE double-click. Making it
zero-touch requires:
The click only existed to trigger the Vaultwarden round-trip. For a
personalized stick that already holds the real provider keys, that round-trip
is unnecessary — and the boot ordering already favors zero-touch:
1. **xdg autostart entry** — on first login, if BW\_\* creds are present in
provider.env AND `colibri_daemon` is not yet fully provisioned, auto-run
`clawdie-vault-fetch --write-env <provider.env>` and restart the daemon.
- `clawdie_live_seed` runs as root, `BEFORE: LOGIN`.
- `colibri_daemon` runs `REQUIRE: LOGIN` — strictly after.
2. **First-boot guard** — run once per boot only (not on every login). A
sentinel file (`/var/db/clawdie/vault-fetched`) prevents re-triggering.
So the importer now merges the active agent's direct provider keys (everything
except `BW_*`) into the daemon's `provider.env` as well as the operator's
`~/.env`. The daemon starts afterward, finds `DEEPSEEK_API_KEY`, and
auto-spawns the agent (`COLIBRI_AUTOSPAWN_PI=YES`) — no click, no vault
round-trip, no typing. `BW_*` still route to `~/.config/vault-bootstrap.env`
for operators who prefer the vault-fetch path.
3. **START-HERE.txt update** — remove the "add your 3 BW secrets" section
when the seed partition is present. Show a confirmation instead:
"Secrets loaded from seed partition. Colibri is starting."
This makes the **personalized seed** the onboarding primitive:
Estimated: ~30 lines of shell.
- The image stays generic and publishable; secrets are never baked in.
- The seed (FAT32 `CLAWDIESEED`) is the personalization layer and stays
physical/offline — `env` (direct keys + optional `TAILSCALE_AUTH_KEY`),
`harness.toml`, `soul/`, `ssh/authorized_keys`, optional `/shred` to wipe
keys off the stick after first import.
- The seed can be generated by an agent (e.g. Hermes) that already holds the
soul and key material, written straight onto the mounted partition.
This supersedes the earlier xdg-autostart "delete the click" plan: it removes
the click for free without a first-login sentinel or a network dependency at
first boot.
## Related: one-secret path (future)

View file

@ -55,6 +55,11 @@ LIVE SEED
Seed partition label:
CLAWDIESEED
If this stick was seeded with provider keys, there is nothing to do: the
agent's keys were loaded before the daemon started, so Colibri auto-spawns
the agent on boot. Check with:
colibri status
Readable operator guide:
/usr/local/share/clawdie-iso/seed/README.txt

View file

@ -54,6 +54,12 @@ SEED_VALID_HARNESSES="pi zot local"
# Vaultwarden bootstrap creds are routed out of .env into this file (relative to
# the agent home) so clawdie-vault-fetch can consume them.
SEED_VAULT_BOOTSTRAP_REL=".config/vault-bootstrap.env"
# colibri_daemon reads provider keys from this file (rc.conf
# colibri_daemon_provider_env), NOT the operator's ~/.env. The active agent's
# direct provider keys are merged here too so the daemon auto-spawns at boot
# from a seeded stick with no operator action (zero-touch provisioning). The
# importer runs as root before LOGIN, so it can write this root-owned file.
SEED_PROVIDER_ENV="${SEED_PROVIDER_ENV:-/usr/local/etc/colibri/provider.env}"
_seed_log() {
printf '%s %s\n' "$(date '+%Y-%m-%dT%H:%M:%S')" "$1" >>"${SEED_LOG}" 2>/dev/null || true
@ -261,6 +267,14 @@ _seed_activate_agent() {
if [ -f "${_dir}/env" ]; then
_seed_split_env "${_dir}/env" "${_stage}"
_seed_merge_env "${_stage}/.app.env" "${_home}/.env" "${_user}"
# Feed the daemon too: colibri_daemon reads provider.env, not ~/.env.
# Routing the active agent's provider keys here lets a seeded stick boot
# straight into an auto-spawned agent — no Join Hive click, no vault
# round-trip. Lands root-owned 0600 (the importer is root, pre-LOGIN).
if [ -s "${_stage}/.app.env" ]; then
_seed_merge_env "${_stage}/.app.env" "${SEED_PROVIDER_ENV}" root
_seed_log "merged active-agent provider keys -> ${SEED_PROVIDER_ENV}"
fi
if [ -s "${_stage}/.boot.env" ]; then
_seed_merge_env "${_stage}/.boot.env" "${_home}/${SEED_VAULT_BOOTSTRAP_REL}" "${_user}"
_seed_log "routed Vaultwarden bootstrap creds -> ${_home}/${SEED_VAULT_BOOTSTRAP_REL}"

View file

@ -40,14 +40,26 @@ LAYER 2 — PER-AGENT DIRECTORIES
Create one directory per agent. THE DIRECTORY NAME IS THE AGENT NAME.
Inside it, any of these are honored:
/<agent>/env Plaintext KEY=VALUE lines. Merged into the
agent's .env (mode 0600). Keys you list
/<agent>/env Plaintext KEY=VALUE lines. Keys you list
replace existing values; keys you omit are
preserved. Blank/`#` lines are ignored.
Typical contents: provider API keys
(ANTHROPIC_API_KEY=..., ZAI_API_KEY=...),
or the Vaultwarden bootstrap
(BW_CLIENTID/BW_CLIENTSECRET/BW_PASSWORD).
Routing for the ACTIVE agent:
- Provider API keys and toggles
(DEEPSEEK_API_KEY=..., OPENROUTER_API_KEY=...,
COLIBRI_AUTOSPAWN_PI=YES, ...) are merged
into BOTH the agent's ~/.env AND the daemon's
/usr/local/etc/colibri/provider.env (mode
0600). Because the importer runs before the
daemon starts, a seeded provider key makes
colibri_daemon auto-spawn the agent on first
boot with NO operator action — fully
zero-touch. No Vaultwarden round-trip needed.
- Vaultwarden bootstrap creds
(BW_CLIENTID/BW_CLIENTSECRET/BW_PASSWORD) are
routed to ~/.config/vault-bootstrap.env for
clawdie-vault-fetch — use these only if you
want the vault-fetch path instead of direct
keys.
The Vaultwarden endpoint is baked into the
image; do not put it on the seed unless you
are deliberately overriding it.