diff --git a/docs/plans/2026-07-31-apikey-acl-enforcement-design.md b/docs/plans/2026-07-31-apikey-acl-enforcement-design.md index ccbeae80..03d4f9cd 100644 --- a/docs/plans/2026-07-31-apikey-acl-enforcement-design.md +++ b/docs/plans/2026-07-31-apikey-acl-enforcement-design.md @@ -401,6 +401,10 @@ stale-central-2 trap from the stage-6 gate. | 9 | Re-picking a valid scope unblocks it | ✅ `NAMESPACE raw` → saved | | 10 | The authored grant **deploys** | ✅ `POST /api/deployments` → `Sealed` | | 11 | The deploy gate rejects what the page rejects | ✅ same row broken by SQL → HTTP 422 `NodeAclScopeUnresolved` | +| 12 | UNS column offers area / line / equipment | ✅ `UNSAREA` / `UNSLINE` / `EQUIPMENT`, eager, after a UNS was authored under SITE-A | +| 13 | Selecting an equipment resolves | ✅ `EQUIPMENT` + `EQ-f3dd0e8e27a4`, "✓ Equipment 'packer-1' in cluster 'SITE-A'" | +| 14 | A UNS-realm grant deploys | ✅ `Sealed` | +| 15 | A cross-cluster scope is caught at both ends | ✅ re-pointed at MAIN's equipment → list marks it `UNRESOLVED` naming `MAIN`; deploy → HTTP 422 `NodeAclScopeCrossCluster` | Legs 8 and 11 are the pair that matters: the page and the deploy gate reject the same row for the same reason, which is the claim "`CheckScopeAsync` mirrors `DraftValidator`" reduces to. Leg 11 also @@ -410,12 +414,22 @@ message named `DriverInstance`. Legs 5 and 9 are the positive controls. Without them the gate passes against a page that refuses everything, which is the same failure shape stage 6's `resolvedScopes=N` leg exists to catch. -**Not covered:** a UNS-realm scope (SITE-A has no UNS areas, so the picker's UNS column correctly showed -"no UNS areas yet" and only the namespace row was selectable there). `UnsArea` / `UnsLine` / `Equipment` -selection is covered offline only. +Legs 12–15 were added in a second pass after authoring a UNS (area → line → equipment) under SITE-A, +which had none when the first pass ran. **Leg 15 is the one worth keeping:** it is the only rejection +whose id is *real*, so a check that asked only "does a row with this id exist?" would accept it and +confer authority across a cluster boundary. Both ends name the owning cluster. -Rig restored afterwards: the gate's ACL row deleted and a further deployment sealed, leaving SITE-A at -zero ACL rows as found. +**Found while authoring that UNS, unrelated to this stage:** the `/uns` **area** and **line** modals do +**not** validate the UNS segment format, so "Packaging" and "Line 1" saved happily and then failed the +deploy with `UnsSegmentInvalid` — fleet-wide, since a deployment snapshots the whole database, so one +bad area name blocks every cluster's deploy. Only `EquipmentPage.razor:570` carries the +`^[a-z0-9-]{1,32}$` rule. Exactly the authoring-vs-gate gap this stage closed for ACLs. Filed +separately; not fixed here. + +Rig state afterwards: the gate's ACL rows deleted and a further deployment sealed, leaving SITE-A at +zero ACL rows as found. The **UNS structure was kept** (area `packaging` → line `line-1` → equipment +`packer-1`, renamed to valid segments), since SITE-A having a UNS is what made legs 12–15 possible and +keeps them re-runnable. ## Known consequence to design around