Uh oh!
There was an error while loading. Please reload this page.
feat(spec): retire the allowRestore / allowPurge object-permission bits — declared gates on operations that do not exist (#12497, ADR-0049) - #12619
Conversation
…ts (#12497, ADR-0049) Maintainer ruling 2026-08-26 (decision-inbox batch 5) accepting #1883's recommendation B. The restore/purge ObjectQL operations the bits claimed to gate have never existed (#8106 vocabulary pin), so authoring them granted nothing. retiredKey() tombstone route (def reachable from the permission root — the rls.priority posture); the evaluator's pre-mapping rows retired in the same batch, with DESTRUCTIVE_OPERATIONS keeping restore/purge denied fail-closed. The keys return with the M2 lifecycle initiative; #1883 stays open as the anchor. - retiredKey tombstones + former-alias guidance in permission.zod.ts - ADR-0087: 4 retired-key entries under major 18, D2 conversion permission-allow-restore-purge-removed, step-18 chain wiring - liveness ledger entries flipped dead (tombstone route keeps them); README + state-counts regenerated - evaluator rows retired; high-privilege allowPurge read removed - pin tests: spec refusal pins, plugin-security fail-closed pins, audience-anchor predicate pin; store-fault suite probe verb purge→transfer - regenerated authorable-surface/, authorable-defaults/, reference docs; hand-written permission docs updated - changeset: spec minor (launch-window convention), plugin-security patch Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012xGvxcwPRTJfA7RfjXEYA4
…tire-allowrestore-allowpurge
📓 Docs Drift CheckThis PR changes 3 package(s): 9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 130 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d7f2bb20da6a74ab475495f93e08f7a83b84d8c3 && git checkout d7f2bb20da6a74ab475495f93e08f7a83b84d8c3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 527e0505d8729ae022121a992b62326035953a82 6f88d6900445dced778c039fb292d5b147cc4e45 && git checkout -B drift-repro 527e0505d8729ae022121a992b62326035953a82 && git merge --no-ff 6f88d6900445dced778c039fb292d5b147cc4e45
node scripts/docs-audit/affected-docs.mjs --json 527e0505d8729ae022121a992b62326035953a82
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#12497
Retires the two declared-but-unenforceable object-permission props
allowRestoreandallowPurgefrompackages/spec, per the maintainer ruling of 2026-08-26 (decision-inbox batch 5, verbatim 「12452 不处理,其他接受」, accepting #1883's presented recommendation B). The keys return with the M2 lifecycle initiative (feature + RBAC in one batch, maintainer 2026-08-03); anchor card #1883 stays open and is deliberately not referenced by any closing keyword here — #1883 remains open, and is out of scope for closure.Clause-②: yes; dispatched at review tier (published-surface accepting-set change; the dispatch ran at the contract-review tier).
⛔
allowTransferis untouched — enforced (#3004), stays.Day-of re-measurement (obligation 1)
Measured on
origin/main@7a25e7d60(2026-08-26):packages/spec/src/security/permission.zod.ts:157-158— as premised.permission.zod.ts:61-62— as premised.packages/spec/src/security/high-privilege.ts:65readsp.allowPurge— as premised.packages/objectql/.../permission-evaluator.ts:14-24; the evaluator lives atpackages/plugins/plugin-security/src/permission-evaluator.ts(rows at :23-24, CRUD folding at :100-102). The pre-wiring itself exists exactly as premised — same map, same fail-closed backstop — so the premise stands; only its address moved.restore/purgeObjectQL operation exists (packages/objectql/srcnon-test grep: zero; the test(objectql): prove purge/transfer/restore never reach the security AST path, and pin it (#7809) #8106 vocabulary pin still pins the 7-verb union).security-plugin.test.ts(fixtures + gating tests),controlled-by-parent-detail-write-authority.test.ts(CRUD fixture + by-id op loop),audience-anchors.test.ts:60,qa/dogfood/test/authz-conformance.matrix.ts:329(prose note), liveness ledger entries (statuslive, verifiedAt 2026-07-30, citing the evaluator rows). All addressed below.PermissionMatrixEditor.tsxrenders authoring columns for both keys,permission-slice.tstypes them,PermissionPreview.tsxreads them.Route choice (playbook §2)
ObjectPermissionSchemais astrictObject, but the def is reachable from thepermissionmetadata root, so the strict-deletion route (thedata/Metric:filtersprecedent) is unavailable: gate (c) ofbuild-schemas.tsrefuses deletion of a LIVE baseline line on a reachable def. The route is theretiredKey()tombstone — therls.priorityprecedent, same module family:retiredKey(...)(tscnever+ parse-time prescription with the houseos migrate meta --from 17sentence);[RETIRED](4 rows: the tombstone rides the.extend()clone intoEffectiveObjectPermissionSchema), and the 4= falsedefaults rows leaveauthorable-defaults/security.json;restore/purgealiases move toguidance(an alias may not prescribe a tombstone —alias-integrityaudit, ReportSchema 的filter别名指向filters—— 一个 ReportSchema 同样拒绝的键(#4001 战役自己的假处方,第 5 例) #5013);RETIRED_KEYS_BY_MAJOR[18](one file each underentries/retired-keys/, registry regenerated bygen:migration-registry), D2 conversionpermission-allow-restore-purge-removed(retiredFromLoadPath: true, strips both keys from every object grant inpermissions[].objects, fixture disjoint from every other entry), wired into the step-18 chain with an extended rationale. Registered under 18 per the launch-window convention (v17.0.0 already cut; changeset shipsminor— the feat(spec): retire the orphan CLICommandContributionSchema export after the contributes.commands tombstone (#12007, ADR-0049) #12613 / feat(spec): retire MetricSchema.filters — the per-metric raw-SQL filter nothing read (#10414, ADR-0049) #10552 precedent).Ratchet readings (route-relative expectations, playbook §2)
Tombstone route ⇒ surface rows move to
[RETIRED], never vanish:authorable-surface/security.json4 rows marked,authorable-defaults/security.json−4 rows,api-surface/unchanged (no export changed),json-schema.manifest/unchanged (def still emitted; these property names were not inlined in the security shard before either — verified by control grep at BASE).spec-changes.json/ upgrade guide byte-identical by design: the projection aggregates to the current protocol major (17) and major-18 conversions join it at the version bump — verified with the landedmetric-filters-removedcontrol, which is likewise absent.Liveness-ledger verdict on the pre-wired evaluator rows (obligation 2)
The ledger's 2026-07-30
liveverdicts for both keys cited only the evaluator pre-mapping rows as evidence. Measured today: those rows were the keys' sole non-test readers, and with the keys tombstoned the rows' grant condition became unsatisfiable through any parse — a mapping onto an unwritable bit is a claim about a surface that rejects authoring. Verdict: the rows retire in the same batch (not the dormant-pre-wiring posture), because:restore/purgestays denied fail-closed viaDESTRUCTIVE_OPERATIONS(now unconditionally — themodifyAllRecordsbypass no longer reaches an unmapped destructive op; it re-covers them when the M2 batch re-adds the rows, the Salesforce semantics confirmed in the [security][立项位] M2 权限生命周期(undelete/purge)功能与 allowRestore/allowPurge RBAC 同批建设(evaluator 已 fail-closed,allowTransfer 已 enforced) #1883 disposition);allowRestore/allowPurge: truerow grant a future operation without re-adjudication;Ledger entries flip to
deadwith the retiredKey evidence and stay (tombstone route keeps the keys in the walked shape — therls.prioritynote pattern); ledger README + generatedstate-counts.mdupdated. The CLI liveness advisory lint is ledger-driven and opt-in (authorWarn), which these entries don't set — no lint change needed.high-privilege.ts:65(obligation 3)The
p.allowPurgeread is removed. Reasoning from measurement: the predicate deliberately accepts legacy ROW shapes, but a stored legacyallowPurge: truenow grants nothing (no operation, no evaluator row, unwritable bit), so flagging it guarded nothing real. The D5 class messagedelete/purge/transferis kept (the class returns with M2); the code comments instruct restoring the read in the same PR that restores the bit. Anchor bindings are re-checked at boot, so a legacy value regains no privilege silently at M2. Pinned inaudience-anchors.test.ts(a legacyallowPurge: trueno longer flags;allowDelete/allowTransferstill do).Verification
(commands run through
scripts/pm/os-verify-lock.sh; exit codes captured before any pipe; readings at final head below; full pre-merge run at0e9cee239, post-merge union at head6f88d6900)pnpm --filter @objectstack/spec build— VERDICT command-exit 0 (includesgen:schemagates a0/a/b/b2/c: the 4 tombstones registered and accepted)pnpm --filter @objectstack/spec check:generated— "✓ All 14 generated artifacts are up to date" (VERDICT command-exit 0)pnpm --filter @objectstack/spec testat0e9cee239: "Test Files 435 passed (435) / Tests 11529 passed (11529)" (VERDICT command-exit 0)pnpm --filter @objectstack/spec typecheck,pnpm --filter @objectstack/plugin-security typecheck— VERDICT command-exit 0pnpm --filter @objectstack/plugin-security test: "Test Files 85 passed (85) / Tests 1554 passed (1554)" (VERDICT command-exit 0) — after two suite amendments this retirement forces: thecrudBucketForOperationpins (restore/purge now resolvenull, pinned as such) andstore-fault-fail-closed.test.ts(its probe verb movedpurgetotransfer— the only lifecycle verb a caller can still be GRANTED, so the two-doors "admitted" leg stays measurable; the fail-closed purge posture is pinned insecurity-plugin.test.tsinstead, including the legacy-stored-grant and modifyAllRecords directions)test/authz-conformance.test.ts,test/expression-conformance.test.ts): "Test Files 2 passed (2) / Tests 30 passed (30)"node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(no paths — script derived the changeset itself, stderr line names this repo at0e9cee239): every named runnable family run per-gate with individually captured exit codes — doc gates (check:doc-anchors,check:doc-authoring,check:docs-audit-scope,check:docs-redirects,check:docs-single-h1,check:role-word,check:quick-reference-counts, lintcheck:doc-formula-expressions, lintcheck:doc-security-posture) all PASS; test-code walkers (check:engine-double-contract,check:where-matcher,check:cross-package-test-inputs,check:test-source-alias,check:type-source-resolution,check:spec-parsed-alias,check:query-options-erasure,check:objectql-double-limit) all PASS; misc (check:i18n,check:i18n-stale-fill,check:changeset-gate-self-tests,check:merge-driver,check:driver-memory-census,check:objectui-changeset,check:page-declaration-shape,check:published-files,check:published-readme-links,check:react-page-adapter-contract,check:slot-lookup) all PASS; spec dist-audits (check:dual-source-exports,check:entry-nameability,check:exported-any) PASS after a rebuild (first attempt refused on a stale-mtime dist — NOT MEASURED, not red)check:nul-bytes— OK (6982 files scanned)check:type-check-coverage/check:type-check-debt(needs the whole-workspace turbo build + per-ledger tsc re-measure — a repo-wide farm run; my new test code already typechecks inside its packages via spec'scheck:test-typecheckartifact gate and plugin-security'stypecheck), repo-widepnpm lint(CI-owned run),check:react-declaration-parity(EXTERNAL_INPUT_REQUIRED — objectui manifest, pin-bump-triggered by settled ruling)6f88d6900) and the union re-run on the merged head:pnpm --filter @objectstack/spec build(gen gates) +check:generated("All 14 generated artifacts are up to date", zero drift from the merge) + specsrc/security/suite (178 passed) + full plugin-security suite (1554 passed) — VERDICT command-exit 0 at6f88d6900Reverse verification
Fix committed first (
0e9cee239), then the schema alone reverted to the pre-retirement BASE (git checkout 7a25e7d60 -- packages/spec/src/security/permission.zod.ts) with the new pins kept:src/security/permission.test.ts— "51 tests | 4 failed", and the four are EXACTLY the new retirement pins ("absent parses clean", "authored values reject with the prescription", "the bare verbs carry the prescription", "the tombstone rides into the EffectiveObjectPermission clone"); the 47 pre-existing tests stayed green.git checkout HEAD -- packages/spec/src/security/permission.zod.ts; restoration proven by observation, not exit code —git diff HEADEMPTY andgit statusclean (0 lines each); re-run: "Tests 51 passed (51)" (VERDICT command-exit 0).No ablation of built artifacts was involved in this leg — the pin file imports
./permission.zodsource directly (in-package relative import, no dist resolution), so the red/green readings are of the reverted/restored source itself; the dist-reading audits were separately re-run after a full rebuild (see above).Out-of-scope findings
allowRestore/allowPurgebits — spec now rejects them at publish objectui#6595 (filed this run, unassigned): the metadata-admin permission matrix still AUTHORS the retired bits —PermissionMatrixEditor.tsx:180-181rendersallowRestore/allowPurgecolumns whose save the spec now refuses at publish;permission-slice.ts:27-28types them;PermissionPreview.tsx:59-60,86renders and lints over them. Out of this card's repo scope; lands with the next spec bump. Dedup: repo-scoped REST search is blocked on this seat (channel probe: repo-scoped REST reads answer 200,/search/issuesrefuses with "sessions are bound to their configured repositories"), so the dedup ran as ONE targeted MCPsearch_issuesover objectstack-ai/objectui — 3 hits, all PermissionMatrixEditor write-tier cards, none about the retired keys.Remaining
allowRestore|allowPurgementions (acceptance sweep)git grep -E "allowRestore|allowPurge"at head returns only:CHANGELOG.mdfiles,content/docs/releases/v12.mdx(⛔ release-owned, never touched in a code PR);docs/audits/2026-06-*,2026-07-*,docs/audits/README.md,packages/spec/ZOD_SCHEMA_AUDIT_REPORT.md(historical measurements, the feat(spec): retire the orphan CLICommandContributionSchema export after the contributes.commands tombstone (#12007, ADR-0049) #12613 precedent leaves these standing);All live teaching surfaces (
content/docs/permissions/*,protocol/objectql/security.mdx, generatedreferences/*) are updated.Generated by Claude Code