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:

  1. Does mound_id stay globally unique, or become unique per organization?
  2. Does an enrolment token bind an organization when it is issued?
  3. 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 MICROMOUND define is set in monorepo builds for the first time: MicromoundRepoPath defaulted 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 to runtimes/micromound now.
  • RouteScopes declares every device route Org, Resource (Mound, DeviceMission) or AnyOrg; TenancyTables classifies micromound_mounds as Scoped, its per-mound tables as Inherit through mound_id, and the widget cache and controller identity as Install.
  • Colony Live's snapshot and the mound roster stay Installation surfaces: another organization does not see them at all, so no device leaks through them; its fleet is /micromound/mounds under 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 active flag, 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.