Repository navigation
feat(spec,lint)!: refuse allowDelete: false beside modifyAllRecords: true - #25707
objectstack-fleet[bot] merged 10 commits into
Conversation
Claude-Session: https://claude.ai/code/session_017Eg8KpUtdKh6r8YTPjbhod Co-authored-by: Claude <noreply@anthropic.com>
…lete pair Claude-Session: https://claude.ai/code/session_017Eg8KpUtdKh6r8YTPjbhod Co-authored-by: Claude <noreply@anthropic.com>
…im the explain line Claude-Session: https://claude.ai/code/session_017Eg8KpUtdKh6r8YTPjbhod Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Eg8KpUtdKh6r8YTPjbhod Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Eg8KpUtdKh6r8YTPjbhod Co-authored-by: Claude <noreply@anthropic.com>
… none 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>
…rds describe 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>
📓 Docs Drift CheckThis PR changes 2 package(s): 14 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 6 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 144 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 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
|
…authors the pair Claude-Session: https://claude.ai/code/session_017Eg8KpUtdKh6r8YTPjbhod Co-authored-by: Claude <noreply@anthropic.com>
席位核对ACCEPT · #25575 ·
Generated by Claude Code |
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #25575
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Ruling-ref: 6377890121 (letter A, under 6377842908); retriage 6382861731
What changes
modifyAllRecordsstays the write bypass for edit, delete and transfer (the runtime andMODIFY_ALL_WRITE_KEYSare untouched). What an author can write narrows:ObjectPermissionSchemarefuses an explicitallowDelete: falsein an entry that setsmodifyAllRecords: true: onecustomissue atobjects.OBJECT.allowDelete(wildcard*included). The message names the route for "edit every record, delete none". It rides the existingcheckScopeAgainstSuperUserBitsrefinement (the A permission set accepts a hierarchyreadScopebesideviewAllRecords: true, never reads it, and emits no diagnostic — the declaration materialises and a capability census counts it as coverage #16870 precedent), so the response-sideEffectiveObjectPermissionSchemastays tolerant.true. A raw-input stage in front of the schema writesallowDelete: trueinto an entry that setsmodifyAllRecords: trueand omitsallowDelete. See "Mechanism assumption" below for why the refinement alone could not work.security-modify-all-delete-withheld(error) in the security-posture family (packages/lint/src/validate-security-posture.ts), with anos explainentry. On parsed doors the schema refuses first, so the arm reaches the unparsed doors only (os linton a raw object-literal config, a direct call), the same posturesecurity-owd-aliasdocuments.permission-modify-all-allow-delete-false-to-true(retired from the load path from day one,retiredAfter: '18.1.0') plus the D3 entrypermission-modify-all-allow-delete-false-refused. Stored rows and built artifacts inside their declared-floor window get the inertfalserewritten to thetruethe evaluator always acted on. A source authored at the current surface is refused, not rewritten.content/docs/permissions/sharing-rules.mdx("Recipe — edit every record, delete none"); one paragraph inpermissions-matrix.mdx; two table rows inpermission-sets.mdx/permission-metadata.mdxthat 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")
allowDeletecarries.default(false)and a refinement sees the parsed value, so{ modifyAllRecords: true }parsed to{ allowDelete: false, modifyAllRecords: true }onorigin/main6021b5e (measured on the built dist). A refinement alone would refuse that output the next time it is parsed:definePermissionSetoutput handed todefineStack, a build re-read at boot, a stored row saved again. So the explicitness is read off the raw input. The stage writestrueinto an omitted key (whatobjectPermissionGrantsalready answers), and the refinement then only ever sees an authoredfalse.parse(parse(x))equalsparse(x)is pinned. Data at rest carries the pair two ways. Authored: HotCRM'ssrc/sales/profiles/sales-manager.profile.ts:57declarescrm_forecastwithallowDelete: falsebesidemodifyAllRecords: true, and its built 17.1 artifact (the fixturepackages/metadata/src/__fixtures__/hotcrm-17.1-built-permissions.artifact.json) carries that entry. Filled in: every earlier parse wrotefalseinto an omittedallowDelete, so an artifact or stored row built from a source that omits it (the showcase'sshowcase_ops) carries the pair too. Hence the D2 conversion. The artifact door converts the 17.1 artifact (floor belowretiredAfter) and refuses the same body when it claims a surface pastretiredAfter; both pinned inplugin-artifact-forward-conversion.test.ts.Reading: can a criteria rule select every row, and does it grant edit without delete?
Read:
checkDeleteinplugin-sharing/src/sharing-service.tshas no share branch (ADR-0111 D3), so aneditshare opens update and never delete.rule-criteria.tsisMatchAllCriteriarefuses or skips a criteria that lowers to an empty filter, and accepts any concrete field predicate.Measured on a booted in-process stack (
@objectstack/verifybootStack, at 9f2f3fa, whose runtime code the later commits do not touch; a temporary test, not committed), the card's ownsrm_materialshape:public_read, admin-owned rows, permission set{ allowRead, allowCreate: false, allowEdit, allowDelete: false }distributed by positionsrm_buyer, rule{ type: 'criteria', condition: "record.id != ''", accessLevel: 'edit', sharedWith: { type: 'position', value: 'srm_buyer' } }:sys_user_positionrow insertedsys_record_sharerow,edit,source: rule, to the buyercategorywritten, owner unchangedControl, same stack with
condition: 'true': boot logsdeclared 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 literaltrueis 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 setmodifyAllRecords: trueand omitallowDelete, and now parse toallowDelete: truewith no edit:examples/app-showcase/src/security/permission-sets.tsshowcase_ops.showcase_announcementandpackages/verify/src/parent-write-reach.tsmodifyAll. The platform sets (admin_full_access,organization_admin) writeallowDelete: trueexplicitly. Raw evaluator fixtures that declare the pair (plugin-securitysecurity-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 atobjects.crm_forecast.allowDeleteonce it upgrades, and the fix is the ruling's choice (dropallowDelete: falseto keep Modify All, or replacemodifyAllRecordswith 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 D3reasonwithout a tracker id, the D2 docblock and fixture comment, a test comment). The CI red was reproduced first onc5b702ced(cli integrationmigrate-meta-engine-guidance.test.ts, 1 failed). At0a8846cd6, each under the lock with VERDICT exit 0: clitest(unit + integration) 499 files / 6286 tests; spec 719 / 21788 andtest:repo79 / 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 101permission.test.tspins 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:node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack): 116 derived, 116 run, every exit captured before any pipe;--ranwith 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 intoexamples/app-showcase; re-run alone it is green (10121 files, 0 disagree).check-plugin-teardown-shape --self-testneeded its pinned fixture commit fetched in this shallow clone; green after.test719 files / 21788 tests, spectest:repo79 / 1269, spectypecheck; linttest161 / 7274 andtypecheck; metadatatest64 / 906 andtypecheck.test:repo3 / 854; core 118 / 2757 andtest:repo6 / 82; verify 46 / 393; metadata-protocol 263 / 21475; rest 320 / 6406 andtest:repo5 / 280; plugin-security 222 / 4628; cli--project unit328 / 4816 (integration layer declared to CI: the diff touches no CLI spawn entry).(narrowing)arm): 295 passed, 3 skipped files; 2520 passed, 18 skipped tests; VERDICT command-exit 0.check:dev-prereqs: green (local-only).Acceptance notes
GetObjectPermissionsResponseSchema.permissionsuses the authoringObjectPermissionSchema(not the effective one), so it inherits the refusal, as it already inherits the A permission set accepts a hierarchyreadScopebesideviewAllRecords: true, never reads it, and emits no diagnostic — the declaration materialises and a capability census counts it as coverage #16870 scope refusals. Noted, not changed.os migrate meta --from 17replays the retired D2 entry, so a source carrying the pair would be rewritten toallowDelete: true(the runtime behaviour) with the D3 entry listed beside it as the judgment owed. Recorded in the D3 entry'sconversionIds.public_readobject: without the share, the update is refused at the record, not at the object CRUD check.Body edited by the
domain:specseat 2 after patch round 1, with the dev's proposed text from6390006109, plus the round's readings.🤖 Generated with Claude Code
https://claude.ai/code/session_017Eg8KpUtdKh6r8YTPjbhod
Generated by Claude Code