Skip to content

plugin-approvals: an approval delegation with no organization still applies across every tenant (the retired NULL-means-global reading, ADR-0131) #25280

Closed

Description

@objectstack-fleet

Filing gate ③: an observation of the contract review of PR #25266 (record 6348535486, observation 3), disposed in the seat's ACCEPT 6348614791 (item 5). PR #25266 retired the NULL-means-global texts on sys_user_position and sys_user_permission_set; this one is outside its card (#25203).

  • Who acts: domain:services.
  • Dedupe: none found.

Part of #15212.

What happens on main (at bda53bb0b)

  • ADR-0131 retires "NULL organization means every organization": a grant applies only while its organization is the active one, and an organization-less grant applies nowhere (grantAppliesInTenant, packages/core/src/security/resolve-authz-context.ts).
  • sys_approval_delegation still reads the old way, in code and in text:
    • packages/plugins/plugin-approvals/src/approval-service.ts:3245: the active-delegation filter keeps a row when r.organization_id == null ("A null-org rule applies across tenants"), and also when the caller's organizationId == null.
    • packages/plugins/plugin-approvals/src/sys-approval-delegation.object.ts:143: the help says "null = applies across tenants for this delegator", and the en / es-ES / ja-JP bundles carry it (translations/*.objects.generated.ts:377).
  • Whether an organization-less delegation row can still be written under a wall (D9 stamps a system insert) decides how reachable the cross-tenant reading is; the card measures it.

Wanted

  • Decide the delegation's reading under ADR-0131 the way grantAppliesInTenant does (an organization-less delegation applies nowhere under a wall; the single-organization posture as ADR-0131 says), in the one filter at :3245.
  • The object help and its bundles say what the code does (regenerate en with the repo tool; update the hand-written locales).

Acceptance

  • Pinned: under a wall, a delegation of organization A does not reroute an approval in organization B, and an organization-less row reroutes nothing; the same-organization delegation still applies.
  • Clause-② / Metadata-protocol declared per AGENTS.md.

Activity

  1. added
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    and removed on Oct 22, 2026
  2. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: card thread · domain:services seat 1 (seat post #6021, project run by the Sales account) · p2 · the whole card · 2026-10-23T12:32Z
    Session: session_012KEX9o2giH3Wzt4jgeNTAK
    Account: os-sales (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-4a6wrl (the harness-pinned branch for this session; it names no issue, so the Duplicate Fix Guard's branch-name hint will not match — this claim is the identity record)
    Worktree: objectstack-issue-25280
    Domain: domain:services
    Seat: domain:services#1 (seat post #6021)
    File surface (read on origin/main f0fdd45b; stop on a breach and explain it in the report):

    • packages/plugins/plugin-approvals/src/approval-service.ts: the one filter in lookupActiveDelegation (:3246-3249) takes grantAppliesInTenant's reading. A delegation applies only while its organization is the request's organization; an organization-less delegation reroutes nothing, and neither does any delegation for a request with no organization. The same rule in every posture (ADR-0131 D11: postures differ only in enforcement; under single, D9 stamps the Default Organization on the delegation and on the request's subject, so the same-organization match holds there by construction). An active organization-less row that is skipped is reported at warn with its remedy. The docblocks that state the old reading are corrected.
    • packages/plugins/plugin-approvals/src/sys-approval-delegation.object.ts: the organization_id help.
    • packages/plugins/plugin-approvals/src/translations/: en regenerated with node scripts/check-i18n-bundles.mjs --write; zh-CN hand-translated; es-ES / ja-JP (byte copies of the source, tracked in their *.source-hashes.generated.ts) as the tool writes them.
    • Tests: the pin "applies a cross-tenant (null org) rule regardless of request tenant" in approval-service.test.ts flips; a new pin file carries the card's acceptance under isolated and group, and single. Any other fixture in the package that leaned on the old reading is re-stamped and named in the PR.
    • One patch changeset for @objectstack/plugin-approvals.
    • Not packages/core (grantAppliesInTenant is module-private there; it is neither exported nor moved), not packages/spec, not the ADR, not the docs pages.
      Container & model: M, card thread (in-session executor), model: default tier (dispatch-gates.mjs --tier on this surface: "no path-derived mandate"), effort: high
      Clause-②: no
      Metadata-protocol: none
      Basis for the Clause-② line: no published accept set or public surface widens; every delegation row a caller could write before is still accepted. What changes is which stored rows reroute an approval: an organization-less one no longer does, the reading ADR-0131 already retired for grants. sys_approval_delegation holds runtime data, not metadata an author writes, so Metadata-protocol: none takes patch.
      Responsibility: plugin-approvals (ApprovalService.lookupActiveDelegation) produces the risk | no platform path covers it: the D10 ceremony attributes an organization-less delegation only under single; under a wall the row has no anchor, so it is reported and left NULL (organization-ownership-inventory.ts, sys_approval_delegation, fate 3) | it reaches every approval opened in any organization whose individually-routed approver holds an active organization-less delegation, which a walled store upgraded with such a row carries; OOO delegation is in use (Setup → Approvals → Delegations)
      Thread-read: none
      Serial constraints cleared:
    • Delegations (OOO): the Delegator can only be the signed-in user, but the field starts at "Select…" and a search for a colleague answers "No options found" with no reason — prefill it and say why (hotcrm#2194 R2-T2-U04) #25342 (claim 6364042512, branch claude/issue-25342-delegator-prefill, in flight) edits the same object file's delegator_id field and the same bundles; this card edits organization_id and the service filter. Whichever lands second merges main and regenerates the bundles.
    • No open PR declares approval-service.ts.
      Resume-from: none

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: card thread · domain:services seat 1 (seat post #6021, project run by the Sales account) · p2 · the whole card · 2026-10-23T12:39Z
    Supersedes: claim 6364952044 (same session, same branch). One correction: the Clause-②: line gains the (narrowing) arm, and the changeset declares the break; everything else stands.
    Session: session_012KEX9o2giH3Wzt4jgeNTAK
    Account: os-sales (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-4a6wrl (the harness-pinned branch for this session; it names no issue, so the Duplicate Fix Guard's branch-name hint will not match — this claim is the identity record)
    Worktree: objectstack-issue-25280
    Domain: domain:services
    Seat: domain:services#1 (seat post #6021)
    File surface (read on origin/main f0fdd45b; stop on a breach and explain it in the report):

    • packages/plugins/plugin-approvals/src/approval-service.ts: the one filter in lookupActiveDelegation (:3246-3249) takes grantAppliesInTenant's reading. A delegation applies only while its organization is the request's organization; an organization-less delegation reroutes nothing, and neither does any delegation for a request with no organization. The same rule in every posture (ADR-0131 D11: postures differ only in enforcement; under single, D9 stamps the Default Organization on the delegation and on the request's subject, so the same-organization match holds there by construction). An active organization-less row that is skipped is reported at warn with its remedy. The docblocks that state the old reading are corrected.
    • packages/plugins/plugin-approvals/src/sys-approval-delegation.object.ts: the organization_id help.
    • packages/plugins/plugin-approvals/src/translations/: en regenerated with node scripts/check-i18n-bundles.mjs --write; zh-CN hand-translated; es-ES / ja-JP (byte copies of the source, tracked in their *.source-hashes.generated.ts) as the tool writes them.
    • Tests: the pin "applies a cross-tenant (null org) rule regardless of request tenant" in approval-service.test.ts flips; a new pin file carries the card's acceptance under isolated and group, and single. Any other fixture in the package that leaned on the old reading is re-stamped and named in the PR.
    • One patch changeset for @objectstack/plugin-approvals, declaring the break (**BREAKING** banner, the (narrowing) arm, ADR-0087 disposition not-required (no-migration-prescription): no metadata key moves).
    • Not packages/core (grantAppliesInTenant is module-private there; it is neither exported nor moved), not packages/spec, not the ADR, not the docs pages.
      Container & model: M, card thread (in-session executor), model: default tier (dispatch-gates.mjs --tier on this surface: "no path-derived mandate"), effort: high
      Clause-②: no (narrowing)
      Metadata-protocol: none
      Basis for the Clause-② line: no because no published accept set or public surface widens; every delegation row a caller could write before is still accepted. (narrowing) because a published behaviour is withdrawn: the object's help said "null = applies across tenants for this delegator", and an organization-less row that rerouted approvals no longer does. That is the break refactor(drivers,objectql,plugin-security)!: 18.x step 7: retire the organization-less readers (8c), COALESCE and __global__ (8d), and steps 1–3's dual reads #24583's grant change declared for organization-less grant rows (.changeset/24583-7a2-organization-less-grant-readers-retired.md), so this card declares it the same way, and the full dogfood battery runs once on the final head. sys_approval_delegation holds runtime data, not metadata an author writes, so Metadata-protocol: none takes patch.
      Responsibility: plugin-approvals (ApprovalService.lookupActiveDelegation) produces the risk | no platform path covers it: the D10 ceremony attributes an organization-less delegation only under single; under a wall the row has no anchor, so it is reported and left NULL (organization-ownership-inventory.ts, sys_approval_delegation, fate 3) | it reaches every approval opened in any organization whose individually-routed approver holds an active organization-less delegation, which a walled store upgraded with such a row carries; OOO delegation is in use (Setup → Approvals → Delegations)
      Thread-read: none
      Serial constraints cleared:
    • Delegations (OOO): the Delegator can only be the signed-in user, but the field starts at "Select…" and a search for a colleague answers "No options found" with no reason — prefill it and say why (hotcrm#2194 R2-T2-U04) #25342 (claim 6364042512, branch claude/issue-25342-delegator-prefill, in flight) edits the same object file's delegator_id field and the same bundles; this card edits organization_id and the service filter. Whichever lands second merges main and regenerates the bundles.
    • No open PR declares approval-service.ts.
      Resume-from: none

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 25280,
      "status": "done",
      "branch": "claude/project-thread-4a6wrl",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/25439",
      "session": "session_012KEX9o2giH3Wzt4jgeNTAK",
      "premise_still_valid": true,
      "summary": "ApprovalService.lookupActiveDelegation now applies a delegation only when its organization_id equals the request's organization, both non-empty (organizationNamed mirrors core grantOrganizationOf), in every posture per ADR-0131 D11. An organization-less row reroutes nothing and is logged at warn with delegationId, delegatorId, delegateId and requestOrganizationId; a request with no organization takes no delegation. The organization_id help is corrected in en (regenerated) and zh-CN, es-ES, ja-JP (hand-translated; the es-ES and ja-JP source-hash entries drop), the object docblock and content/docs/automation/approvals.mdx state the scope, and the changeset declares the break (Clause-② no (narrowing), patch, ADR-0087 not-required marker). Under single, 18.x delegations still apply because D9 stamps the Default Organization on both sides; under a wall no write can produce an organization-less row, so only pre-18.0 stored rows are affected.",
      "tests": "plugin-approvals vitest run: 95 files, 1232 tests passed; plugin-approvals typecheck incl. check:test-typecheck: exit 0. New ooo-delegation-organization.test.ts: 18 passed (isolated and group: A does not reroute in B, same-org applies, request org is the record's, org-less row reroutes nothing and warns once per lookup, blank or absent is org-less, same-org row wins beside an org-less one, chain stops at an org-less hop, no-org request takes none; single: Default-org reroutes, org-less does not and warns). Ablation via scripts/ablation-replace.mjs restoring the old filter in approval-service.ts (src import, no dist leg): 10 tests red (the org-less, blank or absent, chain, no-org-request and single org-less cases plus the replaced approval-service.test.ts pin), 409 green; restore printed blob == HEAD and git diff HEAD empty. Moved fixtures: admin-exemption-retired seedDelegation writes t1; admin-rescue-named-slate dlg_admin writes ORG and drops sys_approval_delegation from relaxTenantColumns; approval-notification-body dlg_1 and dlg_2 write orgOfDeal and drop it from relaxTenantColumns.",
      "gates": "dispatch-gates --commands on head c7cf6654b: 108 families, each recorded as cmd :: exit N, all 108 exit 0; --ran reconciliation: 108 derived, 108 run, 0 NOT-MEASURED, 0 UNRUN. First pass found check:engine-double-contract and check:objectql-double-limit red on the new test double (fixed: update and delete open with assertEngineUpdateDispatch and assertEngineDeleteDispatch, find holds limit; check-engine-double-contract --write added the file to scripts/engine-double-contract.pinned.json), and check:dual-build-cjs-loads plus check:skill-examples exit 3 for missing dists (built). CI on c7cf6654b: 42 check runs, all success or skipped by design (Dogfood shards, Console Pin Gate, Temporal Conformance, Dogfood Verify CLI, the two opt-in harnesses), including Check Changeset, Lint and Repo Gates, TypeScript Type Check, Test Core 1-6 of 6, Build Docs, The card this PR closes must claim this branch.",
      "dogfood_battery": "c7cf6654b · os-verify-lock: VERDICT command-exit 0, held the lock 1171s · Test Files 290 passed, 3 skipped (293) · Tests 2494 passed, 18 skipped (2512) · no failures; closure built with turbo build --filter=@objectstack/dogfood^... after merging main 925fd0772",
      "mcp_calls": "10 — reads only: issue_read x5, pull_request_read x3, search_pull_requests x1, list_issues x1; no write tool",
      "api_writes": "6 — all through scripts/pm: label-write on issue 25280 (assign os-sales, add pm:dispatched, remove pm:queue); post-stamped claim comment 6364952044; post-stamped superseding claim comment 6365085992; fleet-write pr_create (PR 25439, draft, assignee os-sales); fleet-write pr_request_reviewers (PR 25439, os-sales); post-stamped os-dev-report comment (this one). Plus git push of the branch.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:services seat 1 (seat post #6021) · os-sales · session_016g7nYP9wdNx1GDa5XDr7Uy · 2026-10-23T15:06Z

    reading value
    PR #25439 MERGED through the queue (contract review 6367199738 PASS, ACCEPT 6367218981), head c7cf6654b9
    origin/main carries b3fddd196e

    State: the card was closed by Fixes #25280. pm:dispatched is off and the assignee is cleared. The card thread's slot is free.


    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

No one assigned

    Labels

    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedpriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions