papa-hq reads 01 research -> 03 decision -> 02 design. The order is a scar, not a choice: 02-DESIGN existed from its initial commit, and when adr/ was finally promoted on 2026-07-13 it took the next free number rather than its place in the sequence. By then design was too settled to renumber. hal-hq was three commits old, so it is not. adr/ becomes 02-DECISIONS and 02-DESIGN becomes 03-DESIGN, and following the folder numbers now walks the process in the order it happens: research produces a decision, the decision authorises a design. 00-GENESIS becomes 00-META, matching papa's rename from the same restructure. Every path reference rewritten across documents, frontmatter, playbooks and skills. All links resolve; all 58 frontmatter blocks parse and their path fields still point at files that exist.
60 lines
2.9 KiB
Markdown
60 lines
2.9 KiB
Markdown
---
|
|
status: accepted
|
|
date: 2026-07-10
|
|
deciders: jochen
|
|
reconstructed: true
|
|
---
|
|
|
|
# 11. The installer owns linking; nothing else creates a symlink
|
|
|
|
> Reconstructed after the fact from the evidence cited below. The incident that earned the rule
|
|
> predates the record, and its date is not established here.
|
|
|
|
## Context
|
|
|
|
A service's definition lives in the module catalogue; its runtime directory and persistent data
|
|
live outside it. The mesh connects the two by linking the definition into the runtime location
|
|
— deliberately, so that runtime state and source stay separate while the running service reads
|
|
a current definition.
|
|
|
|
A link is also the easiest thing in the world to create by hand while fixing something, and a
|
|
container engine resolves a bind mount through it. A hand-made link pointed a volume somewhere
|
|
it should not have, and **production data was lost**.
|
|
|
|
## Considered options
|
|
|
|
1. **Copy instead of linking.** Rejected. A copy goes stale silently, which trades data loss
|
|
for a service running a definition nobody can find.
|
|
2. **Allow links, document the hazard.** Rejected. The hazard is not knowable at the moment of
|
|
the mistake — the link looks right and the resolution happens inside the container engine.
|
|
3. **One component owns linking; everyone else is forbidden.** Chosen.
|
|
|
|
## Decision
|
|
|
|
The installer creates and repairs every link the mesh needs. It reconciles them: a missing
|
|
link is created, a stale one is repointed, and a real file found where a link belongs is
|
|
adopted into the node's override location and replaced.
|
|
|
|
**Nothing else creates a symlink** — not a hook, not a fix, not an agent, not a person
|
|
debugging. The prohibition is absolute because the judgement required to make a safe exception
|
|
is exactly the judgement that was not available at the moment it mattered.
|
|
|
|
## Consequences
|
|
|
|
- The class of failure is closed, at the cost of a rule that reads as arbitrary to anyone who
|
|
has not seen the incident. That is why it is recorded here rather than only asserted.
|
|
- Links become reconcilable state rather than incidental filesystem facts.
|
|
- The rule is stated for humans and agents and is enforced by convention, not mechanism. A
|
|
check does not exist.
|
|
- The rule as written governs the mechanism rather than removing it. A link made by the
|
|
installer resolves the same way as one made by hand, so the hazard is narrowed and not
|
|
closed. [ADR 0018](0018-the-mesh-creates-no-symlinks.md) proposes widening this to "nothing
|
|
links, the installer included"; until that is accepted, this record governs.
|
|
|
|
## References
|
|
|
|
- Recorded as a non-negotiable in the governed constitution page, §2: *"Symlinks to repos or
|
|
service directories have caused production data loss via Docker volume path resolution. The
|
|
installer handles all linking. Never create symlinks manually."*
|
|
- Knowledge base: `services` — the reconciliation behaviour, including adoption of real files.
|