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)

Merge request reports

Loading
Loading