From 93ae57dcfebef32d2dbdd531123a7aa8c4317f70 Mon Sep 17 00:00:00 2001 From: antoinetoussaint-byte <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Fri, 30 Oct 2026 10:06:10 -0400 Subject: [PATCH 1/3] fix(conformance): a host that applies a page only from its signed payload 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 --- libraries/conformance/go/README.md | 2 +- libraries/conformance/go/conformance.go | 4 ++++ libraries/conformance/go/module_page_roles.go | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/libraries/conformance/go/README.md b/libraries/conformance/go/README.md index db8b69d..7bc0467 100644 --- a/libraries/conformance/go/README.md +++ b/libraries/conformance/go/README.md @@ -26,7 +26,7 @@ providing different labels does not remove an obligation. | Enduring narrowing through two hops | `ChildScopes` and `ParentLimits`: stored delegations retain complete ceilings; parent action/resource limits and entirely dropped kinds remain capped across target changes. Multiple action/resource rows must not become a cross product. | | Installation-owned role scopes: a role scope limited to what the installation owns, created through verified installation provenance with `create` on the whole kind, or recorded after the existing `AuthorizeTransfer` policy authorizes the change of owner; admin-only attachment, missing personal admin, unknown current owners, and an id another installation owns are refused by name; a viewer reads its installation's resources only, is refused another's with a named reason, and a narrowed child never widens; a detached resource leaves its owner and may be attached again | `InstallationResources`: two installed solutions, A (the viewer's) and B, each serving its own kind, a store module serving the shared kind, and two existing unowned resources; the suite delivers all three modules | | A solution's roles and permissions are identical on every host | `solutionDeclaration` (no host fixture beyond `UnentitledToken`): the suite delivers `conformance-declared` (an installer, two roles reaching `conformance-declared-store` through its binding, an attached-only role and a role covering no operation) and installs it as `AdminToken`. `ReviewOffer` must equal one golden offer on every host (host ids, the opaque digest and `tools_composed` left out); a grant of each role to `UnentitledToken` issues exactly its declared scopes within the ceiling, every other ceiling action refused; a redelivery narrowing two roles and withdrawing one changes both answers to exactly the new declaration and refuses the capabilities issued before it. A host that does not serve `OfferService` yet names why in `ReviewOfferNotServed`, which skips the offer comparison alone and fails once the host serves it. | -| A module page's roles, never the organization's | `modulePageRoles` (no host fixture beyond `UnentitledToken`, `ViewerToken` and `AdminToken`; the delivery workloads `conformance-page-module` and `conformance-headless-roles`): the suite delivers a module whose core manifest declares a frontend with an installer and a viewer role, serving its own kind and `ReadKind`; the viewer granted to `UnentitledToken` issues exactly its scope, owner-blind, and `ListResources` answers by that role (every resource), nothing once revoked; `ViewerToken` holds no role there, its organization's read on `ReadKind` included; an undeclared role is refused; a module declaring roles and no page is refused; redelivered without roles, the earlier capability is refused and `ViewerToken`'s organization roles decide again (`ReadKind` read). | +| A module page's roles, never the organization's | `modulePageRoles` (no host fixture beyond `UnentitledToken`, `ViewerToken` and `AdminToken`; the delivery workloads `conformance-page-module` and `conformance-headless-roles`; `PagePayload` when the host applies a page only from its prepared signed payload): the suite delivers a module whose core manifest declares a frontend with an installer and a viewer role, serving its own kind and `ReadKind`; the viewer granted to `UnentitledToken` issues exactly its scope, owner-blind, and `ListResources` answers by that role (every resource), nothing once revoked; `ViewerToken` holds no role there, its organization's read on `ReadKind` included; an undeclared role is refused; a module declaring roles and no page is refused; redelivered without roles, the earlier capability is refused and `ViewerToken`'s organization roles decide again (`ReadKind` read). | | Installation identity, installed caller, explicit organization, pending, active and revoked states | `InstalledSolution`, `InstalledCredential`, `Lifecycle` | | Absent installation versus no entitlement | `FailedPrecondition` with the absent-installation reason versus `PermissionDenied` with the holds-nothing reason | | Inventory access | Admin and owner allowed; ordinary member, anonymous and foreign-organization reads refused | diff --git a/libraries/conformance/go/conformance.go b/libraries/conformance/go/conformance.go index 26f25c0..ff1f36d 100644 --- a/libraries/conformance/go/conformance.go +++ b/libraries/conformance/go/conformance.go @@ -98,6 +98,10 @@ type Fixture struct { // SignAuthority signs the authority's own document delivered with a manifest, // with the key SignManifest signs with. Required for method-policy conformance. SignAuthority func(document []byte) (envelope []byte, err error) + // PagePayload is the signed payload the page a module_page_roles delivery + // declares carries, for a host that applies a page only once its signed + // payload is prepared. Nil delivers the page declaration alone. + PagePayload *solutionhost.FrontendPayload // Chain is a required delegate chain: a person signs in, // is issued at Start, and each hop is taken by its module on the previous diff --git a/libraries/conformance/go/module_page_roles.go b/libraries/conformance/go/module_page_roles.go index cdad912..27c8d57 100644 --- a/libraries/conformance/go/module_page_roles.go +++ b/libraries/conformance/go/module_page_roles.go @@ -29,7 +29,7 @@ func (c *client) modulePageRoles(t *testing.T, ctx context.Context, f Fixture) { if f.SignManifest == nil || f.SignAuthority == nil || f.DeliveryCredential == nil || f.AdminToken == "" || f.UnentitledToken == "" || f.ViewerToken == "" { t.Fatal("required conformance fixture missing: signed delivery, an administrator, a viewer and a member holding no role (UnentitledToken)") } - page := &solutionhost.Frontend{Title: pageModuleTitle, ExposedModule: "./Page", HostContract: 1} + page := &solutionhost.Frontend{Title: pageModuleTitle, ExposedModule: "./Page", HostContract: 1, Payload: f.PagePayload} roles := func(note string) map[string]any { return map[string]any{"scope_ceilings": []any{scopeAsk(note, []string{"read", "write"}, nil)}, "roles": []any{ map[string]any{"name": "installer", "scopes": []any{scopeAsk(note, []string{"read", "write"}, nil)}}, From 141773464b45db61a50ce556bb80dbe8675c3b4a Mon Sep 17 00:00:00 2001 From: antoinetoussaint-byte <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Fri, 30 Oct 2026 10:17:58 -0400 Subject: [PATCH 2/3] fix(core): a module page that declares roles has its offer reviewed as 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 --- libraries/authority-core/go/authority/consent.go | 10 +++++++--- .../authority-core/go/authority/module_page_test.go | 11 +++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/libraries/authority-core/go/authority/consent.go b/libraries/authority-core/go/authority/consent.go index 1544819..5fad0c8 100644 --- a/libraries/authority-core/go/authority/consent.go +++ b/libraries/authority-core/go/authority/consent.go @@ -62,7 +62,8 @@ type ReviewedOffer struct { Modules []domain.Module } -// ReviewOffer reads one solution's offer. An administrator of org reads it +// ReviewOffer reads one solution's offer, or a module page's that declares +// roles. An administrator of org reads it // with the consent tuple Install takes. A member of org who holds a role on // the solution's active installation there (granted to them or to a team of // theirs) reads the same declaration, roles, ceiling and installation without @@ -101,8 +102,11 @@ func (s *Service) reviewOffer(ctx context.Context, caller Person, org, target st if err != nil { return notFound(err) } - if m.Kind != domain.KindSolution { - return domain.Missing("%q is not a composed solution; a module is installed by composition", target) + // A module page declaring roles is reviewed as a solution is: its + // roles are what its Setup shows and grants. Its installation is + // still composition's (Install refuses a module). + if !decide.RoleGated(m) { + return domain.Missing("%q is not a composed solution or a module page declaring roles; a module is installed by composition", target) } if refused == nil { digest, err := offerDigest(m) diff --git a/libraries/authority-core/go/authority/module_page_test.go b/libraries/authority-core/go/authority/module_page_test.go index 718a8f7..2b81d7f 100644 --- a/libraries/authority-core/go/authority/module_page_test.go +++ b/libraries/authority-core/go/authority/module_page_test.go @@ -36,6 +36,15 @@ func TestAModulePageRoleEntitlesItsHolderAlone(t *testing.T) { t.Fatal(err) } + // Its offer is reviewed as a solution's: the roles its Setup shows. Its + // installation stays composition's. + offer, err := h.svc.ReviewOffer(h.ctx, h.admin(acme), acme, "pages") + if err != nil || len(offer.Module.Roles) != 2 || offer.Installation.ID != h.inst(acme, "pages").ID || offer.Approval.Digest == "" { + t.Fatalf("the administrator reviews the module page's offer: %+v, %v", offer, err) + } + _, err = h.svc.Install(h.ctx, h.admin(acme), acme, "pages", "", "", h.approval(acme, "pages"), nil) + refused(t, err, "a module is installed by composition") + h.grant("pages", "viewer", "maya") viewer := h.must(h.root("maya", "pages")) exactly(t, viewer, "pages.note", "", []string{"read"}) @@ -83,6 +92,8 @@ func TestAModulePageRoleEntitlesItsHolderAlone(t *testing.T) { } reader := h.must(h.root("ines", "pages")) exactly(t, reader, "pages.note", "", []string{"read"}) + _, err = h.svc.ReviewOffer(h.ctx, h.admin(acme), acme, "pages") + refused(t, err, "a module is installed by composition") _, err = h.root("maya", "pages") refused(t, err, "holds nothing") _, err = h.svc.GrantRole(h.ctx, h.admin(acme), authority.RoleRequest{Organization: acme, InstallationID: h.inst(acme, "pages").ID, Role: "viewer", PersonID: "maya"}) From f32d30bd5505143ee4cf527c432b23508c9b52e5 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Fri, 30 Oct 2026 11:02:50 -0400 Subject: [PATCH 3/3] fix(core): a module page's setup is offered and reviewed with no consent 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 --- .../authority-core/go/authority/consent.go | 14 +++--- .../go/authority/module_page_test.go | 39 +++++++++++++++- libraries/conformance/go/README.md | 2 +- libraries/conformance/go/module_page_roles.go | 44 +++++++++++++++++++ .../local/code/plugins/installation/offers.go | 3 +- 5 files changed, 93 insertions(+), 9 deletions(-) diff --git a/libraries/authority-core/go/authority/consent.go b/libraries/authority-core/go/authority/consent.go index 5fad0c8..4620e43 100644 --- a/libraries/authority-core/go/authority/consent.go +++ b/libraries/authority-core/go/authority/consent.go @@ -47,7 +47,8 @@ func offerDigest(m domain.Module) (string, error) { // Review reads one consistent offer. It never supplies an operation identity or // approves anything; the caller must retain the returned values when consenting. -// It is an administrator's read: the consent tuple is what Install takes. +// It is an administrator's read: the consent tuple is what Install takes, and +// a module page, which composition installs, answers none. func (s *Service) Review(ctx context.Context, caller Person, org, target string) (domain.ConsentApproval, error) { offer, err := s.reviewOffer(ctx, caller, org, target, false) return offer.Approval, err @@ -104,11 +105,12 @@ func (s *Service) reviewOffer(ctx context.Context, caller Person, org, target st } // A module page declaring roles is reviewed as a solution is: its // roles are what its Setup shows and grants. Its installation is - // still composition's (Install refuses a module). + // still composition's (Install refuses a module), so its review + // carries no consent tuple: there is nothing to approve. if !decide.RoleGated(m) { return domain.Missing("%q is not a composed solution or a module page declaring roles; a module is installed by composition", target) } - if refused == nil { + if refused == nil && m.Kind == domain.KindSolution { digest, err := offerDigest(m) if err != nil { return err @@ -181,7 +183,9 @@ func checkConsent(m domain.Module, existing domain.Installation, a *domain.Conse return false, nil } -// Offers lists delivered solution targets, not composition-installed modules. +// Offers lists what an administrator of org sets up: delivered solutions, and +// module pages declaring roles (composition installs them; their Setup grants +// their roles). A module without roles is the organization's roles' alone. func (s *Service) Offers(ctx context.Context, caller Person, org string) ([]domain.Module, error) { var out []domain.Module err := s.store.Read(ctx, func(tx Tx) error { @@ -190,7 +194,7 @@ func (s *Service) Offers(ctx context.Context, caller Person, org string) ([]doma } modules, err := tx.Modules() for _, m := range modules { - if m.Kind == domain.KindSolution { + if decide.RoleGated(m) { out = append(out, m) } } diff --git a/libraries/authority-core/go/authority/module_page_test.go b/libraries/authority-core/go/authority/module_page_test.go index 2b81d7f..8df9029 100644 --- a/libraries/authority-core/go/authority/module_page_test.go +++ b/libraries/authority-core/go/authority/module_page_test.go @@ -1,12 +1,14 @@ package authority_test import ( + "slices" "testing" "github.com/codefly-dev/authority/libraries/authority-core/go/authority" "github.com/codefly-dev/authority/libraries/authority-core/go/domain" resources "github.com/codefly-dev/authority/libraries/resources/go" basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" + "github.com/google/uuid" ) // pageModule is a module declaring a page; with roles, what a person holds at @@ -37,13 +39,29 @@ func TestAModulePageRoleEntitlesItsHolderAlone(t *testing.T) { } // Its offer is reviewed as a solution's: the roles its Setup shows. Its - // installation stays composition's. + // installation stays composition's, so the review carries no consent + // tuple, and Install refuses it whatever it is handed. offer, err := h.svc.ReviewOffer(h.ctx, h.admin(acme), acme, "pages") - if err != nil || len(offer.Module.Roles) != 2 || offer.Installation.ID != h.inst(acme, "pages").ID || offer.Approval.Digest == "" { + if err != nil || len(offer.Module.Roles) != 2 || offer.Installation.ID != h.inst(acme, "pages").ID || offer.Approval != (domain.ConsentApproval{}) { t.Fatalf("the administrator reviews the module page's offer: %+v, %v", offer, err) } + if approval, err := h.svc.Review(h.ctx, h.admin(acme), acme, "pages"); err != nil || approval != (domain.ConsentApproval{}) { + t.Fatalf("a module page's review answers a consent tuple: %+v, %v", approval, err) + } + // Its review answers no tuple to hand Install, and a well-formed tuple + // made up for it is refused for what the target is. _, err = h.svc.Install(h.ctx, h.admin(acme), acme, "pages", "", "", h.approval(acme, "pages"), nil) + refused(t, err, "INSTALLATION_APPROVAL_REQUIRED") + made := &domain.ConsentApproval{TargetID: "pages", Generation: 1, Digest: "sha256:made-up", OperationID: uuid.NewString()} + _, err = h.svc.Install(h.ctx, h.admin(acme), acme, "pages", "", "", made, nil) refused(t, err, "a module is installed by composition") + // The administrator's Setup lists it beside the solutions; a member's + // read of the list is refused. + if names := h.offers(acme); !slices.Contains(names, "pages") || !slices.Contains(names, "a") || slices.Contains(names, "store") { + t.Fatalf("the offers are the solutions and the module page, not the bare store: %v", names) + } + _, err = h.svc.Offers(h.ctx, h.person("maya"), acme) + code(t, err, domain.Denied) h.grant("pages", "viewer", "maya") viewer := h.must(h.root("maya", "pages")) @@ -94,6 +112,9 @@ func TestAModulePageRoleEntitlesItsHolderAlone(t *testing.T) { exactly(t, reader, "pages.note", "", []string{"read"}) _, err = h.svc.ReviewOffer(h.ctx, h.admin(acme), acme, "pages") refused(t, err, "a module is installed by composition") + if names := h.offers(acme); slices.Contains(names, "pages") { + t.Fatalf("a module page withdrawing its roles is still offered: %v", names) + } _, err = h.root("maya", "pages") refused(t, err, "holds nothing") _, err = h.svc.GrantRole(h.ctx, h.admin(acme), authority.RoleRequest{Organization: acme, InstallationID: h.inst(acme, "pages").ID, Role: "viewer", PersonID: "maya"}) @@ -118,3 +139,17 @@ func TestAModuleWithoutAPageDeclaresNoRoles(t *testing.T) { _, err = h.root("maya", "pages") code(t, err, domain.NotFound) } + +// offers are the names an administrator of org is offered to set up. +func (h *harness) offers(org string) []string { + h.t.Helper() + offered, err := h.svc.Offers(h.ctx, h.admin(org), org) + if err != nil { + h.t.Fatal(err) + } + var names []string + for _, m := range offered { + names = append(names, m.Name) + } + return names +} diff --git a/libraries/conformance/go/README.md b/libraries/conformance/go/README.md index 7bc0467..945ed79 100644 --- a/libraries/conformance/go/README.md +++ b/libraries/conformance/go/README.md @@ -26,7 +26,7 @@ providing different labels does not remove an obligation. | Enduring narrowing through two hops | `ChildScopes` and `ParentLimits`: stored delegations retain complete ceilings; parent action/resource limits and entirely dropped kinds remain capped across target changes. Multiple action/resource rows must not become a cross product. | | Installation-owned role scopes: a role scope limited to what the installation owns, created through verified installation provenance with `create` on the whole kind, or recorded after the existing `AuthorizeTransfer` policy authorizes the change of owner; admin-only attachment, missing personal admin, unknown current owners, and an id another installation owns are refused by name; a viewer reads its installation's resources only, is refused another's with a named reason, and a narrowed child never widens; a detached resource leaves its owner and may be attached again | `InstallationResources`: two installed solutions, A (the viewer's) and B, each serving its own kind, a store module serving the shared kind, and two existing unowned resources; the suite delivers all three modules | | A solution's roles and permissions are identical on every host | `solutionDeclaration` (no host fixture beyond `UnentitledToken`): the suite delivers `conformance-declared` (an installer, two roles reaching `conformance-declared-store` through its binding, an attached-only role and a role covering no operation) and installs it as `AdminToken`. `ReviewOffer` must equal one golden offer on every host (host ids, the opaque digest and `tools_composed` left out); a grant of each role to `UnentitledToken` issues exactly its declared scopes within the ceiling, every other ceiling action refused; a redelivery narrowing two roles and withdrawing one changes both answers to exactly the new declaration and refuses the capabilities issued before it. A host that does not serve `OfferService` yet names why in `ReviewOfferNotServed`, which skips the offer comparison alone and fails once the host serves it. | -| A module page's roles, never the organization's | `modulePageRoles` (no host fixture beyond `UnentitledToken`, `ViewerToken` and `AdminToken`; the delivery workloads `conformance-page-module` and `conformance-headless-roles`; `PagePayload` when the host applies a page only from its prepared signed payload): the suite delivers a module whose core manifest declares a frontend with an installer and a viewer role, serving its own kind and `ReadKind`; the viewer granted to `UnentitledToken` issues exactly its scope, owner-blind, and `ListResources` answers by that role (every resource), nothing once revoked; `ViewerToken` holds no role there, its organization's read on `ReadKind` included; an undeclared role is refused; a module declaring roles and no page is refused; redelivered without roles, the earlier capability is refused and `ViewerToken`'s organization roles decide again (`ReadKind` read). | +| A module page's roles, never the organization's | `modulePageRoles` (no host fixture beyond `UnentitledToken`, `ViewerToken` and `AdminToken`; the delivery workloads `conformance-page-module` and `conformance-headless-roles`; `PagePayload` when the host applies a page only from its prepared signed payload): the suite delivers a module whose core manifest declares a frontend with an installer and a viewer role, serving its own kind and `ReadKind`; an administrator's `ListOffers` lists it, and `ReviewOffer` answers its two roles and its installation to the administrator and to the role holder with no consent tuple (composition installs it), and refuses `ViewerToken`; the viewer granted to `UnentitledToken` issues exactly its scope, owner-blind, and `ListResources` answers by that role (every resource), nothing once revoked; `ViewerToken` holds no role there, its organization's read on `ReadKind` included; an undeclared role is refused; a module declaring roles and no page is refused; redelivered without roles, the earlier capability is refused and `ViewerToken`'s organization roles decide again (`ReadKind` read), and it is no longer offered. | | Installation identity, installed caller, explicit organization, pending, active and revoked states | `InstalledSolution`, `InstalledCredential`, `Lifecycle` | | Absent installation versus no entitlement | `FailedPrecondition` with the absent-installation reason versus `PermissionDenied` with the holds-nothing reason | | Inventory access | Admin and owner allowed; ordinary member, anonymous and foreign-organization reads refused | diff --git a/libraries/conformance/go/module_page_roles.go b/libraries/conformance/go/module_page_roles.go index 27c8d57..7e42614 100644 --- a/libraries/conformance/go/module_page_roles.go +++ b/libraries/conformance/go/module_page_roles.go @@ -2,6 +2,7 @@ package conformance import ( "context" + "slices" "testing" "github.com/codefly-dev/core/solutionhost" @@ -76,6 +77,46 @@ func (c *client) modulePageRoles(t *testing.T, ctx context.Context, f Fixture) { if err := role("GrantRole", "viewer"); err != nil { t.Fatal(err) } + offered := func(t *testing.T) bool { + t.Helper() + listed := c.must(t, adminCtx, "host.installation.v1.OfferService", "ListOffers", map[string]any{"organization_id": f.Organization}) + for _, o := range listMsgs(listed, "offers") { + if str(o, "target") == pageModule { + return true + } + } + return false + } + t.Run("its setup reviews its roles, with nothing to consent to", func(t *testing.T) { + if f.ReviewOfferNotServed != "" { + t.Skipf("this host does not serve OfferService yet: %s", f.ReviewOfferNotServed) + } + if !offered(t) { + t.Fatalf("an administrator's offers at %s do not list %s", f.Organization, pageModule) + } + review := func(sess protoreflect.Message) (protoreflect.Message, error) { + return c.call(as(ctx, sess), "host.installation.v1.OfferService", "ReviewOffer", map[string]any{"organization_id": f.Organization, "target": pageModule}) + } + for name, sess := range map[string]protoreflect.Message{"the administrator": admin, "the role holder": holder} { + out, err := review(sess) + if err != nil { + t.Fatalf("%s reviews %s: %v", name, pageModule, err) + } + var roles []string + for _, r := range listMsgs(out, "roles") { + roles = append(roles, str(r, "name")) + } + slices.Sort(roles) + if !slices.Equal(roles, []string{"installer", "viewer"}) || str(msg(out, "installation"), "id") != id { + t.Fatalf("%s reviews roles %v at installation %q, want [installer viewer] at %q", name, roles, str(msg(out, "installation"), "id"), id) + } + if out.Has(out.Descriptor().Fields().ByName("approval")) { + t.Fatalf("%s is handed a consent tuple for %s, which composition installs", name, pageModule) + } + } + _, err := review(viewer) + refused(t, err, codes.PermissionDenied, "requires an administrator") + }) var held protoreflect.Message t.Run("the role holder is entitled to exactly the role", func(t *testing.T) { out, err := issue(holder) @@ -140,6 +181,9 @@ func (c *client) modulePageRoles(t *testing.T, ctx context.Context, f Fixture) { t.Fatalf("organization roles do not decide again: %v", err) } exactScopeOwnedBy(t, msg(out, "context"), f.ReadKind, []string{"read"}, nil, "") + if f.ReviewOfferNotServed == "" && offered(t) { + t.Fatalf("%s is still offered once it declares no roles", pageModule) + } }) if err := role("RevokeRole", "viewer"); err != nil { t.Fatal(err) diff --git a/modules/authority/services/local/code/plugins/installation/offers.go b/modules/authority/services/local/code/plugins/installation/offers.go index c682a84..846b3ac 100644 --- a/modules/authority/services/local/code/plugins/installation/offers.go +++ b/modules/authority/services/local/code/plugins/installation/offers.go @@ -59,7 +59,8 @@ type Offers struct { runnables RunnableReader } -// ListOffers lists the delivered solutions, for an administrator. +// ListOffers lists what an administrator sets up: the delivered solutions and +// the module pages declaring roles (the core decides which). func (o *Offers) ListOffers(ctx context.Context, req *installationv1.ListOffersRequest) (*installationv1.ListOffersResponse, error) { caller, err := o.plugin.caller(ctx) if err != nil {