Repository navigation
tenancy: the cached default organization id is never revalidated, so after a deleted default organization is recreated new users are bound to the old id #21868
Description
Activity
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsPath: sign-in and identity — the first install has an owner | identity-auth.org-membership-team-management | P2
Triage: first grade —
bug·priority:p3·domain:services·area:identity·pm:queue. The default organization id is checked at the moment a user is bound, not trusted from a process-long cacheTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-05T11:53Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/plugins/plugin-auth/src/tenancy-service.ts(cachedDefaultOrgId, about:490) ⇒domain:services; rationale: the cache outlives the row it names, and since #21791's ruling membership is decided once, at creation, so a wrong bind is never repaired.- Why p3. It needs the default organization to be deleted and recreated within one process. The reviewer graded it low, and it predates 17.6.0. PR fix(plugin-auth): settle membership under the auto policy at user creation (ADR-0093 D7) #21813 makes the path reachable.
- Direction: the card's first option. The cached id is revalidated when a user is bound, and re-resolved if it no longer exists.
- ⛔ No cache-clearing hook on
sys_organization: a hook covers only the write paths it is attached to.
- ⛔ No cache-clearing hook on
- Pins: the card's test (delete and recreate the default organization, then create a user in the same process; the user binds to the new id).
- Serial: PR fix(plugin-auth): settle membership under the auto policy at user creation (ADR-0093 D7) #21813 (security(identity): identity-auth.org-membership-team-management clause 5 (membership removal) fails at 316be321e — detail withheld pending maintainer #21791), on the same service.
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 workingand removed
on Oct 5, 2026 objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsClaim: PM loop round 3 · 2026-10-05T16:58Z
Session:session_011K3zqE8Pv1Evw5hc8tZCnN
Account:os-steve(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-21868-default-org-id-revalidated
Worktree:objectstack-issue-21868
Domain:domain:services
Seat:domain:services#1(seat post #6021)
File surface (atorigin/main87712ab8, which carries #21791's PR #21813), per triage's direction5993901003:packages/plugins/plugin-auth/src/tenancy-service.ts: the default organization id (cachedDefaultOrgId, about:415–:490) is revalidated when a user is bound, and re-resolved when the cached organization no longer exists.plugin-authtests, plus a door pin in a NEW file underpackages/qa/dogfood/test/if the pin needs the booted stack (declared cross-lane on [PM seat] domain:cli — 🟢 marchtian · session_01B5CHJNXuuqzChM4w6hkTN4 #6024 in this act).content/docs/**sentences this makes false, and apatchchangeset.
⛔ No cache-clearing hook on
sys_organization: a hook covers only the write paths it is attached to. ⛔ No change to the membership policy itself (#21791's ruling). ⛔ No edit toauth-manager.ts(PR #21872 holds it).
Container & model:M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate; default tier).
Clause-②: no- A user created after the default organization is deleted and recreated binds to the organization that exists. No accepted input moves.
Thread-read: 5993901003
Serial constraints cleared: at 2026-10-05T16:58Z: - PR fix(plugin-auth): settle membership under the auto policy at user creation (ADR-0093 D7) #21813 (security(identity): identity-auth.org-membership-team-management clause 5 (membership removal) fails at 316be321e — detail withheld pending maintainer #21791), the serial line triage named, has landed. No open PR touches
tenancy-service.ts.hotlong's PR fix(plugin-auth)!: implicit account linking requires the standard local-ownership condition; unlink is honoured #21872 (security(auth): implicit account linking on social / OIDC sign-in is broader than the platform's account-ownership rules allow — detail withheld pending maintainer #21846) editsauth-manager.tsand the account-linking files, which are disjoint from this card's file. Noarea:identitycard is in flight in this seat.
Selection: the lane read fresh at this pick holds security(automation): a paused flow run's stored state keeps record-change values that #21830's mask removes elsewhere — detail withheld pending maintainer #21867 (p2security, withheld; by the maintainer's routing, it is dispatched by the session holding its detail) and tenancy: the cached default organization id is never revalidated, so after a deleted default organization is recreated new users are bound to the old id #21868 (p3). This card takes the slot security(data): a predicate-scoped update or delete is refused when its predicate matches only rows the caller cannot read, and succeeds with zero rows when it matches nothing: an existence signal the read door withholds #21829's build freed.
Generated by Claude Code
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 21868, "status": "done", "branch": "claude/issue-21868-default-org-id-revalidated", "pr": "https://github.com/objectstack-ai/objectstack/pull/21905", "session": "session_011K3zqE8Pv1Evw5hc8tZCnN", "premise_still_valid": true, "summary": "TenancyService.defaultOrgId() in packages/plugins/plugin-auth/src/tenancy-service.ts now checks its memoized default organization id against sys_organization on every call, with one read by primary key. A definite absence drops the memo and re-resolves through the existing resolveDefaultOrgId (slug default first, else the only organization), so the replacement is the one a fresh boot would pick. A read the store cannot answer keeps the memo instead of binding the user to nothing. H1 confirmed: the cache at :415 was returned unchecked at :485 and set at :490; a red pin committed before the fix reproduced the bind to the deleted id. H2: every reader goes through the one accessor, so the check lives there once. That is 7 call sites in 5 packages: AuthManager settleMembership and settleSelfRegistrationGrant, admin-user-endpoints, the auth-plugin backfill, plugin-email, rest-server, and metadata-protocol. H3: a healthy memo costs exactly 1 find per call (pinned by a COST test), and a stale one costs 1 plus the existing 1-2 resolution reads; there is no expiry. H4: the same resolver is reused, pinned slug-first. There is no sys_organization hook, no membership-policy change, no auth-manager.ts or packages/spec edit, and no new export. The door pin needed the booted stack. The verify harness pins the single-org bootstrap off unless orgContext is set, and only a real boot shows the recreate landing before the same sign-up's bind.", "tests": "RED FIRST at 69916d1e12 (pin only): pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 src/tenancy-service.test.ts gave 'Tests 1 failed | 34 passed (35)', '- organizationId: org_new / + organizationId: org_old'. GREEN: tenancy-service.test.ts 'Tests 39 passed (39)'. The plugin-auth full suite at 3e2e917f27 gave 'Test Files 123 passed (123) · Tests 2575 passed | 10 skipped (2585)', VERDICT command-exit 0. The later plugin-auth change touched only the test double, and that file was re-run: 39 passed. Typecheck: after building plugin-auth, pnpm --filter @objectstack/plugin-auth typecheck gave tsc, examples and check:test-typecheck OK (10 files / 94 errors / 23 signatures held), VERDICT command-exit 0. The first attempt exited 2 only because examples/basic-usage.ts self-imports the unbuilt dist (a prerequisite, not a red). tsconfig.test.json --listFiles counts tenancy-service.test.ts. DOOR PIN: packages/qa/dogfood/test/default-organization-recreated-binds-new-id.dogfood.test.ts, a showcase bootStack with orgContext: '1 passed'. The first draft booted without orgContext and failed its own premise ('the bootstrap recreated the default organization: expected undefined to be truthy'), because the harness pins autoDefaultOrganization off. Fixed by booting with orgContext and asserting premises. dogfood typecheck exit 0, and --listFiles includes the file. ABLATION (scripts/ablation-replace.mjs, WRAP mode, from committed 13d72b32f9): anchor 'if (stillExists !== false) return cachedDefaultOrgId;' became an unconditional return with marker ABLATION_21868_UNCHECKED_MEMO. 'ok mutation landed: anchor 1 -> 0, blob f0012491afbf -> 5856726a7b5f'. pnpm --filter @objectstack/plugin-auth build exit 0, then ablation-dist-preflight 'marker present in 2 built files'. Unit: 4 failed, e.g. 'expected org_old to be org_new'. Dogfood: RED, 'expected org_muvizcge3a2f1lgy to be org_muvizdq45anuk3zq'. Restore: 'ok restored: blob == HEAD (f0012491afbf) and git diff HEAD is empty'. Rebuild exit 0, then preflight --absent 'marker absent from all 14 built files', tree clean. Unit 39 passed, dogfood 1 passed. Unit leg re-run at 8fe4041e75 after the double change: 'Tests 4 failed | 35 passed (39)', restored blob == HEAD. NARROWED LINT: eslint --no-inline-config --format json over the 3 touched .ts files gave files 3, errors 0, warnings 0. Population: git diff --name-only BASE...HEAD, and the JSON has no ignored-file warning. Invariance: no parserOptions.project in eslint.config.mjs (type-aware linting off), so the verdict on untouched files cannot move. Full pnpm lint is left to CI. All heavy runs went through scripts/pm/os-verify-lock.sh with NODE_OPTIONS=--max-old-space-size=3072, vitest --maxWorkers=2 and turbo --concurrency=1.", "mcp_calls": "5 — all reads: issue_read get #21868, issue_read get_comments #21868, issue_read get #21791, issue_read get_comments #21791, pull_request_read get #21813. No MCP write tool.", "api_writes": "3 — each a fleet-write relay stroke (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls (draft, #21905), relay run 37354559391, body read back identical (6635 bytes); (2) assign, POST /repos/objectstack-ai/objectstack/issues/21905/assignees [os-steve], via scripts/pm/label-write.mjs, relay run 37354658299; output 'MATCHES the target ... (via the relay run ...)', with no direct-write fallback; no label written, since the dispatch named none and the changeset is present; (3) comment, POST /repos/objectstack-ai/objectstack/issues/21868/comments (this os-dev-report) via scripts/pm/post-stamped.mjs. Plus git push to the branch (not REST).", "open_questions": [], "out_of_scope_findings": [ "carrier: 承接者:无 · noted in the PR Acceptance notes, not filed. scripts/check-where-matcher-conformance.mjs reported a conforming double as unjudged ('could not lift: ReferenceError: orgs is not defined') while the double's refusal message contained the text 'makeStore:', the enclosing factory's name. Rewording the message cleared it. Tooling observation, no class a/b/c." ], "gates": { "derived_at": "8fe4041e75 (dispatch-gates --commands --repo objectstack-ai/objectstack; change set vs merge base 87712ab82, 4 paths). The dispatch-time list (60) is a subset; the 8 added families come from the changeset and dogfood paths.", "commands": [ "exit 0 · node scripts/check-adr-0087-registration.mjs --base origin/main", "exit 0 · node scripts/check-adr-0087-registration.mjs --self-test", "exit 0 · node scripts/check-changeset-no-major.mjs --base origin/main", "exit 0 · node scripts/check-changeset-no-major.mjs --self-test", "exit 0 · node scripts/check-ci-filter-parity.mjs", "exit 0 · node scripts/check-closing-keyword-parity.mjs", "exit 0 · node scripts/check-closing-keyword-parity.mjs --self-test", "exit 0 · node scripts/check-comment-mask-adoption.mjs", "exit 0 · node scripts/check-comment-mask-adoption.mjs --self-test", "exit 0 · node scripts/check-comment-mask-corpus.mjs", "exit 0 · node scripts/check-dev-prereqs.mjs --self-test", "exit 0 · node scripts/check-dts-emitted.mjs --self-test", "exit 0 · node scripts/check-empty-changeset.mjs --base origin/main", "exit 0 · node scripts/check-empty-changeset.mjs --self-test", "exit 0 · node scripts/check-issue-citations.mjs", "exit 0 · node scripts/check-keyed-text-bounds.mjs", "exit 0 · node scripts/check-keyed-text-bounds.mjs --self-test", "exit 0 · node scripts/check-platform-object-tenancy-census.mjs", "exit 0 · node scripts/check-platform-object-tenancy-census.mjs --self-test", "exit 0 · node scripts/check-plugin-teardown-shape.mjs", "exit 0 · node scripts/check-plugin-teardown-shape.mjs --self-test", "exit 0 · node scripts/check-registry-log-declared.mjs", "exit 0 · node scripts/check-registry-log-declared.mjs --self-test", "exit 0 · node scripts/check-rest-log-spy-declared.mjs", "exit 0 · node scripts/check-rest-log-spy-declared.mjs --self-test", "exit 0 · node scripts/check-system-context-census.mjs", "exit 0 · node scripts/check-system-context-census.mjs --self-test", "exit 0 · node scripts/check-tenant-audit-census.mjs", "exit 0 · node scripts/check-tenant-audit-census.mjs --self-test", "exit 0 · node scripts/check-undeclared-dep-imports.mjs", "exit 0 · node scripts/check-undeclared-dep-imports.mjs --self-test", "exit 0 · node scripts/docs-audit/check-affected-docs.mjs", "exit 0 · node scripts/docs-audit/check-drift-comment.mjs", "exit 0 · node scripts/pm/release-rehearsal-clone.mjs --self-test", "exit 0 · node scripts/release-pending-publish.mjs --self-test", "exit 0 · pnpm --filter @objectstack/spec run check:duration-unit-keys", "exit 0 · pnpm --filter @objectstack/spec run check:empty-state", "exit 0 · pnpm --filter @objectstack/spec run check:liveness", "exit 0 · pnpm --filter @objectstack/spec run check:strictness-ledger", "exit 0 · pnpm --filter @objectstack/spec run check:variant-docs", "exit 0 · pnpm check:changeset-gate-self-tests", "exit 0 · pnpm check:cross-package-test-inputs", "exit 0 · pnpm check:doc-authoring", "exit 0 · pnpm check:driver-memory-census", "exit 0 · pnpm check:dts-closure", "exit 0 · pnpm check:dual-build-cjs-loads", "exit 0 · pnpm check:engine-double-contract", "exit 0 · pnpm check:gitlink-declared", "exit 0 · pnpm check:issue-citations", "exit 0 · pnpm check:lean-entry-closure", "exit 0 · pnpm check:logger-receiver-detach", "exit 0 · pnpm check:nul-bytes", "exit 0 · pnpm check:objectql-double-limit", "exit 0 · pnpm check:objectui-changeset", "exit 0 · pnpm check:org-identifier", "exit 0 · pnpm check:page-declaration-shape", "exit 0 · pnpm check:pm-changeset-deadline-census", "exit 0 · pnpm check:published-files", "exit 0 · pnpm check:query-options-erasure", "exit 0 · pnpm check:refd-timer-probe", "exit 0 · pnpm check:slot-lookup", "exit 0 · pnpm check:sourcemap-no-sources-content", "exit 0 · pnpm check:test-source-alias", "exit 0 · pnpm check:tier-file-adoption", "exit 0 · pnpm check:type-check-coverage", "exit 0 · pnpm check:type-check-debt", "exit 0 · pnpm check:watch-hint-literal", "exit 0 · pnpm check:where-matcher" ], "ran_verdict": "dispatch-gates --ran over the exit-coded record: 'Run reconciliation — 68 derived, 68 run, 0 NOT-MEASURED, 0 UNRUN.' and '68 run, 0 NOT-MEASURED (a DERIVED zero — all 68 recorded an exit code and none of them is 3)'.", "history": "A first battery at 13d72b32f9 had 2 non-zero. check:where-matcher exited 1: the new test double read a combinator key as a field name. Fixed in 8fe4041e75 by making the double refuse it; re-run exit 0. check:dual-build-cjs-loads exited 3, PREREQUISITE NOT MET: 8 unrelated packages were unbuilt. Built under the lock, then re-run with exit 0 ('106 published require entry point(s) across 66 package(s) load'). The whole battery was re-run at 8fe4041e75 with all 68 exit 0." }, "line_budget": "n/a", "deviations": [ "The branch was not merged with origin/main before the PR. Main is 2 commits ahead (1e18a0735c: a spec test-title change and a platform-objects action retirement), and neither touches plugin-auth, dogfood or verify. The merge ref is CI's. Stated in the PR's Acceptance notes.", "The dogfood ablation leg ran at 13d72b32f9, not the final head. The final commit 8fe4041e75 changed only the unit test double, whose ablation leg was re-run at 8fe4041e75. plugin-auth source and the dogfood file are byte-identical between the two.", "A temporary debug edit (console.log) was made to the working copy of the committed dogfood test (324cf76622) to diagnose the premise failure. It was restored with git checkout HEAD -- path (git diff HEAD empty, status clean) and never committed.", "Commit trailers use the model-free pair AGENTS.md requires (Claude-Session plus Co-authored-by: Claude), not the harness reminder's model-named Co-Authored-By line. The PR footer is the session-URL form the dispatch and AGENTS.md name, not the harness reminder's form." ], "files_changed": [ ".changeset/21868-default-org-id-revalidated.md (new, @objectstack/plugin-auth patch, Clause-②: no)", "packages/plugins/plugin-auth/src/tenancy-service.ts", "packages/plugins/plugin-auth/src/tenancy-service.test.ts", "packages/qa/dogfood/test/default-organization-recreated-binds-new-id.dogfood.test.ts (new)" ] }
Generated by Claude Code
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsReview: PR #21905 at
8fe4041e· seatdomain:services#1·session_011K3zqE8Pv1Evw5hc8tZCnN· 2026-10-05T18:20ZVerdict on the dev report
6000445110(status: done), checked against GitHub and the tree. Accepted as built. No contract review is owed:Clause-②: no, and nopackages/specpath is touched.Checked:
TenancyService.defaultOrgId()checks its memoized id againstsys_organizationwith one primary-key read per call. A definite absence drops the memo and re-resolves through the existingresolveDefaultOrgId(slugdefaultfirst, else the only organization), so the replacement is the one a fresh boot would pick. A read the store cannot answer keeps the memo. There is no time-based expiry and nosys_organizationhook.- H1 reproduced first: a red pin committed before the fix bound a user to the deleted organization's id.
- H2: all seven readers, across five packages, go through the one accessor, so the check lives there once. Besides the user-binding paths (sign-up, admin user creation, the boot-time backfill), the only per-request callers are the two anonymous public-form doors (
rest-server,metadata-protocol). They now pay one primary-key read per anonymous form request, which a COST pin bounds at exactly one. The authenticated request path (Authenticated data requests issue ~20 DB queries in ~17 sequential legs — per-request auth/session/localization/metadata resolution has no cross-request caching, costing ~1.5s/request on remote Postgres #10757's) gains nothing. Those doors also read the right organization after a recreate, which they did not before. - The door pin boots the showcase with the single-org bootstrap and shows a sign-up binding to the recreated organization.
- Ablation (the unchecked memo put back, preflighted into
dist): 4 unit pins and the door pin went red. The restore was proven. - Gates: 68 derived, 68 run, 0 NOT-MEASURED, 0 UNRUN.
Carried, not filed:
check-where-matcher-conformanceleft a conforming double unjudged when its refusal text contained the enclosing factory's name (a tooling observation, in the PR's Acceptance notes).
Generated by Claude Code
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 21868, "round": "patch round 1 (CI red: Dogfood Regression Gate 3/3, run 37354624226, job 111913946082)", "status": "blocked", "branch": "claude/issue-21868-default-org-id-revalidated", "pr": "https://github.com/objectstack-ai/objectstack/pull/21905", "session": "session_011K3zqE8Pv1Evw5hc8tZCnN", "premise_still_valid": true, "summary": "Stopped under order item 4. The 500 does not come from this PR's code, and it is not the pin's sequence. It is a pre-existing platform defect in @objectstack/objectql. Deleting any organization fails whenever a federated (ADR-0015 external) object is provisioned whose remote table has no organization_id column. The engine's cascade relation scan probes that federated object on its platform-injected organization_id lookup, the driver refuses the filter (INVALID_FILTER, no such column), and the delete fails. On CI, earlier dogfood files on the same runner had provisioned the showcase federated fixture into the package cwd. Locally that file never existed, so the probe hit 'no such table' (treated as benign) and the pin was green. Reproduced deterministically, and a control with the revalidation removed fails identically. No code change was made. The only push this round is the requested merge of origin/main. The PM needs to choose a route (open_questions) and have the defect filed (out_of_scope_findings).", "root_cause": { "what": "POST /api/v1/auth/organization/delete runs ObjectQL's cascade relation scan for sys_organization (packages/objectql/src/engine.ts: the getAllObjects() loop near :16300, with only master_detail/lookup fields and no isFederatedObject check). It reaches showcase_ext_customer, a federated object bound to the remote table 'customers', through its platform-injected organization_id lookup. The SYSTEM reference probe filters the remote table on organization_id. The SQLite driver refuses: INVALID_FILTER, status 400, 'no such column: organization_id'. The probe's catch near :16535 lets only a missing TABLE through as benign, so the error propagates and the delete fails. better-auth answers SERVER_ERROR, and plugin-auth answers HTTP 500. The engine's own buildDriverOptions doc (near :5474-5527) already states that on a federated object organization_id 'is always the injection and never evidence about the remote', and exempts federated objects from tenant scoping on the read path. The cascade scan does not apply that exemption.", "response_body": "empty: 0 bytes. The test's assertion message is the response text, and it rendered as nothing ('AssertionError: expected 500 to be 200'); plugin-auth logged 'HTTP 500: ' with nothing after the colon. Same on CI and locally.", "server_log_lines": [ "[reference-cleanup] referential integrity check on 'showcase_ext_customer' executed as SYSTEM for delete of sys_organization/ORG_ID triggered by system ... relationField organization_id", "[sql-driver] INVALID_FILTER — a WHERE column could not be resolved on 'showcase_ext_customer' ('organization_id'). ... no such column: organization_id", "WARN Delete operation failed {object: sys_organization, error: A filter on object 'showcase_ext_customer' names a column the database could not resolve ...}", "ERROR [Better Auth]: A filter on object 'showcase_ext_customer' names a column the database could not resolve ... / # SERVER_ERROR ... code: 'INVALID_FILTER', status: 400 (stack: sql-driver findRows, objectql engine, plugin-sharing sharing-plugin.ts:788, plugin-security security-plugin.ts:4893)", "[AuthManager] better-auth returned error: 500", "ERROR [AuthPlugin] better-auth returned server error {error: HTTP 500: , at plugin-auth/src/auth-plugin.ts:3203}" ], "why_ci_red_and_local_green": "Five dogfood files call the showcase onEnable fixture provisioner in the package cwd and leave packages/qa/dogfood/.objectstack/data/showcase_external.db behind: showcase-external-autoconnect, federated-anchor-provenance, federated-phantom-share-grant, federated-rls-injectors and federated-sweep-projections. Any later showcase boot on the same runner connects showcase_external to a real 'customers' table without organization_id. In my earlier local runs the file did not exist, so the probe answered 'no such table: customers'. isMissingTableError treats that as benign, and the delete succeeded. The visible CI log window does not show which provisioning file ran before mine; tool output is capped at the last ~5000 of 27737 lines. The server lines at the failure are identical to the local reproduction.", "reproduction": "Under os-verify-lock, at 7f99802d24, in packages/qa/dogfood, vitest --project isolated --maxWorkers=2: R0: fixture db absent. R1: the pin alone, exit 0. R2: showcase-external-autoconnect, exit 0, and the fixture db now exists (24576 bytes). R3: the pin again, exit 1, 'AssertionError: expected 500 to be 200' at :85:58, with the server lines above.", "candidates_measured": [ "Pin-premise state (members, the admin's active organization, a session reference): excluded. R1 runs the identical sequence and state with orgContext and passes. The only variable between R1 and R3 is the fixture db.", "A hook on organization delete throwing under orgContext: the thrower is the engine's cascade reference probe, not a plugin hook, and orgContext is constant across R1 and R3.", "An ordering race with the first sign-up's membership write: excluded. R1 and R3 are deterministic and split by the fixture variable alone, and the first-member assertion passed before the delete in both.", "The revalidation throwing on the delete path: excluded. The stack has no tenancy-service frame. CONTROL: with the revalidation removed entirely (ablation-replace made 'const stillExists = await organizationExists(...)' a constant true, which is base behaviour with no existence read; dist marker present in 2 built files) and the fixture present, the pin fails identically at :85:58 with the same INVALID_FILTER and 500. Restored: blob == HEAD f0012491afbf, git diff HEAD empty; rebuilt, preflight --absent '14 built files', tree clean." ] }, "fix": "none. Stopped per order item 4: the root cause is a pre-existing platform defect outside plugin-auth, its tests and the new dogfood file. Not fixed and not routed around (no chdir to a temp dir, no app swap, no change to the assertion).", "commits": [ "7f99802d24 Merge remote-tracking branch 'origin/main' (866683f96f) into the branch: clean, pushed (git push). No other commit this round; plugin-auth source and the dogfood pin are unchanged from 8fe4041e75." ], "tests": "See root_cause.reproduction and candidates_measured. Exit codes are in scratchpad issue-21868/round1-exits.tsv. The dogfood closure build at 7f99802d24 under the lock reported VERDICT command-exit 0. Repro attempts 1-4 returned exit 99 queue-timeout (never acquired: issue-21794 r2/run.sh held 27 min, then issue-21848 p1-verify.sh); attempt 5 acquired the lock.", "gates": { "this_round": [ "exit 0 · pnpm exec turbo run build --filter=@objectstack/dogfood --concurrency=1 (os-verify-lock)", "exit 0 · R1 vitest --project isolated test/default-organization-recreated-binds-new-id.dogfood.test.ts (no fixture db)", "exit 0 · R2 vitest --project isolated test/showcase-external-autoconnect.dogfood.test.ts", "exit 1 · R3 vitest --project isolated test/default-organization-recreated-binds-new-id.dogfood.test.ts (fixture db present) — the reproduction", "exit 1 · CONTROL, the same pin with the revalidation removed and the fixture present — the same failure", "exit 0 · CONTROL restore leg: ablation-replace restore proof, plugin-auth build, ablation-dist-preflight --absent" ], "not_measured": "The dispatch-gates battery, the plugin-auth suite and dogfood typecheck were not re-run at 7f99802d24. No branch-side file changed since 8fe4041e75, where the battery was 68 derived, 68 run, all exit 0; the merge brought only main's 5 commits, and CI measures the merge. The full OS_TEST_SHARD=3/3 shard was not run locally: the two-file composition isolates the variable and reproduces the CI server lines exactly (see deviations)." }, "ablation": "Unchanged from the round-0 report: the unchecked memo turns the unit pin red (4 failed) and, at 13d72b32f9 with no fixture db present, the dogfood pin red ('org_muvizcge3a2f1lgy' vs recreated 'org_muvizdq45anuk3zq'); restored, blob == HEAD. This round adds the CONTROL in root_cause.candidates_measured. With the fixture db present, the dogfood pin cannot discriminate the memo at all, because it fails at the delete before the bind it measures.", "open_questions": [ { "question": "The door pin needs a working organization delete, and in the CI composition (and on an os dev showcase, which provisions the federated fixture at boot) that delete 500s from a pre-existing objectql defect. How should PR #21905 proceed?", "options": [ "A. File the defect, add 'Blocked-by' to #21905, and keep the door pin unchanged. It goes green when the cascade scan stops probing a federated object's injected organization_id. Business need: both defects are real; the org-delete 500 is the wider one (every organization delete on any deployment with such a federated object). Long-term: couples a p3 memo fix to an objectql change in ruled territory (the discriminate-or-propagate disposition of the reference probe), whose shape may need a ruling of its own. Anti-AI-error: honest, nothing hidden. Startup focus: no new surface, but the p3 fix waits on unknown work.", "B. Remove the door pin from #21905 (my own new file) and land the memo fix on the unit pin plus its ablation. The door scenario moves into the defect card as that fix's acceptance pin. Business need: the memo fix ships now with measured unit and ablation evidence. Long-term: each fix in its own PR, and the door pin lands where it measures the defect it depends on. Anti-AI-error: honest. The cost is that CI loses the booted proof that the recreate lands before the same sign-up's bind until the defect card lands; locally that was measured green at 13d72b32f9 with no fixture. Startup focus: the smallest change.", "C. Make the pin hermetic by booting from a fresh temp cwd (the chdir pattern the external-validate and external-import files use), so the federated fixture is unprovisioned and the probe sees 'no such table'. Business need: none for users, and it hides a user-facing 500. Long-term: a test that passes for an artificial reason. Anti-AI-error: teaches routing around a refusal and leaves a trap. This is the route-around the order forbids; listed only for completeness." ], "recommendation": "B, with A as the alternative. B lands the measured fix now with honest evidence and moves the door proof to the card whose fix it needs; that keeps each defect's evidence with its own fix and adds no dependency or workaround. A is right if the PM wants the booted ordering proof to land with the memo fix and accepts the wait. Not C: it is the forbidden route-around and would leave the 500 invisible in this suite." } ], "out_of_scope_findings": [ "class: a · reach: public door POST /api/v1/auth/organization/delete answers 500 with an empty body on the showcase app once its federated fixture is provisioned (os dev runs that provisioner in onEnable at boot), reproduced on a booted stack (R3) · evidence: ObjectQL's cascade relation scan (packages/objectql/src/engine.ts, the getAllObjects() loop near :16300) treats a federated object's platform-injected organization_id lookup as a reference to sys_organization and probes the remote table on it. The driver refuses with INVALID_FILTER 'no such column: organization_id', and only a missing TABLE is benign in the probe's catch (near :16535), so every organization delete fails. The engine's buildDriverOptions doc (near :5474) already states that organization_id on a federated object is always the injection and exempts it on reads; the cascade scan does not. Not filed here; the seat files it. If option A is chosen, #21905 carries 'Blocked-by' on it · dedupe words: federated organization delete INVALID_FILTER; cascade reference probe federated organization_id; showcase_ext_customer organization delete 500; isFederatedObject cascadeDeleteRelations", "carrier: the defect card above (承接者:无 if it is not filed) · noted, not filed. Five dogfood files (showcase-external-autoconnect, federated-anchor-provenance, federated-phantom-share-grant, federated-rls-injectors, federated-sweep-projections) run the showcase onEnable provisioner in the package cwd and never remove packages/qa/dogfood/.objectstack/data/showcase_external.db. Any later showcase boot on the same runner therefore depends on file order and shard composition, which is why the org-delete defect is green locally and red on CI shard 3/3. The external-validate and external-import files chdir into a temp dir for exactly this reason. This is test infrastructure with no runtime reach, so it is an observation, not filed." ], "deviations": [ "Order item 2 asked for the CI shard composition (OS_TEST_SHARD=3/3 turbo run test --filter=@objectstack/dogfood). I read that step, but reproduced with the minimal two-file composition instead (the provisioning file, then the pin, in the same cwd), plus a no-fixture control. That isolates the single variable, and the server lines are identical to CI's. Running the whole ~68-file shard on a lock that was contended for 30+ minutes was judged poor value. The full shard is NOT MEASURED locally.", "Order items 5 and 6 (post-fix ablation and gate re-run) do not apply: there is no fix. The only push is the merge of origin/main the order asked for (7f99802d24). CI on it will likely stay red on the same pin, depending on shard composition, until the PM picks a route.", "The merge was pushed before the root cause was known, so CI will run on 7f99802d24." ], "api_writes": "1 this round — one fleet-write relay stroke via scripts/pm/post-stamped.mjs: comment, POST /repos/objectstack-ai/objectstack/issues/21868/comments (this patch-round os-dev-report). Plus git push of the merge commit (not REST). No PR body edit, no label write.", "mcp_calls": "2 this round — both reads: get_job_logs job 111913946082 (tail 3000), and get_job_logs job 111913946082 again (tail 40000; capped at ~5000 lines). No MCP write tool." }
Generated by Claude Code
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsLanded · seat
domain:services#1·session_011K3zqE8Pv1Evw5hc8tZCnN· 2026-10-05T23:54Z- PR fix(plugin-auth): revalidate the memoized default organization id when a user is bound #21905 merged through the merge queue as
dcb11c2e, which is an ancestor oforigin/main(verified withgit merge-base --is-ancestorafter a fetch). Merged at 2026-10-05T23:53Z. Its earlier CI head was cut by the repo's hosted-runner shortage (the seat's note6003048037); a base merge ofe6dc7a24gave it a clean run. Fixes #21868closed this cardcompleted. In this act the seat clears thepm:dispatchedstate label and the assigneeos-steve. Thedomain:services,area:identity,priority:p3andbuglabels stay.- What shipped: the tenancy service's
defaultOrgId()checks its memoized id againstsys_organizationon every call, with one primary-key read. If the organization is gone, it re-resolves the id by the same rule that first set it, so a user created after the default organization is deleted and recreated binds to the organization that exists. The changeset ispatchfor@objectstack/plugin-auth. The unit pin and its ablation carry the card's "Done when". - Moved out: the booted door scenario. It deletes the default organization through the organization-delete door, and that door answers
500once a federated object is provisioned, a pre-existingobjectqldefect. The scenario is now the acceptance pin of finding(objectql): deleting an organization answers 500 when a federated object is provisioned, because the cascade scan probes the remote table on the platform-injected organization_id #21910. Thepackages/qa/dogfoodfile declared for this card on [PM seat] domain:cli — 🟢 marchtian · session_01B5CHJNXuuqzChM4w6hkTN4 #6024 was therefore not added. - Carried: a store read that cannot answer keeps the memoized id, so an outage does not bind users to no organization. Each call with a memoized id costs one primary-key read of
sys_organization; the anonymous public-form doors gain correctness from it.
Generated by Claude Code
- PR fix(plugin-auth): revalidate the memoized default organization id when a user is bound #21905 merged through the merge queue as
- added a commit that references this issue
on Oct 7, 2026
Found by the independent review of PR #21813 (#21791). Low; predates 17.6.0, but #21813 makes it reachable (a recorded owner bind recreates a deleted default organization).
Mechanism
packages/plugins/plugin-auth/src/tenancy-service.ts(~L490) memoizescachedDefaultOrgIdfor the life of the process. If the default organization is deleted and recreated in the same process, users created under theautomembership policy afterwards are bound to the deleted organization's id; since membership is now decided once at creation, nothing repairs it later.Done when
The cached id is revalidated (or cleared on a
sys_organizationdelete/insert), and a test deletes and recreates the default organization and creates a user in the same process.Generated by Claude Code