HQ held only the to-be. Every reader had to already know the system the decisions were about, and an as-is claim had nowhere to live except inside an intention. Adds 02-DESIGN/00-as-is — eleven documents written from the implementation and the operational record, not from intent, including the parts nobody would choose again. The two existing designs move under 01-to-be. Layers are declared in frontmatter and never mix: a design that ships does not move, its as-is counterpart is written, and both stand. Back-fills adr/0001-0014 for decisions taken in implementation and never recorded — the broker, the module abstraction, the mesh database, managed files, provisioning, migrations, the workspace removal, failing loudly, the constitution, application placement, linking, the employee model, the artifact, the three silos. Each marked reconstructed, dated from the history, and citing the evidence it was recovered from. The two existing records renumber to 0015 and 0016 so the ledger runs oldest first; 0017 extends 0015 to modules outside the core, principle only — the domain list is deliberately not invented here. how-we-build.md becomes the source of the mesh constitution, with a sync playbook, so the enforced copy stops being the only one that is true. Process becomes explicit: five playbooks, eight thin skills that defer to them, a repository map, and AGENTS.md with CLAUDE.md as its include. The five Observations become 04-ISSUES 001-005 where they can be owned and closed. 006 is new and uncomfortable: HQ is not indexed into the knowledge base. That claim is what decision 27 rests on, it was never checked, and the README now says so instead of repeating it. Also corrects the ADR index into something generated, the "02-DESIGN is empty" claim, the VISION.md pointer that did not survive the repo split, and a note asserting the symlink rule was contradicted — it was a misreading; the rule forbids hand-made links, the installer links by design.
189 lines
9.7 KiB
Markdown
189 lines
9.7 KiB
Markdown
---
|
|
status: accepted
|
|
date: 2026-08-22
|
|
deciders: jochen
|
|
reconstructed: false
|
|
---
|
|
|
|
# 15. The mesh brokers capabilities; nodes host; agents think
|
|
|
|
## Context
|
|
|
|
HAL has 124 modules. The count is not the problem — it is the symptom. Modules are split
|
|
because splitting is the only granularity the platform offers, and domains are merged
|
|
because a shared database is the only integration it offers. Both pressures push in the
|
|
same direction: boundaries end up drawn by deployment accident rather than by domain.
|
|
|
|
Three observations establish the state.
|
|
|
|
**The word "agent" means two different things.** The original design treated a node as an
|
|
agent with thinking abilities. Later, noxflow implemented agents as employees with
|
|
skills, workspaces and tasks. Both survive. The collision is visible in the data — there
|
|
are **two agent rows per node**:
|
|
|
|
| agent | skills |
|
|
|---|---|
|
|
| one named after the node | `{deploy,verify,operate}` |
|
|
| one named `hal-<node>` | `{}` |
|
|
|
|
One carries the work; the other carries only identity, existing to hold a licence for
|
|
HAL's own sessions. The same split appears in the schema: `nodes.hal_claude_account` and
|
|
`agents.claude_account` are one fact in two tables in two databases, and what was
|
|
documented as "three licence touchpoints" is one concept modelled three times.
|
|
|
|
**Domains integrate by sharing a schema.** `noxflow` is 45 tables spanning five domains —
|
|
tasks (7), agents (13), knowledge (8), meetings (7), scheduling (2). Its original purpose
|
|
is 7 of 45. Because agent identity lives in noxflow's database, work that belongs
|
|
elsewhere must be implemented there: per-agent Claude credentials had to be written by
|
|
the noxflow runtime, even though node identity — the same kind of fact — lives in the
|
|
mesh registry.
|
|
|
|
**The core domain has no context to live in.** The mesh's distinguishing feature is that
|
|
a module declares `requires: postgres/database` and never learns where the database
|
|
lives, who owns the credential, or how it rotates. That is capability brokering, and it
|
|
is what makes this a mesh rather than four machines with a configuration manager. Yet the
|
|
logic implementing it sits in `modules/postgres/tools/index.ts` — a provider module,
|
|
where "the mesh brokers credentials" cannot be expressed. On 2026-08-22 three of its
|
|
invariants were found violated simultaneously (see Consequences).
|
|
|
|
## Considered Options
|
|
|
|
1. **Keep the current layout; fix bugs as they surface.** Rejected. The faults are not
|
|
independent. Every incident on 2026-08-22 — credentials written by the wrong module, a
|
|
dependency question answered wrongly twice, an invariant enforced nowhere — traced to
|
|
a boundary that was never stated. Fixing them individually leaves the generator intact.
|
|
|
|
2. **Merge aggressively into few large modules.** Fewer names, same problem: a shared
|
|
schema across domains is what produced noxflow, and doing it deliberately would
|
|
produce it again at larger scale.
|
|
|
|
3. **Decompose by bounded context, with the mesh as a broker.** Name contexts after their
|
|
aggregates, integrate through a published record rather than a shared schema, and let
|
|
deployment granularity be a feature-level concern rather than a reason to create a
|
|
module. **Adopted.**
|
|
|
|
## Decision
|
|
|
|
### The domain, in one sentence
|
|
|
|
**The mesh brokers capabilities. Nodes are places where work runs. Agents are personas
|
|
that think and act.** Everything else supports one of those three.
|
|
|
|
### Nodes and agents are decoupled
|
|
|
|
A node is a place where an agent can run — that is the entire relationship. There is no
|
|
resident agent, no node-owned identity, no ownership in either direction. Agents named
|
|
after a node remain, as **ordinary agents** that happen to hold infra skills.
|
|
|
|
Consequently `nodes.node_license` and `nodes.hal_claude_account` cease to exist: a node
|
|
does not authenticate to a model provider, agents do. The two agent rows per node merge.
|
|
|
|
### There is one kind of participant, and some are human
|
|
|
|
Human and non-human participants are both **agents**. Both hold identity and credentials;
|
|
both act, remember and coordinate. What differs is **modality** — how an agent acts:
|
|
|
|
| modality | credential is delivered to |
|
|
|---|---|
|
|
| spawned session | that agent's own config directory |
|
|
| shell or desktop | that agent's home on the node it acts from |
|
|
|
|
A node holds no licence. **An agent holds credentials, and delivery follows that agent's
|
|
node bindings and modality.** A human agent's grant arrives in the home directory of the
|
|
user it acts as, on the nodes it is bound to — the same rule that puts a spawned agent's
|
|
grant in its config directory, with a different target.
|
|
|
|
This requires one fact the mesh does not record today: which user, on which node, a given
|
|
human agent acts as. Adding it is what removes the node licence — the node is currently
|
|
standing in for an identity the mesh cannot name. It is also what makes a second human
|
|
agent require no new mechanism.
|
|
|
|
### Contexts
|
|
|
|
| context | aggregate | subdomain |
|
|
|---|---|---|
|
|
| `hal/mesh` | **Provision**, Node, Module — brokering and its bookkeeping | core |
|
|
| `hal/agents` | **Agent** — identity, licence, runs, memory, thoughts | core |
|
|
| `hal/work` | **Task** — workflows, bindings | core |
|
|
| `hal/stream` | **Thread** — mentions, messages, meetings, notifications | core |
|
|
| `hal/delivery` | **Pipeline** — jobs, artifacts, features | supporting |
|
|
| `hal/knowledge` | **Document** — spaces, revisions, review | supporting |
|
|
| `hal/ai` | **Licence** — provider grants and rotation | supporting |
|
|
| `hal/observability` | **Check** | supporting |
|
|
| `hal/config` | **Setting** — env, secrets, PKI | generic |
|
|
|
|
A *brain* — memory, thoughts, cognition — is a concept the Agent aggregate owns. It is
|
|
not a module. Anatomy makes attractive names and poor boundaries; today `hal/brain` names
|
|
infrastructure and `hal/cortex` describes itself as messaging while running nowhere.
|
|
|
|
### Provisioning is the core domain, not plumbing
|
|
|
|
`hal/mesh` is a broker; the registry is its bookkeeping. Its invariants are explicit and
|
|
owned:
|
|
|
|
- one rotation source per resource
|
|
- a credential change fans out to every consumer
|
|
- a consumer never holds a credential the provider does not know about
|
|
|
|
### Contexts integrate through the record, never a shared schema
|
|
|
|
`hal/stream` is the published language. A context publishes; it does not join across a
|
|
boundary. This is what dissolves "meetings" as a domain — a meeting is a thread, and a
|
|
notification is a mention not yet read.
|
|
|
|
### Third-party software leaves the repository
|
|
|
|
`plex`, `sonarr`, `postgres`, `verdaccio`, `docker-registry` and 88 others run **on** the
|
|
mesh; they are not **of** it. The pipeline and provisioning are deliberately
|
|
module-agnostic, so HAL's own modules dogfood exactly what external modules use — which
|
|
is what makes the separation safe rather than merely tidy.
|
|
|
|
## Consequences
|
|
|
|
**The invariants now have an owner, and were measurably unowned before.** On 2026-08-22,
|
|
`provision_ensure` — documented as "NEVER rotates an existing secret" — was found to mint
|
|
a new password on every adoption and update only the provider's row. `hal_notifications`
|
|
consumers on three nodes held dead credentials for two days; `hal_transcripts` had two
|
|
rows written 216 ms apart, so at most one could match the live role.
|
|
|
|
**noxflow dissolves.** `hal/work` inherits tasks and workflows — the concept it was built
|
|
for. Agents, knowledge, meetings and scheduling return to their contexts. The name goes
|
|
away.
|
|
|
|
**`hal/sdk` shrinks.** 155 files, 34,636 lines, containing code from every context —
|
|
including `workflow-engine.ts` and `task-commands.ts`, work-domain logic in the kernel
|
|
every module imports. Each landed there to avoid a cycle between modules that both needed
|
|
it; a domain module can only own its shared code once the domain has a module. Extraction
|
|
is therefore downstream of this decision, not independent of it.
|
|
|
|
**Two mechanisms are prerequisites, not follow-ups.**
|
|
|
|
- *Named features with per-node opt-in.* Without it, every independently deployable unit
|
|
inside a context becomes a module again and the count returns. `hal/claude-licences`
|
|
exists solely because one daemon must run on one node.
|
|
- *A local mesh in containers.* `dev_up` starts providers "via systemctl (same as
|
|
production)" — it borrows the host, and no mesh can be stood up locally. Everything that
|
|
manifests **between** nodes is therefore discoverable only in production, which is where
|
|
every fault of 2026-08-22 was found. A refactor of this size is otherwise unverifiable.
|
|
|
|
**Credential delivery becomes uniform.** Per-agent credential directories, built for
|
|
spawned sessions, extend to human agents unchanged. A file previously scoped to a node
|
|
becomes scoped to an agent — which would have prevented the class of failure where a
|
|
rotation reached one node of four while the mesh reported success.
|
|
|
|
**Migration is incremental and long.** Contexts can be extracted one at a time behind the
|
|
existing pipeline. Nothing here requires a flag day, and nothing here is cheap.
|
|
|
|
## References
|
|
|
|
- [`01-RESEARCH/001-module-domain-decomposition`](../01-RESEARCH/001-module-domain-decomposition/analysis.md)
|
|
— current-state evidence, table counts, open questions
|
|
- [`00-GENESIS/how-we-build.md`](../00-GENESIS/how-we-build.md) — naming and integration rules
|
|
- `modules/hal/sdk/src/feature-handlers/index.ts` — `FEATURE_HANDLERS`, the fixed handler
|
|
array that makes a feature a singleton per module
|
|
- `modules/postgres/tools/index.ts` — the adoption path that rotates a shared credential
|
|
- Mediahuis `papa-hq`, ADR 0009 *Composable, independently-shippable modules* — the
|
|
constraints that make a unit independently shippable, applicable unchanged to features
|
|
- impire.io / soulstream — *the record* as integration substrate, personas over services,
|
|
and "cheap awareness and expensive thinking"
|