cloverleaf-larry: one-arg self-contained broker install <client-id> (Bryan-direct P1, approved)

Adds a positional <client-id> self-enroll path to install-larry.sh:
  curl -fsSL .../install-larry.sh | bash -s <client-id>
POSTs {"client_id":<slug>} to the broker's one-shot /enroll-claim endpoint
(no admin secret to copy/paste), receives deployment_id/enroll_secret/profile,
writes $LARRY_HOME/.deployment-id + .enroll-secret (0600), bakes broker env
into the larry shim. Second run of the same client-id fails replay-safe
(409 already_enrolled, zero files written) -- by design, not a bug. Mutually
exclusive with the existing manual LARRY_DEPLOYMENT_ID/LARRY_ENROLL_SECRET
env-var path, which is otherwise unchanged. --uninstall / --uninstall --yes
paths unchanged (dry-run default, --yes for full removal).

Broker-side half (registry.claim_enrollment/client_slug, main.py
/enroll-claim, broker_ctl.py set-client-slug/reopen-enroll) already deployed
live on the broker container since 2026-07-09, sha256-verified matching --
this commit is the client-side half landing to close out the task.

Note: install-larry.sh's own comment refers to this as "v0.9.8+" but VERSION
and CHANGELOG.md are deliberately NOT bumped in this commit -- that wasn't
part of the reviewed/gated diff (install-larry.sh only), and adding it here
would be new unreviewed content. Flagging as a small follow-up nit, not
blocking.

Gated:
  - Vera CODE-PASS+LIVE-PASS, 2026-07-09, token dc705af1-0e1f-41ca-b1d2-7e3446961680
    (fresh throwaway deployment, real public broker, real mint + real LLM
    reply, replay-safe, clean uninstall, kill-switch fail-closed even
    through a reopened claim window)
  - Vera TTL re-confirm CODE-PASS, 2026-07-21, token 598051ce-5c31-434e-aaa1-6cf4e3a549fb
    (zero content drift, upstream HEAD unmoved, from-scratch patch-apply
    reproduces the gated artifact byte-for-byte)
  - Fresh independent live 6/6 round-trip re-run, 2026-07-21 (Clover):
    provision -> install -> mint+LLM -> replay-safe -> uninstall ->
    kill-switch, all PASS against the real public broker, throwaway
    registry row created and cleaned up (test-harness.sh, task
    tsk-2026-07-09-install-larry-one-arg-selfcontained)

Bryan-direct authorization to push: "push it now -- yes" (relayed via Larry).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Server Larry 2026-07-21 13:39:51 -07:00 committed by bj
parent be8af282ef
commit 6900369a41

View File

