Skip to content

fix(frontend): a module page with roles installs through the review, like a solution - #1284

Merged
antoinetoussaint-byte merged 5 commits into
mainfrom
fix/module-page-installs-through-review
Oct 30, 2026
Merged

antoinetoussaint-byte merged 5 commits into
mainfrom
fix/module-page-installs-through-review

Conversation

@antoinetoussaint-byte

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

Copy link
Copy Markdown
Contributor

The refusal (local QA, 2026-10-30, after #1282)

Admin → Modules & solutions → Install on a module page that declares the person roles installer, editor and viewer and installation_role: none opened the agent-installation form. That form asks for an organisation role. With one chosen, the host refused:

InstallationService/InstallSolution 403
UI: The host refused the install: the delivered module declares no standing machine authority
accounts: (refusal) request refused procedure=/saas.accounts.v1.InstallationService/InstallSolution code=permission_denied reason=the delivered module declares no standing machine authority

With "Module viewer" chosen, the long role label also pushed the Install button off-screen: x=2031 in a 1628-wide viewport.

Cause

Two causes, one on each side of the wire:

  • frontend: solution-installations-page.tsx chose the form by kind. Every kind === "module" target went to InstallModuleDialog, and that form requires an agent role.
  • accounts: nothing on the wire could install such a module. InstallSolutionRequest.role_id was a required uuid, so leaving the role out was refused 400 by validation. The 403 above is the refusal you get when you supply a role. prepareInstallation already handles an empty role, selecting the declared installation_role or none. Until now only the in-process supporting-module convergence could reach it.

The handoff proposed sending a module page through ReviewSolutionInstall → InstallReviewedSolution. That route is refused twice:

  • accounts' reviewSolutionInstall rejects Kind != "solution".
  • The shared authority core's Install refuses a module ("a module is installed by composition").

The core's ReviewOffer comment states the contract: a module page's offer carries its roles but no consent tuple, and its installation stays the host's. This change therefore uses the host's module installation and reviews the module page from the same offer the Setup tab reads.

Fix

  • Which form opens follows the authority's answer, not the target's kind. The page now asks useRolesDeclared, the signal feat(accounts): a module page's roles are accepted and gate its page like a solution's #1282 uses for the Setup tab:
    • A module page whose offer names roles opens a review read from ReviewOffer: its machine access, its call ceiling and what each role permits. The admin must approve before Install is enabled. The install goes through InstallSolution with no agent role, then the page navigates to /admin/solutions/<id>. There, the kit's Setup grants each role to a team.
    • A module the authority refuses (it declares no roles) keeps the agent-role form.
    • While roles are being read, a delayed status shows and Install stays disabled.
  • accounts:
    • role_id on InstallSolutionRequest is now optional: IGNORE_IF_ZERO_VALUE, and when given it must still be a uuid.
    • prepareInstallation refuses an empty role for a target that declares neither a named role nor none.
    • Regenerated with codefly generate proto (CLI 0.1.221, the CI pin) and go generate ./pkg/business ./pkg/adapters ./pkg/cataloggen.
  • Approval is browser-side. The module review's approve checkbox is checked only in the browser. A module page's offer carries no consent digest (authority#61), so the host has nothing to bind the approval to. The install's approval binds the catalogue target and generation, as the agent-role form's does. The host still selects the standing role from the signed declaration and refuses a caller role that would override it.
  • Only NotFound means "no roles". useRolesDeclaredReview takes only the offer review's NotFound as "declares no roles". Any other failure is shown on the page with Try again, so it never opens the agent-role form for what may be a module page. useRolesDeclared keeps its contract.
  • Setup grants a module page's teams. Setup offers a module page no staged team-per-role picker: the picker binds to a solution's consent digest. Its roles take teams through Setup's own role grant form (GrantRole).
  • Shared rendering. The solution review and the module page review render declared access through one DeclaredAccess component.
  • Layout fix. In the agent-role form, the form and the select trigger may now shrink (min-w-0) and the label truncates. The full label is the trigger's title.

Reused: useRolesDeclared, OfferService.ReviewOffer, the existing useInstallModule mutation and InstallSolution, prepareInstallation's declared-role selection, and the kit's Setup grant form. Deliberately not added:

  • a module path through InstallReviewedSolution, which the core refuses;
  • staged team choices inside the module review: the module's offer has no consent digest to bind them to, so roles take teams in Setup.

Tests

  • frontend, solution-installations-page.container.test.tsx, three new tests (all three fail against main's UI):
    • Bare module: a module whose offer is refused opens the agent-role form. The test checks that ReviewOffer was asked for that target.
    • Module page with roles: the review shows module access and roles and has no agent-role field. Install is disabled until approval. InstallSolution is sent with no roleId. The page then navigates to the installation's Setup page.
    • Long role label: the form and trigger can shrink, the label truncates, the full text is in title, and Install sits in the same form. happy-dom computes no layout, so this test checks the CSS that bounds it. A separate Chrome check of the same structure measured the form's right edge at 3883 px before the fix and 1078 px after, inside a dialog ending at 1094 px.
  • frontend, review follow-ups (each fails on the first commit):
    • A failed read of a module's roles shows the refusal with Try again rather than the agent-role form.
    • The hook maps NotFound to "no roles" and Unavailable to "unknown".
    • A module page's Setup has the role grant form and no staged picker.
  • accounts:
    • The host conformance suite installs a module page that declares a named installation_role, with an empty role_id. It asserts that the agent's one standing grant is that declared role.
    • TestModuleNoStandingSelectionRequiresCurrentPairedDeclaration gains "legacy without a role refused" (fails without the guard).
    • New TestInstallSolutionRequestRoleIsOptionalButWellFormed (fails on main's descriptor).
    • The tenancy fixture now supplies the role the RPC always carried.
  • Run locally:
    • npm run typecheck, and the full pure vitest suite: 357 files, 5061 tests.
    • The ui-inventory test, and biome and eslint on the changed files.
    • go test -tags pure ./pkg/business/, go test ./pkg/adapters/, and go vet.
    • module/tools go test ./....
    • The naming gate (tree and records), the commit-identity gate, the generated-pins gate, the interface-docs gate, codefly generate contracts --check and the generated-libraries check.
  • Not run: the Postgres-backed accounts integration tests. I checked that every InstallSolution call in them passes a role. I did not re-run this in the browser against a live composition.
  • Generated output, in its own commit:
    • The exported contracts (codefly generate contracts saas-starter).
    • The generated libraries (scripts/ci/check-generated-libraries.sh), with CLI 0.1.221. The source-read SDK's client step needed --force once, because its recorded contract digest moved.
    • codefly generate contracts --check and the library check both pass on the committed tree.
    • @codefly-dev/saas-sdk moves to 0.3.42 because its generated binding changed (kit-version gate).

Classification

Fix, made at the two owners:

  • the frontend now chooses the form from the authority's declared roles;
  • accounts now accepts an omitted role where the delivered declaration decides it.

No stopgap.

🤖 Generated with Claude Code

…like a solution

Admin -> Modules & solutions sent every module target to the agent-role
form, so a module page that declares installer/editor/viewer roles and
installation_role none asked for an organisation role and was refused
403 "the delivered module declares no standing machine authority". No
wire path could install it: InstallSolution's role_id was a required
uuid, so leaving the role out was refused 400 instead.

The Install form now follows the authority's answer, not the target's
kind (useRolesDeclared, the signal the Setup tab already uses). A module
page whose offer names roles opens a review read from that offer (its
machine access and what each role permits), is installed through
InstallSolution with no agent role, and lands on its Setup page, where
each role takes a team. A module the authority refuses (no roles) keeps
the agent-role form. The shared authority core installs only solutions
(Install refuses a module; its ReviewOffer for a module page carries no
consent tuple), so a module page stays this host's installation rather
than going through InstallReviewedSolution.

accounts: InstallSolutionRequest.role_id may be empty; the host's
prepareInstallation already selects a declared installation_role or
none, and now refuses an empty role for a target that declares neither.
Regenerated with codefly generate proto (CLI 0.1.221) and go generate.

The solution and module page reviews share one DeclaredAccess rendering.
A long role label no longer widens the agent-role form past the dialog:
the form and trigger may shrink and the label truncates, with its full
text as the trigger's title.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor Author

Review: module-saas-starter#1284 at 0fcb5bf (base a75fae3)

Blocker

  1. The exported contract snapshots were not regenerated, so CI is red. The job "Codefly SDK boundary" fails at "Check exported API contracts are in sync" (run 41129492485, job 124316487411). Both module/contracts/api/accounts/{connect,authority}/proto/saas/accounts/v1/installations.proto still say role_id … string.uuid = true.
    • Fix: run codefly generate contracts saas-starter (CLI 0.1.221), then scripts/ci/check-generated-libraries.sh.
    • The SDK copy is generated from the connect snapshot (saas-sdk/scripts/generate.mjs:60), so it will also have to be regenerated.
    • The existing difference between src/gen and the SDK copy is already on main. It is not this PR's.

Verified sound

  • Authority.
    • An empty role for a module with no declaration is refused (installation_registry.go:98).
    • A named role is chosen from the signed declaration (installation_by_name.go:158 → solution_roles.go:295). A role the caller sends must match it, so the request cannot widen it (:91).
    • "none" refuses any role the caller sends (:79).
    • The org-admin check is unchanged (installation_rpcs.go:91). The installer path still requires a UUID role (module_installations.go:126).
  • Wire compatibility. Only validation was relaxed (IGNORE_IF_ZERO_VALUE); field presence did not change, so old clients still work. The generated code matches the 0.1.221 pin, and sync-drift is green.
  • The review the page shows is the host's ReviewOffer answer, not assembled by the client.
  • Tests. None were weakened (installation_dynamic_test.go:211 now sends a role, as it must). I ran the accounts tests touched and the three new frontend tests: all pass.

Follow-ups

  1. No positive test covers a named installation_role with an empty role_id (installation_registry.go:84-95). The PR makes this path newly reachable.
  2. The approve checkbox is enforced only in the browser. The approval is tied to the catalogue generation (mutations.ts:36), not to the offer shown. A module-page offer has no digest (authority#61), so this is acceptable, but the PR should say so.
  3. useRolesDeclared treats every error as "declares no roles" (when-roles-declared.tsx:33). A transient error opens the agent-role form for a module page; the server refuses it, which is safe, but the person is stuck. Only NotFound should mean that.
  4. In Setup, teams are granted only through the kit form (solution-setup.tsx:510-514 → core GrantRole, which reads module records through HostInstallsModules). The path exists in code but is not proven live. The disabled team picker (installation-team-choices.tsx:90) sits beside it; hide it for module pages.

VERDICT: hold — the exported contracts (and then the SDK copy) must be regenerated before CI can pass.

antoinetoussaint-byte and others added 4 commits October 30, 2026 17:22
… the optional role_id

Output of `codefly generate contracts saas-starter` and
`scripts/ci/check-generated-libraries.sh` (CLI 0.1.221, the CI pin)
after InstallSolutionRequest.role_id became optional. The source-read
SDK's client step needed `--force` once, because its recorded contract
digest moved. No hand edits.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… pages grant teams in Setup

Review of #1284.

- useRolesDeclaredReview: only the offer review's NotFound means a module
  declares no roles. Any other failure leaves the answer unknown, and the
  install page shows it with Try again instead of opening the agent-role
  form for what may be a module page. useRolesDeclared keeps its contract.
- Setup offers a module page no staged team-per-role picker. The picker
  binds to a solution's consent digest, and a module page's offer has
  none, so it could only stay disabled. Its roles take teams through
  Setup's own role grant form. The setup test that gave a module offer a
  digest now uses the shape the authority sends.
- The host conformance suite now installs a module page whose
  installation_role is a named role with an empty role_id, as the review
  sends it, and asserts that the agent's one standing grant is that
  declared role.
- AGENTS.md states the NotFound rule, the browser-side approval and the
  Setup path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The SDK's generated InstallSolutionRequest binding changed with the
contract regeneration, so the published package moves to a new version,
as the kit-version gate requires.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@antoinetoussaint-byte
antoinetoussaint-byte added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit 70fcf6f Oct 30, 2026
25 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