Uh oh!
There was an error while loading. Please reload this page.
fix(plugin-security): withdraw the sys_capability Deactivate dialog's false grant-revocation claim (#8535) - #8619
Conversation
… false grant-revocation claim (#8535) The deactivate_capability confirmation dialog told the admin that grants and resource requirements referencing the capability 'stop resolving until re-activated'. No code path enforces that: getSystemPermissions() unions permissionSets[].systemPermissions as plain strings and requiredPermissions is matched against that string set — neither loads a sys_capability row. The table's only two production readers are seeders, which write active: true on insert and never read it back. Per the maintainer ruling of 2026-08-13 (ADR-0049 enforce-or-remove, option B), the claim is withdrawn rather than enforced; putting the registry on the authorization hot path is an architectural change needing its own card. - reword the dialog in the source object and in all four shipped locale bundles (editing the source does not rewrite bundles — they were corrected by hand) - declare active's real semantics in a field description it never had - demote active from highlightFields, the danger variant, and the two scoped list views; keep it in the full-catalogue view where it belongs - add a sweep asserting the withdrawn claim survives on no surface in any locale Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012WMpuAfA2KSdDjGF6tm1bH
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012WMpuAfA2KSdDjGF6tm1bH
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 12 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also reference the affected code. These are read-only:
|
os-zhuang
commented
Aug 14, 2026
PM review — accepted. Landing option A (the shape shipped here) on the open question about how far the demotion goes. The ruling said "demote from Recording it because the boundary is a judgement rather than a mechanical consequence, and the next seat should be able to see that it was chosen. It is also cheap to revisit: the demotion is pinned by three cases in the sweep, so flipping to B or C is a visible, reviewable edit rather than an archaeology exercise. Two things verified independently of the PR body before enqueue:
The i18n half is the part worth keeping for the next card in this shape: Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#8535
A shipped confirmation dialog declared behaviour no code path enforces. This withdraws the claim.
The claim, and why it was false
deactivate_capabilitytold the admin, verbatim:Re-verified on
origin/mainatb45c71e85a(measured, not inherited from the card):PermissionEvaluator.getSystemPermissions()unionspermissionSets[].systemPermissions— plain strings — and a resource'srequiredPermissionsis matched against that string set. Neither loads asys_capabilityrow.bootstrap-system-capabilities.ts:214/291,bootstrap-declared-capabilities.ts:210). Both writeactive: trueon insert; neither reads it back. No reader ofactiveexists anywhere.PLATFORM_CAPABILITY_NAMES, stack declarations and seed rows — never the table.The direction of the falsehood was the dangerous one: an admin withdrawing a capability was told the withdrawal took effect and it silently did not. The escalation is what they believed they had prevented.
Shape taken: reword (option B default), not the retirement playbook
Per the maintainer ruling of 2026-08-13 (ADR-0049 enforce-or-remove). Enforcement was rejected there and is not attempted here.
Removal was considered and rejected. The
spec-property-retirementplaybook governs authorablepackages/specproperties — liveness ledger, ADR-0087 conversion,retiredKey()tombstones, the eight generated artifacts.sys_capability.activeis none of those: it is a shipped database column with its own index on a plugin-owned system object, written by two seeders. Removing it is a data migration plus two action deletions, not a spec retirement — heavier, and it would touchbootstrap-system-capabilities.ts, which #8470 landed in and which this PR deliberately leaves untouched.What changed
All presentation and text. There is no behaviour change because there was no behaviour.
en,es-ES,ja-JP,zh-CN). See the note below.activegains adescriptionit never had. Its absence is precisely how the dialog became the only place the field's meaning was stated — and that statement was false.highlightFields, out of thedangervariant, out of the two scoped list views. It stays inall_capabilities, the full-catalogue view — hiding a flag the product still lets an admin set is the opposite error, not a stronger fix.The translation-bundle trap, confirmed live
Editing the source object does not rewrite shipped bundles. Running
node scripts/check-i18n-bundles.mjs --writeprintedregeneratedand, measured against a before-snapshot:fields.active.helpkey to all four bundles (filled with the English string in the three non-enlocales);confirmTextstale in all four bundles,enincluded.So the eight leaf values were corrected by hand, per the bundle header's own documented workflow.
check:i18nreports "in sync" either way, so that gate is not evidence here — the sweep test below is.Verification
The deliverable is that no surface makes the claim, so the test is a sweep, not a pin. It walks every string in the object definition and in all four locale bundles and applies two independent checks: a negative one (the withdrawn claim's own words, per locale, appear nowhere) and a positive one (every locale's dialog and field help actually state the non-effect — catching the cheap non-fix of deleting the sentence and leaving the admin to infer the rest). 19 cases.
Ablations, each with the colour predicted before running, all matching:
activeback inhighlightFieldsThe first two are the ones that matter: they prove the bundle assertions read the bundle files independently of the source, which is exactly the blind spot that let a corrected source ship beside a stale
enbundle in the #8601 case. Harness non-vacuity is further pinned by the case count — 1+4+1+4+1+4+1+2+1 = 19 — so noit.eachsilently expanded to zero.Local gates green after the final commit:
check:i18n(on a built CLI — it reports a meaningless prerequisite failure otherwise),check:nul-bytesplus a control-byte self-scan of every touched file,check:cross-package-test-inputs,check:test-source-alias,check:type-source-resolution,check:changeset-gate-self-tests,check:objectui-changeset,check:pm-dispatch-gates,check:query-options-erasure,check:type-check-coverage,check:type-check-debt(plain,--self-test,--re-measure—surplus: none, no ledger growth),check-adr-0087-registration,check-changeset-no-major,check-empty-changeset, andplugin-securityin full: 57 files / 1103 tests passed,tsc --noEmitclean.Adjacent finding, filed not fixed
#8613 records the same unenforced-
activeshape onsys_permission_setandsys_position— both Deactivate dialogs promise access stops, and noactivepredicate exists anywhere in the resolution chain. On those two it is worse, since a permission set is the grant itself. Filed unassigned; deliberately out of scope here, and its direction is not assumed to follow this card's ruling.Generated by Claude Code