ADR 0053 accepted; adds the scope boundary the umami rework surfaced (credential provisions vs data provisions — analytics' generated siteId return is left to a separate decision) and records the lab proof. Issue 008 marked resolved: the sdk harness and the four adapters are reworked, the symmetric seal removed, and provider-uses-mesh-credential is green. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
8.5 KiB
status, opened, located-in, fixed-by, amended-design
| status | opened | located-in | fixed-by | amended-design | ||
|---|---|---|---|---|---|---|
| resolved | 2026-09-04 |
|
mesh-sdk src/provisioner rework + redis/postgres/minio/umami adapters (ADR 0053) | 0053-a-provider-creates-the-credential-the-mesh-minted.md |
A provider's provisioner seals with a key the mesh has no way to deliver — and does not need to
What was observed
Building the vertical slice for the module runtime (the module runs its own code as its own
process under its own account), a provider module — one that stands up a per-consumer
resource and hands back a credential — was assigned to a node and run as a broker-bound
runtime. The runtime hosts the module's provisioner (the sdk's runProvisioner), and the
harness opens by reading a seal key from $MESH_SEAL_KEY, failing immediately without
one. Every credential it produces for a consumer is sealed to that key with the sdk's
symmetric seal() (AES-256-GCM, mesh-sdk/src/primitives/index.ts) before being written.
Nothing in the mesh sets $MESH_SEAL_KEY. It is read in exactly two places in the sdk and
set nowhere — no manifest, no control-plane code, no host code. So a provider runtime, as
delivered, aborts at start-up. The slice proved the mechanism only by setting a lab-local key
in the manifest by hand.
What a trace of the credential path turned up
The seal key is not a missing delivery. The whole symmetric-seal provisioner is orphaned, and it duplicates — badly — a job the mesh already does.
runProvisionerreads request files named*.grant.json. Nothing writes those.- It writes sealed credential files named
<consumer>.<resource>.credential. Nothing reads those — not the host, not the control plane. The host reports applied-resource digests upward and never ships credentials; the control plane has no reference to that filename. - No consumer ever calls the symmetric
unseal(). Consumers receive plaintext.
Meanwhile the mesh already carries a provider→consumer credential across nodes, with no shared key anywhere:
- The control plane mints the password once (
secrets.Make) and seals it twice, asymmetrically —ForConsumerto the consumer node's X25519 public key,ForProviderto the provider node's (mesh-control/internal/secrets/seal.go,mesh-host/internal/identity/ sealing.go, NaCl box). - Each host opens its own copy with its own private key on the machine; the plaintext exists
only for the length of one function call (
mesh-host/internal/apply/apply.go, the${secret:name}substitution — ADR 0024's "the host is the only thing that ever holds both"). servescarries no credential and says so;receives/boundtell each side where its sealed secret is, never the value.
The two models also contradict each other. The sdk's seal() comment says the key is "a
per-node passphrase the host holds"; the host holds no such passphrase — it holds an X25519
private key, and the control plane's own code refuses a shared symmetric key on principle:
"a key both ends hold is a key the mesh would have to distribute, which is this problem again
one level down" (secrets/seal.go). A symmetric MESH_SEAL_KEY shared between a provider
node and a consumer node is exactly the thing the mesh was built not to have.
And the provisioner's model is wrong in a second way: its adapter generates its own
password (generatePassword()) and creates the resource with it — a different password from
the one the mesh mints and hands the consumer. Even with a seal key delivered, a consumer
would authenticate with the mesh's password against a resource created with the provisioner's.
Why it matters beyond this instance
This is not a four-module problem. The provider contract lives in one place — the sdk's
runProvisioner(resource, adapter) harness — and every provider is built on it. Four exist
today (redis, postgres, minio, umami); a mesh of any size ends up with many. Whatever the
provisioner harness does, every present and future provider inherits, so the orphaned
symmetric seal is a fault stamped into the interface, not into four adapters. That also sets
the cost of getting it wrong: a contract N providers depend on is N migrations to change
later, which is the argument for settling it deliberately now rather than patching around it.
As written, each provider carries a provisioner that cannot start (no key), and that, if it did, would create resources with a password it invented — a different password from the one the mesh minted and handed the consumer — and seal them for a reader that does not exist. The rule the design states, "a consumer receives a sealed credential and unseals it," is enforced by nothing: no consumer unseals, and no shared key exists to unseal with.
The mesh already does this — confirmed
The premise the fix rests on is not a hope; it is in the control plane today. For a served
interface, Inventory.SecretFor mints one password per (consumer, provider) pair via
secrets.Make, sealing it to both node keys — ForConsumer and ForProvider.
SecretsFrom(provider) is documented as "every credential a provider node was issued, so it
can be told what to create," and grantsFor (plan.go) hands the provider node one Grant per
consumer carrying Sealed: ForProvider. The provider receives, at the path its receives
names, one Contribution per consumer: the login to create (As, derived by the mesh so both
ends agree — 04-ISSUES/023), the consumer's address (At) and requested Values, and
Secret, the file holding that consumer's password sealed to this provider and unsealed by
its host. Everything the provisioner needs is delivered. It reads the wrong files
(*.grant.json, which nothing writes) and invents a password instead of reading the one in
Secret.
The fix this points to
A one-place contract change in the sdk harness, plus re-pointing today's adapters at it — not per-provider surgery, and inherited correctly by every provider after them:
runProvisionerreconciles the mesh-deliveredreceivescontributions (not*.grant.json): for each consumer, create the resource under the loginAswith the password read from the deliveredSecretfile, for itsValues; withdraw the login when a consumer leaves the file.- The adapter stops generating a password and stops returning a credential — it is handed the
name and the password and only makes the resource exist. Roughly
create({as, password, values})/remove({as}), no return. sealKey,seal(),writeSealedCredential,MESH_SEAL_KEY, and the.credentialfile leave entirely; the consumer already receives its copy through the mesh's own channel.
This is proposed as ADR 0053, which defines the corrected provider contract, for ratification.
Resolution
ADR 0053 was accepted and implemented on the branches this issue is fixed by:
mesh-sdksrc/provisioner/index.tsnow reconciles the mesh'sreceivescontributions and, per consumer, reads the mesh-minted password from the file the host unsealed, calling the adapter to create the resource under the mesh's login.$MESH_SEAL_KEY, the symmetric seal,writeSealedCredential, and the*.grant.json/*.credentialfiles are gone. The symmetricseal()/unseal()primitive had no other caller and was removed.- The four adapters (redis, postgres, minio, umami) were re-pointed at the new contract —
create({ as, password, values })/remove({ as }), returning nothing. minio's client gained a secret-key argument so it sets the mesh's secret rather than generating one. - Proven in the mesh-lab:
provider-uses-mesh-credentialis green — redis creates the consumer's login with the password the mesh minted, a client authenticates as that consumer and gets PONG, with no seal key set anywhere.
Two things were carved out deliberately, neither blocking:
- Data provisions are a separate shape. umami's
analyticsreturns asiteIdumami generates, not a secret the mesh mints, and a contract that returns nothing cannot hand that back. ADR 0053 is scoped to credential provisions and says so; the provider→consumer return path for generated data is left to a separate decision. umami compiles and reconciles under the new harness; only that return is unaddressed, and it never had the seal-key fault. - Teardown beyond "remove the login" — an object store's leftover data — is each adapter's to name (minio leaves a non-empty bucket for an operator rather than deleting a consumer's data), not the harness's.