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>
This commit is contained in:
parent
c905e7a31c
commit
1af0e62942
5 changed files with 468 additions and 62 deletions
5
build.sh
5
build.sh
|
|
@ -1221,6 +1221,11 @@ configure_live_operator_session() {
|
|||
"${MOUNT_POINT}/usr/local/bin/clawdie-noblank-guard.sh"
|
||||
install -m 0755 "${LIVE_SESSION_DIR}/hw-report" \
|
||||
"${MOUNT_POINT}/usr/local/bin/hw-report"
|
||||
# Vaultwarden secret bridge (bw -> .env). Always staged: needs only the
|
||||
# bundled `bw` CLI and a 0600 bootstrap drop; absent bootstrap = no-op, so
|
||||
# the manual setup wizard stays the floor. See docs/VAULTWARDEN-SETUP.md.
|
||||
install -m 0755 "${LIVE_SESSION_DIR}/clawdie-vault-fetch" \
|
||||
"${MOUNT_POINT}/usr/local/bin/clawdie-vault-fetch"
|
||||
|
||||
# The stock FreeBSD memstick starts bsdinstall from /etc/rc.local before
|
||||
# our graphical live session can own the USB workflow. Preserve a copy for
|
||||
|
|
|
|||
|
|
@ -1,39 +1,56 @@
|
|||
#!/bin/sh
|
||||
# Clawdie operator USB live seed importer.
|
||||
# Reads operator-provided files from the FAT32 CLAWDIESEED partition
|
||||
# (mountable on Linux pre-flash) and installs them into the live system
|
||||
# at every boot. Idempotent by design: editing the seed and rebooting
|
||||
# re-applies it. Failure is never fatal — missing/empty/unmountable seed
|
||||
# logs and continues so the operator can still log in via SDDM.
|
||||
# (mountable on Linux/macOS/Windows pre- or post-flash) and installs them into
|
||||
# the live system at every boot. Idempotent by design: editing the seed and
|
||||
# rebooting re-applies it. Failure is never fatal — a missing/empty/unmountable
|
||||
# seed logs and continues so the operator can still log in via SDDM.
|
||||
#
|
||||
# Runs before LOGIN so sshd sees the imported authorized_keys on first
|
||||
# attach. See live/operator-session/clawdie-live-seed.README.txt for the
|
||||
# allowlisted file contract written to the FAT at build time.
|
||||
# Two layers of contract (see clawdie-live-seed.README.txt for the operator view):
|
||||
# 1. Legacy top-level allowlist: /authorized_keys, /ssh/authorized_keys.
|
||||
# 2. Per-agent directories: /<agent-name>/ holding env, soul/, harness.toml,
|
||||
# ssh/authorized_keys. On the live USB (single agent) the first agent dir
|
||||
# maps to the clawdie user; on a deployed host the importer loops every dir
|
||||
# (multi-agent provisioning is a documented follow-up seam).
|
||||
#
|
||||
# SECURITY: this partition is plaintext FAT32. By operator decision the env
|
||||
# files here may carry secrets (provider API keys, vault-bootstrap creds).
|
||||
# Treat seeded sticks as secret-bearing media. The importer lands secrets 0600
|
||||
# owned by the agent user and supports optional consume-and-shred (a /shred
|
||||
# marker file on the seed) to wipe env files after import.
|
||||
#
|
||||
# Runs before LOGIN so sshd sees imported authorized_keys on first attach.
|
||||
|
||||
# PROVIDE: clawdie_live_seed
|
||||
# REQUIRE: FILESYSTEMS devfs
|
||||
# BEFORE: LOGIN
|
||||
# KEYWORD: nojail
|
||||
|
||||
# rc.subr only exists on the FreeBSD target. Guard it so the import functions
|
||||
# below can be sourced and unit-tested on a non-FreeBSD host (CLAWDIE_SEED_TEST=1).
|
||||
if [ -r /etc/rc.subr ]; then
|
||||
. /etc/rc.subr
|
||||
fi
|
||||
|
||||
name="clawdie_live_seed"
|
||||
rcvar="${name}_enable"
|
||||
start_cmd="${name}_start"
|
||||
stop_cmd=":"
|
||||
# This service is one-shot/idempotent, not a daemon — the default
|
||||
# rc.subr status check looks for a pidfile and would say "not running"
|
||||
# even after a successful import, which is actively misleading. Override
|
||||
# with a log-tail report so `service clawdie_live_seed status` answers
|
||||
# the question the operator is actually asking ("did the import run?").
|
||||
status_cmd="${name}_status"
|
||||
extra_commands="status"
|
||||
|
||||
SEED_LABEL="CLAWDIESEED"
|
||||
SEED_MOUNT="/mnt/clawdie-seed"
|
||||
SEED_LOG="/var/log/clawdie-live-seed.log"
|
||||
SEED_USER="clawdie"
|
||||
SEED_USER_HOME="/home/clawdie"
|
||||
SEED_MOUNT="${SEED_MOUNT:-/mnt/clawdie-seed}"
|
||||
SEED_LOG="${SEED_LOG:-/var/log/clawdie-live-seed.log}"
|
||||
SEED_USER="${SEED_USER:-clawdie}"
|
||||
SEED_USER_HOME="${SEED_USER_HOME:-/home/clawdie}"
|
||||
# Where imported agent payloads are staged. Runtime consumption (loading a soul
|
||||
# into the agent workspace cwd, launching the chosen harness) reads from here.
|
||||
SEED_IMPORT_ROOT="${SEED_IMPORT_ROOT:-/var/db/clawdie/seed}"
|
||||
# Directory names reserved at the seed root (not treated as agent dirs).
|
||||
SEED_RESERVED_DIRS="ssh"
|
||||
# Valid harness values mirror Colibri's AgentRuntime enum (colibri-glasspane).
|
||||
SEED_VALID_HARNESSES="pi zot local"
|
||||
|
||||
_seed_log() {
|
||||
printf '%s %s\n' "$(date '+%Y-%m-%dT%H:%M:%S')" "$1" >>"${SEED_LOG}" 2>/dev/null || true
|
||||
|
|
@ -66,26 +83,189 @@ _seed_find_partition() {
|
|||
|
||||
_seed_install_authorized_keys() {
|
||||
_src="$1"
|
||||
_user="${2:-${SEED_USER}}"
|
||||
_home="${3:-${SEED_USER_HOME}}"
|
||||
|
||||
_ssh_dir="${SEED_USER_HOME}/.ssh"
|
||||
_ssh_dir="${_home}/.ssh"
|
||||
_dst="${_ssh_dir}/authorized_keys"
|
||||
|
||||
mkdir -p "${_ssh_dir}"
|
||||
chown "${SEED_USER}:${SEED_USER}" "${_ssh_dir}" 2>/dev/null || true
|
||||
chown "${_user}:${_user}" "${_ssh_dir}" 2>/dev/null || true
|
||||
chmod 0700 "${_ssh_dir}"
|
||||
|
||||
# Strip CRLF so keys created on Windows/Linux editors don't get rejected
|
||||
# by sshd for trailing whitespace.
|
||||
tr -d '\r' <"${_src}" >"${_dst}.new"
|
||||
mv -f "${_dst}.new" "${_dst}"
|
||||
chown "${SEED_USER}:${SEED_USER}" "${_dst}" 2>/dev/null || true
|
||||
chown "${_user}:${_user}" "${_dst}" 2>/dev/null || true
|
||||
chmod 0600 "${_dst}"
|
||||
|
||||
_seed_log "installed authorized_keys from ${_src} -> ${_dst}"
|
||||
}
|
||||
|
||||
# Merge KEY=VALUE pairs from a plaintext env file into a target .env, preserving
|
||||
# keys the source does not mention and replacing those it does. Lands 0600 owned
|
||||
# by the agent user. Mirrors clawdie-vault-fetch's --write-env upsert.
|
||||
_seed_import_env() {
|
||||
_src="$1"
|
||||
_dst="$2"
|
||||
_user="${3:-${SEED_USER}}"
|
||||
|
||||
[ -f "${_src}" ] || return 0
|
||||
|
||||
mkdir -p "$(dirname "${_dst}")"
|
||||
touch "${_dst}"
|
||||
chmod 0600 "${_dst}"
|
||||
|
||||
_merged="${_dst}.seedmerge"
|
||||
cp "${_dst}" "${_merged}" 2>/dev/null || : >"${_merged}"
|
||||
|
||||
_count=0
|
||||
while IFS= read -r _line || [ -n "${_line}" ]; do
|
||||
# Skip blanks, comments, and lines without a KEY=.
|
||||
case "${_line}" in
|
||||
''|'#'*) continue ;;
|
||||
*=*) : ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
_line="$(printf '%s' "${_line}" | tr -d '\r')"
|
||||
_k="${_line%%=*}"
|
||||
grep -v "^${_k}=" "${_merged}" >"${_merged}.tmp" 2>/dev/null || : >"${_merged}.tmp"
|
||||
mv "${_merged}.tmp" "${_merged}"
|
||||
printf '%s\n' "${_line}" >>"${_merged}"
|
||||
_count=$((_count + 1))
|
||||
done <"${_src}"
|
||||
|
||||
cp "${_merged}" "${_dst}"
|
||||
chmod 0600 "${_dst}"
|
||||
chown "${_user}:${_user}" "${_dst}" 2>/dev/null || true
|
||||
rm -f "${_merged}"
|
||||
_seed_log "imported ${_count} env key(s) from ${_src} -> ${_dst}"
|
||||
}
|
||||
|
||||
# Validate + record a harness descriptor. We only parse the `harness` value to
|
||||
# sanity-check it against the AgentRuntime enum; the full file is staged as-is
|
||||
# for the runtime to read.
|
||||
_seed_import_harness() {
|
||||
_src="$1"
|
||||
_stage="$2"
|
||||
|
||||
[ -f "${_src}" ] || return 0
|
||||
|
||||
_h="$(grep -E '^[[:space:]]*harness[[:space:]]*=' "${_src}" 2>/dev/null \
|
||||
| head -n 1 | sed -E 's/^[^=]*=[[:space:]]*"?([A-Za-z]+)"?.*/\1/' | tr 'A-Z' 'a-z')"
|
||||
_ok=0
|
||||
for _v in ${SEED_VALID_HARNESSES}; do
|
||||
[ "${_h}" = "${_v}" ] && _ok=1
|
||||
done
|
||||
if [ "${_ok}" -ne 1 ]; then
|
||||
_seed_log "WARN harness '${_h:-<none>}' in ${_src} not in {${SEED_VALID_HARNESSES}} — recording anyway"
|
||||
fi
|
||||
|
||||
mkdir -p "${_stage}"
|
||||
cp "${_src}" "${_stage}/harness.toml" 2>/dev/null || true
|
||||
_seed_log "recorded harness '${_h:-<none>}' -> ${_stage}/harness.toml"
|
||||
}
|
||||
|
||||
# Stage a soul/ backup tree for later consumption by the agent workspace.
|
||||
_seed_import_soul() {
|
||||
_src="$1"
|
||||
_stage="$2"
|
||||
|
||||
[ -d "${_src}" ] || return 0
|
||||
|
||||
mkdir -p "${_stage}/soul"
|
||||
# cp -R is portable; the tree is small (layered-soul ~600 KB).
|
||||
cp -R "${_src}/." "${_stage}/soul/" 2>/dev/null || true
|
||||
_seed_log "staged soul backup ${_src} -> ${_stage}/soul"
|
||||
}
|
||||
|
||||
# Import one /<agent-name>/ directory. On the live USB this is called once with
|
||||
# the clawdie user/home as the target; on a deployed host the caller loops.
|
||||
_seed_import_agent_dir() {
|
||||
_dir="$1" # absolute path to the agent dir on the mounted seed
|
||||
_agent="$2" # agent name (already validated)
|
||||
_user="$3"
|
||||
_home="$4"
|
||||
|
||||
_stage="${SEED_IMPORT_ROOT}/${_agent}"
|
||||
mkdir -p "${_stage}"
|
||||
printf '%s\n' "${_agent}" >"${_stage}/agent-name" 2>/dev/null || true
|
||||
|
||||
_seed_import_env "${_dir}/env" "${_home}/.env" "${_user}"
|
||||
_seed_import_harness "${_dir}/harness.toml" "${_stage}"
|
||||
_seed_import_soul "${_dir}/soul" "${_stage}"
|
||||
if [ -f "${_dir}/ssh/authorized_keys" ]; then
|
||||
_seed_install_authorized_keys "${_dir}/ssh/authorized_keys" "${_user}" "${_home}"
|
||||
fi
|
||||
|
||||
_seed_log "imported agent dir '${_agent}' -> stage ${_stage}, user ${_user}"
|
||||
}
|
||||
|
||||
# Return 0 if NAME is a safe agent directory name (no traversal, not reserved).
|
||||
_seed_agent_name_ok() {
|
||||
_n="$1"
|
||||
case "${_n}" in
|
||||
''|.|..|*/*) return 1 ;;
|
||||
esac
|
||||
# Allowlist characters to keep this off the filesystem's sharp edges.
|
||||
case "${_n}" in
|
||||
*[!A-Za-z0-9._-]*) return 1 ;;
|
||||
esac
|
||||
for _r in ${SEED_RESERVED_DIRS}; do
|
||||
[ "${_n}" = "${_r}" ] && return 1
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
# Core import routine, factored out of the rc start_cmd so it can be tested
|
||||
# against a pre-mounted directory (CLAWDIE_SEED_TEST). Operates on $SEED_MOUNT.
|
||||
_seed_import_tree() {
|
||||
_imported=0
|
||||
|
||||
# Layer 1: legacy top-level authorized_keys (default clawdie user).
|
||||
if [ -f "${SEED_MOUNT}/ssh/authorized_keys" ]; then
|
||||
_seed_install_authorized_keys "${SEED_MOUNT}/ssh/authorized_keys"
|
||||
_imported=1
|
||||
elif [ -f "${SEED_MOUNT}/authorized_keys" ]; then
|
||||
_seed_install_authorized_keys "${SEED_MOUNT}/authorized_keys"
|
||||
_imported=1
|
||||
fi
|
||||
|
||||
# Layer 2: per-agent directories. On the live USB (single agent) the first
|
||||
# valid dir, sorted, maps to the clawdie user. Additional dirs are staged
|
||||
# but flagged: deployed multi-agent provisioning is the follow-up seam.
|
||||
_first=1
|
||||
for _entry in "${SEED_MOUNT}"/*; do
|
||||
[ -d "${_entry}" ] || continue
|
||||
_agent="$(basename "${_entry}")"
|
||||
if ! _seed_agent_name_ok "${_agent}"; then
|
||||
[ "${_agent}" = "ssh" ] || _seed_log "skipping non-agent dir '${_agent}'"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ "${_first}" -eq 1 ]; then
|
||||
_seed_import_agent_dir "${_entry}" "${_agent}" "${SEED_USER}" "${SEED_USER_HOME}"
|
||||
printf '%s\n' "${_agent}" >"${SEED_IMPORT_ROOT}/active-agent" 2>/dev/null || true
|
||||
_first=0
|
||||
_imported=1
|
||||
else
|
||||
# Stage payload for visibility but do not provision a second live
|
||||
# identity — the live USB is single-agent.
|
||||
_seed_import_agent_dir "${_entry}" "${_agent}" "${SEED_USER}" "${SEED_USER_HOME}.${_agent}"
|
||||
_seed_log "NOTE additional agent dir '${_agent}' staged; deployed multi-agent provisioning not yet wired"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "${_imported}" -eq 0 ]; then
|
||||
_seed_log "no allowlisted files or agent dirs on seed — nothing to import"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
clawdie_live_seed_start() {
|
||||
: >>"${SEED_LOG}" 2>/dev/null || true
|
||||
mkdir -p "${SEED_IMPORT_ROOT}" 2>/dev/null || true
|
||||
|
||||
_dev=$(_seed_find_partition)
|
||||
if [ -z "${_dev:-}" ]; then
|
||||
|
|
@ -99,19 +279,20 @@ clawdie_live_seed_start() {
|
|||
return 0
|
||||
fi
|
||||
|
||||
# Allowlist: /ssh/authorized_keys takes precedence over /authorized_keys.
|
||||
# Any other file on the partition is ignored on purpose.
|
||||
_imported=0
|
||||
if [ -f "${SEED_MOUNT}/ssh/authorized_keys" ]; then
|
||||
_seed_install_authorized_keys "${SEED_MOUNT}/ssh/authorized_keys"
|
||||
_imported=1
|
||||
elif [ -f "${SEED_MOUNT}/authorized_keys" ]; then
|
||||
_seed_install_authorized_keys "${SEED_MOUNT}/authorized_keys"
|
||||
_imported=1
|
||||
fi
|
||||
_seed_import_tree
|
||||
|
||||
if [ "${_imported}" -eq 0 ]; then
|
||||
_seed_log "no allowlisted files on ${_dev} — nothing to import"
|
||||
# Optional consume-and-shred: a /shred marker on the seed asks us to wipe
|
||||
# env files after import so secrets do not persist on the stick. Requires a
|
||||
# brief RW remount; off unless the operator opts in per stick.
|
||||
if [ -f "${SEED_MOUNT}/shred" ]; then
|
||||
umount "${SEED_MOUNT}" 2>/dev/null || true
|
||||
if mount -t msdosfs "${_dev}" "${SEED_MOUNT}" 2>>"${SEED_LOG}"; then
|
||||
find "${SEED_MOUNT}" -name env -type f -exec rm -f {} + 2>/dev/null || true
|
||||
rm -f "${SEED_MOUNT}/shred" 2>/dev/null || true
|
||||
_seed_log "consume-and-shred: wiped env files from seed"
|
||||
else
|
||||
_seed_log "consume-and-shred requested but RW remount failed — env left on seed"
|
||||
fi
|
||||
fi
|
||||
|
||||
umount "${SEED_MOUNT}" 2>/dev/null || true
|
||||
|
|
@ -128,6 +309,12 @@ clawdie_live_seed_status() {
|
|||
return 1
|
||||
}
|
||||
|
||||
# On FreeBSD, hand off to rc.subr. Under test (no rc.subr / CLAWDIE_SEED_TEST),
|
||||
# skip it so the functions above can be exercised directly.
|
||||
if [ -n "${CLAWDIE_SEED_TEST:-}" ]; then
|
||||
:
|
||||
elif command -v run_rc_command >/dev/null 2>&1; then
|
||||
load_rc_config "$name"
|
||||
: "${clawdie_live_seed_enable:=YES}"
|
||||
run_rc_command "$1"
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -1,28 +1,29 @@
|
|||
CLAWDIE LIVE USB — SEED PARTITION
|
||||
=================================
|
||||
|
||||
This FAT32 partition lets you customize the live USB BEFORE flashing.
|
||||
On every boot, /usr/local/etc/rc.d/clawdie_live_seed imports a small,
|
||||
allowlisted set of files from this partition. Editing a file and
|
||||
This FAT32 partition lets you customize the live USB BEFORE flashing or
|
||||
between boots. On every boot, /usr/local/etc/rc.d/clawdie_live_seed imports
|
||||
an allowlisted set of files from this partition. Editing a file and
|
||||
rebooting re-applies it — the importer is idempotent.
|
||||
|
||||
USAGE FROM LINUX
|
||||
----------------
|
||||
USAGE FROM LINUX / macOS / WINDOWS
|
||||
----------------------------------
|
||||
|
||||
1. Flash the image to USB with dd (or write the .img directly).
|
||||
1. Flash the image to USB (dd, or write the .img directly).
|
||||
2. Mount the CLAWDIESEED partition (typically the third partition on the
|
||||
stick, e.g. /dev/sdX3):
|
||||
stick, e.g. /dev/sdX3 on Linux):
|
||||
|
||||
sudo mount -t vfat /dev/sdX3 /mnt/clawdie-seed
|
||||
|
||||
3. Drop seed files (see ALLOWLIST below).
|
||||
3. Drop seed files (see the two layers below).
|
||||
4. Unmount and boot the USB:
|
||||
|
||||
sync
|
||||
sudo umount /mnt/clawdie-seed
|
||||
|
||||
ALLOWLIST — files honored by the importer
|
||||
-----------------------------------------
|
||||
|
||||
LAYER 1 — SIMPLE ALLOWLIST (top level)
|
||||
--------------------------------------
|
||||
|
||||
/authorized_keys Public SSH keys for the operator account.
|
||||
Installed to ~clawdie/.ssh/authorized_keys
|
||||
|
|
@ -32,31 +33,87 @@ ALLOWLIST — files honored by the importer
|
|||
/ssh/authorized_keys Same as above, in a nested ssh/ namespace.
|
||||
Takes precedence over /authorized_keys.
|
||||
|
||||
Anything else on this partition is IGNORED on purpose. The importer logs
|
||||
to /var/log/clawdie-live-seed.log on the live system.
|
||||
|
||||
PLANNED (not yet active)
|
||||
------------------------
|
||||
LAYER 2 — PER-AGENT DIRECTORIES
|
||||
-------------------------------
|
||||
|
||||
These paths are reserved for future work; do not rely on them yet:
|
||||
Create one directory per agent. THE DIRECTORY NAME IS THE AGENT NAME.
|
||||
Inside it, any of these are honored:
|
||||
|
||||
/hostname Override the live hostname (default: clawdie-live).
|
||||
/tailscale-authkey One-shot Tailscale auth key for headless bring-up.
|
||||
/wifi.env WiFi SSID + PSK for first-boot wpa_supplicant.
|
||||
/<agent>/env Plaintext KEY=VALUE lines. Merged into the
|
||||
agent's .env (mode 0600). 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=...),
|
||||
and optionally the Vaultwarden bootstrap
|
||||
(BW_CLIENTID/BW_CLIENTSECRET/BW_PASSWORD).
|
||||
|
||||
SECURITY NOTES
|
||||
--------------
|
||||
/<agent>/harness.toml Which agent harness to run + basic knobs:
|
||||
|
||||
- This is FAT32 — any user with physical access can read and write it.
|
||||
- Public SSH keys are not secret; this is the right place for them.
|
||||
- Do NOT put private keys, long-lived API tokens, or passwords here.
|
||||
A future encrypted-seed format is the right home for that.
|
||||
harness = "zot" # zot | pi | local
|
||||
model = "claude-opus-4-8"
|
||||
cost_mode = "smart"
|
||||
|
||||
`harness` must be one of zot, pi, local
|
||||
(Colibri's AgentRuntime). Recorded for the
|
||||
runtime to launch the right harness.
|
||||
|
||||
/<agent>/soul/ A layered-soul backup tree (SOUL.md, USER.md,
|
||||
IDENTITY.md, memories/, skills/, ...). Staged
|
||||
under /var/db/clawdie/seed/<agent>/soul for
|
||||
the agent workspace to load.
|
||||
|
||||
/<agent>/ssh/authorized_keys Public SSH keys for this agent.
|
||||
|
||||
Agent directory names may contain only A-Z a-z 0-9 . _ - (no spaces or
|
||||
slashes). The name `ssh` is reserved for Layer 1.
|
||||
|
||||
LIVE USB vs DEPLOYED
|
||||
--------------------
|
||||
|
||||
The live USB is single-agent: the FIRST agent directory (alphabetical) maps
|
||||
to the clawdie user and becomes the active agent (recorded at
|
||||
/var/db/clawdie/seed/active-agent). Additional agent directories are staged
|
||||
and logged, but a second live identity is NOT provisioned here — multi-agent
|
||||
provisioning is a deployed-host feature.
|
||||
|
||||
|
||||
CONSUME-AND-SHRED (optional)
|
||||
----------------------------
|
||||
|
||||
Drop an empty file named `shred` at the seed root to have the importer wipe
|
||||
all `env` files from this partition AFTER importing them, so secrets do not
|
||||
persist on the stick:
|
||||
|
||||
/shred
|
||||
|
||||
This needs a writable seed; if the remount fails the env files are left in
|
||||
place and the importer logs it. Off unless you add the marker, per stick.
|
||||
|
||||
|
||||
SECURITY — READ THIS
|
||||
--------------------
|
||||
|
||||
- This is FAT32: UNENCRYPTED and readable by anyone who plugs the stick
|
||||
into any machine. There is no access control on this partition.
|
||||
- By operator decision, env files here MAY carry secrets (API keys, and
|
||||
the Vaultwarden bootstrap, which includes the master password). That is a
|
||||
deliberate trade-off: treat every seeded stick as SECRET-BEARING MEDIA.
|
||||
Do not lose it; do not lend it; prefer `shred` for one-shot provisioning.
|
||||
- Imported secrets land mode 0600 owned by the agent user. Public SSH keys
|
||||
are not secret and are always safe to place here.
|
||||
- The importer runs at every boot. Removing a file from the seed and
|
||||
rebooting does NOT remove the previously-installed copy from the
|
||||
live system; re-flash the image to wipe state.
|
||||
rebooting does NOT remove an already-installed copy from the live system;
|
||||
re-flash the image to wipe state.
|
||||
|
||||
The importer logs to /var/log/clawdie-live-seed.log
|
||||
(`service clawdie_live_seed status` tails it).
|
||||
|
||||
|
||||
CONTACT
|
||||
-------
|
||||
|
||||
clawdie.si — repository: clawdie-iso, file:
|
||||
clawdie.si — repository: clawdie-iso, files:
|
||||
live/operator-session/clawdie-live-seed
|
||||
live/operator-session/clawdie-live-seed.README.txt
|
||||
|
|
|
|||
152
live/operator-session/clawdie-vault-fetch
Normal file
152
live/operator-session/clawdie-vault-fetch
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
#!/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
|
||||
|
|
@ -5,3 +5,8 @@
|
|||
# npm's moving latest dist-tag during ISO builds.
|
||||
|
||||
@earendil-works/pi-coding-agent@0.78.0
|
||||
|
||||
# Bitwarden CLI (`bw`) — headless access to the Clawdie Vaultwarden instance,
|
||||
# used by clawdie-vault-fetch. Bundled offline so a booted image can pull agent
|
||||
# secrets without a network npm install. See clawdie-ai/docs/VAULTWARDEN-SETUP.md.
|
||||
@bitwarden/cli@2026.5.0
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue