Skip to content

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

@objectstack-fleet

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:services seat 1, #6021, session_011K3zqE8Pv1Evw5hc8tZCnN).

Part of #21908.

The route for every producer here: the explicit system opt-in that exists today (isSystem: true on the engine call's context). ⛔ No new elevation API. ⛔ No change to what any door authorizes.

The trap to measure, per producer: an isSystem context short-circuits the gates the hand-off still runs before next(): 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 on origin/main cab63967):

  • Row 17: plugin-auth src/auth-plugin.ts, the platform-admin OAuth client toggle route (sys_oauth_application on the raw engine; the platform-admin judge runs first).
  • Row 18: src/scim-connection-service.ts verifyScimBearerToken.
  • Row 19: src/auth-manager.ts, the organization-update hook's reads (sys_organization, sys_environment). ⚠️ Its catch returns early, so under a deny the slug guard would be skipped.
  • Row 20: the other raw-engine sites outside the adapter's withSystemContext wrapper: src/adopt-membership.ts, src/membership-ended-session.ts and the auth-manager.ts insert helper. Static; examine each.
  • Row 21: runtime src/http-dispatcher.ts, the environment-membership check (sys_environment_member). ⚠️ Its catch fails open, so under a deny the membership gate would open.

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 withSystemContext where 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 the isSystem census page is current.

Cross-lane: packages/runtime is a domain:cli package. The seat declares the edit on #6024 when it claims. ⛔ auth-manager.ts is held by hotlong's PR #21872 (#21846). Row 19's and row 20's auth-manager.ts edits wait for it to land, or are made after merging it.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3 · 2026-10-05T22:55Z
    Session: session_011K3zqE8Pv1Evw5hc8tZCnN
    Account: os-steve (the seat's linked user as GET /user answers 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 (at origin/main e6dc7a24), per the seat's verdict on #21908 (6003795556):


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    os-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."
    }

  3. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Review: PR #21939 at 9878b925fc · seat domain:services#1 (#6021) · session_011K3zqE8Pv1Evw5hc8tZCnN · 2026-10-06T02:40Z

    ACCEPT. 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.

    • The changeset is patch for plugin-auth and runtime, with Clause-②: no on 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, toggle 200/200/404, SCIM 401, slug update 200, member null, non-member 403).

    • Ablations: four, each red, each restored with blob equal to HEAD.

    • Serial: row 19's auth-manager.ts edit (c3147947) came after hotlong's PR fix(plugin-auth)!: implicit account linking requires the standard local-ownership condition; unlink is honoured #21872 merged (41a11351). The merge 7734aa2a contains it, so the order's H5 held.

    • Contract review: not owed. The PR is not governed (check-governed-merges), has no packages/spec source and declares Clause-②: 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 isSystem also 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 its where.

    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

  4. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · seat domain:services#1 · session_011K3zqE8Pv1Evw5hc8tZCnN · 2026-10-06T03:44Z. ⛔ Classes, positions and functions only.


    Generated by Claude Code

  5. added a commit that references this issue on Oct 7, 2026
    131b937
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:servicespriority:p1High: required for production / M2security

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions