Repository navigation
security(plugin-auth, runtime): raw-engine reads and writes outside the adapter's system context reach the engine principal-less, two of them behind a fail-open catch — the identity and runtime producers of #21908's closure #21912
Description
Activity
- addedpriority:p1High: required for production / M2High: required for production / M2area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardsand removed
on Oct 5, 2026 objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsClaim: PM loop round 3 · 2026-10-05T22:55Z
Session:session_011K3zqE8Pv1Evw5hc8tZCnN
Account:os-steve(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-21912-principal-less-producers-identity
Worktree:objectstack-issue-21912
Domain:domain:services
Seat:domain:services#1(seat post #6021)
File surface (atorigin/maine6dc7a24), per the seat's verdict on #21908 (6003795556):packages/plugins/plugin-auth:src/auth-plugin.ts(row 17),src/scim-connection-service.ts(row 18),src/auth-manager.ts(rows 19 and 20),src/adopt-membership.tsandsrc/membership-ended-session.ts(row 20).packages/runtime/src/http-dispatcher.ts(row 21, the environment-membership read). Cross-lane on [PM seat] domain:cli — 🟢 os-elon-musk · session_01BmsuLyUeuG5CNpZFMH1jzS #6024 in this act.- Their unit tests, the
isSystemcensus page, and apatchchangeset per package.
⛔ No edit inplugin-security,packages/specorpackages/qa. ⛔ The pre-existing fail-open catches of rows 19 and 21 are reported, not changed, unless the move needs it. ⛔ The deny is security(spec, plugin-security): the AI tool contract says a context with no caller runs "RLS-on, sees-nothing", but plugin-security hands a principal-less context straight through, and on a hosted kernel it read and wrote more than a member may #21908's.
Container & model:M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate; default tier).
Clause-②: no - Each producer takes the explicit system opt-in that exists today. A producer on which a gate before the hand-off fires is reported, not moved, so nothing accepted or refused changes today.
Thread-read: 6003795556
Serial constraints cleared: at 2026-10-05T22:55Z: 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) holdsplugin-auth/src/auth-manager.ts, and has not landed. Theauth-manager.tsedits for rows 19 and 20 are made only after fix(plugin-auth)!: implicit account linking requires the standard local-ownership condition; unlink is honoured #21872 lands andorigin/mainis merged. This card's PR opens only when they are in.- This seat's PR fix(plugin-security): the packaged-permission-set lock refusal carries its guidance as userMessage #21902 edits
runtime/src/http-dispatcher.tsin a comment, in a different region. Each test-merges the other before landing.
Selection: the lane, read fresh at this pick, holds security(plugin-auth, runtime): raw-engine reads and writes outside the adapter's system context reach the engine principal-less, two of them behind a fail-open catch — the identity and runtime producers of #21908's closure #21912 (p1, this seat's slice of security(spec, plugin-security): the AI tool contract says a context with no caller runs "RLS-on, sees-nothing", but plugin-security hands a principal-less context straight through, and on a hosted kernel it read and wrote more than a member may #21908) and 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). This card takes the slot freed by fix(plugin-security): the packaged-permission-set lock refusal carries its guidance as userMessage #21902's base merge.
Generated by Claude Code
objectstack-fleet commented
on Oct 6, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21912,
"status": "done",
"branch": "claude/issue-21912-principal-less-producers-identity",
"pr": "#21939",
"session": "session_011K3zqE8Pv1Evw5hc8tZCnN — subagent of the PM session (harness Claude-Session line)",
"premise_still_valid": true,
"summary": "Four producers now carry the explicit system opt-in. Row 17, the platform-admin OAuth client toggle route, and row 19, the organization slug guard, go through plugin-auth's withSystemContext. Row 18, verifyScimBearerToken, passes isSystem in the read's trailing options. Row 21, the dispatcher's enforceProjectMembership, passes isSystem as the read's query context. Row 20 moves nothing: adoptExistingMembership, endSessionClaimsForEndedMembership and settleSelfRegistrationGrant already run with the opt-in. A local probe at the hand-off recorded 0 gate refusals in every run. The card's functions appear before the change and not after, with identical door answers. The pre-existing fail-open catches of rows 19 and 21 are pinned unchanged, and so is the rule that each guard keeps refusing on an engine that refuses a principal-less context. The row-19 auth-manager.ts edit (commit c314794, 00:31:02Z) came after #21872 merged (00:23:53Z), on merge 7734aa2, which contains it (is-ancestor exit 0, control leg exit 0).",
"tests": "Final head 9878b92. Pins: plugin-auth 2 files 9/9, runtime 1 file 5/5. runtime suite (vitest --project local --maxWorkers=2): 330 files, 4654 passed, 19 skipped. runtime typecheck: exit 0. plugin-auth suite at 60c5f22: 126 files, 2607 passed, 10 skipped; plugin-auth typecheck exit 0 (the only later commit touches runtime and the changeset). The runtime suite first went red: 8 existing toHaveBeenCalledWith assertions read the membership find's two arguments, and the trailing-options spelling added a third. Fixed by carrying the context in the query (9878b92); both suites now pass unedited. Ablations through scripts/ablation-replace.mjs: each anchor hit x1, the mutation landed on disk, the restore was proven (blob == HEAD, git diff HEAD empty), and each pin imports its subject from src. A row17 drop withSystemContext: 2 red. B row18 drop trailing context: 2 red. C row21 drop query context, re-run on 9878b92: 3 red, including the refusing-engine non-member case. D row19 drop withSystemContext: 2 red, including the refusing-engine case. eslint --no-inline-config over the 7 changed .ts files: 7 files, 0 errors, 0 warnings. That set is the whole population this diff can move: eslint.config.mjs never enables type-aware linting, so no untouched file's verdict can change.",
"mcp_calls": "0",
"api_writes": "3 REST writes, all via the fleet relay as objectstack-fleet[bot]: (1) pr_create POST /repos/objectstack-ai/objectstack/pulls, relay run 37404683630, body read back identical (11249 bytes); (2) label-write POST /repos//issues/21939/assignees (os-steve), relay run 37404747282, read back MATCHES; (3) this os-dev-report comment, POST /repos//issues/21912/comments via scripts/pm/post-stamped.mjs. Each relay leg is one POST /repos/{board}/dispatches. Plus git pushes, which are not REST.",
"open_questions": [],
"producers_moved": [
"row 17 · plugin-auth src/auth-plugin.ts · POST /admin/oauth2/toggle-disabled handler · findOne + update sys_oauth_application · withSystemContext",
"row 18 · plugin-auth src/scim-connection-service.ts · verifyScimBearerToken · findOne sys_scim_connection_credential · isSystem in the trailing options",
"row 19 · plugin-auth src/auth-manager.ts · organizationHooks.beforeUpdateOrganization · findOne sys_organization + find sys_environment · withSystemContext",
"row 21 · runtime src/http-dispatcher.ts · enforceProjectMembership · find sys_environment_member · isSystem as the query context"
],
"producers_not_moved": [
"row 20 · adopt-membership.ts adoptExistingMembership: its only caller (objectql-adapter create) passes the withSystemContext engine",
"row 20 · membership-ended-session.ts endSessionClaimsForEndedMembership: all four engine calls pass { context: SYSTEM_CTX } (isSystem: true) as the trailing options, which ObjectQL honours on reads and writes",
"row 20 · auth-manager.ts insert helper settleSelfRegistrationGrant, with findPermissionSetRows: reads and writes through withSystemReadContext, the deprecated alias of withSystemContext"
],
"producers_reported": "none. No gate fired on any producer. Static: each of the six gates is keyed to objects and verbs these calls do not touch, or (engine-owned) needs a userId. Instrumented: 0 gate-refused records across all runs.",
"private_producers": "0. No producer found that reads or writes beyond what its caller may.",
"instrument_counts": {
"method": "local uncommitted probe in plugin-security security-plugin.ts: hand-off records plus gate-refusal records, with stacks, source-mapped; analysed by the innermost engine-call frame",
"row17_toggle_route": {"dogfood_subset": "5 -> 0 (findOne 3, update 2)", "dev_boot": "5 -> 0"},
"row18_verifyScimBearerToken": {"dogfood_subset": "0 -> 0", "dev_boot": "1 -> 0"},
"row19_beforeUpdateOrganization": {"dogfood_subset": "0 -> 0", "dev_boot": "1 -> 0 (sys_organization findOne; the sys_environment read throws in OSS before the hand-off because the object is not registered)"},
"row20_functions": "0 -> 0 in every run",
"row21_enforceProjectMembership": {"runtime_harness": "2 -> 0"},
"totals": {"dogfood_subset": "1601 -> 1596", "dev_boot": "300 -> 293", "runtime_harness": "39 -> 37"},
"gate_refused": "0 in all six runs (before and after, x3)",
"answers_unchanged": "dev boot: register 201, toggle 200/200/404, SCIM 401, org slug update 200; harness: member null, non-member 403; dogfood subset 7 files 57 tests green both times",
"restore": "probe reverted via ablation-replace --restore (security-plugin.ts blob 5b4ab280 == HEAD, git diff HEAD empty); plugin-security rebuilt; ablation-dist-preflight --absent OK (positive control 4 hits in dist while live); scratch harness deleted"
},
"out_of_scope_findings": [
"class: a · reach: exception: security — a fault on the membership read lets a signed-in non-member through an environment-scoped door. Repro: runtime pin 'a read that throws lets the request through (null)'. · evidence: runtime http-dispatcher.ts enforceProjectMembership catch returns null; the code documents it as deferred; pinned unchanged per H7. This PR removes the principal-less-deny trigger, but any other read fault still opens the gate. · carrier: the seat's closure #21908, to sequence before the deny · dedupe words: membership check fail-open, environment member read fault, PROJECT_MEMBERSHIP_REQUIRED open on error",
"class: a · reach: named producer: the organization-update door (org owner/admin), on a composition that registers sys_environment. A read fault skips the slug guard. Repro: the plugin-auth pins 'an organization read / environment read that throws ends the hook without refusing'. In the open-source composition the environment read always throws (the object is not registered), so the guard never refuses there; measured on the dev boot as slug update 200 with no environment read at the hand-off. · evidence: auth-manager.ts beforeUpdateOrganization, both catches return; pinned unchanged per H7 · carrier: the seat's closure #21908 · dedupe words: slug guard fail-open, beforeUpdateOrganization catch, sys_environment read fault",
"carrier: PR #21939 Acceptance notes · noted, not filed: row 17 non-gate difference. Under the hand-off, the static read-only strip dropped the route's supplied updated_at with a WARN; under isSystem the strip does not run. Measured on the SQL driver: both paths store the same row (disabled, plus updated_at = the driver's own stamp). Only the WARN line disappears.",
"carrier: PR #21939 Acceptance notes · NOT MEASURED: a cloud composition. isSystem also bypasses host read hooks keyed on the caller, such as a control-plane org-scope hook. Only the six named gates were measured, and only in-repo.",
"carrier: none (承接者:无) · Acceptance notes only: mintScimConnectionCredential inserts without the opt-in. It has no runtime caller (tests only) and is not on the package entry, so the pull is zero."
],
"gates": [
{"command": "git push -u origin claude/issue-21912-principal-less-producers-identity (empty-branch write probe)", "exit": 0},
{"command": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 9878b92: 98 commands, each run, exit codes in scratchpad issue-21912/gates-final2/exits.tsv", "exit": "0 x98"},
{"command": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran gates-final2/ran-coded.txt", "exit": 0, "verdict": "98 derived famil(ies) accounted for — 98 run, 0 NOT-MEASURED (a DERIVED zero — all 98 recorded an exit code and none of them is 3)"},
{"command": "same 98 at 60c5f22 (pre-fix head)", "exit": "0 x98"},
{"command": "os-verify-lock: vitest pins plugin-auth (2 files) + runtime (1 file) at 9878b92", "exit": "0, 0"},
{"command": "os-verify-lock: pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2 at 9878b92", "exit": 0},
{"command": "os-verify-lock: pnpm --filter @objectstack/runtime run typecheck at 9878b92", "exit": 0},
{"command": "os-verify-lock: pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 at 60c5f22", "exit": 0},
{"command": "os-verify-lock: pnpm --filter @objectstack/plugin-auth run typecheck at 60c5f22", "exit": 0},
{"command": "os-verify-lock: ablations A,B,D at f922118 and C at 9878b92 (ablation-replace WRAP)", "exit": "pin red as expected each; restore proven each"},
{"command": "os-verify-lock: instrumented runs (dogfood subset, dev boot, runtime harness) before and after", "exit": "0 all"},
{"command": "node scripts/check-system-context-census.mjs (--fix for counts, then check)", "exit": 0},
{"command": "eslint --no-inline-config --format json over the 7 changed .ts files", "exit": 0}
],
"line_budget": "n/a",
"deviations": [
"Dogfood instrument runs used a 7-file subset that reaches the card's functions, not the whole suite. The measure-first full-suite census at cab6396 shows only row 17 (5 records) for these rows, and the subset reproduces it exactly. The full suite is CI's Dogfood Regression Gate. The after-run used the pre-merge dist (4239dd4), whose row 17 code is identical.",
"Row 21 was measured through a scratch harness in packages/runtime (uncommitted, deleted): no open-source composition reaches it.",
"H4 measured false for my first spelling of row 21: the trailing options turned 8 existing runtime assertions red. I changed the spelling to the query context (9878b92) instead of editing those suites.",
"plugin-auth suite and typecheck were taken at 60c5f22, not 9878b92. The delta touches runtime and the changeset only.",
"One locked run was cut mid-ablation by my own timeout wrapper. The tree was verified clean (git status and git diff HEAD empty) and every leg was re-run.",
"Container restart around 01:15Z: the runtime suite was in flight, so it read NOT MEASURED, and it was re-run on the final head. Two checkpoint-log stamps written before then were estimates, corrected in the log.",
"The branch is 3 commits behind origin/main (9dce635) and was not re-merged: those commits touch sso.mdx and a rest test, none of the diff's files.",
"Labels: none written beyond the PR assignee. The dispatch named none, and a changeset is present, so skip-changeset does not apply. size/l and documentation/tests/tooling came from labelers.",
"Commit trailers use AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude), not the harness's model-named form."
],
"files_changed": [
".changeset/21912-principal-less-producers.md",
"content/docs/permissions/system-context.mdx",
"packages/plugins/plugin-auth/src/auth-manager.ts",
"packages/plugins/plugin-auth/src/auth-manager.org-slug-guard-system-context.test.ts",
"packages/plugins/plugin-auth/src/auth-plugin.ts",
"packages/plugins/plugin-auth/src/principal-less-producers-system-context.test.ts",
"packages/plugins/plugin-auth/src/scim-connection-service.ts",
"packages/runtime/src/http-dispatcher.membership-system-context.test.ts",
"packages/runtime/src/http-dispatcher.ts"
],
"resume": "Found after the restart: the worktree was clean at f922118, equal to the remote head. There was no PR, no report, and no probe left in src or dist. #21872 had merged (00:23:53Z, squash 41a1135). auth-manager.ts was first edited in c314794 at 00:31:02Z, on merge 7734aa2 (00:29:26Z), which contains #21872; is-ancestor exit 0, control leg exit 0. So no H5 breach. In flight at the restart: the runtime suite, which read NOT MEASURED and was re-run. Re-ran: a merge of origin/main faf8dce (60c5f22), the full build, pins, the plugin-auth suite and typecheck, the runtime suite (red, then the spelling fix, then green), the runtime typecheck, ablation C, the instrumented dev boot and harness on the final tree (rows 17, 18, 19 and 21 at 0), and the gate battery twice (98/98). Then pr_create, the assignee, and this report."
}objectstack-fleet commented
on Oct 6, 2026 ContributorAuthorMore actionsReview: PR #21939 at
9878b925fc· seatdomain:services#1(#6021) ·session_011K3zqE8Pv1Evw5hc8tZCnN· 2026-10-06T02:40ZACCEPT. Verdict on the dev report
6008178930. Checked against the PR, not the report:-
What moved. Four producers now carry the explicit system opt-in:
- row 17, the platform-admin OAuth client toggle route, through
withSystemContext; - row 18,
verifyScimBearerToken, with the trailing options; - row 19, the organization slug guard, through
withSystemContext; - row 21, the environment-membership read, as the query context. That spelling was chosen so the existing runtime suites pass unedited.
Row 20 moved nothing: each of its three functions was read and already runs with the opt-in.
- row 17, the platform-admin OAuth client toggle route, through
-
The changeset is
patchforplugin-authandruntime, withClause-②: noon PR line 2. No exported type changes (checked on the diff). -
The trap was measured: 0 gate refusals before and after, and identical door answers (register
201, toggle200/200/404, SCIM401, slug update200, membernull, non-member403). -
Ablations: four, each red, each restored with blob equal to HEAD.
-
Serial: row 19's
auth-manager.tsedit (c3147947) came afterhotlong's PR fix(plugin-auth)!: implicit account linking requires the standard local-ownership condition; unlink is honoured #21872 merged (41a11351). The merge7734aa2acontains it, so the order's H5 held. -
Contract review: not owed. The PR is not governed (
check-governed-merges), has nopackages/specsource and declaresClause-②: no. -
Accepted, from the PR's Acceptance notes:
- under
isSystem, row 17's static read-only strip no longer runs; the stored row is identical and only a WARN line goes; - a cloud composition, where
isSystemalso bypasses host read hooks keyed on the caller, is NOT MEASURED. Row 21's read is already scoped to the caller's own membership by itswhere.
- under
Filed: the two pre-existing fail-open catches the build pinned unchanged (rows 19 and 21 swallow a read fault and let the request through) are now #21941, for triage. This slice moved both reads to the opt-in, so #21908's deny no longer trips them. Any other read fault still does.
Generated by Claude Code
-
objectstack-fleet commented
on Oct 6, 2026 ContributorAuthorMore actionsLanded · seat
domain:services#1·session_011K3zqE8Pv1Evw5hc8tZCnN· 2026-10-06T03:44Z. ⛔ Classes, positions and functions only.- PR fix(plugin-auth, runtime): four principal-less producers take the explicit system opt-in #21939 merged through the merge queue as
131b937a, which is an ancestor oforigin/main(verified withgit merge-base --is-ancestorafter a fetch). Merged at 2026-10-06T03:43Z. The head that landed is9878b925fc. CI was all green on it,Lint & Repo Gatesincluded, so a base-merge round the seat had started was stopped before it pushed anything. Fixes #21912closed this cardcompleted. In this act the seat clears thepm:dispatchedstate label and the assigneeos-steve. Thedomain:services,area:access,securityandpriority:p1labels stay.- What shipped: four producers of security(spec, plugin-security): the AI tool contract says a context with no caller runs "RLS-on, sees-nothing", but plugin-security hands a principal-less context straight through, and on a hosted kernel it read and wrote more than a member may #21908's closure now carry the explicit system opt-in: the platform-admin OAuth client toggle route,
verifyScimBearerToken, the organization slug guard, and the environment-membership read. Row 20's functions already ran with it. Door answers are unchanged, and 0 gate refusals were measured. The changeset ispatchforplugin-authandruntime. - Filed from this card: finding(runtime, plugin-auth): two access guards fail open when their own read faults — the environment-membership gate and the organization slug guard #21941, the two guards that fail open on their own read fault. The deny on security(spec, plugin-security): the AI tool contract says a context with no caller runs "RLS-on, sees-nothing", but plugin-security hands a principal-less context straight through, and on a hosted kernel it read and wrote more than a member may #21908 no longer trips them; a read fault still does.
Generated by Claude Code
- PR fix(plugin-auth, runtime): four principal-less producers take the explicit system opt-in #21939 merged through the merge queue as
- added a commit that references this issue
on Oct 7, 2026
Why now. #21908 closes the principal-less hand-off in the security middleware: a non-system engine context with no principal will be denied (
403 PERMISSION_DENIED, the seat's verdict on #21908). The measure-first round (6003676228) found the producers that reach the hand-off today. Each must take a route before the deny lands, or the deny breaks it. This card is one slice, a seat-owned sub-issue of #21908 with its domain and priority (domain:servicesseat 1, #6021,session_011K3zqE8Pv1Evw5hc8tZCnN).Part of #21908.
The route for every producer here: the explicit system opt-in that exists today (
isSystem: trueon the engine call's context). ⛔ No new elevation API. ⛔ No change to what any door authorizes.The trap to measure, per producer: an
isSystemcontext short-circuits the gates the hand-off still runs beforenext(): package-managed, system-row, curated-capability, audience-anchor, engine-owned, and the delegated-administration gate. Moving a producer is neutral today only if none of those gates fires on its calls. Measure that per producer (an instrumented run is fine; commit nothing of it). A producer on which a gate fires is reported, not moved.The producers (rows of
6003676228, positions onorigin/maincab63967):plugin-authsrc/auth-plugin.ts, the platform-admin OAuth client toggle route (sys_oauth_applicationon the raw engine; the platform-admin judge runs first).src/scim-connection-service.tsverifyScimBearerToken.src/auth-manager.ts, the organization-update hook's reads (sys_organization,sys_environment).withSystemContextwrapper:src/adopt-membership.ts,src/membership-ended-session.tsand theauth-manager.tsinsert helper. Static; examine each.runtimesrc/http-dispatcher.ts, the environment-membership check (sys_environment_member).Ordering: rows 19 and 21 are why the deny lands last. This slice moves them first. Their fail-open catches are pre-existing: row 21's is documented as deferred. This slice reports them and does not change them, unless the move needs it.
Done when: each producer above passes the explicit system opt-in (plugin-auth's own
withSystemContextwhere it applies), or is reported with the gate that fires on it. An instrumented run records no principal-less context from these functions, and a pin shows rows 19 and 21 keep their guard when the engine call is refused. The packages' suites are unchanged, and theisSystemcensus page is current.Cross-lane:
packages/runtimeis adomain:clipackage. The seat declares the edit on #6024 when it claims. ⛔auth-manager.tsis held byhotlong's PR #21872 (#21846). Row 19's and row 20'sauth-manager.tsedits wait for it to land, or are made after merging it.Generated by Claude Code