The rule. A provider is granted exactly the consumers whose own current resolution binds them to it. grantsFor enumerated the pair secrets on record from the provider (inv.SecretsFrom) and granted each one whose consumer still asks for the provision — without checking whom it asks. After issue 273 the five home-server consumers were pinned back to their own store; the credentials from the control node's store were still on record, so that store kept being granted them, counted them as asked, and never retired them under ADR 0230.
Change.
catalogue.Resolution.BindsFrom(provision, module, local) — the providers a pair credential's need is bound to in the consumer's resolution (never a need answered by a record; local name is part of the credential).
In grantsFor, a consumer that still asks but whose resolution does not bind that credential to this node is withdrawn exactly like one nobody asks for (From == ""): the provider stops being told, sees it unasked, and retires it with its data kept (ADR 0230).
It is returned as catalogue.Unbound beside the grants (like ADR 0225's Withheld), carried on Rendering.Unbound → sendable.unbound, and said by plan/push through reportLeftOut: <provider>: board on laptop is bound to laptop for postgres-database, not to anchor — anchor no longer grants it, so it retires that login and keeps its data until cleanup delete (ADR 0230); the credential from anchor stays on record while that login does.
The stale pair secret is kept, not dropped. It is the key to the login the provider keeps (disabled) with the consumer's data until cleanup delete, and a pin back finds it again. No migration. Forgetting it when the provider reports the consumer deleted is a follow-up named in the issue.
Interactions.
ADR 0232: the binding read is the resolution's, which already applies the recorded bindings (keepBound) and pins — a kept binding is a need From the recorded provider, so it keeps its grant.
ADR 0225: the overflow check runs only on what is still granted.
Issue 152: the binding is read from the same planFor result; an unreadable resolution is still the existing error, never an empty (withdrawing) answer. Pinned by a test.
rotate: unchanged. rotate postgres-database --consumer <home server> still lists both holders per consumer; rotating the stale one discards it and nothing remakes it (no need is bound there), and the old provider was already no longer granted it.
today's state through the real stores: consumer pinned to the other store, sent, pinned back, both credentials on record → the store it left no longer grants it, its declaration no longer carries it, the push says so; the store it is bound to grants it; both credentials stay on record. Fails without the fix.
a consumer whose resolution cannot be read → grantsFor errors (issue 152), no withdrawal.
BindsFrom unit: provider of that credential only; other local name → none; by-record need → none.
Full suite go test -p 1 ./... against throwaway postgres:17 + nats:2.11 (MESH_TEST_POSTGRES / MESH_TEST_NATS): all pass. go vet ./..., gofmt clean, -race on the touched tests and internal/catalogue.
After rollout on the control node: the five home-server consumers become unasked at its postgres provider; after five passes they are retire candidates, five > the bound of three, so the provider retires nothing and raises retire-waiting (urgent) naming them; the operator answers with retire approve <control node> postgres. Data kept; deletion only by cleanup delete.
novox/hq issue 274.
**The rule.** A provider is granted exactly the consumers whose own current resolution binds them to it. `grantsFor` enumerated the pair secrets on record from the provider (`inv.SecretsFrom`) and granted each one whose consumer still *asks* for the provision — without checking *whom* it asks. After issue 273 the five home-server consumers were pinned back to their own store; the credentials from the control node's store were still on record, so that store kept being granted them, counted them as asked, and never retired them under ADR 0230.
**Change.**
- `catalogue.Resolution.BindsFrom(provision, module, local)` — the providers a pair credential's need is bound to in the consumer's resolution (never a need answered by a record; local name is part of the credential).
- In `grantsFor`, a consumer that still asks but whose resolution does not bind that credential to this node is withdrawn exactly like one nobody asks for (`From == ""`): the provider stops being told, sees it unasked, and retires it with its data kept (ADR 0230).
- It is returned as `catalogue.Unbound` beside the grants (like ADR 0225's `Withheld`), carried on `Rendering.Unbound` → `sendable.unbound`, and said by `plan`/`push` through `reportLeftOut`: `<provider>: board on laptop is bound to laptop for postgres-database, not to anchor — anchor no longer grants it, so it retires that login and keeps its data until `cleanup delete` (ADR 0230); the credential from anchor stays on record while that login does`.
- **The stale pair secret is kept, not dropped.** It is the key to the login the provider keeps (disabled) with the consumer's data until `cleanup delete`, and a pin back finds it again. No migration. Forgetting it when the provider reports the consumer deleted is a follow-up named in the issue.
**Interactions.**
- ADR 0232: the binding read is the resolution's, which already applies the recorded bindings (`keepBound`) and pins — a kept binding is a need `From` the recorded provider, so it keeps its grant.
- ADR 0225: the overflow check runs only on what is still granted.
- Issue 152: the binding is read from the same `planFor` result; an unreadable resolution is still the existing error, never an empty (withdrawing) answer. Pinned by a test.
- `rotate`: unchanged. `rotate postgres-database --consumer <home server>` still lists both holders per consumer; rotating the stale one discards it and nothing remakes it (no need is bound there), and the old provider was already no longer granted it.
**Tests** (`cmd/mesh-controller/grant_follows_binding_test.go`):
- today's state through the real stores: consumer pinned to the other store, sent, pinned back, both credentials on record → the store it left no longer grants it, its declaration no longer carries it, the push says so; the store it is bound to grants it; both credentials stay on record. Fails without the fix.
- a consumer whose resolution cannot be read → `grantsFor` errors (issue 152), no withdrawal.
- `BindsFrom` unit: provider of that credential only; other local name → none; by-record need → none.
Full suite `go test -p 1 ./...` against throwaway postgres:17 + nats:2.11 (MESH_TEST_POSTGRES / MESH_TEST_NATS): all pass. `go vet ./...`, gofmt clean, `-race` on the touched tests and `internal/catalogue`.
**After rollout on the control node:** the five home-server consumers become unasked at its postgres provider; after five passes they are retire candidates, five > the bound of three, so the provider retires nothing and raises `retire-waiting` (urgent) naming them; the operator answers with `retire approve <control node> postgres`. Data kept; deletion only by `cleanup delete`.
grantsFor granted every consumer a pair credential from the provider was
ever made for, so a consumer pinned back to its own store was still asked
of the store it left, which then never retired it. A credential whose
consumer's resolution binds it elsewhere is now withdrawn like one nobody
asks for, kept on record for the login the provider keeps, and said on
plan and push.
build-agent keeps data (ADR 0233): what it holds is backed up before it moves
route-proxy provides route: its consumers are sent again after it
route-proxy keeps data (ADR 0233): what it holds is backed up before it moves
Transitions
2026-10-06 23:05 (new) → proposed (announced): novox/mesh-controller#87 merged; its head was not heard by this owner, so it is made from the forge's word
2026-10-06 23:05 proposed → held (merged-unchecked): merged without a passing check — a verdict known with it is taken first: only a person decides that it goes on
The commit's note under refs/notes/mesh-plan keeps every transition: git log --notes=mesh-plan.
<!-- mesh-delivery:view -->
**Delivery** `novox/mesh-controller@4b25af2aa3b3` — **held** since 2026-10-06T23:05:28Z
Held for a person: merged without a passing check: only a person decides that it goes on — `mesh-delivery.release` with why lets it go on.
**Delivery plan** — builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step
- tier 0: mesh-controller
- tier 1: build-agent
- tier 2: route-proxy
- ace: receives build-agent, route-proxy
- g14: receives build-agent
- novox: receives mesh-controller, build-agent, route-proxy
- shanks: receives build-agent
- build-agent keeps data (ADR 0233): what it holds is backed up before it moves
- route-proxy provides route: its consumers are sent again after it
- route-proxy keeps data (ADR 0233): what it holds is backed up before it moves
**Transitions**
- 2026-10-06 23:05 (new) → proposed (announced): novox/mesh-controller#87 merged; its head was not heard by this owner, so it is made from the forge's word
- 2026-10-06 23:05 proposed → held (merged-unchecked): merged without a passing check — a verdict known with it is taken first: only a person decides that it goes on
The commit's note under `refs/notes/mesh-plan` keeps every transition: `git log --notes=mesh-plan`.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
novox/hq issue 274.
The rule. A provider is granted exactly the consumers whose own current resolution binds them to it.
grantsForenumerated the pair secrets on record from the provider (inv.SecretsFrom) and granted each one whose consumer still asks for the provision — without checking whom it asks. After issue 273 the five home-server consumers were pinned back to their own store; the credentials from the control node's store were still on record, so that store kept being granted them, counted them as asked, and never retired them under ADR 0230.Change.
catalogue.Resolution.BindsFrom(provision, module, local)— the providers a pair credential's need is bound to in the consumer's resolution (never a need answered by a record; local name is part of the credential).grantsFor, a consumer that still asks but whose resolution does not bind that credential to this node is withdrawn exactly like one nobody asks for (From == ""): the provider stops being told, sees it unasked, and retires it with its data kept (ADR 0230).catalogue.Unboundbeside the grants (like ADR 0225'sWithheld), carried onRendering.Unbound→sendable.unbound, and said byplan/pushthroughreportLeftOut:<provider>: board on laptop is bound to laptop for postgres-database, not to anchor — anchor no longer grants it, so it retires that login and keeps its data untilcleanup delete(ADR 0230); the credential from anchor stays on record while that login does.cleanup delete, and a pin back finds it again. No migration. Forgetting it when the provider reports the consumer deleted is a follow-up named in the issue.Interactions.
keepBound) and pins — a kept binding is a needFromthe recorded provider, so it keeps its grant.planForresult; an unreadable resolution is still the existing error, never an empty (withdrawing) answer. Pinned by a test.rotate: unchanged.rotate postgres-database --consumer <home server>still lists both holders per consumer; rotating the stale one discards it and nothing remakes it (no need is bound there), and the old provider was already no longer granted it.Tests (
cmd/mesh-controller/grant_follows_binding_test.go):grantsForerrors (issue 152), no withdrawal.BindsFromunit: provider of that credential only; other local name → none; by-record need → none.Full suite
go test -p 1 ./...against throwaway postgres:17 + nats:2.11 (MESH_TEST_POSTGRES / MESH_TEST_NATS): all pass.go vet ./..., gofmt clean,-raceon the touched tests andinternal/catalogue.After rollout on the control node: the five home-server consumers become unasked at its postgres provider; after five passes they are retire candidates, five > the bound of three, so the provider retires nothing and raises
retire-waiting(urgent) naming them; the operator answers withretire approve <control node> postgres. Data kept; deletion only bycleanup delete.Delivery
novox/mesh-controller@4b25af2aa3b3— held since 2026-10-06T23:05:28ZHeld for a person: merged without a passing check: only a person decides that it goes on —
mesh-delivery.releasewith why lets it go on.Delivery plan — builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step
Transitions
The commit's note under
refs/notes/mesh-plankeeps every transition:git log --notes=mesh-plan.