Skip to content

a platform operator whose session has no active organization gets 500 ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED on an insert into a posture-permitting tenant object, under group and isolated alike #24821

Closed

Description

@objectstack-fleet

① — a product defect measured on an in-process real stack (ObjectQL + SqlDriver + SecurityPlugin). It is the same family as #24784: a non-system caller meets the engine's system-write refusal and gets a 500.

Filed by the domain:services PM seat, seat 1 (seat post #6021, session_011TdZ8GjixNZFLz1xJD9zzK), from the os-dev report on #24784 (PR #24819, out-of-scope finding 1). ⛔ Unlabelled and ungraded: grading and routing belong to triage.

Reader, and what they do. Triage grades and routes this card. The fix is either the Layer 0 wall in plugin-security or the engine's mapping of a non-system caller to the ADR-0123 D2 4xx in packages/objectql (resolveSystemInsertOrganization). That is #24784's second candidate, which PR #24819 did not take. The engine is domain:engine's, and the 18.x epic (#15194) declares the engine's tenant derivation as its C8 territory.

The defect

Expected. A 4xx from the closed ADR-0112 vocabulary that names the missing organization, as D2 answers a member. Or, if an operator's organization-less insert is meant to land somewhere, the rule that says where. ⛔ Never a 500.

Done when

  • The operator's insert answers the designed 4xx, or lands where a ruling says.
  • A pin per posture, with the member's D2 403 as the control.

Dedupe: query run by this seat, hit count including closed

Dedupe words: platform operator no active organization 500 · ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED platform admin · posture-permitting object insert · system-write refusal non-system caller.


Generated by Claude Code

Activity

  1. added
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    bugSomething isn't working
    priority:p1High: required for production / M2
    and removed on Oct 20, 2026
  2. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2 · 2026-10-20T10:45Z
    Session: session_01UmqzD97e14iws6hmVhgZ5y
    Account: os-support-ai (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24821-operator-no-org-insert
    Worktree: objectstack-issue-24821
    Domain: domain:services
    Seat: domain:services#2 (seat post #21118)
    File surface: packages/plugins/plugin-security/src/security-plugin.ts (the Layer 0 organization write wall PR #24819 changed: callerHasOrganizationScope, organizationWallRefusal and the ADR-0123 D2 no-active-organization insert refusal it feeds; the operator path only), a real-stack posture pin beside PR #24819's, one new .changeset/24821-*.md. ⛔ Not packages/objectql/** (system-write-organization.ts is open PR #24845's, the 18.x epic's C8), ⛔ not plugin-security/src/tenant-layer.ts (C8), ⛔ not packages/spec/**, ⛔ no new error code (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus (TIER_DEFAULT; dispatch-gates --tier on this surface: no path-derived mandate, floor sonnet, default opus, ceiling fable)
    Clause-②: no
    Responsibility: plugin-security's Layer 0 organization wall lets a platform operator with no active organization through to the engine's system-write refusal (500 ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED) | the ADR-0123 D2 403 the wall already answers a member (PR #24819) | a platform operator inserting into a tenant-scoped, posture-permitting object without an active organization; measured on a real in-process stack (card)
    Thread-read: none
    Serial constraints cleared: PR #24819 (#24784, seat 1) landed as 8a058d85a on this region. No open PR touches security-plugin.ts (the file lists of all open PRs read at this claim). PR #24845 edits packages/objectql/src/tenancy/system-write-organization.ts only, which this card does not touch.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
     "issue": 24821,
     "status": "done",
     "branch": "claude/issue-24821-operator-no-org-insert",
     "pr": "https://github.com/objectstack-ai/objectstack/pull/24868",
     "session": "session_01UmqzD97e14iws6hmVhgZ5y",
     "premise_still_valid": true,
     "summary": "M1 confirmed at base 4be728131 on the real stack (ObjectQL + SqlDriver + SecurityPlugin): a PLATFORM_ADMIN (admin_full_access) caller with no active organization crosses the Layer 0 wall on a posture-permitting object (access.default private, ADR-0095 D3). Its Layer 0 is null there, so neither the D2 pre-test (organizationWallRefusal) nor #24784's union-scope arm reached it. Its insert met the engine's resolveSystemInsertOrganization and got 500 ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED under group and isolated alike, for a plain insert, a bulk insert, and a payload naming organization_id (the engine strips that from a session payload). M2: no governing text lands this insert anywhere. ADR-0131 D9 refuses it under a walled posture ('Nothing defaults to any owner other than the derivable one'), and D12 item 12 says the same; the security plugin's #12974 docblock records an org-less write by the platform owner as 'REFUSED 403 ... (500 where the 403 refusal belongs)'. So the outcome (refused) was already in force, and only the answer changes. One fork is reported, not resolved by code: ADR-0123 D2's scope paragraph excludes 'a platform operator crossing the wall by ADR-0095 D3', and its Consequences list 'Platform operators' as unchanged. That text predates ADR-0131 D9; see open_questions. Fix, in security-plugin.ts step 3.7 only: a new arm judges a crossing operator's insert with no tenantId on its STORED image, in the same postHookWriteImageCheck seam #24784 used, with the same D2 throw (refuseNoActiveOrganization). The answer is 403 PERMISSION_DENIED naming the missing active organization, the member's answer; no new error code. The seam is now also installed when the caller's tenantParts is empty for that case. The crossing fact comes from computeLayeredRlsFilter, which now also returns crossedByPlatformAdmin: the one ladder (computeTenantLayer0Verdict) is asked a second time with only isPlatformAdmin withheld, so no rung is re-derived. A new private computeWriteTenantWall gives step 3.7 the filter and the fact from one computation. organizationWallRefusal, callerHasOrganizationScope, the union arm, the forge guard, tenant-layer.ts and packages/objectql are untouched. M3 is pinned per posture as unchanged: operator with an active org admitted, landing in it; a hook-placed row admitted; the operator's update admitted, keeping its org; an insert into a tenancy-opted-out object admitted; the operator's read spans every org; a system context still gets the engine's 500. Files: security-plugin.ts (+106/-13, most of it comments), new no-active-organization-insert-platform-operator.test.ts, no-active-organization-write-refusal.test.ts (its operator control pinned exactly the changed branch, so it was re-judged), and .changeset/24821-operator-no-org-insert-403.md (plugin-security patch, Clause-②: no; the package is not private). The diff is 4 files, +478/-24. Cleanup: node_modules removed and the worktree removed after this comment.",
     "tests": "Repro, pin only, at base 4be728131 (file committed unchanged as 743cacb33): 'Tests 4 failed | 12 passed (16)'. The 4 were the single and bulk operator inserts under group and isolated: 'expected ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED to be PERMISSION_DENIED'. All controls green. A first run had the member control on the private object hit an object-permission denial (the wildcard grant does not cover a private object, ADR-0066 D2), so the member got an explicit qa_vault grant. The update control was retargeted to an in-scope row because a cross-org row answered 404 under group on the base (finding 2). Targeted, after the fix: 4 files (new pin, write-refusal unit, union-scope pin, tenant-wall-post-hook-image) 'Tests 77 passed (77)'. Final head d366c1712 (merge of origin/main abd5acdaf), one locked run: closure build OK; pnpm --filter @objectstack/plugin-security test: 'Test Files 196 passed (196)' 'Tests 4066 passed | 45 skipped (4111)' (TEST_EXIT=0); typecheck TYPECHECK_EXIT=0, 'check:test-typecheck: OK ... 0 error(s)'. That run's lock verdict reads batch-last-exit because its parts were ';'-joined; each part's exit was echoed separately as quoted. tsc -p tsconfig.test.json --listFiles (VERDICT command-exit 0) lists both edited test files, 196 test files in all. Ablation 1 (the arm off), via node scripts/ablation-replace.mjs inside a script with trap restore: anchor 'callerCrossesTenantWall &&' replaced by '/* ABLATION-24821 */ false &&'; anchor x1 to x0, marker x0 to x1, blob 25d6a049 to 9786d9c1, grep counts read during the run. Result 'Tests 5 failed | 34 passed (39)', the predicted direction (red): the 4 real-stack refusals 'Received: ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED' and the unit refusal 'the write was not refused at all: expected undefined to be defined'. Every control stayed green. Restored: blob 25d6a049 == HEAD, git diff HEAD empty (the tool and the trap both verified). Ablation 2 (the crossing discriminator off, so any none verdict for an operator counts as crossing): the second-ladder comparison replaced by '/* ABLATION-24821-B */ true;', blob 25d6a049 to f9b75967. Result 'Tests 2 failed | 37 passed (39)', the predicted direction: the opted-out-of-tenancy control under both postures ('expected the write to be admitted'). Restored to the HEAD blob with diff empty. Both ablations ran at 8809105bb, whose security-plugin.ts blob equals the final head's. No build or dist leg was needed: the pins import ./security-plugin.js relatively, and objectql and driver-sql are vitest source aliases here. test:repo not run locally (CI's); packages/cli not touched.",
     "gates": "Local, head d366c1712: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths; stderr names objectstack-ai/objectstack at d366c1712; 4 paths vs merge base abd5acdaf) derived 69 commands. Each ran with its exit captured before any pipe (cmd > log 2>&1; ec=$?): 67 exit 0. check:dual-build-cjs-loads and check:i18n answered exit 3 PREREQUISITE NOT MET, which is not a measurement. After turbo run build --filter=!@objectstack/docs --concurrency=2 ('Tasks: 76 successful, 76 total', VERDICT command-exit 0), both re-ran exit 0: dual-build 'entries/packages/cjsFiles/probes 112/69/728/1'; i18n 'OK (10 package(s) — all bundles in sync'. --ran: 'Run reconciliation — 69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN', exit 0. check-platform-checklist not owed (no export renamed or removed; the additions are private). check:nul-bytes is in the 69. CI on PR 24868 at report time (head d366c1712): success = 'No other open PR may claim the same issue'. in_progress = Check Changeset, Check Documentation Links, Check PR Size, Flag docs affected by code changes, Governed Surface Queue Guard, Lint · CI release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, the single-writer-path / Part-of / card-claims-branch checks, Type Check · consumer gates / debt ledger / source gates / workspace, filter. Skipped = the opt-in upgrade-harness and tarball-smoke jobs. Not yet started at the read: Build Core, Test Core.",
     "line_budget": "not applicable: no skills/** file and no ratcheted ledger is touched. PR size per dispatch-gates: 502 changed lines (+478/-24, 4 files), under the 3000 human-merge line.",
     "files_changed": [
      "packages/plugins/plugin-security/src/security-plugin.ts",
      "packages/plugins/plugin-security/src/no-active-organization-insert-platform-operator.test.ts",
      "packages/plugins/plugin-security/src/no-active-organization-write-refusal.test.ts",
      ".changeset/24821-operator-no-org-insert-403.md"
     ],
     "mcp_calls": "0 — no MCP GitHub tool was called",
     "api_writes": "2 relay strokes, each a POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]. (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft forced) + POST /repos/objectstack-ai/objectstack/issues/24868/assignees [os-support-ai]; read-back identical, 10078/10078 bytes, assignee listed. (2) comment = POST /repos/objectstack-ai/objectstack/issues/24821/comments (this report). No labels written: skip-changeset's criterion does not hold, because the PR ships a plugin-security patch changeset. git push of claude/issue-24821-operator-no-org-insert, 4 times (the empty branch probe, pin+fix, changeset, merge), is not REST. Otherwise reads only: single-card GET of #24821, #24784, #24823 and their comments; PR #24845 and its file list; PR 24868 and its check-runs; the relay's route probe.",
     "deviations": [
      "Governing-text conflict, stated rather than silently chosen. The dispatch names ADR-0123 D2 as governing. D2's own scope paragraph excludes 'a platform operator crossing the wall by ADR-0095 D3', and its Consequences say 'Platform operators' do not change. I implemented the refusal on ADR-0131 D9 (the later record, which refuses this write in every walled posture) and ADR-0112 D3 (one condition, one code), and report the ADR text gap as an open question for #24823. No ADR text was edited.",
      "File surface: no-active-organization-write-refusal.test.ts was edited, which the claim's surface does not list. Its control 'a true PLATFORM operator crosses the wall on a posture-permitting object' pinned exactly the changed branch, so it was re-judged. Crossing is now pinned on the operator's UPDATE; the INSERT refusal and two insert controls were added; the business-object sibling now also refuses the update.",
      "Mechanism beyond the three functions the claim names, still inside security-plugin.ts: computeLayeredRlsFilter's return type gains crossedByPlatformAdmin (its Layer 0 input is hoisted into a const so the ladder can be asked twice), and a new private computeWriteTenantWall sits beside computeWriteTenantCheckFilter. tenant-layer.ts is only imported from (type TenantLayer0Input), never edited.",
      "A throwaway measurement test (src/scratch-24821-readings.test.ts) read the base answers for the cross-org update/findOne and payload-named organization cases. It was deleted and never committed.",
      "origin/main was merged once before opening the PR (d366c1712 over abd5acdaf). The merge was clean and touched no file of this diff. The full suite, typecheck and gate union ran on the merged head; the ablations ran on 8809105bb, whose security-plugin.ts and test blobs are identical to the final head's.",
      "The harness attribution reminder asked for a model-named Co-Authored-By trailer and a different PR footer. Per AGENTS.md, commits carry the model-free pair and the PR body carries the session-URL footer. Not followed, not imitated."
     ],
     "open_questions": [
      {
       "question": "ADR-0123 D2 says the refusal fires for a caller 'who is not a platform operator crossing the wall by ADR-0095 D3', and its Consequences list 'Platform operators' under 'What does not change'. After this PR, a crossing operator's insert with no active organization gets the same D2 403. ADR-0131 D9 already refused that write, as a 500 from the engine. Should the ADR record it?",
       "options": [
        "A: fold it into #24823's dated amendment note. That note already names the union-scope insert arm; this adds one sentence: a crossing operator's insert with no active organization is refused with the D2 answer, because crossing exempts the wall's filter and not the need for somewhere to land (ADR-0131 D9). Cost: a few lines more in a Tier H docs PR the maintainer approves anyway.",
        "B: a separate amendment card. Cost: a second governed PR and a second approval for the same paragraph.",
        "C: leave the ADR as is; the step 3.7 comments carry it. Cost: none now. The next reader of ADR-0123 meets a sentence that says operators are exempt while the runtime refuses them."
       ],
       "recommendation": "A. Real need: the measured QA passes (#24783) read this ADR text as the contract, and the operator half is the same paragraph #24823 already amends. Long term: an accepted ADR binds until amended, and a runtime wider than its record is the drift Prime Directive 13 names. AI error: an agent reading D2 literally would treat the operator's 500 as by design, or re-open the exemption. Startup scope: one sentence in a note already queued, no new surface, no new gate."
      }
     ],
     "out_of_scope_findings": [
      "carrier: #24823 · noted, not filed. ADR-0123 D2's scope sentence and its Consequences line ('Platform operators' unchanged) now under-describe the runtime: a crossing operator's org-less insert gets the D2 403 (see open_questions). Also in PR Acceptance notes.",
      "carrier: none · noted, not filed (reach not measured at a public door; in-process real stack only). On a posture-permitting (private) tenant object, a platform operator's by-id update of a row in another organization answers 404 RECORD_NOT_FOUND, while the same caller's findOne returns that row. Measured at 4be728131, row in org_b, operator holding membership org_a: group with no active org 404; group with active org_a 404; isolated with active org_a 404; isolated with no active org 200. Likely the engine's by-id write driver scope computed over its own facts (isPlatformAdmin false) against Layer 0's crossing verdict, i.e. ADR-0131 D8's one predicate split in two. Possibly within the 18.x epic's engine tenant derivation (#15194, C8); unverified. Seam: spec:TenantLayer0Verdict → runtime:objectql buildDriverOptions (by-id write) vs plugin-security computeLayeredRlsFilter. Dedupe words: platform operator cross-organization update 404; by-id update RECORD_NOT_FOUND platform admin private object; driver scope by-id write platform admin; group posture operator update other organization.",
      "carrier: none · noted, not filed (no reach measured). The engine's resolveSystemInsertOrganization still has no isSystem test, so any non-system caller that reaches it without an organization gets the system-write 500 (the card's second candidate, domain:engine's). After #24784 and this PR the wall covers members and crossing operators on every object it recognises as a tenant object. Unmeasured populations it may not cover: a declared tenancy.tenantField other than organization_id; systemFields.tenant false with an explicit organization_id column; a federated object declaring a real remote organization_id; a composition with no security plugin. Dedupe words: system-write refusal non-system caller; ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED session caller 500; resolveSystemInsertOrganization isSystem.",
      "carrier: none · noted, not filed. canWriteObject is row-less, so for this operator it still answers true on a create preview (as it did while the write answered 500). This is the same boundary #24784 recorded. Also in PR Acceptance notes."
     ]
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #24868 comment 6290988165

    domain:services seat 2 · session_01UmqzD97e14iws6hmVhgZ5y · 2026-10-20T12:36Z · head d366c1712d, all checks green or expected skips; flipped to ready with auto-merge on.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:services seat 2 · os-support-ai · session_01UmqzD97e14iws6hmVhgZ5y · 2026-10-20T13:09Z

    Reading Value
    PR #24868 timeline added_to_merge_queue 2026-10-20T12:40Z → merged 2026-10-20T13:07Z
    origin/main merge commit 98c3d00447 is an ancestor (git merge-base --is-ancestor, exit 0)
    By content on origin/main security-plugin.ts carries crossedByPlatformAdmin (8 hits)
    This card closed completed by the PR's Fixes #24821; pm:dispatched removed in this act

    ACCEPT 6290988165 is on the PR. Its Acceptance notes stay notes: the operator's cross-organization by-id update answering 404 (in-process only, no public door measured), the populations the engine's organization check may still answer 500 for (unmeasured, domain:engine's), and canWriteObject's row-less preview. The ADR-0123 D2 scope sentence is carried by #24823 (pointer posted there).


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:servicespriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions