Repository navigation
Description
Found while implementing the resume gate for #3801 (PR #3822). Adjacent seam, same
trust boundary, different shape — filing rather than widening that PR.
Reasoned from code, not demonstrated — no repro was written, same caveat as
#3801.
Requirement
#3822 gates POST /api/v1/automation/:name/runs/:runId/resume on the node type
that produced the suspension: a descriptor may declare resumeAuthority: 'service',
and approval does, so an approval pause continues only through ApprovalService.
ADR-0044's revise window is the same trust boundary in a shape that key cannot
see. Send-back resumes the run down the revise edge onto an ordinary wait node
(signal flavor) placed by the flow author (ADR-0044 §D3: "The revise edge targets an
ordinary wait node — the durable pause already shipped for timers/signals"). The run
parks there until ApprovalService.resubmit, which enforces submitter-only, refuses
a colliding pending request, and audits a resubmit action before resuming.
Nothing enforces that path. wait is resumeAuthority: 'any' — correctly, since a
signal-flavored wait is meant to be resumable by an external producer — so a raw
resume of a run parked in a revise window walks the declared back-edge into the
approval node, opening round N+1 with no sys_approval_action row and none of
resubmit's checks. Lower severity than the approval bypass #3801 closed (it forces a
resubmit rather than an approval), but it is the same class: a service-owned
continuation reachable around the service.
Why the #3822 mechanism doesn't extend to it
The gate keys on the node's registry type, and the type here is a generic wait
indistinguishable from every other author-placed wait in the system. Gating all wait
nodes would break the documented signal/webhook/manual resume contract.
Closing it needs a per-suspension owner claim rather than a per-type one — the
suspension itself recording that a service owns this particular pause. Sketch, for
discussion rather than a settled design: let a resume signal carry an owner claim that
is persisted onto the continuation, so the service that drove the run into the wait
also owns driving it out; the type-keyed declaration then becomes the default when no
claim is recorded.
Refs #3801, #3822. Context recorded in the 2026-07-28 addendum to
ADR-0019.
Activity
fix(automation,approvals): gate the generic run-resume route on the s…
fix(automation): resume gate follows `map:` too; the route stops acce…
os-zhuang commentedon Jul 28, 2026
Now demonstrated, and the severity in the description above is understated. Repro built on the existing
approval-revise.test.tsharness (real engine + realApprovalService), run againstmain@93f267f81.Confirmed: unauthorized resubmit, no audit row
A raw
resume(runId)on a run parked at the revise-windowwait:Two corrections to the description: it needs no
branchLabel(an empty body works — the labelled back-edge is walked either way, verified both ways), and it bypassesresubmit's submitter-only check as well as the audit row.Not in the description: it can permanently kill the run
ApprovalService.resubmitrefuses withDUPLICATE_REQUESTwhen another request is already pending on the same record, and the code comment says exactly why:That guard lives in the service, and the raw resume goes around it:
The approval is then unresolvable: the round-1 request stays
returned, the run that owned it is gone, and no resubmit can ever reach it. Opportunistic (it needs a colliding pending request to exist, which an ordinary user can often cause — a record-change trigger re-firing inside the revise window is the documented case), but it is remote run destruction, not just a missing audit row.This changes the recommended fix
The description sketches a per-suspension owner claim. Having looked harder, the simpler option is the one ADR-0044 considered and rejected — "Approval node re-suspends itself in a 'revise mode' (no wait node)" — or an equivalent dedicated node type. Either makes the revise pause typed as service-owned, so the existing
resumeAuthoritygate covers it with no new machinery. ADR-0044 chose the genericwaitfor reuse; what we know now is that a generic node sitting in a service-owned position is exactly what a type-keyed gate cannot see.The owner-claim design is still the more general answer, but it needs care the sketch didn't account for: an approval whose
approvebranch leads to ascreenmust NOT inherit the claim, or the UI can no longer advance that screen. Blanket inheritance breaks screen flows downstream of an approval.Refs #3801, #3853, #3879.
Generated by Claude Code
chore: empty changeset — ADR-0044 amendment releases nothing (#3823)
docs(adr-0044): the revise pause must be service-owned — record why t…
os-zhuang commentedon Aug 5, 2026
分诊(services 车道 PM,
session_01BWS4heBoAitLmzCLhcYdbK):挂needs-user-decision,不入派发队列。背景
#3822 已为 resume 端点建立按节点类型的门(
resumeAuthority),approval 暂停已受保护。本单指出 ADR-0044 的 revise window 停在一个普通wait节点上,类型门天然看不见它;2026-07-28 的实证评论把严重度抬了一级:raw resume 不仅绕过 submitter-only 与审计行,在存在碰撞 pending request 时会在引擎消费掉 suspension 之后失败,永久杀死 run(审批不可再解)。缺陷真实、已实证,不是观察级。要拍板什么
让 revise 暂停变成 service-owned 的机制,两案分叉在契约形状上:
方案 A:专用 typed 暂停(approval 节点以 revise 模式自挂起,或专用节点类型)——现有
resumeAuthority类型门直接覆盖,零新机制。但这正是 ADR-0044 §Alternatives 考虑过并拒绝的形状(当时理由:复用已有的 durable wait)。采纳 = 推翻一处已裁决的 ADR 备选,须维护者确认。方案 B:per-suspension owner claim(驱动 run 进 wait 的服务持有唤出权,持久化在 suspension 上)——更通用,类型门退化为无 claim 时的默认。但实证评论指出了继承陷阱:approval 的 approve 分支通向
screen时不得继承 claim,否则 UI 无法推进 screen —— 规则隐性,机制面更宽。三轴分析
建议
方案 A,三轴同向;因涉及推翻 ADR-0044 的一处已拒绝备选,按升级门槛交维护者拍板,不代答。拍板后本单摘标签回队列,可直接派发。
Generated by Claude Code
os-zhuang commentedon Aug 5, 2026
维护者裁决(2026-08-05,经 services 车道 PM 转达):批准方案 A —— revise 暂停改为 typed / service-owned(approval 节点以 revise 模式自挂起,或专用节点类型 —— 实施者按代码现状定形,两者等价于「类型门可见」),使 #3822 的既有
resumeAuthority门直接覆盖,零新机制。ADR-0044 §Alternatives 中被拒绝的该备选由本裁决显式推翻 —— 实施 PR 须附 ADR-0044 的 amendment 段落记录这次改判及其依据(2026-07-28 实证评论:raw resume 可绕过 submitter-only/审计并在碰撞 pending request 下永久杀死 run)。摘
needs-user-decision,回pm:queue。排程:service-automation 包串行链 —— #5529(在飞)落地后,本单优先于 #5048/#4792 派发(已实证的 run destruction > 日志格式/装配强制)。实施注意(from 实证评论,派发时会写进派发词):
screen的既有 UI 推进;approval-revise.test.ts+ 实证评论的三段实录),反向验证以它为基。Generated by Claude Code
os-zhuang commentedon Aug 5, 2026
认领:PM 循环第 4 轮(services 车道)。维护者已批方案 A(见上方裁决评论);同包前序 #5529(PR #5549)已 MERGED,串行解锁。
会话:
session_01BWS4heBoAitLmzCLhcYdbK分支:
claude/issue-3823-revise-pause-typedWorktree:
objectstack-issue-3823域:
domain:services文件面:
packages/plugins/plugin-approvals/**(approval 节点 revise 模式自挂起 / 专用类型)、packages/services/service-automation/**(如需resumeAuthority声明面)、docs/adr/0044-*.md(amendment 段,记录推翻已拒绝备选的裁决)、两包测试、.changeset/*.md同包 #5048/#4792 继续排队,本单落地后依次。
Generated by Claude Code
os-zhuang commentedon Aug 5, 2026
文件面申报修正(同一认领,PM 补录 —— 供各车道全局在飞检查读取):PR #5560 的实际触碰面超出原申报,全量为:
packages/spec/src/automation/approval.zod.ts(新增APPROVAL_REVISE_NODE_TYPE常量 + 文档)+api-surface生成物再生成 —— 触 spec 契约面;spec 座位当前空缺,由本车道按跨域单一 PR 例外承载,spec 座位接管者请以本评论为准对账;packages/plugins/plugin-approvals/**(新节点类型 + sendBack 拒绝)— 原申报内;packages/lint/**(新 ruleflow-approval-revise-target-not-service-owned,error 级)— devx 面;examples/app-showcase(存量 revise flow 迁移)、skills/objectstack-automation/**、content/docs/automation/approvals.mdx、docs/adr/0044-*.md(amendment,原申报内);packages/service-automation:零改动(裁决验收标准)。原申报漏列 spec/lint/examples 面是 PM 的申报不足,不是 dev 越权 —— 已批形状(typed 节点类型)本质上要求词表声明与编写期拒绝。入队前将按 A 项纪律核对 spec 生成物与在飞无交集。
Generated by Claude Code
os-zhuang commentedon Aug 5, 2026
复核:ACCEPT → PR #5560(CI 绿后转 ready 入合并队列)。
已核项(对照维护者裁决逐条):
approval_revise在 spec 声明、与 approval 节点同一注册点(不可能有半套部署)、resumeAuthority: 'service';service-automation逐字节零改动 —— 正中「既有类型门直接覆盖、零新机制」的验收标准;lintFlowPatterns注册项,零新接线);check:generated仅 api-surface 因新导出 stale,已再生成提交;fixture 按消费半径清扫(5 个测试文件 + showcase);resumeAuthoritydefaults to'any', so every future pausing node ships fail-open — ADR-0044 says this is "tracked separately" and nothing tracks it #5561(resumeAuthority默认'any'对未来暂停类型 fail-open)已按 finding 立单,下轮分诊。本单闭环意义:#3801(approval 绕过)→ #3822(类型门)→ 本单(revise 窗口纳入门内)—— 审批信任边界补完。落地后解锁同包 #5048 → #4792。
Generated by Claude Code
fix(approvals): the ADR-0044 revise window is a service-owned node ty…
feat(spec,service-automation,devx): 未声明 resumeAuthority 的 pausing 节点类…