docs(plans): plan S09 enforcement for maven, npm, and management
🎯 What this is
The implementation plan for S09 enforcement: 20 MR-sized steps off ten independent roots, covering the Maven, npm, and REST management API surfaces, the permission-filtered repository listing, and the fail-closed boot posture that retires the allow-all pass-through. Container/OCI already enforces and contributes no step.
This is a plan merge request. Merging it is the approval gate; no step merge request opens before it lands.
🧭 Shape
| Group | Steps | What |
|---|---|---|
| Groundwork | 1–5 | The authorization.unenforced opt-in and the three rigs that need it, the glaz v2.0.0 vendir bump and its BatchCheck client, the slug-anchored wiring seam, and the masking read action |
| Maven | 6–9 | Leaf grammar and fence, RouteParser, denial renderer, wiring |
| npm | 10–13 | Same shape, plus the exported route table and its parity test |
| Management | 14–17 | Decision-core extraction, denial renderer, route bindings, enforcement |
| Listing | 18–19 | Candidate enumeration, then the filtered branch |
| Closing | 20 | Unconditional wiring, fail-closed boot, stub deletion |
🔍 What a reviewer should check
- The masking read action is a per-surface constructor parameter, not derived from the action.
delete_artifactmasks withread_artifacton the npm unpublish route and withread_repositoryon the management package delete, so no function of the action alone is well defined. - Each format gets its own hand-rolled resolution holder rather than a shared generic one, matching the three that already exist in
internal/auth,internal/iam, andinternal/namespace, and preserving a zero-value guard tied to each type's own fields. Container/OCI is untouched. - The binding table is derived in the plan. S09 attributes it to S17, which has no consolidated table: its bindings sit in per-phase prose, and the list and detail routes are unbound there entirely. Every row carries its source.
- The registration helper rejects an unbound route at construction. That is deliberate and it has a cost: while S17 Phase 6 and Phase 8 route-adding steps remain unopened, each must supply a binding as it lands. Their author needs a heads-up when that step opens.
- Two dependencies gate the listing step, both recorded rather than assumed: the deployed GLAZ sidecar must serve
BatchCheck, and the IAM request limit S09's 9,998 chunk size rests on is inferred from a buffer-sizing comment rather than an enforced cap.
📚 Research corrections carried in the plan
Where research contradicted a source, the plan says so at that point rather than substituting silently:
- S17 has no consolidated binding table, and no Phase 3 section states a binding for the artifact read routes.
- S09's IAM object cap is not enforced anywhere in
internal/iam. - The Maven and npm resolution types already carry the ADR-007 anchor fields, correcting an earlier scoping note.
- ADR-021 now splits the npm dist-tag verbs, reversing an earlier decision to hold it as written.
📏 Size
1,249 added lines, all of it one plan document plus its index row. The guidance asks for a split or a justification past 500 reviewable lines: a plan is a single artifact whose sections are cross-referential, and splitting it would land research findings apart from the steps they size, and the binding table apart from the step that consumes it. The Status table, the Mermaid graph, the dependency table, and the per-step Depends on lines were verified mechanically to agree.
🧪 Verification
- Vale clean at the hook's error level.
- Every file path, symbol, and count the plan asserts was re-verified against
mainafter rebasing onto it. - Two review passes are absorbed, including two structural findings that would otherwise have produced broken step merge requests: the
authzConfiguredgate does not exist inwire_root_dispatcher.go, and the four shipped action constants need exporting or the management bindings would not compile.
🔗 Related
Related issue: #652 (closed)
Deferred developer documentation: #799