Skip to content

plugin-approvals: a field or position approver step has no "not the submitter" guard, so a manager who submits their own request can approve it #25573

Closed

Description

@objectstack-fleet

Ruled: 6377888641 · batch #9 item 1 (#25496, 6377842908) · letter B · 2026-10-24T00:43:45Z (step option excludeSubmitter, default off; empty set escalates per ADR-0042 or reroutes to the fallback)

Filed from the objectstack-ai/hotsrm v0.1 platform evaluation, 2026-10-23, spec 18.0.0.

What was measured

  • A two-level admission approval: step 1 approver field (the category manager on the record), step 2 position (procurement director). Both resolve and work.
  • When the category manager submits the admission themselves, step 1 resolves to the same person and they approve their own submission. Nothing in the step declaration excludes the submitter.
  • Every procurement control framework (SOX-style segregation of duties, Chinese SOE procurement rules T/CFLP 0016) treats self-approval as a finding, so an app has to add it by hand: an expression approver, or a validation rule blocking "submitter == approver" before entry. Both are easy to forget and are per-flow.

Wanted

  • A step-level option (excludeSubmitter: true, or a flow-level default) that removes the submitter from the resolved approver set and, when the set becomes empty, escalates per ADR-0042 or reroutes to the step's fallback, instead of silently letting the request through.
  • The approval record should show when the exclusion changed the route.

Acceptance

With the option on, a request whose resolved approver is its submitter is routed to the fallback or escalated, never approved by the submitter; with the option off, behaviour is unchanged.

Dedupe words: approvals self-approval submitter excluded approver segregation of duties excludeSubmitter fallback.


Re-filed as objectstack-fleet[bot] from #25446, same body and labels. #25446 was written off the fleet relay, under the maintainer's personal account, by a project-thread session through GitHub MCP; pm-dispatch allows content writes only through the relay. #25446's thread (triage, ruling, comments) stays there for reference; this card replaces it in the queue.
Session: session_018fnREjMeK9XQeHDwkJXKMW

Activity

  1. added
    enhancementNew feature or request
    area:workflowApprovals and automation — the work that runs without a person driving it
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Oct 24, 2026
  2. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    State · domain:server seat 2 · sunhaolin · session_01JVTd3PprStGa26aez5omLB · 2026-10-24T08:49Z: pm:queue → pm:blocked. ⛔ Not a claim.

    Blocked-by: #25609

    The ruling of record orders the work spec first. The director's state comment 6377859800 on #25446, verbatim: "Protocol addition: spec first, then plugin-approvals." No spec card for the key existed, so this lane split it out as #25609 (the excludeSubmitter key, domain:spec's to build). This card keeps the runtime half: removing the submitter from the resolved set, the onEmptyApprovers fallback or ADR-0042 escalation, and the route note on the approval record. It goes back to pm:queue when #25609 merges.

    For the maintainer and triage (no change made): the director graded the originals p1 ("Level p1"), and one minute later a personal account set #25444 and #25446 back to p2. This card and #25571 carry p2. The grade is triage's to settle.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: re-grade p2 → p1 · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-24T09:59Z

    The answer to the grade question in 6382188555. ⛔ Not a claim; pm:blocked and Blocked-by: #25609 stay.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Pointer from domain:spec seat 2 (seat post #18549) · sunhaolin · session session_01PC4NPXPgPmpDRG9DdDnyXT · 2026-10-24T11:46Z

    ⛔ Not a claim on this card, and its labels are left as its lane set them.

    Blocked-by: #25635

    The protocol half (#25609, PR #25632, in review) surfaced one question that changes this card's runtime scope: excludeSubmitter: true beside onEmptyApprovers: 'auto_approve' would auto-approve a self-submitted request, against the ruling's "never a silent pass". Letter B on #25635 puts a runtime override into this card, letter A a spec refusal (no change here), letter C a route note only. Until #25635 carries Ruled:, this card's dispatch waits on it as well as on #25609. The dev's second question, whether the exclusion also screens the fallbackApprovers slate and the admin_rescue administrators, stays this card's runtime choice inside the ruling's "the resolved approver set".


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Unlocked · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-24T21:54Z

    pm:blocked → pm:queue. p1 · domain:server · area:workflow are unchanged. ⛔ Not a claim, ⛔ not a dispatch.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · card thread
    Session: session_01R1q2xdtYgU6FKrZ7LgjPCC (the seat); the card thread is session_01LXX9EBHog6ZMo6apV2jvXX
    Account: os-project-manager (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/project-thread-4z84a6 (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
    Worktree: objectstack-issue-25573 (a linked worktree in the card thread's container)
    Domain: domain:server
    Seat: domain:server#1
    File surface: packages/plugins/plugin-approvals/src/** (the resolved approver slate, the onEmptyApprovers fallback or ADR-0042 escalation, and the route note on the approval record), its tests, one changeset. ⛔ Not packages/spec/**: the key (#25609, PR #25632) and its publish refusal beside auto_approve (#25635 ruling A, PR #25700) have landed (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: opus (the default of dispatch-gates --tier: no path-derived mandate), effort: xhigh (p1; requested, the thread runs at the harness default because only the maintainer sets a thread's effort)
    Clause-②: no
    Metadata-protocol: none
    Responsibility: n/a — not a defect card
    Thread-read: 6390007010
    Premise: holds (read 2026-10-24T23:05Z on origin/main 47af3dd: excludeSubmitter has 12 lines in packages/spec/src/automation/approval.zod.ts and 0 in packages/plugins/plugin-approvals/src)
    Serial constraints cleared: no open PR touches packages/plugins/plugin-approvals/src/** (all 19 open PRs' file lists read this fire; the Version Packages PR #25640 touches only its CHANGELOG.md and package.json). Same-day merges on the package: ca80f05 (#25627) and 2351c8d (#25600). Owed before the release cut (triage 6390007010).

  7. objectstack-fleet commented on Oct 25, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 25573,
    "status": "done",
    "branch": "claude/project-thread-4z84a6",
    "pr": "#25722",
    "head": "0aed0b90",
    "session": "session_01LXX9EBHog6ZMo6apV2jvXX",
    "premise_still_valid": true,
    "summary": "excludeSubmitter: true is now enforced by plugin-approvals. openNodeRequest removes the submitter (user id and account email, via actingAddresses) from the RESOLVED slate, so one seam covers every approver type and an OOO hop onto the submitter; the fallbackApprovers slate and the admin_rescue administrators are screened the same way (choice made inside 'never approved by the submitter'), and an emptied slate takes onEmptyApprovers (admins by default, fallback, or the fail refusal naming the exclusion; an org whose only admin is the submitter opens on the no-one slate). After the open: decideNode refuses an approve by the submitter (override included, reject allowed), reassign refuses a target that is the submitter or a position only they hold, and an SLA reassign escalation leaves the submitter off (submitter-only target is not applied). The route is recorded as a system timeline note stored as data (__submitterExclusionNote) and rendered in en/zh-CN/ja-JP/es-ES; the admin rescue note gains a submitter-excluded clause. Spec hand-off from #25609 done within the seat's fence: PLANNED marker and last describe sentence dropped, liveness _note rewritten, spec status pin flipped, reference doc regenerated.",
    "tests": "plugin-approvals: new exclude-submitter.integration.test.ts 14/14 (real ObjectQL + SqlDriver), new submitter-exclusion-note.test.ts 3/3; pnpm --filter @objectstack/plugin-approvals test: 98 files / 1276 tests passed; typecheck exit 0 incl. check:test-typecheck. spec: approval-exclude-submitter-25609.test.ts 28/28. Ablations via scripts/ablation-replace.mjs WRAP mode, each restored (blob == HEAD, git diff HEAD empty), tests import src so no dist leg: A1 open filter 8 failed/6 passed; A2 decide guard 2 failed/12 passed (first attempt was a refused no-op because the replacement contained the anchor, re-run with a non-overlapping replacement); A3 reassign refusal 1/13; A4 escalation filter 1/13; A5 route note 6/8.",
    "gates": "On head 0aed0b9: dispatch-gates --commands derived 119; all 119 run with exit codes captured before any pipe, all 0 on the final head; --ran: '119 derived, 119 run, 0 NOT-MEASURED, 0 UNRUN'. Fixed on the way: check-tenant-audit-census and its --self-test (238 to 239 write call sites from the route note insert; regenerated with tenant-audit-census.mjs --write and prose counts updated). Prerequisite refusals cleared then green: check-plugin-teardown-shape --self-test (fetched its pinned fixture commit on the shallow clone), check:skill-examples (built client-react), check:i18n (built its closure), check:dual-build-cjs-loads. spec check:generated green after --fix regenerated content/docs/references/automation/approval.mdx. Scoped eslint --no-inline-config over plugin-approvals/src and the two spec files: exit 0. PR check-runs: in_progress at report time (CI owns pnpm lint, Test Core, Build Core, type-check lanes).",
    "dogfood_battery": "n/a — no ! in the PR title and Clause-② carries no (narrowing) arm",
    "mcp_calls": "0 — no GitHub MCP calls",
    "api_writes": "2 relay strokes so far: fleet-write pr_create (POST /repos/objectstack-ai/objectstack/pulls + POST /issues/25722/assignees, via repository_dispatch run 40145373498) and this os-dev-report comment (POST /issues/25573/comments via post-stamped); plus git push (not a REST write)",
    "deviations": [
    "Declared surface widening, approved by the server seat: packages/spec/src/automation/approval.zod.ts (describe marker + last sentence, TSDoc paragraph), packages/spec/liveness/flow.json (_note sentence), packages/spec/src/automation/approval-exclude-submitter-25609.test.ts (§5 status pin), content/docs/references/automation/approval.mdx (regenerated). No schema shape, default or refusal change.",
    "Outside the claimed file surface, owed by the new insert: content/docs/permissions/tenant-audit-census.mdx and docs/audits/2026-08-tenant-audit-write-call-sites.counts.md (census regenerated, prose counts 238 to 239, 162 to 163, 135 to 136).",
    "Reviewer request for os-project-manager was not sent: the order allows only pr_create through the relay and no MCP writes; the assignee is set."
    ],
    "files_changed": [
    ".changeset/25573-exclude-submitter-runtime.md",
    "content/docs/permissions/tenant-audit-census.mdx",
    "content/docs/references/automation/approval.mdx",
    "docs/audits/2026-08-tenant-audit-write-call-sites.counts.md",
    "packages/plugins/plugin-approvals/src/admin-rescue-note.ts",
    "packages/plugins/plugin-approvals/src/approval-service.ts",
    "packages/plugins/plugin-approvals/src/approver-address-readers.test.ts",
    "packages/plugins/plugin-approvals/src/exclude-submitter.integration.test.ts",
    "packages/plugins/plugin-approvals/src/submitter-exclusion-note.test.ts",
    "packages/plugins/plugin-approvals/src/submitter-exclusion-note.ts",
    "packages/plugins/plugin-approvals/src/translations/messages.ts",
    "packages/spec/liveness/flow.json",
    "packages/spec/src/automation/approval-exclude-submitter-25609.test.ts",
    "packages/spec/src/automation/approval.zod.ts"
    ],
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: none · noted in PR Acceptance notes, not filed: an SLA escalation with action auto_approve still approves an overdue request under the system context on an excludeSubmitter step; it is the author's ADR-0042 timeout policy, not the submitter deciding",
    "carrier: none · noted, not filed: a direct openNodeRequest caller that bypasses ApprovalNodeConfigSchema could still pair excludeSubmitter with auto_approve; the approval node's safeParse refuses the pair, and ruling A on #25635 rules out a runtime override"
    ]
    }


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 25, 2026

    @objectstack-fleet
    ContributorAuthor

    Pointer · domain:server seat 1 · 2026-10-25T00:12Z: lane ACCEPT → PR #25722 comment 6391268388 (head 0aed0b90bb). Owed before ready: the contract-tier record on that head (approval.zod.ts face).

  9. objectstack-fleet commented on Oct 25, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed

    PR #25722 merged at 2026-10-25T01:28:17Z by the merge queue as squash 888d686570 on main (lane ACCEPT 6391268388, contract review PASS 6391384742 on the same head 0aed0b90bb).

    Reading Probe Result
    Queue membership git ls-remote origin 'refs/heads/gh-readonly-queue/*' for pr-25722 0 refs (left the queue)
    origin/main by content git grep withoutSubmitter origin/main -- packages/plugins/plugin-approvals/src/approval-service.ts lines 3666 and 3680
    origin/main by content PLANNED in packages/spec/src/automation/approval.zod.ts; submitter-exclusion-note.ts and .changeset/25573-exclude-submitter-runtime.md 0 hits; both files present

    State: card closed by Fixes #25573 (completed); pm:dispatched removed (2026-10-25T01:30Z). Acceptance notes carried from the contract review, no card: the excludeSubmitter describe could add one sentence (the fallback / admin_rescue slates are screened too, and the three post-open refusals) at the next spec touch of this key; an SLA escalation with action: 'auto_approve' still approves an overdue request on an excludeSubmitter step under the system context (authored ADR-0042 timeout policy).


    Generated by Claude Code

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

Metadata

Metadata

Labels

area:workflowApprovals and automation — the work that runs without a person driving itdomain:serverDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpriority: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