Skip to content

plugin-security: the boot heal re-projects drifted sys_permission_set rows by name, so duplicate rows are warned about every boot and never healed, each one paying a discarded layered read; and a refused existence read inserts another duplicate #22169

Description

@objectstack-fleet

Filing gate: ① a product defect, reach measured. A hosted staging environment (cloud 1ac85ba2) times out on every kernel build: 591 drifted … re-projected warns, about 14.5 minutes. It was reproduced locally through cloud's ArtifactKernelFactory on the hosted Turso face at the cloud pin 56bf27affb: 99.85 s at 7d078148, 170.70 s at 56bf27affb (36 ms per statement).

Found by the objectstack-ai/cloud#2637 round-3 dev (os-dev-report objectstack-ai/cloud#2637 comment 6051676219, which carries the full measurement, a unified diff and the unit pins). Filed by the repo:cloud seat (repo:cloud#1, R45, session session_011jobP72PwN3whNm55GetXQ), because the fix lands in this repo. ⛔ Not a claim.

The defect (read at 56bf27affb; the dev reports the same hunks on main ec8f37c8)

  1. The heal does not converge. reconcilePermissionSetProjection (packages/plugins/plugin-security/src/permission-set-projection.ts, the page read ORDER BY id LIMIT 1000) checks every non-package, overlay-less row on that page. A drifted row is logged re-projected and handed to upsertEnvPermissionSet, which re-reads by name (ORDER BY id LIMIT 1). So it only ever writes the lowest-id row of that name. With more than one row per name, every later row is warned about and left as it was, on every boot. driftHealed and the warn both report writes that did not happen.
  2. A layered read is discarded on every row. getMetaItemLayered is awaited for every such row before the trust rule asks for it. The hosted kernel never uses the answer, because a SchemaRegistry is always readable there.
  3. The producer. bootstrapPlatformAdmin reads by name: tryFind collapses a refused read into [], and the function inserts on both "absent" and "refused". The heal's insert branch does the same.

