Skip to content

fix(runtime): a flow start input declared a record id is loaded in the caller's own scope from the declared object at every door that starts the flow by name - #30628

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-egpm9z
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-egpm9z

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread
domain:server#2 · os-bill · session_01Utp6CbNMpDS92ifzhDw9vf

维护者速读

  • 改了什么:流程启动输入上写了 objectName(声明“这个输入是某对象一条记录的 id”)后,凡是按名字启动流程的入口(触发路由新旧两种写法、REST 与 MCP 的流程型动作、声明的流程型端点),都会先用调用者自己的权限从声明的对象里加载这条记录:读不到或不存在答 404 RECORD_NOT_FOUND,不是 id 答 400 并点名该输入,两种情况都不会创建运行。没写声明的输入保持原样。
  • 为什么改:执行 [Decision] flows and records: may a flow start input declare a record reference (A), and does a non-system create stop choosing the new record's id (C)? #29425 裁定 A(6663860762,C 撤回):声明的记录引用要和流程定义里“明显当作 id 读”的输入一样被检查。
  • 风险与回滚:收窄。以前能启动的流程,若声明的输入给了调用者读不到的 id,现在被拒。仓内已发布的流程声明这个键的数量为 0。回滚即撤销本 PR。
  • 先看:packages/runtime/src/flow-start-admission.ts 的 declaredReadsOf 新增的声明读取(与可见读取取并集、跟随子流程);packages/spec/liveness/flow.json 的 variables.objectName 行翻为 live;测试文件末尾新 describe。
  • 两分钟验证:pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/flow-start-declared-record-reads.test.ts,预期 1 个文件 90 个用例全绿,其中 7 号 describe 每个入口下 7 个。
  • 需要你做的:无,席位落地。

Fixes #29425

Clause-②: yes (narrowing) — a start that ran before is now refused when it hands a declared record-id input an id the caller cannot read in the declared object, an id that names no record there, or a value that is not one id.
Metadata-protocol: none
Skills-docs: none — the published skills do not teach the start-door record rule or the objectName input declaration: git grep -n -i -E "record reference|isInput: true, objectName|declared record|liveness-planned" -- 'skills/**' has one unrelated hit (skills/objectstack-ui/references/react-blocks.md:52, a redirect URL's record fields).

What changes

The rule: a flow start input that declares it is the id of a record of a named object (objectName on the variable) is loaded, at every door that starts the flow by name, from the declared object in the caller's own scope before the run exists. An unreadable or nonexistent id is refused 404 RECORD_NOT_FOUND; a value that is not one id is refused 400 naming the input. An undeclared input keeps the visible-read rule unchanged.

  • One helper, no door edits. Every door already asks the shared check that PR fix(runtime): every flow start door judges a start input against the object the flow's definition reads it from #29365 introduced: refuseUnreadableFlowStartRecords (action-execution.ts), which loads each input from every object flowDeclaredRecordReads (flow-start-admission.ts) says the run reads it from. declaredReadsOf now adds the declaration as one more read, { object: objectName, many: false }. Door population, measured on main 2ffdb649 and unchanged since fix(runtime): every flow start door judges a start input against the object the flow's definition reads it from #29365:

    Door Entrances Calls the shared check at
    trigger route POST /automation/:name/trigger and the legacy POST /automation/trigger/:name, both through respondToFlowTrigger packages/runtime/src/domains/automation.ts:2824
    action of type: 'flow' REST POST /actions/:object/:action and MCP run_action, both through dispatchFlowAction packages/runtime/src/action-execution.ts:2685
    declared endpoint of type: 'flow' the endpoint executor packages/runtime/src/endpoint-executor.ts:590

    Other execute(flowName, …) callers are engine-internal (the subflow and map nodes, trigger bindings) or the verify harness, none of them a door.

  • Declared and also read in plain sight against another object: both are judged. The declaration is added beside every visible read, never in place of one. If it replaced them, a run that reads object B under the same id would read a row nobody checked, which is the gap fix(runtime): every flow start door judges a start input against the object the flow's definition reads it from #29365 closed. So a caller must be able to read the row in both objects.

  • Followed into callees. A parent that hands its input on, unchanged, to a subflow or map callee whose input carries the declaration is judged by that declaration, through the same recursion as other callee reads.

  • A loop or map that rebinds the name does not withdraw a declaration. The shadowing rule exists because a rebound name no longer holds the caller's value when a node reads it. The declaration is about the value the caller sent, so it stands.

  • packages/spec (seat-authorised, minimal). The objectName liveness row (packages/spec/liveness/flow.json › variables.objectName) flips from planned + authorWarn to live, with this reader as its evidence. The describe and docblock on FlowVariableSchema.objectName drop their planned sentences. Both the row and the docblock said to do this "in the commit that lands the reader". Left as they were, os validate would go on warning liveness-planned-property on every flow that declares the key, and the describe would say that no door applies it. The regenerated artifacts are content/docs/references/{automation/flow,api/automation-api}.mdx and packages/spec/liveness/state-counts/flow.md. No key, type or accepted value changes.

  • Docs. content/docs/automation/flows.mdx: the "Planned, no door applies this yet" callout under A start input that is a record id is replaced by the rule, and the 404 row under Run a flow via API names declared inputs.

  • Changeset. .changeset/29425-declared-record-input.md, patch for @objectstack/runtime and @objectstack/spec, ADR-0087 not-required (no-migration-prescription).

Why the title carries no !. No flow shipped in this repository declares the key today. git grep over examples/, packages/ and apps/ for an input variable literal that carries objectName finds only CHANGELOG example text. An input without the declaration answers exactly as before.

Why

Ruling A on #29425 (6580257050, A half kept by 6663860762, which withdrew C): a flow start input may declare a record reference, and every door that starts the flow by name loads a declared input in the caller's scope before the run exists, with the refusals #29365 gives a visibly read input. The spec half (#29442) landed the key. This is the server half.

Tests

New top-level describe 7, a start input declared a record id is loaded from the declared object at every door, before the run exists, in packages/runtime/src/flow-start-declared-record-reads.test.ts, the shared harness of #29365. It uses that file's row-scoped double, where each row is visible only to its owner. Per door (both trigger spellings, the endpoint, REST action, MCP run_action):

  • a declared input naming a readable record: the run starts, and the declared object was loaded (the firing control);
  • naming a record the caller cannot read, and an id that names nothing: 404 RECORD_NOT_FOUND, execute() never called;
  • given an object, a list or a boolean: 400 naming contractId, nothing loaded from the declared object, no run;
  • the same input undeclared and read by no node: not judged, the run starts on an id the caller cannot read (today's behaviour, the control).

Also pinned: the reading (flowDeclaredRecordReads) for a declaration, a declaration beside a visible read of another object, a callee's declaration, and a loop that rebinds the name; the union at a door (readable in the declared object only → 404; readable in both → run); a parent handing its input to a callee's declared input → 404; the system principal → run, nothing loaded.

Forced test edits (packages/lint/src/lint-liveness-properties.test.ts, test-only). Two pins read the shipped ledger's planned state of this row. Flipping the row is what makes them false.

Acceptance notes

  • At an action door, a param's own type answers first. A declared action param of type text that holds an object, a list or a boolean is refused by the action's param contract (ADR-0104 D2), 400 VALIDATION_ERROR naming the param, before the start check is reached. The trigger routes and the endpoint answer 400 VALIDATION_FAILED from the start check. Either way the refusal is a 400 that names the input, nothing is loaded and no run starts. The pins assert the code per door.
  • Author contradiction, not judged here. An input declared one id that the definition also reads as a list (filter.id.$in) gets both readings, so no value satisfies it and every start is refused 400. This is loud, not silent, and it is a lint candidate. It is not part of this rule.
  • The object-less key. objectName: 'global' passes the spec's name pattern. The shared subject load treats global and * as the object-less action key and loads nothing, so an input declared against global is not judged. This is the same pre-existing behaviour as a visible read of an object named global, and it is noted, not changed.

Verification

Readings on head 469106e8f (after merging origin/main) unless stated; the ablations and the first runtime suite ran on c55e8047, before the merge.

Pins, ablations
  • pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/flow-start-declared-record-reads.test.ts under the verify lock: Tests 90 passed (90), VERDICT command-exit 0.
  • Ablation 1, through scripts/ablation-replace.mjs --delete, on the line that adds the declaration (add(variable.name, { object: variable.objectName, many: false });). The anchor went from 1 to 0 and the blob from 2b18eb885049 to 945bcd4535b1. Result: Tests 30 failed | 60 passed (90). Red: every door's readable / unreadable / nonexistent pin, the trigger and endpoint 400 pins, the union and the callee pins, and the four reading pins. Green as designed: the action-door 400 pins (the param contract answers first), every undeclared control, and the system principal. Restored: blob equals HEAD, git diff HEAD empty.
  • Ablation 2, a declaration that replaces the visible reads (inputs.add(variable.name); made conditional on no objectName). Blob 2b18eb885049 to cbe9b748c523. Result: Tests 2 failed | 88 passed (90), the two union pins. Restored, blob equals HEAD.
  • The test imports the helper from src/ by relative path, so no dist leg applies.
Suites, typecheck, lint
  • pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 (before merging main): Test Files 547 passed (547), Tests 9911 passed | 19 skipped (9930).
  • pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2: Test Files 195 passed (195), Tests 8103 passed (8103) on 469106e8f; the declared-input pin file again Tests 90 passed (90).
  • pnpm --filter @objectstack/runtime typecheck, pnpm --filter @objectstack/lint typecheck: exit 0 (after building @objectstack/driver-turso, which a runtime kernel test imports and the closure build had not produced).
  • eslint --no-inline-config on the five changed source and test files: exit 0. The repo-wide pnpm lint is CI's.
  • pnpm --filter @objectstack/spec build && pnpm --filter @objectstack/spec check:generated: all 14 artifacts current after gen:docs and gen:liveness-counts, and again after merging main.
Full dogfood battery
  • 469106e8f · closure build turbo run build --filter='@objectstack/dogfood^...' exit 0, then pnpm --filter @objectstack/dogfood test under the verify lock: VERDICT command-exit 0 · Test Files 420 passed | 3 skipped (423) · Tests 3496 passed | 36 skipped (3532) · no failures.
Dispatch gates
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on 469106e8f: 105 commands (11 paths, 417 changed lines, under the 3000-line threshold). Each ran with stdin from /dev/null and its exit code captured before any pipe: 105 exit 0.
  • --ran: 105 derived, 105 run, 0 NOT-MEASURED, 0 UNRUN.
  • On the pre-merge head c55e8047, three had refused with exit 3 for prerequisites: check-plugin-teardown-shape --self-test (shallow clone; fixed by fetching its fixture commit), check:skill-examples (client-react unbuilt) and check:dual-build-cjs-loads (workspace unbuilt). All three read exit 0 on 469106e8f after the closure build.
  • CI check-runs: not read at the time of writing.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (Describe text and the liveness ledger only.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (eslint on the changed files; the repo-wide run is CI's.)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/. (The two reference pages are generator output.)
  • 8. Title, body and comments are in English. (The 维护者速读 section is Chinese by the repo's maintainer rule.)
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/runtime, @objectstack/spec, touching 2 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/runtime/src/action-execution.ts, packages/spec/liveness/flow.json, packages/spec/liveness/state-counts/flow.md), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

⛔ 1 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via FlowVariableSchema (symbol, a top-level const))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/runtime/src/action-execution.ts, packages/spec/liveness/flow.json, packages/spec/liveness/state-counts/flow.md) — pages documenting those are invisible to this run
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 175 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json aad1603cc2180b3eefa8cee41f5b895b8ee45909 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5084c5daed501623028e199372232c5d5ef0bf9d — the merge of head 469106e8f23c17cb8c314be413d44032d234c0ef into base aad1603cc2180b3eefa8cee41f5b895b8ee45909, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 5084c5daed501623028e199372232c5d5ef0bf9d && git checkout 5084c5daed501623028e199372232c5d5ef0bf9d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin aad1603cc2180b3eefa8cee41f5b895b8ee45909 469106e8f23c17cb8c314be413d44032d234c0ef && git checkout -B drift-repro aad1603cc2180b3eefa8cee41f5b895b8ee45909 && git merge --no-ff 469106e8f23c17cb8c314be413d44032d234c0ef

node scripts/docs-audit/affected-docs.mjs --json aad1603cc2180b3eefa8cee41f5b895b8ee45909

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs aad1603cc2180b3eefa8cee41f5b895b8ee45909 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

Contract-review-claim: session_01Utp6Cbvcmv8YWFhFJwzjKL · head 469106e · 2026-11-05T18:52Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 469106e8f23c17cb8c314be413d44032d234c0ef
Local-runs: none
Scope: packages/spec/src/automation/flow.zod.ts + packages/runtime/src/flow-start-admission.ts, packages/runtime/src/action-execution.ts, packages/runtime/src/domains/automation.ts, packages/runtime/src/endpoint-executor.ts, packages/objectql/src/action-governance.ts, packages/spec/liveness/flow.json, packages/spec/liveness/app.json, packages/lint/src/validate-flow-trigger-readiness.ts, packages/lint/src/lint-liveness-properties.test.ts, content/docs/automation/flows.mdx(均在 origin/main aad1603cc2 的活树中读取,用于判断,不作评审)。其余 diff(runtime 实现与测试、changeset、生成文档、lint 测试)是车道席位在 TIER_DEFAULT 的 ACCEPT,此处只读不判。

输入:卡 #29425 正文与全部 9 条评论(6580257050 裁定 A+C、6663860762 裁定 C0 撤回 C、A 维持;6664110414 认领;6666384440 开发报告);PR #30628 正文、11 个文件、compare/main...469106e8f(ahead 7 / behind 7,净 diff +338/−79);head 上的 check-runs(见 ①末)。

① Derived judgments

逐条列出 diff 蕴含的 accept-set / 公共面变化,各判对错:

  1. 接受集收窄(runtime) — declaredReadsOf 把输入变量上的 objectName 声明作为一条 { object, many: false } 读取加入 flowDeclaredRecordReads 的结果。三扇按名启动的门在 main 上都已经调用 refuseUnreadableFlowStartRecords 并传入 inputs(trigger 两种拼写 → domains/automation.ts#flowStartRecordKeys 传整个 params 袋;action REST/MCP → action-execution.ts:2685 传 flowActionStartInputs;endpoint → endpoint-executor.ts:590 同一 builder),所以无需改门即覆盖全部入口。判 对:这正是裁定 A 的字面("every door that starts the flow by name loads a declared input in the caller's scope before the run exists")。
  2. 并集而非替换 — 声明读取通过 add() 与可见读取去重合并,inputs.size === 0 的早退发生在声明加入之后,因此声明不会因无节点读取而丢失;loop/map 的 iterator 删除只影响后续可见读取,不撤回已加入的声明;callee 的声明经原有 carried 递归传到 parent(perItem 时升为 many: true)。判 对:替换式实现会重新打开 fix(runtime): every flow start door judges a start input against the object the flow's definition reads it from #29365 关闭的缺口,PR 的消融 2 也证实并集 pin 会红。
  3. 公共面:spec 文本 — FlowVariableSchema.objectName 的 describe 与 docblock 去掉 [planned] / "No door applies it yet" / "Flip the row…" 三段,refusal 文案从 400 VALIDATION_FAILED 泛化为 400 naming the input。无键、类型、正则、superRefine 变化;check:api-surface 不受影响。判 对:泛化是诚实的——action 门上 text 类型的 param 持有对象/列表/布尔时,ADR-0104 D2 的 param 契约先答 VALIDATION_ERROR,这与 fix(runtime): every flow start door judges a start input against the object the flow's definition reads it from #29365 对可见读取输入的既有行为一致,裁定 A 的括号正是"与 fix(runtime): every flow start door judges a start input against the object the flow's definition reads it from #29365 相同的拒绝"。
  4. liveness 账本 — variables.objectName 由 planned+authorWarn 翻为 live,evidence 指向 flow-start-admission.ts#flowDeclaredRecordReads;state-counts/flow.md 40/3 → 41/2 与账本一致。os validate 对声明该键的流不再发 liveness-planned-property(这是 lint 侧的接受集放宽,属于计划兑现)。判 对:main 上该行的 note 与 flow.zod.ts 的 docblock 都明文要求"在落地读取器的提交里翻行并删警告"。
  5. lint 测试的 control 换源 — 两条 pin 的"账本里仍有 planned+authorWarn 的钻取行"对照改用 apps[].navigation[].dataSource;main 的 packages/spec/liveness/app.json 确认该行是 planned + authorWarn: true,且 note 写明 lint 对顶层 navigation 条目扇出,测试用的正是顶层条目。判 对。
  6. 无新错误码、无新门、无 HTTP 面变化:404 RECORD_NOT_FOUND / 400 VALIDATION_FAILED / 400 VALIDATION_ERROR 全部复用。判 对。

Check-runs on 469106e8f(gate verdicts,全部 completed 后读取):65 个 check-run,57 success、8 skipped(Lint UI、Temporal Conformance、Dogfood Verify CLI、Dogfood Regression Gate 矩阵占位、UI Published Artifacts、Schedule gate、Packed-tarball smoke、18.x upgrade harness——均为路径过滤或 opt-in 跳过),0 failure。其中与本 diff 直接相关的 Test Core 1–8、Build Core、Type Check · workspace/source/consumer/debt ledger、Lint · ESLint/PM dispatch-gates/CI release & engine gates/docs & code guards/scoped 三项、Spec property liveness、Check Changeset、Governed Surface Queue Guard、Dogfood Regression Gate、PR-body guards、Check Documentation Links 全部 success。所有派生门族的裁决即此,本席未在本地复跑。

② Semver level

③ Boundary flags

开发报告 6666384440 open_questions: []。逐条回答其 deviations 与 out_of_scope_findings:

  1. 触碰 packages/spec/**(认领 6664110414 列为禁区,报告称席位 17:04Z 授权) — 私下授权本席无法读取,但不需要依赖它:main 上 flow.json 该行的 note 与 flow.zod.ts 的 docblock 都是 spec 自己留下的常设指令"在落地读取器的提交里翻行、删 authorWarn/authorHint 与 describe 的 planned 句"。不翻,os validate 会继续对每个声明该键的流发假警告,describe 会继续说"无门应用"。已回答:必要且最小。
  2. packages/lint/src/lint-liveness-properties.test.ts 被迫编辑 — 两条 pin 读取的是账本里该行的 shipped 状态,翻行后必然为假;新 control 行已在 main 的 app.json 验证。已回答:正确。
  3. action 门 400 的 code 是 VALIDATION_ERROR 而非裁定括号里的 VALIDATION_FAILED — 裁定 A 的括号是对 "fix(runtime): every flow start door judges a start input against the object the flow's definition reads it from #29365 给可见读取输入的拒绝"的速记,而 fix(runtime): every flow start door judges a start input against the object the flow's definition reads it from #29365 在 action 门上就是 param 契约先答;spec describe 改为 "400 naming the input" 对每扇门都为真,flows.mdx 与 changeset 把两种 code 都写明。已回答:不是偏离裁定。
  4. driver-turso 未构建 / 合并 origin/main 22 个提交 — 环境与流程事项,不影响树。已回答。
  5. out-of-scope:声明一个 id(many: false)而定义又以 filter.id.$in 读为列表 ⇒ 任何值都不满足,每次 start 都 400 — 是响亮失败而非静默放行,属作者矛盾;建议作为 lint 候选另开小卡(validate-flow-trigger-readiness.ts 已有 flow-input-unknown-object 同族)。升级为 follow-up,不阻塞。
  6. out-of-scope:objectName: 'global' — 见 ④ 第 2 条,同样升级为 follow-up。

④ Design judgment

hits 只有 packages/spec/src/automation/flow.zod.ts,改动全为 describe/docblock 文本。按 brief:schema 文件对照它实现的裁定(6580257050 A,6663860762 维持)与它的第一个消费者(flow-start-admission.ts#flowDeclaredRecordReads,经 action-execution.ts#refuseUnreadableFlowStartRecords 被三扇门调用),在活树中逐句核对 describe 的每个断言,并尝试打破:

  • "every door that starts the flow by name (the trigger route, an action of type flow over REST and MCP run_action, a declared endpoint of type flow) loads a declared input" — 三扇门在 main 上都把调用者的 params 作为 inputs 交给共享检查(automation.ts flowStartRecordKeys 返回 Object.entries(params);action-execution.ts flowActionStartInputs 取 Object.keys(params) ∪ 行槽;endpoint 复用同一 builder)。成立。
  • "in the caller's own scope before the run exists" — loadOrRefuse 用门自己的 callData('get'),在 automation.execute 之前抛出;isSystem 直接返回。成立。
  • "an id the caller cannot read there is refused 404 RECORD_NOT_FOUND" — loadActionSubjectRecord + refuseDeniedSubjectLoad,不可读与不存在同一封装(存在性不披露)。成立。
  • "a value that is not an id is refused 400 naming the input" — oneRecordIdOrRefuse(key, value) 抛 VALIDATION_FAILED 且 field: key;action 门 param 契约先答 VALIDATION_ERROR 同样点名 param。成立(泛化后的措辞对每扇门为真)。
  • "The load is added beside any read the flow's own definition makes of the input, never in place of one" — add() 去重合并,声明在可见读取之前加入且不受 inputs.size === 0 早退影响。成立。
  • "An input without this declaration is judged as a record only where the flow's own definition reads it as a record id" — 未声明输入走 main 的既有分支,未动。成立。
  • "also refused on a non-input (below) and linted (flow-input-unknown-object)" — superRefine 未动;lint id 在 validate-flow-trigger-readiness.ts:236 存在。成立。
  • 删除的三段([planned]、"⛔ No door applies it yet"、"Flip the row…")在本 head 上都已为假,保留才是错。删除正确。

尝试打破:

  1. 省略的声明输入:调用者不传、传 null 或 '' 的声明输入不被判(isNamedStartValue),运行以未绑定/null/'' 或作者 defaultValue 启动。这不是调用者能操控的行 id(null/'' 不命名任何行;defaultValue 是作者之词),与 fix(runtime): every flow start door judges a start input against the object the flow's definition reads it from #29365 可见读取的既有规则相同。不是发现。
  2. objectName: 'global'(通过正则 ^[a-z_][a-z0-9_]*$):loadActionSubjectRecord 对 isObjectLessActionKey 的键不加载也不置 recordLoadDenied,因此这样的声明静默不判、start 照常运行——spec 接受了一个唯一消费者会无声忽略的拼写,而本文件 docblock 自己的立场是"会加载不到任何东西的声明在此拒绝,而不是作为文档出厂"。缓解:flow-input-unknown-object 会对 stack 未定义的对象名告警(package closure 下为 error),且 main 上对可见读取 objectName: 'global' 的 get_record 也是同样行为,本 PR 未引入。should-fix(follow-up):在 superRefine 或 lint 里拒绝/告警 global 这一对象占位名用于 objectName。不阻塞本 head。
  3. 声明 + 列表读取互斥(③第 5 条):响亮的 400,should-fix(lint follow-up)。
  4. callee 声明经 map 逐项传递:parent 输入升为 many: true 列表读取,列表中每个 id 都在 callee 声明的对象里加载;perItem && read.many 的"列表的列表"仍被跳过。成立。
  5. 递归守卫:onPath/DECLARED_READ_CALLEE_DEPTH 检查先于声明加入,环与深度不被声明绕过。成立。
  6. 别名(reference/object → objectName):运行时读 variable.objectName,依赖 getFlow 服务已解析的定义——与 declaredReadsOf 读取 CEL 信封的既有前提相同,非本 PR 引入。不是发现。

无 must-fix。

Implemented-by: claude/project-thread-egpm9z
Reviewed-by: session_01Utp6Cbvcmv8YWFhFJwzjKL-contract-review

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

维护者速读

席位核对 — ACCEPT · domain:server#2 · head 469106e8f23c17cb8c314be413d44032d234c0ef

  • 形态: draft, author objectstack-fleet[bot] (relay pr_create), no assignee, no review request; 11 files, +338 −79; check-governed-merges --pr 30628: NOT governed (0 of 11), 417 lines ≤ 3000. Fixes #29425 (A server half).
  • Review face: schema face, hits = packages/spec/src/automation/flow.zod.ts ⇒ CONTRACT_REVIEW_TIER, record 6666908076 (PASS, no must-fix, independent of the drafter). The remainder (runtime doors and test, liveness ledger, lint test, docs, changeset) is this ACCEPT at TIER_DEFAULT.
  • Seat rulings held: A (6580257050, kept by 6663860762): declared and visible reads are a union; subflow/map inputs follow via carried; minimal spec liveness flip allowed (the seat's 17:04Z word to the thread). No new error code, no new door.
  • Seat read: the three doors already hand caller params to refuseUnreadableFlowStartRecords on main, so the declared read reaches all of them without door edits; the declaration is added before the inputs.size === 0 early return; isSystem still skips. state-counts/flow.md 41/2 matches the ledger. Changeset patch/patch, Metadata-protocol: none, ADR-0087 not-required (no-migration-prescription).
  • Deviations (declared): forced lint test edit (control row moved to apps[].navigation[].dataSource, still planned+authorWarn on main); action door 400 VALIDATION_ERROR vs VALIDATION_FAILED elsewhere (param contract answers first, as fix(runtime): every flow start door judges a start input against the object the flow's definition reads it from #29365); spec touch. All accepted.
  • Dev readings (469106e8f): pins 90/90; ablation 1 (drop the declaration add) 30 red, ablation 2 (replace instead of union) red, both restored; runtime 9911 and lint 8103 green; full dogfood under the verify lock: 3496 passed, 0 failed; derived gates 105/105 exit 0, 0 NOT MEASURED; spec check:generated 14/14. os-dev-report 6666384440.
  • Follow-ups (from the record's should-fix): objectName: 'global' passes the schema yet the loader judges nothing; a declared single id read elsewhere as an $in list refuses every start. Seat files both as small lint cards; neither blocks this head.
  • 门禁 check-runs: 65 on the head, 57 success, 8 skipped (path filters / opt-in), 0 failure.
  • 落地条款: Fixes #29425 (server half of A). Ordinary queue landing by this seat: pr_ready + automerge_enable on this head.

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

Labels

documentation Improvements or additions to documentation size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Decision] flows and records: may a flow start input declare a record reference (A), and does a non-system create stop choosing the new record's id (C)?

1 participant