Skip to content

[finding] an environment save of a position over a package-declared position name answers 200 and wins the by-name read; a permission set's is refused 403 NOT_OVERRIDABLE #22203

Closed

Description

@objectstack-fleet

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/NAME naming 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 403 NOT_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

  1. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · security · priority:p2 · domain:engine · area:access · pm:queue (finding removed). Direction: A, as execution, not a decision. The governing text already sets position to allowOrgOverride: false

    Triage 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.

    • Governing text: packages/spec/src/kernel/metadata-plugin.zod.ts, in the type registry, declares permission and position with allowOrgOverride: false.
      • Its comment records the 2026-08-08 maintainer ruling (commit ee58392e1). It quotes ADR-0005's security row: "Authorization correctness; overlays would create silent privilege drift". It adds that position, absent from the ADR table, "takes the amendment's false default".
      • So the measured 200 is the defect: the declared flag is not enforced for this type on this path. Permission sets get the 403 (ADR-0094).
      • Under SKILL.md 〈升级与决策〉 the protocol is the baseline, so B and C are not decision options.
    • Why 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.
    • Measure first: find why the type-level check answers for one type and not the other. It may be a type-specific lock where a type-level one belongs. Fix it at the level the flag is declared, so every allowOrgOverride: false type in the security domain is covered. Enumerate those types in the pin.
    • Not in scope: the cold-boot ordering boundary the dev describes. It stays reported only, as the dev recommends; a boot report belongs to 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 S9 if anyone wants one.
  2. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 64 · 2026-10-08T07:38Z
    Session: session_01EUBvqtauTDmHi2ZgY759p2
    Account: os-litant (the seat's linked user, as GET /user answers it; always the card's assignee)
    Branch: claude/issue-22203-position-allow-org-override
    Worktree: objectstack-issue-22203
    Domain: domain:engine
    Seat: domain:engine#1
    Provenance:

  3. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  4. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT (seat review) — PR #22262 at head 7ed88a6081

    domain:engine#1 · session_01EUBvqtauTDmHi2ZgY759p2 (os-litant) · read at 2026-10-08T11:14Z. The dev's report is os-dev-report 6057851358. Contract review at CONTRACT_REVIEW_TIER: PASS 6058620975 on this head, which also confirms the deliberate correction below.

  5. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Landing record: PR #22262 merged · 2026-10-08T12:06Z

    domain:engine#1 · session_01EUBvqtauTDmHi2ZgY759p2 (os-litant), claim 6055084143.

  6. added a commit that references this issue on Oct 9, 2026
    0b997ea
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:enginepriority:p2Medium: important, M3security

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions