feat(managementapi): wire the verdict seam (S09 Verdicts plan: 4/10)
What
Wires the advisory permission-verdict seam into the management API's composition root. Verdicts tell the UI which controls to render; they gate no route, and every request is still authorized by the decision layer.
Deps.Verdicts fronts the already-merged authz.VerdictEvaluator. Three posture arms at the composition root: both clients wired enforces; the operator's authorization.unenforced opt-in answers all-allow without touching IAM or GLAZ; anything else answers 503. The point-check path's allow-all fallback deliberately has no counterpart — an unwired boot renders no control it cannot justify. The spec's refuse-to-boot arm for an unconfigured boot is the enforcement plan's; this step's 503 arm is the seam's own fail-closed default, not that section's discharge.
The verdict path reads through its own relationship cache, separate from the point-check one. A verdict read keys on the whole page's object union, so its keys almost never repeat; sharing the point-check cache would fill it with those keys until an overflow clear() evicted that cache's warm entries.
logManagementVerdictWiring names the posture at boot. The opt-in and an unconfigured boot both show no IAM client, so the existing wiring log is identical across the two while one answers all-allow and the other 503.
glazClient gains glaz.BatchChecker, so the composition root reaches the batch seam without a type assertion that would fail silently.
OpenAPI: the includePermissions parameter, the NamespacePermissions and RepositoryPermissions schemas (one closed action set each), Repository.permissions, and the CacheControlPrivate / VaryAuthorization header components. All unreferenced until the consuming operations land — the repo's redocly config permits it, and those are redocly lint's only warnings. Two schemas rather than one shared: a single schema could only satisfy both surfaces by dropping required: to their six-action intersection, and then read_artifact is required nowhere and the absence-is-never-a-verdict rule fails silently.
No route behavior changes. No handler reads the seam yet; consumers are steps 5, 6, and 8.
Reviewable LOC
685 added / 13 removed, past the 500 guardrail. Splitting would not help: the seam, its five construction sites, and the shared contract components are one atomic change — a part landing without the others either fails requireDeps' nil-check or leaves a $ref dangling.
| Group | LOC |
|---|---|
| tests | +484 / −10 |
| production Go | +105 / −3 |
| OpenAPI contract | +96 |
Production Go is 105 lines across 6 files. The test share is this package's fixed cost: newTestDeps and six wireManagementAPI integration call sites each take one argument per seam.
Merge order
handler_test.go's seam count is 93 (main's 92 plus this seam). !2224 (merged) and the enforcement stack's !2252 (merged)/!2254 (merged)/!2266 (merged) also write that line — whoever lands second must re-derive the count from Deps rather than copy it, since two MRs writing the same number merge clean and leave the union wrong.
e2e
No scenario added or affected: this step changes no route, no request path, and no response body. The verdict-opt-in scenarios belong to the consuming steps, per the plan's Testing Strategy.
Spec coverage
Spec: docs/specs/S09-authorization.md. Rows owned by merged layers or other steps say so.
| # | Criterion | Tests |
|---|---|---|
| AC-5 | Infrastructure failure fails closed | Decision layer's, merged; the verdict evaluator's arm is internal/authz/verdicts_test.go. |
| AC-20 | Boot posture (verdict clause) | TestManagementVerdictEvaluator_UnenforcedAnswersAllAllow; boot-error and stub-retirement clauses are the enforcement plan's. |
| AC-22 | Detail verdicts | Step 5; the closed repository action set is TestContract_PermissionSchemas_AreClosedActionSets/repository. |
| AC-23 | List verdicts under a namespace role | Step 6; the closed namespace action set is TestContract_PermissionSchemas_AreClosedActionSets/namespace. |
| AC-24 | Opt-out is inert | TestRepository_PermissionsOmittedWhenUnset, TestContract_Repository_DeclaresOptionalPermissions, TestContract_IncludePermissions_IsAnOptInParameter; per-surface arms are steps 5, 6, and 8. |
| AC-25 | Malformed BatchCheck fails closed | Evaluator's, internal/authz/verdicts_test.go; listing-filter arm is the enforcement plan's. |
| AC-26 | Management reads are cache-private | Step 9; the header components are TestContract_CacheHeaders_AreDeclaredOnce. |
| E-8 | A canceled request receives no authorization-layer write | Evaluator's abandon flag, internal/authz/verdicts_test.go; TestManagementVerdictEvaluator_UnconfiguredFailsClosed pins the unconfigured arm reports abandon false. |
| S-3 | Fail closed everywhere | TestManagementVerdictEvaluator_UnconfiguredFailsClosed covers the unconfigured verdict arm; the other paths' arms are merged. |
| S-6 | The unenforced opt-in is the largest hazard | TestManagementVerdictEvaluator_UnenforcedAnswersAllAllow pins what the posture answers; the config guard is internal/config's, merged. |
| S-7 | Verdicts are advisory and organization-bound | TestManagementVerdictEvaluator_BothClientsEnforce pins the enforced arm's anonymous all-deny; organization binding is step 7's. |
Criteria not listed (AC-1–4, 6–19, 21, 27; E-1–7, 9; S-1, 2, 4, 5, 8) are owned by merged layers or by steps 5–9 and the enforcement plan; this MR changes no behavior they cover.
Related to #670 (closed)