Skip to content

fix(automation): close the default-routable footgun on refuse-to-execute guards (#3863) - #3889

Merged
os-zhuang merged 2 commits into
mainfrom
claude/guard-refusal-chokepoint
Jul 28, 2026
Merged

fix(automation): close the default-routable footgun on refuse-to-execute guards (#3863)#3889
os-zhuang merged 2 commits into
mainfrom
claude/guard-refusal-chokepoint

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Why

#3881 stopped a fault edge from swallowing a guard refusal, keyed on NodeExecutionResult.errorClass. That field defaults to 'runtime' — the right call for compatibility, since every executor written before the split keeps its routing — but it leaves the footgun pointing the other way:

A new refuse-to-execute guard is routable unless its author remembers to classify it, and forgetting is silent.

Nothing in the type system catches that. The property #3881 secured can be un-secured by omission, which is the weakest possible way to hold a safety guarantee.

Auditing for it also turned up five guards that were already unmarked.

What changed

1. refuseNode(reason) — one act, not two

Writing a guard and marking it un-routable become the same call:

if (!url) return refuseNode('http: url is required');

Its doc states the test for using it — re-running unchanged can never succeed AND the fix is to edit metadata — and states the inverse just as plainly, because over-marking is not the safe direction: classifying a handleable condition as guard turns a recoverable integration into a dead run.

2. Five guards that were never marked

All are a missing required config key or a defective graph; none can succeed on a retry:

Guard Why it is metadata, not the world
http with no url required config key
subflow with no config.flowName required config key
subflow past max nesting depth a recursive graph nests exactly as deep next run
map with no config.flowName required config key (the per-item subflow)
connector_action with no connectorId/actionId required config keys

The seven crud-nodes guards from #3881 move to the helper — same behaviour, one spelling.

3. A behavioural inventory test

Every known guard is driven through the engine with a fault edge attached and asserted still fatal — matching on the refusal text, so a guard that starts failing for a different reason cannot pass vacuously.

Verified to have teeth: temporarily un-marking the http guard fails its row immediately (1 failed | 13 passed), then passes again once restored. A test that pins a safety property is worth little unless you have watched it fail.

The negative half is pinned in the same file — a plain node failure and a thrown error must still route, since that is what fault edges exist for.

Deliberately not marked

Judgment calls, recorded so they are not silently revisited:

Each is a condition the world caused, and an author must be able to handle it.

Considered and rejected

Making errorClass required on NodeExecutionResult would enforce classification at compile time — the genuinely airtight fix. Measured the blast radius: 281 non-test success: false sites across the repo (58 in automation) plus 82 in tests, and it would break third-party executors at compile time. That is a large, noisy change for a type-only gain over the helper, so it is out of scope here; if the executor surface is ever revised for other reasons, it is worth folding in then.

Verification

  • packages/services/service-automation: 421 passed, no regressions (was 407 before this branch's 14 new tests).
  • ESLint clean on all changed files; check-role-word gate OK.
  • tsc --noEmit on the package: 2 errors, both pre-existing in engine.test.ts about resumeAuthority — unchanged from the base.

Generated by Claude Code

…ute guards (#3863)

#3881 stopped a `fault` edge swallowing a guard refusal, keyed on
`errorClass`. That field defaults to 'runtime' — right for compatibility, since
every executor written before the split keeps its routing — but it leaves the
footgun pointing the other way: a NEW guard is routable unless its author
remembers to classify it, and forgetting is silent.

`refuseNode(reason)` returns a guard-class failure, so writing a guard and
marking it un-routable become one act. Its doc states the test for using it
(re-running unchanged can never succeed AND the fix is to edit metadata) and the
inverse, because over-marking turns a recoverable integration into a dead run.

Five guards that were never marked are now un-routable — all missing required
config or a defective graph, none able to succeed on a retry:

  - `http` with no url
  - `subflow` with no flowName, and subflow past max nesting depth (a recursive
    graph nests exactly as deep next run)
  - `map` with no flowName
  - `connector_action` with no connectorId/actionId

The seven crud-nodes guards from #3881 move to the helper — same behaviour, one
spelling.

A behavioural inventory test drives every known guard through the engine with a
fault edge attached and asserts it stays fatal, matching the refusal text so a
guard failing for a different reason cannot pass vacuously. Verified to have
teeth: un-marking one fails its row immediately. The negative half is pinned
too — a plain failure and a thrown error must still route.

Deliberately not marked: a degraded connector (#3017 recovers automatically), a
collection that did not resolve to an array, a collection over the iteration
cap, a subflow that failed on its own. The world caused those.

Considered and rejected: making `errorClass` required on the result type —
compile-time enforcement, but it breaks 281 call sites plus third-party
executors for a type-only gain over the helper.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EdCvGtER9SzJopS4Yh3Pa1
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 28, 2026 2:33pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Jul 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): packages/services.

6 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/automation/webhooks.mdx (via packages/services)
  • content/docs/kernel/runtime-services/audit-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/index.mdx (via packages/services)
  • content/docs/kernel/runtime-services/settings-service.mdx (via packages/services)
  • content/docs/plugins/packages.mdx (via packages/services)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/services)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

The #3881 callout in flows.mdx and the automation skill enumerated the guards
that existed at the time. This PR adds five more, which makes an enumeration the
wrong shape — it was already stale by the end of its own branch.

Both now state the TEST instead: a failure is a guard when re-running unchanged
could never succeed AND the fix is to edit metadata. The examples stay, as
examples rather than as the definition, and the runtime side gains the cases
this PR deliberately left routable (a degraded connector, a subflow that failed
on its own) so the two lists read as one contract.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EdCvGtER9SzJopS4Yh3Pa1
@os-zhuang
os-zhuang marked this pull request as ready for review July 28, 2026 14:46
@os-zhuang
os-zhuang merged commit 5602211 into main Jul 28, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the claude/guard-refusal-chokepoint branch July 28, 2026 14:46
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants