Changelog archive — v1.19
Moved verbatim from CHANGELOG.md; never edited.
[v1.19.1] - 2026-08-22
Features
-
ArgoCD Deploytakes apruneinput, so clearing orphaned resources is reachable through the approval-gated route instead of only from gilbert. Apps sync withprune: false, so the thirteen whose manifests use a kustomizeconfigMapGeneratorstrand their previous hash-suffixed ConfigMap on every content edit; ArgoCD counts the orphan as pending-prune and the app readsOutOfSyncforever, trippingArgoCDAppOutOfSyncon a merge that deployed exactly as intended.argocd app sync --pruneis the documented fix and neither automated sync nor the gated workflow could run it.prunedefaults to false, is refused withoutsync=truerather than silently no-op’ing, and logs an--dry-runpreview of what it is about to delete before deleting it — this run log is the audit record. Thewarrant-policy.yamlentry lands in the same change, per the rule that a capability and its boundary get reviewed together.Also adds
indriandprivto.github/actionlint.yaml. actionlint errors on an unrecognisedruns-onlabel and that error is per-file, so their absence made 10 of the repo’s 12 workflows unlintable; all 12 now pass. -
Warrant 0.4.0 can retire an obsoleted request.
POST /api/requests/{id}/supersedemarks a pending requestsupersededand names its replacement, andmise run request-run … --supersedes <id>drives the whole loop: file the new request, retire the old one, note it on the old PR comment, close its heph tracking task. Before this, a PR that took review feedback left two near-identical requests in the queue with nothing to say which was live (warrant 21/22 on PR #525), and the only recourse was prose in the new request’s--why.The route only ever reduces (invariant 4):
supersededis notpending, so no warrant can be minted and nothing can be dispatched, and it is scoped to the caller’s own undecided requests — an agent cannot retire another identity’s request or undo a human’s decision. -
Deploy Talos at talos.ops.eblu.me — first-party agent workflow service (pi runtime + OpenRouter, per-session cost tracking, voice dictation) behind Authentik SSO with the agent-ws-parity access model. Includes the
forge-apimise task (authenticated Forgejo API calls from gilbert). -
talos now trusts the shared
agents-m2mmachine identity as a second bearer issuer (companion talos#22), so scripts, services, and non-talos agent sessions can drive the talos API — e.g. create scheduled cron jobs (talos#21) — without a browser login. Warrant + human approval still gates every privileged action. Credential:agents-m2m-app-password(blumeops vault); no wrapper task, just the API. -
Talos v0.2.0 reaches agent-ws workspace equivalence: pod-start bootstrap with the agents bot git identity (HTTPS+token through the tag:agent SOCKS sidecar), the shared repos.json clone pool, and the heph + tea CLIs baked into the image.
Bug Fixes
-
Clear out the last references to the Homebrew-era forgejo tree, which the brew→source migration left behind in three places. The husk at
/opt/homebrew/var/forgejostill exists on indri, frozen at 2026-04-06, so each of these read something plausible rather than erroring — the reason none of them were noisy.mirror-update-patsqueried the husk’sforgejo.dbfor the mirror list and resolved bare repos under the husk’sforgejo-repositories. Against a four-month-old snapshot it would skip any mirror added since, and rewrite remotes on paths that are no longer the live ones. It now reads~/forgejo/data, matchingforgejo_work_path.- The alloy role tailed
/opt/homebrew/var/log/forgejo.logfor theforgejoservice. The forge writes~/Library/Logs/mcquack.forgejo.{out,err}.logas a LaunchAgent, so the forge’s own log has never reached Loki since the brew exit. Moved toalloy_mcquack_logswith both streams. - The backup policy doc still listed the husk as the critical forgejo source
directory; borgmatic has backed up
~/forgejoplus a WAL-safeforgejo.dbdump since the migration.
mirror-update-patsalso grows a preflight check on the database path. It swallowed sqlite3’s stderr and treated an empty result as “No GitHub mirrors found”, so a wrong path exited 0 with a success-shaped message; since the task is driven by the forge-ci-github-pat rotation, that surfaces as mirrors quietly going stale after a rotation rather than as an error during it. The query is now-readonly, appropriate for a database forgejo is serving live. -
Fixed
mise run agent-metricsandmise run agent-healthcrashing withRuntimeError: Cannot open a client instance more than onceeverywhere the direct network path works — which is to say, on gilbert, the primary place a human runs them.client_for()probed connectivity by sending a request on the very client it returned, and httpx refuses to enter a client that has already sent one; the probe is now a throwaway request. The bug was invisible from agent pods because their direct tailnet path always fails, taking the fallback branch that builds a fresh client — the scripts were only ever verified from the environment that couldn’t hit the bug. -
mise run request-run’s ntfy approval notification now goes through the*.ops.eblu.meclient with the tailscale sidecar SOCKS fallback, so the notification actually reaches the tailnet when the task runs from an agent pod. -
mise run request-runtakes--repo <owner/name>for the repo that holds the PR the request attaches to (default blumeops): the request comment, the heph task title, and Warrant’s queue and approve page now link that PR instead of a same-numbered, unrelated blumeops PR. Warrant storespr_repo(0.4.1) and links the PR’s diff in its own repo. -
Retire
minikube.prom, whichTextfileStalewas still evaluating on indri two months after the minikube cluster itself was retired. node_exporter exportsnode_textfile_mtime_secondsfor every.promfile it finds, so a collector’s series outlives the service it measured — and withnoDataState: Alerting, a retired thing ends up positioned to page about its own absence.The alloy role grows an
alloy_retired_collectorslist: for each entry it removes the.promfile and unloads/removes anymcquack.eblume.*<name>*.pliststill writing it — plist first, since removing the file while a writer is loaded just gets it rewritten. Declaring the tombstone is the point; a handrmon indri doesn’t survive whichever of the two is still live.The runbook grows the matching instruction, since “the alert names a file that isn’t in this table” is exactly the symptom.
-
talos pod: fix nix builds colliding with the image’s own store paths. The image’s root-owned store paths were unknown to the pod’s fresh store DB, so any substitution whose closure overlapped them failed (nix tried to delete-and-replace the incumbent; root ownership refused). The image now bakes closureInfo’s registration dump of the image closure, which the entrypoint loads, gcrooting the toolchain; registered paths are reused instead of re-downloaded. Also create
/nix/var/nixin the image — its absence made nix fall back to a chroot store that cannot build. Verified in-pod on v0.2.9: registration +hellobuild end-to-end. -
Every
op readinmise-tasks/now runs withstdin=subprocess.DEVNULLand a 30s timeout, and every caller handlesTimeoutExpired.subprocess.run(..., capture_output=True)redirects stdout and stderr but leaves stdin inherited, soop— which misparses a non-TTY stdin — could block forever on a parent whose stdin was a pipe, with no timeout to break it. Four tasks (branch-cleanup,dns-acme-cleanup,spork-create,container-build-and-release) were missing the stdin redirect outright, andspork-createandcontainer-build-and-releaseturned a nonzero exit into an uncaughtCalledProcessErrortraceback rather than a message saying which secret could not be read.The hang is reproducible without touching a real vault: give a parent process a stdin pipe nobody writes to, put a shell
opthat doesread xon PATH, and the call never returns;stdin=DEVNULLmakes it return immediately. -
provision-ringtailno longer hangs, or half-applies a switch, when activation restarts the network under it. Two independent causes, one per fix:nixos-rebuild switchran as a child of the SSH session it was invoked over, and activation restartssshd,tailscaledand the network stack — so a session teardown mid-activation could kill the switch partway through. It now runs as a transient systemd unit (blumeops-nixos-rebuild) viasystemd-run, outside the session’s lifetime; the play reconnects withwait_for_connection, polls the unit to completion, and fails with the unit’s journal if systemd’sResultis anything butsuccess.Separately,
ansible.cfghad no[ssh_connection]keepalives. When the far end goes away mid-task the TCP connection is not closed, only silent, so ssh waited on it forever and a playbook that had finished its work hung until someone noticed.ServerAliveInterval=15withServerAliveCountMax=20bounds that to about five minutes;ConnectTimeout=30bounds the initial dial, which the keepalives do not cover. This applies to every playbook, indri included. -
An approved SHA now has to reach the run it approves.
warrant-policy.yamlgives every warrant-class action abinds_shanaming the dispatch input that carries the commit, andmise run request-runrefuses a request that omits it, sends a different SHA, or sends a mutable ref likemain. Previously the bound SHA and the dispatch inputs were unrelated fields: warrant #22 approvedbcb2b55, dispatched with noref, built main, and reported success.verify-runsaudits the same property afterwards — comparing the request record against itself, never the run’shead_sha, which is main’s tip at dispatch time — and leaves a mismatched task open instead of closing it. Request comments now link the Warrant queue entry, and a bare#Nin--whyno longer autolinks to an unrelated PR. -
agent-ws now authenticates to Anthropic with a long-lived
claude setup-tokencredential read fromop://agents/claude-oauth-token/token, instead of the interactive OAuth login stored on the PVC. That login’s refresh token carried a hard ~7-day expiry and died silently — remote-control kept reporting✔︎ Connectedand the liveness probe stayed green while every session start failedOAuth session expired and could not be refreshed. Auth now survives PVC loss and rotates without a shell in the pod. -
Remote Control refuses
claude setup-tokencredentials — withCLAUDE_CODE_OAUTH_TOKENexported, claude exits at startup (“Long-lived tokens … are limited to inference-only”), so the v0.16.0 agent-ws pod crash-looped on its startup probe. v0.17.0 drops the export and returns to the PVCclaude auth logincredential; its ~7-day expiry is now managed by a recurring 5-day rotation chore documented in the new rotate-agent-ws-claude-login how-to. -
Stop zot’s Trivy DB downloads from poisoning log shipping. The CVE extension downloads trivy-db (~90MiB) and trivy-java-db (~900MiB) daily with trivy’s progress bar enabled — zot hardcodes
quiet=false, no config knob — and under launchd’s file redirection the carriage-return animation frames of one download pile up into a single newline-less line of up to 3.7MB. Loki rejects any entry over its 256KB limit with a 400 that fails the whole batch, which is howmcquack.zot.err.logshipped zero lines despite being actively written (the zot.err half of the indri log-shipping audit): only 9 of the file’s 37k lines were progress bars, but they wedged alloy’s position tracking and dragged the normal lines down with them.Three layers, in order of the pipeline:
- The zot LaunchAgent now starts via a wrapper (
zot-serve.sh) that passes stderr through an awk filter which splits physical lines on CR and drops the bar frames, keeping any real content that shares the line. stdout is untouched. - A one-time idempotent task truncates the polluted stderr log (its real content is already in Loki; the bar frames are unshippable), which also resets alloy’s saved tail position — parked mid-file at the end of the first giant line — via truncation detection.
- The alloy role gains a
loki.processguard that drops any line over 255KB from any tailed service instead of letting it fail the batch, counted inloki_process_dropped_lines_total{reason="line_too_long"}.
- The zot LaunchAgent now starts via a wrapper (
-
Stop spelling
prune: false/selfHeal: falsein Application manifests: the controller strips explicit-false fields (Go omitempty) on every automated sync, leaving a field-level diff that flapped theappsroot OutOfSync (2026-08-18 ntfy alert; recurring since automated sync landed). -
mirror-update-patsworks again on Forgejo v16, which broke both halves of it.The query half died outright:
mirror.remote_addressis nowencrypted_remote_address, a BLOB, so the task failed withno such column— and then printedNo GitHub mirrors found.and exited 0, the same success-shaped failure the previous fix was meant to end. Discovery now comes from the API (/repos/search, filtered onmirror, readingoriginal_url), which needs no database access and cannot break on an internal schema change.The write half died more quietly. v16 moved the credential off disk into that encrypted column and rewrites each mirror’s git config to a sanitized, password-free URL, so
git remote set-urlno longer sets the secret Forgejo actually uses. There is no API for it —routers/api/v1/api.goexposesmirror-syncand thepush_mirrorsgroup and nothing else, and the repo settings form is the only sanctioned writer.So the task drives Forgejo’s own recovery path.
DecryptOrRecoverRemoteAddresstreats a NULLencrypted_remote_addressas “credentials may be on disk”: it reads the URL from git config, encrypts it into the database, and re-sanitizes the config. Writing the authenticated URL and then clearing the column hands Forgejo the new credential through the door it already opens for mirrors that predate the encrypted column. Each mirror is then synced immediately, rather than left up to 8h in a state indistinguishable from having no credential.A new guard compares the API’s mirror count against
SELECT COUNT(*) FROM mirrorand refuses to proceed if they differ. The API returns only what the token can see — 33 of 35 for a token without full visibility — so without this the task would rotate a subset and report success, leaving the invisible mirrors on the expiring PAT.Two caveats worth knowing. Clearing the column is a write to a live WAL database behind a running Forgejo, done with a busy timeout and immediately followed by a sync. And the recovery path is documented upstream as being for mirrors predating the column, so it may be removed — at which point this breaks again. A pull-mirror credential endpoint is the durable fix and needs asking for upstream.
-
Fix zot registry outage: the Trivy stderr progress-bar filter (awk, line-oriented) buffered the newline-less CR-stream of a DB download indefinitely, wedging zot’s stderr pipe — zot froze at “updating cve-db” before binding HTTP, leaving the registry dark for ~12h. Replaced with an unbuffered byte-stream filter (
tr -u+ line-buffered grep).
Infrastructure
-
The thirteen ArgoCD apps that render config through a kustomize
configMapGeneratornow sync withprune: true. Each content edit produces a new hash-suffixed ConfigMap; with prune off the superseded one was never deleted, so the app readOutOfSyncindefinitely andArgoCDAppOutOfSyncfired on a deploy that had worked exactly as intended.selfHealstays off, and the remaining apps keepprune: false. -
Restored two of the three checks that the Lint workflow’s prek cleanup removed rather than reinstated as-is. A
secret-scanjob now runs gitleaks over the repository’s full git history on every PR (~7s for ~1500 commits) — the removed trufflehog hook’s--since-commit HEADrange had scanned nothing in CI, and on a public repo a historical leak matters as much as a new one. A first full-history scan found six findings, all one false-positive class (nix-built image tags likeauthentik:v2025.10.1-b8bc0bf-nix, whose git-hash segment reads as entropic), now allowlisted by shape in.gitleaks.toml; the history is otherwise clean. Aworkflows-validatejob validates.forgejo/workflows/against the runner’s own schema by invoking indri’s source-builtforgejo-runnerdirectly —validateis a plain subcommand, so the docker/dagger requirement that pushed this check out of prek never applied to CI. That made thevalidate_workflowsdagger function andmise run validate-workflowsfully redundant, and both are retired; the how-to keeps a docker one-liner for off-runner use.ty-checkremains the one capability still missing, tracked separately. -
New
.forgejo/CODEOWNERSrequests review fromeblumeon every PR, so an agent-opened one lands in the “Review requests” filter rather than in no queue at all. It is a notification, not a merge gate, and unlike a workflow it costs no runner job per PR. Same file is going into every agent-ws repo. -
Trade Warrant Bot Drift’s
pull_requesttrigger for apushonmainover the same paths. The job reads collaborator permissions and branch protections, which needsFORGE_ADMIN_TOKEN; fork PRs receive no secrets and every agent PR is a fork PR, so as a PR check it could only reach--skip-if-no-token— which exits 0 and renders as a green tick. That false pass is how PR #506 merged a check that could never have passed. The post-mergepushrun is the first one with a real token, and the weekly schedule remains the standing check; neither can pass by abstaining. -
Homepage: add static tile for the heph PWA (heph.ops.eblu.me) — indri-native, so no Ingress to auto-discover.
-
Closed the observability gap where an indri log stream could stop reaching Loki and nothing would notice — the forge’s own log was absent for two months (fixed in #548) and the audit that found it had no way to notice a recurrence. A new
log-shippingGrafana alert group watches the four streams with guaranteed daily traffic (forgejo, forgejo-runner, zot, tailscaled), one rule per stream ridingnoDataState: Alerting, since a silent Loki stream returns no series rather than a zero; streams that currently write nothing or too sparsely to alert on are deliberately excluded, with the exclusions documented. Host inspection during review surfaced one live defect the new self-metrics are built to diagnose —zot.erris actively written but never reaches Loki — plus actively-written logs missing from the declared tail list entirely (caddy.errat 559MB,heph,devpi,borgmatic-photos.err), both tracked as follow-ups. All four rules go NoData together when alloy itself dies, and the new runbook (runbook-log-stream-silent) leads with that signature. Alloy on indri now also scrapes its own component metrics into Prometheus (loki_source_file_*,loki_write_*), so “is the file being tailed at all” is answerable from an agent pod instead of needing ssh. -
ruffnow lintsmise-tasks/, which it has never covered. The hooks selecttypes: [python], andidentifyderives that tag from the shebang — it reads#!/usr/bin/env -S uv run --scriptas the interpreteruv, so none of the 26 uv-script tasks were ever tagged Python and no Python hook matched them. The directory holding most of this repo’s logic was the least-linted part of it.A third ruff pass selects the directory by path.
mise-tasks/is mixed bash and uv-python and ruff cannot parse the bash, so the split isexclude_types = ["shell"]: bash tasks carry ashelltag from their shebang, uv scripts are tagged onlyexecutable, file, text. Nothing needs updating as tasks are added in either language.Turning it on found 13 issues across 7 tasks — dead imports, f-strings with no placeholders, and one dead store in
docs-previewwherecard_filewas assigned in both branches and never read. All fixed here; only the existence checks that guard the not-found branch ever mattered.No
ruff-formatpass overmise-tasks/, deliberately. The formatter normalizes#COMMENTto# COMMENT, and mise matches its task metadata comments without a space. Running it rewrites every#MISE description=and#MISE alias=line, which silently empties the description column ofmise tasksfor all 26 tasks and breaks task aliases. That takes the[human]prefix with it — the marker AGENTS.md tells agents to check before reaching for a task they cannot run. Formatting is cosmetic; that fence is not. -
The four local prek hooks —
container-version-check,changelog-check,docs-check-links,docs-check-frontmatter— now run their script directly instead of throughmise run, which installed the whole[tools]block first and made the Lint job depend on the GitHub API it has no credential for. -
The prek hook set now runs on every PR, as a Lint workflow alongside Docs Checks. Until now the hooks ran nowhere: no workflow invoked prek, and no git hooks are installed in an agent pod, so enforcement was whoever remembered to run
prek run --all-fileson gilbert. That is not a theoretical gap — it is how five Python files drifted out ofruff-formatshape unnoticed, and how ruff came to have never once looked atmise-tasks/, the directory holding most of this repo’s logic.The runner needed nothing added.
prekandactionlintare already inforgejo_runner_host_toolsat revs mirroringprek.toml, anduvalready runs the extensionless mise-tasks. The toolchain was provisioned for exactly this and then never wired up.actionlint-systemtherefore runs here for the first time. A*-systemhook runs whatever is on PATH, and an absent binary does not skip — prek reports the hook FAILED — which is why blumeops’ actionlint hook had never caught a workflow error in either state. The workflows are clean today, verified against actionlint 1.7.12.Three hooks are removed rather than skipped, because CI is now the enforcement point and a hook that cannot run in a clean checkout is one nobody is running:
ty-check—[tool.ty.environment]points atsdk/src, the dagger-generated SDK./sdk/is gitignored, so ty aborts in any fresh checkout, CI or local.validate-workflows— shells todagger call, needing Docker Desktop and an engine container. Still available asmise run validate-workflows.trufflehog— pinned to--since-commit HEAD, an incremental range that scans nothing on a fresh checkout.
Removing beats skipping: a skip list is a second place to drift, and it lets
prek.tomlkeep accumulating hooks that only appear to run. What is left is 25 hooks that all pass, so the job’s result is the whole of what the hook set checks.The job blanks
GITHUB_TOKEN. Forgejo Actions sets it to the forge job token, and mise honours that name forapi.github.com— so it offers a Forgejo credential to GitHub and takes a 401 on any tool it has to resolve, which fails every hook whose entry ismise run. This is the per-repo workaround; the class fix is a scopelessMISE_GITHUB_TOKENin the runner’senvs.Two of these are real capabilities and their loss is not free. Secret scanning is now the builtin
detect-private-keyalone, which catches private keys and nothing else — on a repo whose first rule is that it is public. Workflow schema validation is likewise gone, though actionlint covers much of the same ground. Both are tracked for a CI-shaped rebuild rather than reinstatement as-is. -
Share
eblume/project-templatewith the agents bot —write+canonicalincontainers/agent-ws/repos.json, so it lands in the pod’s checkout pool like any other worked-on repo. Three open template tasks (renamebuild.yaml, pin the mise versions, reconcile theprekstep) all stalled at the same line in their notes: “NOT checked out under ~/code/personal — a later session must clone it to edit.” The repo is public, so it was always readable; what was missing was push, and a checkout to notice it from.Also corrects the bootstrap how-to, which still told a human to grant repo access by hand in the Forgejo web UI and listed a repo set predating
repos.json. That instruction is now actively wrong: the reconcile is authoritative in both directions, so a hand-clicked grant is reverted on the next run. -
Move
repos.jsonfromcontainers/agent-ws/to the repo root. The file has outgrown agent-ws: it defines both theagentsbot’s forge access policy and the workspace checkout pool, and the pool is now consumed by two images (agent-ws and talos — the latter already read it cross-directory,../agent-ws/repos.json). Root placement reflects that it is org-level agent policy, not container config, and survives agent-ws’ eventual retirement. Pure source-tree refactor: both containerdefault.nixfiles bake it at build time (readFile), the reconciler mise task, the workflow path filters, and doc references are updated to match. Nothing reads it at runtime. -
Add
mise-tasks/_require, a guard that refuses to start a task whose tools the machine does not have, and wire it into the four tasks whose blocker really is a missing binary:validate-workflows,frigate-export-modelanddocs-preview(docker, for Dagger’s engine) andservices-check,ensure-k3s-ringtail-kubectl-config(kubectl). It names what is missing, that you are in the agent pod, and what to do instead — a[human]description tag is invisible at the moment of failure and does nothing about a task that runs, half-works and exits 0.Also marks
[human]on eleven tasks that cannot run from the pod but were not labelled, and corrects that label’s definition: it means “needs something the pod does not have”, of which the blumeops vault is only one case — a missing tool or an ssh route to indri/ringtail are the others. -
Retire agent-ws. Remove the agent-ws image (containers/agent-ws), its k8s manifests and ArgoCD app, its service-versions entry, and the claude-login rotation runbook; talos supersedes it. The shared tag:agent Tailscale auth key is renamed to generic agent naming (Pulumi
agent_authkey, 1Password item ‘agent Tailscale Auth Key’,mise run agent-authkey-sync). -
Install
actionlintandstyluaon the Forgejo runner. prek downloads the environment for most hooks, but a*-systemhook runs whatever is onPATHby definition — and a missing binary is reported as a failed hook, not a skipped one. Two consequences had been sitting unnoticed:- blumeops’ own
actionlint-systemhook has never linted a workflow. It would have been failing anyway:.github/actionlint.yamlwas missing theindriandprivlabels, which made 10 of 12 workflows unlintable (fixed separately). hephaestus.nvim’s CI has itsprek run --all-filesstep stubbed out with anecho, blamed on a runner without prek. prek has been installed for a while; what actually still blocked the restore wasstyluaandactionlint.
The runner reference card also listed a toolchain that was never accurate — it advertised a
jqthe role does not install. It now points atforgejo_runner_host_toolsas the source of truth rather than restating it.Realign
flyctl0.4.59 → 0.4.71, which had drifted from themise.tomlpin since 2026-07-19. Every version inforgejo_runner_host_toolsmirrors a pin held elsewhere —dagger.json,mise.toml,service-versions.yaml, theprek.tomlhook revs — and nothing bumps the mirror when the source moves, so it goes stale silently. The variable now documents which source each entry tracks, and theforgejo-runnerentry inservice-versions.yamlsays to diff the list against those sources as part of the review. - blumeops’ own
-
The indri runner now injects
MISE_GITHUB_TOKENinto every job’s environment viarunner.envs, sourced at provision time from theforge-ci-github-pat1Password field. Forgejo setsGITHUB_TOKENin every job to the forge job token, and mise honours that name forapi.github.com— so any GitHub-backed tool resolution took a 401.MISE_GITHUB_TOKENoutranks it in mise’s lookup order (first non-empty ofMISE_GITHUB_TOKEN,GITHUB_API_TOKEN,GITHUB_TOKENwins), fixing the class for every repo the runner builds with no workflow changes.The token is the same zero-permission public-read PAT the mirror sync uses — readable by every job on the runner, which is exactly why it must never grow a scope. Takes effect at the next
provision-indri; after that, hephaestus.nvim’s per-stepGITHUB_TOKEN: ""workaround can come out (blumeops’ own Lint keeps its blank deliberately — that job is credential-free by design and no longer touches the GitHub API at all).heph: 01KZKP8893WM8DQT5N97QF6PBD (design), 01KZP94PSDXAMV3GNN2WE51HGA (credential decision).
-
Add an hourly
skagit-cce-watchuser timer on ringtail that polls the Skagit CCE course-catalog category and files a red heph task (under the new Ceramics project) the first time a new ceramics class is listed, keyed on the catalog’s Item Number. State lives in~/.local/state/skagit-cce-watch/; first run baselines silently and a zero-course parse fails loudly without touching state.On a new ceramics class it also fires a best-effort ntfy push to
ntfy.ops.eblu.me/ceramics(tapping opens the course page) so the alert reaches Erich’s phone; a push failure never fails the tick. -
containers/talos/default.nixwas bumped to 0.4.0 without the matchingservice-versions.yamlentry, which madecontainer-version-checkfail on every push. Sync the recorded current version to v0.4.0. -
Bake an eval-only nix into the talos image (agent-ws precedent):
$HOME-relocated store on the PVC (NIX_{STORE,STATE,LOG,CONF}_DIRin the image config),max-jobs = 0nix.conf, size-swept by the entrypoint. Lets the talos pod computesrcHashvalues for its own image bumps vianix-prefetch-gitinstead of guessing and burning warrant-approved Build Container rounds on hash-mismatch errors — the pod that needed this was caught computing a wrong hash by hand during the v0.2.4 release. Version bump 0.2.3 → 0.2.4 for the toolchain change. -
The talos pod’s nix can now build, not just evaluate: the image’s top layer chowns the canonical
/nix/storeto the container user, so store-path hashes stay canonical andcache.nixos.orgsubstitutes. Builds run unsandboxed (seccomp forbids user namespaces) inside the already-fenced pod, capped bymax-jobs = 2and a newephemeral-storagelimit on the Deployment.docs/explanation/agent-containerization.md§“Nix in the pod” records the eval-only phase this supersedes. -
talos image v0.2.3: bake the
mise runtoolchain (mise, uv, python3, gnutar/gzip, which), a/tmp, and/usr/bin/envinto the pod, and wrapteato route through the tag:agent SOCKS sidecar. Filing a warrant request or opening a PR from the pod previously required hand-installinguv, hand-exporting proxy env fortea, and working around the missing/tmp; the uv-managed CPython it fell back to cannot execute in the non-FHS image, sopython3now rides onPATHinstead. -
The talos pod now enforces the lint gate locally instead of relying on agents to remember
mise run agent-lint(#582): the entrypoint installs prek git hooks into every pool clone that carries aprek.toml(blumeops today).pre-commitruns prek over the staged changes;pre-pushruns the exact CI hook set over all files, skipping prettier (a node hook the pod doesn’t ship). The hooks land in the pool clone’s common git dir, so every session worktree inherits them without any agent action, and the entrypoint reinstalls them idempotently at every pod start. prek resolves throughmise execat run time — nothing new is baked into the image — andgit commit/push --no-verifyremains the escape hatch. Agent PR checks sit pending until a human approval click, so catching lint failures at push time instead of in CI saves a review round; this complements #582’s manual task, which stays as the on-demand form of the same gate. -
Add
talosto the agents repo pool (access: write,pool: canonical), granting theagentsbot collaborator write oneblume/talosand checking the repo out in agent workspaces. Talos is entering active agent-driven development (web-UI features land as branch + PR like the other canonical repos); without a pool entry the reconciler denies the bot access, so pushes from the talos pod are rejected. -
talos pod: wire the base pi subagent assets into
~/.pi/agent/at pod start — symlinkagentsandextensionsfrom the agents repo’s newpi/directory (pool checkout stays the source of truth), ship apiCLI wrapper at~/.pi/agent/bin/pi, and exportPI_BINfor the extension’s child processes. This is what lets talos sessions delegate bounded work to cheaper models (eblume/agents#13, eblume/talos#8). Image bumped to v0.2.9, pinning talos main at 65d9727 (includes the subagent tools allowlist). Toolchain gainsdiffutils,gawkandhostname. -
The talos pod
teawrapper now auto-assignseblumeontea pr create(unless the caller specifies assignees), so agent-opened PRs surface in the assigned-to-me queue. Chosen over a per-repo Forgejo Actions workflow (eblume/hephaestus#43, closed). Takes effect on the next image rebuild + deploy. -
CI secrets migrated to job-time
op readofblumeops-civault items (blumeops-ci-item-migration):argocd-deploy,build-container,deploy-fly,build-blumeopsandcv-deploynow fetch their credentials withBLUMEOPS_CI_OP_TOKENat runtime instead of per-secret Actions secrets,_1password-clijoins both ringtail runner sandboxes, and theforgejo_actions_secretsrole stops syncing the migrated secrets (talos and horkos release CI getBLUMEOPS_CI_OP_TOKENinstead of the raw zot key, per the one-CI-trust-tier decision). -
Made the deploy-fly workflow’s post-deploy health check fatal (with cold-start retries). With
strategy=immediatea machine that fails to boot is a live outage, but the run still reported success — as happened 2026-08-15 when an expiredTS_AUTHKEYcrash-looped the new machine (run 957). Also documented the immediate-strategy downtime window infly/start.shand added a recurring 75-day heph task to rotate the 90-day Tailscale auth key before it expires. -
Horkos (né Warrant) cutover, additive half: ArgoCD Application (manual sync — the approval gate no longer redeploys itself on merge, which also ends the auto-sync/dispatch race),
argocd/manifests/horkos/(namespace, PVC, external-secrets reusing the existing warrant vault items), AuthentikhorkosOIDC client, and thehorkos.ops.eblu.meCaddy route. Warrant keeps running untouched until the decommission PR. -
Horkos cutover, decommission half:
containers/warrant/and the warrant manifests/Application are removed (the service now lives in eblume/horkos),request-run/verify-runspoint athorkos.ops.eblu.me, the AuthentikWarrantprovider and Caddy route retire,warrant-testbecomeshorkos-test(client tooling only — the service suite moved with the service), and the service card is now horkos. Kept on purpose:warrant-policy.yaml,warrant-bot, and theWarrant request: #NPR stamp — the minted artifact is still a warrant. -
Add
horkostorepos.jsonso agent pods get a workspace checkout of it — asaccess: read, pool: fork(authors viaagents/horkos, PRs to canonical), matching blumeops. horkos is pinned into the reconciler’sPINNED_READ_ONLYset: it’s the approval gate itself and its CI carries release secrets, so a write-capable bot could dispatchrelease.yamlfrom a doctored branch and exfiltrate them. Theagents/horkosfork was created alongside. -
repos.json moved to
argocd/manifests/talos/and delivered to the talos pod as a kustomize-generated ConfigMap: the hash-suffixed name changes the pod template on every policy merge, so a repos.json change now deploys itself (merge → ArgoCD sync → pod rolls) instead of waiting for someone to remember a restart. The talos app gainsprune: trueto garbage-collect superseded hashed ConfigMaps; the agent-repo-access reconciler and its workflow triggers follow the file. -
Talos agent pods can now inspect deployment state directly: new
agents-readonlyArgoCD local account (apiKey, get-only on applications/projects — no sync, no logs, no exec, Secret values masked server-side), token in the agents 1Password vault read at runtime via the pod’sopservice account, and a proxiedargocdCLI wrapper in the pod image. The kubectl leg was deliberately skipped: the tag:agent ACL denies the k8s API, and ArgoCD’s resource tree covers health inspection. -
Talos releases are automated: the image derivation moved into the eblume/talos repo (built from its own checkout — no more rev+srcHash pinning here), and every merge to talos main now builds, pushes
vX.Y.Z-<sha>-nix, and opens a pin-bump PR against blumeops.containers/talos/is retired; the repo pool policy (repos.json) is fetched by the pod at start instead of baked into the image. -
Deploy agent-ws 0.18.0 —
project-templatenow lands in the agent workspace’s checkout pool, so sessions get a worktree of it instead of cloning by hand. -
Tooling dependency cycle: bumped mise tool pins (ansible-core, borgmatic, prek, pulumi, dagger, ty, flyctl), the fly proxy’s tailscale image, and warrant-test’s unit-test dependencies to their latest stable releases; synced service-versions.yaml and stamped today’s ansible-core service review. Closes heph 01KT5Q9HFG8TF8TSXDQP36CF3R.
-
Monthly tooling dependency refresh: ruff v0.16.3, prettier v3.9.6, ansible-lint 26.8.0 /
ansible-core2.21.3,actions/checkoutv7.0.1,typer==0.27.1across mise tasks, fly proxy alloy v1.18.1 and anubis v1.27.0. ruff’s lintselectis now pinned inpyproject.toml— 0.16 widened its implicit default from 60 rules to 414, so the enforced set no longer moves with a version bump. -
Forgejo upgraded 15.0.3 → 16.0.2, and
mise run runner-logsmoved onto the Actions job-log REST API that arrived with it. Private-repo CI logs were unreadable outside a browser: the only log route Forgejo 15 has is a web route, and Forgejo does not accept API-token auth on web routes at all — token,Bearer, basic and?token=all leave the request anonymous, which is enough for publicblumeopsand a 404 for every private repo. v16’s/repos/{owner}/{repo}/actions/jobs/{job_id}/logshonours the token, sorunner-logsprefers it, keeps the web route as a pre-upgrade fallback, and now says which of the two failed and why instead of “no log available”. -
Ollama is re-enabled on ringtail (
replicas: 1) andqwen3.8:27bis added to the synced model set, to evaluate local inference as a provider for talos. -
Pin the talos image to
v0.4.1-0444de5-nix(built by warrant run 1173), deploying theteawrapper auto-assign from #598. talos is an auto-sync app, so the argocd sync on merge is the deploy — no explicit argocd-deploy run needed. -
Pin the talos image to
v0.4.2-1b31ea6-nix(built from the #605 bump by a warrant build-container run), deploying the tool/subagent transcript UI from eblume/talos#15. talos is an auto-sync app, so the argocd sync on merge is the deploy — no explicit argocd-deploy run needed. -
Pin the talos image to
v0.4.3-828dee6-nix: archive/rename chat actions, auto session titles, and the stale-client auto-reload fix. -
Pin the talos image to
v0.4.4-b685cfc-nix: programmatic API — non-interactive, token-authenticated runs viaPOST /api/run(eblume/talos#19). -
Release talos image v0.4.1: same upstream talos (rev unchanged) but the baked toolchain gained the
teawrapper auto-assign from #598, which needs a rebuilt image and a deploy to take effect. -
Bump talos image to 0.4.2 (talos
66448e8): the chat UI now shows full tool/subagent transcripts in collapsed blocks, and tool activity survives session reloads (eblume/talos#15). -
Bump talos image to 0.4.3 (talos
56b430a): per-session Archive/Restore and Rename actions in the chat header (eblume/talos#16), auto-generated session titles (eblume/talos#17), and stale-client auto-reload after a deploy — the fix for the broken transcript rendering seen after v0.4.2 (eblume/talos#18). -
Bump talos image to 0.4.4 (talos
eaedc74): programmatic API —POST /api/runfor non-interactive, token-authenticated runs (eblume/talos#19). -
Bump talos image to 0.4.5 (talos
7b2d64c): cron sessions — scheduled headless runs as a third session kind (eblume/talos#21). -
Pin the talos image to v0.2.10-b9c2cb3-nix (build run 1075). The v0.2.10 image registers the image’s own store paths into the pod’s nix DB at startup and creates /nix/var/nix — the two fixes from PR #581. Merge auto-syncs the talos app and restarts the pod. Post-deploy smoke test: nix-build hello must succeed in a fresh pod with no manual setup.
-
Pinned the talos image to
v0.2.11-8fe10b5-nix, built from the #584 merge commit. v0.2.11 makes the lint gate hard: the pod’s entrypoint installs prek pre-commit/pre-push hooks into pool clones carrying aprek.toml, so agent commits and pushes of blumeops run the CI hook set before leaving the pod — no agent action needed,--no-verifyto bypass deliberately.service-versions.yamlalready moved to v0.2.11 in #584 itself, so this pin only moves the kustomization tag; merge auto-syncs talos and restarts the pod. -
Bumped talos to v0.2.12: upstream bump to talos
9dfb7f1(eblume/talos PR #10), which adds structured request logging to the talos server — one[talos]line per login, session create, prompt, tool call, assistant turn, abort, and dictation with the acting user and model, never message content or costs. Transcribe failures, previously invisible, now log the OpenRouter error plus provider detail and surface it in the 502 body. Onlysrc/server.tschanged upstream, sonpmDepsHashis unchanged. -
Pinned the talos image to
v0.2.12-9e31cce-nix, built by warrant run #40 from the #586 merge. v0.2.12 is the structured-logging release of the talos server (eblume/talos PR #10): one[talos]log line per login, session create, prompt, tool call, assistant turn, abort, and dictation, plus real error detail from OpenRouter/provider failures in the transcribe path — the gap that made a dictation failure undiagnosable in Loki.service-versions.yamlalready moved to v0.2.12 in #586, so this pin only moves the kustomization tag; merge auto-syncs talos and restarts the pod. -
Deploy Warrant 0.4.0 — the supersede API, so an obsoleted request stops looking live in the approval queue.
-
Deploy agent-ws
v0.17.0-5418120-nix, the image built from theCLAUDE_CODE_OAUTH_TOKENrevert. The cluster had stayed pinned to the brokenv0.16.0image — which crash-loops, since Remote Control refuses an inference-only token — because the revert changedcontainers/agent-ws/without bumpingkustomization.yaml. -
Upgraded heph to v1.9.0 across the fleet (hub
heph_version+ ringtail spokehephTag): seq-based sync cursors with deferred out-of-order ops and chunked pushes, fixing the HLC-cursor drop and FK-wedge failure classes behind the 2026 pod spoke outages. -
horkos v0.5.1: auto-release from eblume/horkos
8b6b1b1— image pin bumped tov0.5.1-8b6b1b1-nix. -
Pin talos
v0.4.6-c904e38-nix(agents-m2m API bearer auth) in its ArgoCD kustomization. -
Pin warrant
v0.4.1-fbd37ad-nix(request-run--reposupport, pr_repo on requests) and talosv0.4.5-fbd37ad-nix(cron sessions) in their ArgoCD kustomizations. -
Bump the talos image to v0.4.6 (talos#22 — trusts the
agents-m2mmachine identity for bearer auth, so scripts/services can drive the API). -
Talos main model switched from
qwen/qwen3.8-maxtoqwen/qwen3.8-27b(now on OpenRouter): ~4.4x cheaper input, ~2x cheaper output, addressing the ~$5-per-cycle run cost. -
talos v0.4.11: auto-release from eblume/talos
0f6388e— image pin bumped tov0.4.11-0f6388e-nix. -
talos v0.4.7: auto-release from eblume/talos
ed6337f— image pin bumped tov0.4.7-ed6337f-nix. -
talos v0.4.8: auto-release from eblume/talos
ea2791d— image pin bumped tov0.4.8-ea2791d-nix. -
talos v0.4.9: auto-release from eblume/talos
cc88c5e— image pin bumped tov0.4.9-cc88c5e-nix. -
Pin upstream talos 0a95bf1 (v0.2.5): markdown rendering in chat, stop/steer agent controls, dictation-review input, and the dictation status gutter (talos PRs #4–#5). The lockfile gained a direct
markeddep, sonpmDepsHashchanges with this bump.srcHashcomputed in-pod viabuiltins.fetchGit(eval-only nix, agent-containerization). -
talos image v0.2.5 → v0.2.6: pin upstream talos
81011bb(eblume/talos#6 — sessions now start with theagentsrepo’sAGENTS.mdpreloaded into the system prompt via pi’sagentsFilesOverride, restoring the base context agent-ws sessions got from waking up inside theagentscheckout).npmDepsHashunchanged (lockfile untouched);srcHashrecomputed in-pod with the eval-only nix (git archive+nix hash path). -
talos image v0.2.6 → v0.2.7: pin upstream talos
cde7f25(eblume/talos#7 — per-session git worktree isolation: each session works in its own~/sessions/<id>/worktrees of the pool repos instead of sharing the pool checkouts; a startup reaper prunes stale sessions with agent-ws semantics).npmDepsHashunchanged (lockfile untouched);srcHashrecomputed in-pod (git archive+nix hash path). -
talos image v0.2.7 → v0.2.8: pin upstream talos
3995a5a(eblume/talos#9 — fix: sessions whose working directory is a per-session worktree vanished from the sidebar, because the session list filtered on the shared pool path; the filter now matches on the session’s own worktree).npmDepsHashunchanged (lockfile untouched);srcHashrecomputed in-pod (git archive+nix hash path) and verified with a realfetchgiteval. -
talos image v0.2.8 → v0.2.9: subagent base assets wired into the pod (#575 —
agents/pisymlinked into~/.pi/agent/,piCLI wrapper,PI_BINexported) and the pod’s nix upgraded from eval-only to real builds (#579 — canonical/nix/storechowned to the container user,cache.nixos.orgsubstitution,max-jobs = 2, ephemeral-storage cap). Toolchain gainsdiffutils,gawk,hostname. Talos source pinned at65d9727(includes eblume/talos#8, the subagent tools allowlist);npmDepsHashunchanged,srcHashrecomputed in-pod. -
Talos v0.3.0: settings panel for main/subagent model and reasoning-effort selection (eblume/talos#11), persisted on the PVC and applied to live sessions; reasoning effort defaults to low.
-
Talos image pinned to v0.3.0-b28e7ad-nix — the settings-panel release goes live.
-
Talos v0.3.1: SSE stream reliability — server heartbeat + client resync close the “new messages don’t appear until re-entry” gap (talos PR #12), with the reconnect path hardened in review (tracked EventSource, resync on manual reconnect, backoff, stale-resync guard).
-
Pin the talos image to v0.3.1-4bf1f40-nix (SSE stream reliability).
-
Talos v0.3.2: curated model pickers — the settings panel’s primary and subagent selectors are now fixed shortlists (primary: Qwen 3.8 27B / GPT-5 / Qwen 3.8 Max / Claude Opus 5 / Claude Fable 5; subagent: Qwen 3.8 27B / Claude Sonnet 5 / Claude Haiku 4.5 / GPT-5 Mini / Gemini 2.5 Flash), each option showing live $/M pricing (talos PR #13).
-
Bump ollama to 0.32.14 — 0.31.2 returned HTTP 412 pulling qwen3.8:27b (model requires ≥0.32.12).
-
Bump the talos entry in service-versions.yaml to v0.4.0 — the v0.4.0 release PR updated containers/talos/default.nix without it, so container-version-check failed every PR’s lint gate.
-
Deploy hephd v1.8.1 (OIDC refresh-rotation race fix, hephaestus #45) to the indri hub and both ringtail spokes.
-
Scale ollama back to zero replicas — qwen3.8:27b doesn’t fit the RTX 4080 (16GiB VRAM vs ~18GB model), spilling to CPU at ~3 tok/s.
-
Skagit CCE ceramics watch now runs every 5 minutes (was hourly), with jitter and timer accuracy tightened to match.
-
Talos deploy follow-ups: npm-deps fetcher v2 for the container build (v1 cache layout drops nested duplicate entries, breaking offline npm install in CI) and the image tag pin.
-
Talos v0.4.0: dashboard-first UI. The primary view is now a session lifecycle dashboard (filter by running/idle/archived, workflow-kind registry with chat as the first kind, regex transcript search, per-session cost, stop/rename/archive actions, live updates over a global SSE channel); chat moved behind each session’s row. Also picks up the v0.3.2 curated model pickers, which were built but never pinned.
Documentation
- Reviewed the zot container-versioning doc: corrected the sync-check exemption list (kubectl and agent-ws) and clarified that the version assertion guards only some derivations; stamped last-reviewed 2026-08-18. Closes heph 01KT5Q9H78VYR0M0M8P05YZDHB.
- Doc review:
runbook-postgres-unhealthyrewritten against the deployed state — the alert covers two CNPG clusters,pg_isreadypointed at the retiredpg.ops.eblu.me:5432route, Immich was listed under the wrong cluster, and the NoData case was undocumented.PostgresClusterUnhealthy’s summary now names the cluster viainstanceinstead of the repo-wideclusterlabel. - Reviewed the zot harden-zot-registry doc against the role’s current config
template: added the previously omitted
defaultPolicy: ["read"]line of the accessControl policy and stamped last-reviewed 2026-08-21. All other claims (OIDC + API-key auth, external URL, group policies, anonymous metrics scraping, zot-ci key) verified accurate. - Reviewed docs/how-to/zot/register-zot-oidc-client.md: corrected the blueprint entry list (two group policy bindings, zot-ci is a member of artifact-workloads, not a binding target) and stamped last-reviewed.
- The post-upgrade
doctor checkin upgrade-forgejo now runs. It was written ascd ~/forgejo && forgejo doctor …, but Forgejo derives its work path from the binary’s own directory rather than the shell’s, so it looked forapp.iniunder~/code/3rd/forgejoand exited with “Unable to load config file for a installed Forgejo instance”. It needs-wand-cbefore the subcommand, the same two flags the LaunchAgent passes. Found while verifying the v16.0.2 upgrade. - Document why an agent PR’s checks sit
pendinguntil a human clicks Approve and run, and why that click is kept. It is not only friction: it decides which workflow files execute — the target branch’s for an untrusted author, the pull request’s own once that author is trusted — so permanently trusting the agents bot would let an agent PR run its own workflow definitions on theindrirunner before anyone read the diff. Records thatpull_request_targetis the tempting way to skip the click and should not be used here, since it is the one trigger that receives secrets and a write-capable token. - docs/how-to/knowledgebase/review-documentation.md: the “From a Remote-Agent Session” section is updated — remote-agent pods now have a read-only argocd client (agents-readonly account), so the ArgoCD sync/health check can be run live; kubectl/ansible/pulumi still fall back to repo state.
- Correct the agent-ws Claude login rotation cadence from 5 days to 21. The
refresh token’s window was measured at ~29 days, not the ~7 previously
documented — that figure came from mistaking the PVC’s creation date for the
login date, when the credential had actually been carried onto the PVC from the
pre-container host login 22 days earlier. The runbook now reads the real
refreshTokenExpiresAtoff the credential at each rotation instead of trusting the cadence, and records two things learned doing it live: the code paste-back echoes nothing overkubectl exec, and a login performed after expiry needs the container to cycle before Remote Control comes back. - Sweep the last current-tense
minikube-indrireferences out of the docs: troubleshooting, prowler, docs-deploy, tutorials, service reference cards, the infra-health agent brief, and the live 1password-connect bootstrap README now all point atk3s-ringtail(minikube was retired 2026-06; historical runbooks and provenance comments are left as history). - Design doc for Talos, a first-party pi-based agent workflow service: owned session layer (browser-resumable from any tailnet device via Authentik), OpenRouter model freedom, agent-ws-parity access model, and a planned forge-driven issue→PR driver sharing the same runner. Supersedes the Open WebUI approach (PR #562, closed).
Miscellaneous
-
Agent lint gate: new
mise run agent-lintruns the PR prek job’s hook set locally (everything except prettier, which needs node) — agent PR checks sit pending until a human approves the run, so lint failures were discovered late.actionlintjoinsmise.tomlso the actionlint-system hook runs in the pod. Origin: the container-version-check failure on PR #581. -
github-mirror-patis nowforge-ci-github-pat. Mirroring is one consumer of that credential, not the only one — it is indri’s general-purpose credential for reading public GitHub, and CI tool resolution is the next consumer lined up, to stop mise 401ing when it resolves a tool from the GitHub API.Renaming rather than minting a second token, because the capability is already right: it is a fine-grained PAT with no permissions, granting read-only access to public repositories and nothing else. A second token with identical reach would buy only another 20-day rotation chore. The rotation runbook grows a note to keep it scopeless, which matters more once CI jobs can read it.
mise-tasks/mirror-update-patskeeps its name. It updates the PAT on mirror repos, which is still exactly what it does.This requires a matching 1Password field rename, and the two must be sequenced. Add the new field to the Forgejo Secrets item with the same value first, leaving the old one in place; merge this; then delete the old field. Both consumers (
mirror-create,mirror-update-pats) are[human]tasks that only run around a rotation, so the window is forgiving — but additive-then-remove leaves no window at all. -
prek run --all-filesis clean again. Five Python files had drifted out ofruff-formatshape —containers/warrant/app/main.py, two warrant tests and twotests/modules — so the hook rewrote them on every invocation and any run that touched them reported a failure. Formatting only, no behaviour change. Nothing caught it because blumeops CI runs Docs Checks rather than prek, so the hooks are enforced only by whoever happens to run them locally. -
Remove
heph-cli-gilbertfrom service-versions.yaml: gilbert is a dev laptop, deliberately outside IaC and often unreachable, so it doesn’t belong in the automated review queue. The 2026-08-22 spot-check that prompted this found it healthy anyway — heph 1.9.0 (61bf2eea7) matching upstream v1.9.0, spoke fully synced (heph task 01M0K0HC0CYVFRH5MVGADXBY43). Also serves as the changelog fragment for the #635 prek stamp, which shipped without one.
[v1.19.0] - 2026-08-06
Features
-
mise run agent-healthnow names the instance behind a firing or pending alert, not just the rule: the distinguishing labels (file=…,camera_name=…,namespace=…) plus how long it has been in that state. Grafana was already returning them and the task discarded them, so a report said “TextfileStale is pending” when it could have said which textfile — the first question every runbook asks.--jsoncarries the full label set per live instance. -
mise run agent-metrics '<promql>'— ask Prometheus a question from anywhere, without cluster access. Goes through Grafana’s datasource proxy as the sameagents-m2mViewer identityagent-healthuses, so an agent can now verify a fix with data instead of inference: the frigate liveness-probe fix turned out to be a measurable 26x reduction in restarts (5.72/day → 0.22/day), which closed a task that had been open on “verify this held” since June. -
mise run agent-health— fleet health via Grafana alert-rule states, run as theagent-ringtailmachine identity (agents-vault creds only, Viewer access, SOCKS fallback for the pod). Exit 0/2/1 for inactive/pending/ firing — the agent-usable replacement for services-check’s kubectl/ssh legs (heph 01KZ2XGW). -
mise run warrant-bot-driftand a weekly Warrant Bot Drift workflow assert that warrant-bot still holds exactly write oneblume/blumeops, is a collaborator nowhere else, is not a site admin, and is not onmain’s push/merge whitelist. All four live in the forge rather than in this repo, so a change made in the UI leaves no diff for review to catch. Read-only, and an unreadable check reports UNKNOWN and fails rather than passing quietly. -
Grafana gains
[auth.jwt]for theagents-m2mmachine identity (heph 01KXREAB, second half): JWKS-verified tokens asagent-ringtailget Viewer via theagents-sagroups claim,X-JWT-Assertionheader, auto-provisioned on first call. Gives agents a post-deploy observability read path (agent-health follows). -
Ringtail’s heph spoke is now a desktop spoke:
heph-tui(agenda/triage) andheph-quickadd(quick capture) install alongsideheph/hephd, and sway binds Alt+’ to the quick-capture popover from anywhere. The agent’s headless spoke still installs only the daemon and CLI. Shims in eblume’s home-manager profile put all four on the sessionPATH—~/.cargo/binis on none of it, so until now no heph binary (including thehephCLI) was reachable from a terminal opened under sway. Becauseheph-quickaddis a cargo-built GUI,heph-common.nixnow also carries the graphics/input libraries itdlopens at runtime, and the install unit no longer skips a rebuild just becausehephdis already at the pinned tag — it also requires every requested binary to be present.Also installs
teaon ringtail:tea pr createis the documented way to open PRs here, but it was never on this host, so sessions running on ringtail had to fall back to raw Forgejo API calls. -
request-runnow mirrors every privileged-run request into the warrant queue (agents-m2m identity, SOCKS-aware, best-effort) — the broker becomes the single pane of glass while PR comment + heph task remain the system of record. -
Arm Warrant:
WARRANT_DISPATCH_ENABLED=1. Approving a request in warrant now dispatches its workflow aswarrant-bot— the loop from agent request to executed run closes with one human decision. Disarm by setting"0"and syncing. -
mise run warrant-bot-provision— scripted, human-run ceremony for the Warrant dispatch identity: creates thewarrant-botforge user, grants write on blumeops, mints awrite:repositoryPAT, and stores it inblumeops-ci. Deliberately not a workflow — minting privileged credentials from CI would need vault-write in CI and would turn FORGE_ADMIN_TOKEN into an escalation path. -
warrant v0.2.3: the queue links to the code — PR, its diff, the commit, and the workflow definition. Tying each decision to the change it authorizes is the substance of the approve-fatigue answer; richer ergonomics follow later.
-
warrant v0.3.2: an approved warrant records the CI run it caused —
run_number/run_urlcolumns, a run column in the warrants table, and the run URL in the decision API response. Closes the loop visually: approve, then click through to what you started. -
argocd-deploy.yaml— the first approval-gated privileged workflow (warrant-approval-gated-runs Phase 1): a human-dispatchedapp set --revision <sha>+ sync + wait-healthy, with SHA/app validation and env-indirected inputs. Introduces theprivrunner label (advertised by the indri runner until the dedicated Phase-2 runner exists). Makes the “deploy from branch/merge” step agent-requestable viamise run request-run. -
Authentik gains the agent identity tier (warrant-approval-gated-runs P3 prereq, heph 01KXREAB):
agents-sagroup,agent-ringtailservice account, and anagents-m2mclient-credentials OAuth2 provider whose client secret is vault-fed (Authentik (blumeops)→agents-m2m-client-secret), so consumers read the same value from the agents vault. -
warrant v0.1 scaffold — the Phase-3 approval broker’s request queue: FastAPI + SQLite at warrant.ops.eblu.me, agent API authenticated by the
agents-m2mAuthentik JWT, read-only queue UI. Deliberately holds no approval path and no privileged credentials; forge dispatch remains the approval mechanism until v0.2. -
warrant-policy.yaml— the agent-autonomy boundary as a reviewed artifact (warrant-approval-gated-runs Phase 4 seed): per-action classes (warrant/auto/deny, unknown = deny) and input schemas.request-runenforces it from main at request time — app-name typos and denied actions now fail before a human ever sees the request (pilot friction d). -
mise run request-run— agents can now formally request privileged workflow dispatches (warrant-approval-gated-runs Phase 1): structured PR comment + attention-orange heph task (the system of record), optional ntfy push, SHA and definitions-from-main validation. Approval remains a human forge-UI dispatch. See request-a-privileged-run. -
mise run verify-runs— the Phase-2 audit sweep for warrant-approval-gated-runs: matches openApprove:heph tasks to their workflow runs, auto-closes succeeded ones with an audit log line, and flags failures while leaving them open. Closes pilot friction (b). -
Warrant v0.3.0 — approvals can execute: an approved warrant is consumed (single-use, TTL-checked, policy re-checked against main) and its workflow dispatched as
warrant-bot. Ships disabled;WARRANT_DISPATCH_ENABLEDarms it separately. Approving now goes through a confirm page showing the full input set and diff link — the list view can deny, not approve. -
Warrant v0.2a — the human door: Authentik OIDC login for the queue UI (admins-only, TOTP-backed per the recorded decision), signed sessions, and an honest decision endpoint (401/403/501 — authentication without power). Agent requester names become human-readable. The approval flow itself is v0.2b.
-
Warrant v0.2b — decisions and warrants: authenticated approvers approve/deny from the UI (CSRF-guarded forms) or JSON API; approval mints a single-use, TTL’d warrant binding the frozen {action, sha, inputs} (invariants 2 & 5). Still dispatches nothing — execution stays forge-side until v0.2c consumes warrants.
Bug Fixes
-
Recover seven changelog fragments that towncrier would have dropped. Branch names containing a slash produced
docs/changelog.d/agent/*.md, and towncrier skips subdirectories without a word, so entries from PRs #439, #440, #521, #522, #523 and #524 were headed for a release that never mentioned them.mise run changelog-checkhad anticipated exactly this failure since it was written and was wired into nothing; the new Docs Checks workflow now runs it, plus the frontmatter and wiki-link validators, on every PR. -
warrant-bot-driftandagent-repo-accessread a collaborator’s permission level from/collaborators/{who}/permissioninstead of from the collaborator list, whose entries are plain users and never carried a permissions field. Warrant Bot Drift could not pass at all — every real grant read as “not readable” (run 724);agent-repo-accesssaw every existing grant asread, so--checknever reported in sync and an over-privileged grant would have read as under-privileged. -
runner-logsworks from an agent pod: it now targets the public forge host instead of the tailnet name plain httpx cannot reach, prefers theupstreamremote over a fork’soriginwhen detecting the repo, and bounds itsop readwithstdin=DEVNULL, a timeout, and a clean error instead of hanging. Dropped the[human]tag it should never have carried — AGENTS.md points agents at this task for exactly the job it could not do.container-listno longer reports every container as untagged when the registry is unreachable; it says so and exits non-zero, and can be routed through the pod’s SOCKS sidecar withALL_PROXY. -
The ringtail-priv-runner now carries
flyctlin its hostPackages, fixing the first warrant-dispatcheddeploy-flyrun (run 738:flyctl: command not found, exit 127 before any deploy action). The deploy-fly workflow header no longer claims the indri runner advertisespriv— that was stale; the dedicated Phase-2 runner had already landed. -
Fixed
mise run services-check’s “Local services on indri” leg tossh erichblume@indriinstead of baressh indri— the bare form relies on a local-username mapping that only exists on gilbert, so it fails withtailscale: failed to look up local userwhen run from ringtail (indri’s account iserichblume, not the caller’s username). -
ringtail:
WLR_NO_HARDWARE_CURSORS=1now actually reaches the sway session. It had been configured viaprograms.sway.extraSessionCommandssince 2026-02-18 and never applied — the running sway is home-manager’swayland.windowManager.swaybuild, which never executes the NixOS module’s session wrapper. Moved toenvironment.variables, the mechanism theMOZ_ENABLE_WAYLANDworkaround already proved reaches the session. -
TextfileStaleno longer goes pending every hour. Its threshold was 3600s whileborgmatic.promis written by an hourly LaunchAgent — the alert’s threshold and the file’s refresh period were the same number, so ordinary scheduling jitter made it “stale” once per hour, every hour. Raised to 2h; the other five textfiles refresh every 20-50s and lose nothing. Shortening the exporter’s interval was the other option and was rejected: it callsborg infoover SSH to BorgBase, and doubling that collides with the separate connection-count concern. -
verify-runsnow attributes approval tasks to runs by record instead of inference: warrant v0.3.4 serializes each request’srun_number/run_url(captured at dispatch viareturn_run_info) inGET /api/requests,request-runstamps the warrant request id into the heph task, and the sweep closes tasks against exactly that run. The old matcher assumed a dispatched run’shead_shawas the approved payload SHA — it’s actually the main tip at dispatch time, so any request approved after main moved on (deploy-fly request #18, run 739) reported “not dispatched yet” forever. The forge-side heuristic remains only as a marked fallback for UI-dispatched runs. -
warrant v0.3.3: a warrant no longer names a CI run it did not cause. v0.3.2’s
_find_runretried only while the run list was empty, so for any workflow with history the first poll returned the newest pre-existing run — warrants #5 and #7 each asserted a run Erich never authorized. The dispatch now sendsreturn_run_info, so the forge answers 201 naming the run it just created and there is nothing left to infer; a dispatch that comes back without one links the workflow’s run list rather than guessing. Warrants also recorddispatched_at, which makes a bad link detectable after the fact. First unit tests for the service, run withmise run warrant-test. -
role:workflow-botcan read ArgoCD projects. Without itargocd app getwas denied, so the argocd-deploy workflow’s closing “Report application state” step — the run’s audit record — has been empty on every warrant-gated deploy (runs 713, 721, 732), hidden by the step’s|| true. -
The agent-ws image gains
/usr/bin/env, somise run <task>works in the pod — the kernel resolves shebang interpreters literally, and every mise-task script uses#!/usr/bin/env. Tasks needing the blumeops vault are now tagged[human]in their descriptions, and AGENTS.md points agents atagent-healthrather than the kubectl/ssh-boundservices-check. -
The agent-repo-access CI secret is
FORGE_ADMIN_TOKEN, notFORGEJO_ADMIN_TOKEN. Forgejo reserves theFORGEJO_prefix for the Actions tokens it injects itself and rejects creating a secret that uses it, soprovision-indri --tags forgejo_actions_secretsfailed with HTTP 400 on that one entry while the other five synced fine. Thecvrepo’sFORGE_TOKENwas already the same workaround; the reason is now recorded in the role defaults so the name doesn’t get “corrected” back. -
Fix navidrome 0.63.2 crash-loop: the Nix image ships no
/tmp, so SQLite’s largebpm/bit_depthmigration failed withdisk I/O error: permission denied(no writable temp dir for the statement journal). Mount an emptyDir at/tmp(paperless precedent) and switch the deployment toRecreateso the old pod never holds the SQLite DB while a new pod runs startup migrations. -
mise run request-runrejected every one of its own flags, so the invocation printed inAGENTS.mdcould not work.Its
#USAGEblock declared the two positional args and no flags. mise validates argv against that spec before the script runs, so--pr,-i, and--whydied atunexpected word: --pr— and--does not bypass it. The flags existed in the typer signature below, and./mise-tasks/request-run …run directly worked fine, which is presumably why it went unnoticed: the documented path was the broken one. Found while filing the build request for agent-ws v0.14.0, which is the first requestable action an agent has had reason to file from the pod.Declaring the four flags fixes it. Two traps surfaced while doing so, both now written down in the script’s docstring because neither is discoverable:
- The
#USAGEblock must be contiguous. An ordinary comment line between two#USAGElines truncates the spec — every later line is silently dropped, with no warning and no error. The first attempt at this fix put an explanatory comment above the new flags and reproduced the original bug exactly. - Booleans are KDL v2, so a repeatable flag needs
var=#true.var=truefails to parse, and mise’s fallback for an unparseable spec is to forward argv unvalidated — so the broken spelling appears to work, right up until the spec parses again and starts enforcing.
Verified by running the documented invocation against a deliberately bogus workflow name: it now reaches
enforce_policyand is refused there (.forgejo/workflows/zz-not-a-workflow.yaml does not exist on main) rather than dying in the argument parser, and nothing is filed. A scan of the rest ofmise-tasks/found no other task with either defect. - The
-
argocd-deploy maiden-run fixes: unset Forgejo’s injected
GITHUB_TOKENbeforemise x(it 401s against api.github.com), pre-installargocd@3.3.12as a runner host tool, and list declared Applications when theappinput doesn’t match (most carry a-ringtailsuffix). -
warrant-bot-provision: user creation needswrite:admin, which the stored forge token deliberately lacks. Adds--tokenfor an ephemeral admin token (never stored), mints the bot’s PAT as the bot over basic auth (no admin scope for that step), and prints the exact remediation on 403. -
warrant-bot-provision: generate the bot’s login password in-process and store it beside the PAT in oneblumeops-ciitem (the two-item dance had a read-after-create failure mode), and surfaceop’s own error text instead of a bare CalledProcessError. -
The dispatch token belongs in the
blumeopsvault: 1Password Connect (and so external-secrets) is scoped to that vault alone, whileblumeops-ciis for CI job-timeop read. Also:/healthznow reportsarmed | armed-no-token | disarmed, so a missing token is visible instead of masquerading as “disabled”. -
warrant v0.2.2: decision forms gain the note field the API always accepted, notes surface in the warrants table, and deny sits left of approve so the destructive-by-default ordering matches the design. Module docstring refreshed to describe v0.2.
-
ArgoCD
workflow-botRBAC gainsapplications updateso the argocd-deploy workflow canapp set --revision. Pairs with re-minting its stale token — the provisionedARGOCD_AUTH_TOKENwas signed by the retired minikube-era ArgoCD and fails on the ringtail instance (token signature is invalid). -
Fix the silently-skipped
agents-m2mblueprint entry: the OAuth2 provider needsredirect_uriseven for a pure client_credentials flow (discovery 404’d; every working provider in the file has one). Placeholder points at Warrant’s future callback. Also aligns the service account with the zot-ci shape (is_activeinstead ofpath). -
authentik m2m round 2: the client_credentials flow authenticates a specific service account via username + app password (issuing the token as
agent-ringtail, groups claim included); the bare client_secret-only variant hits the app’sagents-sapolicy binding as a foreign auto-SA →invalid_grant. Adds a vault-fed app-password token to the blueprint. -
warrant v0.2.1:
/auth/callback500’d —httpxwas lazily imported and missing from the image (smoke tests masked it). Now a top-level import (missing deps fail at boot, not first login) and in the nix package set.
Infrastructure
-
ArgoCD workload applications now sync automatically on merge to
main(pruneandselfHealboth off). A merge reaches the cluster without a separate sync step;git revertbecomes a real rollback. Theappsapp-of-apps, ArgoCD’s self-management app, and the two apps tracking mutable mirror tags stay manual. -
Begin migrating the ringtail-agent workspace from a shared-host OS user to a k3s pod with its own Tailscale identity (
tag:agent), closing the device-trust hole that let the agent userssh erichblume@indri. This PR lands the identity foundation —tag:agenttagOwner, a fenced egress grant (forge + heph only, no OS SSH), and ACL tests — plus the agent-containerization design doc. No behavior change until the pod ships. -
Bump mealie v3.16.0 -> v3.20.1 (bump-with-review to what nixos-unstable currently carries; no DB/schema breaking changes, SQLite continues to auto-migrate forward via init_db) and miniflux 2.3.1 -> 2.3.2 (converted to the same nixos-unstable self-pin as mealie/navidrome, since stable nixos-25.11 is EOL; 2.3.2 is a security patch preventing username enumeration via login timing).
-
agent-ws 0.15.0:
nixin the pod, deliberately eval-only. Its store, state and log dirs are relocated onto the PVC (~/.local/state/nix), which needs no root, daemon, namespace or capability — enough fornix-instantiate/nix evaland the evaluator-side fetchers (nix-prefetch-url), and structurally incapable of a useful build, since a relocatedstore-dircan never hitcache.nixos.org(max-jobs = 0makes that a clean error). Restores what containerization silently dropped: Nix changes tonixos/ringtail/andcontainers/*/default.nixno longer reach a human with their syntax unchecked. The store is pure cache — nothing in the pod creates a GC root — soagent-ws-workspace gcsweeps it on size (above 6 GiB it collects and clears~/.cache/nix, which the collector does not touch). The agent container’s memory limit goes 4Gi → 6Gi to fit a full NixOS evaluation alongside claude. -
Retire the host-level agent workspace now that the agent runs as a k3s pod (agent-containerization):
nixos/ringtail/agent-workspaces.nix→agent-heph-spoke.nix, slimmed to just theagentuser + its heph spoke (the workspace/remote-control/repo machinery and the unused Forgejo bot SSH key are gone; the heph spoke stays as the shared daemon the pod mounts). Also add a PVC-chown initContainer to the agent-ws Deployment so HOME ownership holds on a fresh volume without a manual chown, and bake a defaultPATHinto the image (image v0.9.0). -
Remove the Mikado apparatus: the
mikado-branch-invariant-checkcommit-msg hook, thedocs-mikadoviewer, themikado-navigatorsubagent, and theC2(<chain>):commit convention. No chain was using it — canonical carried nomikado/*branch and no card with Mikado frontmatter. Multi-phase work is now just a branch and a PR. -
Renamed the
k8scompat label on the indri Forgejo runner to the honestindrilabel across all 6 workflows that used it (runs-on: k8s→runs-on: indri), removedk8sfromforgejo_runner_labelsin theforgejo_runneransible role, and updated the docs that described the old label as current state. Thenix-container-builderrunner is untouched. -
deploy-fly.yamlis now a warrant-requestable privileged workflow:workflow_dispatch-only with a SHA-boundrevisioninput,runs-on: priv, and awarrant-policy.yamlentry. The push-to-main auto-deploy trigger onfly/**is removed — merging no longer deploys the proxy; a human dispatch, gilbert’smise run fly-deploy, or an approvedmise run request-run deploy-fly.yaml …does. -
agent-ws: cut zombie-detection latency from ~10min to ~3min by moving boot grace into a
startupProbe(up to 20min for cold-PVC boots) and tightening theagent-ws-healthlivenessProbe to 30s × 6 failures. The 2026-08-06 incident confirmed the detector fires correctly, but the old window was long enough that the workspace read as “offline” in the Claude app before kubelet recycled it. -
gamedevjoins the agent workspace pool, with the native libraries it actually needs to build.The repo was already granted (
access: write) but held atpool: none, so it had no checkout in the pod. Flipping it tocanonicalis the one-line half. The other half is that containerization silently dropped Bevy’s Linux native deps: the retired host launcher exportedPKG_CONFIG_PATHandLD_LIBRARY_PATHover them, andcontainers/agent-ws/was never given the equivalent. Sogamedevwould have arrived as a checkout that could not be compiled.Verified from inside the pod rather than reasoned about, which is what caught the interesting part. A mise-provided
rust@stablebuilds and links an ordinary crate fine;mise run checkon gamedev fails in a-sysbuild script. But it fails onwayland-sys, not onlyalsa-sys— and the host file’s comment asserted that every windowing library wasdlopen’d at run time and therefore needed nothing at build time. That was wrong for this dependency set. Porting the host’sgameBuildDeps/gameLibssplit verbatim would have shipped a fix that still did not build.So
PKG_CONFIG_PATHnow carries the dev output of the wholegameLibsset rather than a hand-picked subset, with the runtime libs onLD_LIBRARY_PATHas before. A.pcfile no build script asks for costs nothing; a missing one is a hard failure, and which crates probe at build time is a property of Bevy’s feature flags that will drift.Rust itself stays out of the image, and that is now confirmed rather than assumed:
mise install rust@stableworks in the pod (theldLibsloader shim was already sized for it), installs to~/.cargo/binon the PVC so it survives restarts, andgamedev’s ownmise.tomlpinsrust = "stable"— somise run checkprovisions its own toolchain.This lets an agent
cargo check/buildthe Bevy workspace to verify its work. It does not make the pod able to run a windowed Bevy app — no GPU, no display — so playtesting stays a human job, consistent with the timberborn-parsimony rule.Image toolchain version 0.11.0 → 0.12.0.
-
Add
containers/agent-ws/default.nix— the dockerTools image for the containerized agent workspace (step 2 of agent-containerization). Carries the curated toolchain (op, git, tea, mise/uv/pandoc/typst/weasyprint, the CLI + build tools);claudeself-installs at pod-start onto the PVC rather than being baked in. Symlinks the glibc loader so prebuilt ELF binaries run in the non-FHS image. No deployment yet — manifests follow once pod egress identity is proven on-box. -
agent-ws 0.11.0: liveness watchdog for Remote Control zombies — an exec probe (
agent-ws-health) restarts the agent container when no claude process holds an established TCP connection, catching the survived-a-WAN-blip-but-never- reconnected failure mode from the 2026-08-01 outage. -
Deploy the containerized agent workspace (step 3 of agent-containerization): a
tag:agentTailscale auth key (Pulumi), and theagent-wsk8s manifests — Deployment with a userspace Tailscale sidecar for own-identity egress, restricted ServiceAccount, PVC, Secrets, and a NetworkPolicy that forces forge/heph traffic through the sidecar (closing the node-NAT egress trap). Also gives the image an internal schema version so the Build Container workflow can tag it. -
Which repos the
agentsbot may touch is now declared, not clicked.containers/agent-ws/repos.jsonis one file driving both halves of “share a repo with the agent”: the forge collaborator grant (reconciled bymise run agent-repo-access, wired to the new Agent Repo Access workflow) and the pod’s clone loop (default.nixreads the same file viabuiltins.fromJSON).myeveandtimberborn-parsimonyjoin the pool.Those were previously two independent manual steps, and skipping the grant fails invisibly: Forgejo answers 404, not 403, for a private repo the caller cannot see, and the clone loop is deliberately non-fatal (one unreachable repo must not crashloop the workspace) — so a missing grant is indistinguishable from a typo, and the only symptom is a directory that never appears.
timberborn-parsimonywas documented as a sibling checkout for three weeks while being absent for exactly this reason.Reconcile is authoritative: a repo absent from the file has its collaboration removed, and the workflow’s PR job runs
--checkso revocations are visible in review before the merge that applies them.blumeopsandagentsare pinned read-only by an invariant in the reconciler that the data file cannot override — their read-only-on-canonical status is what keeps blumeops CI and its deploy-credentialed Actions secrets out of agent reach, and a fence like that should not be flippable by a one-line edit to a config file.The reconciler’s admin credential reaches CI the established way — declared in the
forgejo_actions_secretsansible role and pushed by a human withmise run provision-indri -- --tags forgejo_actions_secrets, reusing theeblumePAT that role already authenticates with. That human step is the point: theagentsvault is the agent’s, the blumeops vault is privileged, and Forgejo Actions secrets are the curated subset a human deliberately moves across.Image toolchain version 0.9.0 → 0.10.0.
-
Wire the
blumeops-civault tier into CI: the read-onlyblumeops-ci-readerservice-account token provisions into Forgejo Actions asBLUMEOPS_CI_OP_TOKEN(warrant-approval-gated-runs Phase 2). The vault starts empty; items migrate via per-workflow audit. -
EVE Online game state now publishes itself into heph. An hourly systemd user timer on ringtail (
nixos/ringtail/myeve-heph-sync.nix) runs the MyEVE sync, filing PI extractor expiry, undelivered industry jobs, skill-queue exhaustion and undercut market orders as tasks under the MyEVE project — and closing them again when the game state resolves. The motivating case: a manufacturing job finished 2026-07-18 and sat undelivered for 12 days because nothing surfaced it.User scope is load-bearing, the same constraint as the eblume heph spoke — the sync shells out to
heph, which needsXDG_RUNTIME_DIRto findhephd.sock. A system service would bind the~/.local/sharefallback and never meet the spoke. The unit skips cleanly when the CLI, the myeve checkout, the ESI token or the socket is missing, and fails loudly only when the ESI refresh token is revoked.The sync logic lives in the myeve repo and gained matching fixes: reconciliation is now keyed purely on the
myeve-key:line in each task’s context doc, with the heph store as the only state. The local cache file it used to depend on could be lost, and when it was, every live chore looked new — duplicates filed, originals stranded beyond the reach of the closer. Two further bugs fell out: a collector that raised had its live chores closed as if resolved (one flaky ESI call could close “deliver your finished job”), and--only piclosed every non-PI chore. Both now scope closing to the collectors that actually ran. -
Each Remote Control session now gets its own worktree of every pooled repo, and the pool itself is kept pinned to canonical
main.Only the
agentsrepo had per-session isolation, because that is allclaude remote-control --spawn worktreedoes — it operates on its own cwd repo and nothing else. The other seven pooled repos were one shared checkout each, so two concurrent sessions editing blumeops contended for one HEAD and one index. The only thing preventing that was a paragraph in the agents repo’sAGENTS.mdtelling sessions to branch first, and the evidence that it is load-bearing is already on the PVC: two prior sessions had hand-rolled their own blumeops worktrees, by two different methods, one detached and one on a branch.agent-ws-workspace(new, generated fromrepos.jsonlike the clone loop) has three verbs.initis aSessionStarthook and gives the session a detached worktree of each repo at~/code/sessions/<session-id>/<repo>.syncfetches and fast-forwards each pool checkout onto canonicalmain.gcreaps the worktrees of sessions that have ended.syncandgcalso run once at pod boot.Worktrees rather than clones: they share the object store, and they enforce one-branch-one-checkout in git rather than by convention. Measured cost is 14 MB for all seven — the whole pool is 43 MB.
Three things fell out of building it that were not the original goal:
Sessions were waking up stale. The clone loop only fetches at pod boot, so
--spawn worktreebranches off whatevermainwas when the pod started. On a pod up for days that is visibly wrong in the one repo where it matters most: this change was authored from a session whoseagentsworktree — and therefore whose own base instructions — was two commits behind canonical.initnow fast-forwards the session’sagentsworktree too, when it is clean.A fork’s
mainlies.originonagentsandblumeopsis the bot’s fork, sogit statussays “up to date with origin/main” while canonical is 110 commits ahead.syncpushes canonicalmainonto the fork so that sentence means what it appears to mean and cross-repo PR diffs stay honest. Fast-forward only; a diverged fork is left alone and reported.Nothing ever reaped worktrees. Fourteen had accumulated since 2026-07-31.
gcuses Remote Control’s own worktree lock as the liveness signal, waitsAGENT_WS_GC_AGE_DAYS(7) after the session goes quiet, and — the part worth reviewing — refuses to remove anything with a dirty tree or a commit canonicalmaindoes not already contain, reporting it instead. Losing an agent’s unpushed work is worse than the disk. A crashed session would hold its lock forever, so a lock older thanAGENT_WS_GC_LOCK_MAX_DAYS(30) is treated as dead.The hook is seeded into user settings (
~/.claude/settings.json, jq-merged, re-written every boot) rather than committed to the agents repo as project settings, because project-scoped hooks prompt for trust on first use and there is nobody at a terminal in this pod. The image stays the source of truth for it, which is also what the agents repo’s own “changing your own environment is a blumeops PR” rule asks for.CARGO_TARGET_DIRis now shared across every checkout. Without it, per-session worktrees each build Rust from cold — minutes for Bevy — and atarget/per worktree per session fills a 20Gi PVC quickly. Cargo locks the directory, so concurrent builds serialize rather than corrupt each other.Verified in the pod rather than reasoned about: the script was extracted from the derivation and run against the real pool.
syncis clean across all eight repos;initproduces seven detached worktrees at canonicalmainand is idempotent;gcreaps a clean aged session and refuses an aged one carrying an unpushed commit.This is working-tree isolation, not a security boundary and not repository-level isolation — refs, remotes, config, and the object store all stay shared, and all sessions remain one process, one uid, one PVC. True per-session isolation means a pod per session, which the RWO PVC and single rooted Remote Control server rule out today.
Image toolchain version 0.13.0 → 0.14.0.
-
Point the warrant manifest at the first built image (
v0.1.0-68f84a3-nix, run 708) — replaces the UNBUILT placeholder. -
Fly proxy: Grafana Alloy v1.17.1 → v1.18.0. The v1.18.0 breaking changes are confined to
otelcol.*components andfly/alloy.riveruses none of them — onlylocal.file_match,loki.*, andprometheus.*— so the upgrade is a no-op for our config. Deferred at the 2026-07-20 service review purely because the release was hours old and this is the public-facing edge; v1.18.0 has since stood 17 days as the head of the train with no patch behind it. -
Deploy warrant v0.3.3 — the dispatch asks the forge which run it created (
return_run_info) instead of inferring it from the run list. -
Warrant image bumped to v0.3.4-8a18aed-nix (run-attribution serializer in GET /api/requests, PR #523; built by approved run 741).
-
Deploy agent-ws 0.10.0 (
v0.10.0-55e2996-nix) to the ringtail cluster. -
Deploy agent-ws 0.12.0 (
v0.12.0-47e7efd-nix) to the ringtail cluster — the image carryinggamedevin the repo pool and Bevy’s native build/run libraries. -
agent-ws 0.13.0 — the image version catches up with the
/usr/bin/envfix that shipped in 0.12.0’s source without a bump. The image also gains aservice-versions.yamlentry, soservice-reviewtracks it like every other first-party container. -
Deploy agent-ws v0.13.0 —
mise run <task>works in the pod. -
Deploy agent-ws v0.14.0 — each Remote Control session gets its own worktree of every pooled repo, and the pool tracks canonical
main.Built from f6b3274 (the merge of #499) as request #12, run 720. Tag confirmed present in the registry rather than derived from the naming convention —
container-listreports no tags from inside the pod, since it has no route toregistry.ops.eblu.me.First boot on this image does three things the previous one did not: seeds the
SessionStarthook into~/.claude/settings.json, fast-forwards each pool checkout onto canonicalmain(including pushing the bot’s forkmainup to match), and reaps the fourteen session worktrees that have accumulated on the PVC since 2026-07-31 — skipping any that are dirty or hold a commit canonicalmainlacks. -
Pilot follow-up to #440: point mealie/miniflux manifests at the images actually built post-merge (
-892eeac-nix) — the pre-bumped-483b3d6-nixtags referenced a branch SHA orphaned by the squash-merge (the last one: squash-merge is now disabled, so future approved SHAs survive merge). -
Service review: upgraded navidrome v0.61.1 → v0.63.2 by self-pinning the container’s nixpkgs to nixos-unstable (mealie precedent — ringtail’s stable 25.11 nixpkgs lags at 0.61.1). Picks up the v0.62.0 security fixes (cross-account share disclosure, authorization checks, transcode-DoS cap) and the v0.63.x scanner/search overhaul. Sharing stays off (
ND_ENABLE_SHARING=falsepinned — v0.63.0 flips the default to enabled). The Build Container workflow now fails fast with a clear error when dispatched with a short commit SHA (actions/checkout treats those as branch names; see run #665). -
Warrant P0 chores:
agent-repo-access.yamlmoves off the retiredk8srunner label (the one straggler added after #439 branched), andpolicy.hujsongainssshTestspinning two invariants —tag:agenthas no Tailscale SSH anywhere, and the load-bearing homelab→homelab SSH (borgmatic’sssh:eblume@ringtaildumps, the rule PR #441 nearly removed) can’t be dropped silently. -
Dedicated privileged runner (warrant-approval-gated-runs Phase 2):
ringtail-priv-runner, a sandboxed NixOS DynamicUser instance carrying theprivlabel — privileged dispatch-only jobs move off host-modeerichblume@indri. The indri runner dropspriv; argocd-deploy prefers the runner’s nixpkgsargocdwith a mise-x fallback. -
Deploy warrant v0.2.0 (
v0.2.0-9060183-nix, run 709) — the human door + decisions/warrants go live. -
Deploy warrant v0.2.1 (
v0.2.1-14970b4-nix) — the login-flow fix. -
Deploy warrant v0.3.0 (
v0.3.0-b87e4a2-nix) — dispatch machinery present, still disabled. -
Deploy warrant v0.3.1 — dispatch reads the blumeops vault, and
/healthzreports the arming state. -
Deploy warrant v0.3.2 — warrants link to the run they caused.
Documentation
-
Deployment docs describe the sync policy the fleet actually has. AGENTS.md called ArgoCD “manual sync” and prescribed
app set --revision main && app syncafter every merge; 31 of 35 applications sync themselves, so that step raced the auto-sync the merge had already started.deploy-k8s-service’s Application template also omittedautomated, quietly making each new service a manual-sync exception. -
The observability replication tutorial had the three pillars wrong: it listed metrics, logs and dashboards, omitting traces entirely — despite Tempo, Beyla eBPF auto-instrumentation and full trace↔log↔metric correlation all being deployed. Corrected to metrics/logs/traces with collection and presentation as supporting layers, and added the two steps that were missing: deploying Tempo, and the privileged
alloy-tracingDaemonSet that produces spans without instrumenting any application. -
Retire the post-merge container rebuild. Since canonical stopped squash-merging, a build from the PR branch head stays reachable from
mainafter the merge, so its tag becomes[main]on its own. Build once from the final branch head and put the manifest tag bump in the same PR — no rebuild, no follow-up commit. -
The PodNotReady runbook told you to run
kubectl top— which cannot work on ringtail, whose k3s runs--disable=metrics-server. Step 4 was a guaranteed dead end, reached exactly when someone is mid-incident chasing a Pending pod. Replaced with Prometheus queries (every metric verified to exist first), and noted why they are the better tool anyway:Pendingis a question about requests versus allocatable, which utilisation numbers do not answer. -
Correct the Warrant doc’s claim that Forgejo login is local and unprotected by Authentik MFA. The forge has been behind Authentik SSO with TOTP enforced since 2026-02-20 (PR #228), so Phase 0’s “enable WebAuthn on the forge account” item was satisfied before it was written, and dispatch-as-approval met invariant 4 from the start.
-
The blumeops-ci item-migration audit (blumeops-ci-item-migration): which Actions secrets migrate to job-time
op read(argocd pilot first), the FORGE_ADMIN_TOKEN flag, and the verdict that wholesaleprovision-*never migrates — decomposition into narrow per-role actions is the path. -
Track the two user-facing heph installs in
service-versions.yaml:heph-cli-ringtail(Erich’s desktop spoke, pinned byhephTag) andheph-cli-gilbert(installed by hand, the only heph install not under IaC). Previously only the indri hub and the ringtail agent spoke were tracked, so a version review could never surface the CLIs Erich actually types at.heph-cli-gilbertstarts with nulllast-reviewed/current-version— nothing asserts gilbert’s version and the host was unreachable — which floats it to the top of the review queue, where an untracked install belongs. Documents thetypevalues actually in use (the reference card listed neithercontainernormanual) and when null fields are the right answer. -
Rename the planned CI vault tier
ops-ci→blumeops-cibefore anything functional references it (docs, policy notes, and the futureBLUMEOPS_CI_OP_TOKENsecret name). -
Proposal doc: warrant-approval-gated-runs — a phased design for agent-requested, human-approved privileged runs (request loop over dispatch-as-approval,
ops-civault tier, dedicated privileged runner, and the Warrant broker), consolidating the agent-boundary program and disposing of open PRs 439/440/441. -
Documentation catches up with reality: the program doc reports built state with a per-phase table, the warrant service card describes v0.3.2 (flow, API, operating it, known gaps), request-a-privileged-run points approvals at Warrant rather than the forge UI, and
AGENTS.mdtells agents to file requests instead of asking in prose. -
Record the approval-factor decision: no hardware key on hand, so Warrant v0.2 ships with Authentik session + 1Password-managed TOTP as the step-up. Invariant 4 amended accordingly; the WebAuthn/hardware upgrade path is preserved structurally (decisions gate on an authentik flow slug, so hardware later = authentik config, not Warrant code).
-
Retire the C0/C1/C2 change classification from the docs, matching AGENTS.md, which replaced it with a two-route split: direct to main for small interactive fixes, feature branch + PR for everything else and all remote-agent work. The
change-classifiersubagent, whose only job was the retired triage, is removed. -
Doc review: reviewed
reference/services/paperless.md(never reviewed) — verified against the live manifests, added the missing ArgoCD app / sync policy / tracked-version rows, clarified that the “Redis” sidecar is the nix-built valkey image, and stampedlast-reviewed. Also refreshed the stale Quick Reference inreference/services/navidrome.md(post-minikube app/manifest names) alongside the navidrome upgrade. -
Document UX7 diagnostic API read access (X-API-KEY endpoints), current Internal-zone firewall posture, and IoT SSID compatibility notes (Owlet base station onboarding) in the UniFi reference card.