Epic: aspect inventory, separation-of-concerns audit, and the prose/ad-hoc debt #9
Labels
No labels
blocked
conflicted
curated
in-progress
in-review
needs-decision
No milestone
No project
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set
Reference
larandar/flaky-mesh#9
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Stable ID: FM-ASPECT-AUDIT
Source: full-tree audit of
modules/andoutputs/hosts/, 2026-08-04Source status: new finding set, none of it previously tracked
Disposition: open epic — inventory published, twelve child items to split out
Blocked by: nothing
Why
Two questions, asked together: what is actually in the aspect registry, and
where does it fail to hold. The census is authoritative —
nix eval --json .#aspects.graph, not grep. Every finding below was verified against the tree,not inferred from a comment.
Inventory
residual)residualmodules/+outputs/Kinds: 7 apps, 4 contracts, 2 hardware, 4 implementations, 2 integrations,
6 policies, 3 services, 4 systems, 1 user.
Host closures, before the residual set is added to each: bifrost 6,
fenrisulfr 6, heimdallr 6, modron 6, tamiel 8, odin 32.
Full census — every record with kind, classes, requires/provides/owns, source
file, LOC and comment ratio, plus the 18 residual and the schema utilisation
table — is FM-AUDIT-12 below, destined for the wiki.
FM-AUDIT-01 —
nix flake checkis red on the committed treenix build .#checks.x86_64-linux.aspects-shadowfails. 13 of the 14 aspectchecks pass; this one does not.
implementations/forgejo-agentsentered odin's roots(
modules/aspects/hosts.nix:107) without regeneratingtests/aspects/fixtures/shadow-hosts.json:The check did its job — a closure moved and the pin caught it. What is broken
is that it was left red. A permanently-red check is a check nobody reads,
which is the argument
modules/aspects/checks.nixmakes against itself.Distinct from #1 (FM-B3-PARITY): that one is
parity-*.json, this one isshadow-hosts.jsonand needs no deploy — regenerate and commit.FM-AUDIT-02 — the registry governs 33 of 51 modules
modules/hosts.nix:52-56importsresidualIds class ++ closure. A host'sroot list decides 33 modules and is silent about the other 18 — 2 069 lines,
26% of the module tree, and it includes every service the fleet actually runs:
services/forgejo,services/matrix,services/librefang,services/syncthing,services/perses,policies/vaults,policies/base,policies/identity,integrations/mesh-dns.tests/aspects/fixtures/warning-budget.jsonpins this at{"residual": 18}.A budget is a ratchet only if something lowers it; nothing has.
The five servers resolve to six aspects each. Their real configuration lives
in the residual set and in their host file.
aspects-isolationguards exactlyone aspect on exactly one host (
services/victoriaon bifrost) — a proof ofconcept, not a fleet property, and it cannot become one while 18 modules
bypass selection by construction.
Decide per module: annotate or delete, lowering the budget each time. Name the
date
residualmust be 0, or state that broad injection is the design andretire the migration framing.
FM-AUDIT-03 —
contracts/routesis bypassed by half its usersmesh.routes?implementations/caddyintegrations/victoria-dashboardsintegrations/vault-dashboardsintegrations/mesh-whoami:297implementations/hermes:743integrations/mesh-librefang:24outputs/hosts/heimdallr:132outputs/hosts/bifrost:154Four of eight bypass the contract; three of those run the other web server;
two are host files.
bifrost also hand-rolls what
implementations/caddyalready does, and gets itwrong in the exact way that aspect documents fixing:
The aspect explains that the bare-string form leaves
/var/lib/caddyroot-owned where impermanence creates the bind mount first, and Caddy then
cannot write its ACME account. bifrost is latent, not broken — its directory
predates the entry — but a cold rebuild walks into the documented failure. The
aspect's fix does not apply because bifrost never sets
mesh.implementations.caddy.enable.Either every vhost goes through
contracts/routesand one renderer, or thecontract is deleted as aspirational. The current state costs the abstraction's
maintenance and delivers none of its guarantee.
Blocked by: FM-AUDIT-02 —
contracts/routesandimplementations/caddyareboth residual and cannot be required until they have records.
FM-AUDIT-04 —
implementations/hermesis odin wearing an aspect's clothesletbindings, not options. Any second host selectingcontracts/agent-runtimedecrypts odin's secrets file.hermesState = "/data/odin"is hardcoded the same way;userandlinkUsersdefault tolarandar.implementations/forgejo-agentshas the same coupling, but atleast made it an option.
The record's header argues it bundles five things — Hermes, Modron, Firecrawl,
ByteRover, Ollama — "because they are only correct together", and that
splitting them would push coupling into the host root. 794 lines, one enable
flag. Its own history refutes it: opencode became the sixth limb and went into
outputs/hosts/odin/configuration.nix:194as a raw systemd unit instead. Whenthe next thing to add goes in the host file rather than the record, the record
is not the right home.
Parameterise the secrets paths and the state root, or rename it to what it is
— an odin-only assembly — and stop it answering
contracts/agent-runtime.FM-AUDIT-05 — heimdallr restates
policies/identityas four literalspolicies/identityexists so that "moving identity is one edit here". Thehost that runs identity does not read it:
outputs/hosts/heimdallr/configuration.nix:67firewall9091mesh.identity.port:94server.addresstcp://0.0.0.0:9091mesh.identity.port:116authelia_urlhttps://auth.et0.pwmesh.identity.portalUrl:114session.cookies.domainet0.pwmesh.identity.cookieDomainOnly
protectedDomainsis read from the policy (:23).Change
mesh.identity.portand every consumer follows it while Authelia keepslistening on 9091 and the firewall keeps opening 9091. Nothing fails at eval;
forward-auth just stops answering.
FM-AUDIT-06 — five host files grant blanket passwordless sudo
policies/sudois careful. It grants NOPASSWD to podman and to exactly thesystemctl lifecycle verbs, with its own comment saying why: "unrestricted
systemctl (edit, set-property, link) would be passwordless root."
That aspect is in one host's roots: odin. The five servers do not select it.
Each instead writes, verbatim:
bifrost:85, fenrisulfr:77, heimdallr:56, modron:55, tamiel:20.
So the fleet's considered sudo policy protects the workstation, and every
server — including the one holding fleet identity secrets and the one holding
the vault NFS mounts — has blanket passwordless root for wheel. Five identical
lines is the tell: this is a fleet policy that was never written as one.
Decide the server posture deliberately, put it in
policies/sudoor asibling, and add it to
commoninmodules/aspects/hosts.nix.FM-AUDIT-07 — odin's host file carries a service, a scrape config, a resolver and 90 lines of WireGuard
The file opens by defining what a host file is: "facts that are nobody else's
business … and option values for aspects the host record selected." Then:
:194-206— a fullsystemd.user.services.opencodeunit: package pulledstraight from
inputs.llm-agents, port 4999 hardcoded, andEnvironmentFile = /run/secrets/rendered/profile.omo.env— an undeclareddependency on
implementations/forgejo-agentsrendering theomopersona.apps/codingalready installsllmAgents.opencode; this is a secondreference to the same package from a different layer.
:211-216— rawservices.victoriametrics.prometheusConfig.scrape_configs.Skipping
integrations/victoria-scrapeis correct (it is tamiel's fleetedge); the answer is a scrape-self aspect, not raw upstream options.
:222-237—mesh.services.perses, with the comment conceding it: "Persesis residual (no catalog record yet), so the old enable flag is the honest
surface."
:59-149— 90 lines ofnetworking.networkmanager.ensureProfilesrestatingmesh membership
contracts/mesh-nodesalready holds: both addresses, thehub's public key, six allowed-ip ranges, both resolver addresses. The
comment says the addressing "MUST match the hub's peer entry" — a second
source of truth admitting it is one.
:32-33—services.resolved.enableandnetworking.networkmanager.dns,the client half of
integrations/mesh-dns, by hand.:23—time.timeZone, alreadymkDefault-ed inpolicies/base:25. Sameduplicate at fenrisulfr:11.
Roughly 130 of 242 lines are not host facts.
The NetworkManager profile has a real justification — rootless toggling, and
the wg0-adoption incident. That argues for a NetworkManager provider for
policies/mesh, selected per host, not for hand-written config diffed againstthe registry by eye.
FM-AUDIT-08 — agent persona content lives in host files, in two shapes
Prompt text is content. It is currently topology:
outputs/hosts/fenrisulfr/configuration.nix:114-192— two full LLM systemprompts (freki, geri) inside
mesh.services.librefang.hands. Thechannel_sendparagraph is copy-pasted verbatim into both, as is thecomment above each explaining the 2026-08-01 duplicate-reply flail.
outputs/hosts/odin/brain.nix— a system prompt plus display settings,delegation models and council profiles for Hermes.
Neither has an aspect. Editing a persona means editing a machine; reviewing a
machine means reading prompts. The near-duplicate wolf prompts are what always
happens when content has no home — they will drift.
Related: #5 (restore chat presence for freki and geri) touches the same
declarations.
FM-AUDIT-09 — a quarter of the nix tree is comment prose
2 490 comment-only lines out of 10 044 (24.8%). Worst ratios, files over 40
lines:
Most is not "why this line" — it is history, and history has a home already:
owner call,owner directive,2026-08-01)mirror. It was considered and rejected…", "No forge here"; bifrost's Buzz
paragraph; modron's "Why the name changed" and "What was stripped"
mesh-dns.nixopens with ~70 lines under headings — "Why not /etc/hosts","What used to be here, and why it changed" — before the first line of code
that is not in this repo
It also rots silently:
modron:59still says topology "lives in policies/mesh"after it moved to
contracts/mesh-nodes.Rule to adopt: a comment explains the line above it, or it goes to the wiki.
Rationale worth keeping — the measured 1280 MTU, the
stable-privacyfix, thedns-prioritytrade, the Caddyfile backtick-quoting trap, the Authelia OIDCbootstrap trap — becomes wiki pages linked from a one-line comment.
FM-AUDIT-10 — eight of the record schema's 21 fields have no user
modules/aspects/_registrar.nix:65-151. Zero uses anywhere in the catalog:traits,references,instantiable,publishes,visibility,stability,defaultImplementation,ownerWrites. One use each:params,configures,tags. Two:optional.traitsis the expensive one — a 32-line submodule type (kind enum, peer,direction, reason, tcp/udp port lists, path, route), a per-host
allow/uncheckedoverride block inhostType, and atraits = {}line inthe graph builder. Declared by no record, read by no check. Its own comment
concedes it: "the checks that consume it land post-Milestone-C."
Delete what has no user. Each is re-addable in the commit that needs it, and
every one is currently something a reader must understand before concluding it
does nothing.
FM-AUDIT-11 —
lib/forgejo.nustill ships a one-shot migration reading gitignoredplans/AGENTS.md: "
just forgejo …is now admin work only … It does not read orwrite issues." The file disagrees. Roughly 340 of 805 lines (450-790) are the
issue/wiki sync path:
issue-records,backlog-items,backlog-preview,create-issue,sync-issues,ensure-chain-dependencies,wiki-sources,wiki-pages,sync-wiki,verify-sync,sync. Two are still live recipes(
forgejo.just:61,forgejo.just:69).It also depends on files the repo forbids tracking:
Tracked code reading untracked paths works on the one machine that has them
and errors everywhere else — and
syncis a[confirm]recipe that writesissues, so the failure mode is a half-applied write against this tracker.
Delete the migration half; keep
probe,contract*,search-repos,credential-status,auth,mint,whoami.Blocked by: #7 — the same
plans/deletion set.FM-AUDIT-12 — publish the aspect census as a wiki page
The inventory has no home.
flake.aspects.graphis evaluable andnix run .#aspects-graphrenders it, but neither answers "what is in thisfleet" for someone who has not already loaded the model.
Content is ready: 33 records with kind/classes/requires/provides/owns/LOC, the
18 residual, per-host closures, schema utilisation, check status.
Blocked by: FM-AUDIT-02, FM-AUDIT-10 — the numbers move when those land.
Acceptance
Blocked byrow,or explicitly closed here as won't-do with the reason recorded.
just checkpasses in the devshell (FM-AUDIT-01 first — it is red today).residualintests/aspects/fixtures/warning-budget.jsonis lower than 18,or the migration framing is retired.
Provenance
Audit run 2026-08-04 by agent.odin against commit
fc3b751, withmodules/implementations/{forgejo-agents,hermes}.nix,outputs/hosts/odin/{configuration.nix,secrets.agents.yaml}dirty in theworking copy. Counts come from
nix eval --json .#aspects.graphand from linecounts over
modules/andoutputs/; check status from building each.#checks.x86_64-linux.aspects-*derivation.just checkis red #10implementations/hermesor rename it to an odin-only assembly #12policies/identityas four literals #13policies/sudocovers only odin #14traits#18lib/forgejo.nu#19contracts/routes, or delete the contract #20