Kubernetes¶
Two separate k8s environments managed by GitOps.
There are TWO clusters reconciling this GitOps repo¶
Read this before adding a manifest under apps/. dashecorp/rig-gitops is reconciled by two Kubernetes clusters, and clusters/ in that repo is the whole check:
| Cluster | Where | Reconciles | Nodes |
|---|---|---|---|
GCP rig (invotek-k3s) |
GCE, europe-north1-b | clusters/gcp/ — the production rig |
3 (1 control plane + 2 workers) |
Dell (dell-stig) |
Ålesund, no hands-on access | clusters/dell/: apps/buzz, apps/finance-dashboard (plus Stig-Johnny/finance-brain) and an image-cleanup CronJob |
1 |
The two clusters share no apps/ path since 2026-10-02 (rig-gitops#888, #892). Until then clusters/dell/ also pointed at the production apps/rig-conductor, apps/dev-e, apps/review-e and apps/keda, so anything added there deployed to both. A CronJob written for the GCP rig on 2026-09-23 was running in Ålesund minutes later, alerting into Discord #admin, with nobody intending it. Check clusters/ before pointing a second cluster at an existing path. Scheduler messages are the quickest tell of which cluster spoke: GCP says 0/3 nodes are available, the Dell says 0/1.
Dell k3s cluster (Ålesund)¶
dell-stig is a Dell Precision 5520 laptop: single node, k3s v1.36.5+k3s1 (since 2026-10-02) on Ubuntu 24.04.5, kernel 6.8.0-146. ssh -i ~/.ssh/dell-stig-1 claude@100.95.212.93.
What it runs (after the 2026-10-02 cleanup):
- Flux, with 4 Kustomizations from clusters/dell/: flux-system, buzz, finance-dashboard-config and finance-dashboard. The last one builds Stig-Johnny/finance-brain.
- Apps: buzz (relay, MinIO, Postgres, Redis), finance-dashboard, and the image-cleanup CronJob.
- kube-system: CoreDNS, Traefik, metrics-server, local-path and nfs-subdir-external-provisioner.
- On the host: k3s, tailscaled, ssh, and buzz-relay-port-forward.service, a kubectl port-forward that publishes buzz-relay on 100.95.212.93:13000.
- Size: 8 namespaces and 23 pods.
- Keys: it still holds production's sops-age key, because buzz and finance-dashboard-config decrypt with it.
The 2026-10-02 cleanup. The operator approved it group by group, one question at a time.
- The stale rig. clusters/dell/ reconciled production's apps/rig-conductor, apps/dev-e, apps/review-e and apps/keda, plus a hand-applied planner on the stale branch fix/planner-switch-to-codex.
- None of it worked, but it posted its own Pending pods to #admin and was one image or volume away from a second set of agents on the shared queues (rig-gitops#816).
- rig-gitops#888 and #892 removed it, and the planner was deleted by hand. The record is rig-gitops docs/2026-10-02-dell-drops-the-rig.md.
- Dead workloads, removed by hand because they were never in Git:
- vCluster Platform (Loft), and the tablez, runthedocs and team-x vclusters;
- ArgoCD, Vault, kubernetes-dashboard, Metabase and ai-accountant;
- the invotek tunnel pod, and sealed-secrets with its 17 keys;
- empty namespaces and leftover RBAC.
Their NAS data stays: nfs-csi archives on delete, and the static PVs were Retain.
- Host: code-server and the host cloudflared.service, with both packages purged. The 2 Actions runners for Stig-Johnny/mcp-server-dotnet were deregistered through the API, which settles the open infra#389 decision.
- infra:
- The never-applied k8s/dell Synology module is retired (dashecorp/infra#547).
- The Cloudflare objects of the removed workloads are adopted into OpenTofu and deleted (dashecorp/infra#548).
- The stigjohnny.no ones stay, by operator decision; see Networking.
A stale APIService blocks every namespace deletion. Loft's v1.cluster.loft.sh and v1.management.loft.sh had no endpoints once Loft was scaled to 0. The namespace controller's discovery failed (NamespaceDeletionDiscoveryFailure), so conductor-e stayed in Terminating from the spring until they were deleted. Run kubectl get apiservices | grep -v Local before deleting namespaces.
Open risk: buzz's MinIO image has no upstream.
- The image: Docker Hub no longer has the minio/minio or minio/mc repositories (both 404 on 2026-10-02). minio/minio:RELEASE.2025-09-07T16-13-09Z therefore exists only in the Dell's containerd cache.
- minio-init: buzz/minio-init has been in ImagePullBackOff since 2026-09-14, on a tag that never existed.
- Backup: /var/lib/rancher/image-backup/minio-RELEASE.2025-09-07T16-13-09Z.tar (OCI archive, sha256 fcaf11d2…). Restore it with sudo k3s ctr -n k8s.io images import <tar>.
- Decision needed: the image source is the operator's call (rig-gitops#884).
NAS volumes, and their 2026-10-01 fix (rig-gitops#882).
- Setup (since 2026-10-14, rig-gitops#988): buzz uses the nfs-subdir-external-provisioner (storage class nfs-csi) against the Synology DS412plus.
- Flux owns all of it. The provisioner and nfs-csi are in apps/nfs-subdir-external-provisioner/. buzz's four volumes are static PVs on Retain, and each claim names its PV. A reinstall of the Dell (the drive swap) therefore brings buzz back on its existing data. Every adopted object carries prune: disabled, so a revert leaves them in place.
- Where it came from: an Argo CD render of chart 4.0.18 on 2025-06-04, on the tailnet address. On 2026-08-01 at 20:56Z it was moved by hand to the LAN address, 192.168.1.116:/volume1/docker, because NFS over the tailnet timed out during buzz's rollout (rig-gitops#695). That timeout was the outage described below.
- It stays on the LAN address. The tailnet path works again since infra#525, but from the Dell on 2026-10-14 it read 4–11× slower: a 64 MiB read ran at 7.6/21.0 MB/s (cold/warm), against 82.1/92.2 MB/s on the LAN. Details are in rig-gitops docs/2026-10-14-dell-nas-provisioner-under-flux.md.
- Both LAN addresses come from DHCP: the NAS's 192.168.1.116, which buzz's PVs name (and spec.nfs is immutable), and the Dell's 192.168.1.173, on a USB Ethernet adapter.
- The export is open: /volume1/docker *, so any device on the home LAN can mount it. In operator-inbox#40 the operator reserves both addresses in the router first, then restricts the export in DSM to the Dell (192.168.1.173).
- The outage: the Tailscale policy that went into OpenTofu on 2026-04-13 lets tag:server reach only tag:server, and the NAS is untagged. From then on every NFS mount hung: load about 9,000, 321 processes in state D, the provisioner 0/1.
- The fix: dashecorp/infra#525 added host nas and one rule, dell → nas:2049. The mounts resumed by themselves, with no reboot.
- Small fix pending: both local-path and nfs-csi are marked as the default storage class. Kubernetes gives a claim with no class the newer of the two, nfs-csi.
Operator plan (2026-10-02). It supersedes rig-gitops#807's "dev/test rig, later". claude-5 owns this thread.
- Clean up this cluster. Done 2026-10-02, except the MinIO decision.
- Update the OS and reboot. Done 2026-10-02. apt full-upgrade, then a reboot into kernel 6.8.0-146, booted once through
grub-reboot. GRUB's saved default stayed on the old kernel, and a trial-boot check would have rebooted back to it if the tailnet was not online within 10 minutes. It wasn't needed, and it has been removed. - The Dell had been up 26 weeks with a kernel reboot pending (
/var/run/reboot-required) and 69 packages upgradable. Security updates already come in through unattended-upgrades. - There is no disk encryption (
/etc/crypttabis empty, root is plain ext4), so a reboot cannot stall on a passphrase.tailscaled,sshandk3sstart at boot. - Upgrade k3s one minor version at a time. Done 2026-10-02: v1.32.5 → v1.32.13 (the etcd 3.5.26 bridge) → v1.33.13 → v1.34.12 → v1.35.9 → v1.36.5, with Flux moved to v2.9.3 before v1.36 (dashecorp/rig-gitops#894). Each step was a PR bumping
var.k3s_version(dashecorp/infra#554, #556, #558, #560 and #562), applied byapply (hosts)and verified. The path and its sources are in dashecorp/infradocs/2026-10-02-dell-k3s-upgrade-path.md. The upgrade research was delegated to the Mac Mini Claude. - Move The Rig here. Re-scoped by the operator on 2026-10-02 to a multi-location rig: "Keep GCP on until move is complete. We need to support that we have multiple rig clusters in different locations. It should be possible to choose in rig-cockpit what cluster or rig location we are using." The design and every decision are on dashecorp/infra#508.
- Model: each tenant lives in exactly one location, and can be moved.
- Where the map lives: the tenant → location map is in Cloudflare D1, owned by rig-cockpit-worker. Every conductor fences its GitHub writes on it and fails closed.
- Moves: manual only, from Rig Cockpit. A move is a fresh start: nothing is copied, and the tenant's work state is rebuilt from GitHub.
- Webhooks: they go through a Worker with a Queue buffer on
rig-conductor.dashecorp.com. - Images and secrets: GHCR only, and one shared SOPS age key for every location.
- Before any tenant moves to the Dell: off-site backups.
- Drain timeout on moves: 15 min, then force (decided 2026-10-02, posted on infra#508).
-
Implementation, for the rig, in order:
- rig-gitops#895 (base plus a GCP overlay). Merged as rig-gitops#898; all 17 GCP Flux Kustomizations render byte-identical.
- rig-conductor#2561 (fencing). Merged in 8 stacked parts (#2570, #2572, #2575, #2576, #2578, #2580, #2582, #2588).
- rig-cockpit-worker#250 (D1 map and moves). Merged: schema #252, read APIs #257, move APIs #259, cron #261, Locations page #263.
- rig-cockpit-worker#251 (webhook router). Merged: the shadow router #264 (deployed,
760741d) and the routing policy #267. Live Queue routing itself is not built (checked 2026-10-06):- The live PR, rig-cockpit-worker#266, closed unmerged.
-
267 merged only "part A: the pure routing policy" and closed the live-routing issue #265 with it.¶
- On
main,src/handlers/webhook-router.jsis shadow-only, nothing readsROUTING_MODE, andwrangler.tomlhas no queue binding for it. - No open issue tracks the build.
- dashecorp/infra#565 (Cloudflare pieces). Applied: the queues and
rig-conductor-gcp(#567), and the webhook-path route (#569). See Networking → Rig webhook router. - rig-conductor#2562 (reconcile sweep). Merged: #2590, #2592, #2595 and #2597, plus the fix rig-conductor#2601 (merged as #2602): the sweep was marked done before its append, so a failed write was never retried.
- rig-gitops#896 (GHCR, and the Dell overlay in standby). Merged so far: PR-1 #901, PR-2 #902 and PR-2b #904 (the CI parity proof, with curl), infra#571 (the Artifact Registry reader for rig-gitops CI), the C2 workflow rig-gitops#906 with its selftest #908, PR-3 rig-gitops#917, and rig-gitops#919 (the parity proof via the workflow token). Plan:
docs/2026-10-03-ghcr-and-dell-standby-plan.md. -
rig-gitops#897 (the Dell's off-site backups). Merged so far:
- the backup tools, reviewed and whitelisted: claude-3#1024, #1026, #1030 (sops) and #1034 (openssl), and rig-docs#343, #345, #347 and #349;
- P1a, the R2 bucket (infra#573, lifecycle fix infra#577; see Networking → Cloudflare R2);
- P3, the backup image (rig-conductor#2605), and P3b, openssl pinned (#2622). See CI/CD for the image reference;
- P4 (#2608, #2610), P5 (#2613, #2616) and P6 (#2620, #2626, #2630, #2633): the signed runs with the D5
not_afterrule,fetch-run.sh, the drill kit and the runbooks; - the image digest bump, rig-conductor#2637.
All of it is suspended and inert until P9.
signing.pubis still an.invalidplaceholder until O2b. - Live state (2026-10-03): - Rig Cockpit: the D1 tenant → location map and the Locations page are live in rig-cockpit-worker. - Conductor: fencing and the reconcile sweep, with its fix, are merged but inert untilLOCATIONis set. - Webhooks: the router runs in shadow mode onrig-conductor.dashecorp.com/api/webhook/github(infra#569, operator-approved, checked with a 200 ping). Live Queue routing is not built yet (step 4 above), so there is noROUTING_MODEto flip. Building it is agent work; switching it to live is a production change and needs the operator. Superseded: live since 2026-10-15 (rig-cockpit-worker#304). - Phone C2: thecredential-opsenvironment (infra#575) and the merged C2 workflow. See CI/CD → credential-ops. - Approvals MVP (rig-cockpit-worker#268): phase 1 is live, with the waiting-run banner and the operator inbox on the web and on iOS build 15; push waits on the APNs secrets (operator-inbox #5). Phase 2, Face ID approvals, is merged onrig-approvebut not deployed. See Operator inbox. - Current focus (since 2026-10-04): the operator asked to "focus on getting everything running on the Dell cluster". The Dell standby overlay (#896 Phase B) is open on rig-gitops as a stack: - #911 (PR-10): merged on 2026-10-04. - #913 (PR-11): a draft, still waiting on the C6 pre-checks (operator-inbox #7). - The conflict: by 2026-10-06 it conflicted withmain, because it still carried PR-10's commit from before PR-10 was squash-merged. - The update: it was brought up to date by mergingmain. A rebase would have needed a force-push, which the auto-mode classifier blocks. D7 was settled in the same PR (below). - Review: the new head11fd790is green but needs a fresh Review-E approval, because MergeGate only counts an approval of the current head. The 2026-10-04 approval no longer counts. - #915 (PR-19): a draft stacked on #913. - Update: on 2026-10-06 the new #913 head was merged into it (03bb609), so itsdell render invariantsCI runs against the KEDA pin. On the old head it rendered PR-11'skedaandegress-gwclean: 30 objects, all 10 rules OK. - Next: once #913 merges and #915 retargets tomain, it goes ready and needs a fresh approval. - Order: PR-19 can't go ahead of PR-11, because it edits a doc that PR-11 creates. - PR-12 needs only PR-11 now: PR-3, PR-4, D3 and rc#2601 (merged as #2602) are done. PR-13 waits for PR-6 to PR-9 and PR-12's live checks (the plan's dependency list). - D7, settled 2026-10-06 in #913: the Dell's KEDA chart is frozen to2.21.*. - keda.sh's compatibility table lists KEDA 2.21 for Kubernetes 1.34–1.36. It is the only release that covers the Dell's v1.36.5, and chart 2.21.0 is in the kedacore Helm index. - GCP stays on 2.16. - The GCP render is unchanged apart from a render-harness stub name. - Phase A, #896 (2026-10-06): - C2 ran on 2026-10-06, approved by the operator throughcredential-ops, and wrote six SOPS-encryptedghcr-pull-secretfiles with 7ENC[markers each. The token is in Credentials. - PR-3, rig-gitops#917, merged: the six pull secrets are wired into thedashecorp,dev-e,review-e,rig-conductor,rig-plannerandstigjohnnynamespaces. - rig-gitops#919, merged: the parity proof reads GHCR with the workflow token. A parity run onmainproved every own tag has the same digest in Artifact Registry and GHCR (CI/CD). - PR-4, rig-conductor#2688, open (opened with the operator's go): the conductor pullsghcr.io/dashecorp/rig-conductorwithghcr-pull-secret, and its pin writes GHCR. Publishing still pushes to both registries, so a rollback is a revert, and if the pull failed, the rolling update would keep the old pod serving. - Next, #896: - PR-5 to PR-9: the rest of the GHCR switches, after PR-4. They are production changes, gated by the CI parity proof. - PR-10 to PR-15: the Dell overlay. It needs the Dell tunnel token (C4) and the tenant-map token (C5), and it must drop GCP's GCS backup CronJob. The Dell already holds production'ssops-agekey (see above), so it must confirm that the Dell's Flux decrypts the rig's secrets with it. - PR-16 to PR-18: retire Artifact Registry after a 7-day soak. - Next, #897: - O1 (operator-inbox #2), then P1b, the R2 tokens, then P2, GitHub Actions secrets from remote state. - P7: the rig-gitopsapps/rig-backup-r2Dell overlay. It needs #896's overlay, plusghcr-pull-secretin therig-backup-drillnamespace. - O2 and O2b: the age keypair and the signing key, each made offline. - P8: the R2 writer token into SOPS. - The proving runs, drill A, then P9, the go-live. - Go-live, each needing the operator:ROUTING_MODE=live(done 2026-10-15, rig-cockpit-worker#304), an infra route for/api/merge*, andLOCATION=gcpwith theTENANT_MAP_*secrets in rig-gitops (TENANT_MAP_BASE_URL,TENANT_MAP_ACCESS_*; done 2026-10-15, rig-gitops#1027). - Status, 2026-10-06 (evening): - Triage: every open issue in rig-conductor, rig-agent-runtime, rig-gitops, infra, rig-docs, operator-inbox and claude-3 (222) was read and sorted by what it means for the Dell: - 7 block it; - 29 are Dell-specific; - 69 are rig bugs at any location; - 43 look stale; - 74 are unrelated.
The register, the fix list and the 14-step path are on the private page Dell Rig Readiness. - No Dell access from the orchestrator. The claude-3 workspace runs on the Mac Mini, which has no Dell key (see Hosts), so it can't run C6 or the live checks. dashecorp/infra#601 proposes one OpenTofu-managed
authorized_keysline, applied byapply (hosts). The auto-mode classifier refused to generate the key, so this waits on the operator. - Gaps found, now tracked: - live webhook routing: rig-cockpit-worker#265, reopened (step 4 above); - the Dell tunnel, its hostname and its Access app (PR-14's infra step): dashecorp/infra#602; - a reader token for the Dell's conductor on the tenant map (PR-15's infra and worker change): dashecorp/infra#603; - etcd snapshots and a NAS copy, which #897's body promises but nothing builds: noted on rig-gitops#897. - PR-12 is drafted: rig-gitops#925 (issue #924), stacked on PR-19. - What it does: the Dell's conductor and data plane, fenced, with the conductor at 0.dell render invariantspassed with flux 2.9.3 (53 objects, every rule OK), and Review-E approved it. - What CI can't check: CI doesn't build the nestedrig-conductor-apiKustomization, which comes from the rig-conductor repo. A local kustomize build showed exactly the five intended fields differ from GCP's. - Before it merges: C9, the Dell'sflux-systemsource readingdashecorp/rig-conductor. - The stack PR-11 → PR-19 → PR-12 is approved but held in draft. All three wait on C6. - Why draft: MergeGate squash-merges an approved stacked PR into its parent branch, so #915 and #925 stay drafts until they are retargeted tomain. - Approvals given in draft: these don't trigger MergeGate when the PR goes ready, so each one needs a plain squash merge, or a fresh approval after it goes ready. - Merge path fixed: rig-agent-runtime#775 merged at 17:27Z and closed #773. The runtime now reads Review-E's verdict from GitHub, not from its reply text, and it takes effect with the next runtime pin. BRAIN's description is corrected in rig-docs#352. rig-conductor's comment above/api/mergestill describes the text match, and gets fixed with the next conductor change. - GHCR Phase A, PR-5 merged: rig-gitops#927, merged by MergeGate at 17:25Z. It moves the dormant stigjohnny fleet (4 releases at 0 and paused) and the unrendered staging and Job files to GHCR. Theliveproof showed 25 tags matching and none mismatched. Next is PR-6, which restarts production Dev-E pods and needs the operator's go and C0. - Runtime pin: rig-gitops#921 (sha-8d0c6e0) had stalled after its approval. Its onlydispatchcheck had finished as cancelled, and MergeGate's guard reads the latest run of each check. Re-running that check fired the check-completed webhook, and MergeGate merged it at 17:07Z. - CI: inframainis green again; infra#600 was a GitHub 500 while fetching the Cloudflare provider, and a re-run passed. rig-agent-runtime's Build & Push for8d0c6e0, which failed on 2026-10-02 with "unknown blob", passed on a re-run. - Deadline, 2026-10-14: GPT-5.5 leaves Codex for ChatGPT sign-in plans, and the rig's Codex lanes sign in with ChatGPT Business. 13 HelmReleases in rig-gitops set it, 6 of them as the main model. - The fix, superseded the same evening (see below): rig-gitops#923 (issue #922) moves all 17model:lines togpt-6-sol. That is the replacement OpenAI names for Business plans, and openai/codex's model catalog mapsgpt-5.5to it. The rig's codex CLI, 0.160.0, supports it. - Held in draft: OpenAI makes GPT-6 Sol available in a workspace "once an administrator has enabled it". Merging before the operator confirms that would break the Codex lanes now instead of on 10-14. Its approval came in draft, so it needs a plain merge. - Status, 2026-10-06 (late evening): - Dell access from the Mac Mini: dashecorp/infra#604 (closes #601) adds an OpenTofu-managed key for the orchestrator, andapply (hosts)installed it (Hosts). - C6: all seven pre-checks passed (operator-inbox#7, closed). - KEDA's CRDs and stale APIServices: none. - kube-dns sits on10.43.0.10, and10.43.200.53was free. - Capacity: 8 CPUs, 29 GiB of RAM available, 177 GB of disk free. - Bothlocal-pathandnfs-csiare marked as the default storage class; clearing one belongs to rig-gitops#696. - PR-11 is live on the Dell. rig-gitops#913 merged at 20:17Z, aftergh pr readyand a fresh Review-E approval. The live checks pass: - thekedaHelmRelease is Ready on chart 2.21.0; -v1beta1.external.metrics.k8s.iois Available; -egress-dnsis on10.43.200.53; - buzz and finance-dashboard are unaffected. - Next on the Dell: PR-19 (#915), then PR-12 (#925). Each needs retargeting tomain, an update frommain,gh pr ready, and its live checks. PR-12 also needs C9, the Dell's Flux GitRepository for rig-conductor reporting Ready. PR-13 waits on PR-6 to PR-9. - The Codex lanes rungpt-6.1-sol. - The first switch: #923 merged at 20:25Z withgpt-6-sol. - The correction: the operator then confirmed only that GPT-6.1 Sol is enabled in the Business workspace. rig-gitops#933 (closes #932) moved the same 17 lines togpt-6.1-soland merged at 20:47Z. codex 0.160.0's catalog lists that model. - To do: it defaults tolowreasoning effort, wheregpt-5.5usedmedium, so raising it tomediumis a follow-up. - Stale issues: 37 were closed after a re-check, each with an evidence comment. Five stay open: rig-conductor #1991, #2091 and #2320, and infra #365 and #366. - Status, 2026-10-07: - PR-19 and PR-12 are live on the Dell. - PR-19: rig-gitops#915 merged at 09:54Z. Every rig-gitops PR that touchesapps/orclusters/now runs thedell render invariantscheck. - PR-12: rig-gitops#925 merged at 10:01Z. Its live checks pass: - Valkey and Postgres are Running, with PVCs onlocal-path. - The conductor sits at 0 replicas withLOCATION=delland noRUNPOD_API_KEY. - The backup and the daily cost report are suspended, and there is no NodePort. - C9 was not needed: the Dell's Flux credential reads rig-conductor. - Merge trick: a stacked PR whose parent was squash-merged gets the parent's final head merged in first, thenmain. Mergingmaindirectly hits add/add conflicts. - k3s exited on big applies, now fixed (infra#606, infra#607). - What happened: after PR-11 and again after PR-12, etcd stalled for up to 5 s. The embedded controller-manager missed its 10 s lease renew deadline, k3s exited, and the API was down about 40 s. The containers kept running. - The fix: the k3s unit now passesleader-elect-lease-duration=60sandleader-elect-renew-deadline=40sto the controller-manager, scheduler and cloud-controller-manager, throughvar.install_exec. - Verification:apply (hosts)applied it at 10:41Z after an etcd snapshot. The leases show 60 s. - Still to prove: whether the next big apply (PR-13) passes without an exit. k3s has since exited once at idle, on a lease this fix doesn't cover (2026-10-13 below). - Pending gaps for PR-13: - The Dell's Postgres has nopgvectorextension, because Flux never creates the one-time Job (reconcile: disabled). PR-13 must enable it before the conductor starts. - Both StorageClasses are marked default, sonfs-csi, the newer one, wins. Nothing relies on the default. ArgoCD is gone from the Dell, and the NFS provisioner is unowned. rig-gitops#696 has the plan to adopt it and clear its default, which waits for the operator. - GHCR PR-6 (rig-gitops#939, the Dev-E fleet): every check is green, including theliveparity proof. It is held as a draft for C0 (operator-inbox#24), because this Mac Mini cannot read the GCP cluster. operator-inbox#25 asks whether the orchestrator should get read-only access. - The claimed-issue opt-out is live (rig-agent-runtime#778): - The rule: an issue with any GitHub assignee is claimed, and no rig agent works on it. - The two halves: - rig-conductor#2691 skips the planner signal, theagent-readydispatch and the 10-minute scan. - rig-agent-runtime#780, pinned by rig-gitops#941, adds a planner stop rule and a runtime guard that recordsISSUE_CANCELLEDwithreason=claimed_by_assignee. - Existing bug found on the way:ASSIGNMENT_SKIPPED_CLOSEDhas always been rejected with a 400 (rig-agent-runtime#779). Fixed that evening, below. - Evening of 2026-10-07 (operator AFK): the rig's event contract and MergeGate. - Six runtime events were silently rejected. rig-conductor'sPOST /api/eventsanswers 400Unknown event typeto any type itsMapToEventlacks, and six that rig-agent-runtime posts got that answer, five of them since April or May. Some posts didn't log the answer, so nobody saw it: -ASSIGNMENT_SKIPPED_CLOSEDis stored as anAssignmentSuppressedwith reasonagent_skipped_closed, on the issue stream. It changes no row state (rig-conductor#2694). -STREAM_CONSUMER_DOWNandCODEX_LOGIN_REQUIREDare stored asAgentDegradedand alert #admin, deduplicated per pod for 6 h. Both meant an agent that heartbeats but does no work. The Codex alert carries the CLI'sdetailsand points at rig-gitopsdocs/codex-auth-bootstrap.md(rig-conductor#2698). -COI_BLOCKED,SESSION_CWD_INVALIDATEDandAGENT_SESSION_INITIALIZEDare no longer posted; their log lines stay (rig-agent-runtime#783). - The guard: every rejected post is now logged. rig-agent-runtime'ssrc/conductor-event-types.test.jspins the set of types the runtime posts, so a new type fails CI until rig-conductor has an arm for it (rar#783, #785, #787). rig-conductor#2367 is closed. - Pins: rig-gitops#947, #949 and #951. A pin whose runtime change alters agent behaviour needs a Rollout log entry in rig-gitopsdocs/image-pinning.md, and Review-E asked for one even on a docs-only pin. - MergeGate sweeps for lost triggers (rig-conductor#2365, via #2700 and #2696). - How it works: every 5 min,MergeGateSweepServicere-runs the guardedMerge()for a Valkey hash withreview-approvedand nomerged:key.ci-passedis not required, because the guard reads the checks. - Bounds: each trigger gets 12 attempts, backing off from 5 min to 2 h, kept inmerge-gate-sweep:{repo}#{pr}. Only a newer condition resets that count. - Effect: a merge trigger lost to a pod rollover or a dropped webhook no longer strands an approved PR. BRAIN says so since rig-docs#372. - Supply-chain pins from the Structurizr review: - rig-docs loadsmermaid@11.17.2exactly, atsecurityLevel: 'antiscript'(rig-docs#370). Review-E rejected'strict', which Mermaid documents as encoding the<br/>and<small>labels 16 pages use. - claude-3's C4-PlantUML includes pin the v2.14.0 commit (claude-3#1069). - Mermaid is still unreviewed and unregistered: operator-inbox#27. - TAGS GONE CRAZY playtest server (claude-4): it runs on the GCP cluster in namespacetgc-play(rig-gitops#937). It is reached attgc-play.dashecorp.combehind an owner-only Access app (infra#609). Its pull secret came from a C2 run with the newnamespacesinput (rig-gitops#935). - Status, 2026-10-13: - GHCR PR-6 is live. rig-gitops#939 merged at 13:10:41Z on the operator's "yes PR-6", and Flux applied it by 13:11:24Z. - The rollout: the four running Dev-E lanes restarted onghcr.io/dashecorp/rig-agent-runtimewithghcr-pull-secret, and the pull events showghcr.io. The digests are unchanged, and the agents are idle with fresh heartbeats. - One change from the plan: the new Jobdev-e-codex-home-permissions-v2mountsagent-homeon GCP, not the Filestore claimrig-shared-claude, because the agent-home soak had passed. It completed on a worker, and the old Job was pruned. The plan now says the same for PR-7's two Jobs. - PR-6 brokedev-e-bootstrapfrom 13:18:49Z to 13:55:12Z (rig-gitops#969). - What happened: PR-6 renamed the Job and, in the same commit, gave the GCP overlay a control-plane patch for the new name. The child Kustomization built the new revision before its parentflux-systemhad applied the newspec.patches, so-v2was created without the patch. Every later reconcile then failed withspec.template: field is immutable, and Flux applied nothing indev-e. The running agents were unaffected. - The fix: rig-gitops#970 dropped the patch, so the desired Job equals the live one. - Lesson: a PR that renames an immutable object can rely only on the base. Check a Kustomization'sReadyagain after its next interval, not only at the first apply. The 13:12Z rollout check passed six minutes before the failure. - C0: run read-only by the claude-3 session on the MacBook Air, which has the GCP access (operator-inbox#24, closed). Pull-secret names came from the Flux inventory, so no Secret was read. - Review-E: editing a PR's description doesn't trigger a re-review. An empty commit did. - Next: PR-7, after a day's soak. It went the same day instead (below). - The Dell's k3s exited at idle (dashecorp/infra#622). - What happened: on 2026-10-08 at 02:18:05Z, k3s loggedleaderelection lost for k3s-etcdand was down about 43 s. etcd had stalled for 3 minutes from 02:15:03Z, with nothing being deployed. - Why the 10-07 fix missed it: infra#607 relaxed only the controller-manager, scheduler and cloud-controller-manager leases. k3s's ownk3sandk3s-etcdleases are still 45 s. - Since then: a milder stall on 2026-10-12, 2.3 s at worst, passed without an exit. The cause is unknown. - The agent-home soak passed (checked 2026-10-09). - Filestore has had no writes since the switch, and 7 daily snapshots of the new disk exist. - Cleanup step (0) is done (2026-10-13, on the operator's "yes step 0"). - What it did: one forced upgrade per release (spec.upgrade.forcefrom rig-gitops#968, thenflux reconcile hr --forcewhile each agent was idle). review-e went to v517 at 13:55:46Z and dev-e at 13:56:46Z. This dropped the leftovershared-claudevolume from both StatefulSets, and rig-gitops#972 removed the flag. - What's left on old claims: only the review-e and rig-planner permission Jobs. GHCR PR-7 moves them toagent-homewhen it renames them. - Then: the rest of rig-gitops#883 (prune the old PVs and PVCs, docs), and the Filestore deletion (infra#526, #527), each with the operator's yes. - GHCR PR-7 is live. rig-gitops#974 merged at 16:40:54Z, so Review-E and the planner pull from GHCR. - The soak waiver: the operator approved the merge ("yes PR-7", 14:53:28Z) and waived the rest of PR-6's day of soak ("Yes now", 16:37:25Z), because Dev-E had already pulled the same image tag from GHCR. Review-E had held its approval to the soak date written in the PR, which MergeGate does not check. The waiver went into the PR body, and Review-E approved the new head. - The rollout: both Review-E pods and the planner restarted at 16:41:10–11Z, and the pull events showghcr.io. The-v2permission Jobs completed on a worker with claimagent-home, and the old Jobs were pruned. - The agent-home cleanup is done (rig-gitops#883, closed). - Adoption: rig-gitops#976 put the hand-appliedrig-shared-claudePVs and PVCs into alegacy-filestoreKustomization. A server-side dry run passed before the merge. - Pruning: rig-gitops#978 (merged 16:47:57Z) deleted that Kustomization, and Flux garbage-collected the four objects, together with the planner'srig-planner-filestorePV and PVC. - The result: only the threeagent-homePVs remain, all on10.166.0.33, and no workload names an old claim. The infra docs update is dashecorp/infra#629. - Left: unmount/mnt/fs-*on invotek-k3s, then infra#526 and #527, each with the operator's yes. Deleting the two instances saves about $368 a month. - Filestore is deleted. On the operator's "yes delete filestore" (20:46:18Z) I unmounted the read-only mounts on invotek-k3s. Then dashecorp/infra#631 liftedrig-planner's deletion protection (step 5a), and dashecorp/infra#632 destroyed both instances (step 5b). The apply finished at 21:05Z with0 added, 0 changed, 3 destroyed, andgcloud filestore instances listis empty. The Filestore API stays enabled, infra#508 is closed, and the saving is about $368 a month. - Review-E misrouting (rig-conductor#2713). The review of rig-gitops#976 arrived keyed to its issue #975, with an implementation template appended. The claude-tmux session stalled until the 360 s fallback, which approved it correctly. - The Codex temp-folder leak is fixed: rig-agent-runtime#763 was closed by #790. - Status, 2026-10-14: - GHCR PR-8 is live. rig-gitops#980 merged at 12:11:19Z on the operator's "yes PR-8" (12:09:34Z), with the dashecorp lanes idle. Nothing underapps/orclusters/references Artifact Registry any more. - The rollout: the four dashecorp HelmReleases (Review-E, Dev-E, the planner and Rig-E) upgraded, and they pullghcr.io/dashecorp/rig-agent-runtimewithghcr-pull-secret; the pull events showghcr.io.dashecorp-bootstrapwas still Ready after its next interval (12:21:49Z). - The codex Job: v3 becamedashecorp-codex-home-permissions-v4and completed on worker-2. v1 (Failedsince it was created) and v3 were pruned. The v3 Job's GCP placement patch was dropped, not re-targeted, per the PR-6 lesson: the claim is already bound on worker-2, so its volume places the Job there. - A pre-approved draft merges when it is marked ready. Review-E approved the draft before the operator's yes, so MergeGate merged it 19 s after it left draft. Mark a production PR ready only once the lanes are idle. - Rig-E was down about 3 minutes, because its new pod landed on worker-2, which had to pull the image. Its unauthenticated probes answer as before:/api/health200,POST /api/chatwithout a token 401,/api/state404. - PR-9 merged at 12:35:12Z (rig-gitops#982), so GHCR Phase A is complete. The image-pin gate now rejects any Artifact Registry reference. Review-E caught a hole in the first version: an explicit registry port (us-docker.pkg.dev:443/…) got past the check. The gate now parses the registry host. The D8 soak before retiring Artifact Registry (at least 7 days) ends no earlier than 2026-10-21. - Worker disks are too small for bursts (dashecorp/infra#633). - What happened: the two fresh pulls on worker-2 hitno space left on device, and the node was under DiskPressure from 12:14:51 to 12:19:57Z. The retry succeeded, and nothing was evicted. - Why: the workers' 30 GB disks idle at 80–84%, and the kubelet's image GC starts only at 85%. The dailyimage-pruneCronJob covers only the control-plane node, though the agents moved to the workers on 2026-09-25. - Fixed the same afternoon with no spend; dashecorp/infra#633 is closed.initialize_params.sizeis ForceNew, so growing the disks would have needed each one adopted as agoogle_compute_disk(draft infra#640). The operator chose a cleanup instead ("yes cleanup to all workers and nodes"). - Over SSH: the journals were trimmed to 500M, and only images that no workload references were deleted. worker-1 went from 79% to 57%, worker-2 from 84% to 71%, and the control plane from 37% to 30%. A blindcrictl rmi --prunewould have deleted worker-2's current agent images.crictl rmireturned non-zero for deletes that worked; a listing confirmed the images were gone. - rig-gitops#986 (14:22Z): thenode-configDaemonSet caps journald at 500M on the workers. It also runsgrowpartandresize2fs, which do nothing until a disk grows. - rig-gitops#985 (14:27Z):image-pruneis now a DaemonSet that prunes every node daily in the 04:xx UTC hour. The CronJob is gone. - The disk grow (infra#640/#639) is closed. Reopen it if a worker passes 80%. - The Dell's NAS provisioner and buzz's volumes are in Git (rig-gitops#988, Closes #987). It merged at 15:59:45Z on the operator's "Yes 988" (15:57:50Z). Review-E approved the head 9 s earlier. - Before the merge: a server-side dry run on the Dell, under Flux's field manager (kubectl diff --server-side --field-manager=kustomize-controller --force-conflicts), showed the exact changes and no immutable-field errors. - The result at 16:00:30Z: the provisioner restarted once, ontov4.0.2@sha256:63d5e045…, and took its lease at 16:00:41. buzz's four PVs are onRetain, and its pods were not restarted. The Dell has the same five NFS mounts, all on the LAN address. - The render check: the Dell'sno-nfs-volumerule now allows a location's own NFS server (LOCAL_NFS). Two selftest mutations prove that every other server still fails. - A test trap: an NFSv4.1 mount of a second address of the same server reuses the existing client and its connection. A tailnet test mount next to LAN mounts therefore measures the LAN. Test withvers=4.0. - 2026-10-15 morning: the Dell's Cloudflare side, capacity guardrails, and a tunnel-secret leak. - Merged and verified live: - rig-gitops#999: image-prune at 512Mi. The first daily run had been OOMKilled on 2 of 3 GCP nodes by k3s's 253 MB in-container unpack. - rig-gitops#1001: the temporaryupgrade.forceremoved from 13 releases, with no release version change and no restart on either cluster. - infra#650: the reinstall runbook, now with an NFS-client step andmax-pods=200ininstall_exec. - infra#652: the Dell tunneldashecorp-dell, the hostnamerig-conductor-dell(403 without a token) and its Access app. - infra#653: the Dell conductor's own tenant-map reader token. - rig-gitops#1004: capacity guardrails on the Dell. There are 6 agent slots plus Rig-E in a 13.5 GiB budget, with a quota and LimitRange per agent namespace and KEDA capped at 1. At the cap, work waits instead of running out of memory. Kubelet reservations for the reinstall are on infra#648. - Security, infra#654: OpenTofu prints a resource'sid, and arandom_id'sidis its value. - GCP's live tunnel secret has been in every OpenTofu CI log that refreshescloudflare/dashecorp.com, and in PR plan comments. - The new Dell secret was printed once at creation, and the orchestrator printed it again into its session output. - Fix draft infra#655 moves the Dell secret torandom_bytes, takes GCP's out of the printed resource with no tunnel change, and adds amodule-lintguard. Review-E asked for an explicit first Dell replacement. - Rotating GCP's secret (a no-downtime swap to a new tunnel, or at GCP's retirement) is the operator's decision. Rule: mask[id=...]in every CI-log grep. - Open: - rig-gitops#1005: the Dell disk and memory alert to #admin. - rig-cockpit-worker#300: the worker accepts one tenant-map reader token per location, synced bydeploy.ymlfrom an OpenTofu-written secret. - rig-gitops#1002: C4/C5 through credential-ops, so no one copies a Cloudflare-made token. The operator approves the encrypt run on the phone. The workflow is drafted on the Mac Mini. - The move is still blocked on the worn drive: the external SSD decision is the operator's. - 2026-10-15 midday: the Dell's alert and reader set are live, the C4/C5 route is merged, and the drive's write load is measured. - Merged and verified live: - rig-gitops#1005:node-pressure-watchon the Dell. Its first run (08:00Z) read disk 18% used and memory 88% available, so no alert. - rig-cockpit-worker#300: deployed with no extra readers (TENANT_MAP_READERSis unset), and/api/tenantsstill refuses a request with no token. GCP's conductor never reads the tenant map, because its fence is off. Only the Dell's fenced conductor will, after PR-15. - rig-gitops#1008, #1009 and #1010: C4/C5 throughcredential-ops. They add a shared recipient-pinning library, the encrypt helper with its selftest (175 checks, green on its first CI run) and the encrypt job. The operator approves each run on the phone. - infra#655, the tunnel-secret fix: OpenTofu 1.9'sreplace_triggered_byignores a create of its trigger, so the Dell's new secret goes on a new tunnel,dashecorp-dell-v2. Its name is new too, because Cloudflare answers HTTP 409 for a name a live tunnel has. The operator said yes at 09:25:42Z. It merged at 09:35:58Z and applied as planned: 3 added, 1 changed, 3 destroyed, and nothing on GCP. The new tunnel's ID (f3b364b4…) differs from GCP's and from the old one, andrig-conductor-dellstill answers 403 without a token. - infra#658, OpenTofu writes the C4/C5 secrets and the worker'sTENANT_MAP_READERS: -terraform_remote_statedrops thesensitivemark of outputs, so every value is wrapped insensitive()again. The older tgc-docs and tgc-media consumers are tracked in infra#659. - A precondition fails the plan while the old Dell tunnel's ID is still current. - Review-E asked for a switch that writes[], to remove the Dell's reader. - The worn drive's write load, measured at idle: about 30 GB a day and 600 flushes a minute. - About 60% comes from Flux re-extracting artifacts onto disk-backedemptyDirs, and etcd comes next. - No stall over 100 ms has happened since the PR-13 install. - The I/O scheduler isnone, so I/O priority has no effect. - The fixes need no new hardware: - rig-gitops#1014: Flux's working space in RAM, a 5m Git fetch, 120s/100s/20s leader leases for Flux and KEDA, images kept until the disk is 70% full, and the alert moved off the busy minutes. - infra#661: apt at 02:41 and 02:56 UTC, withman-dboff. - The operator approved both at 09:25:42Z. #1014 was live on the Dell by 09:42:42Z: flags, RAM volumes and 120s leases checked, and all 17 Kustomizations and 19 HelmReleases Ready. #661 was live by 09:47Z, with next runs at 02:41:00 and 02:56:00 UTC. The write rate is re-measured on 2026-10-16 against a baseline taken at 09:42:23Z. Neither covers a sudden drive failure; the off-site backups (rig-gitops#897) and the SSD do. - PR-14 and PR-15 (rig-gitops#1011, #1012) are drafted and verified as branches. Their PRs open after the C4/C5 commits land. - Still the operator's: rotating GCP's tunnel secret, and the SSD. - 2026-10-15 afternoon: the Dell's own tunnel is live, the tenant map is wired, and two format bugs are fixed. - infra#658: Review-E's one blocker, a removal path with no code behind it, was fixed withvar.dell_tenant_map_reader_enabled(default true; false writes[]and keeps it written after the token is gone). Approved 09:58:38Z, operator yes 10:08:05Z, merged 10:08:37Z. The apply wrote 4 secrets with no value printed:DELL_TUNNEL_TOKEN,DELL_TENANT_MAP_CLIENT_IDandDELL_TENANT_MAP_CLIENT_SECRETon the rig-gitops environmentcredential-ops(none at repo level), andTENANT_MAP_READERSon rig-cockpit-worker. Follow-ups: infra#659 (tgc remote-state sensitivity), infra#660 (Access tokens expire after 1 year by default), infra#662 (plaintext_value→value). - Worker deploy (operator yes 10:08:05Z):deploy.ymlrun 38718557371 on main 9d3444b putTENANT_MAP_READERSon the Worker at about 10:13Z. Unauthenticated/api/tenants,/api/locationsand/api/healthstill return 401. - C4 (run 38718895703, approved by the operator on the phone) pushedapps/cloudflared/dell/tunnel-token.sops.yamlto PR-14's branch at 10:27:37Z. - PR-14 = rig-gitops#1015, the Dell's own cloudflared: Review-E approved 10:42:29Z, operator yes 10:44:27Z, merged 10:45:30Z. - Live on the Dell by 10:50:31Z: one pod, tunneldashecorp-dell-v2(f3b364b4…), 4 registered QUIC connections (osl02 ×2, arn08, arn09), no errors. - GCP's cloudflared is untouched (same pod, 0 restarts).rig-conductor-dell.dashecorp.comstill returns 403 without a token. - C5's first run (38718908545) stopped safely and wrote nothing:ABORT: DELL_TENANT_MAP_CLIENT_SECRET is not an Access client secret (64 hex). - Cloudflare changed the format: Access service-token secrets made on or after 2026-08-26 arecfast_[40 alphanumeric][8-character checksum]. Older 64-hex secrets still work. - Fix: rig-gitops#1017 (issue #1016). Its test-only commit went red in CI with exactly the 5 predicted failures, and the fix went green. MergeGate merged it at 10:44:25Z. - The re-run 38722557796, approved by the operator, pushed the file at 10:55:00Z. - PR-15 = rig-gitops#1018, the Dell's conductor reads the tenant map with its own token: - Review-E first requested changes: Flux cannot decrypt a Secret whose kind and metadata are encrypted. The orchestrator answered with evidence, and Review-E withdrew it at 11:09:52Z: kustomize-controller v1.9.4decryptor.go:639decrypts any resource that carriessops.mac; the same Kustomization already reconciles four such files on both clusters; and no Kustomize transformer touches an encrypted resource. Docs: rig-gitopsdocs/sops.md, "Whole-file encryption, and the one rule it needs". Guard: rig-gitops#1019. - Operator "yes PR-15, once Review-E approves" at 11:02:28Z. Merged 11:11:02Z. - On the Dell the conductor rolled,mapConfigured: true, and the worker accepts its token. But every fetch fails to parse: the worker sendsgeneratedAtas epoch milliseconds (a number), and the conductor's DTO expects a string. - The fence stays fail-closed (map_unreachable). GCP is untouched, because its fence is off. - The fix, rig-conductor#2717 (issue #2716):EpochMsOrStringConverteronGeneratedAt. It never throws: an integer becomes ISO-8601 UTC, and a string is kept as sent. - The new test reproduced the Dell's exact error, and with the fix the whole API test project passes (2381). - Its merge auto-pins the image, so the conductor restarts on GCP and the Dell. - Operator yes at 11:36:45Z. Merged at 11:55:22Z (89b6787) once Review-E approved, and the image pin #2719 auto-merged at 11:57:30Z. - Live on both clusters by 12:01:33Z, 0 restarts. - On the Dell the map loads (mapFetchedAt12:01:06Z,lastFetchErrornull). It lists 3 tenants, dashecorp, invotek and runthedocs, all atgcp, so the standby refuses their work withmapped_elsewhere. - GCP's conductor restarted onto the same image; its fence is off, and its public probes are unchanged. - The SOPS render guard, rig-gitops#1020 (issue #1019): a render rule,sops-untouched, fails when a Kustomize transformer would rewrite a whole-file SOPS resource before Flux decrypts it. CI also runs it on GCP. - Flux'scommonMetadatais safe: kustomize-controller applies it after decryption. - Review-E found two ways around the first version: a patch that replaces a stub's metadata, and an annotation key thatcommonMetadatarepeats. - The fix walks each build chain and expects exactly one untouched stub for every SOPS file. - Testing it on real files found a third gap: Flux appendsspec.componentstospec.path's own kustomization file. That is fixed too. Review-E approved the fixed head, and MergeGate merged it at 12:33:47Z (f50c92c). The Dell re-applied it with 18/18 Kustomizations and 19/19 HelmReleases Ready. - Live webhook routing, steps 2–5 done: no code gave the router its HMAC secret (GITHUB_WEBHOOK_SECRET), so: - Step 2: OpenTofu writes it to rig-cockpit-worker asROUTER_WEBHOOK_SECRET, because GitHub refuses Actions secret names that start withGITHUB_. - infra#664 merged at 12:44:05Z on the operator's "yes 663" (12:43:31Z). Its apply added exactly 1 resource; every other module had no changes. - rig-cockpit-worker#302 merged at 12:47:54Z on "yes rcw-301" (12:46:46Z). Its deploy loggedset worker secret GITHUB_WEBHOOK_SECRET. - The probes are unchanged. The GCP conductor kept receiving deliveries through the shadow router (27 in 4 minutes) with no signature rejects. - Step 4: the operator checked theRouter shadow: would routelogs and replied "shadow ok" at 12:51:07Z: every delivery on the right tenant,hmacValid: true. The orchestrator could not read them: the operator's browser Cloudflare login has only other accounts. - Step 4b, rig-cockpit-worker#306 (issue #305), thegcprepoint: - The hold: before asking for "yes rcw-303", the orchestrator re-checked the live path. In D1,gcp'sbase_urlwas still migration 0009's seed, the stable hosthttps://rig-conductor.dashecorp.com("Repoint … once #565 lands"), which carries the router's own route. - The queue consumer is invoked by no request, so it is not on the route's zone. Cloudflare documents the same-zone origin bypass only for "a Worker's own zone". A public case, QNFO/qnfo-workers#635, shows a scheduled handler's fetch to same-account Workers on workers.dev getting error 1042. - So a live forward would most likely have re-entered the router and got508. GitHub would see202for every delivery, and none would reach the conductor. #304 was held. - The fix: migration 0014 repointsgcptohttps://rig-conductor-gcp.dashecorp.com(infra#566: same tunnel and origin, Access admits only the Worker's service token, no Worker route). It runs only while the row still holds the stable host, and it clears the four health columns. A test checks that no location'sbase_urlhost isWEBHOOK_ROUTER_HOST. - A Mac Mini job wrote it: red 4/4, then green,npm test1123/1123. The orchestrator softened one unverified claim. - Review-E approved28db2a7at 13:53:02Z, and the operator said "yes rcw-305" at 13:53:01Z. Merged at 13:53:39Z (ee6502e); the migration applied at 13:53:59Z. -/api/locationsat 13:55:50Z:gcpatrig-conductor-gcp,healthy, last probe OK at 13:55:25.748Z, no error. The cron probe is the same kind of zone-less fetch as the consumer, through Access with the Worker's token. - Step 5, rig-cockpit-worker#304 (issue #303), done: it setsROUTING_MODE = "live". Review-E approved head91870f9at 13:01:33Z (1119/1119); main plus #304 passed 1123/1123 locally before the merge. - The operator said "yes rcw-303" at 14:02:43Z. Merged at 14:04:01Z (d88f250); the deploy putROUTING_MODE"live"at 14:04:33Z. - Verified: repo hook 639292554 deliveries answer202(200before), and 4 of 4 reached the conductor by GUID, 2–8 s later:| Delivery | `202` at | Conductor | |---|---|---| | workflow_run.completed | 14:05:05.1Z | 14:05:09.8Z | | check_run.completed | 14:05:04.9Z | 14:05:13.2Z | | workflow_run.completed | 14:05:48.3Z | 14:05:50.7Z | | check_run.completed | 14:05:48.3Z | 14:05:51.5Z | - Org-hook deliveries from `dashecorp/rig-docs` reached the conductor at 14:06:03–13Z. - The conductor log since the switch has 0 signature lines and 0 exceptions. - **Before the live version deployed:** 2 deliveries in the merge burst (14:04:16–17Z, shadow pass-through) got `500`, while the conductor's Kestrel logged a slow-request-body timeout. - One of them (check_run.created) was still processed. The other (workflow_run.requested, informational) was not; GitHub can redeliver it. - They are the only non-2xx answers in the hook's last 100 deliveries. In live mode the queue absorbs such a burst and retries a 5xx. - The router doc in rig-cockpit-worker records steps 4b and 5 in #308 (issue #307), a docs-only PR.- Afternoon:
- infra#666 (issue #665), merged by MergeGate at 14:24:40Z: the Dell reinstall runbook now stages the four rig-gitops#989 kubelet args (reservations, eviction, serialized pulls). Docs only; no apply ran.
<and%are safe ininstall_exec: the installer'sescape()backslash-escapes<beforeeval(get.k3s.io line 1203), and on the Dell's systemd 255 a test user unit kept%,and a trailing%literal.- The new check expects allocatable 7 CPU and 28870Mi. The measured capacity is 8 CPU and
32708648Ki, and today allocatable equals it. - The router since it went live (14:04:33Z): all 28 deliveries on hook 639292554 up to 17:04Z answered
202. The GCP conductor processed 132 deliveries by GUID between 15:15:33Z and 17:04:17Z (its log window), with 0 signature lines.
- The goal changed: the Dell primary, GCP a cold standby. The operator at 14:52:29Z: "I want dell too be the main rig. I want the GCP to run on as low cost as possible. But if Dell goes down it should be ready to start up gcp again and take over".
- GCP is not deleted. #990's step 14 becomes "GCP to cold standby, with failover and failback", tracked in rig-gitops#1021; a comment on #990 records it.
- The design, rig-gitops#1023 (issue #1022): merged at 15:32:52Z (
b9f233a) after Review-E approved it. It is rig-gitopsdocs/2026-10-15-gcp-cold-standby.md(status: proposed), written by Mac Mini job standby-1021. - Cost: about $179/month today, about $8 in standby, about $7 once Artifact Registry and the 3 orphan PVC disks are gone. Prices checked on gcloud-compute.com on 2026-10-15:
e2-standard-2$53.86 a month; pd-standard $0.044 and pd-balanced $0.11 per GB-month (europe-north1). - Standby: OpenTofu stops the VMs through a stored desired-state object, not a
-var, because infra's workflow applies every module on every merge and its dispatch takes no inputs. The disks keep etcd, the Secrets and the images. - Failover: GCP starts itself after the Dell has been down 10 min, which needs a narrow infra-dispatch token in the Worker. The tenants move on the operator's tap, which needs a
skipDrainchange (the drain is clock-only today).- The default is a fresh start on GCP's own database with the reconcile sweep.
ReconcileSweep__DryRunis still true at both locations and must be turned off. - RTO is about 25–40 min. Restoring R2 first takes about 2.5 h: the R2 backups are suspended, and a restore decrypts by hand with the offline key.
- The default is a fresh start on GCP's own database with the reconcile sweep.
- Before GCP can sleep: a role swap (GCP runs the planner, Rig-E and the dotnet-codex canary, and only GCP holds
RUNPOD_API_KEY); the stable host must follow the primary; GCP's tunnel secret must be rotated, now that there is no retirement. - The plan: "now" items first (fence GCP with
LOCATION=gcpand a tenant-map reader; the Worker's down alert, Fail-over button and drain skip; the infra state object and the stable-host CNAME), then the rest after #990 Phase 3.
- Evening: GCP is fenced, standby steps 2–5 are live, and the approve-rule is merged.
- GCP's own tenant-map token, infra#668: merged 18:08:52Z. The second apply (18:14:43Z) wrote
GCP_TENANT_MAP_CLIENT_ID/_SECRETto rig-gitops'credential-opsand madeTENANT_MAP_READERS[dell, gcp]. The Worker deploy put it live at 18:17:52Z; the Dell's map reads stayed clean. - The
c5-gcpstep, rig-gitops#1025: merged 17:55:26Z. Run 38784866311 pushed the encrypted file to the fence branch. The operator approved it in Safari: GitHub Mobile shows no Approve for runs triggered under the operator's own login, because no notification is sent. - GCP's conductor is fenced, rig-gitops#1027:
LOCATION=gcpwith its own token. Merged 18:57:52Z; the new pod was Ready at 18:58:37Z. - All 10 runbook checks passed. A 30-min capture was clean: 0 stale maps, 0 errors, 0 refusals, 3 tenants
gcp/active. - Finding: dashecorp's dry-run activation sweep stopped on a 403 on
dashecorp/dashe-websitecheck-runs, with 14714 rate limit remaining. It must be fixed before the sweep goes live (design step 9). - rig-cockpit-worker, the failover side:
- #310 (19:42:10Z): migration 0015 adds the
delllocation row, astandbyflag andhealth_notified. Down/up health alerts go to the operator by APNs push, claimed with a compare-and-swap. There is no #admin fallback, becauseDISCORD_ADMIN_WEBHOOK_URLis not provisioned. The operator confirmed that pushes arrive. - #312 (20:20:21Z):
skipDrain, only while the source is unhealthy;POST /api/locations/{from}/evacuate; a failed notice is retried. - #314 (21:15:21Z): the Fail over… action in the Locations tab, with Review-E's fix for a stale-response race.
- Operator decisions, 21:36:16Z ("yes standby defaults", "yes rotate-gcp"): a fresh start on GCP's own DB at failover; every GCP-only app moves to the Dell in Phase 3; a narrow
INFRA_DISPATCH_TOKEN, created by the operator, lets the Worker start GCP automatically (step 7); the GCP tunnel-secret rotation runs right after step 6. - The design is accepted (rig-gitops#1028). Step 6, the rig-location switch, is in progress as infra#669.
- The approve-rule, claude-3#1191 (merged 21:42:24Z,
34061e0): a standing exception inCLAUDE.md. The orchestrator may approve a waitingcredential-opsrun in rig-gitops (C2, C4/C5) only if it dispatched the run itself frommain, and the operator replied with a literal yes naming that step after being told the run number, step and target branch. The approval goes throughPOST /repos/dashecorp/rig-gitops/actions/runs/<id>/pending_deploymentswith a comment quoting the yes and its timestamp. Never a rejection, never another environment (CI/CD).
- GCP's own tenant-map token, infra#668: merged 18:08:52Z. The second apply (18:14:43Z) wrote
- 2026-10-18: runthedocs watch day 2 is green. One check at 04:14Z (deadline #48, ends 2026-10-19):
- runthedocs is active at the Dell (epoch 2), and dashecorp and invotek stay at GCP (epoch 1). The Dell's fence map was 50 s old, with no fetch error.
- k3s has not restarted since the planned 2026-10-16 13:53Z restart.
- The drive matches its baseline: smart_ok, critical-warning bit 0x4 and 107% used as before, 100% spare, no media errors, worst await 2 ms.
- The Dell's conductor (pod
k8mlz) had run 19 h with 0 restarts. Its log had 0 fail/crit lines in 11 h; the warnings are the expectedmapped_elsewherefence lines for the two GCP tenants.
- 2026-10-17: runthedocs holds on the Dell (watch day 1 green), and the re-dispatch hold is enforced.
- Watch day 1 (deadline #48, ends 2026-10-19): three checks, all green, at 07:32Z, 14:19Z and 19:53Z.
- runthedocs stays active at the Dell (epoch 2). Both fences are fresh: map age under 40 s, no fetch error.
- k3s has not restarted since the planned 2026-10-16 13:53Z restart.
- The drive matches its baseline: smart_ok, no media errors, await 1–4 ms.
- The Dell's conductor runs with 0 restarts and no fail/crit lines.
- rig-conductor#2723 ("yes 2723", merged 08:58:34Z,
bac1e72):REDISPATCH_HOLD_MODE=enforceat both locations. - The two swift issues that kept looping to the stopped iBuild-E (dashe-nutri-e#63, rig-cockpit-ios#116) were held at 09:06Z and set to failed. That ended the #admin spam.
- Since then, every StaleClaimWatcher tick at GCP has released 0 claims.
- False "review-e DOWN (crashloop)" alerts (21:14:56Z and 22:02:56Z).
- Review-E is KEDA-scaled from zero, and one activation starts two StatefulSets that share the agent id.
- So two reviews within 30 minutes trip
AgentCrashLoopPolicy's 3-starts rule, even though the heartbeat is 0–1 s old. - Filed as rig-conductor#2725.
- Open, deferred by the operator: at 22:12:06Z the Dell's ReviewScan routed run-the-docs/engine#95, labelled
needs-reviewsince 2026-07-02, toassignments:review-e@runthedocs. runthedocs has no Review-E fleet, so nothing will take it.
- 2026-10-16 evening: #990 re-planned, three gaps closed, and runthedocs is the first tenant on the Dell.
- The re-plan (rig-gitops#990): the drive stays.
- The tenants move one at a time, lightest first, and GCP keeps running until the last one is stable.
- runthedocs goes first, before the backups, because its history stays on GCP. invotek waits for the backups (#897, which starts with O2). dashecorp waits for a clean week with invotek.
- Stop rule: a drive alert, or an unplanned k3s restart with tenants on the Dell.
- Measured: GCP's pods write about 34–42 GB/day, so the Dell would go from 13 to about 45–55 GB/day, which is 1% of rated wear every 3 weeks (110.7 TB written of about 103 TBW).
- Three gaps that would have broken the first move:
- rig-gitops#1045 ("yes 1045"): the Dell's conductor knew only
invotek, under the wrong org (dashecorp). A one-shot Job registered all three tenants with GCP's identities. The finished Job was removed in #1047. - rig-cockpit-worker#320 ("yes 320"): the webhook router now checks repo overrides before the installation, as the conductor's
TenantMatchdoes. The six rig-internal repos are pinned to invotek, so invotek can move apart from dashecorp.- Its deploy failed, because D1 caps a compound SELECT at 5 terms (stock SQLite allows 500). The pipeline stopped before "Deploy worker", so nothing live changed.
- #323 ("yes 323") rewrote migration 0017 as six single inserts and added
test/utils/migrations-d1-limits.test.js. It deployed at 20:05:49Z (version9fff35ce).
- infra#690 ("yes 690"):
/etc/rancher/k3s/config.yaml, hand-written since March withmax-pods=200, is now owned by OpenTofu (null_resource.kubelet_config). It adds rig-gitops#989's reservations, the eviction thresholds andserialize-image-pulls.- Not through
install_exec: the k3s docs say a CLI list replaces the file's list, and%is a systemd specifier. - Applied at 13:53Z with one k3s restart: allocatable 7 CPU and 29562920Ki, worst drive await 5 ms.
- Review-E caught that an interrupted restart would never recover. A pending-restart marker fixes it.
- Not through
- runthedocs moved to the Dell (#990 step 12). The operator tapped Move in Rig Cockpit at about 21:11Z.
- Timeline: draining from 21:11:49Z, switched at 21:27:44Z, active at
dell(epoch 2) by 21:28:55Z. - The Dell's dry-run sweep completed with nothing set. GCP now refuses runthedocs as
mapped_elsewhere. - Two test webhooks reached the Dell's conductor about 3 s after GitHub sent them, and GCP got neither.
- A 3-day watch runs until 2026-10-19 (deadline #48).
- Not proven: an agent run on the Dell. runthedocs has no agent fleet anywhere:
assignments:*@runthedocshas no consumer, and run-the-docs PRs have not been reviewed since at least June. - Gotchas from the day:
- An operator-authored PR outside the six rig-internal repos gets Review-E only with the
needs-reviewlabel. rcw#320 waited 1h45m without it. - In portrait on an iPhone, the Rig Cockpit Locations tab does not scroll; landscape works (rcw#325). The iOS app has no Locations tab.
RepoDiscoveryServiceis not fenced (rig-conductor#2721, low).- The GCP conductor's log buffer holds only about 30 minutes.
- 2026-10-16 midday: standby steps 7 and 8, and an early warning on the Dell's drive.
- Step 7 is live: the Worker starts GCP itself. The operator said "yes rcw-318, yes rcw-317" at 09:54:19Z.
- rig-cockpit-worker#318 (deployed 09:55Z, migration
0016_location_auto_starts): when every location holding anactivetenant has been unhealthy for 10 min or more andgcpis markedstandbyand not healthy, the cron dispatches infra'srig-location.ymlwithgcp=running primary=keep, once per incident, and tells the operator. It never stops GCP, moves a tenant or changesprimary. -
317 (deployed 10:00Z) put
INFRA_DISPATCH_TOKENon the Worker; the deploy logged "set worker secret".¶ - It is inert today, because every tenant is on GCP. Drill 1 is the first real dispatch.
- Step 8: infra#688 merged with no changes (
metadata_startup_scriptis inignore_changes). The startup scripts skiptailscale upwhen already logged in, and never use--force-reauth. The live VMs change only throughscripts/gcp-startup-script-push.sh, which the operator runs, and only if drill 1 shows the problem. - The Dell keeps its worn drive (107% used,
critical_warning0x4). The operator declined a replacement, because it sits at their parents' house and they live in Gran Canaria. The 24 h re-measure after the write-load cut: 13.4 GB/day, down from about 30, and no stalls (max 34 ms). - The early warning is live instead. The operator said "yes 687, yes 1043" at 11:15:40Z.
- infra#687: nvme-diag writes
/run/nvme-diag-health/healthevery 30 s. - rig-gitops#1043: node-pressure-watch posts to #admin on media errors, spare below 100, a new warning bit, a 30 s average await over 200 ms, a failed read, or a missing or stale file.
- The stall check is sustained latency only. Review-E showed that one slow I/O among many fast ones averages away, and
/sys/blockkeeps no per-I/O latency. The first run, at 11:37Z, was quiet.
- 2026-10-16 morning: the GCP tunnel rotation is complete, and the step-7 token exists.
- The rotation (infra
docs/2026-10-15-gcp-tunnel-rotation.md): - R2: rig-gitops#1034, the
c4-gcpstep, merged at 00:32:18Z. A cancelled notifier check had held it, approved, for 37 minutes. MergeGate accepts onlysuccess,skippedandneutral, and its "Notify" skip token never matched the job namedispatch. rig-gitops#1037 renamed the job. - The
c4-gcprun: on the operator's "yes c4-gcp" (08:03:57Z), the orchestrator approved run #16 under the approve-rule. It pushed the encrypted v2 token to #1035, which then merged at 08:15:22Z.cloudflared-v2registered 4 connections todashecorp-gcp-v2(9b7a1aa0…). - R3: infra#682 (applied 08:23:12Z) moved all 7 GCP CNAMEs to v2: 0 added, 7 changed, 0 destroyed. The old connector's request counter, read from its metrics port by pod IP, went flat.
- R4a: rig-gitops#1039 (08:44:24Z) deleted GCP's base
cloudflaredwith a Component$patch: delete, and Flux pruned it and its Secret. The Dell keeps the base Deployment.tunnel-idnow names v2.c4-gcprefuses the old tunnel's fixed ID and requirestunnel-id, so it can still re-encrypt v2's token. - R4b: infra#684 (applied 08:53Z) destroyed
dashecorp-gcpand its config (2 destroyed). - Afterwards: all 8 hostnames answered as at the 08:13Z baseline, webhooks arrived with 0 signature rejects, and v2 had served 306 requests with 0 errors.
- Step 7: the operator created
INFRA_DISPATCH_TOKEN(credentials; deadline #47). The Worker change that uses it is rig-cockpit-worker#315. - invotek.no: the operator renewed it on 2026-10-16. Deadline #5 has moved to 2027-10-16. Norid's public WHOIS shows no expiry date, so this rests on the operator's word.
- Still the operator's:
- the offline backup key O2 (operator-inbox#43);
- the SSD decision, after the write re-measure;
- the literal yes that deploys #315.
- The rotation (infra
- Late evening: step 6 is complete, and the GCP tunnel rotation has started.
- The rig-location switch (infra#669, step 6): four PRs, all merged and verified.
- infra#673 (22:28:29Z): the state object
gs://tablez-terraform-state/rig-location/desired.json({"gcp":"running","primary":"gcp"}), infra'srig-standbyenvironment (the operator is the only reviewer,mainonly, no admin bypass), and an inert stable-host rule on the Dell tunnel. - infra#676 (23:17:02Z):
gcp/dashecorpandcloudflare/dashecorp.comread the object at plan time. The apply changed nothing. - infra#680 (23:30:27Z), Review-E's P1: a stop is published only while the stable host's live CNAME names
dashecorp-dell-v2, andprimary=gcponly while GCP's three VMs are liveRUNNING, re-checked at write time. A 41-case offline selftest runs in CI. - infra#675 (23:41:03Z):
rig-location.yml, dispatch only, not yet dispatched. - Change from the design:
primary=dellalso waits for the operator inrig-standby, so the step-7 token must have no Deployments permission. Failover and failback are two dispatches each. - The GCP tunnel rotation (design step 13, moved earlier):
- R1, infra#678 (23:21:59Z):
dashecorp-gcp-v2with arandom_bytessecret that OpenTofu never prints. A second run wroteGCP_TUNNEL_TOKENto rig-gitops'credential-ops(23:27:55Z). Nothing moved. - R2, in review: rig-gitops#1034 (the
c4-gcpstep) and #1035 (a second GCP connector,cloudflared-v2, a draft until the run). #1035 also widens tgc-play's NetworkPolicy, which admitted onlyapp: cloudflared. - Next: the
c4-gcprun on the operator's literal yes, then R3 (every GCP CNAME → v2) and R4 (the old tunnel deleted). - Found: after a force-push, GitHub reported earlier reviews against the new head, in REST and GraphQL. MergeGate matches approvals by
commit_id, so a stale approval might satisfy it. To be tested: dashecorp/rig-conductor#2720.
- Backups (rig-gitops#897): O3, the R2 writer key pair, now goes through credential-ops as C4 and C5 did, so no person handles it. O2, the offline age key (operator-inbox#43), stays the operator's.
- Still the operator's (both standby yeses came at 21:36:16Z, see Evening):
- the literal yes for the
c4-gcpcredential-ops run (rotation R2); - creating the narrow
INFRA_DISPATCH_TOKENfor GCP's auto-start (step 7), with Actions only and no Deployments; - renewing invotek.no, due 2026-10-16;
- the offline backup key O2 for rig-gitops#897, needed before the first tenant moves to the Dell;
- the SSD decision, after the 2026-10-16 write re-measure.
- the literal yes for the
- Evening: the Dell standby is complete, and the operator wants everything moved there.
- PR-13 is live (rig-gitops#992, merged 18:07Z). Every agent fleet is defined on the Dell at 0 replicas. The
agent-homeclaims are onlocal-path, and the fenced conductor runs at 1 (LOCATION=dell, no tenant map, every write fails closed). By 20:05Z all 19 HelmReleases were Ready, with 0 agents running. - The install burst stalled the worn drive: 1.7–6.7 s per I/O with
ps=0, so wear, not APST (posted on infra#622). k3s restarted once, and the API was down about 2 minutes. Apps were unaffected. - Live webhook routing (rig-cockpit-worker#296) is deployed in shadow mode. Review-E caught a bug in it: the Queue's default
jsoncontent type emptied every webhook body. It was fixed withcontentType: 'v8'. - Incident, 18:54–20:03Z, the GCP planner was down (rig-gitops
docs/2026-10-14-agent-env-duplicates-incident.md). #994 removed duplicateextraEnventries. Helm 3's strategic merge then deleted both copies of each duplicated key from the live StatefulSets: the planner lostDISCORD_BOT_TOKEN, and four agents lostOPENAI_API_KEY. The revert (#996) could not re-add them ($setElementOrder). The fix was #997: the dedupe plusspec.upgrade.force(replace, not patch). It was verified on the live objects. - The goal now: everything on the Dell, GCP to zero, then deleted (plan rig-gitops#990). The Dell has more CPU and RAM than the whole GCP rig. The blocker is the worn drive. The recommendation is an external USB SSD at the house to hold k3s's data, awaiting the operator's decision. The other steps are the off-site backups (the O2 key, operator-inbox#43), the Dell tunnel and tenant map (PR-14 and PR-15), live routing, capacity guardrails (#989), and the tenant moves. Changed on 2026-10-15: the Dell becomes the primary and GCP a cold standby (rig-gitops#1021).
- The Dell's etcd stalls have a cause (dashecorp/infra#622). A Mac Mini job analysed a read-only evidence bundle from the Dell, and the orchestrator spot-checked its findings.
- The cause: for 2–5 hours at a time, the NVMe drive gets 15–21 times slower at the same load. There have been five such episodes since 10-07. Inside one, any write burst stalls etcd's WAL
fdatasync:- The
*/15CronJobunschedulable-pod-watchcauses 90% of the warnings. - On 10-08,
apt-dailycaused a 25.7 s fsync, after which k3s lost itsk3s-etcdlease and exited.
- The
-
What slows the drive: it is worn out. The capture below found it at 13:47Z.
smart-logreads:critical_warning0x4(reliability degraded) andpercentage_used107%;- 110.63 TB written in 63,270 power-on hours;
- spare at 100% and no media errors.
Thermal throttling is ruled out: the drive has never had a thermal-management transition. APST is on, and not yet ruled out. The rig writes about 13 GB a day to the drive. fwupd lists no NVMe firmware update; the BIOS has one, 1.28.0 to 1.40.0. - Next: replace the drive (any M.2 2280 NVMe) at the operator's next visit, along with the BIOS update. No tenant moves to the Dell before then. - Data at risk is small. buzz's volumes are on the NAS, and the rest (the standby Postgres and Valkey, and a 550 MB etcd) rebuilds from GitOps. - Backups: etcd stores Secrets unencrypted here (
secrets-encryptis disabled), and the NAS export is open. So the NAS copy planned in rig-gitops#897 must be age-encrypted, which waits on the operator's O2 key. - Cleanup (the operator's yes): used space went from 45.0 to 30.3 GiB, by removing the two dead runner directories, 89 stale buildah images and two old k3s data dirs. Two more k3s data dirs stay, because running container shims still use them. A cleanup does not undo wear. - Corrections: my first read on the issue had two errors. Two Job pods did start in the 10-08 window, and sar does record nvme0n1. - Fixes, both live, through OpenTofuk8s/dell/k3s: - dashecorp/infra#637 (Closes #634): all five leases go to 120 s with a 90 s renew deadline. The controller-manager, scheduler and cloud-controller-manager are set throughinstall_exec. k3s's ownk3sandk3s-etcdare set through a systemd drop-in withCATTLE_ELECTION_*, which k3s v1.36.5 reads through rancher/wrangler v3.7.0. - Applied at 13:12:09Z: all five have read 120 s since 13:13:10Z. k3s restarted twice, about 15 s down in total. -k3sandk3s-etcdkept their holder. The three kube controllers had no leader for about 82 s while their old 60 s leases expired. -NRestartsreads 0, because systemd resets it on a manual restart. - dashecorp/infra#638 (Closes #635), live at 13:46:53Z: a drive capture. -nvme-cli2.8-1ubuntu0.1. The operator approved it ("yes nvme-cli", 2026-10-14T12:40:55Z); it is recorded inapproved-tools.md, and in infra's own registry with a security review. - A 30 s journal line, tagnvme-diag, and per-minute sar. - A one-shot read, tagnvme-diag-once. It stays in the journal only, becauseid-ctrlprints the serial. - The Dell's agents (PR-13) wait for the new drive. - Kept on purpose: the Dell's automatic security updates. It holds the shared SOPS key, so surviving the stall beats removing the trigger. - Operator decisions (2026-10-03): - #896 D1: GHCR pulls use a read-only classic PAT. - Merging: the orchestrator merges a PR once Review-E or an independent subagent review has approved it. Production changes, credentials and spend still go to the operator. - Approvals: every operator approval is to become a Rig Cockpit push notification with an Approve tap. GitHub Mobile is the stopgap until then. A GitHub App user token would approve deployments as the operator, so the design adds a passkey step-up.
GCP k3s Cluster (Dashecorp)¶
Host: GCP Compute Engine, europe-north1-b (Finland)
Nodes: 3 Standard (non-preemptible) VMs, all e2-standard-2 (2 vCPU, 8 GB; ~7.75 GiB allocatable): invotek-k3s control plane + invotek-k3s-worker-1/2. The workers were e2-medium (4 GB) until infra#461 resized them on 2026-09-26 (see the 2026-09-25/26 incidents below). The workers were Spot until infra#385 merged 2026-07-02; verified PROVISIONING_MODEL=STANDARD, all RUNNING/Ready on 2026-09-08.
GitOps repo: dashecorp/rig-gitops (Stig-Johnny/cluster-gitops archived 2026-06-03)
Orchestrator: FluxCD
Secrets: SOPS + GCP KMS
IaC: OpenTofu (dashecorp/infra/gcp/dashecorp/, tofu CLI)
Storage: local-path (aliased as nfs-csi). The agents' home is NFS. Since 2026-10-01 13:23Z every lane mounts PVC agent-home, an NFSv4.2 export of the 10 GB pd-balanced disk agent-home on invotek-k3s (10.166.0.33:/srv/agent-home, daily snapshots kept 7 days; dashecorp/infra#508, rig-gitops#885). The two 1 TiB Filestore instances it replaced were destroyed on 2026-10-13 (dashecorp/infra#632, on the operator's approval) after the 7-day soak and the rig-gitops cleanup (#883). Inode budget: Codex left one empty tmp/arg0/codex-arg0* directory per run, about 2,175 a day on this 655,360-inode disk. rig-agent-runtime#790 now prunes them after 2 hours (#763 closed), and the inode count stays flat.
Cost: compute at europe-north1 on-demand list price is 3 × $53.86 = ~$162/month since 2026-09-26 (was ~$108 with e2-medium workers), before sustained-use discounts; disks extra. The startup credit is spent: the $2,000 Google for Startups credit ran out in August 2026, so the bill is paid in real money. September cost $542.80 gross: two 1 TiB Filestore volumes $366.24 (replaced on 2026-10-01 by the ~$1.25/month agent-home disk, and deleted on 2026-10-13, infra#632), E2 compute $112.74, Artifact Registry storage $43.78, the rest under $15. Read the current figures from the billing export (invotek-github-infra.billing_export) with bq query, not from this page. Billing account: since 2026-10-01 10:10:59Z the project bills to Dashecorp Billing (0151A6-EDAFD0-29E42A), no longer 015BBC-422A59-EB7AF4 (dashecorp/infra#530). Until Dashecorp Billing's BigQuery export is on, the table above stops at that time.
Migrated 2026-03-27: Dell k3s (Oslo) → GKE Autopilot → GCP spot VMs + FluxCD
Node service account and metadata access (since 2026-10-02)¶
All three VMs run as the project's default Compute account, 1090472687120-compute@. Until 2026-10-02 it held project Editor. Pods can reach the GCE metadata server, which the backup CronJob relies on. So any pod could take an Editor token and mint a key for github-actions@, the OpenTofu CI account that decrypts all infra state; the project has no organisation, so no policy blocked that.
Two layers close it:
- Agent namespaces cannot reach 169.254.169.254 (rig-gitops#887, apps/metadata-guard). This covers dashecorp, dev-e, review-e, rig-planner, stigjohnny, rig-assistant and rig-lint. gce-pd-csi-driver and rig-conductor keep access.
- The account holds exactly roles/compute.storageAdmin plus the custom role gcePdCsiAttach (instance get, attach and detach), and nobody holds Editor (dashecorp/infra BOOTSTRAP §24). gcp/dashecorp/node-sa.tf fails the plan if that drifts (infra#539). Changing those roles takes an operator command, because CI cannot change project IAM.
What Runs on It¶
| Service | Namespace | Endpoint |
|---|---|---|
| rig-conductor API + dashboard | rig-conductor | rig-conductor.dashecorp.com via CF tunnel + Access; also NodePort 30080, no auth (in-VPC/tailnet only; networking.md, dashecorp/infra#448) |
| rig-planner | rig-planner | (cluster-internal) |
| Dev-E agent pods | dev-e | — |
| Review-E agent pods | review-e | — |
| Postgres (Marten event store) + Valkey (streams) | rig-conductor | Postgres cluster-internal, on the control node's local-path boot disk. Valkey is also on NodePort 30079 with auth off (in-VPC/tailnet only, dashecorp/infra#448), and its data is on a 2Gi local-path PVC (data-valkey-upstream-0) |
Rig-E — dashecorp tenant chat agent (rig-agent-runtime, read-only, brain = operator vLLM pod) |
dashecorp | rig-e.dashecorp.com via CF tunnel → Rig Cockpit picker |
dashecorp tenant fleet (review-e / dev-e / planner @dashecorp, KEDA 0→1) |
dashecorp | — |
| stigjohnny tenant fleet | stigjohnny | Pending — waits on the missing stigjohnny-shared-claude PVC (parked), not on node capacity |
RunPod brain (conductor-owned)¶
The rig-conductor pod on this cluster owns the operator's on-demand vLLM GPU pod on RunPod (dashecorp/rig-conductor → docs/runpod-pod-control.md is canonical): POST /api/pods/qwen/start runs a volume-first wake — reuse the pod attached to volume qwen-weights (bjn7d94ej9, EU-RO-1) if it can get its GPU back, otherwise create a replacement from template cnz5fxyadi on the cheapest acceptable GPU (≥ 32 GB, ≤ 1.00 USD/h) and terminate the stale pod; POST …/stop; GET … reports podId + proxyUrl. RunPodIdleMonitor stops the pod after 15 idle minutes counted from model-ready — the first successful vLLM /metrics scrape, not from RUNNING (rig-conductor#2056, live 2026-09-09; the flat window used to spend the ~6-minute model load as idle). A pod that never becomes ready is stopped after RUNPOD_BOOT_GRACE_MINUTES (default 20), and the 6 h wall-clock cap is unchanged. GET … also returns readySince — isRunning: true with readySince: null means "waking, not usable yet". All on RunPod REST v2 (v1 retired, rig-conductor#2043). Key: rig-conductor-secrets/runpod-api-key (see credentials.md). Rig-E no longer pins a literal pod URL: llm.baseUrlFrom resolves it from the conductor's GET /api/pods/qwen at boot and per turn, re-resolving once on a 404/502/503/504/network failure (rig-agent-runtime#677, shipped 2026-09-09), so a RunPod migration or a replacement pod costs no gitops PR. A chat turn against a stopped pod also wakes it (rig-agent-runtime#682/#689). Proven unattended on 2026-09-08 (idle stop fired; volume resolution without a pin); first live wake-on-chat 2026-09-09.
Operator runbook: gpu-brain.md — wake it, tell whether it is actually usable, stop it, and what the waking replies mean.
Three-node placement — the 2026-09-07 outage and its fixes¶
What happened: invotek-k3s-worker-2 went into a memory-pressure livelock (no OOM-kill, kubelet stopped answering) and took the conductor and Rig-E down with it — a full rig outage surfacing as a cockpit 502. Root cause: every runtime and the conductor still carried requests.memory: 1Mi from the single-node era, so the scheduler had bin-packed 7 runtimes + the conductor + Rig-E onto one 3.9 GiB worker. Post-mortem: rig-conductor#2003.
Fixes (verified in production the same night):
| PR | Change | Effect |
|---|---|---|
rig-gitops#725 |
13 HelmReleases: requests.memory 1Mi → 640Mi (7 × 640 Mi > one worker's 3.9 GiB) |
scheduler is forced to spread the fleet — now 4 / 4 across the workers |
rig-conductor#2005 |
conductor requests.memory 512Mi → 1536Mi; preferred nodeAffinity for the control plane (weight 100); preferred podAntiAffinity vs app.kubernetes.io/name=rig-agent-runtime with namespaceSelector: {} (weight 50) |
conductor lands on the 8 GiB control node next to Postgres, never packed in with runtimes |
rig-cockpit-worker#77 |
external node-liveness check (open) | the one post-mortem item not yet built — nothing outside the cluster notices a hung node today |
Placement on 2026-09-08 (live):
| Node | Allocatable | Runs |
|---|---|---|
invotek-k3s (control plane) |
8 GiB | postgres-0, rig-conductor-api |
invotek-k3s-worker-1 |
4 GiB | rig-e, dev-e-codex, review-e-codex, rig-planner |
invotek-k3s-worker-2 |
4 GiB | dev-e, dev-e-dotnet, dev-e-dotnet-codex, review-e |
Rules that came out of it: memory requests are what the scheduler bin-packs on, so they must be honest on a multi-node cluster (the old "1Mi so it always schedules" rule is single-node-only — Dell, not the rig). Pod (anti-)affinity is namespace-scoped by default; a rule meant to keep the conductor away from runtimes in other namespaces is a silent no-op without namespaceSelector: {} (Review-E caught this on #2005). Canonical write-up of the memory-request change lives in dashecorp/rig-gitops → docs/2026-06-18-single-node-cluster-resilience.md (amended by #725).
The 2026-09-25/26 incidents — two nodes overloaded by the heaviest agent¶
Control plane, 2026-09-25 from ~19:52Z. The dashecorp Dev-E pod landed on the control-plane node by chance: no agent had node affinity, and its local-path PVC dashecorp-shared-claude had bound there. It ran a full vitest/workerd suite (~1.07 GB cgroup) next to k3s-server's SQLite, Postgres and the conductor, and the boot disk saturated: I/O PSI ~90%, load 20 on 2 CPUs. That caused 67 Postgres read timeouts an hour, a conductor liveness restart, ~25% failed heartbeats, k3s API timeouts and Flux/KEDA crash-loops. Full event-table scans in four pollers made it worse (rig-conductor#2476).
Worker-2, 2026-09-26 from 12:10Z. After the fix below moved the dashecorp agents to invotek-k3s-worker-2, that node (then e2-medium, 3.8 GiB allocatable) ran five agent pods, each requesting 640Mi with a 2Gi limit. Dev-E alone peaked at 1516Mi. The node livelocked on memory: kubelet and sshd dead, the VM still RUNNING. Both invotek Review-E pods were stuck Terminating on it, so no PR could be reviewed or merged.
| Change | What it did |
|---|---|
rig-gitops#865 |
Dashecorp agents get required anti-control-plane affinity (a Flux postRenderers patch, because the chart has no affinity value) and a new claim, dashecorp-shared-claude-v2, which bound on a worker. The v1 claim is kept, so Flux does not prune it. |
rig-gitops#867 |
The v2 permissions Job gets an imagePullSecrets it lacked. v1's copy had been Failed for 88 days for the same reason. |
infra#461 |
Workers e2-medium → e2-standard-2 (8 GB), +$53.86/month for both. allow_stopping_for_update made the CI apply a stop/resize/start, which power-cycled the hung worker-2 through IaC instead of a manual reset. |
rig-gitops#869 |
The dashecorp Dev-E requests 1536Mi, its measured peak, instead of 640Mi. |
rig-conductor#2481/#2489/#2495 |
The conductor builds a (type, seq_id) index on mt_events per tenant with CREATE INDEX CONCURRENTLY (invotek took 17.7 s). There were 0 Postgres read timeouts in the first 70 minutes after it went live. |
Still open:
- The other agent fleets still request 640Mi. Review-E peaks at ~860Mi.
- There is no external node-liveness alert (rig-cockpit-worker#77).
- Three Cloudflare modules fail every infra apply on a metadata-only diff (infra#463).
The anti-control-plane rule for apps/dev-e and apps/review-e, listed here before, landed in rig-gitops#880: all 10 base HelmReleases in apps/dev-e, apps/review-e and apps/rig-planner carry it (checked 2026-10-02).
Single-node-era resilience (historical, 2026-06; tolerations still in force)¶
Written when the workers were Spot-preempted and everything ran on the control node. That premise ended 2026-07-02, but the
tolerationSeconds: 3600mitigation and the boot-disk hygiene below remain in force and still matter — Postgres and Valkey still live on the control node's local-path boot disk.
Everything used to run on the one on-demand e2-standard-2 control node (the two spot workers stay preempted), with Postgres + Valkey on its local-path boot disk. That makes the node I/O-bound: a burst of disk I/O (a ~1.3 GB agent image-roll, even a backup pg_dump) can saturate the boot disk, stall the kubelet heartbeat → NodeNotReady → the TaintManager mass-evicts every pod (a full cascade outage hit 2026-06-18).
Eviction mitigation (complete 2026-06-18): long not-ready/unreachable NoExecute tolerations (tolerationSeconds: 3600) now ride every GitOps-managed pod through a transient NotReady instead of evicting it — data plane (rc#1842), agent fleet (rig-agent-runtime#633), Flux + KEDA controllers (rig-gitops#640/#642), Valkey (rig-gitops#645). Only k3s-managed coredns/metrics-server keep the default 300 s. Canonical write-up: dashecorp/rig-gitops → docs/2026-06-18-single-node-cluster-resilience.md.
Boot-disk hygiene: the image-prune CronJob (kube-system, 04:17 UTC; since 2026-10-14 a DaemonSet that prunes every node daily in the 04:xx UTC hour, rig-gitops#985) was fixed so it actually reclaims the ~27 GB containerd image store (rig-gitops#638); the rig-conductor Postgres backup runs daily 02:00 UTC (rig-conductor#1846/#1849/#1852).
Backups: running, more complete since 2026-09-23, and still never restored¶
The daily 02:00 dump succeeds and has for months. Measured on 2026-09-23, four separate things were wrong with what that success meant — read this before trusting "there are backups":
| finding | state |
|---|---|
Nothing had ever restored a dump. The restore-test CronJob was written in June, declared shipped in rc#1781, and was never in the cluster — it sat only in the unused deploy/k8s/kustomization.yaml while Flux applies deploy/. It also still carried the bookworm-pgdg apt repo that rc#1848 fixed in its sibling, so its first run would have died at setup |
rc#2310 fixed. Now deployed as rig-conductor-restore-test (0 4 * * 1) but suspend: true — it needs one manual proving run before it means anything: kubectl -n rig-conductor create job --from=cronjob/rig-conductor-restore-test restore-test-manual-1, read the log for ok tenant=… mt_events=<n>, then unsuspend |
It dumped 5 of 7 non-system databases. rig_control — the tenant-to-database map the job reads to decide what to dump — was never dumped, so a restore gave per-tenant objects with no record of whose they were |
rc#2316 fixed (rc#2318). rig_control is dumped first, and the job now enumerates pg_database and names anything unaccounted. A control-dump failure alerts and fails the run but no longer skips the tenant dumps (rc#2322). First control dump: backups/_control/2026-09-23/control.dump, 40,760 B, 13:56:22Z |
invotek's memory DB has never been dumped, once. mem is derived by the convention rig_t_<id>_mem, and the primary tenant predates TenantProvisioner (its event DB is still rig_conductor), so that name has never existed. The run still printed "all tenants ok (evt + mem)" |
reported per-tenant now; resolving where invotek's memory actually lives is still open |
No retry could succeed. The backup SA is roles/storage.objectCreator (create, not delete), so a second attempt 403'd on everything the first had uploaded (a GCS overwrite is a delete plus a create), and backoffLimit: 2 was decorative |
rc#2325 shipped create-only uploads (ifGenerationMatch=0). A 412 keeps the existing object. A 403 falls back once to the exact old request, so a retry is never worse than before. Whether GCS returns 412 before the IAM 403 for an objectCreator-only SA is unproven, and rc#2320 stays OPEN until a real backup log shows it. Until then, never re-run the backup on a day it already ran |
Every third-party image that ran on a floating tag in the GCP rig's manifests is now digest-pinned
(2026-09-23). CI keeps only the rig-gitops half of them that way. cloudflare/cloudflared (the public tunnel),
envoyproxy/envoy, nginx (cost-dashboard), the alpine:3.20/3.21 CronJobs and alpine/git (rig-gitops#822);
google/cloud-sdk:slim in the backup and restore-test CronJobs (rc#2330; this tag had already moved Debian under
the backup once, which is rc#1848); pgvector/pgvector:pg16 for postgres-0 (rc#2332, PostgreSQL 16.13).
rig-gitops image-pin-check.yml (rig-gitops#826) fails any image: in apps/ or clusters/ that is not a
digest, one of our own sha- builds or a full X.Y.Z tag. Only three Dell-only images are allowlisted
(rig-gitops#825). The rig-conductor repo's deploy/ has no such gate (rc#2339): Flux applies it from that
repo, so the google/cloud-sdk and postgres-0 pgvector pins would survive a PR only by review. That is not "every image is digest-pinned". The Flux controllers and coredns/coredns:1.11.3
run on upstream semver release tags, which the gate accepts by policy, so they are immutable by convention only.
And the gate cannot see an image that is not written in the repo: KEDA (2.16.*) and Weave GitOps (4.*)
render theirs from chart defaults and set no image:, and the k3s-bundled rancher/* images are not in the
manifests at all. KEDA and the rancher/* images run on upstream release tags too. A pin makes an image
immutable. It does not review it (see approved-tools.md).
Valkey is upstream docker.io/valkey/valkey:9.0.6, pinned by index digest sha256:63346cb2… (the same on
ghcr.io/valkey-io/valkey). It is a plain StatefulSet valkey-upstream in rig-conductor, and clients reach it as
valkey-primary (and valkey-external NodePort 30079). It replaced Bitnami 9.0.3 on 2026-09-23 at 18:34Z
(rig-gitops#819): first a replica (#834), then a prep step that keeps the Service across helm uninstall and holds
the replica until Bitnami stops (#836), then the cutover (#837). The client gap was about 10 s. Runbook:
dashecorp/rig-gitops → docs/2026-09-23-valkey-switchover.md. Auth is off. RESTORE, FLUSHDB and
FLUSHALL are disabled in its valkey.conf (upstream fixed CVE-2026-25243 in 9.0.4 and CVE-2026-63639 in 9.0.5;
the security review still requires the command off). Data lives on PVC data-valkey-upstream-0 (local-path, 2Gi).
A few things are left over (rig-gitops#838):
- Bitnami's PVC
valkey-data-valkey-primary-0is kept as the rollback until 2026-09-30. - Suspended staging still renders the Bitnami chart.
- Never roll back to
bitnamilegacy/valkey:8.0.2. It would downgrade onto a 9.x AOF.
The pin gate does not stop this digest being deleted on its own: a bare :9.0.6 passes as a full release tag
(rig-gitops#839).
Durable fix (operator-gated): move Postgres off the boot disk onto an SSD pd-balanced PVC — dashecorp/rig-conductor → docs/2026-06-11-postgres-pd-balanced-migration.md (needs the #admin-approved GCE PD CSI driver + the node-SA IAM grant in dashecorp/infra BOOTSTRAP step 20). Tolerations only bound the blast radius; they do not stop the node going NotReady. Open conductor finding: rig-conductor#1854 (agent heartbeats event-sourced → unbounded mt_events growth).
Worker nodes — Spot preemption → Standard migration — RESOLVED 2026-07-02¶
Resolved:
infra#385merged 2026-07-02; both workers are Standard and Running (verified 2026-09-08), ande2-standard-2sinceinfra#461(2026-09-26). The stigjohnny fleet is stillPending, but on the missingstigjohnny-shared-claudePVC (parked), not on worker capacity. Thegcp/dashecorpapply on main is still RED, for an unrelated pre-existing KMS permission issue (infra#440). The original text is kept below as history.
The two workers (invotek-k3s-worker-1/2) are Spot e2-medium VMs, so GCP preempts them and they sit TERMINATED (last preempted 2026-06-10). While they're down, anything that can't fit on the single control node stays Pending — e.g. the per-tenant stigjohnny agent fleet (dev-e/review-e/planner StatefulSets) has been Pending since the workers died. The preempted VMs also leave orphan NotReady node objects (Kubelet stopped posting node status) that must be deleted as part of the fix. The shared dev-e/review-e/rig-planner fleets run fine on the control node; only workloads needing worker capacity are affected.
Durable fix — infra#382 / #385 (DRAFT, operator-gated): switch the workers to Standard (non-preemptible) VMs that self-join via a k3s node-token fetched from GCP Secret Manager by the instance SA (no token in TF state). The CI WIF SA has no Secret Manager role, so the secret + IAM are operator-owned: rotate the node-token, gcloud secrets create k3s-node-token + add the version + grant secretAccessor to the compute SA, delete the two terminated VMs, then mark infra#385 ready → apply creates the Standard workers. Runbook: dashecorp/infra → docs/2026-06-27-k3s-worker-secret-manager.md. Until then gcp/dashecorp apply on main is RED (benign — only the worker resources fail; all other infra applies are green).
ARC Runners — RETIRED¶
ARC k3s runners were retired 2026-05-20 (operator-confirmed). The arc-systems and arc-runners namespaces, the 25 HelmReleases formerly in dashecorp/runners/, and the arc-linux-{repo} / arc-linux-docker labels are no longer in use. All Linux CI moved to GitHub-hosted ubuntu-latest.
Access¶
# kubectl via kubeconfig
export KUBECONFIG=~/.kube/k3s-gcp-config
kubectl get pods -A
# SSH — BOTH flags are mandatory (see the note below)
gcloud compute ssh invotek-k3s \
--account stig@dashecorp.com --project invotek-github-infra --zone europe-north1-b
# ^ RESOLVED 2026-09-21 (claude-3#901). The 2026-09-10 failure
# ("Required 'compute.instances.get' permission for projects/invotek-github-infra",
# which blocked every cockpit change for hours during a Review-E stall —
# dashecorp/rig-conductor#2072) was a WRONG-ACCOUNT error wearing a lost-access
# costume. The project name is NOT stale. Listing both accounts directly:
# post@nutrieapp.com -> includes invotek-github-infra (1090472687120) YES
# invotekas@gmail.com -> sunlit-mantra-476909-r4, and nothing else NO
# invotekas@gmail.com is the Invotek OWNER account (Play Store owner actions,
# Cloudflare + Tailscale login, Cloudflare Access, GitHub enterprise billing) and
# has never had GCP compute access. stig@dashecorp.com is the rig account (post@nutrieapp.com until
# 2026-09-29; that domain was to lapse 2026-10-05 (claude-3#975) but auto-renewed to 2027-10-05, refund
# requested); it is
# also what every `gcloud compute ssh --command=` line in gpu-brain.md uses.
# The trap: the only gcloud config is `default` -> sjs@veriton.io / verirton-dev,
# so an unflagged command runs as the wrong identity and fails as above. Pass
# --account AND --project every time. Do NOT `gcloud config set account` — that
# would disturb unrelated Veriton work on the same machine.
# Still true: the local kubeconfig holds only unrelated clusters, so reach the
# cluster over SSH (or `--command=...`) rather than expecting kubectl to work here.
# Not declared anywhere: human IAM on invotek-github-infra has no
# google_project_iam_member/_binding in dashecorp/infra, so there is no
# authoritative record of who can reach the rig. That is why this took an outage
# plus eleven days to answer.
# FluxCD status
flux get kustomizations
flux get helmreleases -A
Why agents have no read access to the rig cluster (2026-10-04). The only route is gcloud compute ssh plus sudo k3s kubectl, which is root on the control plane, and the auto-mode classifier blocks it as a "Production Read". The MacBook also has no rig kubeconfig context and no Tailscale. So reading the cluster, for example the logs behind rig-conductor#2639, is the operator's job.
Proposed fix, NOT approved: a GET-only "rig status" endpoint on rig-cockpit-worker for the agent identity (agents online, stream pending). The operator hasn't answered, so don't build it without their OK.
Dell k3s¶
Its current state, the 2026-10-02 cleanup and the operator's plan are in Dell k3s cluster (Ålesund) above. vCluster Platform and the starc leftovers listed here before were removed that day.