clawdie-iso/live/operator-session/clawdie-vault-fetch
Sam & Claude 1af0e62942 Wire encrypted secrets: Vaultwarden fetch + per-agent seed import
Two parallel, additive paths so a host gets its secrets out of the box;
the manual setup wizard stays the floor (no config = no-op).

clawdie-vault-fetch (new): language-neutral bw bridge. Reads a 0600
~/.config/vault-bootstrap.env, pulls keys from the agent-secrets
collection (item name = env var name, value in password field, so no jq),
prints KEY=VALUE or --write-env upserts 0600. Exit codes distinguish
skip (3, no bootstrap) / broken (1) / no bw (4). Pinned
@bitwarden/cli@2026.5.0 for offline bundling; staged in
configure_live_operator_session.

clawdie-live-seed: extend the CLAWDIESEED FAT32 importer from the
authorized_keys allowlist to a per-agent directory convention —
/<agent>/ with env (merged 0600), harness.toml (pi|zot|local), soul/
(staged), ssh/authorized_keys. Live USB single-agent (first dir = active);
extra dirs staged + flagged for deployed multi-agent. Optional
consume-and-shred. Import core is unit-testable via CLAWDIE_SEED_TEST.

README rewritten to document the per-agent contract and the operator
decision to allow plaintext secrets on the seed (seeded sticks are
secret-bearing media; 0600 landing + shred mitigations).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 17:27:01 +02:00

152 lines
5.6 KiB
Bash

#!/bin/sh
# clawdie-vault-fetch — pull agent secrets from Vaultwarden into the host.
#
# Language-neutral bridge between the Clawdie Vaultwarden instance and a host's
# secret store. The TS post-install setup flow shells out to this; the live USB
# and firstboot can call it directly. It depends only on the `bw` (Bitwarden)
# CLI — no node module, no jq — so the same helper serves the deployed disk and
# the live operator image (see docs in clawdie-ai/docs/VAULTWARDEN-SETUP.md).
#
# Contract: each secret is one login item in the `agent-secrets` collection
# whose ITEM NAME is exactly the env var name (e.g. ANTHROPIC_API_KEY) with the
# value in the password field. `bw get password <NAME>` then returns it raw.
#
# Bootstrap (the one secret that can't live in the vault) is read from a 0600
# env file holding BW_CLIENTID / BW_CLIENTSECRET / BW_PASSWORD. Absent file =>
# nothing to do (exit 3), so the manual setup wizard remains the floor.
#
# Usage:
# clawdie-vault-fetch # print KEY=VALUE lines to stdout
# clawdie-vault-fetch --write-env FILE # upsert results into FILE (0600)
# clawdie-vault-fetch --bootstrap FILE # explicit bootstrap env file
# clawdie-vault-fetch --keys "A B C" # override the key name list
#
# Exit codes (so callers can distinguish "skip" from "broken"):
# 0 ran cleanly (zero or more keys printed/written)
# 1 vault was configured but login/unlock/fetch failed
# 3 no bootstrap config found — caller should fall back to manual entry
# 4 `bw` CLI not installed
set -eu
SERVER="${VAULT_SERVER:-https://vault.smilepowered.org}"
# agent-secrets collection in the Clawdie org. Overridable for other vaults.
COLLECTION_ID="${VAULT_COLLECTION_ID:-94ba61b8-633c-454e-b749-f115617eeac3}"
BOOTSTRAP_FILE="${VAULT_BOOTSTRAP_FILE:-${HOME}/.config/vault-bootstrap.env}"
WRITE_ENV=""
# Provider key names mirror clawdie-ai's PROVIDER_KEY_BY_PROVIDER (the non-null
# ones). Extend via --keys or VAULT_FETCH_KEYS without editing this file.
KEYS="${VAULT_FETCH_KEYS:-ANTHROPIC_API_KEY OPENAI_API_KEY OPENROUTER_API_KEY ZAI_API_KEY DEEPSEEK_API_KEY GEMINI_API_KEY GROQ_API_KEY}"
usage() {
echo "usage: clawdie-vault-fetch [--write-env FILE] [--bootstrap FILE] [--keys \"K1 K2\"]"
exit "${1:-0}"
}
while [ $# -gt 0 ]; do
case "$1" in
--write-env) WRITE_ENV="${2:?--write-env needs a value}"; shift 2 ;;
--bootstrap) BOOTSTRAP_FILE="${2:?--bootstrap needs a value}"; shift 2 ;;
--keys) KEYS="${2:?--keys needs a value}"; shift 2 ;;
-h|--help) usage 0 ;;
*) echo "clawdie-vault-fetch: unknown argument: $1" >&2; usage 1 ;;
esac
done
log() { echo "clawdie-vault-fetch: $*" >&2; }
# No vault intent on this host => skip quietly so the manual path is unaffected.
# Checked before the bw probe: a host with no bootstrap drop shouldn't care
# whether the CLI is installed.
if [ ! -f "$BOOTSTRAP_FILE" ]; then
log "no bootstrap file at $BOOTSTRAP_FILE — skipping vault fetch"
exit 3
fi
if ! command -v bw >/dev/null 2>&1; then
log "bw (Bitwarden CLI) not found — install @bitwarden/cli first"
exit 4
fi
# Load bootstrap creds without echoing them. set -a so they reach bw via env.
set -a
# shellcheck disable=SC1090
. "$BOOTSTRAP_FILE"
set +a
if [ -z "${BW_CLIENTID:-}" ] || [ -z "${BW_CLIENTSECRET:-}" ] || [ -z "${BW_PASSWORD:-}" ]; then
log "bootstrap file is missing BW_CLIENTID / BW_CLIENTSECRET / BW_PASSWORD"
exit 1
fi
WORK="$(mktemp -d "${TMPDIR:-/tmp}/clawdie-vault.XXXXXX")"
# Lock the vault on any exit; never leave an unlocked session behind.
cleanup() {
bw lock >/dev/null 2>&1 || true
rm -rf "$WORK"
}
trap cleanup EXIT INT TERM
bw config server "$SERVER" >/dev/null 2>&1 || {
log "could not set bw server to $SERVER"
exit 1
}
# login --apikey reads BW_CLIENTID/BW_CLIENTSECRET from env. Tolerate the
# "already logged in" case so repeat runs don't fail.
if ! bw login --apikey >/dev/null 2>"$WORK/login.err"; then
if ! grep -qi 'already logged in' "$WORK/login.err"; then
log "bw login failed:"
sed 's/^/ /' "$WORK/login.err" >&2
exit 1
fi
fi
SESSION="$(bw unlock --raw --passwordenv BW_PASSWORD 2>"$WORK/unlock.err")" || {
log "bw unlock failed:"
sed 's/^/ /' "$WORK/unlock.err" >&2
exit 1
}
if [ -z "$SESSION" ]; then
log "bw unlock returned an empty session"
exit 1
fi
# Make sure local cache reflects the server (web-UI edits don't sync otherwise).
bw sync --session "$SESSION" >/dev/null 2>&1 || true
# Collect KEY=VALUE pairs for every key name that resolves to a value. A missing
# item is not an error — the operator may only have stored a subset.
found=0
out="$WORK/out.env"
: >"$out"
for key in $KEYS; do
val="$(bw get password "$key" --session "$SESSION" 2>/dev/null)" || continue
[ -n "$val" ] || continue
printf '%s=%s\n' "$key" "$val" >>"$out"
found=$((found + 1))
done
log "resolved $found of $(echo "$KEYS" | wc -w | tr -d ' ') key(s) from agent-secrets"
if [ -n "$WRITE_ENV" ]; then
# Upsert into the target env file at 0600 without disturbing other keys.
touch "$WRITE_ENV"
chmod 0600 "$WRITE_ENV"
merged="$WORK/merged.env"
cp "$WRITE_ENV" "$merged" 2>/dev/null || : >"$merged"
while IFS= read -r line; do
k="${line%%=*}"
# Drop any existing definition of this key, then append the new one.
grep -v "^${k}=" "$merged" >"$merged.tmp" 2>/dev/null || : >"$merged.tmp"
mv "$merged.tmp" "$merged"
printf '%s\n' "$line" >>"$merged"
done <"$out"
cp "$merged" "$WRITE_ENV"
chmod 0600 "$WRITE_ENV"
log "wrote $found key(s) into $WRITE_ENV"
else
cat "$out"
fi
exit 0