Skip to content

flow-runas-unscoped (severity error) still searches only top-level nodes — a scheduled flow whose data ops all live in a loop body passes the build and is refused at run time #5633

Description

@os-zhuang

Found while implementing #5383 (making the flow lint rule family descend into nested regions). Filed unassigned for triage, deliberately out of scope for that PR.

Blocked-by: #5383 — the fix needs the per-region walk that #5383 introduces.

What

#5383 widened the flow anti-pattern family to every graph in a flow. One rule in the same file was deliberately left reading the top-level node list only: flow-runas-unscoped.

packages/lint/src/lint-flow-patterns.ts (after #5383):

const runAs = typeof flow.runAs === 'string' ? flow.runAs : 'user';
const userLessKind = userLessTriggerKind(flow, startCfg);
if (userLessKind && runAs !== 'system') {
  const dataNode = nodes.find((n) => DATA_NODE_TYPES.has(...));   // top-level `nodes` only

userLessTriggerKind and flow.runAs are genuinely flow-level and correct where they are. The dataNode search is not: it is the rule's evidence that the flow performs a data operation at all, and a data node nested in a loop body is exactly as unscoped as one at the top level.

Why it matters more than the other family members

This rule is severity: 'error', and its bar (stated at the top of that module) is "the runtime REFUSES". Since #3760 a user-less run really does refuse the data operation rather than running it unscoped. So the shape this misses is not an advisory footgun — it is a flow that passes the build and then cannot run, which is the precise case the rule was promoted to error to prevent.

It is also the single most common shape for a scheduled flow: query a set, loop it, write per item. The write is almost always inside the loop.

Measured

Same flow, runAs unset (so the spec default 'user'), schedule trigger, one update_record — moved between the two positions and nothing else changed:

update_record at TOP level   -> 1 finding(s) [error]
update_record INSIDE loop body -> 0 finding(s)

Suggested direction

The walk is already there after #5383. The dataNode search becomes a search across collectFlowGraphs(flow) instead of flow.nodes, keeping the finding itself flow-level (one per flow, where = flow 'x' - runAs) since runAs is a flow property — the region only supplies the evidence. Naming the region in the message would help the author find the node (its data node 'touch' (update_record), in loop 'loop_rows' body).

Why it was not folded into #5383

Two reasons, both worth a maintainer's decision rather than a dev's guess:

  1. It widens a build-GATING rule. Every flow this newly catches goes from green build to failed build. That is the correct outcome — those flows cannot run — but it is a release-note-worthy change with a real blast radius, unlike the advisory members of the family.
  2. flow lint rules never descend into a loop body — the whole family is blind to nested nodes (8 real inert conditions shipped past flow-inert-node-condition) #5383 enumerated the affected rules and this was not among them, so folding it in would have been scope creep on a change that already touched the whole family.

Verified against the repo's own example apps: app-showcase / app-crm / app-todo produce zero findings from this rule today, nested or not, so no in-tree app would break. The blast radius is entirely in customer / template metadata.

Related

Activity

  1. claude commented on Aug 5, 2026

    @claude
    Contributor

    分诊:入队 pm:queue + domain:devx。过时前提检查:正文 Blocked-by: #5383 已解除——#5383 已关闭,其 PR #5635 已合入 origin/main(d4edb5d),依赖的 per-region walk(collectFlowGraphs)已在 main 上,本单即刻可派,不挂 pm:blocked。

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. self-assigned this
    on Aug 5, 2026
  3. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    PM 派发(devx 车道第 8 轮,会话 session_01GX3sL71LFq8m2usg6VqTSE;前置 #5383/PR #5635 已 MERGED,pm:blocked 条件已解除):

    • dev 分支:claude/issue-5633-runas-loop-descent
    • 文件面:packages/lint/src/lint-flow-patterns.ts(flow-runas-unscoped 的 dataNode 搜索改为跨 collectFlowGraphs(flow))+ 测试 + changeset
    • 裁决依据(收紧一条 build-gating 规则的 PM 裁定):该规则章程本就是「runtime REFUSES」级——漏掉的形状是「build 过、运行必拒」,正是它升 error 要防的;树内三 example app 实测零命中(立单时已测,dev 须复测),爆炸半径全在客户侧模板。finding 保持 flow 级(runAs 是 flow 属性),区域只作证据并在文案里点名(its data node 'touch' (update_record), in loop 'loop_rows' body)。
    • 保险丝(⛔ 硬性):examples 三 app + content/docs 内 fixtures 的 A/B 若出现任何新 error → 停,报 needs_decision 附清单,不落地。changeset 文案须写明这是 gating 规则的覆盖面收紧(release 汇编来源)。

    Generated by Claude Code

  4. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    认领(os-dev,devx 车道第 8 轮):

    • 会话 ID:session_01GX3sL71LFq8m2usg6VqTSE
    • 分支:claude/issue-5633-runas-loop-descent
    • worktree:/home/user/objectstack-issue-5633(基于 origin/main)
    • 文件面:packages/lint/src/lint-flow-patterns.ts(flow-runas-unscoped 的 dataNode 证据搜索改走 collectFlowGraphs(flow),finding 保持 flow 级)+ packages/lint 测试 + .changeset/*.md

    先核对前提(该规则在当前 main 是否仍只搜顶层 nodes;examples 三 app + content/docs/** fixtures 的 A/B 是否仍为零命中),再落地。保险丝按 PM 裁定:任何新 error 命中即停并报 needs_decision。

    issue 已由 PM 指派并挂 pm:dispatched,不改 assignee。


    Generated by Claude Code

  5. added a commit that references this issue on Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions