You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
plugin-security: RLS membership staging is marked done before the resolver answers, so a concurrent read on the same context can compile against an empty membership bag #33379
Loose end of #33051 (PR #33303, landed). Not measured: read from the code while tracing step 2.6.
Problem
stageResolverMembership (packages/plugins/plugin-security/src/security-plugin-rls-filter.ts) sets context.__rlsMembershipStaged = true before it awaits the membership resolver. A second operation running concurrently on the same execution context (a record page's related slices, stream or timeline parts) sees the flag, skips staging, and can compile current_user.KEY row-level policies against a membership bag that is not filled yet. That fails closed: the second read returns fewer rows than it should. Reachable only when an RLS membership resolver is registered and no earlier operation in the request has staged.
Do
Pin it first: two concurrent reads on one context with a resolver that resolves after a tick; the second read's rows must equal the same read alone.
Then make the staging await-safe (e.g. store the in-flight promise and have later operations await it, instead of a boolean set before the await).
Control: one read alone, and sequential reads, unchanged.
Claim: area-lease pilot #33059 · area A3 · 2026-11-14T21:17Z
Session: session_016WV4cwotoQMyrtSr5EqqAF (area thread A3, os-dev executor)
Account: os-elon-musk
Branch: claude/area-A3-33379
File surface: packages/plugins/plugin-security/src/security-plugin-rls-filter.ts (stageResolverMembership), its tests, root .changeset/. No open PR touches this file (checked #33290, the open plugin-security PR).
Premise: measured by the dev at step 0 on origin/main.
{
"issue": 33379,
"status": "done",
"branch": "claude/area-A3-33379",
"pr": "https://github.com/objectstack-ai/objectstack/pull/33474",
"session": "session_016WV4cwotoQMyrtSr5EqqAF",
"premise_still_valid": true,
"summary": "Premise held on origin/main ba35dfd7: stageResolverMembership set context.__rlsMembershipStaged = true before awaiting the resolver, and a second concurrent read on the same context returned [] where the same read alone returned [job_own] (pinned red first). The in-flight resolution is now kept per execution context in a WeakMap (the request scope object-membership-keys.ts already uses) and every concurrent operation awaits it; __rlsMembershipStaged is set only after the resolver answers, so it still rides along on the per-operation envelope copies. After a failure, every operation that awaited that attempt stays closed (policy dropped, zero rows, CHECK refusal on insert) and the failure is not memoized: the next staging call on the context asks the resolver again (on the read path that is already the second staging call of the same read, measured: 2 resolver calls for 2 concurrent failing reads, not 4). Code: 40 changed lines in security-plugin-rls-filter.ts (+35/-5); test +122 in the existing harness file; changeset patch. Clause-②: no. Metadata-protocol: none.",
"tests": "Head 1e349cb371. RED first (test only, origin/main source): 'x two concurrent reads on one context each return what the read alone returns' — 'AssertionError: the second read compiled against the staged bag, not an empty one: expected [] to deeply equal [ job_own ]'; Tests 3 failed | 15 passed (18); controls (alone, sequential) green. GREEN with fix: src/rls-check-membership-staging.test.ts Tests 18 passed (18), lock VERDICT command-exit 0. Whole package: pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2 gives Test Files 267 passed (267), Tests 6658 passed | 45 skipped (6703), VERDICT command-exit 0. Reverse verification (fix committed first at ff35171f, src restored to BASE ba35dfd7 with the committed test kept; trap restore from HEAD): mutant markers RESOLVER_STAGING count=0, flag-before-await count=1; 3 new pins red (second read [] vs [job_own]; 'shared each failing attempt: expected 2 calls, got 1'; 'the second read asked again: expected 1 to be greater than 1'), 15 green; restore proved by blob hash 6c4dc132 = HEAD blob and empty git diff HEAD. Direction observed: turned red. Subject is imported from ./security-plugin.js (source), so no dist leg was needed. No ablation beyond that one reverse run; no permanent mutant file.",
"gates": "Local, head 1e349cb371: plugin-security typecheck (tsc --noEmit + scripts project + check:test-typecheck) VERDICT command-exit 0, 'check:test-typecheck: OK … 0 file(s) / 0 error(s)'. eslint --no-inline-config (repo config) over the 2 changed source files: exit 0, no output. dispatch-gates --commands derived 65 families; run with exit codes captured before any pipe: 63 exit 0, 2 exit 3 PREREQUISITE NOT MET (no full workspace build in this worktree) = NOT MEASURED: check:dual-build-cjs-loads, check:i18n. --ran reconciliation: '65 derived famil(ies) accounted for — 63 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)', exit 0. check-nul-bytes OK; control-byte self-scan of the 3 changed files: 0. Not run locally (CI-owned, declared): Test Core over the affected set (select-shard-packages.sh pull_request shape: 64 packages incl. the spec/core/types/objectql cross-package scans whose globs match the edited test), repo-wide pnpm lint, Build Core, UI lanes. PR check-runs at report time (one read, no polling): Check Changeset in_progress; Check PR Size in_progress; Governed Surface Queue Guard in_progress; Lint · ESLint in_progress; Lint · PM dispatch-gates in_progress; Lint · CI, release & engine gates in_progress; Lint · docs & code guards in_progress; Lint · repo & tooling gates in_progress; Lint scoped x3 in_progress; PR-body guards in_progress; Type Check · workspace/source gates/consumer gates/debt ledger in_progress; Test Core not yet listed; opt-in and queue-only jobs skipped.",
"dogfood_battery": "n/a — no `!` in the PR title and Clause-②: no carries no (narrowing) arm",
"mcp_calls": "0",
"api_writes": "3 REST writes in 2 fleet-write relay strokes (each one POST /repos/objectstack-ai/objectstack/dispatches): POST /repos/objectstack-ai/objectstack/pulls (draft #33474), POST /repos/objectstack-ai/objectstack/issues/33474/assignees (os-elon-musk), POST /repos/objectstack-ai/objectstack/issues/33379/comments (this report). git push x3 (empty-branch probe, fix, changeset) not counted as REST.",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": [
"carrier: none · noted, not filed — SecurityPlugin#stageRlsMembership docblock in security-plugin.ts still says 'once per request'; true for an answering resolver, a failing one is now asked again. Outside the card's file surface; in the PR's Acceptance notes.",
"carrier: none · noted, not filed — with a failing resolver the read path's two staging calls per operation each ask it, so a resolver that is down is called up to twice per read (previously once per request). In the PR's Acceptance notes."
],
"deviations": [
"Dispatch asked for dependents' tests and pnpm lint before push; os-dev.md local scope (which wins on conflict) owes the own package's test + typecheck and the derived gate families, with the affected dependents and repo-wide lint run by CI. Ran eslint over the changed files instead and declared the rest to CI.",
"The claim comment carries no Clause-②/Metadata-protocol lines (os-dev.md: copy, else write none); the dispatch order and AGENTS.md post-task step 3 require both lines in PR body and changeset, so they were written (Clause-②: no, Metadata-protocol: none).",
"Mechanism: the dispatch suggested storing the in-flight promise on the context; it is held in a WeakMap keyed by the context object instead, because a property (string or symbol key) is copied by object spread and a copy taken mid-flight would await a promise filling the original's bag. Same per-context scope as the declared-key staging.",
"Commit trailers are the model-free pair (Claude-Session + Co-authored-by: Claude) per the dispatch and AGENTS.md; the harness reminder's model-named Co-Authored-By was not used.",
"PR body opens with the dispatch-ordered attribution comment line (an HTML comment), the documented harness exception; read back 9383 bytes sent = 9383 stored."
]
}
Activity
objectstack-fleet commentedon Nov 14, 2026
domain:server#5 · os-elon-musk · session_016WV4cwotoQMyrtSr5EqqAF
Claim: area-lease pilot #33059 · area A3 · 2026-11-14T21:17Z
Session:
session_016WV4cwotoQMyrtSr5EqqAF(area thread A3, os-dev executor)Account:
os-elon-muskBranch:
claude/area-A3-33379File surface:
packages/plugins/plugin-security/src/security-plugin-rls-filter.ts(stageResolverMembership), its tests, root.changeset/. No open PR touches this file (checked #33290, the open plugin-security PR).Premise: measured by the dev at step 0 on
origin/main.objectstack-fleet commentedon Nov 14, 2026
domain:server#5 · os-elon-musk · session_016WV4cwotoQMyrtSr5EqqAF
os-dev-report
{ "issue": 33379, "status": "done", "branch": "claude/area-A3-33379", "pr": "https://github.com/objectstack-ai/objectstack/pull/33474", "session": "session_016WV4cwotoQMyrtSr5EqqAF", "premise_still_valid": true, "summary": "Premise held on origin/main ba35dfd7: stageResolverMembership set context.__rlsMembershipStaged = true before awaiting the resolver, and a second concurrent read on the same context returned [] where the same read alone returned [job_own] (pinned red first). The in-flight resolution is now kept per execution context in a WeakMap (the request scope object-membership-keys.ts already uses) and every concurrent operation awaits it; __rlsMembershipStaged is set only after the resolver answers, so it still rides along on the per-operation envelope copies. After a failure, every operation that awaited that attempt stays closed (policy dropped, zero rows, CHECK refusal on insert) and the failure is not memoized: the next staging call on the context asks the resolver again (on the read path that is already the second staging call of the same read, measured: 2 resolver calls for 2 concurrent failing reads, not 4). Code: 40 changed lines in security-plugin-rls-filter.ts (+35/-5); test +122 in the existing harness file; changeset patch. Clause-②: no. Metadata-protocol: none.", "tests": "Head 1e349cb371. RED first (test only, origin/main source): 'x two concurrent reads on one context each return what the read alone returns' — 'AssertionError: the second read compiled against the staged bag, not an empty one: expected [] to deeply equal [ job_own ]'; Tests 3 failed | 15 passed (18); controls (alone, sequential) green. GREEN with fix: src/rls-check-membership-staging.test.ts Tests 18 passed (18), lock VERDICT command-exit 0. Whole package: pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2 gives Test Files 267 passed (267), Tests 6658 passed | 45 skipped (6703), VERDICT command-exit 0. Reverse verification (fix committed first at ff35171f, src restored to BASE ba35dfd7 with the committed test kept; trap restore from HEAD): mutant markers RESOLVER_STAGING count=0, flag-before-await count=1; 3 new pins red (second read [] vs [job_own]; 'shared each failing attempt: expected 2 calls, got 1'; 'the second read asked again: expected 1 to be greater than 1'), 15 green; restore proved by blob hash 6c4dc132 = HEAD blob and empty git diff HEAD. Direction observed: turned red. Subject is imported from ./security-plugin.js (source), so no dist leg was needed. No ablation beyond that one reverse run; no permanent mutant file.", "gates": "Local, head 1e349cb371: plugin-security typecheck (tsc --noEmit + scripts project + check:test-typecheck) VERDICT command-exit 0, 'check:test-typecheck: OK … 0 file(s) / 0 error(s)'. eslint --no-inline-config (repo config) over the 2 changed source files: exit 0, no output. dispatch-gates --commands derived 65 families; run with exit codes captured before any pipe: 63 exit 0, 2 exit 3 PREREQUISITE NOT MET (no full workspace build in this worktree) = NOT MEASURED: check:dual-build-cjs-loads, check:i18n. --ran reconciliation: '65 derived famil(ies) accounted for — 63 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)', exit 0. check-nul-bytes OK; control-byte self-scan of the 3 changed files: 0. Not run locally (CI-owned, declared): Test Core over the affected set (select-shard-packages.sh pull_request shape: 64 packages incl. the spec/core/types/objectql cross-package scans whose globs match the edited test), repo-wide pnpm lint, Build Core, UI lanes. PR check-runs at report time (one read, no polling): Check Changeset in_progress; Check PR Size in_progress; Governed Surface Queue Guard in_progress; Lint · ESLint in_progress; Lint · PM dispatch-gates in_progress; Lint · CI, release & engine gates in_progress; Lint · docs & code guards in_progress; Lint · repo & tooling gates in_progress; Lint scoped x3 in_progress; PR-body guards in_progress; Type Check · workspace/source gates/consumer gates/debt ledger in_progress; Test Core not yet listed; opt-in and queue-only jobs skipped.", "dogfood_battery": "n/a — no `!` in the PR title and Clause-②: no carries no (narrowing) arm", "mcp_calls": "0", "api_writes": "3 REST writes in 2 fleet-write relay strokes (each one POST /repos/objectstack-ai/objectstack/dispatches): POST /repos/objectstack-ai/objectstack/pulls (draft #33474), POST /repos/objectstack-ai/objectstack/issues/33474/assignees (os-elon-musk), POST /repos/objectstack-ai/objectstack/issues/33379/comments (this report). git push x3 (empty-branch probe, fix, changeset) not counted as REST.", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [ "carrier: none · noted, not filed — SecurityPlugin#stageRlsMembership docblock in security-plugin.ts still says 'once per request'; true for an answering resolver, a failing one is now asked again. Outside the card's file surface; in the PR's Acceptance notes.", "carrier: none · noted, not filed — with a failing resolver the read path's two staging calls per operation each ask it, so a resolver that is down is called up to twice per read (previously once per request). In the PR's Acceptance notes." ], "deviations": [ "Dispatch asked for dependents' tests and pnpm lint before push; os-dev.md local scope (which wins on conflict) owes the own package's test + typecheck and the derived gate families, with the affected dependents and repo-wide lint run by CI. Ran eslint over the changed files instead and declared the rest to CI.", "The claim comment carries no Clause-②/Metadata-protocol lines (os-dev.md: copy, else write none); the dispatch order and AGENTS.md post-task step 3 require both lines in PR body and changeset, so they were written (Clause-②: no, Metadata-protocol: none).", "Mechanism: the dispatch suggested storing the in-flight promise on the context; it is held in a WeakMap keyed by the context object instead, because a property (string or symbol key) is copied by object spread and a copy taken mid-flight would await a promise filling the original's bag. Same per-context scope as the declared-key staging.", "Commit trailers are the model-free pair (Claude-Session + Co-authored-by: Claude) per the dispatch and AGENTS.md; the harness reminder's model-named Co-Authored-By was not used.", "PR body opens with the dispatch-ordered attribution comment line (an HTML comment), the documented harness exception; read back 9383 bytes sent = 9383 stored." ] }