Files
hq/04-ISSUES/007-an-installed-package-is-not-a-capability/00-report.md
T
jschoubben 333356cff3 Order the records the way the system is learned
Jochen asked whether the order made sense. It did not -- it followed when
things happened to be decided, which after consolidation is fictional anyway
since record 5 alone folds decisions taken across a week.

Concretely wrong before: the domain statement sat at 8, after five engineering
rules; the constitution was scattered across 5, 12 and 17; the tiers landed at
15, 16, 21 and 22 with process records in between.

Now it walks: what the mesh is (1-3), its tiers from the bottom up (4-8), what
runs on them and how it gets there (9-10), how it is built (11-16), how it is
checked (17-18), how we work (19-23).

Two things made this safe rather than free. It is a permutation, not a
compaction, so the renames go through temporary names -- otherwise two files
want one slot and one is lost. And the reference rewrite is a single
simultaneous pass, because almost every number moved into a slot another number
was vacating; replacing one at a time would have cascaded and pointed things at
the wrong record while still resolving.

Verified: 284 [ADR NNNN](path) links across the repository, all with matching
text and target.

The ordering principle is now stated in 19 rather than left implicit -- the
repository already said "the numbering is the flow" about its folders, and
there was no reason for the records to be the exception.
2026-08-28 23:30:42 +02:00

118 lines
5.8 KiB
Markdown

---
status: diagnosing
opened: 2026-08-23
located-in: [hal]
fixed-by:
- "the instance only: PR #962 — incus hook. Merged, ran on one node in 6s of a 60s budget; pipeline #6832 green in 48s. The class remains open."
amended-design:
---
# 007 — An installed package is not an available capability
## Symptom
A module declares the virtualisation package the lab needs. The package is installed —
version 7.3.0-1, recorded as explicitly installed. The client binary runs.
The capability does not exist:
| Checked | State |
|---|---|
| `incus.service` | disabled, inactive |
| `incus.socket` | disabled, inactive |
| `incus-user.socket` | disabled, inactive |
| the operator's group membership | not a member of any incus group |
| the client | reports **`Server version: unreachable`** |
Nothing failed. Nothing reported anything. The declaration was satisfied exactly as written,
and the thing it was declared for cannot be used.
## Why this is not issue 001 again
[Issue 001](../001-failed-package-install-reports-success/00-report.md) is *the install failed
and the job reported success*. This is the opposite and arguably worse: **the install
succeeded, and success was not the point.**
A package is a set of files. A capability is a running service, an enabled socket, and an
identity permitted to reach it. The module model declares the first and has no vocabulary for
the second, so the gap between them is invisible — there is no state in which the mesh believes
this node has virtualisation and is wrong, because the mesh was never asked to believe it.
The distance between the two is the same one the delivery layer already has a name for:
**transport versus effect.** A package install reports that files arrived, which is transport.
## Why it matters now
This is the first requirement of the lab
([ADR 0016](../../02-DECISIONS/0016-the-lab.md)), which is
phase 0 of the entire migration. The first capability the new work depends on is present,
declared, and unusable — and would have stayed unusable silently.
It also generalises. Every module that declares a package needing a unit enabled, a group
joined, a kernel module loaded, or a socket activated has this gap. Post-install work lives in
hooks, and hooks have their own recorded failure mode: thirteen were found that had never run.
## Evidence
- Package recorded as installed 2026-08-23 00:02, explicitly.
- Both units disabled and inactive; the operator in no incus group; client reports the server
unreachable.
## Open questions
- Should a module be able to declare a **capability** — a unit that must be enabled, a group
the operator must be in — rather than only the package that provides it?
- If that is what hooks are for, why is the gap invisible when a hook does not run? A hook that
never fires and a hook that fires and does nothing are indistinguishable today.
- Is this what the verify stage should be asserting? It exists, and a module's own assertions
are meant to test outcomes rather than steps — "the socket accepts a connection" is exactly
that shape.
- How many other declared packages are in this state? Nothing currently reports it, which means
the answer is unknown rather than zero.
## The instance is fixed; the class is what this issue is now about
*Updated 2026-08-23.* A hook now does the post-install work — group membership, subordinate id
ranges, enabling both units, creating the storage pool, the bridge, and the default profile.
Verified independently afterwards: the group exists with the operator in it, both id files
carry the range, the service is active, and the storage pool reports `CREATED`.
**So the mechanism was never missing.** Hooks are exactly the right place for this, and used
properly they work. The gap is narrower and worse than "there is no way to do it":
> The hook did six things. **Six checks were then performed by a human, by hand.** Nothing in
> the pipeline asserted any of them, and a pipeline that dispatched a hook which silently never
> fired would have been green in the same 48 seconds.
That is not hypothetical — a hook named for a feature its module does not carry is skipped
without complaint, and thirteen such hooks were found at once in the past. The distance between
*the hook ran and did six things* and *the hook was dispatched* is invisible from the outside,
and it is the whole of this issue.
### The verification that was done by hand is the assertion set
The six checks performed after the merge are, almost word for word, what the module's own
verification should assert — outcomes, not steps, exactly as the lab design requires:
| Asserted by hand | As a module assertion |
|---|---|
| operator in the admin group | the group exists and contains the operator |
| subordinate id range in both files | both files carry the range |
| both units enabled, service active | the service is active, and still active shortly after |
| storage pool present | the pool exists and reports created |
| bridge present with an address | the bridge exists and holds its address |
| default profile wired to both | the profile references the bridge and the pool |
They currently live in a chat message. Moved into the module, they would run on every delivery
to every node, and the difference between a hook that worked and a hook that was merely
dispatched would stop being something a person has to notice.
### What this issue now asks
- Does a module gain a way to declare a **capability** — the outcome — separately from the
package that provides it, or is "write assertions" the whole answer?
- The verify stage exists and asserts almost nothing. Is this simply the first module that
should use it properly, making the issue a coverage problem rather than a design gap?
- **How many other declared packages are in the state this one was in?** Still unknown, still
unreported by anything, and now demonstrably worth asking.