Skip to content

service-automation: a decision condition accepts a CEL envelope that neither validator can see — a malformed one evaluates to false SILENTLY at run time and takes the wrong branch #15572

Description

@os-warren

Found by the Clause-② contract review of PR #15432 (card #15137) at CONTRACT_REVIEW_TIER, hunting past the four items it was sent to check. Filed by the domain:services execution seat (session 03324ae2-0f5b-5ad2-8a2e-cf4aaff5a909). ⛔ domain:*, type and priority are triage's — this seat does not produce them.

Pre-existing; not introduced by PR #15432 and outside its surface. That PR bound the value role on assignment. This is the predicate role on decision, which nothing in that PR touches.

The gap

Four declarations that are individually reasonable and jointly leave a hole:

DecisionConditionSchema.expression z.string()schemaless-node-config.zod.ts:381
the parse-time schema walk exempts decisionengine.ts:6907
the ledger's predicate arm emits strings onlyflow-node-expression-paths.ts:304
evaluateCondition at run time accepts { dialect, source, ast }engine.ts:8079

⇒ An authored decision condition written as an envelope is invisible to both validators and is nevertheless executed.

Measured, driven through the real doors

  • resolveFlowNodeExpressions('decision', …envelope…)[]
  • registerFlowREGISTERED, for source: ' ' and for the un-parseable 'rows.map(r,'
  • lint → 0 issues for both
  • at run time: the whitespace envelope returns false silently (engine.ts:8090); the bad-CEL envelope throws

The silent false is the serious half. A decision node whose condition quietly evaluates false takes the other branch, with no error anywhere — the flow runs to completion down the wrong path. The throwing case at least announces itself.

⚠️ One measurement in the above is weaker than it looks — the reviewer said so itself

The two "lint → 0 issues" readings were taken with another envelope as the control, i.e. a control that could plausibly be the answer, which makes it not a control. Those zeros are explained by reading flow-node-expression-paths.ts:304 (the arm emits strings only) and by the existing string-form tests at validate-expressions.test.ts:678 — not by a control that could have failed. ⇒ Whoever takes this should re-establish the lint half with a control that can go red. The registerFlow and run-time readings were driven and do not depend on it.

⭐ Worth keeping: the reviewer noticed and reported this about its own evidence rather than shipping the zero unqualified.

Not a defect, stated so nobody "fixes" it

A whitespace-only string predicate behaves consistently on both sides — no seam there. The hole is specifically the envelope shape reaching a slot declared as z.string().

The shape this belongs to

Three cards now describe one family — a declaration that two layers disagree about, where the disagreement surfaces only at run time:

Whoever triages may want to decide whether the first two are one card. ⚠️ Do not assume they are: #15430 is a shape both validators see and accept; this one is a shape both validators cannot see at all. Different mechanism, possibly one fix.

Refs: PR #15432 / #15137 (the review that found it) · #14149 (the maintainer ruling behind the value role) · #15430.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions