Repository navigation
feat(plugin-security)!: the six built-in positions are declared position metadata (ADR-0131 C2 stage S2) - #22139
Conversation
… metadata (wip) ADR-0131 C2 stage S2. The four identity names and the everyone/guest anchors are declared once (builtin-positions.ts), registered with the engine registry under the plugin's package id, and read by the built-in seeder; the declared-positions seeder unions its two sources and skips the six. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
…tions stage Four principals in two postures, through the plugin's real kernel:ready bootstrap, held to a golden recorded against the pre-change sources. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
…ared seeder's input Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
…p, with the six built-ins out of its decision and its result Replaces the union read: the registry-first / metadata-service fallback is unchanged, and the six declared built-in positions neither count as the registry holding a position nor reach the seeding loop. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
…ns stage (goldens pending) Catalog listing, sys_position row census and write ledger, grant envelopes and tenant position-write refusals, over four boot scenarios. Replaces the grant-equivalence file. Goldens are recorded from the pre-declaration tree in the next commit. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
…pre-declaration tree Recorded by running this file against the sources at 51290bc (the built-in seeder's own code list, main's declared-positions seeder, no registration); every census, ledger, grant and refusal reading there equals the reading after the declarations. The catalog listing is the one widening: before, it listed the stack's position alone. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
… declared metadata Clause-② yes (narrowing): two metadata-door reads widen, and an in-place PUT of one of the six names at the metadata door is now refused. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
…-builtin-positions
…ed query options Four find() calls had their options erased to any, growing the query-options erasure ratchet's test surface; the signatures infer without it. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 16 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 888a8d258c03e26146593d2ed112b1c164adfb24 && git checkout 888a8d258c03e26146593d2ed112b1c164adfb24
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ef1fcb26a24ff3842e00226186477db8f4850fbf 4e969bae6e92673ea6c064c315c64fe3426e4b8d && git checkout -B drift-repro ef1fcb26a24ff3842e00226186477db8f4850fbf && git merge --no-ff 4e969bae6e92673ea6c064c315c64fe3426e4b8d
node scripts/docs-audit/affected-docs.mjs --json ef1fcb26a24ff3842e00226186477db8f4850fbf
|
…n-security now declares S1's showcase catalog pin expects the six beside the stack's positions, and the runtime registry-copy row expects the registry's positions to be exactly the six built-ins and none of the stack's; both read the names from @objectstack/spec. Declared cross-lane on the CLI lane post. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #15196 (body and all 30 comments, rulings ① Derived judgmentsThe diff registers the six built-in positions ( The widening arm — what a client can newly read. RIGHT.
The narrowing arm — what is newly refused. RIGHT.
Row writes — RIGHT, and the no-change claim holds where it is pinned.
Public package surface — RIGHT. Alignment with the rulings. Q1 = A (seat verdict ② Semver level
③ Boundary flagsDev flags (PR body; report
Reviewer's own flags:
Implemented-by: VERDICT: PASS Generated by Claude Code |
Part of #15196
Clause-②: yes (narrowing)
Read-shape change. Two doors now answer six more position names,
platform_admin,org_owner,org_admin,org_member,everyoneandguest, in all three postures measured (single,singlewith the Default Organization, walled). The metadata door'sGET /api/v1/meta/positionlists them beside the stack's own positions (booted showcase: 10 names before, 16 after). S1's catalog read,createSecurityCatalogReaderin@objectstack/core, lists them from the engine registry, withsource: 'registry'andpackageId: 'com.objectstack.plugin-security'(showcase: 10 entries before, 16 after). A client can now also read each one's definition:GET /api/v1/meta/position/:nameanswers200withname,label,descriptionand the package provenance, where it answered404 RESOURCE_NOT_FOUND. A definition carries identity and display only. Activation, the default flag, themanaged_byprovenance and the permission-set bindings stay on thesys_positionrows and their junction, and those are unchanged.Why the arm reads
narrowing, where the dispatch namedwidening. Registering the six as a code package's items also narrows one write door. A platform administrator'sPUT /api/v1/meta/position/:namenaming one of the six answered200(saved as an environment-wide definition) and now answers403 NOT_OVERRIDABLE: the name is provided by a package, andpositionhas no overlay. That is the package provenance the stage asked for, the same provenance the manifest stamps on the platform's permission sets, doing what the metadata door does for every packaged item of a non-overridable type. Both facts are true, so the line names the narrowing arm, and the changeset is declared breaking with an ADR-0087 disposition (no-migration-prescription). This deviates from the dispatch, and the contract review decides it. A name that is not one of the six, and every other door, answer as before.The rest of #15196 stays open: this is stage S2 only. No reader moves to the catalog read here (S8), and nothing in the position read, the grant columns or the seeders' other writes changes.
What changed
builtin-positions.ts): the four identity names with their label and description from@objectstack/spec(BUILTIN_IDENTITY_NAMES,BUILTIN_IDENTITY_METADATA), then the two audience anchors (AUDIENCE_ANCHOR_POSITIONS, i.e.EVERYONE_POSITIONandGUEST_POSITION) with the anchor text that lived inbootstrap-builtin-positions.ts, moved verbatim.PositionSchemaaccepts every one of them.positionskey reaches no reader. The engine's stack-collection loop has nopositionsentry (a waived row incheck:stack-collection-maps), andManifestSchemadeclares none.ObjectQL.registerAppwith apositionsand apermissionslist registered the permission set under the package id and no position at all. On a booted showcase, apositionslist on this plugin's manifest reached neither the engine registry, the metadata service, the metadata door nor the catalog read. SoSecurityPlugin.startregisters the six throughregistry.registerItem('position', item, 'name', SECURITY_PLUGIN_ID), on the engine handlestart()already holds, beforekernel:readyruns the seeders. That is the provenance the manifest stamps onpermissions. If the engine exposes no registration seam, it says so once atwarn.bootstrapBuiltinRolesreads that list and has no list of its own. The rows it writes are byte-identical: same names, same organizations,managed_by: 'platform',active: true,is_default: false, same labels and descriptions.mainplus the registration alone: the seeder reads the engine registry first and the metadata service only when the registry holds no position, so the six registered names made the registry answer every boot, and the stack's positions (which only the metadata service holds) stopped seeding. The fix is the smallest one that keeps the census identical. The registry counts as holding a position only when it holds one besides the six, and the six are filtered out of whatever the seeder then reads. The two-step itself is untouched, so the seeder's behaviour is identical tomainin every state, including a registry that holds a door-authored position.plugin-securityfollow the new read shape (patch round,4e969bae6e; declared cross-lane on the CLI lane post in comment 6051162186, and S2's surface amended on feat(core,objectql,plugin-security,plugin-sharing): the catalog is read from the registry; assignment tables reference it by name (ADR-0131 D2/D3/D4) #15196). S1's showcase pinsecurity-catalog-showcase.dogfood.test.tsexpects the six beside the stack's positions, and the runtime row instandalone-stack-seeder-declaration-copy.test.tsexpects the registry's positions to be exactly the six built-ins, none of the stack's (its title follows). Both read the six from@objectstack/spec(BUILTIN_IDENTITY_NAMES,AUDIENCE_ANCHOR_POSITIONS); no exact-set row is weakened. Measured: 31 of 31 and 13 of 13.Pins
All in
builtin-positions.boot.test.ts: a real boot of this plugin (init,start, then itskernel:readyhandlers in order) over a real ObjectQL engine on SQLite, with a stack-declared position in the metadata service. Four scenarios:singlewith no organization,singlewith an organization and its memberships, walled with an organization at boot plus one created after it, and walled with a door-authored position already in the registry. The goldens were recorded by running this file against the sources at51290bca2c(the built-in seeder's own code list,main's declared seeder, no registration). Every census, ledger, grant and refusal reading there equals the reading after the change.@registryand owned by this plugin, beside the declared position, in all four scenarios. At51290bca2cit listed the declared position alone.sys_positioncensus, read as (name, organization,managed_by,active,is_default, label, description), and the write ledger (everysys_positioninsert and update the boot makes, refused ones included, with the provenance written) are identical before and after.singleand walled, equals the golden.field_defaultreaches every human principal through theeveryonebinding.VALIDATION_FAILED,position:reference_not_found), deleting theeveryonerow (PERMISSION_DENIED403) and relabelling theplatform_adminrow (PERMISSION_DENIED403). Controls: creating an ordinary position and assigning it both succeed.Ablations. Each one went through
scripts/ablation-replace.mjs: the anchor hit 1 to 0 and the blob changed; after the run the blob equalledHEADandgit diff HEADwas empty.everyonefrom the declarationbuiltin-positions.tsbootstrap-declared-positions.tsbootstrap-declared-positions.tsinsert REFUSEDfor the four identity names,insert everyoneandinsert guestwith no provenance, and the built-in pass's restampupdate everyone description,label,managed_by managed_by=platform; the final rows are identical, so only the ledger sees itposition-catalog-refusal.tsadmininstead ofplatformbootstrap-builtin-positions.tsDELETE_RESTRICTED409, the relabel succeeds)Measured on a booted showcase (three postures, same worktree)
Before means
plugin-securitybuilt from the sources at51290bca2c; after means this branch.sys_positionrows (name, organization,managed_by,active,is_default)GET /meta/positionposition@metadata@registryGET /meta/position/everyone,/org_ownerRESOURCE_NOT_FOUNDPUT /meta/position/everyone,/platform_admin(platform admin)NOT_OVERRIDABLEPUT /meta/position/zz_s2_control(control)DELETE /meta/position/guest, nothing storedeveryone, patchplatform_admin, createorg_admin, create a controlVerification (at
85b5c7df04unless stated)plugin-security:vitest run174 files, 3686 passed, 45 skipped (ata8fae6d146; the later commit touches only the boot test, re-run green: 12 of 12);typecheckexit 0, test layer 0 errors.core: 78 files, 2192 tests passed;typecheckexit 0.@objectstack/plugin-security..., then the dogfood closure).rls-multitenantskipped by its own organizations probe, and S1's showcase pin red on 3 rows before the patch round (31 of 31 green at4e969bae6e).dispatch-gates --commandsderives for this diff, run at85b5c7df04: all 65 exit 0, reconciled with--ran(65 derived, 65 run, NOT-MEASURED a derived zero). On the first pass,check:dual-build-cjs-loadsanswered PREREQUISITE NOT MET (eight unbuilt packages outside this diff's closure) and passed once they were built, andcheck:query-options-erasurecaught fouras anyoptions onfind()in the new test (test surface 236 to 240), typed in85b5c7df04and back at 236.Two pins outside the original surface, updated in the patch round
Both asserted the pre-S2 read shape and went red with the six in the engine registry. They lay outside the stage's first declared surface (
plugin-securityonly); the seat declared both (6051162186) and the patch round edited them as below.packages/qa/dogfood/test/security-catalog-showcase.dogfood.test.ts(domain:cli):position: lists exactly the declared namesderives the expected set from the stack'spositionsonly, so all three postures list six names more than it expects. Its door-parity rows stay green: the read and the door agree on 16 names. The edit adds the six, read off@objectstack/spec's constants, to its declared set.packages/runtime/src/standalone-stack-seeder-declaration-copy.test.ts: the row at:285asserts the engine registry holds no position at all withSecurityPluginbooted; it now holds the six. The edit asserts the registry holds exactly the six and none of the stack's.Acceptance notes
PUT /api/v1/meta/position/:namestill silences the stack's declared positions for every organization created after it (walled): the declared seeder's registry-first either-or, unchanged here on purpose. Measured identical before and after: the late organization got the six and the authored position, and none of the showcase's ten. Carried in feat(core,objectql,plugin-security,plugin-sharing): the catalog is read from the registry; assignment tables reference it by name (ADR-0131 D2/D3/D4) #15196 as stage S2b (seat review 6051173832): the declared-positions seeder reads through S1's catalog read.security-catalog.tsrecords a measured table (positions: engine registry 0) taken at3d9188502e; after this stage the engine registry holds the six. It is a dated measurement, not a false claim. Noted for whoever next edits that seam.skip-changesetdoes not apply:@objectstack/plugin-securitypublishesdist, and the changeset isminor.Generated by Claude Code