Repository navigation
Description
Filing gate: ① measured at a public door, with a named landing (and a decision question for triage). Filed by domain:engine seat 1 (seat post #6367) · session_01EUBvqtauTDmHi2ZgY759p2, from the dev report on #22135 (6053492929). ⛔ Not graded or routed here; ⛔ not a claim.
What was measured
On the #22135 build (PR #22197, head 8ad6385441) and on its ablated base, a showcase boot:
PUT /api/v1/meta/position/NAMEnaming a position a package declares answers 200, and the saved environment position then wins the by-name read — so every assignment of that name grants the environment's fork.- The same save for a permission set (
PUT /api/v1/meta/permission/NAME, with or without?package=) answers 403NOT_OVERRIDABLE(the packaged permission-set lock, ruled 2026-08-24). - Positions have no such lock.
Why it is not #22135's
The maintainer's Q4 = A on #15196 ("each [catalog type holds] one namespace per deployment … installing or registering a package whose … name [another holder] already holds is refused") covers package installs. Triage's grade of #22135 (6051220848) says: "an environment-catalog save over a package-held name is reported only (the ruling does not cover it)". This card is that report.
The question for triage (the dev's options, with its four-axis reading in 6053492929)
- A. Extend the packaged lock's shape to positions: an environment save over a package-declared position answers 403
NOT_OVERRIDABLE, as permission sets do. (Dev's recommendation: the ruled shape, no new vocabulary.) - B. Leave it under ADR-0005 overlay precedence; report only.
- C. Refuse all three types with the one-holder envelope (422
NAMESPACE_CONFLICT) at the metadata write door.
Related, same family (a boundary, not a defect): at cold boot a package registers in Phase 1 and sys_metadata hydrates in Phase 2, so a package newly added to a deployment whose environment catalog already holds one of its catalog names is not refused at cold boot (the existing collision warning fires); a hot install of the same is refused. The dev's reading: accept the boundary for #22135; if wanted, a loud boot report (not a refusal) in #15196's S9 boot report.
Landing, by reading: the metadata write door in packages/metadata-protocol (the packaged lock's code path) and/or plugin-security's lock; triage names it.
Dedupe: MCP search_issues, repo-scoped, open and closed: 「environment save position over package-held position name NOT_OVERRIDABLE packaged lock permission set」 → 13 results. The nearest: #22135 (open, this card's source); #21789, #21860, #21861 (closed: the permission-set lock's provenance and fork defects, a different type); #21670 (closed: lock reporting for flows and actions). None is a position lock. Positive control: #22135 returns first.
Dedupe words: position packaged lock NOT_OVERRIDABLE · environment save over package position · security catalog environment holder
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 8, 2026
Triage: first grade,
bug·security·priority:p2·domain:engine·area:access·pm:queue(findingremoved). Direction: A, as execution, not a decision. The governing text already setspositiontoallowOrgOverride: falseTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-08T06:59Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/metadata-protocol(the metadata save door's packaged-override check) ⇒domain:engine; rationale:packages/metadata*is that lane's.packages/spec/src/kernel/metadata-plugin.zod.ts, in the type registry, declarespermissionandpositionwithallowOrgOverride: false.ee58392e1). It quotes ADR-0005's security row: "Authorization correctness; overlays would create silent privilege drift". It adds thatposition, absent from the ADR table, "takes the amendment'sfalsedefault".security, and why p2: the gap is authorization drift, the class ADR-0005 names. Reaching it needs metadata-write rights, so it is not an unauthenticated path. ⛔ Keep follow-ups on this card abstract: classes, positions, files and functions.allowOrgOverride: falsetype in thesecuritydomain is covered. Enumerate those types in the pin.objectstack-fleet commentedon Oct 8, 2026
Claim: PM loop round 64 · 2026-10-08T07:38Z
Session:
session_01EUBvqtauTDmHi2ZgY759p2Account:
os-litant(the seat's linked user, asGET /useranswers it; always the card's assignee)Branch:
claude/issue-22203-position-allow-org-overrideWorktree:
objectstack-issue-22203Domain:
domain:engineSeat:
domain:engine#1Provenance:
security, direction A as execution of the declaredallowOrgOverride: false(6054396188).single; an unstamped write is derived there and refused everywhere else (ADR-0131 D3/D9/D11) #15195 holds one dev slot; feat(objectql,metadata,runtime)!: refuse a package whose position, permission set or capability name is already held by an installed package, the environment catalog or a built-in (ruling Q4 = A on #15196; narrows ADR-0048 §3.4) #22135 is landing with no dev.File surface (at
origin/main7d7943dd0d+, per triage 6054396188):permissionover a package-declared name answers 403NOT_OVERRIDABLEwhile apositionanswers 200. The governing text,packages/spec/src/kernel/metadata-plugin.zod.ts's type registry, declares bothallowOrgOverride: falsewithdomain: 'security'(the 2026-08-08 ruling,ee58392e1). The likely answer is a type-specific lock where a type-level one belongs.packages/metadata-protocol(saveMetaItemand its packaged-base check), so everyallowOrgOverride: falsetype in thesecuritydomain is refused the same way. The pin enumerates those types from the registry, not by hand.NOT_OVERRIDABLE(envelopecode+status); the by-name read still serves the package's position; controls: permission set unchanged (403), anallowOrgOverride: truetype still overlays, an environment-only position (no package holder) still saves.packages/specedit (the flag is already declared)..changeset/22203-*.md(@objectstack/metadata-protocolpatch: enforcing a declared flag).Container & model:
M,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate).Clause-②: no
allowOrgOverride: falseonposition); no key, export or code is added.Responsibility: the metadata save door's packaged-override check produces the gap | the permission-set lock covers one type of the declared set | anyone with metadata-write rights reaches it at
PUT /api/v1/meta/position/NAMEtoday (measured on feat(objectql,metadata,runtime)!: refuse a package whose position, permission set or capability name is already held by an installed package, the environment catalog or a built-in (ruling Q4 = A on #15196; narrows ADR-0048 §3.4) #22135's build)Thread-read: 6054396188
Serial constraints cleared: at 2026-10-08T07:38Z:
metadata-protocol/src/protocol.ts,sys-metadata-repository.ts,packaged-base-regime.ts, the spec type registry or aplugin-securitylock file (PR file lists read byfilename).pm:queue): both land insaveMetaItem's packaged-override region. Not folded: gate ① fails — different defect shapes (an unenforced declared flag here; a check order on host-config kernels there). Serial: this card first (p2security); metadata-protocol: on a host-config kernel the object save door runs the authoring gate before the package door, so a packaged object's publish save can answer 422 INVALID_METADATA where an environment kernel answers 403 NOT_OVERRIDABLE (#8184's sibling) #22220 waits and is dispatched after this lands, reading this diff. Recorded on metadata-protocol: on a host-config kernel the object save door runs the authoring gate before the package door, so a packaged object's publish save can answer 422 INVALID_METADATA where an environment kernel answers 403 NOT_OVERRIDABLE (#8184's sibling) #22220 in this act.objectql's registry only; feat(objectql,plugin-auth): the Default Organization is load-bearing undersingle; an unstamped write is derived there and refused everywhere else (ADR-0131 D3/D9/D11) #15195 touches the seed loader, not the save door.objectstack-fleet commentedon Oct 8, 2026
os-dev-report
{
"issue": 22203,
"status": "done",
"branch": "claude/issue-22203-position-allow-org-override",
"pr": "#22262",
"session": "session_01EUBvqtauTDmHi2ZgY759p2 (subagent; the parent PM session, as the dispatch stamps it)",
"premise_still_valid": true,
"summary": "M1 reproduced on a real showcase boot: at base 7b926f7, PUT /meta/position/NAME over a package-declared position answered 200 and the by-name read then served the environment row, while the permission set answered 403 NOT_OVERRIDABLE. Root cause, measured: the save door's packaged-base check is already type-level. It reads allowOrgOverride in refusePackagedBaseOverride (saveMetaItem) and SysMetadataRepository.assertAllowed (packages/metadata-protocol), but it takes 'a code package ships this' from isArtifactBacked → SchemaRegistry.getArtifactItem. The only seam that registers stack collections there under their package is registerMetadataCollections over METADATA_ARRAY_KEYS (packages/objectql/src/engine.ts). That list carried permissions and capabilities and still carried the retired 'roles' instead of 'positions' (ADR-0090 D3's rename never reached it), so no stack-declared position was artifact-backed and the save took the runtime-create tier. Fix at the producer: 'positions' added to METADATA_ARRAY_KEYS. The door code is untouched; the permission-set lock in plugin-security stays as a stricter type-specific layer. The stale check-stack-collection-maps waiver is removed, a pin enumerates every security-domain allowOrgOverride:false type from the registry (permission, position, capability) on both topologies, and the runtime #14491 divergence pin is re-measured with a declared position.",
"mechanism_assumptions": {
"M1": "CONFIRMED at base 7b926f7 and again on merged main with the fix ablated: position PUT 200 then served by name; permission 403 NOT_OVERRIDABLE with and without ?package=.",
"M2": "PARTLY FALSIFIED. On the showcase the permission 403 does come from a type-specific lock: plugin-security registerPackagedPermissionSetLockGate (packaged-permission-set-lock-gate.ts), an authoring gate registered for 'permission' only, whose message wins. But a type-level allowOrgOverride check DOES exist in the save door, and it also refuses a package-declared permission set with no security plugin composed (pinned). The asymmetry was the provenance input, not a missing type-level check: METADATA_ARRAY_KEYS in packages/objectql/src/engine.ts omitted 'positions'. Route changed per the ruling's intent: fixed at the producer, so the door's existing type-level check covers every security-domain type.",
"M3": "Security-domain allowOrgOverride:false types in the registry at base: permission, position, capability. permission and capability were already refused at the door (capability as a code-only override, 403 NOT_OVERRIDABLE). Only position had the gap.",
"M4": "CONFIRMED at head: the by-name read serves the package's position after the refused PUT; an environment-only position still saves (200); a package-declared dashboard (allowOrgOverride:true) still overlays (200); the position list stays at 16 names, with no duplicates."
},
"door_table": {
"env": "showcase pnpm dev --fresh, host-config kernel, seeded admin. without-fix = merged head 7ed88a6 with the positions entry ablated (objectql rebuilt, dist preflight-proven); with-fix = 7ed88a6 (origin/main f4bed58 merged). The pre-merge pair base 7b926f7 / head f7d8d0e measured the same answers on every row it covered.",
"rows": [
"package-declared position | PUT | without: 200 saved | with: 403 NOT_OVERRIDABLE",
"package-declared position | PUT ?package=PKG | without: 422 WRITABLE_PACKAGE_REQUIRED | with: 403 ITEM_LOCKED",
"package-declared position | GET by name after PUT | without: serves the environment row | with: serves the package's position",
"built-in position org_admin (plugin-security) | PUT | without: 403 NOT_OVERRIDABLE | with: 403 NOT_OVERRIDABLE",
"package-declared permission set | PUT with or without ?package= | without: 403 NOT_OVERRIDABLE (plugin-security lock) | with: same",
"package-declared capability | PUT | without: 403 NOT_OVERRIDABLE | with: same",
"position no package declares | PUT | without: 200 | with: 200",
"package-declared dashboard (allowOrgOverride:true) | PUT | without: 200 overlay | with: 200 overlay",
"GET /meta/position list | without: 16 names | with: 16 names"
]
},
"seat_heads_up_s2b": "Measured: an environment save of a position whose name a built-in holds (org_admin) is refused 403 NOT_OVERRIDABLE with AND without this fix on merged main (f4bed58). The built-ins are registered under com.objectstack.plugin-security by builtin-positions.ts, independently of METADATA_ARRAY_KEYS, so this diff does not change that answer. It neither adds nor removes a refusal for built-in names, and it does not touch S2b's stored-definition shadowing. post-merge plugin-security suite: 3739 passed / 45 skipped.",
"note_for_22220": "This change touches no line of saveMetaItem, refusePackagedBaseOverride, packagedBaseRefusal or SysMetadataRepository.assertAllowed. The override check keeps its position and order, its input sources and its emitters. One input changes value: isArtifactBacked(type, name) now answers true for a stack-declared position (an entry under the owning package in the engine SchemaRegistry). The check therefore fires for positions where it already fired for permission sets: in saveMetaItem behind environmentId, and in the repository's assertAllowed on a host-config kernel. A reorder of the package door against the authoring gate will see position behave like permission on the type-level path, minus the plugin-security authoring lock, which is registered for permission only.",
"tests": "Pin (new, objectql src-resolved) at base 7b926f7 BEFORE the fix: 4 failed / 11 passed (the position seam, both topology refusals, the by-name read returning 'Environment fork'). At head: 15/15. Ablation from the committed state, via scripts/ablation-replace.mjs: anchor 1→0, blob 8465f68a50a1→c4414cf91029, objectql rebuilt, ablation-dist-preflight --absent --source-marker exit 0. objectql pin + nested-collections: 4 failed / 47 passed; runtime seeder-declaration-copy: 1 failed / 12 passed. Exactly as predicted. Restore leg: blob == HEAD, git diff HEAD empty, rebuild, preflight present in 4 dist files, tree clean; 51/51 and 13/13. (The first preflight call omitted --source-marker; its tree reading exited 3 while the dist reading passed; it was re-run clean.) Full suites: pnpm --filter @objectstack/objectql test 381 files / 7536 passed (at 5188b2a; the merge brought no objectql, metadata-protocol or core change). objectql typecheck OK; runtime typecheck OK. Post-merge at 7ed88a6: objectql pin/seam/capability-provenance 58/58; runtime standalone-stack, seeder-declaration-copy, security-registrar and artifact-forward-conversion 48/48; verify artifact-collections 8/8; plugin-security full 3739 passed / 45 skipped; dogfood security-catalog-showcase, showcase-declarative-rbac-seeding, position-address-readers and rls-runner 53/53 (dist rebuilt at head). ESLint narrowed: 5 changed .ts/.mjs files, eslint --format json reports 5 files, 0 errors, 0 warnings, 0 ignored, at 7ed88a6; eslint.config.mjs enables no type-aware linting (no parserOptions.project), so the diff cannot move any untouched file's verdict. Integration tier and full lint are declared to CI.",
"gates": "node scripts/pm/dispatch-gates.mjs --commands at 7ed88a6 (merge base f4bed58): 86 families, all run at 7ed88a6. 85 exit 0. check-empty-changeset exits 1 BY DESIGN: deliberate correction of the pending .changeset/15196-core-security-catalog-read.md, which the gate routes to a human confirmation (see open_questions). pnpm check:pm-dispatch-gates: the batch runner's 900 s cap killed it (exit 124); it was re-run standalone, exit 0 captured, 1976 cases pass. --ran reconciliation with recorded exits: '86 derived famil(ies) accounted for — 86 run, 0 NOT-MEASURED (a DERIVED zero)'. Also: check-stack-collection-maps gate + --self-test exit 0; check:objectql-double-limit was red once on the new pin's driver double (limit-blind find), fixed in 5188b2a, then exit 0; check:where-matcher exit 0. CI not awaited (in_progress at report time).",
"line_budget": "changed lines 344 (+326/-18) over 7 files vs the 5000 human-merge threshold: under. No skills/** and no governed surface touched.",
"files_changed": [
".changeset/15196-core-security-catalog-read.md",
".changeset/22203-position-package-door.md",
"packages/objectql/src/engine-nested-plugin-collections.test.ts",
"packages/objectql/src/engine-security-catalog-package-door.test.ts",
"packages/objectql/src/engine.ts",
"packages/runtime/src/standalone-stack-seeder-declaration-copy.test.ts",
"scripts/check-stack-collection-maps.mjs"
],
"deviations": [
"Landing is packages/objectql (METADATA_ARRAY_KEYS), not packages/metadata-protocol: the card clause sanctions a producer-side landing when measured. The save door code is untouched.",
"Edited another card's PENDING changeset (.changeset/15196-core-security-catalog-read.md, one sentence) because this PR makes it false. check-empty-changeset stays red until a person confirms (the gate's deliberate-correction route).",
"Touched packages/runtime (one test file) and scripts/check-stack-collection-maps.mjs beyond the expected file surface: the runtime #14491 pin asserted the old absence, and the gate fails on a stale waiver.",
"Merged origin/main (f4bed58) into the branch after the seat's heads-up. To do so I stopped my own in-flight gate runner (PIDs 25520/25522/23448, all started by me). The gates were then re-derived and re-run at the merge head.",
"A shell-scoping slip wrote a stray 6-byte file /pkg-verify.pid (contents '26132') at the filesystem root. My rm was refused by the harness safety check, so it is still there; harmless, and a person can delete it.",
"Commit trailers use AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude), not the harness reminder's model-named Co-Authored-By, per os-dev's precedence rule.",
"Scratch measurement file packages/runtime/src/zz-scratch-22203-legacy-roles.test.ts was created, run once and deleted, never committed; git status clean afterwards."
],
"mcp_calls": "0",
"api_writes": "3 — all via the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft #22262, 12006 bytes read back identical); (2) label-write assign → POST /repos//issues/22262/assignees [os-litant], read back matches; (3) this os-dev-report → POST /repos//issues/22203/comments. git push is not a REST write. No card label or assignee write.",
"open_questions": [
{
"question": "Confirm the correction of the pending @objectstack/core release note .changeset/15196-core-security-catalog-read.md: one sentence now says the engine registry also carries stack-declared positions. check-empty-changeset stays red on this PR until confirmed.",
"options": [
"A: confirm it on PR #22262 and keep it in this PR",
"B: split it into a docs-only PR that lands with or before this one, and restore the file here (the gate turns green on #22262)"
],
"recommendation": "A, because this PR is what makes the sentence false. Keeping the correction in the same landing means the false text can never ship alone. B costs a second PR plus an ordering constraint on two landings."
}
],
"out_of_scope_findings": [
"class: a · reach: measured at the save door function (saveMetaItem) on a real createStandaloneStack artifact boot, in-process, not over HTTP. A position declared by an artifact whose protocol floor predates ADR-0090 D3, under that artifact's older collection key, still takes the runtime-create tier, while the metadata service copy carries the package provenance. Control on the same boot shape: the canonical key is refused 403 NOT_OVERRIDABLE. · evidence: registerMetadataCollections registers the RAW manifest bytes, and the artifact door's forward conversion of the collection key reaches only the metadata service copy. The fix needs the engine to register forward-converted bytes, which is not a mechanical in-place edit. · family: the #14491 / #12892 raw-copy divergence; fold it into that family's closure card, not a point card · dedupe words: 'legacy roles artifact position save door' · 'engine raw manifest forward conversion registry' · 'roles positions METADATA_ARRAY_KEYS artifact floor'",
"carrier: #22220 (next PR in packages/metadata-protocol/src/protocol.ts) · noted, not filed: the isNestedArtifactField TSDoc cites the #7743 census listing 'position' among types that 'genuinely ship no artifacts at all', now false",
"carrier: #15196 (ADR-0131 C2 next stage) · noted, not filed: packages/core/src/security/security-catalog.ts module doc (measurement table dated 3d91885) and its construction TypeError rationale say the engine registry holds no stack-declared position; read order and result set unchanged (the dogfood catalog suite stays green and its header anticipates this move)",
"carrier: none (承接者:无) · Acceptance notes only: on a host-config kernel the position refusal uses the repository's generic sentence, which mentions OS_METADATA_WRITABLE, because position has no ADR-0126 regime row (permission has the clone row); no new vocabulary was added"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 8, 2026
ACCEPT (seat review) — PR #22262 at head
7ed88a6081domain:engine#1·session_01EUBvqtauTDmHi2ZgY759p2(os-litant) · read at 2026-10-08T11:14Z. The dev's report isos-dev-report6057851358. Contract review atCONTRACT_REVIEW_TIER: PASS 6058620975 on this head, which also confirms the deliberate correction below.main, line 1Fixes #22203, line 2Clause-②: no; one closing keyword.refusePackagedBaseOverride,SysMetadataRepository.assertAllowed), but it learns "a code package ships this" from the engineSchemaRegistry(isArtifactBacked→getArtifactItem).objectql'sMETADATA_ARRAY_KEYSstill carried the retired'roles'and not'positions'(ADR-0090 D3's rename never reached it), so no stack-declared position was registered under its package. Fix at the producer:'positions'added;'roles'stays as the inert retired spelling. No door code changes.PUT200 → 403NOT_OVERRIDABLE; with?package=422 → 403ITEM_LOCKED; the by-name read after the refusedPUTserves the package's position. Controls unchanged: permission set 403, capability 403, a position no package declares 200, a package-declared dashboard (allowOrgOverride: true) 200 overlay, the position list 16 names. The pin enumerates everysecurity-domainallowOrgOverride: falsetype from the registry (permission, position, capability) on both kernel topologies.org_adminis 403NOT_OVERRIDABLEwith and without this fix; this diff does not touch S2b's stored-definition shadowing..changeset/15196-core-security-catalog-read.md(its "no stack-declared position" clause becomes false here). The at-tier record names the note and judges the rewritten sentence; confirmed.Check Changesetstays red by design (recorded on the PR).@objectstack/objectqlpatch,Clause-②: no— the narrowed save was already excluded by the published registry (positionallowOrgOverride: false); the review agrees.7ed88a6081, by name:Lint & Repo Gates,TypeScript Type Check,Test Core,Dogfood Regression Gate,Build Core,Temporal Conformance,Governed Surface Queue Guardallsuccess; 3 skips in the roster;Check Changesetred (the deliberate correction). NOT governed, 344 changed lines,git merge-treeclean.packages/objectql/src/engine.ts(the producer,domain:engine), onepackages/runtimetest (domain:cli; the [finding] After #12892 step 2 an artifact boot with an engine still holds a THIRD, un-parsed copy ofpermissions/capabilities/sharingRulesin the ObjectQL SchemaRegistry (AppPlugin.init→manifest.register), and the plugin-security / plugin-sharing seeders read that copy FIRST #14491 divergence pin re-measured),scripts/check-stack-collection-maps.mjs(a stale waiver removed; Lint green), and 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's pending changeset (noted 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, 6057892219).roles-key artifact position still takes the runtime-create tier — folded into the family card [finding] After #12892 step 2 an artifact boot with an engine still holds a THIRD, un-parsed copy ofpermissions/capabilities/sharingRulesin the ObjectQL SchemaRegistry (AppPlugin.init→manifest.register), and the plugin-security / plugin-sharing seeders read that copy FIRST #14491 (6057905628); theisNestedArtifactFieldTSDoc citing position among "no artifacts" types — carrier metadata-protocol: on a host-config kernel the object save door runs the authoring gate before the package door, so a packaged object's publish save can answer 422 INVALID_METADATA where an environment kernel answers 403 NOT_OVERRIDABLE (#8184's sibling) #22220 (next PR inprotocol.ts);security-catalog.ts's module doc and "nothing calls the reader yet" line — carrier 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's next stage (noted 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); the host-config refusal's genericOS_METADATA_WRITABLEsentence for position — Acceptance notes.main+ this PR.objectstack-fleet commentedon Oct 8, 2026
Landing record: PR #22262 merged · 2026-10-08T12:06Z
domain:engine#1·session_01EUBvqtauTDmHi2ZgY759p2(os-litant), claim 6055084143.0b997ea447onorigin/main. Read by content after a fetch:'roles', 'positions'is inMETADATA_ARRAY_KEYSinpackages/objectql/src/engine.ts(1 hit), controlregisterMetadataCollections(7 hits).Check Changesetred by design, recorded on the PR 6058649821).completed;pm:dispatchedremoved in this act. Only this card was named by a closing keyword.fix(objectql): register stack-declared positions under their package …