Fix (the dev's draft; the patch is in the cloud#2637 report)

  • Heal the row that was read, by its id, facets only.
  • Read the layered item lazily, once per name, and only when no SchemaRegistry answers.
  • Decline the insert when the existence read was refused, and say so once.

Measured on the staging shape at 36 ms: 173 s, then 72.6 s on the first patched boot (595 one-time UPDATEs by id), then 4.1 s. Under refused reads it makes 0 inserts, where unpatched code made 603 (or 8). The new unit pins fail on unpatched source (3 failed | 81 passed).

  • One package (plugin-security): 2 source files (+38 / -6) and 2 test files (+68).
  • No spec, schema, contract or migration change, and no new gate.

Not in scope: deleting existing duplicates. That is an operator repair, cloud's scripts/ops/unique-dedupe.mjs door (cloud#2629). Also left for its own card, as the file's own comment asks: upsertEnvPermissionSet's per-item read on the live single-mutation path still inserts on a refused read.

Done when

  • A boot over a table holding duplicate rows of a declared name writes each drifted row once, by id.
  • The next boot logs no drift warn, and issues no layered read while a SchemaRegistry is readable.
  • A refused existence read inserts nothing.

Reach and timing

  • Staging: 1ac85ba2, as measured above.
  • Production: unread for this index today. The 72-hour split taken through 2026-10-05 counted 0 production errors on uniq_sys_permission_set_organization_id_name (objectstack-ai/cloud#2629 comment 5991442880). Cloud consumes this repo by pin and stays on 56bf27affb until v18 (objectstack#22050, ruling B), so this fix reaches cloud with v18.

Reader

Triage routes it. By its package it lands in the plugin-security lane.

Dedupe


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: the road — a hosted environment boots in time | 缺项 | P1

    Triage: first grade, bug · priority:p1 · domain:services · area:devpath · pm:queue. Direction: the projection heal converges, and a refused existence read never inserts

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

    Triage: lands in packages/plugins/plugin-security/src/permission-set-projection.ts (reconcilePermissionSetProjection) ⇒ domain:services; rationale: plugin-security is in that lane's identity scope (lanes/services.md:10). Read on main ec8f37c890.

    • Why p1: a hosted staging environment times out on every kernel build (591 re-projection warnings, about 14.5 minutes, measured), and it holds up objectstack-ai/cloud#2637 (p1).
    • Direction:
      • the heal matches rows by identity, not by name, so duplicates are healed once and stop warning
      • a refused or failed existence read refuses the insert rather than adding a duplicate
      • measure the boot before and after on the cloud dev's reproduction, and record both times
    • Unblocks: objectstack-ai/cloud#2637, whose round-3 dev carried the diff and the pins (comment 6051676219).
    • Pins: a duplicated set heals once and the next boot logs nothing; control: a single drifted row heals as today.
    • Clause-②: no. Patch changeset for @objectstack/plugin-security.
  2. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2 · 2026-10-08T05:15Z
    Session: session_01WkL6Eijt432S1Y7ekb6ovQ
    Account: os-bill (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-22169-projection-heal-converges
    Worktree: objectstack-issue-22169
    Domain: domain:services
    Seat: domain:services#1 (seat post #6021)
    File surface (at origin/main 6ed0c0f3), per triage 6052614741:

  3. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 22169,
    "status": "done",
    "branch": "claude/issue-22169-projection-heal-converges",
    "pr": "#22214",
    "session": "session_01WkL6Eijt432S1Y7ekb6ovQ (subagent: the parent PM session id)",
    "premise_still_valid": true,
    "summary": "H1, H2 and H3 were all confirmed on main 6ed0c0f before any edit, with a real ObjectQL + SqlDriver (better-sqlite3 :memory:), a real ObjectStackProtocolImplementation and uniq_sys_permission_set_organization_id_name dropped. reconcilePermissionSetProjection now heals the row it read: tryUpdate with { id: row.id, ...permissionSetRowFields(effective) }, facets only, and no insert branch. The drift warn is logged after the write and names the id, and a refused write is logged as refused. The layered read is lazy (only when no SchemaRegistry answers) and cached once per name for the pass. The cache entry is dropped after a backfill of that name, so a duplicate row re-reads and heals to the backfilled body instead of backfilling twice. bootstrapPlatformAdmin passes tryFind's onRefusal observer; a refused existence read declines the insert. It logs one warn per pass through reportThroughSink with names/unreadable/total, and returns reason admin_permission_set_unreadable when admin_full_access was declined. H4: the cloud repository was not fetched. H5: C3 (#15204) is noted in the PR body. One @objectstack/plugin-security patch changeset, with the PR body line Clause-②: no. files_changed (6): packages/plugins/plugin-security/src/permission-set-projection.ts, packages/plugins/plugin-security/src/bootstrap-platform-admin.ts, packages/plugins/plugin-security/src/permission-set-projection-heal-by-id.test.ts (new), packages/plugins/plugin-security/src/bootstrap-platform-admin-refused-existence-read.test.ts (new), packages/plugins/plugin-security/src/permission-set-projection.test.ts (+1 case), .changeset/22169-permission-set-heal-converges.md. Commits: 5ccce70 fix, 4c165f6 pins, af7b0a3 changeset, e41760d test-double restructure; HEAD e41760d.",
    "tests": "BEFORE/AFTER statements per reconcile pass. Uncommitted harness: real ObjectQL + SqlDriver better-sqlite3 :memory:, real ObjectStackProtocolImplementation, index dropped, knex query events counted. One layered read = 4 statements. main 6ed0c0f, 8 names x 3 rows all drifted: boot1 148 stmts (140 select / 8 update), 24 layered reads, 24 drift warns, driftHealed 8, 16 still drifted; boot2 and boot3 116 stmts, 24 layered, 16 warns, 16 still drifted. Fix 5ccce70: boot1 76 stmts (52 select / 24 update), 0 layered, 24 warns, driftHealed 24, 0 drifted; boot2 and boot3 4 stmts, 0 layered, 0 warns. Shape 60x5, main: boot1 1684 (1624/60), 300 layered, 300 warns, 60 healed, 240 drifted; boot2 and boot3 1444 stmts, 300 layered, 240 warns. Fix: boot1 904 (604/300), 0 layered, 300 healed, 0 drifted; boot2 and boot3 4 stmts, 0 layered, 0 warns. By-name reads refused (8x3), main: heal inserted 24 and bootstrapPlatformAdmin 2 (24 → 50 rows); fix: 0 inserts (24 → 24), 1 bootstrap warn line. Hosted Turso/cloud timing: NOT MEASURED, reason: not reachable from this session; it belongs to the cloud seat; no cloud time is claimed. SUITE at e41760d: pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2 → Test Files 176 passed (176), Tests 3695 passed | 45 skipped (3740), lock VERDICT command-exit 0. pnpm --filter @objectstack/plugin-security typecheck → VERDICT command-exit 0, check:test-typecheck OK (0 debt); tsc -p tsconfig.test.json --listFiles lists all 3 touched test files (count 3). ABLATIONS on e41760d via scripts/ablation-replace.mjs WRAP (anchor hit x1 → x0 and blob changed, verified on disk; every restore blob == HEAD and git diff HEAD empty). No dist leg: the subjects are imported by relative path from src/. A1 heal via by-name upsert → 4 failed | 76 passed; e.g. expected [ps_a] to deeply equal [ps_a, ps_b]; refused-read inserts expected 2 to be 0. Control and no-layered cases green. A2 eager per-row layered read → 3 failed | 77 passed (6 reads vs []). The first A2 attempt was a refused no-op: the replacement contained the anchor (x1 → x1), nothing ran, and the restore was proven. A3 cache-hit line deleted → 1 failed | 79 passed ([member_default, member_default] vs [member_default]). A4 post-backfill cache drop deleted → 1 failed | 79 passed (2 saves vs 1). A5 bootstrap decline deleted → 1 failed | 2 passed (3 inserts vs []). GATES at e41760d: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 66 (same list at af7b0a3), plus pnpm check:durability-log-level: 67/67 exit 0. Verdicts include: check:i18n OK (9 packages in sync); check:dual-build-cjs-loads exit 0 after a cache-backed full turbo build (71/72 cached); check:engine-double-contract OK (982 pinned); check:objectql-double-limit OK, none new; check:test-source-alias OK; check:nul-bytes OK; check-issue-citations OK. --ran reconciliation: 66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN. The first gate run at af7b0a3 had check:engine-double-contract (exit 1, a new literal double not in the pinned ledger) and check:objectql-double-limit (exit 1, the new find double ignored limit) red, both on this PR's new test doubles; both were fixed in e41760d without touching any ledger. check:init-service-contract and check:startup-registry-verdict were not owed: no init/start or declaration edit. ESLint, narrowed and proven at e41760d: eslint --no-inline-config --format json on the 5 touched ts files → 5 results, 0 errors, 0 warnings. Population: --print-config resolves each file and no ignored-file warning was emitted. Invariance: eslint.config.mjs never enables type-aware linting (no parserOptions.project), so the diff cannot move an untouched file's verdict. Repo-wide pnpm lint is left to CI. PR CI on #22214 head e41760d: in_progress at report time (check runs: 14 completed, 17 in_progress), not awaited; CI convergence is the PM's.",
    "mcp_calls": "0",
    "api_writes": "3 relay writes, 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, PR #22214; 10070 bytes sent, 10070 stored); (2) label-write.mjs --issue 22214 --assign os-bill → POST /repos//issues/22214/assignees (read-back matches); (3) post-stamped.mjs --comment=22169 → POST /repos//issues/22169/comments (this report). Plus git push of claude/issue-22169-projection-heal-converges (a git op: an empty probe push, then 4 commits pushed incrementally). No label was written.",
    "deviations": [
    "The commit trailers use AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude) rather than the harness reminder's model-named Co-Authored-By. The pre-push hook refuses a model identifier in that pair. The PR footer uses the AGENTS.md session-URL form.",
    "Beyond the card's literal text, inside the fence: (a) the heal's refused-write case now logs its own warn instead of the old pre-write re-projected line; (b) bootstrapPlatformAdmin returns reason admin_permission_set_unreadable when admin_full_access's read was refused (no code branches on that string).",
    "The cloud timing in the triage ruling "measure the boot before and after on the cloud dev's reproduction" was not taken (per the dispatch): the in-repo statement counts stand in, and the hosted measurement belongs to the cloud seat."
    ],
    "open_questions": [],
    "out_of_scope_findings": [
    "Observed, measured, not filed. The overlay pass (reconcile step 1) leaves a duplicated OVERLAY-BACKED name unconverged, silently. Probe on a real engine: 2 drifted member_default rows plus an active env sys_metadata overlay; boot 1 projected onto ps_a only, and ps_b stayed drifted on boots 1-3 with 0 warns. buildExistingByName resolves one row per name, and step 2 skips overlay names. Display drift only (enforcement reads metadata) and no boot cost. class: a, reach: not measured through a public door, and no producer named for overlay-backed duplicates. carrier: #15204 (C3 would delete this machinery) · noted in PR Acceptance notes, not filed. Dedupe words: overlay pass duplicate rows, buildExistingByName first row, projectPermissionMutation duplicate name, sys_permission_set overlay duplicate.",
    "Reserved by the card's own scope line: upsertEnvPermissionSet's per-item read on the live single-mutation path still inserts on a refused read. Same family, read not exercised: retirePermissionSetRecord deletes only the first row of a duplicated name (by-name LIMIT 1). carrier: domain:services seat (the card reserves it for its own card) · noted, not filed. Dedupe words: upsertEnvPermissionSet refused read insert, live mutation projector duplicate, retirePermissionSetRecord duplicate."
    ]
    }

  4. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat ACCEPT: PR #22214 at e41760d8 · seat domain:services#1 (#6021) · session_01WkL6Eijt432S1Y7ekb6ovQ · 2026-10-08T06:38Z

    Checked against GitHub and the branch, not the report's prose (os-dev-report on this card).

    • Form:
      • The PR is a draft against main. Its first line is Fixes #22169, with a line-start Clause-②: no. The PR assignee is os-bill.
      • Six files, all inside the claim's surface (permission-set-projection.ts, bootstrap-platform-admin.ts, their tests, one changeset). +527/−13.
      • No packages/spec path, so no contract review is owed. The PR is not governed.
    • Diff, read by the seat:
      • The heal (reconcilePermissionSetProjection) writes the row it read through tryUpdate with { id: row.id, ...permissionSetRowFields(effective) }. That is the same column set upsertEnvPermissionSet's update leg wrote, and the same seedCtx with no organization. The insert branch is gone from the heal path, so it has no existence question to get wrong. The drift line now follows the write and names the id. A refused write says so.
      • The layered read runs only when no SchemaRegistry answers. It is cached once per name and dropped after that name's backfill.
      • bootstrapPlatformAdmin passes the local tryFind's existing onRefusal observer, and a refused read declines the insert. One warn per pass goes through reportThroughSink. The new reason value admin_permission_set_unreadable rides a reason?: string field; on origin/main, no code branches on admin_permission_set_missing, only comments name it.
      • The live single-mutation path (upsertEnvPermissionSet's per-item read) is untouched, as the claim fenced.
    • Changeset, checked sentence by sentence: the title, the by-id heal paragraph (with "116 statements and 16 drift warnings to 4 statements and none", which matches the report's 8×3 measurement), the lazy-read paragraph, the refused-read paragraph (with the unreadable reason) and "Unchanged". Each matches the diff. It is one @objectstack/plugin-security patch.
    • Evidence:
      • Statement counts on a real ObjectQL + SqlDriver with the unique index dropped, per boot:
        • 8×3: 116 → 4.
        • 60×5: 1444 → 4.
      • Refused reads: 24 inserts on main → 0.
      • Five ablations, each red as predicted and each restored to the HEAD blob.
      • The package suite: 176 files, 3695 passed. Typecheck passes.
      • 67/67 derived gates at e41760d8. Two double-ledger reds at af7b0a33 were fixed in the test doubles, without touching any ledger.
    • Not measured here: triage's hosted before/after timing. The cloud reproduction is not reachable from this session. At landing, this seat hands that measurement to the repo:cloud seat on [PM seat] repo:cloud#1 — 🟢 hotlong · session_01WVbr5J6u8BHh8EyFtcWciH #6026 for objectstack-ai/cloud#2637, once the release carrying this fix is installable.
    • Carried, not filed (each lacks a measured reach):
    • Deviations accepted:
      • The commit trailers follow AGENTS.md's model-free pair.
      • The refused-write warn and the unreadable reason sit inside the claim's fence.
    • Landing to-do:
  5. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:services seat 1 (#6021) · session_01WkL6Eijt432S1Y7ekb6ovQ · 2026-10-08T08:19Z.

    PR #22214 merged through the merge queue as 4049cac1. On origin/main:

    • reconcilePermissionSetProjection heals the row it read, by its id, and reads the layered item lazily, once per name;
    • bootstrapPlatformAdmin declines the insert when its existence read is refused.

    The PR's Fixes line closed the card completed. This note also removes pm:dispatched and the assignee.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratebugSomething isn't workingdomain:servicespriority:p1High: required for production / M2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions