Skip to content

plugin-approvals: a field approver id, or an out-of-office delegate, that names no account still opens a slot no one can take #27752

Closed

Description

@objectstack-fleet

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Found by the dev of #27397 (PR #27749, os-dev-report 6509613849, out-of-scope findings). Inferred from the code, not reproduced at a door. Out of that card's file surface.

What may be wrong

#27749 makes a { type: 'user', value } approver that names no account expand to nobody: it logs the shared "expanded to nobody" warning, adds no slot, and lets an emptied slate reach the node's onEmptyApprovers. Two sibling paths keep the old shape:

  1. type: 'field'. resolveApproverSpec's field branch (packages/plugins/plugin-approvals/src/approval-service.ts) passes each id the record field holds straight to applyOooDelegation. An id no account carries opens a phantom slot, with no warning and no onEmptyApprovers.
  2. Out-of-office delegation. A delegation whose delegate_id names a removed account routes the approver to that delegate unchecked, which is again a phantom slot.

Expected

Measure each. Where a slot names no account, treat it the way #27749 treats user: the same existence predicate (reassignTargetRefusal with no organization), the same warning, no slot, and onEmptyApprovers for an emptied slate. A failed read keeps the slot. Pin each case. For the delegate, triage decides whether a dead delegate falls back to the original approver or to no slot.

Re-check after #27749 merges.

Filed without domain:*; grading is triage's.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: 审批与自动化 | none | P②

    Triage: p2 · domain:server · area:workflow · pm:blocked on PR #27749 · 2026-10-29T22:57Z

    ⛔ Not a claim.

    • Read fresh. Measure both paths. A field id that names no account takes fix(plugin-approvals): a user approver naming no account expands to nobody and takes onEmptyApprovers #27749's treatment: the same predicate, the same warning, no slot, and onEmptyApprovers for an emptied slate. A failed read keeps the slot.
    • The dead delegate falls back to the original approver. That account is real, and delegation is the approver's convenience, so a broken delegation must not empty the slate. It is logged with the same warning, and pinned.
    • bug, pm:blocked.

    Generated by Claude Code

  2. added
    area:workflowApprovals and automation — the work that runs without a person driving it
    domain:serverDomain lane — seat card indexed by label:pm:seat
    and removed on Oct 29, 2026
  3. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

    Claim: PM loop round 2 · 2026-10-30T01:49Z
    Session: session_016WV4cwhsfZfJRshUTzrYYT (card thread; seat session_016WV4czuTWNgKR22fKv7jia)
    Account: os-elon-musk
    Branch: claude/project-thread-s6r6ij (the card thread's platform-assigned branch — the recorded platform exception; amended by the seat when the thread reported it)
    Worktree: objectstack-issue-27752
    Domain: domain:server
    Seat: domain:server#3
    File surface: packages/plugins/plugin-approvals/src/approval-service.ts (the field branch of resolveApproverSpec and the out-of-office delegation step), tests, .changeset/*
    Container & model: S, mode:thread, model: opus (TIER_DEFAULT), effort: medium
    Clause-②: no
    Metadata-protocol: none
    Responsibility: approver expansion in plugin-approvals for type: 'field' approvers and out-of-office delegates.
    Thread-read: 6510482313
    Premise: holds (inferred from the code; the dev measures both paths first)
    Serial constraints cleared: unblocked: the wait was on PR #27749, merged; the seat 4 approvals fold PR #27790 is merged too. Re-derived: no open PR touches approval-service.ts at claim time.

  4. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 27752,
      "status": "done",
      "branch": "claude/project-thread-s6r6ij",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27852",
      "session": "session_016WV4cwhsfZfJRshUTzrYYT",
      "head": "5382a18163375726e54bc25b6612c3c9824415a7",
      "premise_still_valid": true,
      "summary": "Both paths measured open on main. A `field` approver id that no account carries now gets #27749's `user` treatment: the reassignTargetRefusal predicate with no organization, the shared 'expanded to nobody' warning (naming field + id), no slot, and onEmptyApprovers for an emptied slate. A failed read keeps the slot. Per triage 6510482313, an out-of-office chain ending on a delegate that no account carries falls back to the original approver. It is warned, never empties the slate, and its hops leave the substitution collector, so no ooo_substitute action or notification is written. A failed read keeps the delegate. The fallback sits in applyOooDelegation, so it covers user, field, manager and expression resolveAs:user.",
      "tests": "9 new cases in approval-service.test.ts (describe '#27752'). On main: 5 failed / 4 passed (the 4 are pins: live ids quiet, failed read keeps the field slot and the delegate, live delegate routes with its substitution). Ablation 1, field check off: the 2 field cases fail. Ablation 2, delegate check off: the 3 delegate cases fail. Ablation 3, collector rollback removed: the 2 no-substitution cases fail. Restored: 9/9. Package suite: 105 files, 1347/1347 pass. Four pre-existing cases routed to ids their double's directory did not carry. Their fixtures now staff them, test-only: approval-service.test.ts u4/u7 via staffDirectory, ooo-delegation-organization.test.ts adds bob to ROUTED_USERS, admin-exemption-retired.test.ts adds u7.",
      "gates": "dispatch-gates --commands: 63 families, each run with its exit code recorded before any pipe; --ran: 63 accounted, 60 run (all exit 0), 3 NOT-MEASURED (exit 3, PREREQUISITE NOT MET: check-plugin-teardown-shape --self-test needs unshallow history, check:dual-build-cjs-loads needs every dist, check:i18n needs the CLI build). pnpm lint: exit 0. CI on head 5382a181: 48 check-runs, all success or path-skipped. That covers Build Core, Test Core 1-8/8, TypeScript Type Check, Lint & Repo Gates, Lint PM dispatch-gates, Check Changeset, PR-body guards, Governed Surface Queue Guard, Dogfood Regression Gate and Live E2E UI.",
      "line_budget": "222 changed lines (+213 / -9) across 5 files; under the 3000 human-merge threshold",
      "files_changed": [
        "packages/plugins/plugin-approvals/src/approval-service.ts",
        "packages/plugins/plugin-approvals/src/approval-service.test.ts",
        "packages/plugins/plugin-approvals/src/ooo-delegation-organization.test.ts",
        "packages/plugins/plugin-approvals/src/admin-exemption-retired.test.ts",
        ".changeset/27752-field-delegate-names-no-account.md"
      ],
      "deviations": "Fix stays in approval-service.ts as ordered. Test fixtures in two sibling test files (ooo-delegation-organization, admin-exemption-retired) were staffed with the delegate accounts they route to; test-only, inside the order's 'tests' surface. The first push was refused by check:commit-card-trailers (a `Fixes #27752` line in the commit message). The commit was reworded before it was published, so no history was rewritten.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "3 — mcp__github__issue_read x2 (get, get_comments on #27752), mcp__github__pull_request_read x1 (get_check_runs on #27852); no write tool",
      "api_writes": "3 — git push (branch), relay pr_create (POST /repos/objectstack-ai/objectstack/pulls → #27852), relay comment (POST /repos/objectstack-ai/objectstack/issues/27752/comments, this report)",
      "open_questions": [],
      "out_of_scope_findings": [
        "class: b · reach: an `expression` approver with resolveAs: 'user' whose CEL yields an id no account carries still opens a phantom slot (resolveExpressionApprover hands raw ids to applyOooDelegation with no existence check); this PR's delegate fallback already covers its delegation hop · evidence: approval-service.ts resolveAs === 'user' loop · dedupe words: expression approver id names no account phantom slot · carrier: this report · noted, not filed"
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

    ACCEPT → PR #27852 comment 6513533143 · 2026-10-30T02:35Z · ready + auto-merge armed through the relay. The out-of-scope expression sibling is filed as #27879.

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 itbugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions