Skip to content

[Decision] One seam, two strictness levels: POST /api/v1/automation hard-refuses an undeclared node config key but accepts an unknown node type #7545

Description

@huangyiirene

Background

The flow-node-type-matrix checklist item records a FAIL, but it is not a bug and not a regression — it is a checklist-vs-implementation contract disagreement where the implementation side has explicit, documented rationale. The QA run says so itself: this needs a maintainer ruling, not a fix ticket.

POST /api/v1/automation with a node of type: 'bogus_node' returns 200 and really registers the flow.

Crucially it is not silent, which was the #1887 anti-goal:

  • registration logs a WARN naming the offending flow, listing unknownTypes and the full knownTypes vocabulary;
  • triggering the flow answers success: false with "No executor registered for node type bogus_node";
  • the run is recorded failed.

So the failure is loud, located, and observable at three points. The disagreement is purely about when — registration time versus execution time.

The rest of the item is strong: 20 of 21 node types proven executed success across a 235-step sweep over ~100 runs; region tagging (loop-body / try / catch / parallel-branch, plus iteration and parentNodeId) all correct; every step names its nodeType, none missing. The one other gap is that end never appears as an executed step, only as a skipped branch target.

Premises

Each premise is independently re-checkable; the command to re-check it is on its own line.

P1 — Registration accepts an unknown node type and logs a WARN rather than refusing. The check is deliberately gated on a sealed vocabulary and on the flow being enabled, and warns once per offending flow.

rg -n "nodeTypeVocabularySealed|warnUnknownNodeTypes|unknownNodeTypes" packages/services/service-automation/src/engine.ts

P2 — The soft-fail is deliberate and documented, not an oversight.registerFlow() checks membership at that seam and stays soft-fail so that a flow authored against a currently-absent plugin still registers; the rationale is written into the code, into ADR-0018 §M1, and into #4771 (which fixed the ordering bug where approval flows were falsely warned about because validation ran before plugin executors registered).

rg -n "M1" docs/adr/0018-unified-node-action-registry.md

P3 — The WARN is genuinely informative, carrying both the unknown names and the whole known vocabulary. The phrase "no registered executor or descriptor" is load-bearing — tests and log filters count per-flow findings by it.

rg -n -A6 "references node type\(s\) with no registered executor" packages/services/service-automation/src/engine.ts

P4 — The same endpoint HARD-REFUSES an undeclared node config key.#4277 shipped the error half of the #4045 unknown-key ladder (#4059 shipped the warn half), with a located message listing the declared keys.

rg -n -B4 -A12 "REJECT node .config. keys" packages/services/service-automation/src/engine.ts

P5 — There is prior art on this seam's coverage being incomplete.#4389 (closed) recorded that registerFlow's three validators walked top-level nodes only, so a region's malformed structure, unknown node type, and undeclared config key all passed registration. That was about traversal; the present question is about strictness policy. Worth reading before ruling so the two are not conflated.

gh issue view 4389 --repo objectstack-ai/objectstack

The question

One seam, two strictness levels.POST /api/v1/automation refuses an undeclared node config key outright, but accepts an unknown node type.

That is hard to defend as a coherent contract. The config-key rule says "what the descriptor does not declare, the door does not accept". The node-type rule says "what no executor provides, the door accepts anyway, and you find out when it runs". A flow author hitting both rules on the same POST learns that declaredness is enforced for the inner structure of a node but not for the node's identity — the stricter check guards the less consequential mistake.

Is that split intentional and worth keeping, or should the two arms be aligned?

Options

Option A — Keep soft-fail; revise the checklist item to match

Accept the documented rationale as the contract and rewrite the flow-node-type-matrix clause to assert what the platform actually promises: registration succeeds, a located WARN is emitted naming unknownTypes + knownTypes, and triggering fails with a named NO_EXECUTOR error and a failed run.

  • 实际业务需求 — this is the only option backed by measured pull rather than speculation. bug(service-automation): flow 节点类型校验跑在插件贡献的执行器注册之前 —— 每个 ADR-0019 approval flow 都被误报「will fail at execution time」 #4771 is a real, closed incident where premature strictness at this seam falsely condemned every ADR-0019 approval flow. The plugin-absent case is not hypothetical; it has already bitten once. That said, the measured pull is for not hard-refusing at boot ordering time, which is narrower than "never refuse".
  • 项目长远合理性 — cheap and honest, and it keeps a genuine capability (author a flow against a plugin you will install later). Its weakness is that it leaves the incoherence in place permanently: the config-key/node-type asymmetry becomes a documented feature that every future reader has to be told about.
  • 防 AI 写代码/元数据犯错 — the weakest on this axis. An agent authoring a flow with a hallucinated node type gets a 200. The WARN lands in a log the agent is not reading, and the failure surfaces only when someone triggers the flow — possibly in production, possibly long after the authoring session ended. Consumer-side tolerance (the run fails cleanly) is doing the work that author-side enforcement should do.

Option B — Harden to match the config-key strictness

Refuse an unknown node type at registration, with the same shape as the #4277 config-key refusal: a located message naming the offending node and listing the known vocabulary.

Option C — Make it configurable

A strictness setting: refuse by default, soft-fail when a deployment opts in (for the plugin-absent case).

  • 实际业务需求 — no measured pull for the knob. Nobody has asked for the choice; the QA run asked for a ruling.
  • 项目长远合理性 — the worst of the three. It converts a contract question into a configuration matrix, doubles the behaviours to test and document, and guarantees that "does this instance refuse unknown node types?" becomes a question no one can answer without checking config. Deferring a decision by making it settable is how one seam becomes two contracts.
  • 防 AI 写代码/元数据犯错 — actively harmful. An agent cannot rely on a guarantee that varies per deployment, so it must code against the weakest setting — which means the strict setting buys nothing at author time while costing everything in complexity.

Recommendation

Option B, with the #4771 escape hatch preserved explicitly.

The three axes do not point the same way and the honest reading is that they trade off:

So: harden the default to refuse, mirroring #4277's message shape (name the node, name the type, list the known vocabulary). Then keep the plugin-absent capability alive deliberately rather than by accident — the cleanest form is for a flow to declare that it depends on a plugin, so an unknown type covered by a declared-but-absent plugin degrades to today's WARN while an undeclared unknown type is refused. That preserves the one measured need without preserving the typo hole.

If that declaration mechanism is judged too much scope for now, take Option A rather than Option C — and rewrite the checklist clause to assert the WARN-plus-named-runtime-failure contract, so the item stops reporting FAIL against a promise the platform never made. A wrong-but-documented contract is recoverable; a per-deployment contract is not.

⛔ Not Option C in any case.

Source

Extracted from the QA run #7516 (framework a86db17). Checklist item flow-node-type-matrix, recorded there as one of the two FAILs that are contract decisions rather than defects.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions