Repository navigation
Description
The fleet runs on k3s and Flux from this repository, from an empty PC to every app live, and hal is archived.
Problem and caller
-
Ruling. The operator, in hal's head chat on 06.11.2026 (recorded in an Plattform: vom leeren PC bis alle Dienste live hinter dem Login (übergreifend) hal#20 comment): „ja das ist das ziel k3s und FLUX.“ It replaces the ruling of 23.10.2026 to keep Compose plus hal.
-
His words before the ruling: „ich will es professionell … wir können doch nicht mit sowas wie k3s konkurrieren was ein standard ist“; „und dann will ich einen plan! udn wie wir dahin kommen! auch wenn das heißt dass 50% unseres codes wegfallen. natürlich will ich die einfache einrichtung“; „am ende wenn wir hal nciht brauchen auch okay?“; „also am ende will ich auch einfach nur eni befehl tippen oder w und es passoiert?“
-
The repository. hal's head proposed a new public repository beside hal, with secrets encrypted in a separate private repository. The operator answered: „Ja platform kling für mich gut! go for it! leg es an!“
-
Why. hal grew its own deploy controller against its blessed guiding sentence of 23.10.2026, „hal builds nothing a maintained tool already does“. The two analyses put that controller and the other replaceable parts at 30 to 46 % of hal's code.
-
Evidence. Two independent read-only analyses, posted below as comments:
- Opus 5.5 at max effort recommends k3s plus Flux, reached through a prefix that also pays off on Compose.
- Codex gpt-6-astra at max effort recommends Compose plus maintained tools.
He chose the first.
Guiding sentence
This repository builds nothing a maintained tool already does. It is the one description of the platform, the k3s and Flux configuration in Git, plus the few tools no tool covers: legal pages, person export and erasure, AI keys and budgets, the restore test and the secret checks.
Blessed by the operator in platform's head tab, 06.11.2026: „1 ja“, his answer to this sentence put to him in German. It projects hal's blessed sentence of 23.10.2026 (overnightworks/hal#20) onto this repository.
Target picture
- Platform. k3s runs on the one PC, and Flux reconciles this repository. Every change goes through a pull request; a rollback is
git revert. - Images. Every app image is built in that app's CI and stored privately in GHCR; the PC pulls it with a read-only token. Where model weights live is decided per service in the image slice, by what serves best. The operator: „den besten weg gehen … wenn sie in ein volume gehören sollten wir das so machen oder nicht idk“.
- Secrets. SOPS with age, kept in a private repository
platform-secrets; Flux decrypts them. - A fresh PC.
git clone … && scripts/bootstrap_pc.shinstalls mise-pinned tools, asks once for the age key and shows the deploy key to add once on GitHub. - Services carried over:
- Traefik;
- Keycloak, its realm applied by keycloak-config-cli and its theme built into the image;
- LiteLLM and Postgres (step-ca has no caller and is not carried over; P8 decides in-cluster TLS by its own needs, the head's decision of 07.11.2026 in P7: Postgres, Keycloak, LiteLLM and backups run on the platform, tried on the PC with copies of hal's backups #116's plan review);
- restic backups through k8up;
- the Cloudflare tunnel.
- Docker stays beside k3s on the PC for good, for agents' k3d runs and test containers, and the bootstrap owns it (the operator, 06.11.2026, ruling Q7 in the comment on this item).
- Logs and checks. Standard tools only: Alloy into Loki, Grafana, Prometheus with Alertmanager, Gatus.
- Shipping an app. A release in the app → CI builds the image → its last job opens the bump pull request here within minutes → it lands once its review and CI are green (Agents run every platform step that needs neither sudo nor a secret the operator fetched #140, ruled 08.11.2026) → Flux deploys it.
- Libraries are out of scope. They release through CI-gated GitHub releases; the operator in appkit's session, 06.11.2026: „also alle libraries sollten so gehen!“ They stay pinned as The fleet's libraries reach their hosts through a way Dependabot can read webauth#543 ruled.
Invariants every slice keeps
- Built live beside hal. The platform is built and tried on the PC beside hal, with no throwaway VM per step. The operator, 06.11.2026, in platform's tab, choosing A on the line below: „A => einfach sagen wenn soweit ist! sir haben ja noch die hal backups ich hab keine wichtigen daten! => du kannst hal offline schalten für unserenr test alles gut!“
- The head tells him once before the first host step; after it, lanes run host steps without asking again.
- hal may be stopped for a test, the way hal's head names. Deleting hal's data or
~/srv, andhal apply,hal update,hal setup,hal migrateor agit pullin~/srv/hal, still need his yes, and so does any step that would delete data or stop hal's containers (adaemon.jsonchange, a Docker restart), as the ruled card below says. 127.0.0.1:8081 and 127.0.0.1:8090 stay untouched, andk3s-uninstall.shis the known way back while the cluster holds no data it would delete. - k3s keeps its own containerd, and no GPU workload runs in k3s before the GPU slice: songmaker and speech share the one card.
- The fresh-machine proof (bootstrap from nothing) runs once in a VM before hal is archived. The cutover itself runs on his word.
- Apps go live one by one, as a test. Each app moves its traffic to the platform as soon as its slices are ready, hopin first, without waiting for the cutover: the apps have no users yet. This replaces “live stays on hal until the cutover” in the ruled card below. The operator, 06.11.2026, on hopin runs on the platform from its attested private image, routed by Traefik #46's first card: „3b) all zusammen oder von mir aus erst hopin für den test“; „warum nich alle? es tut nicht weh die apps haben keine user! also was ist los? sind die apps vorbereite? einfach fucking test go und wir sehen ob alle apss gehen oder nicht?“
- Nothing a maintained tool does is built here. Every tool piece names the tools it was checked against.
- One living copy. Until then hal is frozen: it takes only fixes, the agent-service removal (hal#1086) and the renames that keep the cutover a pure platform swap. A part moves here once, and its hal copy goes with hal's archive. One exception, by the operator's ruling of 07.11.2026 (P7: Postgres, Keycloak, LiteLLM and backups run on the platform, tried on the PC with copies of hal's backups #116, line 5, A): Keycloak and LiteLLM move before songmaker and presentator, which keep hal's LiteLLM as their gateway until they move; their spend there is not carried over, and Keycloak's back-channel logout does not reach them in that time.
- The cutover changes only the platform. Names, groups and legal wording are settled on hal before it. One exception, by the operator's ruling of 07.11.2026 (P7: Postgres, Keycloak, LiteLLM and backups run on the platform, tried on the PC with copies of hal's backups #116, line 3, A):
/services/agent-service/apibecomes/services/litellmat the Keycloak move, in one window in which every app changes its API group in step. - No secret enters this repository, a log, a brief or a test, encrypted or not. Encrypted secrets live only in
platform-secrets. - The fleet's quality gate holds from the first pull request.
Outline
Each slice is cut at its pull, by the phase planner where it spans more than a few files.
| Slice | Done when |
|---|---|
| P1 Images, #4 | quality ships one reusable image workflow (build, SBOM, provenance, private GHCR push, attestation). Every app's tag yields an attested private image, and Keycloak's image carries its theme (hal#908). |
| P2 Bootstrap, #12 | scripts/bootstrap_pc.sh with a pinned mise manifest sets up a machine's tools from nothing: a bare container in CI, then the PC. |
| P3 Cluster skeleton, #11 | k3s configuration, the Flux layout, Traefik and storage that retains volumes exist, and a k3d CI job reconciles the tree. |
| P4 Secrets, #13 | SOPS with age and platform-secrets work, Flux decrypts, and the age key has a backup path. |
| P5 Logs and checks, #171 | Alloy into Loki, Grafana, Prometheus with Alertmanager, and Gatus run; one log rule covers the fleet. |
| P6 Stateless apps | hopin, speech-runtime and presentator's web run in k3d and on the PC beside hal. |
| P7 Stateful services | Postgres, Keycloak, LiteLLM, backups through k8up and the restore test run, tried on the PC with copies of backups. |
| P8 songmaker, #172 | Web, workers, the GPU and the model volumes run. |
| P9 Tools, #173 | Legal pages, person export and erasure, AI keys and their expiry, and the secret expiry checks have moved from hal. |
| P10 Cutover | The cutover and its rollback are tried on the PC, with hal stopped for the test; the live switch runs on his word, and the fresh-machine proof runs once in a VM before P11. |
| P11 Predecessors leave | hal is archived; every app's compose file, deploy script and own monitoring are removed through items on their boards. |
Predecessors and their exits
- overnightworks/hal: frozen until P10 and archived in P11. Its commit-hook setup and its journal-retention timer leave with it; their replacements are P2: scripts/bootstrap_pc.sh sets up a machine from nothing #12's.
- App deploy paths, each leaving in P11 through an item on its own board:
- songmaker's auto-deploy (songmaker#2284);
- songmaker's own monitoring (songmaker#2206, line 35);
- the compose files and runbooks of hopin, presentator, speech-runtime and songmaker.
- hal's naming rule (
hal/docs/naming.md, ruled by the operator on 16.10.2026) and the toolhal rename, which its procedures P1 (a repository rename) and P2 (a Sonar key) use. The rule's new owner is The fleet's naming rule gets an owner before hal is archived marketplace#571, proposed as a fleet standard for quality, and the rule leaves hal only once that item has landed. Its rows that only hal's deploy reads (fleet.yamlkeys,~/srv/hal, thehal_<service>_<app>networks and the MCP hosts) leave with hal's deploy in P11. The platform's names are its namespaces and Kustomizations, which need no rule from hal. - hal#24's open rename slices, among them marketplace to
atelier. They land in hal or move to marketplace#571's owner before hal is archived. P11 checks that none is left.
Owners the switch moves
From the app heads' replies of 06.11.2026:
- hopin#493 and #495. Composing the legal pages from app releases needs an owner here (P9).
- hopin#46. It waits on hal#167, hal#268 and hal#280, which need a home here before hal is archived.
- presentator#243. The build worker's sandbox is built on the Docker socket and needs a new isolation plan on k3s.
- songmaker#2271 and #2270. The GPU worker's isolation becomes volumes, a NetworkPolicy and a Redis ACL.
- webauth's end-to-end proof. It takes its Keycloak from a pinned hal checkout, so it needs the realm's new home (P7).
- speech-runtime. It bakes its weights in so that it runs with no egress. Its 9.8 GB image builds and is proven on a standard runner with no disk-freeing step (its main's
Speech: imagejob, under five minutes); a GHCR push and attestation of that size is not measured yet (P1). Its GPU voice (Qwen, GPU voices: the service loads the selected GPU voice and fails visibly when the card is taken speech#2) shares the one card with songmaker, and nothing arbitrates between them at runtime: after the move it stays off until he needs it, and the card belongs to songmaker's music (P8: songmaker runs on the platform with its GPU and model volumes #172, P8a, ruled 08.11.2026); a card reservation for it is planned only when he turns it on. Source: speech-runtime's head, relayed by hal's head, 06.11.2026.
version = 1
now = "On the PC beside hal and Ready in Flux on 08.11.2026, not yet live: k3s and Flux, Traefik, the admission policies, SOPS secrets, the platform tunnel, hopin, speech-runtime, skat, songmaker's stores, Keycloak, LiteLLM, Loki, Alloy, Prometheus, Gatus, the backup layer, the imprint and the GPU plugin."
next = "The switch (P10) is cut as #235, whose slices are in build; presentator moves under #322; hal stops when both items' done-when hold."
done_when = "Every app and platform service runs on k3s and Flux on the PC from this repository; a fresh PC reaches that state with scripts/bootstrap_pc.sh and the age key; shipping an app version is a tag plus a merge; the restore test passes on the platform; hal is archived and no app repository holds a deploy path."
[[expectation]]
text = "Die neue Plattform wird auf dem gewählten Weg aufgebaut und getestet."
asked_on = 2026-11-06
ruling = "A"
ruled_on = 2026-11-06
card_hash = "sha256:9f1a788fd76540c57697857ebd8b6e0d297dac769d366b7fe2384f1dcb9806da"
via = "chat"
question = "Soll die neue Plattform gleich auf deinem PC neben dem alten Stand aufgebaut und getestet werden, statt vorher in einem Wegwerf-Rechner?"
problem = "Jeder Schritt wird erst in einem Wegwerf-Rechner geprobt; das kostet Zeit und beweist nicht, dass es auf deinem PC läuft."
decision_kind = "data_risk"
area = "Plattform"
topic = "Live-Test oder Wegwerf-Rechner"
recommended = "A"
reason = "Es gibt keine Nutzer; der alte Stand belegt keinen Port, den die neue Plattform braucht, und setzt keine Firewall; ob beide Netze sich vertragen und die Grafikkarte läuft, zeigt nur dein echter PC, und der alte Stand bleibt der Rückweg."
[[expectation.option]]
name = "Live neben dem Alten"
effect = "Die neue Plattform wird Schritt für Schritt auf deinem PC aufgebaut, während der alte Stand weiterläuft und der Rückweg bleibt."
notice = "Vor dem ersten Schritt auf deinem PC sage ich Bescheid, danach probiere ich ohne Rückfragen; nur bevor etwas Daten löschen oder alle Apps stoppen würde, frage ich dich."
cost = "Vertragen sich die Netze nicht, kann die Lösung alle Apps etwa eine Minute stoppen; dein einziges Backup liegt auf derselben Platte. Den Neuaufbau von null proben wir einmal am Ende."
undo = "reversible"
undo_how = "Die neue Plattform lässt sich mit einem Befehl wieder entfernen; der alte Stand läuft weiter."
contract = "Invariant 1 of #1 becomes: live stays on hal until the cutover, and the platform is built and tested on the live PC beside hal, announced to the operator before the first host step and then run by platform's lanes without asking again; a step that would delete data or stop hal's containers (a daemon.json change, a Docker restart) waits for his word. No backup ceremony: the restic copy stays on the same disk, a named risk. k3s keeps its own containerd (no `--docker`); 127.0.0.1:8081 and 127.0.0.1:8090 stay untouched; no GPU workload in k3s before the GPU slice, since the one card is shared with songmaker and speech; `k3s-uninstall.sh` is the known way back. hal's data and `~/srv` are never touched; stateful services run on copies of backups in their own volumes. The fresh-machine proof (bootstrap from nothing) runs once in a VM before hal is archived. The VM rows of P2, P3c, P6, P7 and P10 become host steps on the PC."
[[expectation.option]]
name = "Erst im Wegwerf-Rechner"
effect = "Jeder Schritt wird erst in einem Wegwerf-Rechner mit Kopien der Backups geprobt; dein PC bleibt bis zur Umstellung unberührt."
same_as_today = true
notice = "Auf deinem PC ändert sich bis zur Umstellung nichts; bevor ein Wegwerf-Rechner startet, sage ich es dir."
cost = "Etwa eine Lane mehr für den Wegwerf-Rechner und Last auf deinem PC bei jedem Lauf; wie es neben dem alten Stand läuft, zeigt sich erst bei der Umstellung."
undo = "reversible"
undo_how = "Später auf den Live-Weg umstellbar."
contract = "Invariant 1 of #1 stays: no slice touches the live PC, its data, its containers or `~/srv`; proofs run in k3d in CI or in a VM with copies of backups, P3c1 builds the VM harness, and the cutover runs only on the operator's word."
Activity
FlexOr2 commentedon Nov 6, 2026
Evidence: architecture analysis by Opus 5.5 at max effort, 06.11.2026. It was run read-only against hal at bac221a1, from the brief in hal head chat, and is posted unchanged. It recommends k3s plus Flux. One correction: its gate S6, the agent service's containment, no longer applies, because hal#1086 removes that service.
hal's boundaries, and whether to run on k3s (read-only analysis, 06.11.2026)
Repository read:
/home/felix-hummert/git/hal-worktrees/arch-boundary-analysis(bac221a1). I changed no file and ran nothing live. Where I could not confirm something, it says "unverified".Short answers
docs/architecture.md§8, "The applied record".curl -sfL https://get.k3s.io | sh -andflux bootstrap github …. But nothing would run afterwards. No image is in a registry and no manifests exist. Slices S1–S9 below build exactly that.1. Inventory
Line counts come from
wc -loversrc/hal, 28,617 lines in total. Tests are about 79.8k lines and probes about 29k (rough counts by file name).stack/catalog.py(622)fleet.yaml(external/*.tf)run.enabled/setup/buildduplicate Compose profiles or Flux Kustomizations. Hosts, exposure,deny_publicand legal have no tool equivalent.apply779,unit_plans707,core523,compose411,core_phase216,service_recreate208,built_image133,environment118,setup111,plan68 (= 3,274)docker compose up -d --waitrecreates on config or image change and keeps volumes (Compose up). Flux kustomize-controller hasprune,wait,healthChecks,dependsOn(Kustomization).docs/architecture.md§8).apps594,app_contract341,app_compose172,deploy_checkout152,releases67 (= 1,326)apps.py_run: checkout tag, build, create, start, wait, record). No for the contract's rules, but enforcing them is standard (PSS, CI).checkout477,update378,stack/cli128,stack_commands153,apply_lock40,waiting28,terminal23 (= 1,227)hal updateis a Git pull behind a yes (stack/update.pydocstring).plan_rows613,result_rows527 (= 1,140)flux getemergency799,barrier424,switches198,switch_events160,tunnel104 (= 1,685)flux suspend(CLI);systemctl stop cloudflaredrealm/observe799,realm/reconcile462,realm/__init__104,realm/declared97,pre_apply141 (= 1,603)observe(list what exists only in Keycloak) is hal's own; whether config-cli has a diff mode is unverified.external/*, 10 files (= 2,407)host_check/host_probe(the ready guard) are hal's own but moot behind readiness.cloudflare_read(undeclared records) is hal's own.legal_section421,legal_step422,legal_pages299,appkit_step213,legal_commands65 (= 1,420)litellm_keys489,litellm_admin429,api_limits59,litellm_expiry156,expire_commands104 (= 1,237)litellm_expiry.pydeletes. I found no LiteLLM Terraform provider (unverified).backup/*plusbackup_commands, 13 files (= 3,392)pg_dumpviak8up.io/backupcommand, schedules backup, check and prune, and has a Restore resource, but no restore test (docs).backup/verify.py).check707,hook_check200,platform_check160,report139,backup_check131,logs_check131,mail111,alarm89,exposure_check24,check_commands135,alarm_commands44 (= 1,871)exposure/*plusexposure_rows, 9 files (= 817)secrets_view822,secrets_setup767,secrets_rows261,hidden_paste131,manifest_fields58,secrets_commands53,secrets_create49 (= 2,141)sops editwould hold. The mode and expiry checks are hal's own.setup740,system_step421,user_units347,layout112,jobs111,path_link93,ports84,hal_events80,setup_commands68,tunnel_config40 (= 2,096), plussystem/playbook.yml(546)setup.pyeven pinsMISE_VERSIONitself).rename_commands799,rename_rows182,migrate_commands142,project_move117 (= 1,240)keycloak/go-live937,step-ca/init540,agent-service/setup546scripts/auto-deploy.sh(1,771 lines) plus two unitshal's own docs say it: Compose swap cost is "high for edge (apply, check, backup labels, barrier and the Traefik provider all speak Compose)" (
docs/architecture.md§10, swap table).2. The boundary
hal is:
fleet.yaml, read by OpenTofu and the legal build);hal must never:
What hal hands the platform is one Git commit on
main:fleet.yaml, which OpenTofu applies to Cloudflare.What the platform owns alone: running, health, rollout, rollback, routing, secret decryption, volumes, backup schedules, log and metric retention, and alerts.
The same boundary holds under (B); only the component that pulls the commit changes. That is why the boundary work is never wasted.
flowchart LR subgraph GH["GitHub, org overnightworks (Team)"] APP["app repo, public<br/>git tag vYYYY.MM.DD.N"] WF["shared image workflow in quality<br/>build, SBOM, provenance"] REG[("GHCR, private<br/>image@sha256")] PR["bump PR in hal<br/>CI: attestation, policy, legal build"] HAL["hal repo = desired state<br/>cluster/, fleet.yaml, legal/, realm, own tools"] SEC["private repo<br/>SOPS-encrypted secrets"] end subgraph PC["the PC"] subgraph K3S["k3s: the platform alone runs, heals, rolls out, routes, stores"] FLUX["Flux controllers"] WL["apps, Keycloak + Postgres,<br/>LiteLLM, agent service, step-ca"] OPS["k8up, Gatus, Loki + Alloy, Grafana"] JOBS["hal jobs: LiteLLM expiry, restore test,<br/>secret check, daily summary"] TR["Traefik on 127.0.0.1"] end CFD["cloudflared, systemd<br/>global off switch"] OPR["operator: git, gh, mise run"] end TOFU["OpenTofu, mise task"] CF["Cloudflare: DNS, Access,<br/>tunnel config, rulesets"] APP --> WF --> REG WF --> PR OPR -- "merge = the yes" --> PR PR --> HAL HAL -- "Flux pulls main" --> FLUX SEC -- "pulled, decrypted" --> FLUX FLUX --> WL FLUX --> OPS FLUX --> JOBS REG -- "pulled by digest" --> WL HAL -- "fleet.yaml" --> TOFU --> CF CF --> CFD --> TR --> WL3. The alternatives
podman auto-updaterolls back a failed restart by default (man page), but Git pull and digest pins would again need hal-like glue.suspend, Helm hooks and remediation. Argo CD's core install drops its UI, RBAC and notifications (core).3.1 Day to day
hal applyandyeson the PC. songmaker deploys itself by its own timer.gh pr merge <n> --squash. The Compose deploy tool runs the update.flux reconcile ks <app> --with-source.hal update(yes), thenhal apply(yes)fleet.yamlentry,backup/apps.toml, realm client, the app's legal part; thenhal updateandhal applyapps/<name>/compose.yaml, catalog entry, backup profile; merge;mise run tofu:applycluster/apps/<name>/(start fromkompose convert, Kompose), host entry, k8up Schedule, realm client; merge;mise run tofu:applyhal restore <unit> --livefrom the named pre-apply snapshot (§8). An app has no rollback today (#660).git revert <bump> && git push(Compose has no rollback option). Data:restic restore, thendocker compose up -d.git revert <bump> && git push. Stateful:flux suspend hr keycloak, thenkubectl -n keycloak scale deploy keycloak --replicas=0, thenmise run restore -- keycloak <snapshot>, then revert, thenflux resume hr keycloak.hal stop <app>; everything public:hal offlinedocker compose -p <app> stop. It stays off only if the deploy tool honours a hold (doco-cd: unverified).flux suspend ks <app> && kubectl -n <app> scale deploy --all --replicas=0, back withflux resume ks <app>. This holds while Git does not declaresuspend(Flux). Everything public:sudo systemctl stop cloudflared.3.2 Cost on this machine (12 cores, 60 GB)
~/.secretsin no backup (docs/architecture.md§6)INSTALL_K3S_VERSION(env); chart bumps by Renovate (flux manager); datastore backup (doc)reclaimPolicy: Delete(manifest); Flux prune deletes what leaves Git unless the resource carrieskustomize.toolkit.fluxcd.io/prune: disabled; Docker and k3s together (iptables, unverified); 6443/10250 bind scope (unverified)What of hal dies:
On D14 (agents are root through the docker group): k3s does not fix this by itself. Through a bind mount, an agent can still read the root-only kubeconfig. What (C) adds is RBAC: a limited kubeconfig for the agents' account, which
playbook.ymlalready creates dormant, once that account leaves the docker group.3.3 Risks for stateful services
stack/pre_apply.py)runtimeClassName: nvidia(k3s). local-path has no capacity limits (README).3.4 Logs and the abuse trail
Today's settings explain the unexplained 401:
system_step.py:DOCKER_LOG_DRIVER,JOURNAL_VACUUM_DAYS).log:and no access log (traefik.yml,agent-service/litellm/front/traefik.yml,agent-service/egress/traefik.yml).turn_off_message_logging,disable_spend_logsanddisable_error_logsare on (agent-service/litellm/config.yaml), andLITELLM_LOG: CRITICALis set (compose.yaml).The gateway lines are #1084, not planned here.
journalctlloki.source.journalinto Loki (Promtail is end of life since 2026-03-02, doc), or VictoriaLogs (doc); searched in Grafanaretention_period: 720h; the default 0s keeps logs forever (doc). VictoriaLogs-retentionPeriod=30d(its default is 7d)./var/log/pods. Kubernetes rotates these by size only (10Mi × 5), has no log storage of its own, and logs vanish with evicted pods (doc). An Alloy DaemonSet ships them to Loki./var/log/pods: a quiet container's live log file can hold lines older than 30 days. Whether the kubelet can rotate by age is unverified.The five sources of the abuse trail:
Cf-Connecting-IpandCf-Rayheaders (that the tunnel passes them on is unverified). Setfields.queryParameters.defaultMode: drop, because the default is keep and Keycloak's?code=would be logged. Headers are dropped by default (doc).sub.Fleet-wide rule (it lives in quality, not in hal code):
sub, asdocs/people.mdalready does.4. Recommendation: (C) through a (B) prefix, ending as (D) in substance
Why the complexity earns its keep:
suspend. Rollback becomesgit revert, which apps lack today (#660). Hardening becomes PSS; isolated networks become NetworkPolicy.What would change it:
In each case: stay on (B). A GHCR price change only changes the registry.
Slices. Each lists what is added, what of hal is deleted, and how it is proven without touching the live system.
built_image.py,run.build, the build branch ofapps.pyandapp_compose.py,appkit_step.py;legal_step.pybecomes a CI step;deploy_checkout.py; songmaker'sauto-deploy.shgh attestation verify; a pull with the read-only token insidescripts/system-proof/; the Keycloak image starts in CIbootstrap.sh: mise pinned by sha256,mise install --locked,mise run setup(Ansible).mise.tomlandmise.lockwith kubectl, flux, helm, sops, age, opentofu, gh, k3d, kubeconform, restic.workstation/setup.py,setup_commands.py,path_link.py; the units move into Ansiblescripts/system-proof/(Ubuntu 26.04 container with systemd) runsbootstrap.shin CIsops -dwrites today's file paths; a small check script that also watches token expirysecrets_setup.py,hidden_paste.py,secrets_create.py, most ofsecrets_view; closes "~/.secretsin no backup"sha256sumof decrypted and current filesplatform_check,report,mail,alarm,alarm_commands;check.pyshrinks?code=secretand an Authorization header and asserts the stored line contains neithercluster/pc, plus an infrastructure layer. Traefik runs withhostNetwork: trueand an entry point on127.0.0.1:<port>(chart, entry points). Also a Retain StorageClass, k8up, Gatus, Loki, Grafana, and an Ansible role for k3s with--disable=traefik --disable=servicelb(doc) plus a firewall.kubeconformandkustomize buildon every PR; a k3d job (k3d) installs Flux and waits until everything is ready; a VM with both Docker and k3srestricted: read-only root, drop ALL, memoryemptyDir, PVCs for the CLI logins. The egress gateway gets a pinned ClusterIP plushostAliases, withdnsPolicy: None. NetworkPolicies are default deny, enforced by k3s's embedded kube-router netpol (doc). step-ca and traefik-mcp stay, since cert-manager's CA issuer is "for … demos or for advanced users" (doc).probe/agent-serviceported to the VM: no DNS, no direct egress, no foreign SNI, no reach into other namespaces, the smoke green. Red: stop at (B).readypathslocalbackend (mountPath, API); a restore-test CronJob (verify.pylogic):latestjoin #4; presentator's SQLite PVC; closes gap 35nvidia-smiin a pod, one song generatedhal offline, final backups,docker compose -p hal stopand the same for each app project,mise run k3s:install,flux bootstrap github --owner=overnightworks --repository=hal --branch=main --path=cluster/pc(doc), restore, check on loopback,systemctl start cloudflared/usr/local/bin/k3s-uninstall.sh("deletes the local cluster datastore", doc), thendocker compose -p hal start; the old volumes are untouched until S11docs/stack.md, most ofdocs/architecture.md§8S1 image pipeline in detail:
overnightworks/quality/.github/workflows/image.yml, is pinned by SHA, and each repo'srelease.ymlcalls it onv*tags after CI is green. A shared reusable workflow is GitHub's documented route to SLSA v1.0 Build Level 3 (attestations).docker/build-push-actionwithpush: true,provenance: mode=maxandsbom: true. Build args are visible in the provenance, so secrets go in as mounts (Docker). Thenactions/attestwithpush-to-registry: trueand the permissionsid-token: write,attestations: write,packages: write.read:packagesand nothing more (GHCR). Set an expiry and let the secret check watch it.docker login ghcr.io./etc/rancher/k3s/registries.yaml, which needs a restart (doc).ghcr.io/overnightworks/<app>:<tag>@sha256:<digest>.gh attestation verify oci://… -R overnightworks/<app>.kc.sh buildand run withstart --optimized(containers); this fixes #908.permissions:; base images pinned and bumped by a bot; no secrets in build args.Own-domain jobs and their homes (the "no hal" question):
legal_section.pyandlegal_pages.py; output is an nginx imagemise run people -- export <name>playbook.yml)No single tool does these jobs. They are about 2.5–3k lines either way. Keep them in one small package with one test suite rather than six scripts.
5. Open items
git tagas a mise task..github/dependabot.ymlignoring semver-minor, which is Postgres's in-series step. Fix the bot rule./var/log/podsage rule.6. "Welchen Befehl führe ich aus?"
Ship an app version on the target:
With auto-merge on green ruled under #1016, the first line alone ships. A config change is
gh pr create --fill && gh pr merge --squash --auto.A fresh PC:
git clone https://github.com/overnightworks/hal ~/git/hal && ~/git/hal/bootstrap.sh.Unverified:
Cf-*headers;FlexOr2 commentedon Nov 6, 2026
Evidence: architecture analysis by Codex gpt-6-astra at max effort, 06.11.2026. It was run read-only against hal at bac221a1, from the same brief, and is posted unchanged. It recommends Compose plus maintained tools. The operator chose k3s plus Flux. Its slices 0 to 3 (images, bootstrap, secrets, and logs and checks) appear in this epic as P1, P2, P4 and P5.
Choose B: keep Compose, replace hal’s general deployment machinery with maintained tools, and keep hal only for fleet-specific policy.
Hal has not rebuilt k3s. It has built a substantial deployment manager around Compose. Much of that manager can disappear without introducing Kubernetes. K3s becomes the stronger choice when you need continuous reconciliation, placement across machines, or rolling deployments—not simply because hal has become large.
The PC can accommodate k3s. The harder cost is migrating storage, network isolation, emergency controls and operational knowledge. Installing k3s does not resolve those boundaries automatically.
Evidence is from
bac221a130d02c055c2fff4ba08580d4d3c0c552. No files changed, tests ran, containers started, or live systems were inspected. Shell access to GitHub failed; issue bodies and closed rulings remain unverified. The separate web tool could retrieve public documentation, so linked external claims below were re-read on 6 November 2026. Other repositories were read only through localorigin/main, which may be stale. Proposed commands and replacement configurations have not been executed.1. Inventory. The Python inventory is 118 files, 28,617 physical lines, 23,369 nonblank lines. The table assigns every
src/hal/**/*.pyfile exactly once. Counts include comments and docstrings.“Mixed” means a standard tool owns the mechanism, while a specific fleet requirement still needs custom policy. Replacement recommendations are follow-up architecture work requiring plan review, not permission to remove existing protections.
stack/catalog.py,app_contract.py,releases.py;catalog.apps,app_contract.checkapply,apps,app_compose,checkout,deploy_checkout,compose,core,core_phase,unit_plans,service_recreate,built_image,environment,plan,pre_apply,setup, locks and waitingcore.dependency_order,Compose.config_hashes,AppsPlan.run,apply._plan.emergency,barrier,switches,switch_events,tunnel;stop_unit,Switches,Barrierstack/cli.py,plan_rows.py,result_rows.py,terminal.pystack/realm/;plan_realm,Observer,observestack/external/;apply_plan,check_hosts,CloudflareRead,tofulegal_pages,legal_section,legal_step,appkit_step;plan_legal,plan_appkitlitellm_keys,litellm_admin,api_limits, rootlitellm_expiry,expire_commandsKeysPlanconfigures native APIs.litellm_expiry.statementdirectly depends on LiteLLM’s database schema; that maintenance obligation survives k3s.backup/,backup_commands.py;runner.backup,restore_test,live.restore_live,sqlite_copy.copy/loadworkstation/setup,system_step, callback,path_link,user_units,jobs,tunnel_config,setup_commandssecrets_view,secrets_setup,secrets_create, manifest fields, ports and command/output modulessecrets_viewprimarily checks presence and metadata;secrets_setupprovisions missing entries.alarm,mail,report, layout and root command modulesexposure/,exposure_rows.py;Problem,checkrename_commands,rename_rows,migrate_commands,stack/project_move,stack/updaterename_commands.make_planhandles real local relocation details, not a Kubernetes responsibility. Source-checkout deployment creates part of this work.output/, root CLI/config/grammar/version and remaining door modulesThe principal replacement capabilities are documented directly: Compose already recreates changed services and offers health waiting; mise now documents Compose project state, model-hash comparison and one-shot services. Ansible also has a maintained Compose module, but these should not become two owners of the same deployment. Compose, mise Compose projects, Ansible Compose module.
Other areas must not disappear from the accounting:
compose.yaml,fleet.yaml, both Traefik configurationsexternal/*.tftunnel/,mcp/,dynamic/keycloak/GoLivealone is 937 lines. Config-cli and Keycloak remain the mechanism; bootstrap policy needs a separate reduction pass.step-ca/StepCaInitwrapper. Keep CA trust/material ownership; simplify orchestration around the maintained CA.agent-service/agent-runtime; this tree is not another container scheduler.legal/docs/people.md, not an existing capability k3s could replace.tests/,probe/,scripts/system-proof/The code already has structural separation: pyproject.toml declares import-layer and independence contracts. That is useful, but module separation does not decide whether hal should own the underlying job.
2. The boundary. Hal should own fleet meaning and exceptional policy, then hand ordinary configuration and release artifacts to maintained tools. It should not maintain a parallel container-reconciliation engine, dependency scheduler or general deployment-state database.
The handoff should be a reviewed release bundle: OCI image digests, native Compose definitions, configuration and legal assets, selected by the fleet declaration. Credentials travel separately through the existing secret boundary. On a future Kubernetes target, the deployment handoff becomes native manifests consumed by Flux; the apps’ images and domain contracts remain.
flowchart TD A["App repositories<br/>images and Compose release bundles"] F["Fleet repository<br/>fleet.yaml, settings, realm and policy"] H["Small hal<br/>validate fleet relationships and build legal assets"] B["Reviewed deployment artifacts<br/>native configuration and fixed releases"] O["Operator<br/>mise task"] T["Maintained execution tools<br/>Compose, OpenTofu, Ansible, restic"] R["Running services and durable data"] P["Explicit domain operations<br/>people, budgets, restore proof, emergency priority"] A --> B F --> H H --> B O --> T B --> T T --> R P --> T P --> RProposed ownership boundary for the operator. Derived from the existing catalog, Compose, realm, backup and domain modules; it is not a picture of today’s implementation. Mermaid rendering was not verified.
Two details prevent this from becoming the same system under another name:
docs/architecture.md, “The deployed record,” and implemented byapps.DeploymentRecord.There is concrete coupling to remove. apply._apply_changes writes legal/appkit files before deploying apps. Consequently, a failed app update can leave its new legal text served with the old app. Packaging and promoting release artifacts gives that association an explicit owner.
3. The real alternatives. Compose is a supported production deployment option; Kubernetes is not a prerequisite for professional operation. Docker’s production guidance.
workstation/jobs.py. Actual CPU/RSS unmeasured. Current app builds also run on this PC throughAppCompose.build.Sources for C: k3s requirements, Flux deployment model, k3s networking. Sources for E: Coolify installation requirements, Compose support.
For B, mise’s newer documentation also covers machine bootstrap. I would initially retain the existing Ansible host role because its specific account, tunnel and logging policies already have repository proofs. Mise replaces its Python installation/presentation wrapper. A later replacement of that role must demonstrate equivalent behaviour; mise and Ansible must never manage the same host resource concurrently. mise bootstrap; local evidence:
workstation/system/playbook.yml,scripts/system-proof/.Day-to-day operation. Commands marked target are interfaces to implement and prove, not commands available today. Release/configuration changes first pass through the deployment repository’s review.
hal apply. Update hal’s own checkout first withhal updatewhen required.hal update;hal apply. Local[api]settings follow the existing separate path.hal update;hal apply.hal stop keycloak,hal restore keycloak --live --snapshot ID, then the matching image/configuration andhal start keycloak.hal stop hopinmise run deploy -- hopin.mise run publish.hal stop hopin, with its policy guard shared by the new deployment path.flux reconcile kustomization hopin --with-source.mise run stop -- hopin, closing ingress, suspending the relevant reconciler and stopping workloads, with observed confirmation.mise run deploy -- hopin.mise run restore -- keycloak ID, invoking the retained domain recovery command.mise run stop -- hopin, invoking the retained emergency policy.A is grounded in
docs/stack.md,docs/backup.md,apps.AppsPlanandemergency.stop_unit. Flux’s immediate reconciliation command is documented here.For C, suspending Flux alone does not stop an app. Scaling it down alone does not establish a durable stop against another controller. Parent configuration can also overwrite a suspension declared differently in Git. The emergency procedure must establish and prove the complete policy. Flux suspension semantics.
Coolify explicitly says rollback changes the application image, not databases or persistent storage. Its application-level rolling updates exclude Compose applications. Rollback, rolling updates.
Stateful services and isolation are the migration cost:
backup/apps.toml,pre_apply.Backupandlive.restore_liveencode this today. A realm import is not a full user/database recovery. The official Kubernetes realm-import resource only creates realms; it does not update existing ones. Keycloak operator.backup/apps.tomldescribes Postgres plus audio for songmaker and SQLite for presentator. A Git revert cannot reverse data changes. PostgreSQL dumps are internally consistent, but that does not synchronize separately copied files. PostgreSQL backup documentation.backup/verify.pychecks counts and referenced-file presence; it does not prove that database and files represent one transactional moment. #678 remains necessary.presentator@origin/main:compose.yaml, servicebuild-worker, declaresnetwork_mode: noneand a shared Unix socket. Putting web and worker into one Kubernetes Pod would share their network namespace. A faithful port therefore requires a deliberate containment design. Kubernetes Pods.compose.yamlandspeech-runtime@origin/main:compose.yamluse separate internal networks. Kubernetes requires corresponding policies and verified enforcement; namespace separation alone is insufficient. NetworkPolicy.docs/backup.md, “Restore LiteLLM,” explicitly says restoring loses spend recorded after the snapshot. Kubernetes does not change that consequence.If the operator installed k3s today, the official command would install a cluster:
curl -sfL https://get.k3s.io | sh -It would migrate none of these services or data. It is not the recommended deployment command, and no k3s migration command exists in
hal.cli.COMMANDS. Official k3s quick start.For D, the supposedly “hal-only” jobs need this accounting:
docs/people.md. This is substantive custom work, still unbuilt.qualityand its scanner engines; native Git hooks invoke it. Evidence:workstation/hooks/commit-hook,quality@origin/main:README.md.4. Recommendation. Adopt B as a deliberate reduction, rather than continuing A while calling it temporary.
The boundary is the reason for the choice:
The six broad buckets covering deployment, deployment presentation, setup, daily checks, migrations/update and shared CLI contain 13,138 physical lines—46% of
src/hal. This is an opportunity inventory, not a promised deletion count: some policy survives, and replacement configuration also costs maintenance. There is further simplification potential in backup and external wrappers. Do not preserve code to protect an investment; do not rewrite it line-for-line into Ansible or mise tasks either.I would change the recommendation when any of these becomes concrete:
The existing “about ten apps” trigger remains a useful review point, not a technical capacity limit.
The ordered plan is:
scripts/bootstrap: verified pinned mise installation, locked tools, existing Ansible host role, credentials onboarding and resumable setup. Keep host resources solely owned by Ansible.workstation/setup.py; unnecessary Ansible output parsing/callback; custom unit writing once standard configuration owns it.scripts/system-proof/.built_image,deploy_checkout,checkout,releasespaths after their final caller moves.fleet.yaml. Let mise/Compose perform model reconciliation. Integrate the existing emergency policy as a narrow guard. Start with a disposable app, then hopin.corehash bookkeeping, recreation planning, polling and corresponding plan/result presentation for migrated callers. Remove the oldapplydispatcher after the last caller moves.backup/restic.py,runner.pyand timer generation. Delete only after equivalence, not merely because a backup exits zero.apply.The host remains usable after an interrupted provisioning run; it must report partial completion and resume. A collection of tools cannot supply an atomic transaction across Cloudflare, Keycloak, databases and containers. The plan must make recovery explicit rather than imply that one command creates atomicity.
Existing safety distinctions are worth preserving:
5. Open items. All 70 supplied titles are accounted for below. These are proposed dispositions from titles and code, not closure decisions; the bodies were unreachable.
service_recreate/pre_applyalready implement substantial backup gating.fleet.yamland the speech-runtime deployment definition. The body may contain remaining work.pyproject.tomlalready pins appkit 0.14.0. Immutable theme packaging addresses #908’s underlying coupling. Dependency-update automation should own #639.fleet.yaml, so #446 requires its body before judging what remains.6. “Welchen Befehl führe ich aus?” For the recommended target, CI publishes the release, and a reviewed promotion selects it in the deployment repository. After merging that promotion:
deployis a proposed repository task. It prepares the selected immutable bundle, applies the narrow safety policy, and calls the maintained deployment tool. It does not implement another reconciliation engine.On a fresh supported PC, from the repository checkout:
That is also proposed. It installs the pinned tools, configures the host, guides necessary account/secret setup and resumes after interruption.
Today’s existing command remains
hal apply. The target is complete only when the new commands are proven and the old deployment machinery is removed.FlexOr2 commentedon Nov 6, 2026
Evidence: the fleet heads' replies to the platform switch, 06.11.2026, as relayed to hal's head. Each head keeps its own items. This comment gathers what the platform has to plan for.
Local image sizes on the PC, 06.11.2026 (unpacked;
docker image inspect):Replies:
hal_sha.txt), so it needs the realm's new home; webauth opens that item when this repository's realm slice is cut. Libraries stay on git tags as webauth#543 ruled on 02.11.2026, because Dependabot fails on release-asset URLs.FlexOr2 commentedon Nov 6, 2026
Inputs for later slices, from the heads on 06.11.2026. Each one is named in its slice's body when that slice is cut.
image, and their image proofs stay as tests; songmaker'sbuild_images.shandcheck_pinned_images_match.pyleave at P8 and P11.git commitand merge commits for secrets, whichhal setupinstalls today (hal AGENTS.md; docs/home.md, „Was der Hook nicht garantiert“), or names its replacement. hal setup installs a commit-msg hook that refuses a commit without a body hal#1056 reopens if the bootstrap ships without it.[gate] legacyinquality/config.tomlgoes back to"fail", as aco, quality and hal have it. It is"report"only because an unanalysed main makes"fail"unjudgeable (qualitydocs/sonar.md). This lands as a fix lane right after P0.checksjob pinscheck-jsonschemabut not its dependencies. When P3 extendschecks, its tools get a lock or a pinned constraint (review of platform#3).--keep-weekly 4 --keep-monthly 3 --keep-tag pre-apply, pre-apply snapshots--keep-last 10, anything older than 90 days forgotten by ID (halsrc/hal/backup/restic.py). presentator dates a data-upgrade removal by it: the last presentator backup from before 2026-11-06 stays restorable until the first forget past 2027-02-04. A shorter policy, or dropping the existing snapshots, goes to presentator's head first (hal's head).agentsclient, themcp-<app>scopes and the empty/apps/<app>/mcpgroups. Ruled sentences (agent-runtime#672 and hal#903): one pre-registered public client with PKCE S256, no secret, DCR off, consent required, no refresh tokens; an audience mapper per app,https://<app>.<EDGE_DOMAIN>/mcp; the realm's 300-second access token lifespan with no per-client override; no role claim in the token; the groups stay empty until each app's privacy notice names the person's own agent. The operator confirmed MCP stays on 06.11.2026 in agent-runtime's session („ok mcp“). Left of HAL-1:/mcpand the well-known path on each app's host (P6 routing), and two questions from agent-runtime#672: how Codex behaves as an MCP client, and where Claude Code stores its MCP tokens on Linux. agent-runtime's head repoints #672's HAL-1 row once P7 has its number./var/log/podsrotate by size only (containerLogMaxSize,containerLogMaxFiles), so a quiet long-lived pod can keep lines past 30 days on the host. P5 says what deletes them.daemon.json,~/.local/bin/mise, the globalcore.hooksPathand hal's journal timer each have two owners on the day of the switch, and whether the live PC runs ufw is read only then, with the operator's leave.The README opens with the guiding sentence of platform#1
Add the repository skeleton and the fleet's quality gate (#3)
The gate fails on legacy Sonar findings now that main is analysed (#7)
Three unused tokens leave hal's secret list (#1091)
FlexOr2 commentedon Nov 6, 2026
The operator's rulings of 06.11.2026 in platform's head tab, on the questions from the plan for P2, P3, P4 and #4. The head put each line to him in German with options and a recommendation; his words follow each line. Each line moves into its slice's body when that slice is cut.
.sops.yaml, the recovery key on paper or USB) is required before the cutover, P10. „ja passt alles!“, answering the head's proposal „jetzt 1a, und 1b ist Pflicht vor der Umstellung (P10)“.platform-secrets: quality's hygiene with the secret scan plus a check that every file is encrypted; no Sonar and no CodeQL, which on a private repository would cost money. „ja passt alles!“read:packagesand an expiry, watched by P9's expiry check. „ja passt alles!“clusters/,infrastructure/,apps/andhost/at the root, as four declared deviations from the root-placement rule for this repository. „ja passt alles!“The head decided these without a question: no Ansible, using
mise bootstrapwith a reopen trigger;scripts/bootstrap_pc.shin place of./bootstrap.sh, since quality's script-name and root rules forbid a script at the root; bump pull requests merged by hand until P10, re-asked at P10.The repository is licensed under MIT (#9)
43 remaining items
hal setup installs quality v0.10.0 and its scanners; the hook runs ch…
Stream every chat completion and read each round whole before it is j…
The delete test cites only the sentences it proves
docs/github-settings.md holds the six ruled sentences on GitHub setti…
docs/VISION.md states what the platform promises, for whom, and what …
A request waits for a running key-set refresh when no current key set…
An organisation ruleset blocks the merge of a pull request that bring…
webauth ships its in-memory stores as a public adapter (#699)
One standard wiring installs webauth with its defaults and the app's …