DECISIONS · v0.4.2.14
ADR-009 Device tenancy and the controller key
Source: modules/anthill/docs/adr/ADR-009-device-tenancy-and-controller-key.md, a path from the root of the product's repository — versioned with the code, rendered here at every release.
Status: Accepted — implemented at v0.3.19.0 (W3-07)
Date: 2026-09-17
Baseline: v0.3.18.0 (2d1eb7cc)
Settles: W1-02 D7 (mound id scope, controller key custody), W1-06 K04 (one signing identity per
installation or per organization). Defers W1-06 K05 (the second identity row) and K06 (the
two-key protocol amendment).
1. Context
Until this release the device fleet was single-installation: every micromound_* table keyed on
mound_id alone, the controller held one Ed25519 signing identity under CHECK (id = 1), and the
three global roles decided who could charter a motor. W3-02 gave the colony organizations, project
memberships and a declared scope on every route, and left the device routes Installation — reachable
only from the installation's own organization — with the note that W3-07 would scope them.
Three questions had to be answered together, because each constrains the others:
- Does
mound_idstay globally unique, or become unique per organization? - Does an enrolment token bind an organization when it is issued?
- Is there one controller signing key for the installation, or one per organization?
W1-06 §13 costed the third and recommended per installation, and §4.2 recorded why the answer had to come before the fleet was scoped: rotating the controller key is not an operation the protocol has. What exists is fleet re-enrolment, which has a physical component. Choosing per-organization after devices had been scoped would have meant re-enrolling every device twice.
2. Decision
One controller signing identity per installation. micromound_controller_identity keeps
CHECK (id = 1) and is classified Install: it belongs to the deployment, not to any tenant. The
installation is the hardware trust boundary. An installation that serves two organizations shares
one key between them by design, and that is stated rather than hidden: the second organization's
devices trust the same controller the first's do, because they are the same controller.
A device is placed at mint, and the placement is the token's. POST /micromound/mounds runs in
the acting organization; the mound it mints is written into micromound_mounds.org_id and
project_id there and then, before any device exists. The device never names an organization: it
presents the one-time token, and the token was minted for a mound already placed. A token that
leaks from one organization enrols a device into that organization and no other, which is the
binding W3-07 item 3 asked for, achieved with no wire change — /micromound/v0/enroll and
/micromound/v0/sync are byte-for-byte what they were, so there is no compatibility window to
declare and every enrolled device keeps working (A16).
mound_id stays globally unique. Per-organization uniqueness (PRIMARY KEY (org_id, mound_id))
would have removed the existence oracle W1-02 §5.5 names — a second organization learning that an
id is taken — at the cost of rekeying seven tables, the enrolment token table, the device's own
identity binding and every mound_id in a charter, evidence chain and firmware. The oracle is
closed instead at the one place it opens: a mint naming an id another organization has placed is
refused as bad_request in the same words as any other id that cannot be used here, and every
other route answers not_found for a device the caller's organization does not reach. The
residual — that the refusal itself reveals the id is in use somewhere — is accepted and recorded.
The first placement is final. PlaceMound writes only where org_id IS NULL or already equals
the caller's organization; a placement naming another organization changes nothing. A re-mint by
the device's own organization may assign or change its project. The device module's own upsert
names every column but these two, so saving a record can never move a device between tenants.
Roles are the project's. A device with a project is reached through a role on it: any role
reads it; operator charters, dispatches and authors; project_admin configures, retires, purges
and re-mints. A device with no project is its organization's owners' alone, and minting one is
refused for anyone who cannot reach unfiled rows (project_required). This is W1-02's model
applied without a device-specific role.
A stop is never out of reach. POST /micromound/stop is declared AnyOrg. Engaging a stop
needs any role on the device's project, or the installation's organization, whose operators can
halt every device on the installation whatever tenant it serves. Clearing a stop re-grants motion
and needs operator on the device's own project. The global stop file stays outside the API.
Commercial entitlement is not consulted anywhere on this path (W1-04 §2.1).
Devices from before tenancy are asserted into the installation's organization on the first
start of this release, once, with a tenancy_assertions row beside the one migration 0001 wrote:
the same fact — one installation, one controller key, one tenant — stated for the rows a module
added after the migration ran. They have no project until re-minted with one.
3. Consequences
- The
MICROMOUNDdefine is set in monorepo builds for the first time:MicromoundRepoPathdefaulted to a sibling checkout that does not exist in the monorepo, so the device module's API and its tests had not compiled here since the cutover. It resolves toruntimes/micromoundnow. RouteScopesdeclares every device routeOrg,Resource(Mound,DeviceMission) orAnyOrg;TenancyTablesclassifiesmicromound_moundsasScoped, its per-mound tables asInheritthroughmound_id, and the widget cache and controller identity asInstall.- Colony Live's snapshot and the mound roster stay
Installationsurfaces: another organization does not see them at all, so no device leaks through them; its fleet is/micromound/moundsunder its own acting organization. Drawing another organization's devices into its own colony view is W3-08's to decide when the colony view is scoped. - Deferred, on purpose. K05 (a second identity row with an
activeflag, so a re-enrolment can be aborted halfway) and K06 (the two-controller-key protocol amendment) are the re-enrolment procedure's, not this release's; nothing here makes them harder, and per-installation custody means neither is needed for the fleet to be scoped. If K04 is ever revisited to per-organization, the cost is one fleet re-enrolment per organization, and W1-06 §4.2 is the runbook.
