feat(plugin-security,cli): #15211 S3 — retire sys_user_permission_set.permission_set_id and its indexes, delete the boot grant-name backfill, add the named-grant unique index (C7b, split for seat 1) #23430
Copy link
Copy link
Closed
Labels
area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:enginepm:dispatchedpriority:p1High: required for production / M2High: required for production / M2target:v18
Description
#15211's stage S3, split out for domain:engine seat 1, on the maintainer's word (「哪些可以分拆给 seat 1」, to spread the work across accounts). Seat 2 keeps #15211 and its stages S1 and S4. Part of #15211 (C7b) and of the ADR-0131 epic chain (#15194).
S3: retire sys_user_permission_set.permission_set_id with its unique (user_id, permission_set_id, organization_id) and its (permission_set_id) index, plus one ADR-0087 column-retired entry (precedent 17.ups-delegated-from-column-retired). The measured surface is in stage plan 6162468695. Seat 2's answers are 6162538253.
- The ceremony:
organization-ownership.ts's column-drops port keeps onlyorganization_idops today (about :488–491). It learns this one named column, dropped after the rewrite step, which reads it raw, gated on the physical column. organization-ownership column-drops never completes where no platform-global objects are declared: organization_id is still injected on the sys_metadata family and sys_flow_dispatch, so the drop is never proposed #23399 (cli seat 2) edits the same port; whichever lands second mergesmain(pointer 6163407103). - Q4 → A (the surface widens to four readers):
- delete
plugin-security'sgrant-permission-set-name-backfill.ts, because the ceremony's verified rewrite replaces it; - remove the id leg from
auto-org-admin-grant.ts,catalog-reference-report.tsandbootstrap-platform-admin.ts(Leg C); - follow the deletion clause.
- delete
- Q5 → A:
- add a unique index on (
user_id,permission_set,organization_id); - the plan reports existing duplicate groups as a blocker of column-drops, and the ceremony never deletes them;
- a duplicate platform-wide grant (NULL organization) is refused at the write door;
- pin a revoke that leaves the grant in force.
- add a unique index on (
Order: S3 overlaps #22975's 8-post on catalog-reference-report.ts, so it opens after 8-post lands.
Process:
- One PR off
main, withPart of #15211. - Merge
mainonly on a conflict, amain-caused red, or a queue removal (ruling [Decision] After a PR is open, merge main only on a conflict, a main-caused CI red or a queue removal, and re-check only the overlap; or keep merging main before every push #23390). - Contract review on opus.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:enginepm:dispatchedpriority:p1High: required for production / M2High: required for production / M2target:v18
Activity
objectstack-fleet commentedon Oct 14, 2026
Claim: PM loop round 1 · 2026-10-14T08:24Z ·
domain:engineseat 1, term 3, on the maintainer's order 「你接管 engine seat 1」「从队列认领本 epic 相关的任务 从队列里认领 #23429 和 #23430」 (seat post #6367, opening marker 6164065757)Session:
session_01Rerax7QTjKMPCUZxQUtPFRAccount:
marchtian(the card's assignee)Branch:
claude/issue-23430-grant-column, started fromclaude/issue-15211-c7b-grant-column@ddb97b15b7Worktree:
objectstack-issue-23430Domain:
domain:engineSeat:
domain:engine#1File surface:
packages/plugins/plugin-security/src/**(grant-permission-set-name-backfill.tsdeleted;auto-org-admin-grant.ts,catalog-reference-report.ts,bootstrap-platform-admin.ts; thesys_user_permission_setdeclaration and write door),packages/cli/src/commands/migrate/organization-ownership.ts(the column-drops port), the ADR-0087 column-retired entry, and the tests that pin them (stop on breach; explain in the report)Container & model:
M,mode:cloud,model: opus(dispatch-gates --tier: "no path-derived mandate … default opus"); contract review atCONTRACT_REVIEW_TIER, which the card asks for ("Contract review on opus")Clause-②: yes (narrowing): a published column and two indexes retire, a named-grant unique index is added, and a duplicate platform-wide grant is refused at the write door
Responsibility: n/a — not a defect card
Thread-read: none
Serial constraints cleared: #22975 8-post (seat 2) overlaps on
catalog-reference-report.ts, so the PR opens after 8-post lands; the build goes ahead now. #23399 (domain:cliseat 2) edits the same column-drops port, so whichever lands second mergesmain(6163407103). #15211 S1 (seat 2) edits the ceremony too, so whichever lands second mergesmain.Resume-from:
claude/issue-15211-c7b-grant-column@ddb97b15b7. This is seat 2's build-ahead, and seat 2 withdrew its assignment in 6163876391. The new branch name keeps seat 2's sessions off this ref.Generated by Claude Code
objectstack-fleet commentedon Oct 14, 2026
Session note for claim 6164141135 ·
domain:engineseat 1 · 2026-10-14T08:27Z: the developer is the cloud sessionsession_014fyKy7M7fEuwkfqc5JHdJJ(opus), started from this seat's session. Branch:claude/issue-23430-grant-column, resumed from seat 2's build-ahead atddb97b15b7. The build goes ahead now; the PR opens after #22975's 8-post is onmain.Generated by Claude Code
objectstack-fleet commentedon Oct 14, 2026
os-dev-report
{ "issue": 23430, "status": "built-pr-held", "session": "session_014fyKy7M7fEuwkfqc5JHdJJ", "claim": "6164141135", "branch": "claude/issue-23430-grant-column", "head": "c38af85e0", "resume_from": "claude/issue-15211-c7b-grant-column@ddb97b15b7 (one WIP commit, 'tests not yet run'), then origin/main 04a31487 merged once before work (512cf541d, clean)", "pr": null, "pr_held_reason": "#22975's 8-post (#23428) is still an open draft, not on main; it shares catalog-reference-report.ts with this card. The draft PR opens when the PM reports 8-post landed: merge main once, re-validate, open.", "summary": "S3 is built and green. The start commit covered the declaration (permission_set_id and its (permission_set_id) index gone, the unique index re-keyed to (user_id, permission_set, organization_id)), the ceremony's column-drops port learning the named column (ColumnDrop {table, column}, dropped after rewrite, gated on the physical column), the duplicate-grants plan category and its column-drops blocker (never deletes), the deleted boot backfill, the three readers' id legs, the write-door refusal, and the 18.ups-permission-set-id-column-retired entry. It had not run tests: plugin-security had 111 failing cases. Added: (1) the write door's platform-wide duplicate refusal now covers every writer, system writers included, as the index it stands in for does, and an update that changes the key (user, set, or organization cleared to NULL) is judged too; (2) fixture triage across 14 plugin-security suites, one plugin-auth suite and one dogfood pin; (3) new pins on the real engine: an org-scoped duplicate refused 409 by the index, a platform-wide duplicate refused 409 at the write door for an admin and a system writer, an update making a platform-wide duplicate refused, a control, and for each, the revoke of the one grant leaves no grant of that set in force; (4) the changeset (BREAKING, Clause-② yes (narrowing), adr-0087 registered ups-permission-set-id-column-retired); (5) the i18n bundles regenerated, the grant-name ADR anchor's stale invariant restated, @objectstack/objectql moved to plugin-security's dependencies (the 409 is objectql's DuplicateRecordError), and the tenant-audit census re-measured (225 -> 223 write call sites, the deleted backfill's writes).", "scope_checklist": { "ceremony_column_drops_port": "done (start commit): organization-ownership.ts columnDrops.drop keys table.column; runColumnDrops appends sys_user_permission_set.permission_set_id when the physical column exists", "q4_backfill_deleted": "done (start commit): grant-permission-set-name-backfill.ts, its test and its ADR anchor deleted; security-plugin no longer registers it", "q4_id_legs_removed": "done: auto-org-admin-grant.ts (resolvePermissionSetIdsForName and the id dedupe gone), catalog-reference-report.ts (UnnamedGrant.permissionSetId gone), bootstrap-platform-admin.ts Leg C (hasLegacySetId gone; any unnamed unscoped human grant restricts)", "q5_unique_index": "done: { fields: ['user_id', 'permission_set', 'organization_id'], unique: 'global' }", "q5_plan_blocker_no_delete": "done (start commit) and pinned in organization-ownership-apply.test.ts: 2 grants in 1 group block the drop, none deleted, the re-run after the operator's delete drops the column; NULL organizations group together", "q5_platform_wide_refused": "done, widened to every writer (open_questions 1)", "q5_revoke_pin": "done: grant-permission-set-name.test.ts 'one grant per user, set and organization' (4 cases)", "adr_0087_entry": "done: packages/spec/src/migrations/entries/semantic/18.ups-permission-set-id-column-retired.ts", "changeset": "done: .changeset/23430-grant-permission-set-id-retired.md" }, "deletion_clause": [ "grant-permission-set-name-backfill.test.ts: deleted with the code it pins (start commit); the ceremony's acceptance fixture (organization-ownership-apply.test.ts, 'Every grant names its set, and the id column the rewrite read is gone') pins the id-to-name rewrite at the layer that now owns it", "resolve-authz-grant-set-by-name.test.ts 'the upgrade boot': deleted with the backfill it pins; same duplicate as above", "auto-org-admin-grant.test.ts id-reach cases: flipped, not deleted (an unnamed grant is reached by no demotion or sweep; it does not stand for the pair); the cross-copy case merged into the flipped one, which pins the same assertion", "grant-readers-unnamed-grant.test.ts (plugin-security) reconcile cases: flipped, and each now also pins that once the rewrite names the grant the boot sweep revokes it", "grant-readers-unnamed-grant.test.ts (plugin-auth) 'writing the legacy id moves no standing': flipped to the 400 INVALID_FIELD refusal" ], "files_outside_claimed_surface": [ "packages/plugins/plugin-auth/src/{grant-readers-unnamed-grant.test.ts, ensure-default-organization.ts, last-admin-guard.ts}: one test pinning the retired column, two comments naming the deleted backfill (comment-only)", "packages/qa/dogfood/test/grant-name-pickers.dogfood.test.ts: pinned permission_set_id served hidden; now pins it not served", "packages/plugins/plugin-security/package.json + pnpm-lock.yaml: objectql to dependencies (check:undeclared-dep-imports); the lockfile regen also re-resolves one vitest peer variant, deterministic from this manifest (a lockfile-only install on origin/main reproduces no churn)", "content/docs/permissions/tenant-audit-census.mdx, docs/audits/2026-08-tenant-audit-write-call-sites.counts.md: census --write plus six hand-written prose figures (check:tenant-audit-census)", "scripts/adr-anchors/packages__plugins__plugin-security__src__grant-permission-set-name.ts.json: invariant restated (it described the id-derivation the stage-1 hooks no longer do)" ], "tests": { "measured_at": "c38af85e0 unless noted", "commands": [ "bash scripts/pm/os-verify-lock.sh -c \"pnpm --filter @objectstack/plugin-security typecheck && pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2\" :: exit 0; 184 files, 3763 passed, 45 skipped", "bash scripts/pm/os-verify-lock.sh -c \"pnpm --filter @objectstack/cli typecheck && pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2\" :: exit 0; 287 files, 4307 passed (at 00fb35917; cli untouched since)", "pnpm --filter @objectstack/cli exec vitest run --project integration src/utils/organization-ownership-plan.integration.test.ts src/utils/organization-ownership-host-supplement.integration.test.ts :: exit 0; 18 passed (at 00fb35917)", "pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 :: 139/140 files; the one red (grant-readers-unnamed-grant 'writing the legacy id') was triaged, then that file 6/6 (at 22618ee8b + edit)", "pnpm --filter @objectstack/spec build && pnpm --filter @objectstack/spec check:generated && pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/migrations :: exit 0; 4 files, 208 passed (at 6ece6b224)", "turbo build --filter='@objectstack/dogfood^...' && pnpm --filter @objectstack/dogfood exec vitest run test/grant-name-pickers.dogfood.test.ts :: exit 0; 3 passed (at c38af85e0)", "node scripts/check-i18n-bundles.mjs --write :: exit 0 (four plugin-security bundles regenerated)" ], "consumer_sweep": "Every non-plugin-security test naming permission_set_id was read: the client, rest, runtime, trigger, service and plugin-auth suites declare their OWN local sys_user_permission_set schema, so the retired declaration does not reach them; plugin-auth's full suite confirms. Not run locally: rest, runtime, client (declared to CI).", "ablations": [ "write door: node scripts/ablation-replace.mjs on grant-permission-set-name.ts anchor 'if (grantOrganizationOf(data) === null) await assertNoPlatformWideDuplicate(...)' -> red: 1 failed (the platform-wide case), 14 passed; restored blob == HEAD 4dd3db350c7c, git diff HEAD empty", "index: anchor \"{ fields: ['user_id', 'permission_set', 'organization_id'], unique: 'global' },\" -> non-unique -> red: 1 failed (the org-scoped case); restored blob == HEAD b30f2f2b2f6e" ] }, "gates": { "derivation": "node scripts/pm/dispatch-gates.mjs --commands at c38af85e0: 135 families", "reconciliation": "node scripts/pm/dispatch-gates.mjs --ran <record with exit codes> :: exit 0 — 135 accounted, 133 run exit 0, 2 NOT MEASURED", "not_measured": [ "NOT MEASURED: pnpm check:dual-build-cjs-loads, reason: exit 3, needs a whole-repo build (studio, client-react, connectors and others have no dist here); CI runs it", "NOT MEASURED: pnpm --filter @objectstack/spec run check:skill-examples, reason: exit 3 prerequisite; CI runs it" ], "fixed_on_the_way": [ "check:undeclared-dep-imports (objectql import) -> dependency declared", "check:tenant-audit-census + --self-test (225 -> 223) -> census rewritten, prose figures updated", "check-plugin-teardown-shape --self-test exit 3 (shallow clone) -> fetched its pinned positive control, exit 0" ], "local_only": "check:dev-prereqs and check:skill-frame-freshness not run (no packages/spec dist reliance beyond the build above; reported, not claimed)" }, "changed_lines": "42 files, +673 / -1996 vs merge base 04a31487 (under 3,000)", "open_questions": [ "1. Decided by the card's own reasoning: the platform-wide duplicate refusal applies to EVERY writer, system writers included (seat 2's start commit stood it down for isSystem like the catalog check). Q5's reason was 'an index is the only guard every writer meets'; the write door stands in for the index on NULL organizations, so it meets every writer too. The catalog check alone keeps its system stand-down (seed ordering). Platform writers (bootstrap, reconcile) check existence before inserting, and every suite stays green.", "2. Q4 A's consequence, recorded: an unnamed legacy grant is reached by no reconcile until the ceremony names it. A demoted owner's unnamed org-admin grant confers nothing meanwhile; once the ceremony names it, the next boot's kernel:ready sweep revokes it by name before any request. Pinned in plugin-security grant-readers-unnamed-grant.test.ts.", "3. The write door judges rows one at a time against STORED rows: two platform-wide grants of one set to one user inside ONE batch insert are not seen by it (the index sees the org-scoped equivalent). Not built: no writer in the repo batches a user's platform-wide grants. Named for the reviewer.", "4. Seat answers 6162538253 say S2 and S3 PRs stay drafts until #23413 Q3 is ruled; the PR will open as a draft regardless.", "5. Landing order: #23399 (cli seat 2) and #15211 S1 also edit the ceremony; whichever lands second merges main (not done here; nothing of theirs is on main yet)." ], "out_of_scope_findings": [ "class: none (comment drift) · reach: none · packages/core/src/security/resolve-authz-context.ts:746 still says an unnamed grant is one 'the one-time backfill has not named yet'. carrier: 承接者:无 — goes to the PR's Acceptance notes." ], "dedupe_terms": ["permission_set_id retired", "grant-permission-set-name-backfill", "duplicate platform-wide grant", "sys_user_permission_set unique index"], "deviations": [ "The first gate battery was still running when the second started on the new head; the results interleaved. Reconciled by last result per command against the head's derivation (135/135 accounted)." ], "api_writes": "git push x8 to claude/issue-23430-grant-column (branch probe, merge, WIP commits); 1 REST write: this comment, through scripts/pm/post-stamped.mjs", "mcp_calls": "5 reads (issue 23430, its comments, the 8-post PR search, tool loads); 0 writes", "cleanup": "worktree ../objectstack-issue-23430 kept: the PR is not open yet" }Generated by Claude Code
objectstack-fleet commentedon Oct 14, 2026
Contract review
Served-tier:
CONTRACT_REVIEW_TIERHead-sha:
c38af85e0e5e325c2f01091a30597fa112d5f72eLocal-runs: none
This is a pre-PR review of the pushed branch
claude/issue-23430-grant-column, because the card holds the PR until #22975's 8-post lands. No PR check-runs exist yet, so the dev's reported runs are read as claims, not gate verdicts. Served on opus by the maintainer's order 「后续契约复审用 opus就够了」.Diff read: merge base
04a314873cto head. It covers 42 files, +673 / −1996, and touches no governed surface.① Derived judgments
1. The declaration (
plugin-security/src/objects/sys-user-permission-set.object.ts)permission_set_idis no longer declared. Any write that names it is refused400 INVALID_FIELD, and that includes system writers. It is pinned ingrant-permission-set-name.test.tsand in plugin-authgrant-readers-unnamed-grant.test.ts. Right.GET /meta/object/sys_user_permission_setstops serving the field, and the four generated bundles drop its keys. It is pinned ingrant-name-pickers.dogfood.test.ts. Right.user_id,permission_set_id,organization_id) and the (permission_set_id) index.{ fields: ['user_id', 'permission_set', 'organization_id'], unique: 'global' }.2. The ceremony (
cli/src/commands/migrate/organization-ownership.tscolumnDrops.drop;cli/src/utils/organization-ownership-apply.tsColumnDrop,runColumnDrops;organization-ownership-inventory.tscategoryduplicate-grants)permission_set_idis due only whilecolumnsOf(run, GRANTS)holds it, so it is gated on the physical column.runRewrite(step 3) has read the column.permission_setis absent.3. The backfill deletion and the deletion clause
grant-permission-set-name-backfill.ts, its anchor and its boot wiring insecurity-plugin.tsare gone, and the census is re-measured. This matches Q4 → A and ADR-0131 D10 ("never an automatic boot step").grant-permission-set-name-backfill.test.ts(the whole file);resolve-authz-grant-set-by-name.test.ts"the upgrade boot";auto-org-admin-grant.test.ts"the backfill sweep reaches an orphan…";auto-org-admin-grant.test.ts"…stands for the pair: no duplicate is written".organization-ownership-apply.test.ts(grant_x,grant_late, the checkpoint cases). That is compliant, on condition of fix 5.auto-org-admin-grant.test.tsand in plugin-securitygrant-readers-unnamed-grant.test.ts;4. The readers
auto-org-admin-grant.ts:resolvePermissionSetIdsForNameand the id dedupe-delete;catalog-reference-report.ts:UnnamedGrant.permissionSetIdand the id projection;bootstrap-platform-admin.tsLeg C:hasLegacySetId. Now any unnamed unscoped human grant restricts, which is stricter and fail-closed.sys_user_permission_set.permission_set_idsurvives in non-test source. The only reads left are the ceremony's raw SQL inrunRewriteandverifyReferences, by design.permission_set_idhits belong tosys_position_permission_setand are unaffected:security-plugin.tsassertAudienceAnchorBindingGate,delegated-admin-gate.tsassertBindingWrite, and coreposition-binding-conversion.ts.5. The write door (
grant-permission-set-name.tsassertNoPlatformWideDuplicate,onInsert,onUpdate)DuplicateRecordError(409,UNIQUE_VIOLATIONon the wire).6. ADR-0087
packages/spec/src/migrations/entries/semantic/18.ups-permission-set-id-column-retired.tsexists. Its id isups-permission-set-id-column-retired, and its shape matches the precedent17.ups-delegated-from-column-retired.adr-0087: registered ups-permission-set-id-column-retired.acceptanceCriteria("platform-wide grants included") claims more than the door enforces until fix 2 lands.7. Files outside the claimed surface
ensure-default-organization.ts,last-admin-guard.ts). Right.plugin-security/package.json:@objectstack/objectqlmoves todependenciesforDuplicateRecordError. objectql's dependencies do not name plugin-security, so there is no cycle. plugin-sharing and plugin-audit are precedent. Right.pnpm-lock.yaml: the plugin-security importer lines are right.packages/apps/accountvitest resolution moves to an esbuild 0.28.1 variant and adds two snapshot entries.mainmerge. Non-blocking.organization-ownership-apply.ts,organization-ownership-inventory.tsandorganization-ownership-apply.test.tsare outside the claim's literal surface, which names onlyorganization-ownership.ts. They are missing from the report's outside-surface list. Q5 → A requires them (the plan category and the step's blocker). They are right in scope; list them in the PR.② Semver level
@objectstack/plugin-security,@objectstack/cliand@objectstack/specminor, marked BREAKING, withClause-②: yes (narrowing).yestakes at least minor and narrowing is BREAKING. This uses the launch-window minor.INVALID_FIELD, the 409, the served field, the boot backfill with itssys_migrationentry, and the new objectql dependency.--plancategory and a column-drops blocker.ColumnDropPortchanges shape, but it is internal: the package entry does not export it.--apply, including the platform administrator's.③ Boundary flags
Open question 1: the refusal is widened to system writers. Accepted.
auth-manager.tsself-registration, the bootstrap promotion, the reconcile, and the verify persona.Open question 2: unnamed grants are reached by no reconcile. Accepted as Q4 → A's consequence, with three notes:
--apply. The changeset says this grant by grant; say it plainly (see ②).--applykeeps serving a newly named, superseded grant until it restarts, because the revoking sweep runs at boot. Tell the operator to restart after--apply.Open question 3: batches. Not accepted as a named gap. It is reachable from the data door (fix 2).
Escalated to the seat: the premise of Q5 → A.
'global'only.'organization'index's tenant part asCOALESCE(organization_id, '__global__').normalizeDeclaredIndex(driver-sqlschema-drift.ts) applies that to a listed tenant column too.unique: 'organization'would:permission_set, so they would not collide.origin/main:normalizeDeclaredIndextakes theunique === 'organization'arm with a listed tenant column ("its own key part becomes the NULL-safe form instead", the S6 spelling opted in), and ADR-0120 D3 states theCOALESCE(organization_id, '__global__')key part. The claim holds. Seat 1 puts the question to seat 2, who gave 6162538253, on feat(objectql,cli): inventory + migration — four fates per object, mirrors deleted only after the id→name rewrite is verified, per-table boot report (ADR-0131 D10) #15211.Open question 4: the draft hold.
Open question 5: S3 lands second.
mainas4369677be(fix(cli): the ceremony's column drops answer not-declared for a table the booted composition declares no object for #23446), after this branch's merge base.ColumnDropOutcome/runColumnDropsedits. Thenot-declaredoutcome must carrycolumn.Non-blocking notes:
position-catalog-refusal.tsmodule note.resolve-authz-context.ts:746(the dev's own finding).install-local-principal.fixtures.tsand service-datasourceentitled-caller.fixture.ts.skills/objectstack-data/rules/security.mdstill says "a legacy column: do not write it". The advice is still right in effect. Fixing it is Tier H, so leave it for a follow-up.DuplicateRecordErrorhas nocause, so itsdeveloperMessage("The driver refused this write…") is false here.assertNoPlatformWideDuplicatereturns silently when the engine has nofind. That fails open, though it cannot happen on ObjectQL.organization_iddrops on MySQL. That is for the seat to triage.Blocking fixes:
verifyReferences) creates the new unique index on that same boot:sql-driver.tsre-reads columns after the ALTER, then runssyncTableIndexes.kernel:readysweep (backfillOrgAdminGrants→reconcileOrgAdminGrant) then writes a named org-admin grant beside each legacy one.runRewrite's rawUPDATE … SET permission_setthen violates the index.executethrows "The write to sys_user_permission_set failed", and the run stops in step 3 with no blocker and no remedy.CREATE TABLE sys_user_permission_sethas no unique index. On a real database,grant_2's rename collides withgrant_dupfirst.--plan.--apply.catalog-reference-report.tslogReport("Fix: grant the intended permission set again by name") to lead with running the ceremony. Re-granting by name produces the same collision.assertNoPlatformWideDuplicatereads stored rows only.beforeInsertbefore a batch write.bulk, socreateMany(protocol.tscreateManyData) or/batchwith two identical platform-wide rows stores both.updateManyover two rows does the same.ctx.dispatch.scope, on both hooks.drop_columnentries toapplyMigrationEntries. The two retired indexes still exist, because schema sync is additive.applyDriftOpInPlaceruns a bareALTER TABLE … DROP COLUMN. MySQL keeps a multi-column index after one of its columns is dropped, minus that column. The driver's owndrop_indexarm also notes that MySQL refuses to drop a column a key still uses.user_id,organization_id) in place, which refuses any second grant per user and organization.drop_indexentries, in a separate earlier call.applyMigrationEntriesruns column ops before index ops, so they cannot go in the same call.permission_setis absent.duplicate-grantscategory callsrequireColumn('permission_set'). Every 17.x database lacks that column before v18 boots.--planand--applynow refuse withmissing-columnand give no remedy.verifyReferencesblocker ("Boot this release once") becomes unreachable.organization-ownership-apply.test.tsimportsbetter-sqlite3, sopackages/cli/vitest-tiers.tsplaces it in the integration tier.--project unit, plus two named integration files) never executed it.Implemented-by:
session_014fyKy7M7fEuwkfqc5JHdJJReviewed-by:
session_01Rerax7QTjKMPCUZxQUtPFRVERDICT: FAIL
Generated by Claude Code
objectstack-fleet commentedon Oct 14, 2026
Relay from
domain:engineseat 1 ·session_01Rerax7QTjKMPCUZxQUtPFR· 2026-10-14T11:24Z. ⛔ Not a claim. This changes the patch round on contract record 6166827307.Seat 2 answered seat 1's pointer 6166864546 in 6167037655, amending its own 6162538253 Q5 → A. Both answers were relayed to the dev,
session_014fyKy7M7fEuwkfqc5JHdJJ.Q-S3a: the index is respelled
unique: 'organization', and the door check retires.'global'.Q-S3b → (ii): the reconcile writes nothing beside an unnamed grant in that organization.
--plancount stay as the safety net. The end-to-end fixture must now show the sweep writing nothing andrunRewritecompleting with no duplicate group.Unchanged: fixes 3, 4 and 5, and the merge of
main.The re-review of the new head runs on opus. The draft hold on #22975's 8-post stands.
Generated by Claude Code
objectstack-fleet commentedon Oct 14, 2026
os-dev-report
{ "issue": 23430, "status": "built-pr-held", "round": "patch round on contract review 6166827307 (FAIL), amended by the seat answers 6167037655 (Q-S3a, Q-S3b)", "session": "session_014fyKy7M7fEuwkfqc5JHdJJ", "branch": "claude/issue-23430-grant-column", "head": "5a9d40ff06", "reviewed_head": "c38af85e0e", "pr": null, "pr_held_reason": "#22975's 8-post (PR #23428) is still a draft and not on main. No PR opened, as ordered.", "commits": [ "fe84a70e8 merge of origin/main 4e0838d99 (a merge commit, no rebase). It brings #23399 (4369677be) and #23397 (0d784da01). Conflicts were resolved minimally: the port is keyed by table and column and keeps #23399's not-declared reading; the not-declared outcome and blocker carry `column`; the backfill test that main had re-goldened stays deleted with its code.", "ea68dc414 fixes 1, 3 and 4 (cli)", "bc150970b5 fix 2 (door in-call scope); b1afc461a5 text restatements of that round", "b3420c9e6e revert of bc150970b5 (Q-S3a)", "32971a8646 the index respelled unique 'organization', the door check retired", "3dd068f874 Q-S3b: the reconcile writes nothing beside an unnamed grant", "4b980ec24d restatements: the index is the guard for every writer", "5a9d40ff06 the fixture shim's delete routes through assertEngineDeleteDispatch, and the ledger records it" ], "fixes": { "0_merge": "Done first. #23399's `not-declared` outcome carries `column` (ColumnDropOutcome extends ColumnDrop). Its apply test now counts the grant's id column as one more due drop: {dropped: 2, due: 3, notDeclared: 1}.", "1_rewrite_collision": "organization-ownership-grants.ts holds one pure decision used by both --plan and --apply. A legacy grant whose derived name its user already holds in its organization (NULL keyed with NULL), whether by a stored named grant or by one named earlier in the same run, is not named. It is reported as a rewrite blocker naming both ids, and nothing is deleted. --plan carries `grants.rewriteCollisions`. The catalog-reference-report remedy now leads with running the ceremony, then restarting. Per Q-S3b this is the safety net; its end-to-end pin uses a fixture carrying the declared NULL-safe index.", "2_batch_door": "Built in its own commit (bc150970b5), then reverted (b3420c9e6e) per Q-S3a. The whole door check is gone: assertNoPlatformWideDuplicate, its onInsert/onUpdate calls and the DuplicateRecordError import. plugin-security's package.json and pnpm-lock.yaml are byte-identical to origin/main again, so the packages/apps/account vitest churn is gone too (`pnpm install --frozen-lockfile` exit 0).", "3_indexes_before_column": "The port moved into organization-ownership-column-drops.ts (`dropColumnsThroughDrift`). A ColumnDrop carries `retiredIndexes`, and the command maps each to the driver's name via `buildIndexName`. Their drop_index entries apply in an earlier, separate applyMigrationEntries call. A skipped index fails its column, naming the index, and that column is not dropped.", "4_no_name_column": "The grant-specific plan block replaces the inventory category, so packages/spec's host-authorable category schema stays untouched. Without `permission_set` the block reports nameColumn 'absent' with zero counts, and its note names 'Boot this release once…'. --plan no longer refuses, and --apply's rewrite blocks with the verifyReferences remedy. Pinned.", "5_cli_integration": "The cli integration project ran on head 4b980ec24: 128 files, 1108 passed, 2 skipped. organization-ownership-apply.test.ts was re-run on the final head 5a9d40ff06: 23 passed." }, "q_s3a": { "index": "{ fields: ['user_id', 'permission_set', 'organization_id'], unique: 'organization' }", "premise": "HOLDS, measured on a real SqlDriver (SQLite) in grant-permission-set-name.test.ts. Two NULL-organization grants of the same user and set are refused by the index itself on two paths that bypass any door: a raw knex INSERT beneath the engine ('UNIQUE constraint failed'), and a two-row batch insert, which stores nothing. A two-user control lands, through a batch and raw SQL alike. normalizeDeclaredIndex takes the `unique === 'organization' && tenantField` arm; the raw-SQL refusal is the proof that the tenant field resolved to organization_id.", "wire_shape": "The engine throws DUPLICATE_RECORD 409 (resolveThrownHttpError). The data doors' mapper, `mapDataError` from @objectstack/types, answers 409 with body.code UNIQUE_VIOLATION. Pinned on all four refusal cases: org-scoped, platform-wide (admin and system writers), the batch, and the multi-field update.", "ablation": "scripts/ablation-replace.mjs respelled the index `unique: 'global'`. That turned red exactly the three platform-wide pins: the platform-wide insert, the batch/raw-SQL pin, and the rename update. The org-scoped pin and the control stayed green. The restore matched the HEAD blob d0b541fbc4f2, with git diff HEAD empty.", "kept_pins": "Org-scoped and platform-wide duplicates are refused, and in each case the revoke of the one grant leaves no grant of that set in force. The refusal now comes from the index." }, "q_s3b": { "change": "In reconcileOrgAdminGrant's grant branch, a qualifying pair holding ANY grant with permission_set NULL in that organization is written nothing; it returns { action: 'skipped', reason: 'unnamed_grant_awaits_ceremony' }. backfillOrgAdminGrants goes through the same function. An unnamed grant in another organization does not hold the pair back (control).", "pin_end_to_end": "In organization-ownership-apply.test.ts, the real backfillOrgAdminGrants from @objectstack/plugin-security dist runs through a thin engine shim over the fixture's SQLite. The table carries the declared NULL-safe index, a 17.x owner's membership, and a legacy grant of the org-admin set by id. The sweep writes no row ({granted: 0, skipped: 1}), --plan counts no collision, and --apply's rewrite names the grant and completes (collisions 0). The next sweep writes nothing either.", "ablation": "Deleting the skip line in auto-org-admin-grant.ts turned red the two Q-S3b pins: the stub pin and the plugin-security real-engine pin. The restore matched HEAD 01db4f91c52c." }, "s5_ordering_measurement": "Read from S5's head claude/issue-23429-boot-refusal@540c5b3c28, not executed. On `os serve`, the guard plugin is kernel.use'd at serve.ts:3880 (step 5c-ter), and the auto-registered SecurityPlugin follows at :4238 (5d). The guard has optionalDependencies [engine.objectql] and SecurityPlugin has dependencies [engine.objectql], with no edge between them, so resolvePluginOrder keeps insertion order and the guard's start(), hence its kernel:ready handler, registers first. kernel:ready is dispatched by dispatchHookPropagating (kernel-base.ts), so the guard's throw stops the remaining handlers, the org-admin sweep (security-plugin.ts runOrgAdminBackfill) included. The index itself is still created at that boot, because schema sync runs before kernel:ready. Caveats: a host that composes its own kernel without the guard, or lists SecurityPlugin under a different position, is not covered by this reading. Q-S3b makes the answer moot for the collision.", "tests": { "measured_at": "4b980ec24 (the final commit 5a9d40ff06 touches only the cli apply test and scripts/engine-double-contract.pinned.json; that file was re-run there)", "commands": [ "build: turbo build --filter='@objectstack/cli...' --filter='@objectstack/plugin-security...' --filter=@objectstack/plugin-auth --filter='@objectstack/dogfood^...' :: exit 0", "plugin-security typecheck + full suite :: exit 0; 185 files, 3763 passed, 45 skipped", "plugin-auth full suite :: exit 0; 140 files, 2806 passed, 10 skipped", "cli typecheck :: exit 0", "cli --project unit :: exit 0; 287 files, 4307 passed", "cli --project integration :: exit 0; 128 files, 1108 passed, 2 skipped", "spec build && check:generated && vitest src/migrations :: exit 0; 209 passed", "core typecheck (comment-only edit) :: exit 0", "dogfood test/grant-name-pickers.dogfood.test.ts :: exit 0; 3 passed", "final head 5a9d40ff06: cli organization-ownership-apply.test.ts 23 passed; pnpm check:engine-double-contract exit 0" ], "ablations": [ "index 'organization' -> 'global': 3 red, restored (above)", "Q-S3b skip removed: 2 red, restored", "fix 1, planGrantRewrite collision check disabled: the end-to-end safety-net pin red, restored 2b24a02144b8", "fix 3, index-first apply disabled: 2 red (ordering and the failed-index pin), restored a674684c3f50. On SQLite the column itself drops either way; the MySQL refusal is the review's reading of the driver arm, not measured here (no MySQL in the box)." ] }, "gates": { "derivation": "node scripts/pm/dispatch-gates.mjs --commands at 5a9d40ff06: 127 families, the same list as at 4b980ec24", "reconciliation": "--ran with exit codes :: exit 0 — 127 derived, 127 run, 0 NOT-MEASURED, 0 UNRUN", "fixed_on_the_way": "check:engine-double-contract: the fixture's engine shim now opens delete() with assertEngineDeleteDispatch, and `--write` recorded the new pinned double" }, "changed_lines": "3266 (+1228 / -2038, 47 files) vs merge base — OVER the 3,000 threshold. The mandated deletions are 1,359 lines (the backfill and its test). The fix round added the shared planner, the column-drop helper and the real-driver and end-to-end pins. I did not thin pins to fit. Per the dispatch, the PR body will carry `## 维护者速读(草稿)`, and the PR lands as a human merge (AGENTS §7 class c), unless the seat wants a split.", "open_questions": [ "1. Over 3,000 changed lines (above). Options: land it as a human merge with the Chinese maintainer section, or split the CLI ceremony half from the plugin-security half. They are coupled: the index respelling needs the ceremony's duplicate blockers. Recommend the single PR.", "2. The safety-net fixture writes a named grant beside a legacy one as 'any other writer'. In-repo, after Q-S3b, the remaining writers are Setup or data-door grants and seeds; the rewrite reports such a pair, never names it.", "3. The reconcile's unnamed-grant read uses tryFind, so a read that fails answers [] and the pair is granted. That is the module's existing best-effort contract; the ceremony's skip-and-report then covers the resulting pair." ], "non_blocking_items": [ "packages/apps/account lockfile churn: gone (lockfile identical to origin/main)", "changeset: the '17.x: no grant confers anything between the first v18 boot and --apply' sentence added; operators told to restart after --apply", "DuplicateRecordError without cause: moot, the door no longer throws one", "stale comments: position-catalog-refusal.ts module note and core resolve-authz-context.ts grantSetNameOf doc fixed", "skills/objectstack-data/rules/security.md: left alone (Tier H follow-up)", "not touched: the cloud-connection install-local-principal.fixtures.ts and service-datasource entitled-caller.fixture.ts doubles that carry the key (non-blocking note; they define their own rows and do not reach the retired declaration)", "PR body (when it opens) will list the cli files outside the claim's literal surface: organization-ownership-apply.ts, organization-ownership-inventory.ts (now identical to main), organization-ownership-plan.ts, the two new helpers organization-ownership-grants.ts and organization-ownership-column-drops.ts, and their tests" ], "deviations": [ "My first fix commit folded the fixes into the merge commit. Before pushing, I reset that unpushed commit and redid it as a pure merge plus a separate fix commit; the reset touched only my own unpushed work.", "The plugin-security dist was rebuilt while fix 2 was mid-edit; every result above comes from the rebuild at 4b980ec24 or later." ], "api_writes": "git push x5 this round; 1 REST write: this comment, through scripts/pm/post-stamped.mjs and the relay", "mcp_calls": "0 this round (comment reads via public REST GET)" }Generated by Claude Code
objectstack-fleet commentedon Oct 14, 2026
Pointer from
domain:engineseat 2 ·session_01ADzJtzYTLUfgrRZHxkagkX· 2026-10-14T13:04Z. ⛔ Not a claim. No reply is owed.#22975's 8-post is on
main: PR #23428 →0d3ea295d1(landing record 6168936118). S3's hold on it is released, so its PR may open now.catalog-reference-report.ts(the sixth class, positions naming a set the catalog cannot resolve) anddelegated-admin-gate.ts(comments only, plus the strict-containment prose). Mergemainbefore opening.AdminScope.manageBindings/authorEnvironmentSetsand the permissions pages. Nothing in S3's surface needs to wait for it.Generated by Claude Code
objectstack-fleet commentedon Oct 14, 2026
os-dev-report
{ "issue": 23430, "status": "pr-open-draft", "session": "session_014fyKy7M7fEuwkfqc5JHdJJ", "branch": "claude/issue-23430-grant-column", "head": "7b50158db85fc41fb1b6a7c2b77f00be5f857651", "pr": 23503, "pr_state": "draft, opened through the fleet-write relay (run 38581458007), assignee marchtian, body read back byte-identical (8673 bytes)", "pr_body_lines": "line 1 `Fixes #23430 · Part of #15211 …`; line 2 `Clause-②: yes (narrowing)`; carries `## 维护者速读(草稿)` (seat opinion left empty), the cli files outside the claim's literal surface, and the verification record", "since_last_report": [ "95898bba18 merges origin/main at 0d3ea295d1 (8-post) as a merge commit. Conflicts: catalog-reference-report.ts keeps 8-post's SecurityCatalogEntry import and sixth class, without GRANT_SET_ID_FIELD; the logReport remedy wording survives. Its test now expects exactly one kernel:bootstrapped handler, since the backfill is retired. 8-post's edit to grant-permission-set-name-backfill.test.ts stays deleted with its code. The census docs took main's versions.", "7b50158db8 re-measures the tenant-audit census on the merged tree (227 -> 225) and its six prose figures, and moves the changeset's reconcile bullet out of the --plan sub-list, where the previous round had misplaced it" ], "validation_on_head": [ "pnpm install --frozen-lockfile :: exit 0", "build (cli, plugin-security, plugin-auth, dogfood closures) :: exit 0", "plugin-security typecheck + full suite :: 185 files, 3690 passed, 45 skipped", "plugin-auth full suite :: 140 files, 2815 passed, 10 skipped", "cli typecheck :: exit 0; --project unit :: 287 files, 4307 passed; --project integration :: 129 files, 1125 passed, 2 skipped", "spec build && check:generated && vitest src/migrations :: 209 passed; core typecheck :: exit 0", "dogfood test/grant-name-pickers.dogfood.test.ts :: 3 passed", "dispatch-gates --ran :: 127 derived, 127 run, all exit 0, 0 NOT MEASURED" ], "changed_lines": "3267 (+1229 / -2038, 47 files): over 3,000. Single PR, no split (open question 1 per the seat). It lands on a governed approver's approval after the seat's ACCEPT.", "labels": "none written; the dispatch named none, and skip-changeset does not apply (a changeset ships)", "open_questions": "Q2 and Q3 are accepted as stated in 6169001411; none new.", "api_writes": "git push x2 this round; pr_create x1 (draft + assignee, one relay stroke); this comment x1", "mcp_calls": "0 this round", "cleanup": "Worktree ../objectstack-issue-23430 kept while the PR is under review (re-review fixes expected)" }Generated by Claude Code
objectstack-fleet commentedon Oct 14, 2026
ACCEPT → PR #23503 comment 6172222879 ·
domain:engineseat 1 ·session_01Rerax7QTjKMPCUZxQUtPFR· 2026-10-14T15:44Z. Contract review of record: 6172184615 (PASS). The PR is Tier H by size (3,267 lines). It carriesneeds-user-decision, and review is requested fromos-zhuangandhotlong. The seat lands it after an authorized approval.Generated by Claude Code
objectstack-fleet commentedon Oct 14, 2026
Landed: PR #23503 (C7b S3) MERGED as
6f212166addomain:engineseat 1,session_01Rerax7QTjKMPCUZxQUtPFR(marchtian), 2026-10-14T16:34Z.How it landed:
os-zhuangapproved it (Tier H by size, 3,267 lines) and enqueued it.maind1e2841137. The queue's CI ran on that tree and was green.Fixes #23430closed this card as completed.Byte check, the merged tree against the head the seat accepted (
7b50158db8, ACCEPT 6172222879, review PASS 6172184615):7b50158db8.mainafter the PR's base0d3ea295d1. They come from 15 commits, two of which touch these files:f9b421716d(feat(cli): os migrate organization-ownership --apply executes only the reviewed plan file (ADR-0131 D10, C7b S4) #23437, C7b S4:--applyexecutes only the reviewed plan file) anddaafae6d6d(API-key owner posture). The 7 files:organization-ownership.tsorganization-ownership-apply.ts, its unit test and its served-catalog integration testresolve-authz-context.tsexplain-*tests in plugin-securitygit merge-treeofmain(d1e2841137) and the accepted head. The whole merged tree is equal to that three-way merge, so the PR's hunks are intact and nothing else entered.Carried forward:
skills/objectstack-data/rules/security.mdsentence (Tier H), the post-S4 remedy text and the stale wording.mainto pick S3 up, since both edit the ceremony. The seat is telling seat 2 on feat(objectql,cli): inventory + migration — four fates per object, mirrors deleted only after the id→name rewrite is verified, per-table boot report (ADR-0131 D10) #15211.The dev session is archived.
Generated by Claude Code