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.

Activity

  1. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    Triage: domain:services appended; decision state untouched (disjunction-5 routing repair — a decision card with no lane has no seat to execute the ruling once it lands).

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


    Generated by Claude Code

  2. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    Maintainer ruling recorded 2026-08-11 (PM session, executing the maintainer's direct instruction in chat — verbatim: 「接受你的全部建议,请更新 issue 的状态和标签」, accepting the four-lens decision-inbox review in full).

    Ruling: status quo upheld. Registration-time soft-fail on an unknown node type is the intended posture. The rationale is real and documented (ADR-0018 §M1: a flow authored against a currently-absent plugin must still register; #4771 fixed the ordering bug this protects against), and the failure is already loud at three points (registration WARN with the full vocabulary, trigger answers success: false, run recorded failed). This is deliberate late binding with a declaration — not tolerance masking an error, so the hard-refuse instinct does not apply here.

    Remaining scope of this card: record this ruling in the flow-node-type-matrix checklist clause so future runs read the 200-on-unknown-type as pass-by-design, then close.

    State: needs-user-decision → pm:queue (domain:services seat, checklist-recording scope only — zero engine code).


    Generated by Claude Code

  3. self-assigned this
    on Aug 11, 2026
  4. os-help commented on Aug 11, 2026

    @os-help
    Collaborator

    Claim: PM loop, services lane, wave 2 round 4
    Session: session_015fkdTyGmMD5s8ZtEifvuGy
    Branch: claude/issue-7545-node-type-clause-ruling
    Worktree: objectstack-issue-7545
    Domain: domain:services
    File surface: docs/qa/platform-checklist/areas/automation.json (the flow-node-type-matrix clause) — recording the maintainer's status-quo ruling so future runs read 200-on-unknown-type as pass-by-design. Zero engine code, per the ruling. Stop on breach.
    Container & model: S-grade but prose-judgment (the clause must state the WARN + named-runtime-failure contract accurately — its failure mode is wording, not gates), mode:subagent, model: opus per the borderline-goes-up rule.
    Serial constraints cleared: ⚠️ #7546 (sibling ruling, flow-error-handling clause) lives in the SAME file and is deliberately held to the NEXT round — this card lands first, alone, per the same-file serial rule. No other in-flight claim touches docs/qa/platform-checklist/.


    Generated by Claude Code

  5. os-help commented on Aug 11, 2026

    @os-help
    Collaborator

    <!-- os-dev-report -->
    os-dev-report

    {
      "issue": 7545,
      "status": "done",
      "branch": "claude/issue-7545-node-type-clause-ruling",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/7664",
      "premise_still_valid": true,
      "summary": "Premise verified before editing: the flow-node-type-matrix item's negative clause did demand a registration-time refusal ('must be REFUSED at registerFlow / POST /api/v1/automation'), a promise engine.ts and ADR-0018 §M1 never made — so run #7516's FAIL was against a non-existent contract. Recorded the maintainer's status-quo ruling checklist-side only, zero engine code: the refusal expectation is replaced by an acceptance clause asserting the real four-part late-binding contract (200 + located WARN carrying 'no registered executor or descriptor' with unknownTypes and the full knownTypes vocabulary + trigger success:false with the NO_EXECUTOR message + run recorded 'failed'), stating pass-by-design explicitly and citing #7545 / ADR-0018 §M1 / #4771; the negative is rewritten to name SILENCE as the failure shape with the 200 explicitly disclaimed as a finding. Also added the driving step the clause needs for evidence, the ADR/engine/ruling source anchors, and revision 2 to 3 with the matching history entry. Two accuracy corrections against engine.ts beyond the ruling text: unknownTypes/knownTypes ride the log record's structured meta slot rather than the message (#6654 — a runner grepping the message would wrongly fail the item), and the WARN is gated on BOTH nodeTypeVocabularySealed and the flow being enabled. Did NOT touch flow-error-handling (sibling card #7546). coverage.json indexes this item by id only, no expectation text — grepped and confirmed nothing to keep consistent.",
      "tests": "node scripts/checklist-select.mjs --self-test -> '✓ checklist-select self-test: 17 cases pass.' | node scripts/checklist-select.mjs automation.flow-node-type-matrix -> 'selector: automation.flow-node-type-matrix → 1 runnable item(s) / P1 mixed automation.flow-node-type-matrix' | node scripts/check-nul-bytes.mjs -> 'OK (scanned 7089 text file(s) -- 7089 tracked, 0 untracked-not-ignored; skipped 5 binary; no raw ASCII control bytes)' plus a targeted self-scan of the edited file with grep -naP for the control-byte class: no hits. node scripts/check-platform-checklist.mjs (the pnpm check:platform-checklist gate) reports exactly 1 problem — 'coverage.json · qa: UNCLASSIFIED' — which is PRE-EXISTING on main and unrelated to this item: reproduced byte-identically on a pristine origin/main worktree with this PR's changes absent, and already filed as open issue #7347. The item's own structural checks pass (revision==last history revision, every acceptance clause names an oracle, enumSource variants ratchet). JSON re-parsed clean: revision 3, 6 acceptance clauses, 1 negative, 10 steps. Per the card's explicit '⛔ no idle-polling CI / report at draft-PR time', CI was not waited to convergence: at report time ESLint and TypeScript Type Check are in_progress, Test Core / Dogfood Regression Gate / Check PR Size / Console Pin Freshness / ADR maintainer approval are green.",
      "open_questions": [],
      "out_of_scope_findings": [
        "NOT filed (already tracked): check:platform-checklist is red on main with 'coverage.json · qa: UNCLASSIFIED — the platform has this capability (liveness ledger exists) but the checklist neither tests nor waives it'. Searched open issues before filing and found the exact match — #7347, open since 2026-08-10. Commented nothing there; it needs no new signal. Flagging it because this PR's check:platform-checklist job will be red for that reason and not for this change."
      ]
    }

    Notes for the PM, outside the JSON:


    Generated by Claude Code


    Generated by Claude Code

  6. os-help commented on Aug 11, 2026

    @os-help
    Collaborator

    PM review — ACCEPT (session a31dbd89, PR #7664)

    Scope check: exactly what the ruling ordered — checklist-recording only, zero engine code, 1-file surface (docs/qa/platform-checklist/areas/automation.json, +20/−4). Verified against the diff:

    Driving PR #7664 to green now; this issue auto-closes on merge.


    Generated by Claude Code

  7. added a commit that references this issue on Aug 17, 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