eleven / messenger Public

deploy: ops canary reset — in-place first-run wipe + secret-safe link staging (#1293)

reset re-runs onboarding on the same host (same token, same pinned build,
no new LE cert); create/reset/destroy now stage minted links in ~/<host>.link
on the box and redact URL fragments from local echo (transcripts are public).
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 48687a94521fcccaebd0a9c1ab6b7c122a7f1c82 parent b113198
3 files changed, +54 −10
  • deploy/README.md +1 −0
  • deploy/ops +1 −1
  • deploy/ops-canary.sh +52 −9
diff --git a/deploy/README.md b/deploy/README.md
index 1a66f29..f2b965e 100644
--- a/deploy/README.md
+++ b/deploy/README.md
@@ -16,6 +16,7 @@ Bare `./deploy/ops` lists the verbs:
./deploy/ops ship # every fleet box catches up NOW
./deploy/ops canary create x # per-session test instance (test-x.11ch.at)
./deploy/ops canary deploy x # build THIS branch onto it — always pinned
+./deploy/ops canary reset x # wipe back to first-run (iterate onboarding)
./deploy/ops canary destroy x # reset + remove, data archived to trash
./deploy/ops dev plan # dev-mesh tofu plan, AMI-replacement guarded
./deploy/ops dev apply # tofu apply behind the same guard
diff --git a/deploy/ops b/deploy/ops
index 6edc882..ff9e3d7 100755
--- a/deploy/ops
+++ b/deploy/ops
@@ -24,7 +24,7 @@ ops — the operator command surface
./deploy/ops publish "notes" build main -> release -> point edge at it
./deploy/ops promote <ver> <chan> move beta/stable to a published version
./deploy/ops ship [box...] make every box's instances catch up NOW
- ./deploy/ops canary <create|deploy|destroy|list> [slug]
+ ./deploy/ops canary <create|deploy|reset|destroy|list> [slug]
per-session test instances (test-<slug>.11ch.at)
./deploy/ops dev <plan|apply|deploy|rebuild>
the two-node dev mesh (tofu / scp+stage+swap)
diff --git a/deploy/ops-canary.sh b/deploy/ops-canary.sh
index 5f0ed03..8917e6a 100755
--- a/deploy/ops-canary.sh
+++ b/deploy/ops-canary.sh
@@ -4,6 +4,7 @@
#
# ./deploy/ops canary create <slug> # test-<slug>.11ch.at, flagged --canary
# ./deploy/ops canary deploy <slug> # build THIS branch -> stage -> deploy --pin
+# ./deploy/ops canary reset <slug> # wipe back to first-run (iterate onboarding)
# ./deploy/ops canary destroy <slug> # reset (archive DB) + remove --stop
# ./deploy/ops canary list # every canary + pinned row on the box
#
@@ -49,11 +50,18 @@ DOMAIN="${LCHAT_CANARY_DOMAIN:-fi.dev.11ch.at}"
LCHAT=/opt/lchat/lchat
usage() {
- echo "usage: ops canary create|deploy|destroy <slug> | ops canary list" >&2
+ echo "usage: ops canary create|deploy|reset|destroy <slug> | ops canary list" >&2
echo " slug: lowercase letters/digits/hyphens — your branch or feature name" >&2
exit 2
}
+# Canary links (the admin link's token, the setup/onboarding link's one-time
+# secret) must never land in a session transcript — transcripts are public. So
+# every verb that mints one writes the full output to ~/<host>.link ON THE BOX
+# and this filter redacts the URL fragments/tokens from what echoes locally.
+# Fetch the real links from your own terminal: ssh <box> cat <host>.link
+redact_secrets() { sed -E 's/#[^[:space:]]+/#<redacted>/g'; }
+
cmd="${1:-}"; slug="${2:-}"
host_for() {
@@ -70,13 +78,21 @@ case "$cmd" in
create)
host="$(host_for "$slug")"
echo "ops canary: creating $host on $BOX (flagged --canary)"
- ssh "$BOX" "sudo -n $LCHAT add-instance --canary '$host'"
+ # add-instance prints the admin link + the one-time setup link — both carry
+ # secrets, so the full output is staged box-side and echoed here redacted.
+ out="$(ssh "$BOX" "sudo -n $LCHAT add-instance --canary '$host' > \$HOME/$host.link 2>&1; st=\$?; cat \$HOME/$host.link; exit \$st")" || {
+ printf '%s\n' "$out" | redact_secrets >&2
+ exit 1
+ }
+ printf '%s\n' "$out" | redact_secrets
cat <<EOF
-Canary up. The admin/setup link above is stored nowhere else — canary tokens
-deliberately die with the session (don't vault them).
- next: ./deploy/ops canary deploy $slug # put THIS branch's build on it
- tear down: ./deploy/ops canary destroy $slug # when the work merges
+Canary up. Its links live in $host.link on the box, nowhere else — canary
+tokens deliberately die with the session (don't vault them).
+ fetch links: ssh $BOX cat $host.link # run in YOUR terminal
+ next: ./deploy/ops canary deploy $slug # put THIS branch's build on it
+ reset: ./deploy/ops canary reset $slug # back to first-run, same token
+ tear down: ./deploy/ops canary destroy $slug # when the work merges
EOF
;;
@@ -117,6 +133,29 @@ $host is running $ver (pinned — the auto-updater will leave it alone).
EOF
;;
+reset)
+ host="$(host_for "$slug")"
+ echo "ops canary: resetting $host in place (DB archived to /data/trash; env/token kept)"
+ # `lchat reset` is canary-gated (a real instance refuses), restarts the
+ # instance on an empty DB, and mints a fresh one-time onboarding link — the
+ # loop for iterating setup/onboarding scripts without a new host (each new
+ # dev host would burn another cert from the shared 50/week LE budget).
+ out="$(ssh "$BOX" "sudo -n $LCHAT reset '$host' --yes > \$HOME/$host.link 2>&1; st=\$?; cat \$HOME/$host.link; exit \$st")" || {
+ printf '%s\n' "$out" | redact_secrets >&2
+ echo "ops canary: reset refused — if $host is genuinely a throwaway, flag it:" >&2
+ echo " ssh $BOX sudo -n $LCHAT canary $host --on # then re-run reset" >&2
+ exit 1
+ }
+ printf '%s\n' "$out" | redact_secrets
+ cat <<EOF
+
+$host is back at first-run (same admin token, same pinned build). A bot
+sidecar, if the canary had one, must be re-provisioned — its identities died
+with the DB (add-bot again after the new admin claims the space).
+ fetch the fresh onboarding link: ssh $BOX cat $host.link # YOUR terminal
+EOF
+ ;;
+
destroy)
host="$(host_for "$slug")"
echo "ops canary: destroying $host on $BOX"
@@ -124,14 +163,18 @@ destroy)
# blobs to /data/trash — canary-gated, so a host that was never flagged
# --canary refuses here rather than wiping. That refusal is the safety
# model working; flag it by hand only if it is genuinely a throwaway.
- ssh "$BOX" "sudo -n $LCHAT reset '$host' --yes" || {
+ out="$(ssh "$BOX" "sudo -n $LCHAT reset '$host' --yes")" || {
+ printf '%s\n' "$out" | redact_secrets >&2
echo "ops canary: reset refused — if $host is genuinely a throwaway, flag it:" >&2
echo " ssh $BOX sudo -n $LCHAT canary $host --on # then re-run destroy" >&2
exit 1
}
+ printf '%s\n' "$out" | redact_secrets
# remove --stop: drops the route (and in.<host>), stops + disables the
- # instance unit and any bot/hooks sidecar units.
- ssh "$BOX" "sudo -n $LCHAT remove '$host' --stop"
+ # instance unit and any bot/hooks sidecar units. The staged link file goes
+ # with it — its secrets are dead, and a stale file would mislead the next
+ # session to reuse this slug.
+ ssh "$BOX" "sudo -n $LCHAT remove '$host' --stop; rm -f \$HOME/$host.link"
cat <<EOF
$host destroyed. Its DB is archived in /data/trash (the reaper's trash purge