Skip to content

fix(core): a module page's roles can be set up and conformance-tested on a payload-serving host - #61

Merged
antoinetoussaint-byte merged 3 commits into
mainfrom
fix/conformance-page-payload
Oct 30, 2026
Merged

antoinetoussaint-byte merged 3 commits into
mainfrom
fix/conformance-page-payload

Conversation

@antoinetoussaint-byte

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

Copy link
Copy Markdown
Collaborator

This follows #59 (a module that declares a page declares roles like a solution). Two gaps stopped a host from adopting that rule.

What changed

  1. Conformance. module_page_roles delivered a module page whose frontend names no payload. A host that prepares a page's signed payload before applying the delivery refuses that page with "frontend declares no signed payload", so the case could not run there at all.
    • New Fixture.PagePayload names the signed payload the delivered page carries.
    • When it is nil, the suite delivers the bare declaration, which the local authority accepts unchanged.
  2. Core ReviewOffer. It refused every module ("a module is installed by composition"), so the one Setup UI (<SolutionSetup>) could neither show nor grant a module page's roles.
    • It now reads a role-gated module (decide.RoleGated) as it reads a solution: its roles, their grants and its installation.
    • Install still refuses a module, because composition installs it.

Classification

Both are fixes at the owning place. The suite now delivers a page that such a host can apply. The core's offer review now follows the core's own role-gating rule.

Tests

  • authority/module_page_test.go: an administrator reviews the module page's offer and gets its two roles and its installation. The review carries no consent tuple, through ReviewOffer and Review alike. Install refuses it, both with no tuple and with a well-formed made-up one. Offers lists it beside the solutions but not the bare store, and refuses a member. Once it is redelivered without roles, the review is refused and the page leaves Offers. The new assertions fail when consent.go is reverted (checked).
  • go test ./..., go vet and gofmt are clean in authority-core, conformance, authority-client and the local authority.
  • The consuming host (module-saas-starter accounts) runs module_page_roles with a prepared payload. It is pinned to this branch's head in its own PR.

Review

A fresh-context security bug-hunter reviewed head 1417734 and found no blocker (verdict: merge). It confirmed:

Its three findings are fixed in f32d30b:

  1. Conformance never called ReviewOffer on the module page. module_page_roles.go now checks, under ReviewOfferNotServed like the declaration case:
    • an administrator's ListOffers lists the page;
    • ReviewOffer answers [installer viewer] and the page's installation to the administrator and to the role holder;
    • no approval is attached;
    • ViewerToken, which holds no role there, is refused;
    • after redelivery without roles, the page is no longer offered.
      Reverting consent.go fails it ("offers ... do not list conformance-page-module").
  2. Approval digest for a module page. reviewOffer (consent.go:113) mints the tuple only for a solution, so Review and ReviewOffer give a module page an empty approval. module_page_test.go now requires that.
  3. Offers listed solutions only. Offers (consent.go:197) now lists solutions and module pages that declare roles, under the same administrator-of-org check, so the Setup list (edge/code/setup.go:243) shows a module page. The local ListOffers comment says so. Core test added.

go test ./..., go vet and gofmt are clean in authority-core, conformance and the local authority. Accounts has not run the new assertions yet (module-saas-starter#1282).

🤖 Generated with Claude Code

antoinetoussaint-byte and others added 2 commits October 30, 2026 10:06
…load can run module_page_roles

module_page_roles delivers a module page whose frontend names no payload. A
host that prepares a page's signed payload before it applies the delivery
refuses that page ("frontend declares no signed payload"), so it could not run
the case at all. Fixture.PagePayload names the payload the delivered page
carries; nil keeps the bare declaration the local authority accepts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s a solution's

ReviewOffer refused every module, so the one Setup UI could not show or grant
the roles a module page now declares. It reads a role-gated module
(decide.RoleGated) as it reads a solution: the roles, their grants and the
installation. Install still refuses a module: composition installs it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@antoinetoussaint-byte antoinetoussaint-byte changed the title fix(conformance): module_page_roles can run on a host that applies a page only from its signed payload fix(core): a module page's roles can be set up and conformance-tested on a payload-serving host Oct 30, 2026
@antoinetoussaint-byte

Copy link
Copy Markdown
Collaborator Author

codefly-dev/authority #61 review (head 1417734, base a61acf9)

go test ./... (GOWORK=off) in authority-core, conformance and the local authority (local replaces): all green.

Blockers

None.

Follow-ups (ranked)

  1. The conformance suite does not check the new ReviewOffer behaviour. libraries/conformance/go/module_page_roles.go never calls ReviewOffer on the module page. Accounts could keep refusing it and still pass (AGENTS.md rule 6). Add: admin reads the page's roles; a roleless member is refused.
  2. The approval tuple for a module page leads nowhere. authority/consent.go:113-117 mints a digest for a module page, through both ReviewOffer and Review (consent.go:51), but Install refuses any module (lifecycle.go:335). module_page_test.go:42 requires it. The UI may offer an Install that always fails. Suggest: no Approval when m.Kind != KindSolution.
  3. Offers still lists solutions only (consent.go:209). The Setup UI's list (edge/code/setup.go:243) will not show a module page; it can be reached only by typing its target. Intended or not, say so.

Hunt results

  • a) Does it widen anything? No. The authorization order is unchanged: an administrator, or a member holding a role on the active installation of this org (holdsRoleOn checks inst.Organization == org). Both run before the module lookup. The installation id is StableInstallationID(org, target), so no other org's installation can be returned. The projection (offers/offers.go:63) is the one solutions use: roles, ceiling, covered capabilities, this installation's owned ids only. An unentitled member is still refused with the same reason.
  • b) Is the conformance contract weaker? No. PagePayload is optional data from the host's fixture, placed inside a manifest still signed by SignManifest (conformance.go:101-104, module_page_roles.go:32). No test key in production paths, no if testing branch; nil keeps the earlier behaviour.
  • c) Is it consistent with feat(core): a module that declares a page declares roles like a solution #59? Yes. decide.RoleGated (decide/manifest.go:107) means a solution, or a module with roles. Delivery already refuses roles without a page (manifest.go:64). A module with no roles is still refused (local offers_test.go:174, "documents" → NotFound). Redelivered without roles, the review is refused again (module_page_test.go:95).
  • d) Was any test weakened? No. The diff only adds assertions.

VERDICT: merge (nothing widens: the same tenancy check runs before the lookup, and the gaps above are follow-ups)

…ent tuple

Offers lists the module pages that declare roles beside the solutions, so
an administrator's Setup reaches them; a page withdrawing its roles leaves
the list. Reviewing a module page answers its roles and installation but no
consent tuple, since composition installs it and Install refuses a module:
the review no longer offers an approval that leads nowhere. Conformance
asserts the listing, the administrator's and the role holder's review with
no tuple, and the refusal of a member holding no role.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@antoinetoussaint-byte
antoinetoussaint-byte merged commit 9577d43 into main Oct 30, 2026
17 checks passed
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