feat(mother): add mother-sync-hive-keys — rebuild authorized_keys from vault
Some checks failed
CI / rust (pull_request) Has been cancelled
CI / markdown (pull_request) Has been cancelled
CI / port (pull_request) Has been cancelled
CI / agent-jail-pkgs (pull_request) Has been cancelled

Mother side of the vault-mediated hive key exchange (direction B — agents call
mother). Pulls the hive-pubkey-* items agents publish to Vaultwarden and rebuilds
the colibri user's authorized_keys, each entry restricted to the MCP command
(command="colibri-mcp",restrict,no-pty,no-*-forwarding).

- Rebuild, not append: deleting an agent's vault item revokes it next run.
- Fail-safe: a vault/login failure leaves authorized_keys untouched.
- Atomic write (mktemp + mv); colibri-owned 0600.
- Tunable via PROVIDER_ENV / COLIBRI_HOME / COLIBRI_USER / MCP_COMMAND
  (mother = osa for now; a dedicated host is a config change).
- Cron-driven (sample line in the header). Uses the bitwarden-cli-vault skill's
  session + authorized_keys-rebuild patterns.

sh -n clean; parse/rebuild core tested (filters non-key items, strips key
comments, applies the restriction wrapper).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Sam & Claude 2026-06-21 20:19:25 +02:00
parent f232bbeb69
commit 7f0782635d

View file

@ -0,0 +1,87 @@
#!/bin/sh
# mother-sync-hive-keys — rebuild the colibri user's authorized_keys from the
# `hive-pubkey-*` items that agents publish to Vaultwarden (clawdie-enable-mother).
#
# Direction: agents dial IN to this (mother) node and run `colibri-mcp`, so their
# pubkeys must be authorized here, each restricted to that one command. The file
# is REBUILT (not appended) every run, so removing an agent's vault item revokes
# its access on the next sync.
#
# Run periodically, e.g. cron (mother host):
# */5 * * * * /usr/local/bin/mother-sync-hive-keys >/dev/null 2>&1
#
# Fail-safe: a vault/login/network failure leaves the existing authorized_keys
# untouched (the file is only replaced after a successful fetch).
#
# Tunables (env): PROVIDER_ENV, COLIBRI_HOME, COLIBRI_USER, MCP_COMMAND.
set -eu
PROVIDER_ENV="${PROVIDER_ENV:-/usr/local/etc/colibri/provider.env}"
COLIBRI_HOME="${COLIBRI_HOME:-/var/db/colibri}"
COLIBRI_USER="${COLIBRI_USER:-colibri}"
MCP_COMMAND="${MCP_COMMAND:-colibri-mcp}"
AUTHKEYS="${COLIBRI_HOME}/.ssh/authorized_keys"
RESTRICT="command=\"${MCP_COMMAND}\",restrict,no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding"
log() { echo "mother-sync-hive-keys: $*" >&2; }
command -v bw >/dev/null 2>&1 || { log "bw not found"; exit 4; }
command -v python3 >/dev/null 2>&1 || { log "python3 not found"; exit 4; }
[ -r "$PROVIDER_ENV" ] || { log "cannot read ${PROVIDER_ENV}"; exit 1; }
# Load bootstrap creds (set -a so bw inherits them via the environment).
set -a
# shellcheck disable=SC1090
. "$PROVIDER_ENV"
set +a
[ -n "${BW_CLIENTID:-}" ] && [ -n "${BW_CLIENTSECRET:-}" ] && [ -n "${BW_PASSWORD:-}" ] ||
{ log "provider.env lacks BW_CLIENTID/BW_CLIENTSECRET/BW_PASSWORD"; exit 1; }
bw logout >/dev/null 2>&1 || true
bw login --apikey >/dev/null 2>&1 || true
SESS="$(bw unlock --passwordenv BW_PASSWORD --raw 2>/dev/null)" || { log "bw unlock failed"; exit 1; }
[ -n "$SESS" ] || { log "empty bw session"; exit 1; }
bw sync --session "$SESS" >/dev/null 2>&1 || true
# Pull hive pubkeys: items named hive-pubkey-* whose notes hold an ssh key.
# Emit "type keymaterial name" (comment/extra fields dropped) for safe wrapping.
if ! KEYS="$(bw list items --search "hive-pubkey-" --session "$SESS" 2>/dev/null | python3 -c "
import sys, json
try:
data = json.load(sys.stdin)
except Exception:
data = []
for i in data:
name = i.get('name', '')
notes = (i.get('notes') or '').strip()
if name.startswith('hive-pubkey-') and notes.startswith('ssh-'):
parts = notes.split()
if len(parts) >= 2:
print(parts[0] + ' ' + parts[1] + ' ' + name)
")"; then
log "vault fetch failed; leaving ${AUTHKEYS} untouched"
bw lock >/dev/null 2>&1 || true
exit 1
fi
bw lock >/dev/null 2>&1 || true
# Rebuild atomically. An empty result is legitimate (no agents → no access).
install -d -o "$COLIBRI_USER" -g "$COLIBRI_USER" -m 0700 "${COLIBRI_HOME}/.ssh"
tmp="$(mktemp "${COLIBRI_HOME}/.ssh/authorized_keys.XXXXXX")"
{
echo "# Managed by mother-sync-hive-keys — rebuilt from Vaultwarden hive-pubkey-* items."
echo "# Manual edits are overwritten on the next sync."
printf '%s\n' "$KEYS" | while IFS= read -r line; do
[ -n "$line" ] || continue
# shellcheck disable=SC2086
set -- $line
printf '%s %s %s %s\n' "$RESTRICT" "$1" "$2" "$3"
done
} >"$tmp"
chown "${COLIBRI_USER}:${COLIBRI_USER}" "$tmp"
chmod 0600 "$tmp"
mv "$tmp" "$AUTHKEYS"
_count="$(printf '%s\n' "$KEYS" | grep -c . 2>/dev/null || true)"
log "rebuilt ${AUTHKEYS} with ${_count} hive key(s)"