Repository navigation
fix(accounts): the confined mint's ceiling is the consumer's delivered authority (host half of #1083) - #1084
Merged
antoinetoussaint-byte merged 5 commits intoOct 12, 2026
Conversation
…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>
… 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
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
deleted the
issue-1083-a-confined-capability-s-ceiling-should-be-the
branch
October 25, 2026 17:07
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.yamland from nothing else.bindings(audience slot + per-operation scope ceiling)contracts/modulepkg/gitops/authority.go,authorityInstancesOf→modulecontract.Load).module-identityservice.solutionhostOperationAudiencesfrom it, the applied presence and the envelope.api.consumes(solution.codefly.yaml)solution/manifest, clicmd/sync/solution_sdk.go) andCODEFLY__API_CONSUMES(pkg/solutionrun). It names module/service/endpoint and no scopes.Server.Contract/.well-known/module-contract. The runtime's own source says the renderer does not read it.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):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.allowed_audiences/allowed_scopesonly 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.TestASelfAdmittingBindingNeverAdmitsAConfinedMint). Decided: refused by name, because the credential already reaches that consumer's own routes. Both its prefix andsolution:<its binding>count.FailedPrecondition, naming the remedy.StartTaskopens 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,ConsumerCeilingStillCurrentdecides again throughauthorizeModuleCapabilityin the same span. It requires the declaration and the consent to be unchanged, and otherwise refuses withAborted("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.
validateOperationAudiencesrequired non-empty lookup scopes, so the reconciler omitted every binding that declares onlyinvoke. 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-attenuatedExchangeDelegatedOperationAudiencefor 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:WORK_CONTEXTS.mdstates 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 throughauthorizeModuleCapabilityinstead of reading the registry directly.Tests (the reviewer's adopted, renamed, with assertions flipped where the finding is fixed)
Unavailable), and a different installation of the same principal.deriveModulePrincipalGrantis 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 everyAdmitverdict.Aborted), and a republish that changes nothing does not.ConsumerCeilingStillCurrentis tested for unchanged, withdrawn, moved binding and principal gone.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.TestAnAudienceWithoutItsLookupIsOmittedByNamebecomesTestAnInvokeOnlyAudienceIsKept.TestInstalledOperationAudiencePolicynow holds invoke-only as valid and present-but-empty lookup as invalid.StartTaskopens, 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.InstallSolutionRequest.allowed_audiencesproto 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.Verified, and not
Green:
go vet ./...untagged, andgo test -tags pure ./...;pkg/businessandpkg/adaptersuntagged, with Postgres and Vault booted;pkg/infrafull DB suite (Postgres);module/tools(both authority gates);solution-installations(58);check-boundaries.sh.Not verified:
codefly ci run, the full frontend suite andtsc.m5 (from review): the fence connection (
authorityUsepool, 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