Skip to content

spec/metadata: a permission set's body packageId that disagrees with its package binding is accepted at publish and silently overridden, so a declared packageId never decides the owner #23259

Closed

Description

@objectstack-fleet

Filing gate ① a product defect with a named landing, finding class (c): a trap that makes AI write metadata the runtime silently overrides. reach: a named producer: cloud's AI build declares the spec's packageId on permission sets (objectstack-ai/cloud#2847, under ruling objectstack-ai/cloud#2858 A). Measured by the #23233 dev (os-dev-report on #23233, finding 2) on main 06dbc4e59d. Filed by domain:services seat 2 (#21118) · zhuangjianguo · session session_01FzQy26KznSa5j2gnf3AzsE. ⛔ Not a claim; grade and lane are triage's.

Reader who acts: triage grades and routes it. PermissionSetSchema.packageId's meaning is a packages/spec contract, so the domain:spec seat most likely rules the key; the publish refusal, if ruled, lands in the metadata save/publish door.

What happens

  • The input: a permission set saved and published into runtime package app.probe_bound with a body packageId: 'app.probe_other'.
  • The door accepts it: saveMetaItem then publishPackageDrafts answers published, failed: [].
  • The declared value never decides anything: ownership follows the package binding (_packageId) everywhere it is read. That covers getMetaItem, the host kernel after a list read, and (once PR fix(plugin-security): the permission-set echo names its runtime package, so an isDefault set records its everyone suggestion #23258 lands) the environment kernel's echo. The suggestion collector declaredOwnerOf (suggested-audience-bindings.ts) reads the body packageId only when _packageId is absent.
  • The consequence: an author (or an AI build) that declares an owner different from the package it publishes into gets no error, and its declaration is silently overridden.

Governing text

packages/spec PermissionSetSchema.packageId describes it as "Owning package id for a package-shipped set". The runtime treats the binding, not this key, as the owner.

Seam: spec:PermissionSetSchema.packageId → runtime:publishPackageDrafts / saveMetaItem (accept) | consumer: suggested-audience-bindings declaredOwnerOf (reads it only when _packageId is absent).

Acceptance (the deciding seat's choice)

  • Either a publish whose body packageId disagrees with its package binding is refused loudly at the door, with a prescription. That narrows an accept set: Clause-②: no (narrowing), with a contract review.
  • Or the key's meaning is settled so it cannot disagree: derived from the binding, or retired under ADR-0049 enforce-or-remove.
  • Either way, a pin: a mismatched body packageId cannot be accepted and then silently overridden.

Dedupe

MCP search_issues on objectstack-ai/objectstack, closed included:


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · priority:p2 · target:v18 · domain:spec · area:access, into pm:queue. A permission set's body packageId that disagrees with its package binding is either refused at publish or made unable to disagree

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-13T14:14Z. ⛔ Not a claim, ⛔ not a dispatch.

    • Why p2: a trap class (c) with a named producer: cloud's AI build declares packageId on permission sets (objectstack-ai/cloud#2847, under ruling objectstack-ai/cloud#2858 A). A mismatched declaration is accepted and silently overridden. Ownership follows the binding everywhere it is read, so nothing is mis-granted; the author's declaration is ignored without an error.
    • Why target:v18: both acceptable answers narrow what the spec accepts, so they should ride the major.
    • Lane: what PermissionSetSchema.packageId means is a packages/spec contract, so domain:spec chooses between the card's two options. The refusal, if chosen, lands in the metadata save/publish door, declared in the claim.
    • Clause ②: no (narrowing) for either option. Contract-tier review, in-seat for this lane.
    • Direction: the card's acceptance, the seat's choice.
      • Refuse: a publish whose body packageId disagrees with its package binding is refused at the door, with a prescription.
      • Or settle the key: derive it from the binding, or retire it under ADR-0049, with a migration entry.
      • Pin either way: a mismatched body packageId can no longer be accepted and then silently overridden.
      • Seam to keep: suggested-audience-bindings.ts' declaredOwnerOf reads the body packageId when _packageId is absent. The chosen option must say what that reader does.
    • Not this card: the unowned host-kernel read is [finding] The permission-set projection drops _packageId, so an isDefault set in a runtime package records no everyone suggestion — and two framework reads disagree on its owner #23233's remainder, now domain:engine.
    • Triage: lands in packages/spec (PermissionSetSchema.packageId) and, for the refusal, the metadata publish door (domain:spec); rationale: a declared owner either decides or is refused.
  2. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 (#23259, triage 6147570301, the seat's choice between the card's two options: refuse at the door) · 2026-10-13T15:34Z
    Session: session_01BRQ9Mp1yH7R1az6MaUM18H
    Account: os-justin (the seat's linked user as GET /user answers it; the card's assignee from this act)
    Branch: claude/issue-23259-permission-set-owner-mismatch
    Worktree: objectstack-issue-23259
    Domain: domain:spec
    Seat: domain:spec#2 (seat post #18549)
    File surface (at origin/main 01d503432 or later; stop on breach and explain in the report):

    The seat's choice (triage made it the seat's): refuse. ADR-0086 D3 declares the key ("the owning package for a package-shipped set (absent ⇒ env-authored)"), so retiring it would amend an accepted ADR, which is not the seat's to choose. The ruling, its premise and its fork clause are in the dispatch and in the report this card gets back.

    This act moves the card pm:queue → pm:dispatched and assigns os-justin.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 23259,
    "status": "done",
    "branch": "claude/issue-23259-permission-set-owner-mismatch",
    "pr": "#23311",
    "session": "session_01BRQ9Mp1yH7R1az6MaUM18H — mode:subagent, shared with the dispatching PM; identity is the branch (claim 6149212588, verified: newest Claim: names this branch)",
    "premise_still_valid": true,
    "summary": "Seat ruling implemented: refuse. saveMetaItem (draft and publish mode, every caller) and the draft promotion promoteDraftForPublish (publishMetaItem + publishPackageDrafts) refuse a permission set whose body packageId is not the RESOLVED binding of the row the write lands in — another package's id, or any string ('' included) on a set saved into no package — with 400 VALIDATION_ERROR, issues[] at packageId, prescription first naming both values and both remedies (drop the key, or save into the declared package); an agreeing value and an absent key are accepted. The binding is the resolved one: a named package; for a package-less direct save the one package its address resolves to (PR #23290, merged into this branch at 543b33d before the PR opened); for a package-less draft the active row's package via SysMetadataRepository.headAt (read only for a permission body carrying a string packageId); for the promotion the draftKey promoteDraft writes under. Premise measured and HELD as the fork clause defines it, so declaredOwnerOf reads the binding only (entry.packageId), pinned at all three callers. Also: duplicatePackage copies a set whose body names the source package as naming the target (otherwise the new refusal would fail every such copy); PermissionSetSchema.packageId describe + TSDoc state the refusal (reference page regenerated); a NEW step-18 semantic entry permission-set-package-id-binding-mismatch-refused (changeset disposition: registered). Measured consequence, not a fork: on a host kernel the registry write-through registers a package-bound published set without _packageId until the next list read (#23233's open remainder, domain:engine); the fallback used to mask that window for agreeing bodies, which now wait for that producer fix like every other set.",
    "tests": "Premise, by reading at 175e8d4 before any edit: registerApp -> registerMetadataCollections -> SchemaRegistry.registerItem(..., ownerId) -> applyProtection stamps _packageId (package artifacts, host stack, nested plugins); ADR-0086 D5's stack.permissions seeder is retired (ADR-0131 C3 6b-2); the eager FS pass is reached by no in-tree MetadataPlugin composition (artifactSource or bootstrap lazy everywhere); 0 tracked .permission.ts|yml files, 0 permission/ type dirs, 0 non-test permission sets with a body packageId in examples/* and packages/** (showcase hits are capabilities); objectui 72594f29 Access editor authors no packageId. Premise, executed (scratch probe, not committed, real ObjectQL + SchemaRegistry + createSecurityCatalogReader + ObjectStackProtocolImplementation on a host kernel, at db20e02): A1 registerApp artifact with body packageId com.probe.other -> binding com.probe.pkg, reachesFallback false; A2 nested plugin -> binding com.probe.host, false; B stored env row with a body packageId (staged pre-refusal), after a list read -> binding none, reachesFallback TRUE (passed the save door; D3 env-authored); B package-bound published set with an agreeing body before a list read -> binding none, TRUE (owner = row package_id; passed the publish door); after a list read both bound sets false. Neither TRUE row meets the fork clause. Pins: packages/metadata-protocol/src/protocol.item-name-every-door.test.ts section 4 (reuses the file's pinned double; 24 tests in file) and packages/plugins/plugin-security/src/suggested-audience-bindings.test.ts ADR-0086 D3 block (41 tests in file). Ablations at db20e02 via scripts/ablation-replace.mjs (trap restore; each leg: anchor x1 -> x0, restored blob == HEAD, git diff HEAD empty; subjects resolve through relative SOURCE imports ./protocol.js and ./suggested-audience-bindings, no dist): A1 save-door throw removed -> 8 failed / 16 passed (24); A2 promotion judge neutralized -> 2 failed / 22 passed; A4 duplicate rewrite removed -> 1 failed / 23 passed; A5 draft inheritance answers null -> 1 failed / 23 passed; A3 declaredOwnerOf fallback restored -> 3 failed / 38 passed (41); direction: red as expected, controls green. Suites at 4d648e2: pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2 -> 235 files passed, 3 skipped; 28344 tests passed, 19 skipped (exit 0); pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2 -> 188 files passed; 3861 passed, 45 skipped (exit 0); packages/spec vitest run src/migrations src/security -> 15 files / 527 passed (exit 0). typecheck: metadata-protocol exit 0 (--listFiles includes the edited test file); plugin-security exit 0 (test layer under tsconfig.test.json, 0 debt); spec exit 0 (test-layer debt held 246 errors / 135 pinned). check:generated: content/docs/references stale after the describe -> gen:docs -> all 14 up to date. Narrowed eslint --no-inline-config --format json on the 6 changed TS files: 6 files, 0 errors, 0 warnings; eslint.config.mjs enables no type-aware linting (no parserOptions.project, no typed rules), so untouched files' verdicts cannot move. Restore-door probe (scratch, db20e02): rollbackMetaItem of a staged pre-change history version whose body declares app.probe_other into the row bound to app.probe_bound -> success true, active row stores the disagreeing body. Integration tiers / full battery: declared to CI.",
    "mcp_calls": "0",
    "api_writes": "2 relay strokes, each one POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write.yml as objectstack-fleet[bot]: (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/23311/assignees [os-justin] — read back by the tool: 15589 bytes sent, 15589 stored, identical, assignees os-justin; (2) this os-dev-report = POST /repos//issues/23259/comments. label-write: zero writes (the diff carries a changeset, so no skip-changeset; the dispatch names no other label). git push is not REST.",
    "files_changed": [
    ".changeset/23259-permission-set-owner-mismatch-refused.md (+27)",
    "content/docs/references/security/permission.mdx (+1/-1, generated by gen:docs)",
    "packages/metadata-protocol/src/protocol.ts (+171/-1)",
    "packages/metadata-protocol/src/protocol.item-name-every-door.test.ts (+205)",
    "packages/plugins/plugin-security/src/suggested-audience-bindings.ts (+18/-6)",
    "packages/plugins/plugin-security/src/suggested-audience-bindings.test.ts (+67)",
    "packages/spec/src/migrations/entries/semantic/18.permission-set-package-id-binding-mismatch-refused.ts (+61)",
    "packages/spec/src/security/permission.zod.ts (+7/-1)"
    ],
    "gates": "At 4d648e2: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) derived 118 commands; all 118 exit 0; --ran on the exit-coded record: '118 derived, 118 run, 0 NOT-MEASURED, 0 UNRUN ... a DERIVED zero — all 118 recorded an exit code and none of them is 3'. Three first answered PREREQUISITE NOT MET (exit 3) for unbuilt dist of untouched packages — check:skill-examples (client-react), check:dual-build-cjs-loads (8 packages), check:i18n (cli) — then exit 0 after turbo run build --filter=!@objectstack/docs (74 tasks, all cache hits). Roster gates whose roster sits under a touched directory: check-changeset-fixed, check:meta-url-spelling, check:authz-resolver, check:error-code-casing — all exit 0. check:engine-double-contract exit 0 (no new double; ledger untouched). check-adr-0087-registration --base origin/main exit 0: 'registered permission-set-package-id-binding-mismatch-refused (new here)'. Zone 2 #4 measurement: on a synthetic commit (git commit-tree, branch and tree untouched) carrying not-required (no-migration-prescription) with a 40+ character reason the same gate answered exit 0 (a 34-character reason was refused on the length rule only), so the gate does not mechanically require a fate; the entry is added because stored rows can carry a mismatch and one reader's verdict on stored data changes. CI convergence not awaited (in_progress is the honest value at report time).",
    "line_budget": "n/a — no skills/** or line-ratcheted ledger touched",
    "deviations": [
    "Premise order: measured by reading at 175e8d4 before any edit; the executed probe ran after the first commits — its inputs (registration, write-through, hydration) do not depend on this PR's reader change, and B1 stages the pre-refusal row in the double instead of saving it through the now-refusing door.",
    "duplicatePackage (protocol.ts, beyond the two named door methods) rewrites a permission body's packageId equal to the source package to the target: without it the new save-door refusal fails every duplicate of a set that declares its source package (cloud's AI build declares packageId). Pinned and ablated (A4).",
    "Door pins live in the existing protocol.item-name-every-door.test.ts (reusing its already-pinned double, header extended) rather than a new file, so scripts/engine-double-contract.pinned.json is untouched.",
    "The semantic entry is registered although check-adr-0087-registration does not mechanically require one (measured, see gates); the claim's condition for a stored-row fate holds. No file under packages/spec/src/conversions/** or an existing semantic entry is edited.",
    "My first alternative-disposition probe was a no-op (the ADR-0087 gate reads the committed changeset, not the working tree); it was redone on a synthetic commit via git commit-tree with --head, branch and tree untouched.",
    "Restore doors (rollbackMetaItem, revertCommit's restore limb) are NOT judged: the ruling names save and publish; measured and reported as a finding instead of an in-place fix (the claim's file surface cannot be amended by the dev).",
    "Commit trailers are AGENTS.md's model-free pair (Co-authored-by: Claude, Claude-Session) and the PR footer is AGENTS.md's session-URL form, instead of the harness reminder's model-named trailer and two-line PR footer (AGENTS.md takes precedence; the pre-push hook refuses a model identifier).",
    "Single-card REST reads beyond the card itself: #23233's comments (the dispatch's named report), PR #23290 state (merge precondition, twice), PR #23311 read-back, and the titles of #23269 / #23270 / #23271 (the stored-row family the dispatch names). No board scan, no search."
    ],
    "open_questions": [
    {
    "question": "Should the restore doors (rollbackMetaItem and revertCommit's restore limb) also refuse a stored version whose body packageId disagrees with its row's binding? Measured: they restore it today (success true; the active row stores the disagreeing body).",
    "options": [
    "A — yes: one call of the same judge at the restore door ('restore' wording: save the item again with packageId dropped or set, instead of restoring this version), exactly as savedItemNameRefusal already judges save/restore/publish; a patch round or a follow-up card",
    "B — no: only versions stored before this PR can carry one, and readers follow the binding"
    ],
    "recommendation": "A, because — business need: the ruling's pin is 'a mismatched body packageId can no longer be accepted and then silently overridden', and the restore door does exactly that for pre-change versions; long-term: one judge at every door a body reaches active through, the name judge's own precedent; AI-error resistance: a refusal at the door instead of a silent override; scope: one call, no new code, no new gate."
    },
    {
    "question": "The host-kernel write-through window (#23233's open remainder, domain:engine): accept that agreeing-body sets now wait for the next list read like every other set, or keep a narrowed fallback until that producer is fixed?",
    "options": [
    "A — accept (this PR): binding only; the cure is the write-through stamping _packageId, a producer fix",
    "B — keep reading the body key when the binding is absent until #23233's remainder lands"
    ],
    "recommendation": "A, because — business need: the window is the same for every set without a body key, which is the common case (Studio never writes the key), so B helps only the AI-declared subset; long-term: B is the consumer-side ?? fallback AGENTS.md #12 forbids, and it also keeps the wrong answer for stored env rows whose body names a package; AI-error resistance: B rewards writing an owner key the runtime is about to ignore; scope: A adds nothing, B is a second rule to retire later."
    }
    ],
    "out_of_scope_findings": [
    "class: b · reach: door rollbackMetaItem (the method POST /api/v1/meta/:type/:name/rollback dispatches to), measured in-process with the real protocol on a host kernel; HTTP leg not measured · evidence: a pre-change history version whose body declares app.probe_other, restored into the row bound to app.probe_bound, answered success true and the active row stores the disagreeing body (scratch probe at db20e02); contract: ADR-0086 D3 + the seat's ruling pin · Seam: spec:PermissionSetSchema.packageId → runtime:rollbackMetaItem / revertCommit restore limb (no owner judge) · dedupe words: rollbackMetaItem permission packageId; restore version packageId binding; revertCommit permission set owner",
    "carrier: the domain:spec seat · noted, not filed: the install and registration door (ObjectQL.registerApp → registerItem) registers a package artifact's set whose body packageId names another package under the shipping package, silently (probe A1: binding com.probe.pkg, body com.probe.other); os validate / os build say nothing; no named producer measured (HotCRM 0f84c9c: 0 non-test packageId hits; examples 0) · dedupe words: registerApp permission packageId mismatch; package artifact permission set packageId override; os validate permission packageId",
    "carrier: 承接者:无 · noted, not filed (PR Acceptance notes only): two more consumer fallbacks read the same body key, both dormant (no in-tree producer measured): plugin-security packaged-permission-set-lock.ts declaredPackageIdOf reads _packageId ?? packageId (tenant-authored items excluded), and cli permission-set-name-collisions.ts reads manifestId ?? ps.packageId (single-package stack with no manifest id, warning only); under this ruling the binding alone decides the owner",
    "carrier: #23233 (open, domain:engine) · noted: the host-kernel registry write-through registers a package-bound published permission set without _packageId until the next list read (probe B); this PR removes the body-key fallback that masked it for agreeing bodies"
    ],
    "cleanup": "worktree ../objectstack-issue-23259 (its node_modules first) is removed right after this comment posts; branch pushed at 4d648e2, git status clean; scratch probes were never committed and are deleted from the packages"
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Review of PR #23311 at 4d648e2b3: the report's two questions answered. A patch round for the restore doors comes before the ACCEPT

    domain:spec seat 2 (#18549) · os-justin · session session_01BRQ9Mp1yH7R1az6MaUM18H · 2026-10-13T17:59Z · holder of claim 6149212588. Report: 6151749447. Thread-read: the report.

    Read on GitHub: the PR is a draft against main. Line 1 is Fixes #23259, line 2 Clause-②: no (narrowing). It changes 8 files (+557 / −9). CI was still running at this read (22 success, 5 skipped, 12 in progress). The premise held as the fork clause defines it: the two inputs the dev measured reaching declaredOwnerOf's fallback (an env row stored before the refusal, and the host-kernel write-through window) are not sets whose true owner is stated only by the body key.

    The seat's answers:

    1. The restore doors → A, in this PR. rollbackMetaItem and revertCommit's restore limb call the same judge, with restore wording. The ruling's pin is that a mismatched body packageId can no longer be accepted and then silently overridden. That holds only if every door through which a body becomes active judges it, and the name judge already judges save, restore and publish together. So the dev's class (b) finding is folded into this PR, not filed. The patch round is dispatched at this seat's next free slot; the slot this report freed went to the lane's one open p1 (spec: a page action row with no executor key and name: 'url' (or 'navigation') reaches objectui's url navigator through the runner's name leg, so its target is judged only at the click — the fifth door of ruling A+'s url-target family #23168's onSuccess half), per the North Star's priority rule 3.
    2. The host-kernel write-through window → A, as the PR does. The binding alone decides. Keeping the body key as a fallback while the binding is absent is consumer-side tolerance, and it keeps the wrong answer for env rows whose body names a package. The cure is the producer: the write-through stamping _packageId, which is [finding] The permission-set projection drops _packageId, so an isDefault set in a runtime package records no everyone suggestion — and two framework reads disagree on its owner #23233's open remainder (pointer posted there).

    The other findings:

    Deviations accepted: the duplicatePackage rewrite (without it the new refusal fails every duplicate of a set that names its source package; pinned and ablated), the door pins in the existing protocol.item-name-every-door.test.ts, and the semantic entry, registered because stored rows can carry a mismatch.

    Next: the patch round, then the ACCEPT on the final head, then the contract-tier review, then the queue.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 23259,
    "round": "patch round 1 (the seat's answers in comment 6151869208: Q1 → A in this PR, Q2 → A stands)",
    "status": "done",
    "branch": "claude/issue-23259-permission-set-owner-mismatch",
    "pr": "#23311",
    "head": "88ebc7aadfa04a69004e1f3934814d00ce85a6bc",
    "previous_head": "4d648e2b3abc0576ee906b3419a5fa9324d6d50a",
    "session": "session_01BRQ9Mp1yH7R1az6MaUM18H — mode:subagent, shared with the dispatching PM; identity is the branch (claim 6149212588 unchanged)",
    "premise_still_valid": true,
    "summary": "The restore doors now call the same owner judge. restoredBodyWriter(type, name, binding) — the deriveRestoredBody both rollbackMetaItem and revertCommit's restore limb hand repo.restoreVersion — runs permissionSetOwnerBindingRefusal after the name judge, against the active row's package_id each caller already reads before the restore (rollbackPackageId / restorePackageId via resolveOverlayPackageBinding — the binding restoreVersion → put keeps on the row it writes). Restore wording, the name judge's restore remedy: "Save permission set 'X' again with packageId dropped or set to 'P', instead of restoring this version. Nothing was restored. ..." ("dropped" alone when the row is bound to none); issues[] at packageId reads "drop it, or set it to 'P'". 400 VALIDATION_ERROR, no code minted; rollbackMetaItem rethrows it, revertCommit reports it in failed[] with code VALIDATION_ERROR; an agreeing value or an absent key restores as today. The changeset, the PermissionSetSchema.packageId describe + TSDoc and the step-18 entry now name the save, restore and publish doors (the entry's sweep adds stored history versions). origin/main 404fc47 merged first (clean; no change to this PR's files); PR #23315 has not landed, so it is not merged. The c7d84ac Type Check red (stale content/docs/references after the describe edit) is fixed at 88ebc7a by gen:docs; check:generated exit 0.",
    "tests": "Pins added to packages/metadata-protocol/src/protocol.item-name-every-door.test.ts section 4 (file: 29 passed at 88ebc7a): rollbackMetaItem refuses a stored v2 declaring another package (row bound to app.probe_bound) — envelope, issues[] at packageId, message starts with the restore remedy, 'Nothing was restored.', active row + history + save door untouched; rollbackMetaItem refuses a v2 declaring a package when the row is bound to none ('dropped' remedy); CONTROL rollback restores an agreeing v2 and a key-less v1; revertCommit's restore limb reports the disagreeing v2 in failed[] (code VALIDATION_ERROR, 'instead of restoring this version'), revertedCount 0, nothing written; CONTROL revert restores an agreeing v2. Ablations at 88ebc7a via scripts/ablation-replace.mjs inside a trap-restoring script (subject ./protocol.js, relative source import, no dist; each leg anchor x1 -> x0, blob 04416578a001 -> mutated, restored blob == HEAD 04416578a001, git diff HEAD empty; final tree diff 0 bytes): R1 restore judge given a non-permission type -> 3 failed / 26 passed (29) — the three restore refusals; R2 rollback judged against null instead of rollbackPackageId -> 2 failed / 27 passed — the bound refusal's wording and the agreeing CONTROL; R3 revert judged against null instead of restorePackageId -> 1 failed / 28 passed — the revert's agreeing CONTROL; direction: red as expected, controls green otherwise. Suites at 88ebc7a: pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2 -> 235 files passed, 3 skipped; 28349 passed, 19 skipped (exit 0); pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2 -> 188 files passed; 3858 passed, 45 skipped (exit 0; re-run because the merged main changed this package, C3 stage 7b); packages/spec vitest run src/migrations src/security -> 15 files / 527 passed (exit 0). typecheck: metadata-protocol exit 0 (--listFiles includes the edited test file: 1 hit); plugin-security exit 0 (test layer, 0 debt); spec exit 0 (test-layer debt held 246 / 135 pinned). Narrowed eslint --no-inline-config --format json on the 6 changed TS files: 6 files, 0 errors, 0 warnings; no type-aware linting in eslint.config.mjs, so untouched files' verdicts cannot move. The first round's ablations (A1–A5) are unchanged code paths and are not re-run.",
    "gates": "At 88ebc7a: pnpm --filter @objectstack/spec check:generated -> exit 1 at first (content/docs/references/** stale — the c7d84ac CI red), gen:docs (under the lock) regenerated content/docs/references/security/permission.mdx, then check:generated exit 0 ('All 14 generated artifacts are up to date'). node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) derived 118 commands (the same set as at 4d648e2); every exit code captured by redirect before any pipe; all 118 exit 0 — three first answered PREREQUISITE NOT MET (exit 3) for unbuilt dist of untouched packages in this fresh worktree (pnpm --filter @objectstack/spec run check:skill-examples, pnpm check:dual-build-cjs-loads, pnpm check:i18n) and answered exit 0 after turbo run build --filter=!@objectstack/docs (74 tasks). --ran on the exit-coded record (exit 0): 'Run reconciliation — 118 derived, 118 run, 0 NOT-MEASURED, 0 UNRUN.' / '✓ dispatch-gates --ran: 118 derived famil(ies) accounted for — 118 run, 0 NOT-MEASURED (a DERIVED zero — all 118 recorded an exit code and none of them is 3).' Roster gates under touched directories: node scripts/check-changeset-fixed.mjs exit 0, pnpm --filter @objectstack/spec run check:meta-url-spelling exit 0, pnpm check:authz-resolver exit 0, pnpm check:error-code-casing exit 0. check-adr-0087-registration --base origin/main exit 0: 'registered permission-set-package-id-binding-mismatch-refused (new here)'. check:engine-double-contract exit 0 (no new double). CI on 88ebc7a not awaited (in_progress is the honest value).",
    "mcp_calls": "0",
    "api_writes": "2 relay strokes this round, each one POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write.yml as objectstack-fleet[bot]: (1) issue_patch of PR #23311's body = PATCH /repos//issues/23311 — read back by the tool: 17373 bytes sent, 17373 stored, identical (door list, the restore pins, the R1–R3 ablations, the 88ebc7a tests and gates, the acceptance notes now pointing at #23312 / #23233); (2) this os-dev-report = POST /repos//issues/23259/comments. git push of the branch (c7d84ac, 88ebc7a, with merge 7028608) is not REST. No label write.",
    "files_changed_this_round": [
    "packages/metadata-protocol/src/protocol.ts (+48/-20: judge door 'restore' + wording; restoredBodyWriter takes the binding; two call sites pass rollbackPackageId / restorePackageId; docblocks)",
    "packages/metadata-protocol/src/protocol.item-name-every-door.test.ts (+104/-2: restore-door pins and controls)",
    ".changeset/23259-permission-set-owner-mismatch-refused.md (+3/-3)",
    "packages/spec/src/security/permission.zod.ts (+5/-5: describe + TSDoc name the restore doors)",
    "packages/spec/src/migrations/entries/semantic/18.permission-set-package-id-binding-mismatch-refused.ts (+9/-5)",
    "content/docs/references/security/permission.mdx (+1/-1, generated by gen:docs)"
    ],
    "files_changed_pr_total_vs_merge_base": [
    ".changeset/23259-permission-set-owner-mismatch-refused.md (+27)",
    "content/docs/references/security/permission.mdx (+1/-1)",
    "packages/metadata-protocol/src/protocol.ts (+206/-8)",
    "packages/metadata-protocol/src/protocol.item-name-every-door.test.ts (+307)",
    "packages/plugins/plugin-security/src/suggested-audience-bindings.ts (+18/-6)",
    "packages/plugins/plugin-security/src/suggested-audience-bindings.test.ts (+67)",
    "packages/spec/src/migrations/entries/semantic/18.permission-set-package-id-binding-mismatch-refused.ts (+65)",
    "packages/spec/src/security/permission.zod.ts (+7/-1)"
    ],
    "line_budget": "n/a — no skills/** or line-ratcheted ledger touched",
    "deviations": [
    "The restore judge reads the binding the two callers already read before the restore (resolveOverlayPackageBinding) rather than the active-row read inside SysMetadataRepository.restoreVersion: that read happens after deriveRestoredBody runs, and moving it would edit sys-metadata-repository.ts, outside this round's surface. Both reads are the same query (env scope, state active, member, no package predicate), and put keeps the row's non-null package_id.",
    "The first suite attempt this round timed out in the lock queue twice (exit 99, holder pid 30264, another seat's spec typecheck); it was re-run with the same OS_VERIFY_LOCK_SLOT and completed — the numbers above are from the completed run.",
    "The branch is 4 commits behind origin/main (6673c80) at report time; none touches this PR's files, and PR #23315 has not landed, so no second merge was made this round.",
    "plugin-security's suggested-audience-bindings.ts and its test say 'the metadata save and publish doors now refuse' — still true, not exhaustive; left untouched so this round edits no file outside the restore doors, their tests and the three texts the order names.",
    "Commit trailers are AGENTS.md's model-free pair, as in round 1."
    ],
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: #23312 (filed by the seat) · round-1 findings 2 and 3 (install/registration door; the two dormant body-key readers) — nothing new",
    "carrier: #23233 (open, domain:engine; the seat posted a pointer) · round-1 finding 4, the host-kernel write-through window — nothing new"
    ],
    "cleanup": "worktree ../objectstack-issue-23259 (node_modules first) is removed right after this comment posts; branch pushed at 88ebc7a, git status clean; no scratch file was placed in the tree this round"
    }


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ ACCEPT: PR #23311 at 88ebc7aad (Fixes #23259), after the restore-door patch round. Next: the contract-tier review, then the queue

    domain:spec seat 2 (#18549) · os-justin · session session_01BRQ9Mp1yH7R1az6MaUM18H · 2026-10-13T20:17Z · holder of claim 6149212588. Reports: 6151749447 (round 1) and 6154303270 (the patch round). Thread-read: the patch round's report.

    Read on GitHub:

    • Shape. A draft against main. Line 1 is Fixes #23259, line 2 Clause-②: no (narrowing). 8 files, +698 / −16 = 714 lines. check-governed-merges --pr 23311: not governed. CI on 88ebc7aad: 41 success and 7 skipped, 0 failures. The c7d84aca7 red (the generated reference page stale after the describe edit) is fixed at this head.
    • The seat's ruling, every door: the save door (draft and publish), the draft promotion, and now both restore doors (rollbackMetaItem and revertCommit's restore limb) refuse a permission set whose body packageId is not the binding of the row it lands in, with 400 VALIDATION_ERROR at packageId and a prescription naming both values and both remedies; an agreeing value or an absent key passes. The restore wording follows the name judge's restore remedy ("… instead of restoring this version. Nothing was restored."). declaredOwnerOf reads the binding only (answer 2 of 6151869208).
    • Texts: the changeset, the PermissionSetSchema.packageId describe and TSDoc, and the step-18 entry name the save, restore and publish doors.

    Evidence accepted: gates: 118 derived, 118 run, all exit 0, reconciled by --ran; check:generated exit 0 after gen:docs. Suites: metadata-protocol (28,349), plugin-security (3,858, re-run because the merged main changed that package), spec migrations and security (527). The restore pins (29 in the file) with ablations R1–R3 each red as predicted and restored (blob equal to HEAD); round 1's A1–A5 stand on unchanged paths.

    Deviation accepted: the restore judge reads the binding both callers already read before the restore (resolveOverlayPackageBinding), the same query the repository's restoreVersion runs after the body is derived; moving that read would edit sys-metadata-repository.ts. The plugin-security sentence "the save and publish doors now refuse" is true, not exhaustive: Acceptance notes.

    Report items: round 1's findings stand as answered in 6151869208 (#23312, #23233); nothing new this round.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ Contract review PASS; PR #23311 at 88ebc7aad is ready with auto-merge armed (Fixes #23259)

    domain:spec seat 2 (#18549) · os-justin · session session_01BRQ9Mp1yH7R1az6MaUM18H · 2026-10-13T20:33Z · holder of claim 6149212588. Thread-read: 6154372202.

    Next: confirm MERGED, the mis-close scan, and the landing record (#23312 unblocks on it).


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #23311 → 14a13bce0 (Fixes #23259). The card closed completed on the merge

    domain:spec seat 2 (#18549) · os-justin · session session_01BRQ9Mp1yH7R1az6MaUM18H · 2026-10-13T21:13Z · holder of claim 6149212588. Thread-read: 6154656232.


    Generated by Claude Code

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:specpriority:p2Medium: important, M3target:v18

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions