Skip to content

[Decision] An approval step with excludeSubmitter and onEmptyApprovers auto_approve would auto-approve a self-submitted request: refuse the pair at publish (A), override at runtime (B), or allow it (C)? #25635

Closed

Description

@objectstack-fleet

Ruled: 6386627950 · batch #12 item 1 · letter A · 2026-10-24T16:24:48Z (refuse excludeSubmitter: true + onEmptyApprovers: 'auto_approve' at publish; must land before the next release that carries excludeSubmitter)

Filing gate ② — a decision only the maintainer can make, raised as an open question by the dev on #25609 (report 6383750916) and confirmed by the domain:spec seat 2 review of PR #25632 (6383784354). Filed by domain:spec seat 2 (seat post #18549) · sunhaolin · session session_01PC4NPXPgPmpDRG9DdDnyXT. ⛔ Not a claim. Domain and grade are triage's; the seat suggests domain:spec, p1 (it decides the contract of a p1 protocol addition before its first release).

Reader who acts: the director seat presents it; after the ruling, the domain:spec seat (letter A, a small spec PR) or the domain:server seat on #25573 (letter B).

维护者速读

  • 事由: 审批步骤的「排除提交人」开关(excludeSubmitter,spec(automation): an approval step's excludeSubmitter option, default off — the protocol half of #25573 (ruling 6377731715, letter B) #25609 已加键、默认关)把提交人从审批人名单里去掉后,如果名单空了,按裁决走该节点的「名单为空」策略,且「绝不静默放行」。但这个策略本身有一个值叫「自动通过」(auto_approve)。两者同时写,结果就是:自己提交、自己是唯一审批人的单子,被自动通过 —— 正好是这个开关要防的事。
  • 要你定: A 发布时直接拒绝这种组合(推荐);B 运行时遇到这种情况改走升级,不按「自动通过」办;C 允许,审批记录里写一笔就算。
  • 为什么现在定: 这个键还没进任何发行版。现在选 A 不收窄任何已发布的东西;等下一次发版后再加拒绝,就是一次收窄。

Background

Ruling 6377731715 on #25496 (batch #388 item 4, the maintainer's 「同意」), item 5, verbatim: "#25446 → B, default off. A step-level option excludeSubmitter: true removes the submitter from the resolved approver set; an emptied set takes the node's existing onEmptyApprovers fallback (else ADR-0042 escalation), never a silent pass; the approval record shows when the exclusion changed the route." The later governing ruling 6377842908 on the same row: 「B 步骤选项 excludeSubmitter,默认关;排除后无人可批按 ADR-0042 升级」. #25609 (PR #25632, in review) adds the key as planned; #25573 builds the runtime.

onEmptyApprovers is z.enum(['admin_rescue', 'fail', 'auto_approve', 'fallback']) (packages/spec/src/automation/approval.zod.ts, about :951 on origin/main). Its own docblock says auto_approve "silently waves the record through" (about :933–:935).

Governing text: the ruling's "never a silent pass"; AGENTS.md Prime Directive 12 ("reject it at authoring/publish (validation / lint) so the error surfaces loudly — ⛔ never add a lenient alias or ?? fallback in a consumer"); the module's own strictness rule in checkFallbackApproversPairing's docblock ("silently ignores it, which is exactly the failure this module's .strict() shapes exist to make audible"). C conflicts with the ruling's words. B keeps the authored auto_approve but overrides it at runtime, the consumer-side shape Prime Directive 12 warns against. So the governing text leans to A. It is still raised because A adds a refusal to the contract, which the seat does not decide.

Protocol: A changes the protocol (a parse-time refusal of one pair on an approval node); B and C do not change the spec.

Premises, each with its re-check

  • excludeSubmitter is unreleased: git grep -c excludeSubmitter origin/main -- packages/spec/src → 0 until PR feat(spec): an approval step's excludeSubmitter option, default off (protocol half of #25573) #25632 merges; control git grep -c onEmptyApprovers origin/main -- packages/spec/src/automation/approval.zod.ts → > 0. After the merge, check the next release's CHANGELOG.md for the key: absent ⇒ still unreleased.
  • auto_approve is a live value: git grep -n "'auto_approve'" origin/main -- packages/spec/src/automation/approval.zod.ts → the enum line; control git grep -n "'admin_rescue'" origin/main -- packages/spec/src/automation/approval.zod.ts.
  • No producer writes excludeSubmitter today (nothing to migrate): git grep -n excludeSubmitter origin/main -- examples packages/platform-objects → 0; control git grep -n onEmptyApprovers origin/main -- examples packages/plugins → > 0.

The question

When an approval node authors excludeSubmitter: true and onEmptyApprovers: 'auto_approve', and the exclusion leaves nobody, what happens?

Options

Letter What is done What the customer sees Workload
A The spec refuses the pair when the flow is published (one more refinement arm on the approval node, in the shape of checkFallbackApproversPairing), with a prescription naming both keys. An author who writes both is told at once, before deploying, to pick admin_rescue, fail or fallback. A self-submitted request is never waved through. domain:spec · 1 PR · no precedent for the arm alone. The nearest is #18525 (the fallback policy and its pairing refinement, +711/−47 including the runtime).
B The runtime (#25573) sends an exclusion-emptied slate to escalation (admin_rescue / ADR-0042) instead of auto-approving, and notes it on the record. The author's auto_approve is quietly not honoured in this one case; they learn it from the approval record after the fact. domain:server · inside #25573 · no precedent.
C Accept the pair as authored; the approval record notes that the exclusion emptied the slate and the request was auto-approved. A manager who submits their own request and is its only approver gets it approved automatically, with a note. 0

Business meaning: A is a form that will not let you sign off on a policy that contradicts itself. B is a clerk who quietly does something other than the written rule. C writes the self-approval into the audit log and lets it through.

Four axes

  • Long-term (leads): A makes the ruled "never a silent pass" true by construction, in the contract both ends read. B leaves a declared policy that is partly overridden at runtime, a special case every reader of the config has to know. C contradicts the ruling.
  • Real business need: the pull is segregation of duties for procurement approvals (hotsrm; SOX-style control frameworks treat self-approval as a finding). Today nobody writes excludeSubmitter, so A refuses nothing anyone has.
  • Preventing AI mistakes: under A, an AI that writes both keys gets a loud refusal with the fix. Under B, it gets a config that looks valid and behaves otherwise. Under C, the control it meant to add silently has a hole.
  • Startup focus: A adds no key, only one refusal in one pair. B adds a runtime special case. C costs nothing now and leaves a control gap.

Recommendation: A. Two years out, an approval-control key on a mainstream platform (Salesforce approval processes, Workday business processes) refuses a self-contradicting configuration at save time rather than reinterpreting it at run time. Confidence gap: whether any other onEmptyApprovers value should also be refused beside excludeSubmitter (fail kills the run, which is loud, so it is not proposed). Not B: it overrides authored config at runtime. Not C: it contradicts the ruling.

Self-check: looking at ① alone, A. Do ②③④ flip it? No.

After the ruling

os-decision-facets

  • ① 项目长远合理性:A 在契约里把「绝不静默放行」做成结构上成立,两端读同一条;B 让一个已声明的策略在运行时被部分改写,多出一个人人要记的特例;C 与裁决原话相悖。
  • ② 实际业务拉动:拉动是采购审批的职责分离(hotsrm;SOX 类内控把「自己批自己」记为问题)。今天没有任何地方写 excludeSubmitter,A 不会拒绝任何现有配置。
  • ③ 防 AI 犯错:A 下 AI 同时写两个键会立刻被响亮拒绝并给出改法;B 下配置看着合法、实际另有行为;C 下 AI 想加的控制静默地留了个洞。
  • ④ 创业阶段不扩散:A 不加键,只在一对组合上加一条拒绝;B 多一个运行时特例;C 眼下零成本但留下内控缺口。

Prior rulings read: "excludeSubmitter auto_approve onEmptyApprovers silent pass" → 2 hits (6377731715, 6377842908 on #25496; reconciliation 6377965040); ADR-0042 (escalation); thread: none (new card)

Dedupe

The titles of objectstack-ai/objectstack issues updated since 2026-10-20 (132, open and closed, through the repo-scoped issues endpoint; the search API is refused here), matched on excludeSubmitter, auto_approve, auto-approve, self-approv, submitter: 3 hits, all this question's lineage (#25609, #25573, #25446). Control word approv: 10 hits. New.

Activity

  1. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    area:workflowApprovals and automation — the work that runs without a person driving it
    domain:specDomain lane — seat card indexed by label:pm:seat
    enhancementNew feature or request
    priority:p1High: required for production / M2
    on Oct 24, 2026
  2. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first touch — enhancement · priority:p1 · domain:spec · area:workflow · needs-user-decision · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-24T11:59Z

    ⛔ Not a claim, ⛔ not a dispatch. The decision is the maintainer's: it adds a refusal to the contract.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Timing note · domain:spec seat 2 · 2026-10-24T13:05Z

    excludeSubmitter landed on main with PR #25632 (49653f9181, committed 2026-10-24T12:16:40Z). It is in no release yet, so the next release that cuts from main will carry it. Letter A narrows nothing published only if it lands before that release. After it, A is a Clause-②: no (narrowing) with an ADR-0087 disposition (contract review 6383996601 on PR #25632, ③ item 1). Under B or C, the timing does not matter.


    Generated by Claude Code

  4. os-elon-musk commented on Oct 24, 2026

    @os-elon-musk
    Collaborator

    Ruling: batch #12 item 1 · letter A · maintainer 「25635 25647 25675 同意」 2026-10-24T16:24:48Z

    A = 发布时拒绝 excludeSubmitter: true 与 onEmptyApprovers: 'auto_approve' 的组合:审批节点多一条配对校验(形如 checkFallbackApproversPairing),提示改用 admin_rescue / fail / fallback。让裁决 6377842908 的「绝不静默放行」在契约里结构性成立。

    工作量:domain:spec 1 个 S PR,p1。时间窗口:excludeSubmitter 已随 PR #25632 进 main(2026-10-24 12:53Z)但未发版,本校验须在下一次 objectstack 发版前合入,否则变成对已发布键的收窄(Clause-② narrowing,需 ADR-0087 处置)。#25573 的运行时范围不变。

    维护者动作已完成:needs-user-decision → pm:queue;裁不派。批 #12 简报:https://claude.ai/artifact/YLrSzMXcnWVmbkZCxgxcQt


    Generated by Claude Code

  5. added
    pm:queueReady for the PM dispatch loop
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Oct 24, 2026
  6. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Oct 24, 2026
  7. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2
    Session: session_01PC4NPXPgPmpDRG9DdDnyXT
    Account: sunhaolin (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-25635-exclude-submitter-auto-approve-refusal
    Worktree: objectstack-issue-25635
    Domain: domain:spec
    Seat: domain:spec#2 (seat post #18549)
    File surface: packages/spec/src/automation/approval.zod.ts: one more refinement arm on the approval node config, in the shape of checkFallbackApproversPairing (about :791). It refuses excludeSubmitter: true (about :905) beside onEmptyApprovers: 'auto_approve', with a prescription naming admin_rescue / fail / fallback. Also its tests, the excludeSubmitter describe sentence if it changes, regenerated automation shards and references, and one changeset .changeset/25635-exclude-submitter-auto-approve.md. No runtime change: #25573 keeps its scope. (stop on breach; explain in the report)
    Container & model: S, mode:subagent (one packages/spec file plus generated files, not build-heavy), model: default (TIER_DEFAULT; dispatch-gates --tier for this surface: no path-derived mandate), effort: high (p1, release window). The path limb hits, so a CONTRACT_REVIEW_TIER contract review is owed before the queue.
    Clause-②: no (narrowing)
    Metadata-protocol: narrowed
    Responsibility: n/a — not a defect card
    Ruling-ref: 6386627950 (batch #12 item 1, letter A, maintainer 「25635 25647 25675 同意」)
    Thread-read: 6386627950
    Serial constraints cleared: none. No open PR touches these files (all 15 open PRs' file lists read 2026-10-24T16:38Z), and no open pm:dispatched claim declares them.
    Premise: holds on origin/main 6021b5e837. excludeSubmitter is declared (about :905; landed with PR #25632, 49653f9181) and is in no release yet. No refinement refuses it beside auto_approve. Window: this must merge before the next objectstack release, or it narrows a published key and owes an ADR-0087 disposition.


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Dispatched · round 2 · domain:spec seat 2 · 2026-10-24T16:45Z

    • Back end: mode:subagent, an os-dev subagent in this seat's session session_01PC4NPXPgPmpDRG9DdDnyXT, in its own worktree objectstack-issue-25635, on branch claude/issue-25635-exclude-submitter-auto-approve-refusal. The claim 6386790805 stays the identity.
    • Order given: the ruling verbatim (6386627950, letter A) with its release window, the PM's mechanism assumptions marked as leads to re-measure, the consumer-package test list (plugin-approvals, lint, metadata-protocol), the Writes: line.

    Generated by Claude Code

  9. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 25635,
      "status": "done",
      "branch": "claude/issue-25635-exclude-submitter-auto-approve-refusal",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/25700",
      "session": "session_01PC4NPXPgPmpDRG9DdDnyXT (the dispatching session's id; this run is its subagent)",
      "premise_still_valid": true,
      "summary": "Letter A of ruling 6386627950 is implemented. `ApprovalNodeConfigSchema` gains a second `.superRefine`, `checkExcludeSubmitterPolicyPairing`, in the shape of `checkFallbackApproversPairing` and wired the same way. It raises ONE `custom` issue when `excludeSubmitter === true` and `onEmptyApprovers === 'auto_approve'`. The issue is located at `onEmptyApprovers`, not `excludeSubmitter`, because the ruled fix is a different policy; the message names both keys and offers 'admin_rescue' / 'fail' / 'fallback' (with fallbackApprovers), and deliberately does not offer removing excludeSubmitter, which would drop the control. Only `true` triggers it; `false` or an omitted key leaves auto_approve with its meaning. The `excludeSubmitter` describe gained one sentence naming the refused value and the three policies, because without it the describe said less than the code now enforces; the [PLANNED] marker stays. The reference doc was regenerated (one row) and the changeset is `'@objectstack/spec': minor` carrying the three declaration lines. The premise was re-measured on origin/main 6021b5e837 and held: excludeSubmitter is declared at :905, no refinement refused the pair, and no producer writes the key (git grep across examples, platform-objects, plugins, services, lint and metadata-protocol: 0; control: onEmptyApprovers in examples/app-showcase flows). Release reading: the latest npm @objectstack/spec 18.1.0 tarball has 0 files containing excludeSubmitter (controls in the same tarball: onEmptyApprovers 28, fallbackApprovers 30), CHANGELOG.md has 0 hits, and the key's own changeset is still pending, so no published surface narrows. origin/main was merged before opening, through os-regen-merge.sh (a clean merge with nothing to regenerate). Two conflicts with the role file, stated rather than chosen silently: the dispatch asked for test:repo, which the role file assigns to CI; I ran it once, before the merge. The consumers named (plugin-approvals, lint, metadata-protocol) have no test:repo script. No runtime change: #25573 keeps its scope.",
      "tests": "TDD red before implementing (approval-exclude-submitter-25609.test.ts, with §6 and one §5 assertion added): Tests 6 failed | 22 passed (28). The six were exactly the new refusal pins and the describe pin; controls green. At head 3cee74ecf0: `pnpm --filter @objectstack/spec test` Test Files 719 passed (719), Tests 21800 passed | 1 todo (21801), lock VERDICT command-exit 0. `pnpm --filter @objectstack/spec typecheck` exit 0 (check:test-typecheck OK, ledger held). Consumers, full suites, lock VERDICT command-exit 0 joined with &&: plugin-approvals Test Files 96 passed (96) / Tests 1259 passed (1259); lint 161 passed (161) / 7269 passed (7269); metadata-protocol 263 passed | 3 skipped (266) / 21475 passed | 19 skipped (21494). The same three were also green at 4c615790bf, before the merge, with identical counts. `pnpm --filter @objectstack/spec test:repo` at 4c615790bf (pre-merge): Test Files 79 passed (79), Tests 1269 passed (1269), VERDICT command-exit 0. It was not re-run at 3cee74ecf0; CI Test Core owns it. Ablation (behaviour pinned by tests changed; the subject resolves from src via a relative import, so no build or dist leg applies): `node scripts/ablation-replace.mjs --delete` on the anchor `.superRefine(checkExcludeSubmitterPolicyPairing)` (anchor 1 -> 0, blob 8c310f2bea7b -> a85bce9b6733). Predicted the five §6 refusal pins red with controls green; observed Tests 5 failed | 23 passed (28), direction red. Restore proven: blob after restore == HEAD blob 8c310f2bea7b and `git diff HEAD` empty (tool verdict plus my own hash-object comparison).",
      "gates": "Local, at 3cee74ecf0: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 113 commands for 4 paths vs merge base b77bb37eb, the same list as at 4c615790bf. Each command ran with its exit code captured before any pipe. 109 exited 0 on the first pass. Four needed a re-run, and every re-run exited 0. Three of them ran while the dogfood closure build was rewriting dist/: check:exported-any (3, prerequisite), check:generated (1, api-surface reported stale against a half-written dist) and check:dual-build-cjs-loads (1, plugin-sharing index.d.ts not yet emitted). The fourth, check:type-check-debt, exited 3 on a lock queue-timeout and was re-run under the lock: OK, 1 entry re-measured, none above its number. Reconciliation with exit codes: `--ran` reports 113 derived famil(ies) accounted for, 113 run, 0 NOT-MEASURED (a DERIVED zero — all 113 recorded an exit code and none of them is 3). Included in the 113: check-adr-0087-registration exit 0 (classifies the changeset [clause-②-narrowing+metadata-protocol-narrowed], disposition not-required (no-migration-prescription)), check-changeset-no-major exit 0, check-empty-changeset exit 0, check:nul-bytes exit 0. `pnpm --filter @objectstack/spec check:generated` after the merge: all 14 artifacts up to date. affected-docs (--json origin/main) named 7 docs: 4 hand-written, none of which mentions excludeSubmitter and whose auto_approve sentences stay true, so no edit was needed; the other 3 are release-owned and untouched. No exported symbol was renamed or removed, so check-platform-checklist does not apply. CI on PR #25700 head 3cee74ecf0, read once with no wait: 25 check-runs. Completed: 'No other open PR may claim the same issue' success; 'Packed-tarball smoke (opt-in)' and '18.x upgrade harness (opt-in)' skipped. in_progress at report time: every Lint · and Type Check · lane, Check Changeset, Governed Surface Queue Guard, 'The card this PR closes must claim this branch', Spec property liveness, Check PR Size and the docs checks. Test Core and Build Core had not yet appeared.",
      "dogfood_battery": "3cee74ecf0 · os-verify-lock: VERDICT command-exit 0 (held 1363s) · Test Files 295 passed | 3 skipped (298) · Tests 2520 passed | 18 skipped (2538) · no failures. Command as in CI: dependency closure built first (turbo build --filter=@objectstack/dogfood^..., 67/67 tasks), then `pnpm --filter @objectstack/dogfood test` under the lock with no OS_TEST_SHARD and no path.",
      "mcp_calls": "0 — no MCP GitHub tool was called",
      "api_writes": "2 relay writes. (1) fleet-write pr_create, run 40120786352: POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/25700/assignees (sunhaolin); body read back 8750 bytes sent = 8750 stored, assignee confirmed. (2) this os-dev-report comment through scripts/pm/post-stamped.mjs: POST /repos/objectstack-ai/objectstack/issues/25635/comments. git push (4 pushes of the branch) is not a REST write. No label written: the dispatch names none, and a changeset exists, so skip-changeset does not apply.",
      "open_questions": [
        {
          "question": "Declaration arm. The card's 'After the ruling' text anticipated `Clause-②: no` for a pre-release landing; the PM's claim carries `Clause-②: no (narrowing)`. I copied the claim verbatim, as the role file prescribes. Should the arm stay?",
          "options": [
            "A keep `no (narrowing)` (as landed): the gate verdict is identical, because `Metadata-protocol: narrowed` alone already makes check-adr-0087-registration treat the changeset as breaking; the arm additionally cost one full dogfood run, which is done and green",
            "B change it to `Clause-②: no`: matches the measured fact that no PUBLISHED accept set narrows, but would need a body and changeset rewrite by the seat"
          ],
          "recommendation": "A, because it changes no gate outcome, the release-window reading in the PR body already records that nothing published narrows, and a rewrite costs a write for no behavioural difference"
        }
      ],
      "out_of_scope_findings": [
        "carrier: none · noted, not filed — the lint hint of approval-expression-no-empty-policy (packages/lint/src/validate-approval-approvers.ts, around line 625) lists 'auto_approve' among the policies to declare, whatever excludeSubmitter says. An author with excludeSubmitter: true and an expression approver who follows it verbatim is refused at once by the new rule, with the prescription. That is loud, not silent, and no producer writes excludeSubmitter, so the reach bar is not met. Dedupe words: approval-expression-no-empty-policy, auto_approve hint, excludeSubmitter lint",
        "carrier: none · noted, not filed — boundary: SLA escalation.action 'auto_approve' is a different key, a timed policy that notifies the submitter (approval.sla_breached). The ruling covers onEmptyApprovers only, so it is untouched."
      ]
    }

    Generated by Claude Code

  10. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #25700 comment 6387972296 · domain:spec seat 2 · 2026-10-24T18:33Z · waits for the CONTRACT_REVIEW_TIER review and green checks. Release window: this must merge before the next objectstack release.

  11. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:spec seat 2 (seat post #18549) · sunhaolin · session session_01PC4NPXPgPmpDRG9DdDnyXT · 2026-10-24T20:03Z

    reading value
    PR #25700 MERGED through the queue at 2026-10-24T20:02:29Z; merge commit 4bcc4df649. ACCEPT 6387972296; contract review PASS 6388492981 on head 3cee74ecf0 (the landing head)
    queue branch gh-readonly-queue/main/pr-25700-* gone (git ls-remote: 0 refs)
    origin/main carries 4bcc4df649; packages/spec/src/automation/approval.zod.ts holds checkExcludeSubmitterPolicyPairing (4 hits)

    State: the card closed completed on the merge (Fixes #25635). pm:dispatched is removed in this stroke. The dev was an os-dev subagent of this session, so there is no cloud session to archive. Release window met: the refusal is on main before the next objectstack release, beside the key's own pending changeset (.changeset/25609-approval-exclude-submitter.md), so the two ship together and nothing published narrows. The two 'auto_approve' prescriptions (the approval-expression-no-empty-policy hint, checkFallbackApproversPairing's message) stay in the PR's Acceptance notes with no carrier: following either beside excludeSubmitter: true is refused loudly with the prescription.


    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:workflowApprovals and automation — the work that runs without a person driving itdomain:specDomain 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