Skip to content

feat(solution/manifest): a consumed API declares the scopes it asks for, and derives the solution's authority request - #784

Merged
antoinetoussaint-byte merged 2 commits into
mainfrom
solution-consumption-authority
Oct 12, 2026
Merged

antoinetoussaint-byte merged 2 commits into
mainfrom
solution-consumption-authority

Conversation

@antoinetoussaint-byte

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

Copy link
Copy Markdown
Contributor

The delivery half of codefly-dev/module-saas-starter#1083, core's part. The render is codefly-dev/cli#972; the host half is codefly-dev/module-saas-starter#1084.

Why

A host bounds the capability a person's delegated client mints by what the consumer it was delivered to declares it consumes. It reads that from the consumer's registry grant, which is derived from the consumer's signed authority document. A renderer derives authority documents only from module.contract.codefly.yaml. A composed solution declares what it consumes in solution.codefly.yaml's api.consumes, which named module, service and endpoint, and no audience or scopes. So a solution had no authority document at all.

The shape (revised after review)

schema_version: codefly.solution-manifest/v1   # a request needs v1; v0 is still read
api:
  consumes:
    - id: documents
      protocol: rest
      module: documents
      service: documents
      endpoint: rest
      as: documents                              # the runtime's facade name; no rule ties it to anything
      audience: {from: documents/documents-prefix}   # a slot, resolved like a contract's
      revision: 2                                # optional, default 1
      scopes:                                    # optional; present means non-empty
        - resource_kind: documents.passages
          actions: [read]
  • Manifest.ConsumptionContract(principal) turns the scoped entries into a modulecontract.Contract: one invoke binding per entry, with the entry's ID, its audience slot, its revision, and the declared scopes as explicit ceiling scopes. Namespaces, queues, ceilings and destinations are written as empty lists. The request is then judged by the module contract's own rules: grammar of kinds and actions, a slot key that names its meaning (*-audience/*-prefix), and revision. Its audience is resolved by the module contract's own Resolve, with the same secret and ambiguity refusals. Nothing is reimplemented; the earlier copied grammar and the Resolved built outside the package's rules are gone.
  • The audience is the composition's to say. A module composed as model may verify modelservice. The earlier rule tying as to module is gone, and so is "audience = composed name".
  • LoadConsumption(data) reads the schema version and api.consumes, strictly, and nothing else. A renderer that derives authority from the consumption must not fail on a section it does not read. A manifest that Load refuses for its agent block, or for a section this core does not model, asks for no authority because of that.
  • The runtime projection carries the scopes. ConsumedAPI.Scopes is projected into CODEFLY__API_CONSUMES, so a runtime can refuse a mint outside what it declared. The scopes are written once, in the YAML.
  • Refused:
    • scopes: [], rather than read as absent;
    • audience or revision without scopes;
    • scopes without an audience;
    • scopes on an unbound entry or on an exposed API;
    • a request in a v0 manifest. An older reader would read it as asking for nothing, so v1 makes it refuse the version by name instead.
  • modulecontract.Slot gains MarshalJSON ({"from": …}), so a manifest's canonical JSON spells a slot the way its YAML does.

One open question for the owners: merging a solution's derived consumption with a module contract it also publishes, for example for its destinations. The CLI refuses that combination by name for now.

Classification: fix, at the owner.

API changes callers will see:

  • ConsumedAPI now holds a slice, so it is no longer comparable with ==. The CLI's one test that compared it moves to reflect.DeepEqual in cli#972. solution-runtime-go has no such comparison in its tree; I checked with grep only, not a build.
  • ConsumptionAuthority, from the first revision of this PR, is replaced by ConsumptionContract and was never released.

Tests

solution/manifest/consumption_authority_test.go, with structural fixtures in testdata/consumption/ and no downstream names:

  • a scoped entry is one invoke binding, and the audience is resolved from the composition;
  • a module whose audience differs from its composed name is addressed through as (the reviewer's case);
  • an unresolved audience fails with ErrUnresolvedSlot;
  • two scoped endpoints of one module give two bindings at one audience;
  • the revision can be bumped;
  • no scopes means nil, and the digest does not depend on order;
  • the runtime projection carries the scopes (the reviewer's case);
  • every refusal is checked through both Load and LoadConsumption;
  • one grammar: a corpus of kinds is accepted or refused exactly as modulecontract.Parse does (the reviewer's case);
  • LoadConsumption reads an agentless manifest and one with an undeclared section, which Load refuses, as asking for nothing, and gives the same contract as Load for a scoped one.

Verified, and not

  • Green: go test ./contracts/... ./solution/... ./internal/... and go vet ./solution/manifest. Earlier full ./... runs were green except two packages that pass alone and do not import this one.
  • Not run this round: the race job, the sandbox tags and coverage.
  • This branch was rebased onto current main and force-pushed with lease.

Order

  1. fix(accounts): the confined mint's ceiling is the consumer's delivered authority (host half of #1083) module-saas-starter#1084 (host half).
  2. This PR.
  3. feat(gitops): derive a composed solution's authority document from its scoped api.consumes cli#972, pinned to this PR's merge commit on core main as a pseudo-version. No core tag is cut here; a tag is a release and needs the owner's say-so. cli#972 explains why its conformance-matrix test needs a release line.

🤖 Generated with Claude Code

antoinetoussaint-byte and others added 2 commits October 12, 2026 14:25
…or, and derives the solution's authority request

A composed solution is a module, but it declares what it consumes in
solution.codefly.yaml's api.consumes, and never in a module contract.
Nothing derived an authority request from that declaration. So a solution
held no authority document and no registry grant, and a host that bounds a
person's capability by what the consumer declares had nothing to read for it.

An api.consumes entry may now carry `scopes`: the {resource_kind, actions}
the solution asks for at the consumed module when it calls it on a person's
behalf. They are validated to the module contract's grammar, refused on
exposes and on unbound entries, and kept canonical in the digest. A scoped
entry whose `as` names another audience than its module is refused, because
a runtime addresses the module by `as` and the binding by its name.

Manifest.ConsumptionAuthority(principal) derives the same
modulecontract.Resolved a contract resolves to. Each scoped entry becomes one
invoke binding with the entry's ID, addressed to the consumed module's name,
and bounded by exactly the declared scopes. Namespaces, queues and scope
ceilings are empty, not absent. An entry without scopes asks for nothing, and
a solution with none returns nil.

docs/solution-host-binding.md describes it beside the module contract.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…act, and read only api.consumes for authority

From review:

- The audience is a slot ({from: group/key}) on a scoped entry, resolved by
  the module contract's Resolve, because the audience a module verifies is
  the composition's to say. The rule tying `as` to the module is gone.
- ConsumptionContract builds a module contract, so the contract's rules hold
  the request's grammar (kinds, actions, slot key meaning, revision) and its
  Resolve handles secrets and ambiguity. This replaces
  ConsumptionAuthority, which built a Resolved past the rule kit and
  duplicated the grammar.
- LoadConsumption reads only the schema version and api.consumes, strictly.
  A section that has nothing to do with consumption no longer fails a reader
  that derives authority.
- The runtime projection (ConsumedAPI) carries the declared scopes.
- `scopes: []` is refused. An entry's revision can be bumped. A request
  requires schema codefly.solution-manifest/v1, and v0 is still read.
- Slot marshals as {"from": ...} in JSON, so canonical bytes spell it as YAML
  does.
- Fixtures use structural names.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@antoinetoussaint-byte
antoinetoussaint-byte force-pushed the solution-consumption-authority branch from 4b58bf6 to 03d6e85 Compare October 12, 2026 18:28
antoinetoussaint-byte added a commit to codefly-dev/cli that referenced this pull request Oct 12, 2026
…esolve its audience like a contract

From review:

- The render reads a solution manifest through core's LoadConsumption: the
  schema version and api.consumes, strictly, and nothing else. A solution
  whose manifest the strict loader refuses for an unrelated section (no agent
  block, a section core does not model), and which asks for no authority,
  renders as before.
- A solution's consumption is a module contract (core's
  ConsumptionContract), resolved through the same Resolve as a published one.
  Its audience is the composition's, not the composed module name.
- The render refuses a scoped entry for a module the workspace does not
  compose, and one addressed to the solution itself, by module or by a
  resolved audience equal to its principal. The host could never install
  either.
- A contract plus scoped consumption is still refused by name, including a
  destinations-only contract, until merging the two is decided.
- Fixtures use structural names. Adds the review's tests.

Pins core at codefly-dev/core#784's head as a pseudo-version.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@antoinetoussaint-byte
antoinetoussaint-byte merged commit 7ab1214 into main Oct 12, 2026
9 checks passed
@antoinetoussaint-byte
antoinetoussaint-byte deleted the solution-consumption-authority branch October 12, 2026 18:46
antoinetoussaint-byte added a commit to codefly-dev/cli that referenced this pull request Oct 12, 2026
The PR-head pseudo-version named a commit the squash merge does not keep.
This one is the commit core main carries, with an identical tree.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
antoinetoussaint-byte added a commit to codefly-dev/cli that referenced this pull request Oct 12, 2026
go.mod pins core main at codefly-dev/core#784 by pseudo-version, which is the
v0.19.2 release line, so the matrix declares that line in the same change
(docs/supported-matrix.md, "Updating the core pin"). The line has no release
yet, and the docs say so.

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
…solution's consumed APIs declare their scopes (#787)

Carries, since v0.19.1:
- #783 workcontext: StartInput/ChildInput take an issuer-supplied SessionID
  (empty mints a fresh one; a derived capability may not reuse its parent's).
- #784 solution/manifest: solution-manifest/v1 — a consumed API declares
  `scopes` and `audience: {from: <group>/<key>}`, and the manifest derives the
  solution's authority request from them; the runtime projection carries the
  declared scopes.
- #778 CI: config-defined hooks refused; git asked which filter each path gets.
- #779, #780, #781, #786: dependency updates (Go modules, rust image, pydantic,
  GitHub Actions with the recorded job shapes and coverage-tool version).

Co-authored-by: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
gael-obin pushed a commit to gael-obin/cli that referenced this pull request Oct 13, 2026
…s scoped api.consumes (codefly-dev#972)

* feat(gitops): derive a composed solution's authority document from its scoped api.consumes

The render derived an authority document only from module.contract.codefly.yaml.
A composed solution declares what it consumes in solution.codefly.yaml and
never in a contract, so it got no authority document, and a host that bounds
a delegated person's capability by the consumer's declaration had nothing
for it.

A module's authority request now comes from exactly one source: its contract,
or its solution manifest's scoped api.consumes, derived through core's
Manifest.ConsumptionAuthority. Each scoped entry becomes an invoke unit
addressed to the consumed module. The manifest is read strictly here, unlike
the run path's lenient read, because an authority document is derived from
it. Asking from both sources is refused. The existing rules apply unchanged:
one module-identity service, a presence, and the solution's composed name as
its principal. A solution that asks for scopes with no presence is refused
instead of being dropped.

Pins core at codefly-dev/core#784's commit until that is tagged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* style(gitops): drop the blank import line the contract import left

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(gitops): read only api.consumes for a solution's authority, and resolve its audience like a contract

From review:

- The render reads a solution manifest through core's LoadConsumption: the
  schema version and api.consumes, strictly, and nothing else. A solution
  whose manifest the strict loader refuses for an unrelated section (no agent
  block, a section core does not model), and which asks for no authority,
  renders as before.
- A solution's consumption is a module contract (core's
  ConsumptionContract), resolved through the same Resolve as a published one.
  Its audience is the composition's, not the composed module name.
- The render refuses a scoped entry for a module the workspace does not
  compose, and one addressed to the solution itself, by module or by a
  resolved audience equal to its principal. The host could never install
  either.
- A contract plus scoped consumption is still refused by name, including a
  destinations-only contract, until merging the two is decided.
- Fixtures use structural names. Adds the review's tests.

Pins core at codefly-dev/core#784's head as a pseudo-version.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* style(gitops): index the consumption loops instead of copying each entry

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore(deps): pin core at main's merge of codefly-dev/core#784

The PR-head pseudo-version named a commit the squash merge does not keep.
This one is the commit core main carries, with an identical tree.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore(conformance): declare the v0.19.2 core line the pin is on

go.mod pins core main at codefly-dev/core#784 by pseudo-version, which is the
v0.19.2 release line, so the matrix declares that line in the same change
(docs/supported-matrix.md, "Updating the core pin"). The line has no release
yet, and the docs say so.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.

1 participant