Repository navigation
security(identity): identity-auth.org-membership-team-management clause 5 (membership removal) fails at 316be321e — detail withheld pending maintainer #21791
Description
Activity
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsPath: sign-in and identity | identity-auth.org-membership-team-management | P2
Triage: first grade —
bug·security·priority:p2·domain:services·area:identity·needs-user-decision. The intended membership-removal behaviour is a security-boundary ruling, so it goes to the maintainerTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-05T02:09Z. ⛔ Not a claim, ⛔ not a dispatch. ⛔ Classes and positions only. The withheld reproduction stays withheld (RUNNER rule 2); this seat does not hold it.Triage: lands in the identity family (
plugin-auth/plugin-security, by class) ⇒domain:services; rationale: membership removal is an authentication-and-access boundary, which the services lane owns and always escalates.Why
needs-user-decision, notpm:queue. The card says the intended behaviour has to be ruled under one of ADR-0093's documented membership policies before a direction exists. Moving or confirming an access boundary is on the human floor; this seat does not proxy-rule it.Why p2. The verifier graded it medium, and the card records no escalation beyond the plain member grade. It predates 17.6.0.
维护者速读
成员被移出组织这件事,平台现在的行为和清单写的不一致。按 ADR-0093 的哪一种成员策略算"对",要你来定。细节和选项都在 QA 跑测会话里(
session_018zT8d8NpiQ1ExhuNd5TxY6),已经报给你了,这里不重述。请你在那个会话里裁,裁定以Ruled:记到本卡,只写到类别一级。四棱(os-decision-facets)
- ① 长远:按已有的一种成员策略统一行为,不为这个场景另造特例。
- ② 拉动:今天就有清单项失败;凡是会移出成员的组织都会碰到。
- ③ 防 AI 犯错:边界不清时,选响亮拒绝的那一边。
- ④ 不扩散:选已有策略,不新增策略键。
Prior rulings read: ADR-0093 (membership policies) → named by the card; ADR D-number unresolved (the held detail names the clause); thread: none
Generated by Claude Code
- addedarea:identityLogin and identity — sign-up, sessions, organization membership, SSOLogin and identity — sign-up, sessions, organization membership, SSObugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3
on Oct 5, 2026 objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsRuled: membership is decided at creation only (ADR-0093 D7). Under the
automembership policy the platform binds a user to the default organization when the user is created (and in the one-time backfill of users who predate the policy); it never re-binds a user whose membership was removed. Recorded by the PM seat from the maintainer's answer in Claude Code sessionsession_018zT8d8NpiQ1ExhuNd5TxY6, 2026-10-05, choosing the option 「成员关系只在创建时定」. Class level only; the held detail stays withheld.Claim: PM loop round 1 (17.7 pre-release defect from QA run #21784, dispatched on the maintainer's direct order)
Session:session_018zT8d8NpiQ1ExhuNd5TxY6
Account:hotlong(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-21791-membership-create-time
Worktree:objectstack-issue-21791
Domain:domain:services(card label, as triage set it); family confirmed against the held detail: identity,plugin-auth
File surface:packages/plugins/plugin-auth/src,packages/qa/dogfood/test,.changeset/
Container & model:M,mode:subagent,model: opus(default tier; no path-derived mandate)
Clause-②: no
Thread-read: 5986914973
Serial constraints cleared: none namedProvenance: the maintainer, in Claude Code session
session_018zT8d8NpiQ1ExhuNd5TxY6, 2026-10-05, verbatim: 「鉴权类问题的细节,这几个任务你直接接受派发处理」, with the ruling above; landing per the standing order 「开发完整就进队列合并」. This session holds the withheld reproduction; the dispatch carries it privately.
Generated by Claude Code
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 21791, "status": "done", "branch": "claude/issue-21791-membership-create-time", "pr": "https://github.com/objectstack-ai/objectstack/pull/21813", "session": "session_018zT8d8NpiQ1ExhuNd5TxY6", "premise_still_valid": true, "summary": "Under the auto membership policy, membership is now settled at user creation (ADR-0093 D7). In AuthManager, the session.create.before settle runs only for the session minted in the request that creates the user. user.create.before stages the address in a WeakMap keyed by better-auth's endpoint context, so the first session of a sign-up still carries the default organization, and a later sign-in decides nothing. The ADR-0093 D6 backfill is now one-time: a new membership-backfill-ledger.ts records the verdict in sys_migration (id adr-0093-membership-backfill) and later kernel:ready / app:seeded triggers bind nobody. The pass also runs when the default organization is first created. With no ledger or an unreadable one it does not run (warn); a record that fails to write is logged at error. invite-only and multi-org are unchanged. The merge-ref head is 0581a91c16. Beyond the claim's declared file surface, three derived artefacts were refreshed because the ledger insert is a new engine write site: content/docs/permissions/tenant-audit-census.mdx, docs/audits/2026-08-tenant-audit-write-call-sites.counts.md, and scripts/engine-double-contract.pinned.json (via --write). Confidentiality note for the seat: the PR title, changeset and commits stay at class level. The PR body's What/Tests sections and the code comments do name the hook seams that were changed, which the diff itself shows. If the seat wants the body trimmed further, the edit is to cut those two sections down to the class-level sentence; this dev did not PATCH the body.", "tests": "Code measured at f41cb3fe79; head 0581a91c16 = that code + census/ledger refresh (a641515ccf) + a clean merge of origin/main (3 commits, none in plugin-auth or dogfood). (1) Build: pnpm --filter '@objectstack/plugin-auth^...' build, VERDICT command-exit 0; pnpm --filter '@objectstack/dogfood^...' build, VERDICT command-exit 0. (2) pnpm --filter @objectstack/plugin-auth typecheck (tsc + examples + check:test-typecheck OK) and the full vitest run: 'Test Files 120 passed (120) · Tests 2526 passed | 10 skipped (2536)', VERDICT command-exit 0. pnpm --filter @objectstack/dogfood typecheck: exit 0. (3) Dogfood on a real showcase boot, test/membership-decided-at-creation.dogfood.test.ts: '1 passed'. It covers sign-up bound to the default org, the first session carrying the org, remove-member returning 200, sign-in again with no activeOrganizationId, and no sys_member row after 1s. (4) Ablation 1 via scripts/ablation-replace.mjs, from the committed fix: the session gate forced open (|| true). Mutation landed (anchor 1 to 0, blob bf9bf9cc6e04 to 80006b23a7e3). pnpm --filter @objectstack/plugin-auth build, then ablation-dist-preflight: 'marker present in 2 built files'. Dogfood went RED: 'AssertionError: expected ... to be null' (the sign-in session was stamped with the org). Restored (blob == HEAD, git diff HEAD empty). Restore leg: rebuild, preflight --absent ('marker absent from all 14 built files', tree clean), dogfood GREEN. The first two attempts were no-ops and are reported as such: the first broke the DTS build (unused symbol, exit 90), the second used a wrong dist marker spelling (preflight exit 91); neither produced a reading. Ablation 2: the ledger already-run check disabled. 4 cases RED across membership-backfill-ledger.test.ts and auth-plugin.test.ts; restored, blob == HEAD. (5) Gates: dispatch-gates --commands derived 71 families at f41cb3fe79 and 106 at a641515ccf; dispatch-gates --ran reports '106 derived, 106 run, 0 UNRUN'. All exit 0 except: check-tenant-audit-census and its --self-test (exit 1: census 231 to 232 from the new write site; fixed by --write plus the prose figures, re-run exit 0, also exit 0 after the merge); check:engine-double-contract (exit 1: 2 new pinned findOne doubles; fixed by --write, re-run exit 0, also exit 0 after the merge). check:dual-build-cjs-loads: NOT MEASURED, family dual-build-cjs-loads, reason: exit 3 PREREQUISITE NOT MET (unrelated packages unbuilt in this worktree, e.g. studio, organizations). Declared to CI. (6) Narrowed lint: eslint --no-inline-config --format json over the 8 touched .ts files: files 8, errors 0, warnings 0. Population: git diff --name-only origin/main...HEAD -- '*.ts'. Invariance: type-aware linting is not enabled (no parserOptions.project in eslint.config.mjs), so this diff cannot move the verdict on any untouched file. Full pnpm lint belongs to CI.", "mcp_calls": "0", "api_writes": "3 — all via the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls (draft, #21813); (2) assign, POST /repos/objectstack-ai/objectstack/issues/21813/assignees [hotlong], via label-write.mjs with read-back MATCHES; (3) comment, POST /repos/objectstack-ai/objectstack/issues/21791/comments (this os-dev-report). No label written: the dispatch named none and a changeset is present, so skip-changeset does not apply. Reads used gh api GET only.", "open_questions": [ { "question": "After the one-time pass is recorded, a sys_user row inserted directly through the data engine (bypassing better-auth user creation, e.g. a seed) is no longer auto-bound by a later app:seeded pass. Is that acceptable under the creation-only ruling?", "options": [ "A: keep it. Such a row never crosses user creation, and in-tree examples create users through sign-up. Documented in the changeset as Narrowed.", "B: add a creation seam for raw inserts: an engine afterInsert on sys_user that records rows not staged by better-auth, bound at seed settle. More code, and it is the parallel data-layer seam that ADR-0093 D2's guidance warns against." ], "recommendation": "A, because it keeps one creation seam (D2) and no in-tree producer is affected; revisit only if a real producer appears." }, { "question": "On an existing deployment, the first boot of this version has no ledger record, so it runs the backfill one last time.", "options": [ "A: run it once (implemented). This is the literal 'one-time backfill' of the ruling, and the change is no worse than the current behaviour for exactly one boot.", "B: on first boot, write the record without scanning, assuming earlier versions already backfilled. Deployments that predate the reconciler entirely would then never be backfilled." ], "recommendation": "A, because it honours the ruling's one-time backfill and fails toward the documented D6 behaviour, not toward silently skipping it." } ], "out_of_scope_findings": [ "carrier: maintainer (Tier H). ADR-0093 D6's text still describes the app:seeded re-run as a recurring idempotent net. The trigger is kept, but the one-time semantics ruled under D7 are not stated in the ADR. Noted in the PR Acceptance notes, not filed.", "carrier: 承接者:无. scripts/check-durability-degradation-log-level.mjs DURABILITY_CRITICAL_CALLEES does not name the new ledger-record seam. Its failure path logs inline at error today. PR Acceptance notes only." ] }
Generated by Claude Code
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 21791, "status": "done", "branch": "claude/issue-21791-membership-create-time", "pr": "https://github.com/objectstack-ai/objectstack/pull/21813", "session": "session_018zT8d8NpiQ1ExhuNd5TxY6", "premise_still_valid": true, "summary": "Review round 1, pushed once as head 2e64d22953 (PR still draft, body not edited). (1) Default-org owner bind: the AuthPlugin bootstrap reads ledger id adr-0093-default-org-owner-bind. When recorded, it binds nobody, so a later removal stands. When absent, it runs ensureDefaultOrganization and records once the admin is bound or already holds a membership; no_admin and failed writes stay open. With no readable ledger, a new bindOnlyOnCreate option binds the admin only on the call that creates the default org (new reason owner_bind_decided). The verdict is cached per process. Fresh-install bootstrap is unchanged. (2) no-target-org is now recorded when any sys_organization row exists; it is deferred only on an empty organization table, or when that read fails. (3) The backfill walks sys_member and sys_user whole with keysetWalk (page size = deps.limit, default 500, no row cap). A walk that cannot complete binds nobody and returns the new reason scan-incomplete, which is never recorded. (4) The session gate no longer reads the store: the objectql-adapter factory gains an onRecordCreated callback, awaited after each create with the protocol object name and the stored row. AuthManager stages the sys_user id against the better-auth endpoint context from that callback, and the session seam checks it in memory. A real boot proves this wiring; it is not proven on PG. Core better-auth flows also run no native transaction on this adapter outside SCIM requests, so the row is already committed before the session hook. (5) Tests added: restart after removal re-binds nobody, admin included; email-verification flow (creation seam binds with no session, the later sign-in resolves it); multi-org refusal recorded; pagination past one page; incomplete scan not recorded; adapter-to-session wiring, with a control from another request; adapter callback contract. (6) The ledger operator strings now read 're-deciding membership for users whose membership was already decided'. Registered persistLedgerDecisionRow in DURABILITY_CRITICAL_CALLEES, with the matching gate-vocabulary copy in measure-durability-swallow-family.mjs WRITE_SHAPED_CALLEES. The changeset gains Default organization owner / Full scan / Upgrade lines. All three CI reds on a476ec387b were fixed before this push: the swallow-census copy; the reconcile fixture membership with no id (the keyset walk seeks on id; the assertion is unchanged, and the fixture now carries the primary key a stored row has); and the engine-double ledger, refreshed via --write. Open questions 1 and 2 were ruled A; no code change was needed for them.", "tests": "plugin-auth suite measured at 34782539ce, which is round-1 code before the double/ledger-only commits: `pnpm --filter @objectstack/plugin-auth build && typecheck` (check:test-typecheck OK) and `vitest run`: 'Test Files 121 passed (121) · Tests 2534 passed | 10 skipped (2544)', exit 0. After the where-matcher double fix: user-created-in-request.test.ts 4 passed. Dogfood membership-decided-at-creation, real showcase boot, after rebuilding the closure: 1 passed. Ablation 3 (adapter staging disabled, `|| Date.now() > 0` on the sys_user guard) through ablation-replace: mutation landed (blob f95f8054f486 to e50ba75816d1); dist preflight 'marker present in 2 built files'. Dogfood went RED at the first-session assertion ('expected null to be ORG_ID'), which proves the no-read staging is load-bearing on a real boot. Restored (blob == HEAD). Restore leg: rebuild, preflight --absent ('absent from all 14 built files', tree clean), dogfood GREEN. Gates: 119 families derived at 34782539ce and run; all exit 0 except: check:where-matcher (exit 1, the new double had no combinator branch; fixed by refusing `$` keys, exit 0); spec check:skill-examples (exit 3 PREREQUISITE NOT MET, client-react unbuilt in this fresh worktree; built client and client-react and re-ran, exit 0); check:dual-build-cjs-loads NOT MEASURED, reason: exit 3 PREREQUISITE NOT MET (unrelated packages unbuilt), declared to CI. After merging origin/main (3 commits, overlapping only scripts/engine-double-contract.pinned.json and dogfood test files), re-ran on 2e64d22953 with exit 0 each: check:engine-double-contract, check-tenant-audit-census, check:swallow-census-controls, check:durability-log-level, check:where-matcher, check:nul-bytes. The plugin-auth suite was not re-run on the merge commit: the incoming core change is a write-response masking fix outside plugin-auth's unit doubles. NOT MEASURED on 2e64d22953 itself; CI covers it.", "mcp_calls": "0", "api_writes": "1 this round — fleet-write relay comment, POST /repos/objectstack-ai/objectstack/issues/21791/comments (this round-1 os-dev-report). Plus git push (not REST). PR body not edited.", "open_questions": [], "out_of_scope_findings": [ "carrier: @objectstack/organizations owner (承接者:无 named). packages/plugins/organizations/src/ensure-default-organization.ts is the walled-posture copy of the bootstrap and was not given the one-time owner-bind gate. It is outside this card's plugin-auth surface and posture. PR Acceptance notes only; dedupe words: walled default org owner rebind, organizations ensureDefaultOrganization, owner bind ledger." ] }
Generated by Claude Code
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 21791, "status": "done", "branch": "claude/issue-21791-membership-create-time", "pr": "https://github.com/objectstack-ai/objectstack/pull/21813", "session": "session_018zT8d8NpiQ1ExhuNd5TxY6", "premise_still_valid": true, "summary": "Review round 2, pushed once as head c4f7db3d7b (draft; body not edited). FINAL DESIGN, class level (ADR-0093 D7: under the auto policy, membership is decided when the user is created). (a) Creation: user creation binds a new user to the default organization, as before. The first session minted by that same request also carries it. The request recognises its own new user from the adapter's create result, staged per request context; nothing is read back. Later sign-ins decide nothing. (b) One-time decisions in the sys_migration ledger: the pre-existing-user backfill (adr-0093-membership-backfill) and the default-organization owner bind (adr-0093-default-org-owner-bind). Each runs until it reaches a verdict, is recorded, and is latched in-process even if the record fails to write (that failure is logged at error). The backfill scans both tables in full with keyset pages and no row cap. An incomplete scan binds nobody and records nothing. A multi-org refusal (organizations exist, no default target) is recorded. A deployment with no organization at all defers, and the pass runs again when the default organization is first created. (c) One owner-bind gate, the new export createEnsureDefaultOrganizationOnce, is called by both the single-org AuthPlugin and the walled @objectstack/organizations wiring. Once the bind is decided, a missing default organization is recreated with nobody bound and no seed-ownership handoff. Without a readable ledger, the owner is bound only when the bootstrap creates the organization. (d) Users inserted raw through the data engine never cross user creation and stay unbound once the backfill is recorded; that includes seeds that finish after their budget. The showcase approval-demo seed now writes its personas' membership when it creates them; existing personas are left as they are. (e) keysetWalk (@objectstack/types) detects a stalled cursor by key equality or a repeated page, never by JS string order. (f) invite-only and multi-org binding are unchanged. ROUND-2 ITEMS: F1, the persona seed binds at creation, the changeset Narrowed line now says plainly that raw-insert users stay unbound once recorded, and a new dogfood asserts the personas' organization. F3, in-process latches for the owner bind and the backfill. F4, the walled wiring uses the shared gate, with a walled test. F5, tests listed under tests. F6, the org is recreated without a bind. F7, keyset equality. F8 skipped. Changeset now covers plugin-auth, organizations and types (all patch, Clause-②: no).", "tests": "All measured at head c4f7db3d7b (merge of origin/main, 7 commits, after the dogfood closure was rebuilt). plugin-auth: typecheck OK, then vitest 'Test Files 121 passed (121) · Tests 2540 passed | 10 skipped (2550)'. organizations: typecheck OK, then vitest 'Test Files 9 passed (9) · Tests 131 passed (131)'. types: typecheck OK, then vitest 'Test Files 24 passed (24) · Tests 717 passed (717)'. dogfood typecheck OK; dogfood isolated run of membership-decided-at-creation, showcase-demo-personas-membership (new, orgContext boot), showcase-demo-personas-loginable, approval-override-composite-pin and membership-ended-session-revoke: 'Test Files 5 passed (5) · Tests 10 passed (10)'. showcase vitest (approval-resume-relation-expand, inert-wirings): 47 passed; showcase typecheck (tsc) OK. New tests: auth-plugin.test.ts covers the default-org-created trigger running the pass; an unreadable ledger plus an existing org plus an unbound admin not binding, with a fresh-install CONTROL that does bind; a failed ledger write then second triggers (middleware, app:seeded, kernel:ready) re-binding nobody; recorded owner bind plus a deleted org recreating it with no member; and restart after removal. user-created-in-request.test.ts covers a different pre-existing user in a creating context. Also the walled once-gate test in organizations, and the keyset collation test. Ablations, all via ablation-replace with blob restored and equal to HEAD: (4) keyset compare reverted to JS order, collation test RED, 1 failed. (5) owner-bind latch removed, 'a failed ledger write is latched in-process' RED. (6) backfill latch removed, the same test RED. (7) persona membership call removed in the source-resolved showcase seed, 'phone.demo@example.com has an organization: expected +0 to be 1' RED. Two first attempts of (5)/(6) were refused by the tool as no-ops (deletion spelled as empty replacement) and were rerun with marker replacements. Ablation 3 from round 1 (adapter staging) still stands. Gates: dispatch-gates derived 121 families at the merge head; all exit 0 except check:dual-build-cjs-loads, NOT MEASURED (exit 3 PREREQUISITE NOT MET, unrelated packages unbuilt), declared to CI, and spec check:skill-examples (exit 3 prerequisite; after building client and client-react, re-run exit 0). Touched-gate set also exit 0 before the merge: engine-double-contract, tenant-audit-census, swallow-census-controls, durability-log-level, where-matcher, nul-bytes, cross-package-test-inputs, doc-authoring, issue-citations.", "mcp_calls": "0", "api_writes": "1 this round — fleet-write relay comment, POST /repos/objectstack-ai/objectstack/issues/21791/comments (this report). Plus git push (not REST).", "open_questions": [], "out_of_scope_findings": [] }
Generated by Claude Code
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsClaim correction (PM seat), superseding the claim's
Clause-②: no:Clause-②: yes (widening)
PR #21813 adds public exports to
@objectstack/plugin-auth(the gated default-organization bootstrap, an adapter-factory option, two bootstrap options and two result reasons) and changes the documented contract of the exported backfill helper; the changeset declaresminorfor plugin-auth andpatchfor@objectstack/organizationsand@objectstack/types. A contract-tier review record is owed on the PR head before landing.Rulings recorded for the developer's two open questions (maintainer's answer in Claude Code session
session_018zT8d8NpiQ1ExhuNd5TxY6, 2026-10-05, accepting the recommended options):- Ruled: users inserted directly through the data engine (bypassing user creation) stay unbound once the one-time backfill is recorded; seeds create users through the creation seam or write their membership themselves.
- Ruled: the first boot of an upgraded deployment runs the one-time backfill once.
Generated by Claude Code
- added 2 commits that reference this issue
on Oct 7, 2026
QA-source: #21784 · identity-auth.org-membership-team-management · acceptance[4]
The 17.7 pre-release run #21784 (subject
316be321e) failed clause A5 and negative N2 ofidentity-auth.org-membership-team-management(rev 3), the membership-removal contract.session_018zT8d8NpiQ1ExhuNd5TxY6) and reported to the maintainer there.objectstack. No existing card covers it (open issues searched).Full run evidence (per-item table; this row carries the same withholding): #21784.
Generated by Claude Code