Skip to content

feat(spec,lint)!: refuse allowDelete: false beside modifyAllRecords: true - #25707

Merged
objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/issue-25575-modify-all-allow-delete-refusal
Oct 24, 2026
Merged

objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/issue-25575-modify-all-allow-delete-refusal

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #25575

Clause-②: no (narrowing)
Metadata-protocol: narrowed
Ruling-ref: 6377890121 (letter A, under 6377842908); retriage 6382861731

What changes

modifyAllRecords stays the write bypass for edit, delete and transfer (the runtime and MODIFY_ALL_WRITE_KEYS are untouched). What an author can write narrows:

  • Schema refusal. ObjectPermissionSchema refuses an explicit allowDelete: false in an entry that sets modifyAllRecords: true: one custom issue at objects.OBJECT.allowDelete (wildcard * included). The message names the route for "edit every record, delete none". It rides the existing checkScopeAgainstSuperUserBits refinement (the A permission set accepts a hierarchy readScope beside viewAllRecords: true, never reads it, and emits no diagnostic — the declaration materialises and a capability census counts it as coverage #16870 precedent), so the response-side EffectiveObjectPermissionSchema stays tolerant.
  • Omitted key materialises as true. A raw-input stage in front of the schema writes allowDelete: true into an entry that sets modifyAllRecords: true and omits allowDelete. See "Mechanism assumption" below for why the refinement alone could not work.
  • Lint arm. security-modify-all-delete-withheld (error) in the security-posture family (packages/lint/src/validate-security-posture.ts), with an os explain entry. On parsed doors the schema refuses first, so the arm reaches the unparsed doors only (os lint on a raw object-literal config, a direct call), the same posture security-owd-alias documents.
  • Data at rest. ADR-0087 D2 conversion permission-modify-all-allow-delete-false-to-true (retired from the load path from day one, retiredAfter: '18.1.0') plus the D3 entry permission-modify-all-allow-delete-false-refused. Stored rows and built artifacts inside their declared-floor window get the inert false rewritten to the true the evaluator always acted on. A source authored at the current surface is refused, not rewritten.
  • Docs. A measured recipe in content/docs/permissions/sharing-rules.mdx ("Recipe — edit every record, delete none"); one paragraph in permissions-matrix.mdx; two table rows in permission-sets.mdx / permission-metadata.mdx that said Modify All is "edit" now say "edit and delete". Generated references regenerated.

Mechanism assumption (re-measured, and why the shape differs from "a parse-time refinement")

allowDelete carries .default(false) and a refinement sees the parsed value, so { modifyAllRecords: true } parsed to { allowDelete: false, modifyAllRecords: true } on origin/main 6021b5e (measured on the built dist). A refinement alone would refuse that output the next time it is parsed: definePermissionSet output handed to defineStack, a build re-read at boot, a stored row saved again. So the explicitness is read off the raw input. The stage writes true into an omitted key (what objectPermissionGrants already answers), and the refinement then only ever sees an authored false. parse(parse(x)) equals parse(x) is pinned. Data at rest carries the pair two ways. Authored: HotCRM's src/sales/profiles/sales-manager.profile.ts:57 declares crm_forecast with allowDelete: false beside modifyAllRecords: true, and its built 17.1 artifact (the fixture packages/metadata/src/__fixtures__/hotcrm-17.1-built-permissions.artifact.json) carries that entry. Filled in: every earlier parse wrote false into an omitted allowDelete, so an artifact or stored row built from a source that omits it (the showcase's showcase_ops) carries the pair too. Hence the D2 conversion. The artifact door converts the 17.1 artifact (floor below retiredAfter) and refuses the same body when it claims a surface past retiredAfter; both pinned in plugin-artifact-forward-conversion.test.ts.

Reading: can a criteria rule select every row, and does it grant edit without delete?

Read: checkDelete in plugin-sharing/src/sharing-service.ts has no share branch (ADR-0111 D3), so an edit share opens update and never delete. rule-criteria.ts isMatchAllCriteria refuses or skips a criteria that lowers to an empty filter, and accepts any concrete field predicate.

Measured on a booted in-process stack (@objectstack/verify bootStack, at 9f2f3fa, whose runtime code the later commits do not touch; a temporary test, not committed), the card's own srm_material shape: public_read, admin-owned rows, permission set { allowRead, allowCreate: false, allowEdit, allowDelete: false } distributed by position srm_buyer, rule { type: 'criteria', condition: "record.id != ''", accessLevel: 'edit', sharedWith: { type: 'position', value: 'srm_buyer' } }:

step result
buyer updates an admin-owned row, before holding the position 403 PERMISSION_DENIED
sys_user_position row inserted one sys_record_share row, edit, source: rule, to the buyer
buyer updates the admin-owned row 200, category written, owner unchanged
buyer deletes it 403 PERMISSION_DENIED; row still present
admin inserts a new row; buyer updates / deletes it 200 / 403
buyer creates a row 403

Control, same stack with condition: 'true': boot logs declared rule not served … reason: match-all-criteria, no share row, and the buyer holding the position is refused the same update at the record ("You do not have access to this record"). So the update above came from the share. A literal true is not refused at parse; it is never served. The docs and every prescription say so.

Producers

Measured on 6021b5e before the change: no permission set in this repository (examples/**, packages/platform-objects, plugins, services) authors the pair. Two set modifyAllRecords: true and omit allowDelete, and now parse to allowDelete: true with no edit: examples/app-showcase/src/security/permission-sets.ts showcase_ops.showcase_announcement and packages/verify/src/parent-write-reach.ts modifyAll. The platform sets (admin_full_access, organization_admin) write allowDelete: true explicitly. Raw evaluator fixtures that declare the pair (plugin-security security-plugin.test.ts, "should allow edit/delete via modifyAllRecords") are never parsed and keep pinning the runtime semantics the ruling keeps. Downstream producer that authors the pair: HotCRM. src/sales/profiles/sales-manager.profile.ts:57 (sales_manager.crm_forecast). Its deployed 17.1 artifact is converted at load (the evaluator's behaviour unchanged); its source build is refused at objects.crm_forecast.allowDelete once it upgrades, and the fix is the ruling's choice (drop allowDelete: false to keep Modify All, or replace modifyAllRecords with the edit-share route). The seat files the HotCRM card at merge.

Tests and gates

Patch round 1, at head 0a8846cd6 (strings and comments only: the D3 reason without a tracker id, the D2 docblock and fixture comment, a test comment). The CI red was reproduced first on c5b702ced (cli integration migrate-meta-engine-guidance.test.ts, 1 failed). At 0a8846cd6, each under the lock with VERDICT exit 0: cli test (unit + integration) 499 files / 6286 tests; spec 719 / 21788 and test:repo 79 / 1269; lint 161 / 7274; metadata 64 / 906. dispatch-gates --commands: 116 run, 0 NOT-MEASURED. Ablations, each restored blob-identical: (a) disabling the refinement arm turns 2 of 101 permission.test.ts pins red, as predicted; (b) making the materialise stage return its input unchanged turns 2 of 101 red, as predicted. The dogfood battery was not re-run: the push changes strings and comments only. retiredAfter: '18.1.0' still names the latest published @objectstack/spec.

Round 1, at head c5b702ced:

  • Derived gates (node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack): 116 derived, 116 run, every exit captured before any pipe; --ran with exit codes answers "116 run, 0 NOT-MEASURED (a DERIVED zero)". One gate (check-comment-mask-corpus) first went red on ENOENT for a temporary bundle the concurrent dogfood run wrote into examples/app-showcase; re-run alone it is green (10121 files, 0 disagree). check-plugin-teardown-shape --self-test needed its pinned fixture commit fetched in this shallow clone; green after.
  • Package suites (lock VERDICT exit 0 each): spec test 719 files / 21788 tests, spec test:repo 79 / 1269, spec typecheck; lint test 161 / 7274 and typecheck; metadata test 64 / 906 and typecheck.
  • Consumers of the contract (each green): metadata-core 19 / 800; runtime 441 / 7080 and test:repo 3 / 854; core 118 / 2757 and test:repo 6 / 82; verify 46 / 393; metadata-protocol 263 / 21475; rest 320 / 6406 and test:repo 5 / 280; plugin-security 222 / 4628; cli --project unit 328 / 4816 (integration layer declared to CI: the diff touches no CLI spawn entry).
  • Dogfood battery ((narrowing) arm): 295 passed, 3 skipped files; 2520 passed, 18 skipped tests; VERDICT command-exit 0.
  • check:dev-prereqs: green (local-only).

Acceptance notes

Body edited by the domain:spec seat 2 after patch round 1, with the dev's proposed text from 6390006109, plus the round's readings.

🤖 Generated with Claude Code

https://claude.ai/code/session_017Eg8KpUtdKh6r8YTPjbhod


Generated by Claude Code

…im the explain line

Claude-Session: https://claude.ai/code/session_017Eg8KpUtdKh6r8YTPjbhod
Co-authored-by: Claude <noreply@anthropic.com>
…e the release in the message

Claude-Session: https://claude.ai/code/session_017Eg8KpUtdKh6r8YTPjbhod
Co-authored-by: Claude <noreply@anthropic.com>
… inside its window and refuses it past

Claude-Session: https://claude.ai/code/session_017Eg8KpUtdKh6r8YTPjbhod
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 24, 2026
@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/spec, touching 19 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/lint/src/index.ts, packages/spec/dropped-refinements.baseline.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

14 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/approvals.mdx (via sales_manager (literal, a string literal in fixture))
  • content/docs/concepts/architecture.mdx (via allowDelete (literal, a string literal in checkScopeAgainstSuperUserBits; a string literal in materializeModifyAllDelete))
  • content/docs/getting-started/common-patterns.mdx (via allowDelete (literal, a string literal in checkScopeAgainstSuperUserBits; a string literal in materializeModifyAllDelete))
  • content/docs/permissions/authorization.mdx (via validateSecurityPosture (symbol, a top-level function))
  • content/docs/permissions/delegated-administration.mdx (via allowDelete (literal, a string literal in checkScopeAgainstSuperUserBits; a string literal in materializeModifyAllDelete), sales_manager (literal, a string literal in fixture))
  • content/docs/permissions/index.mdx (via allowDelete (literal, a string literal in checkScopeAgainstSuperUserBits; a string literal in materializeModifyAllDelete), sales_manager (literal, a string literal in fixture))
  • content/docs/permissions/permission-metadata.mdx (via allowDelete (literal, a string literal in checkScopeAgainstSuperUserBits; a string literal in materializeModifyAllDelete), sales_manager (literal, a string literal in fixture))
  • content/docs/permissions/permission-sets.mdx (via allowDelete (literal, a string literal in checkScopeAgainstSuperUserBits; a string literal in materializeModifyAllDelete))
  • content/docs/permissions/permissions-matrix.mdx (via allowDelete (literal, a string literal in checkScopeAgainstSuperUserBits; a string literal in materializeModifyAllDelete), sales_manager (literal, a string literal in fixture))
  • content/docs/permissions/positions.mdx (via sales_manager (literal, a string literal in fixture))
  • content/docs/permissions/sharing-rules.mdx (via allowDelete (literal, a string literal in checkScopeAgainstSuperUserBits; a string literal in materializeModifyAllDelete), sales_manager (literal, a string literal in fixture))
  • content/docs/protocol/objectql/security.mdx (via ObjectPermissionSchema (symbol, a top-level const), allowDelete (literal, a string literal in checkScopeAgainstSuperUserBits; a string literal in materializeModifyAllDelete))
  • content/docs/protocol/objectui/layout-dsl.mdx (via sales_manager (literal, a string literal in fixture))
  • content/docs/ui/pages.mdx (via sales_manager (literal, a string literal in fixture))

⛔ 6 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v13.mdx (via validateSecurityPosture (symbol, a top-level function))
  • content/docs/releases/v14.mdx (via allowDelete (literal, a string literal in checkScopeAgainstSuperUserBits; a string literal in materializeModifyAllDelete))
  • content/docs/releases/v16.mdx (via sales_manager (literal, a string literal in fixture))
  • content/docs/releases/v17/17-0.mdx (via ObjectPermissionSchema (symbol, a top-level const))
  • content/docs/releases/v17/17-5.mdx (via retiredAfter (symbol, a field of const object permissionModifyAllAllowDeleteFalseToTrue), retiredFromLoadPath (symbol, a field of const object permissionModifyAllAllowDeleteFalseToTrue))
  • content/docs/releases/v17/17-7.mdx (via allowDelete (literal, a string literal in checkScopeAgainstSuperUserBits; a string literal in materializeModifyAllDelete))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/lint/src/index.ts, packages/spec/dropped-refinements.baseline.json) — pages documenting those are invisible to this run
  • 9 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 144 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4bcc4df6496ef2b327739b65c7f2fa2ff4f304e0 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from f14ef9e7884fbca210f647c5b4bd87b833deadce — the merge of head 0a8846cd638233e4b47e94ea91bc44a3fff4bd7a into base 4bcc4df6496ef2b327739b65c7f2fa2ff4f304e0, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin f14ef9e7884fbca210f647c5b4bd87b833deadce && git checkout f14ef9e7884fbca210f647c5b4bd87b833deadce
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4bcc4df6496ef2b327739b65c7f2fa2ff4f304e0 0a8846cd638233e4b47e94ea91bc44a3fff4bd7a && git checkout -B drift-repro 4bcc4df6496ef2b327739b65c7f2fa2ff4f304e0 && git merge --no-ff 0a8846cd638233e4b47e94ea91bc44a3fff4bd7a

node scripts/docs-audit/affected-docs.mjs --json 4bcc4df6496ef2b327739b65c7f2fa2ff4f304e0

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 4bcc4df6496ef2b327739b65c7f2fa2ff4f304e0 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…authors the pair

Claude-Session: https://claude.ai/code/session_017Eg8KpUtdKh6r8YTPjbhod
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · #25575 · domain:spec seat 2 (seat post #18549) · sunhaolin · session session_01PC4NPXPgPmpDRG9DdDnyXT · 2026-10-24T22:13Z · head 0a8846cd63 · report 6388606923 and addendum 6390006109 (on #25575; cloud session session_017Eg8KpUtdKh6r8YTPjbhod) · seat review at TIER_DEFAULT. The change is a narrowing (security, BREAKING), and the path limb hits (packages/spec/src/security/**). So a same-shape ## Contract review PASS at CONTRACT_REVIEW_TIER on the landing head is owed, and needs:contract-review is added in this stroke.

  1. Shape. Draft, base main, first line Fixes #25575, title feat(spec,lint)!:. The Clause-②: no (narrowing), Metadata-protocol: narrowed and Ruling-ref: 6377890121 … ; retriage 6382861731 lines are line-initial in the body and in the changeset (@objectstack/spec, @objectstack/lint minor, BREAKING section, ADR-0087 marker registering both entries). 18 files, +593/−20. check-governed-merges --pr 25707: NOT governed. No content/docs/releases/**.
  2. The ruling, measured on the diff. Letter A of 6377890121, under 6377842908: keep Modify All as the write bypass for edit, delete and transfer, refuse the pair at publish and in lint, and name the route for "edit all, delete none".
    • Schema: checkScopeAgainstSuperUserBits gains the arm modifyAllRecords === true && allowDelete === false, giving one custom issue at allowDelete, wildcard * included. It rides the A permission set accepts a hierarchy readScope beside viewAllRecords: true, never reads it, and emits no diagnostic — the declaration materialises and a capability census counts it as coverage #16870 refinement, so EffectiveObjectPermissionSchema stays tolerant.
    • Mechanism the claim did not foresee: allowDelete has .default(false), so the parsed output of { modifyAllRecords: true } was itself the refused pair. materializeModifyAllDelete, a z.preprocess stage outside the residue stage (with shape read through), writes true into an omitted key only. parse(parse(x)) = parse(x) is pinned.
    • Lint: security-modify-all-delete-withheld (error) in the security-posture family, reaching only the unparsed doors, the same posture as security-owd-alias.
    • Runtime: unchanged. MODIFY_ALL_WRITE_KEYS is untouched, and there is no editAllRecords key.
  3. The route, measured. On a booted stack (bootStack, a temporary test, not committed), with a criteria rule accessLevel: 'edit' to a position on a public_read object:
    • update is 200 and delete is 403, on both old and newly inserted rows, and create is 403;
    • control: condition: 'true' is logged "not served … match-all-criteria", and the same update is refused at the record.
      The docs recipe and every prescription name record.id != '', never a literal true. That matches ADR-0111 D3 (no share branch in checkDelete).
  4. Data at rest.
    • D2 permission-modify-all-allow-delete-false-to-true is retired from the load path, with retiredAfter: '18.1.0' = the latest published @objectstack/spec (read this round). It is lossless against the runtime: the holder could always delete.
    • D3 permission-modify-all-allow-delete-false-refused links it through conversionIds.
    • Premise corrected in patch round 1: HotCRM authors the pair. At src/sales/profiles/sales-manager.profile.ts:57 (crm_forecast; seat read at HotCRM 8a14b56, dev at a4b3560), the 17.1 artifact's entry is authored, not a filled default. The docblock, the fixture comment and the PR body now say so. The showcase showcase_ops stays the filled-default instance.
  5. Gates. ci-failure --pr 25707 GREEN: 42 check-runs on 0a8846cd63, all completed, none failed.
    • The round-1 red (cli integration, a tracker id #4001 in the D3 reason) was reproduced on c5b702ced before the fix.
    • At 0a8846cd6: cli unit + integration 499 / 6286; spec 719 / 21788 and test:repo 79 / 1269; lint 161 / 7274; metadata 64 / 906; 116/116 derived, --ran 0 NOT-MEASURED.
    • Round-1 consumers stand: runtime, core, verify, metadata-core, metadata-protocol, rest, plugin-security, with test:repo where declared.
    • The dogfood battery ran at c5b702ced (295 files / 2520 tests, exit 0). It was not re-run after a strings-and-comments push; accepted.
    • Ablations, predicted and met, each restored blob-identical: (a) the refinement arm off gives 2 of 101 red (the explicit refusal, the wildcard); (b) the materialise stage off gives 2 of 101 red (omitted key parses to true, idempotency).
  6. Open questions (answered in the patch round): (1) materialise true for an omitted key, A: the minimum mechanism that makes the refusal idempotent. (2) Keep the lint arm, A: the director's state comment 6377871867 names it ("at publish and in lint"). (3) os migrate meta replays the D2 entry, A: D3 conversionIds carries the judgment.
  7. Deviations accepted.
  8. Downstream, carried by the seat: at this PR's merge, the seat files a HotCRM card for sales_manager.crm_forecast. The choice is HotCRM's: drop allowDelete: false to keep Modify All, or move to the edit-share route. Its deployed 17.1 artifact keeps loading through D2.
  9. Landing. Not governed, 613 changed lines. Once a CONTRACT_REVIEW_TIER PASS is on record and every check on the then-current head is green, the seat sends pr_ready + automerge_enable through the relay. At landing, the seat re-checks that retiredAfter still names the latest published release.

Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 0a8846cd638233e4b47e94ea91bc44a3fff4bd7a
Local-runs: none
Scope: schema face (Clause-②: no (narrowing), Metadata-protocol: narrowed) — recogniser hits packages/spec/src/security/permission.zod.ts, packages/spec/src/conversions/registry.ts, packages/spec/src/migrations/entries/semantic/18.permission-modify-all-allow-delete-false-refused.ts, packages/spec/src/security/permission.test.ts, packages/spec/src/shared/retired-key-removal-version.pin.test.ts; cited packages/spec/dropped-refinements.baseline.json, packages/lint/src/validate-security-posture.ts, packages/lint/src/validate-security-posture.test.ts, packages/lint/src/rule-explanations.ts, packages/lint/src/index.ts, packages/metadata/src/plugin-artifact-forward-conversion.test.ts; prose .changeset/25575-modify-all-allow-delete.md, content/docs/permissions/*.mdx, content/docs/references/{api/protocol,security/permission}.mdx.

① Derived judgments

  • Declaration matches the diff. ObjectPermissionSchema now refuses an authored allowDelete: false beside modifyAllRecords: true at path allowDelete (wildcard included) inside checkScopeAgainstSuperUserBits; nothing is widened. Clause-②: no (narrowing) and Metadata-protocol: narrowed are the right declarations for a published key that loses an accepted shape.
  • Ruling conformance. Letter A (6377890121 under 6377842908, retriage 6382861731): schema refusal at publish, lint refusal (security-modify-all-delete-withheld, error, unparsed intakes only), runtime and MODIFY_ALL_WRITE_KEYS untouched, docs name the sharing-rule route for "edit every record, delete none". All four present; Salesforce Modify All semantics kept.
  • The materializeModifyAllDelete preprocess is the minimal mechanism, not a widening. allowDelete carries .default(false), so a refinement alone would refuse its own output on the second parse. Writing true into an omitted key records what the evaluator already grants and keeps the parse idempotent. Only the authoring schema takes the stage; EffectiveObjectPermissionSchema stays tolerant (authorable/wire split). The shape read-through is preserved for the schema walkers, which the baseline out → out.out site moves confirm.
  • ADR-0087 disposition complete: D2 conversion permission-modify-all-allow-delete-false-to-true (toMajor 18, order 75, retiredFromLoadPath: true, retiredAfter: '18.1.0', fixture with one notice, idempotent) plus D3 semantic entry with acceptanceCriteria naming the D2 replay judgment for os migrate meta --from 17. Marker registered in the changeset.
  • Docs recipe is measured, not asserted: criteria rule with condition: "record.id != ''" and accessLevel: 'edit' (update 200, delete 403 on a booted stack); the condition: 'true' trap (empty filter, never served) is documented beside it.
  • Dogfood battery ran at c5b702ced (295/2520, exit 0); the push to 0a8846cd6 is strings-only and the seat accepted it (6390192995). Ablations (a)(b) each turn 2 of 101 red, restored.

② Semver level

@objectstack/spec minor, @objectstack/lint minor, with ! and a BREAKING banner carrying Before/Now/Fix. Metadata-protocol: narrowed ⇒ at least minor, so the level is right. The 54 check runs on the head are all success or skipped (Test Core 1–6, Build Core, Build Docs, Dogfood Regression Gate, Type Check, Lint, Check Changeset, Governed Surface Queue Guard, Spec property liveness, claim gates).

③ Boundary flags

  • retiredAfter: '18.1.0' must track the latest published spec at landing: if a spec release cuts before this merges, the value moves (seat already notes this).
  • Producer in another repo: HotCRM src/sales/profiles/sales-manager.profile.ts authors the pair on crm_forecast; its 17.1 artifact gets the D2 rewrite, its next rebuild meets the refusal. Seat files the HotCRM card at merge.
  • GetObjectPermissionsResponseSchema.permissions inherits the refusal (authoring schema reused on the response side); noted in Acceptance, no client in repo sends the pair.
  • os migrate meta --from 17 replays D2 and keeps the delete grant; the D3 entry is the judgment owed. Acceptable under letter A since no access decision changes.

Implemented-by: session_017Eg8KpUtdKh6r8YTPjbhod
Reviewed-by: session_01Bf4vhia9fHNXpqC1PCfxie

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 24, 2026 23:07
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 24, 2026
Merged via the queue into main with commit f6ba134 Oct 24, 2026
58 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-25575-modify-all-allow-delete-refusal branch October 24, 2026 23:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Permission sets: no way to grant "edit any record" without also granting delete (modifyAllRecords implies allowDelete)

3 participants