@ -2,8 +2,27 @@
# install-larry.sh — bootstrap Larry-Anywhere on a fresh remote shell. # install-larry.sh — bootstrap Larry-Anywhere on a fresh remote shell.
# No root, no package install, no sudo. Writes only into $LARRY_HOME. # No root, no package install, no sudo. Writes only into $LARRY_HOME.
# #
# Usage: # Usage (v0.9.8+ — ONE-ARG self-contained broker install, Bryan-approved 2026-07-09):
# curl -fsSL <BASE_URL>/install-larry.sh | bash -s <client-id>
# Self-enrolls against the broker using ONLY the client-id — no manual env
# vars, no secret to paste. The broker (which the box owner already
# provisioned with `provision_broker_client.sh <dep-id> --open-enroll
# <client-id>`) hands back the deployment-id + a fresh enrollment secret
# ONCE. A second run of the same client-id fails replay-safe (the broker
# returns 409 already_enrolled) — that is BY DESIGN, not a bug; the trust
# boundary is the single admin-opened claim window, closed either by the
# first successful claim or by the kill-switch (broker_ctl set-authorized
# <dep-id> false). See "ONE-ARG SELF-ENROLL" below.
#
# curl -fsSL <BASE_URL>/install-larry.sh | bash -s -- --uninstall [flags]
# Fetches uninstall-larry.sh fresh and runs it (dry-run by default, same
# as running it directly — pass --yes to actually delete). See its own
# --help for --keep-data / --keep-rc / --no-shred.
#
# curl -fsSL <BASE_URL>/install-larry.sh | bash # curl -fsSL <BASE_URL>/install-larry.sh | bash
# (no args) — existing interactive/apikey path, UNCHANGED. Also still
# accepts the original manual broker env vars below if you already have a
# secret (e.g. from the admin `enroll` flow rather than open-enroll).
# #
# Or with explicit base URL: # Or with explicit base URL:
# LARRY_BASE_URL=https://example.com/larry-anywhere bash install-larry.sh # LARRY_BASE_URL=https://example.com/larry-anywhere bash install-larry.sh
@ -14,6 +33,14 @@
# LARRY_BIN_DIR where to symlink the `larry` command (default: $HOME/bin) # LARRY_BIN_DIR where to symlink the `larry` command (default: $HOME/bin)
# LARRY_GITEA_TOKEN optional Gitea PAT (read scope) for authenticated fetch # LARRY_GITEA_TOKEN optional Gitea PAT (read scope) for authenticated fetch
# against a PRIVATE repo. Alias: GITEA_TOKEN. Never logged. # against a PRIVATE repo. Alias: GITEA_TOKEN. Never logged.
# LARRY_BROKER_URL broker base URL. Defaults to the public broker
# (https://broker.bjnoela.com) when a <client-id> arg is
# given; override for a dev/staging broker.
# LARRY_DEPLOYMENT_ID / LARRY_ENROLL_SECRET / LARRY_PROFILE
# the ORIGINAL manual broker-enrollment path (v0.9.0) —
# still supported, unchanged, for operators who already
# hold a secret from the admin `enroll` flow. Mutually
# exclusive with the <client-id> positional arg.
set -eu set -eu
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}" LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
@ -124,6 +151,42 @@ fetch_validate() {
} }
# <<< fetch-safe inline <<< # <<< fetch-safe inline <<<
# ─────────────────────────────────────────────────────────────────────────────
# v0.9.8 — CLI arg parsing: <client-id> (one-arg self-enroll) | --uninstall |
# (no args, unchanged). Deliberately minimal — this installer has never had
# positional-arg parsing before, so there is no legacy behavior to preserve
# beyond "no args = today's interactive/apikey/manual-broker-env path".
#
# NOTE: SCRIPT_DIR (the local-checkout fallback path, used elsewhere by
# fetch()) is NOT YET DEFINED at this point in the script (it's computed much
# later, in the "Fetch the scripts" section) — this block runs under `set -eu`
# so any reference here MUST use the ${VAR:-} default-guard form, never a bare
# "$SCRIPT_DIR", or a plain unset-variable reference would abort the whole
# script with "unbound variable" before reaching any of our own error
# messages. The --uninstall path below relies on this guard.
# ─────────────────────────────────────────────────────────────────────────────
LARRY_CLIENT_ID=""
if [ "${1:-}" = "--uninstall" ]; then
shift
say "uninstall requested — fetching uninstall-larry.sh fresh from origin"
_un_tmp="$(mktemp 2>/dev/null || echo "/tmp/uninstall-larry.$$.sh")"
if [ -n "$LARRY_BASE_URL" ] && fetch_validate "$LARRY_BASE_URL/uninstall-larry.sh" "$_un_tmp" sh 30; then
chmod +x "$_un_tmp" 2>/dev/null || true
exec bash "$_un_tmp" "$@"
elif [ -n "${SCRIPT_DIR:-}" ] && [ -f "${SCRIPT_DIR:-}/uninstall-larry.sh" ]; then
exec bash "$SCRIPT_DIR/uninstall-larry.sh" "$@"
else
rm -f "$_un_tmp"
die "could not fetch uninstall-larry.sh from LARRY_BASE_URL=$LARRY_BASE_URL — run it directly instead: bash \$LARRY_HOME/uninstall-larry.sh $*"
fi
elif [ $# -gt 0 ]; then
case "$1" in
-*) die "unknown flag: $1 (usage: install-larry.sh [<client-id>] | install-larry.sh --uninstall [flags])" ;;
*) LARRY_CLIENT_ID="$1"; shift
[ $# -eq 0 ] || die "unexpected extra argument(s) after client-id '$LARRY_CLIENT_ID': $*" ;;
esac
fi
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# Detect platform # Detect platform
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
@ -446,12 +509,92 @@ else
esac esac
fi fi
# ─────────────────────────────────────────────────────────────────────────────
# v0.9.8 — ONE-ARG SELF-ENROLL (client-id argument drives EVERYTHING: broker
# URL, deployment-id lookup, profile, AND the enrollment secret itself — zero
# manual env vars, matching Bryan's approved 2026-07-09 design).
#
# Calls the broker's one-shot POST /enroll-claim {"client_id": "<id>"} — no
# secret in the request. Only succeeds for a deployment the broker owner
# already provisioned + left claim-window-open via:
# provision_broker_client.sh <dep-id> --client "<Label>" --open-enroll <client-id>
# and only ONCE: a second claim on the same client-id is a fail-closed 409
# ("already enrolled") — replay-safe BY DESIGN, not a bug. If a box needs to
# re-claim (e.g. it lost its secret before finishing install), the broker
# owner reopens the window (`broker_ctl.py reopen-enroll <dep-id>`) rather
# than the box retrying on its own.
#
# This is ADDITIVE, not a replacement: it just POPULATES LARRY_DEPLOYMENT_ID /
# LARRY_ENROLL_SECRET / LARRY_PROFILE / LARRY_BROKER_URL so the EXISTING
# v0.9.0 manual-env broker-enrollment block below picks them up completely
# unchanged. If an operator already has a secret from the admin `enroll` flow,
# they can still pass LARRY_DEPLOYMENT_ID/LARRY_ENROLL_SECRET manually with NO
# client-id arg — that path is untouched.
# ─────────────────────────────────────────────────────────────────────────────
LARRY_BROKER_URL_DEFAULT="https://broker.bjnoela.com"
if [ -n "$LARRY_CLIENT_ID" ]; then
if [ -n "${LARRY_DEPLOYMENT_ID:-}" ] || [ -n "${LARRY_ENROLL_SECRET:-}" ]; then
die "a client-id argument ('$LARRY_CLIENT_ID') and manual LARRY_DEPLOYMENT_ID/LARRY_ENROLL_SECRET env vars were both given — use ONE method, not both."
fi
LARRY_BROKER_URL="${LARRY_BROKER_URL:-$LARRY_BROKER_URL_DEFAULT}"
say "self-enrolling as client-id '${LARRY_CLIENT_ID}' against broker ${LARRY_BROKER_URL} ..."
command -v curl >/dev/null 2>&1 || die "curl not found — required for broker self-enroll"
_claim_id_json="$(printf '%s' "$LARRY_CLIENT_ID" | sed 's/\\/\\\\/g; s/"/\\"/g')"
_claim_tmp="$(mktemp 2>/dev/null || echo "${LARRY_HOME}/.claim.$$.json")"
_claim_code="$(curl -sS --max-time 20 -w '%{http_code}' -o "$_claim_tmp" \
-H 'Content-Type: application/json' \
-X POST "${LARRY_BROKER_URL}/enroll-claim" \
-d "{\"client_id\":\"${_claim_id_json}\"}" 2>/dev/null)"
_claim_rc=$?
if [ "$_claim_rc" != "0" ]; then
rm -f "$_claim_tmp"
die "broker unreachable at ${LARRY_BROKER_URL} (curl rc=${_claim_rc}) — check network and retry. If you're on a restricted network, verify you can reach ${LARRY_BROKER_URL}/healthz."
fi
case "$_claim_code" in
200)
# Bare grep/sed extraction (no jq dependency at this point in install).
# Tolerant of key order; JSON body has no nesting so this is safe.
LARRY_DEPLOYMENT_ID="$(grep -o '"deployment_id"[[:space:]]*:[[:space:]]*"[^"]*"' "$_claim_tmp" | sed -E 's/.*:[[:space:]]*"([^"]*)"/\1/')"
LARRY_ENROLL_SECRET="$(grep -o '"enroll_secret"[[:space:]]*:[[:space:]]*"[^"]*"' "$_claim_tmp" | sed -E 's/.*:[[:space:]]*"([^"]*)"/\1/')"
LARRY_PROFILE="$(grep -o '"profile"[[:space:]]*:[[:space:]]*"[^"]*"' "$_claim_tmp" | sed -E 's/.*:[[:space:]]*"([^"]*)"/\1/')"
rm -f "$_claim_tmp"
if [ -z "$LARRY_DEPLOYMENT_ID" ] || [ -z "$LARRY_ENROLL_SECRET" ]; then
die "broker returned 200 but the response body was unparseable — aborting BEFORE writing any secret. This looks like a broker-side bug; nothing was leaked to stdout/logs. Report it."
fi
export LARRY_DEPLOYMENT_ID LARRY_ENROLL_SECRET LARRY_PROFILE LARRY_BROKER_URL
ok "claimed deployment '${LARRY_DEPLOYMENT_ID}' (profile=${LARRY_PROFILE:-default}) — enrollment secret received, never printed"
;;
409)
rm -f "$_claim_tmp"
die "client-id '${LARRY_CLIENT_ID}' is already enrolled (one-shot claim already used). This is expected on a second run of the same install command — it is NOT a retry-able error. If this box genuinely never finished enrolling (e.g. it lost power mid-install), ask whoever provisioned you to re-open the window: provision_broker_client.sh <dep-id> --client <Label> --open-enroll ${LARRY_CLIENT_ID}"
;;
404)
rm -f "$_claim_tmp"
die "client-id '${LARRY_CLIENT_ID}' is unknown to the broker (or not yet authorized). Ask whoever gave you this client-id to provision it first: provision_broker_client.sh <dep-id> --client <Label> --open-enroll ${LARRY_CLIENT_ID}"
;;
400)
rm -f "$_claim_tmp"
die "broker rejected the enroll-claim request as malformed (HTTP 400) — this shouldn't happen; report it with the client-id you used."
;;
*)
rm -f "$_claim_tmp"
die "broker rejected the enroll-claim (HTTP ${_claim_code}) — see broker logs, or retry in a moment if this looks transient."
;;
esac
fi
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# v0.9.0 — BROKER ENROLLMENT (the remote kill-switch; DEFAULT for every deploy). # v0.9.0 — BROKER ENROLLMENT (the remote kill-switch; DEFAULT for every deploy).
# #
# Broker mode needs two facts baked per-box: a deployment id (its row in the # Broker mode needs two facts baked per-box: a deployment id (its row in the
# broker dashboard) and the enrollment secret (exchanged for short-lived tokens). # broker dashboard) and the enrollment secret (exchanged for short-lived tokens).
# Bryan provisions them ONCE per box via env at install time: #
# EASIEST PATH (v0.9.8+): pass a client-id as the positional arg and skip all
# of this — see "ONE-ARG SELF-ENROLL" above. This block only matters if you
# already hold a secret from the admin `enroll` flow (no client-id arg used):
# #
# docker exec larry-broker python /app/broker_ctl.py enroll gundersen-epic \ # docker exec larry-broker python /app/broker_ctl.py enroll gundersen-epic \
# --client "Gundersen" --profile phi --note "Epic interface box" # --client "Gundersen" --profile phi --note "Epic interface box"
@ -459,7 +602,7 @@ fi
# LARRY_DEPLOYMENT_ID=gundersen-epic \ # LARRY_DEPLOYMENT_ID=gundersen-epic \
# LARRY_ENROLL_SECRET=<paste> \ # LARRY_ENROLL_SECRET=<paste> \
# LARRY_PROFILE=phi \ # LARRY_PROFILE=phi \
# LARRY_BROKER_URL=http://100.86.16.114:8181 \ # LARRY_BROKER_URL=https://broker.bjnoela.com \
# bash install-larry.sh # bash install-larry.sh
# #
# The id+profile show up in Iris's dashboard ready to toggle. The secret is # The id+profile show up in Iris's dashboard ready to toggle. The secret is
@ -519,10 +662,10 @@ echo "Next steps:"
if [ "$BROKER_ENROLLED" = "1" ]; then if [ "$BROKER_ENROLLED" = "1" ]; then
echo " AUTH: broker mode (DEFAULT) — NO API key on this box. The kill-switch is ON." echo " AUTH: broker mode (DEFAULT) — NO API key on this box. The kill-switch is ON."
echo " deployment '${LARRY_DEPLOYMENT_ID}' will appear in the dashboard, ready to toggle." echo " deployment '${LARRY_DEPLOYMENT_ID}' will appear in the dashboard, ready to toggle."
echo " REACHABILITY: the broker is LAN + Tailscale only (no public route). This box MUST" echo " REACHABILITY: the broker is public at ${LARRY_BROKER_URL:-https://broker.bjnoela.com}"
echo " reach ${LARRY_BROKER_URL:-the broker} — over Tailscale on an egress-restricted network." echo " (also reachable via Tailscale/LAN). This box MUST be able to reach it over HTTPS."
echo " If it cannot reach the broker, larry FAIL-CLOSES (refuses to run). That is a correct" echo " If it cannot reach the broker, larry FAIL-CLOSES (refuses to run). That is a correct"
echo " kill state but a useless working state — bring Tailscale up first." echo " kill state but a useless working state — check your network/DNS/firewall first."
echo " 1) larry (or $LARRY_HOME/larry.sh)" echo " 1) larry (or $LARRY_HOME/larry.sh)"
echo " 2) larry /path/to/cloverleaf/site_root (to start with a working dir)" echo " 2) larry /path/to/cloverleaf/site_root (to start with a working dir)"
else else