Skip to content

fix(accounts): the confined mint's ceiling is the consumer's delivered authority (host half of #1083) - #1084

Merged
antoinetoussaint-byte merged 5 commits into
mainfrom
issue-1083-a-confined-capability-s-ceiling-should-be-the
Oct 12, 2026
Merged

antoinetoussaint-byte merged 5 commits into
mainfrom
issue-1083-a-confined-capability-s-ceiling-should-be-the

Conversation

@antoinetoussaint-byte

@antoinetoussaint-byte antoinetoussaint-byte commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Refs #1083. Follows #1019. This is the host half. #1083 stays open until the delivery half lands: codefly-dev/cli rendering a solution's declared consumption into its signed authority document (codefly-dev/core#784 and codefly-dev/cli#972, both merged; a solution adopting it needs a core/CLI release).

What reaches the ceiling, and what does not (corrected)

The record this PR reads is the consumer's registry grant. The renderer produces it from a delivered authority document, and the renderer derives that document from module.contract.codefly.yaml and from nothing else.

Layer Owner Today Reaches this ceiling?
Module contract bindings (audience slot + per-operation scope ceiling) core contracts/module Read by the renderer (cli pkg/gitops/authority.go, authorityInstancesOf → modulecontract.Load). Yes, for any module that publishes a contract and a module-identity service.
Authority document core solutionhost Signed by delivery; the host derives OperationAudiences from it, the applied presence and the envelope. It is the record.
A solution's api.consumes (solution.codefly.yaml) core solution/manifest, cli Feeds SDK generation (cmd/sync/solution_sdk.go) and CODEFLY__API_CONSUMES (pkg/solutionrun). It names module/service/endpoint and no scopes. No.
solution-runtime-go Server.Contract solution-runtime-go Its own schema at /.well-known/module-contract. The runtime's own source says the renderer does not read it. No.

So after this PR, no solution in a composition today can receive a confined mint: none publishes a module contract. A solution's confined mint (for example an MCP tool calling documents) is refused, and no setting an administrator controls can admit it. This does not break a path that works on main. #1072 already requires a registry principal to install a solution or be entitled to it, so a confined StartTask through a contract-less solution is already refused at the gateway. What this PR does is make the host ready for the delivery half. It does not close #1083.

The earlier version of this PR claimed the solution's declaration already reached the ceiling and listed the runtime declaration as "no change needed". Both claims were wrong, and both are corrected here and in module/services/auth-gateway/AGENTS.md.

What changes

holdConfinedMintToItsConsumer (accounts, StartTask):

  1. Unchanged: the resource must be one this host issues, the credential's own organization is required, and an installation must be stated.
  2. The ceiling is the consumer's delivered authority. business.ConsumerInvokeCeiling(org, installation) reads the installation's delivered principal. It then runs the same decision every capability of that consumer goes through (authorizeModuleCapability, sealed to that installation). The result is the envelope ∩ signed authority ∩ installation consent, and its invoke bindings are the ceiling. The audience must be one the consumer declares, and the scopes must fit one binding there.
  3. The installation's allowed_audiences / allowed_scopes only narrow. They are applied through the ordinary ceiling: an empty list adds no bound, and a wider list admits nothing extra. Security posture audit 2026-10-04: the 21 findings this repo owns (Closes #1018) #1019's "an empty list refuses a confined mint" is replaced.
  4. Each refusal names its cause, so the operator knows who has to act:
    • Not consented (M2). Consent stays fixed at install; that is the safe rule and it is unchanged. A module the consumer declares that this installation does not consent to is refused as not consented by this installation, and the message says that installing again consents to what the consumer declares now. It used to read as "undeclared".
    • Undeclared, or scopes outside what is declared at that audience.
    • The consumer's own audience (m4). Since the merge with main, a self-admitting binding (headless or delegation work) may name its own module and carries invoke scopes. The own-audience refusal runs before any binding is read, so such a binding never admits a confined mint (TestASelfAdmittingBindingNeverAdmitsAConfinedMint). Decided: refused by name, because the credential already reaches that consumer's own routes. Both its prefix and solution:<its binding> count.
    • Capabilities withdrawn (m2) by the installation's consent: named as such, not as "installation not active".
    • No delivered principal (an installation that predates the recording): FailedPrecondition, naming the remedy.
  5. Fencing (m3). StartTask opens the authority use span for a confined request before any other transaction, so an uninstall or narrowed consent waits for the mint. The span does not hold the in-memory registry a redelivery replaces. So after signing, and before the capability leaves the process, ConsumerCeilingStillCurrent decides again through authorizeModuleCapability in the same span. It requires the declaration and the consent to be unchanged, and otherwise refuses with Aborted ("ask again"). A withdrawal published after that check lands after issuance, bounded by the capability's lifetime like every other one. A republish that changes nothing about the consumer does not refuse.

M1: an invoke-only binding is no longer dropped. validateOperationAudiences required non-empty lookup scopes, so the reconciler omitted every binding that declares only invoke. Published module contracts in use today declare bindings of exactly that shape: a person-attenuated read against a document store with "nothing to look up", and a separate caller-scoped write binding. I checked one such contract (in the coordinator's review, named there). Those bindings were omitted from that module's registry entry, so its person-attenuated ExchangeDelegatedOperationAudience for them failed too, not just this ceiling. I picked accepting invoke-only, because refusing the delivery would refuse that module's whole authority document, and its declaration is correct. Lookup is now optional, like headless:

  • absent means "this binding redeems no lookup";
  • present but empty is still a misconfiguration;
  • a lookup exchange under an invoke-only binding is refused by name instead of signing a scopeless capability;
  • installation consent intersects lookup to nil instead of to an invalid empty list.

WORK_CONTEXTS.md states the rule.

Classification: fix, at the place that owns each behaviour: the confined-mint ceiling and the registry's binding validation are accounts'. No hack, no stopgap. Nothing is added to unrouted_capability_paths.txt: the after-signing re-check goes through authorizeModuleCapability instead of reading the registry directly.

Tests (the reviewer's adopted, renamed, with assertions flipped where the finding is fixed)

  • Issue cases. An undeclared audience, and undeclared kinds or actions, are refused whether the installation's lists are empty or wider. Declared requests are issued with empty lists. The lists still narrow.
  • Consent, read through real seams. A module declared but not consented gets the "not consented, install again" message. Capabilities withdrawn get their own message. Other covered cases: no delivered principal, a withdrawn installation, an unreadable live authority (Unavailable), and a different installation of the same principal.
  • Shape of the declaration. Resource-bound declarations work. Two bindings for one module are not unioned. Consumption derived through the real deriveModulePrincipalGrant is bounded by both the envelope and the document. An invoke-only declaration reaches the ceiling (the reviewer's M1 test, now passing). A consumer cannot declare itself. A table covers every Admit verdict.
  • Fencing. One authority span is held through the mint. A redelivery that withdraws the binding mid-mint refuses it (the reviewer's m3 test, now asserting Aborted), and a republish that changes nothing does not. ConsumerCeilingStillCurrent is tested for unchanged, withdrawn, moved binding and principal gone.
  • Postgres (pkg/infra/consumer_invoke_ceiling_db_test.go): the declared and consented binding is the ceiling. A module declared after install gets the not consented verdict (the reviewer's M2 test, now asserting the verdict). A row without delivered authority is refused by name.
  • Updated. TestAnAudienceWithoutItsLookupIsOmittedByName becomes TestAnInvokeOnlyAudienceIsKept. TestInstalledOperationAudiencePolicy now holds invoke-only as valid and present-but-empty lookup as invalid.
  • Mutations fail the suite: deleting the declared ceiling, the list narrowing, the installation seal, the span StartTask opens, or the after-signing re-check.

Docs updated

  • module/services/auth-gateway/AGENTS.md: what reaches the ceiling and what does not, each refusal and who acts on it, consent fixed at install, and fencing. m1: it no longer promises operators the "no delivered authority" refusal, because the gateway's entitlement check answers a generic 403 first.
  • module/WORK_CONTEXTS.md: lookup scopes are optional.
  • The InstallSolutionRequest.allowed_audiences proto comment no longer says a solution's declaration is the ceiling. Regenerated with CI's pinned CLI 0.1.185: both contract descriptors, catalog, module package, generated Go, both TypeScript copies, both swagger documents, and the saas-sdk contract. saas-sdk is now 0.3.27: main took 0.3.26 while this PR was open, and the merge regenerated the contract from both protos.
  • The install dialog's description of the field.

Verified, and not

Green:

  • accounts go vet ./... untagged, and go test -tags pure ./...;
  • accounts pkg/business and pkg/adapters untagged, with Postgres and Vault booted;
  • accounts pkg/infra full DB suite (Postgres);
  • module/tools (both authority gates);
  • frontend solution-installations (58);
  • the naming gate (tree and this body) and check-boundaries.sh.

Not verified:

  • codefly ci run, the full frontend suite and tsc.
  • No booted graph and no MCP client driven end to end.
  • No module with invoke-only bindings was exercised running. M1's effect on such a module is inferred from its contract and the derivation code, not observed.

m5 (from review): the fence connection (authorityUse pool, default size) is held for the whole confined mint. No self-deadlock path exists: nothing on that path takes a second authority lock on another connection. But pool pressure under MCP load is new, and nothing measures it yet.

🤖 Generated with Claude Code

antoinetoussaint-byte and others added 2 commits October 11, 2026 20:09
…s it consumes

A mint made with a resource-confined credential is held to the consumer that
credential was delivered to. Its ceiling is now that consumer's own
declaration, not the installation's `allowed_audiences` / `allowed_scopes`.

The declaration already reaches this host in the terms a mint is checked in.
A consumer's published contract (module.contract.codefly.yaml) names each
module it calls on a person's behalf: the audience a capability for that module
is addressed to, plus the scope ceiling of every operation it redeems there.
The render derives the authority document from it and delivery signs it. This
host derives its registry from the signed document, the applied presence and
the envelope, and the organization's installation scope narrows the result.
business.ConsumerInvokeCeiling reads the invoke bindings of that grant for the
installation the perimeter stated, through AuthorizeModuleCapability, sealed to
that installation. A confined mint must name an audience the consumer declares,
and its scopes must fit one binding declared there.

The installation's lists only narrow that ceiling. When empty they add no
bound, and a list naming more than the declaration admits nothing extra. An
installation that records no delivered authority declares nothing, and a
confined mint under it is refused by name.

The decision holds until the capability is signed. StartTask opens the
authority use span for a confined request before any other transaction, and
the ceiling read shares that span.

The proto comment on InstallSolutionRequest.allowed_audiences, the install
form's description and the gateway's AGENTS.md now state this rule. The
regenerated artifacts all follow from the proto comment: both contract
descriptors, the catalog, the module package, the generated Go and TypeScript,
both swagger documents, and the published client's patch version. They were
regenerated with CI's pinned CLI (0.1.185), and the chain leaves origin/main
unchanged.

Closes #1083

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…only bindings, and re-decides after signing

The confined mint's ceiling is the consumer's delivered authority: the
invoke bindings of its registry grant, as the installation consented to them.
What reaches that grant is a module's published contract. A solution's
`api.consumes` does not, and the docs now say so. Rendering it is the
delivery half of #1083.

- Lookup is optional on an operation audience, like headless. A binding that
  declares only invoke is kept, not dropped from the registry. A lookup
  exchange under it is refused by name, and installation consent intersects
  lookup to nil.
- A module the consumer declares that the installation does not consent to is
  refused as not consented, and the message names reinstalling as the remedy.
  Consent stays fixed at install.
- A capability addressed to the consumer itself is refused by name.
- Capabilities withdrawn by consent are named, not reported as an inactive
  installation.
- After signing, the ceiling is decided again through the same authority, in
  the same span. A changed declaration or consent refuses the capability
  (Aborted).

The proto comment no longer says a solution's declaration is the ceiling;
regenerated with CI's pinned CLI 0.1.185. Adds the review's tests, including
Postgres-backed ones.

Refs #1083

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@antoinetoussaint-byte antoinetoussaint-byte changed the title fix(accounts): a confined mint's ceiling is what its consumer declares it consumes fix(accounts): the confined mint's ceiling is the consumer's delivered authority (host half of #1083) Oct 12, 2026
antoinetoussaint-byte and others added 3 commits October 12, 2026 14:02
… does

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Resolutions:
- module_operation_audience.go: main's self-admitting exception (a headless
  or delegation binding may name its own module) is kept, and so is this
  branch's optional lookup. The consumer ceiling refuses the consumer's own
  audience before reading any binding. With main's exception that check now
  stops a self-admitting binding from admitting a confined mint; it is
  documented and pinned by TestASelfAdmittingBindingNeverAdmitsAConfinedMint.
- Generated contracts, catalog, module package and the saas-sdk contract:
  regenerated from the merged inputs (main's datasource change plus this
  branch's installations comment) with CI's pinned CLI 0.1.185, not
  hand-merged. saas-sdk moves to 0.3.27, because main already took 0.3.26.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… the own-audience refusal comes first

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@antoinetoussaint-byte
antoinetoussaint-byte added this pull request to the merge queue Oct 12, 2026
Merged via the queue into main with commit 404127d Oct 12, 2026
24 checks passed
antoinetoussaint-byte added a commit that referenced this pull request Oct 12, 2026
#1084's confined-mint ceiling composes with the core mint path unchanged: in
StartTask its authority use span opens before any other transaction and stays
open across the core signature, its ceiling (the consumer's delivered
authority, which the installation only narrows, with the own-audience refusal
first) is decided before the viewer's authority and before the audience
installation the capability is sealed to, and its post-signing re-check
(ConsumerCeilingStillCurrent) runs between the core signature and the journal.
#1084's own confined-mint suites now run against the core mint and pass.

The seven conflicts were generated contract artefacts differing only in
digests, regenerated from the merged protos with CI's pinned codefly 0.1.185
(proto, go generate, contracts, SDK); a second pass changed nothing.

The combined gateway -> StartTask qualification declares what a deployment
under #1084 records: the consumer solution's delivered authority (one invoke
binding reading the module) and its installation recording it, which the
gateway's entitlement names. It now also proves the ceiling through the real
gateway: a capability addressed to the consumer itself and one outside the
declared binding are both refused with #1084's own reasons. Forcing the
ceiling to admit everything signs the second and fails the test.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@antoinetoussaint-byte
antoinetoussaint-byte deleted the issue-1083-a-confined-capability-s-ceiling-should-be-the branch October 25, 2026 17:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A confined capability's ceiling should be the modules its consumer declares it consumes, not the installation's audience list

1 participant