Repository navigation
Description
Written against 40cc365 (2026-09-02, merge of #1424) and against cel-go at the pin (github.com/google/cel-go v0.31.0), the v0.32.0 tag, and upstream master at f8478f1. Third audit beside #1459 (how a value's type travels) and #1421 (how a name resolves): this one reads how an expression is planned and executed — cel-go's interpreter package (the planner, Activation, Attribute/Qualifier, InterpretableDecorator, ExecutionFrame, the cost and memory trackers) against the one evaluator in pkg/flowstate/v1/celenv.go and the activation in eval.go — and what moved upstream in August 2026. Every claim in the sub-issues cites file:line at that revision, a go doc result against the pinned module, or a measurement reproduced in this pass.
What holds
One builder (buildEnv, celenv.go:748), one program cache, one cost estimator installed where both drivers build a program (Limits.programOptions, celenv.go:80), one custom interpreter.Activation both drivers fill through Scope.Activation (nodes.go:93), and a profile pinned per run that every evaluation site resolves from the spec. The ordered-map rewrite (celmap.go) is the one evaluator-side rule and it is applied as an AST rewrite before planning, which keeps both drivers and the stored specification honest. ExecutionFrame (interpreter/frame.go), ConcurrentEval and async functions are already in the pinned version and correctly unused: Flowstate's cancellation contract is ContextEval plus InterruptCheckFrequency, and an async function would be I/O inside workflow code.
What the audit found
- The runtime plans parsed ASTs against a variable-less environment. Every reference is a maybe-attribute probed three times; every call has an empty overload id, which is why
byteCostEstimatorexists and why plan-time regex compilation and static cost estimation are unreachable. cel: both drivers plan parsed ASTs against a variable-less environment — everysteps.x.yis a three-candidate maybe-attribute, every call carries an empty overload id, and cel-go's overload-aware costing, plan-time regex compilation and static cost estimation are unreachable #1481. - Answering the
stepsroot whole is eager. One field read converts every prior output: 21 ms and 24 MB per evaluation at 1,000 steps, measured. eval: onesteps.<id>.<output>read converts every prior step's every output into a CEL value — 21 ms and 24 MB for one field at 1,000 steps, linear in what the run has accumulated, on both drivers #1482. Limitsis two knobs. Seven cel-go bounds are unset (node count, nesting depth, comprehension and bind nesting, regex program size; parser size and recursion sit at cel-go's defaults), the memory bound does not exist at the pin, and the five policy compilers set their own three. cel:Limitsis two knobs (cost, interrupt frequency); the pinned cel-go exposes nine bounds and the five policy compilers set three of their own — decide each bound once, including the memory bound no site has #1483.- Compaction reimplements partial evaluation by hand.
refs.goretains every step when it cannot name one;cel.PartialVarsand unknown attribute trails name them, dynamic indexes included. engine: Continue-As-New compaction rediscovers references with a 394-line walker over the proto AST and retains every step whenever it cannot name one; cel-go's partial evaluation names the exactsteps.<id>.<output>trails an expression touches, dynamic indexes included #1484. EvalDetailsis discarded at every site. Cost is measured everywhere and reported nowhere;OptTrackStateis the traceflow policy explain, DAPevaluateand check witnesses approximate by re-evaluation. cel: every evaluation site discardsEvalDetails— actual cost is measured on every expression and reported nowhere, andOptTrackStatewould giveflow policy explain, DAPevaluateand check witnesses a real per-node trace instead of a re-evaluation #1485.- The profile is a string slice.
Env.ToConfigrenders it as data cel-go's own tools load; cel: eight of eleven profile libraries are version-unpinned — the strings pin's own doctrine, unapplied to the rest of 2026.1 #1358's "cel.md unchanged" becomes a structural diff. cel: the profile is a Go string slice and a generated markdown table; cel-go'sEnv.ToConfigrenders the same environment as a YAMLenv.Config— pin 2026.1 as data, feed it to the REPL and the editor, and make #1358's "cel.md unchanged" a structural diff #1486. - Upstream moved.
cel.dev/cel-goat v0.32.0; per-operation cost observation (the cel: the runtime cost observer makes every large comprehension quadratic in wall time — a 50k-element map costs ~2.5s CPU while the budget reads it as affordable #1357 mechanism), memory tracking,common/cost, type aliasing, and a batch of runtime names landed after the tag. The bump is an interpreter change under invariant 4, gated on cel: eight of eleven profile libraries are version-unpinned — the strings pin's own doctrine, unapplied to the rest of 2026.1 #1358: cel: the pin is google/cel-go v0.31.0; upstream moved to cel.dev/cel-go at v0.32.0 and thirty-one commits past it rewrote cost, memory and observers — decide the bump as a versioned interpreter change, after the pins #1480. The runtime names are screened against the profile rule row by row, including the refusal of HMAC/JWT in the workflow dialect: cel: what cel-go grew past the pin, screened by the profile's own admission rule —json.parsewith a target type,hasValue(v),hasOnly/hasAny/hasAll/hasExactly, base64url, HMAC/JWT helpers, timestamp parsing,maps.merge, cross-type numerics #1487. - Policy composition exists upstream. cel-go's policy compiler implements what Policy: one vocabulary, composable modules, a test verb, and enforcement below the layer that can bypass it #548 sketches; decide adopt, borrow or refuse before a module system is written. policy: cel-go's own policy compiler already implements the composition #548 sketches — variables, nested first-match rules, typed outputs, source-preserving composition, a conformance runner — evaluate it for the five surfaces before writing a module system #1488.
The slate, in the order the pass recommends
Independent, landable now (no upstream change needed):
- eval: one
steps.<id>.<output>read converts every prior step's every output into a CEL value — 21 ms and 24 MB for one field at 1,000 steps, linear in what the run has accumulated, on both drivers #1482 — lazystepsroot: a bounded-work defect on both drivers, measured. - cel: both drivers plan parsed ASTs against a variable-less environment — every
steps.x.yis a three-candidate maybe-attribute, every call carries an empty overload id, and cel-go's overload-aware costing, plan-time regex compilation and static cost estimation are unreachable #1481 — check before plan withdyndeclarations only; deletes the debugger's copy ofenvDeclaring. - cel:
Limitsis two knobs (cost, interrupt frequency); the pinned cel-go exposes nine bounds and the five policy compilers set three of their own — decide each bound once, including the memory bound no site has #1483 — decide the eight bounds that exist at the pin; route the five policy sites throughLimits. - cel: every evaluation site discards
EvalDetails— actual cost is measured on every expression and reported nowhere, andOptTrackStatewould giveflow policy explain, DAPevaluateand check witnesses a real per-node trace instead of a re-evaluation #1485 — keepEvalDetails: cost into spans and metrics, one redacted per-node trace for the explain surfaces. - cel: the profile is a Go string slice and a generated markdown table; cel-go's
Env.ToConfigrenders the same environment as a YAMLenv.Config— pin 2026.1 as data, feed it to the REPL and the editor, and make #1358's "cel.md unchanged" a structural diff #1486 — the profile asenv.Config, pinned and round-tripped.
Upstream-gated:
6. #1358 (existing) — pin the eight open libraries, read from v0.31.0's gates.
7. #1480 — the bump, as a build-id change, byte-identical for 2026.1; brings the memory bound (#1483's last row) and the cost-observer fix (#1357). Note the comment there: three dependencies still import github.com/google/cel-go, so the rename links two copies of cel-go until protovalidate moves.
8. #1484 — partial evaluation for compaction; the comprehension case waits on cel-expr/cel-go#1341.
9. #1487 — the next profile's membership, decided from the screen after 6 and 7.
Decision record:
10. #1488 — cel-policy for the five surfaces, before #548's module system.
Second pass, same revision
Filed after the first slate, from the surfaces the first pass touched but did not open. Each was reproduced or counted before filing.
Bugs, reproduced with go run ./cmd/flow:
- flowfile: an invalid regex, duration or timestamp literal compiles clean — the compiler's
env.Checkruns none of cel-go's AST validators, so the run fails whereflow validatesaid ok #1500 —flow validatesaysokto'abc'.matches('['),duration('3 days')andtimestamp('yesterday'); the run fails on the first. cel-go's literal validators (ValidateRegexLiterals,ValidateDurationLiterals,ValidateTimestampLiterals) are unused anywhere in the tree. - flowtest: a
check:claim written with optional syntax is refused at load with "unsupported syntax '.?'" — claims are parsed against the library-less base environment and evaluated against the run's profile #1512 —flow testrefuses acheck:claim written with.?at load ("unsupported syntax '.?'") while the workflow it tests uses the same idiom: claims are parsed against the library-less base environment and evaluated against the run's profile.
One mechanism:
- dsl: CEL AST traversal is re-derived in 59 functions over the v1alpha1 proto across 20 files — the child enumeration exists in three full copies and every walker re-lists the expression kinds; cel-go's
common/astis the one traversal, and three files already use it #1501 — 59 hand-written walkers over the v1alpha1 proto AST in 20 files, the child enumeration written out three times; cel-go'scommon/astis the traversal and three files already use it. - dsl:
must:is the one expression position stored as source text — re-parsed and re-checked on every compile, refused by a walker no other position uses, and evaluated through the uncached path with anenv.Extendper check #1503 —must:is the one expression position stored as source text: re-parsed and re-checked per compile, refused by its ownnowwalker, evaluated through the uncached path.
What the checked AST could say at validate:
- flowfile: the compiler checks every expression and never asks what it will cost — cel-go's static estimator, with
max_items/max_lenas size hints, can refuse at validate an expression that cannot fitDefaultCostLimitand report the bound inflow audit#1504 — static cost estimation (Env.EstimateCost) withmax_items/max_lenas size hints;flow auditreports the bound,flow validaterefuses the expression whose minimum cost exceeds the budget.
Tooling the existing data already supports:
- lsp: signature help is unadvertised while the catalog already renders one signature per overload — the data
celcompletesays "is not available" is whatdocs/reference/cel.mdprints in its Signature column #1506 — LSP signature help fromLibraryFunction.Signature, which hover anddocs/reference/cel.mdalready render. - flowdap: breakpoints are by step id and frames carry no source because
v1.Nodehas no position — the compiler'sPositionsside table already maps step ids to spans for the launched file, so line breakpoints and navigable frames are a lookup away #1507 — DAP line breakpoints and navigable frames from the compiler'sPositionsside table, the sliceflowdap.go:19-47names.
Pinning semantics, not only names:
- cel: profile 2026.1's semantics are pinned by a function listing and an examples corpus, not by the language's own conformance suite —
cel.dev/expralready ships the textproto cases for every library the profile admits, and it is already ingo.mod#1509 — runcel.dev/expr's conformance textprotos (already ingo.modas an indirect dependency) for the profile's libraries throughEvaluator, with a reviewed skip list.
Checked and not filed: plugins evaluate no CEL in their own process (cel-go is an indirect dependency of the six plugin modules and no plugin or SDK file references it), so there is no second evaluator across the socket; the macroExamples hand table is correctly hand-written (cel-go's Macro.IsReceiverStyle cannot distinguish a namespace receiver from a value receiver, as catalog_functions.go:261-273 records); the default time zone is UTC unless DefaultUTCTimeZone(false) is set (cel/env.go:904 at v0.31.0), so timestamp accessors are worker-independent.
Third pass: the protobuf side
The second evaluator the second pass said did not exist does, one dependency over.
- validate: protovalidate is a second CEL evaluator in the binary — its own environment, no cost limit, no interrupt,
nowbound, cross-type numerics on — and plugin-authoredcelrules already run through it atflow validateand in the editor #1531 — protovalidate is a second CEL evaluator in the binary: its own environment (cel/library.go:66-90inbuf.build/go/protovalidate@v1.3.0: unpinnedext.Strings,nowbound,CrossTypeNumericComparisons(true)), no cost limit, no interrupt, no option to add one — and plugin-authored(buf.validate.field).celrules already run through it atflow validate, in the LSP on every change, and in the MCP validate tool (flowfile/schema.go:410-432builds adynamicpbmessage from the plugin's descriptor and callsv1.Validate). plugin: the descriptor's own protovalidate rules are checked for literals atflow validateand never at dispatch — every first-party plugin hand-writes its bounds again, and the refusal crosses as free text with no field #1491's open question about dispatch-timecelrules is this decision, and it applies today. Comment on cel: the numeric model is split three ways — the checker refuses int/double mixing the runtime performs behind dyn, JSON numbers arrive as double, and YAML can mint a uint nothing can add to #1432 records the numeric divergence. - Design: workflow input constraints are 1,559 lines of hand-written Go beside protovalidate, which already validates task inputs from descriptors — the declarations spell
buf.validate's names andmust:spells itsthis; decide whether a declaration becomes a descriptor #1534 — workflow input constraints are 1,559 lines of hand-written Go (constraints.go) beside protovalidate, which validates task inputs from descriptors; the declarations spellbuf.validate's names (docs/DSL.md:4115-4117) andmust:spells itsthis. Decide whether a declaration compiles to a descriptor, with the dialect and bound tensions stated. - schema:
Node.Outputs.named_valuesisrequired, so the wire cannot say "a step produced no outputs" — the server substitutes an empty map for an absent signal payload, and four engine sites build the empty message the rule forbids #1537 —Node.Outputs.named_valuesisrequired, so the wire cannot say "no outputs"; the server substitutes an empty map for an absent signal payload (server/lifecycle.go:641,:728) and four engine sites build the message the rule forbids. Relax the rule or refuse the sites.
Checked and not filed: payload encoding is already decided (#911, binary protobuf since #1196); task claims as descriptor options is #1493; the *.test.yaml and policy-file formats as schema messages is #923's question (a comment on #1488 records that the policy documents are Go structs too); secret-typed inputs are #535's family.
Fourth pass: options, prose and the agent surface
Where the schema's own comments and #729's options package should be the one source, and are not yet.
- dsl: the Flowfile grammar's keys and their prose are hand-kept in
parse.go, a 65-entry LSP completion table and DSL.md, beside the schema's own comments — derive the key table, hover, a generated grammar reference and a JSON Schema fromworkflow.protothrough oneyamloption #1542 — the Flowfile grammar's keys and prose are hand-kept three times:parse.go's key lists, a 65-entry LSP completion table (lsp/completion.go:96-278), and DSL.md — besideworkflow.proto's own field comments; no generated grammar reference page and no JSON Schema exist. Oneyamloption on Design: flowstate/options/v1, a minimal congruent custom-options package modeled on protovalidate and google.api, composable with plugins #729'sFieldRules(key and STYLE.md R4 position class), one walker, four projections: the compiler's tables, hover and completion,docs/reference/flowfile.md,flowstate://schema/flowfile.json. - mcp: the eighteen derived tools carry no annotations —
Cancel,Terminate,SignalandRunlook identical toValidateto an agent's client; derivereadOnlyHint/destructiveHint/idempotentHintfrom method options and giveflow mcp servea read-only posture #1543 — the eighteen derived MCP tools carry noToolAnnotations:TerminateandValidatelook the same to an agent's client. Derive read-only, destructive and idempotent hints fromMethodRules, and giveflow mcp servea--read-onlyposture backed by scope rather than hints. - ci: hover,
flow tasks, the MCP catalog anddocs/reference/tasks.mdare rendered from schema comments, andbuf lintdoes not require one — 72 of 606 fields inflowstate/v1have no comment, including every arm ofValue#1544 — hover,flow tasks, the MCP catalog anddocs/reference/tasks.mdrender schema comments, andbuf lintdoes not require one: 72 of 606 fields inflowstate/v1are uncommented, every arm ofValueamong them; enable theCOMMENTSrules here and in the plugin SDK's recipe.
Comments rather than issues: on #1493, that a plugin's example should be a flowstate.v1.Value literal validated against the field's own rules (as InputDeclaration.example already is) rather than a second literal vocabulary, and that a bounded CEL snippet option compiled at manifest load would make plugin documentation executable the way TestEveryDocumentedCELIdiomCompiles makes the reference docs; on #1452, that the TODO(kent) at value.proto:160-167 is the schema's only in-line statement of the value-kind question and should be retired by that decision.
Checked and not filed: diagnostic codes travel as strings by a recorded decision (diagnosticcodes.go:28-40); plugin comments already travel in the manifest (#723, closed by #874).
Fifth pass: hands-on, from the outside
A two-file workload (a caller with typed inputs, a for_each over call:s, a signal gate, a switch:, typed enum and struct outputs, and a test file) was written against the same build and pushed through every surface — validate, lint, audit, fmt --check, compile, test, run local in text and JSON, twenty-eight deliberate mutations, embed.Compile/RunLocal from a Go program, flow mcp over stdio, flow lsp over stdio, and flow debug replay. What held: twenty-two of the twenty-eight mutations were refused at validate with a position, the right name and usually a suggestion; the test verb's coverage report named the unreached arm by line; the debugger replay refused a bad script before running anything. The findings are at the joints between surfaces, and every one is a reproducer in its issue.
The evaluator's own voice (sub-issues here):
- cel: a runtime expression failure reads "no such overload" and nothing else — cel-go's
Err.NodeID()is already in the error chain, the spec holds every node's offset, andPositionsholds the step's line #1551 — a runtime expression failure readsno such overload/no such key: urlland nothing else; cel-go'sErr.NodeID()is in the error chain,SourceInfo.positionshas the offset,Positionshas the step's line, and the callee's declared outputs are in the spec. Operator, operand types, caret and candidates are all in hand and discarded atevalProgram. - flowfile: two checks the compiler already performs are not applied where they are needed — a
with:binding of a bare${inputs.<name>}is not typed against the callee's declaration, and an enum output'svalues:is not checked against the literal domainswitch:already infers #1554 — two checks the compiler already performs are not applied where they are needed: awith:binding of a bare${inputs.<name>}is not typed against the callee's declaration (checkCallArgumentTypelacks the tiercheckOutputValueTypehas), and an enum output'svalues:is not checked against the literal domainvalidate_switch.goalready infers for the same expression. - flowfile: a
must:that does not type-check is reported three times with cel-go's raw issue text (ERROR: <input>:1:6 …plus an ASCII caret) — and amust:whose regex literal cannot compile validates clean unless adefault:orexample:happens to evaluate it #1556 — amust:that does not type-check is reported three times with cel-go's raw issue rendering (ERROR: <input>:1:6 …and a caret block), and amust:whose regex literal cannot compile validates clean unless adefault:orexample:happens to evaluate it (flowfile: an invalid regex, duration or timestamp literal compiles clean — the compiler'senv.Checkruns none of cel-go's AST validators, so the run fails whereflow validatesaid ok #1500 on the declaration surface).
The surfaces around it (standalone):
- cli/embed/mcp: a refusal at the submit boundary is an untyped
fmt.Errorf—--output jsonprints prose and no document,embed.RunLocalclassifies "declared int but was given string" asInternal, and three verbs print a text trailer after their JSON report #1552 — a refusal at the submit boundary is an untypedfmt.Errorf:--output jsonprints prose and no document,embed.RunLocalclassifies "declared int but was given string" asInternal, MCP relays prose;fmt --check,debug replayandauditprint text after or instead of their JSON report. - mcp:
flowstate_run_local,runandgetanswer with protojsonValuetrees ({"literal":{"int64Value":"2"}}) while--output jsonprojects the same run to plain JSON — the code says the two are "the same bytes", and they are two dialects #1553 —flowstate_run_local,runandgetanswer with protojsonValuetrees while--output jsonprojects the same run to plain JSON; the comment atmcp.go:941says they are the same bytes. - flowfile: a called workflow's own defect is reported at the caller's
call:line in the caller's file — the callee's position lives inside the message text,DiagnosticReporthas onefile,codeisgeneral, and a direct self-call is described as "a chain of files rather than directly" #1555 — a called workflow's defect is reported at the caller'scall:line in the caller's file, the callee's position lives inside the message text,DiagnosticReporthas onefile, and a direct self-call is described as "a chain of files rather than directly". - embed: a
Task.Fnthat returns a plainerroris retried five times over fifteen seconds and reported asInternal— the facade never says how a task classifies its failure, and a nil-descriptor task lets a misspelled input passValidatein silence #1557 — an embeddedTask.Fnreturning a plainerroris retried five times over fifteen seconds and reported asInternal; nothing in the facade says how to classify; a nil-descriptor task lets a misspelled input passValidate. - flowtest: a failed expectation is a
Diagnosticwithline: 0, column: 0— the loader can locate everyexpect.*key (positionOf/positionOfKey), only coverage arms use it, and--output jsonhands an editor nothing to underline #1558 — a failedflow testexpectation is aDiagnosticwithline: 0; the loader can locate everyexpect.*key and only coverage arms use it.
Comments rather than issues: on #1439, the debugger's break place arms both the caller's and the callee's step of that id and the preflight lists callee ids flattened into the caller's universe; on #1452, the catalog's map[string, string] / info | warn | error prose and the MCP JSON Schema are two more spellings of the type vocabulary.
Checked and not filed: fmt reorders with: keys alphabetically and the declaration keys into a fixed order (R3, one spelling; a callee-declaration order for with: would be a style decision, not a defect); flow validate is quiet on int + "x" over steps.* (that is #1481, not a new finding); hover on ${inputs.plan} inside with: answers nothing and completion after r.place. inside a results.map offers nothing (#1463 and #1434 already carry both); embed.Compile refuses a call: with a diagnostic that names the fix (#1376, working as documented); the LSP advertises no signatureHelpProvider (#1506); the MCP tools carry no annotations (#1543) and take base64 for validate and plain YAML for run_local (#1290).
Recorded, not filed
- Decorators. cel-go's
InterpretableDecoratorV2(interpreter/decorators.go) is the plan-time hook for evaluator-side rules; Flowstate's one such rule (ordered map iteration) is an AST rewrite instead becauseevalFoldis unexported and a decorator cannot reach a comprehension's range. The cleaner end state is upstream — aceloption for deterministic map iteration would deletecelmap.go— and is worth proposing to cel-expr/cel-go rather than filing here. - Sensitive taint through expressions.
sensitivevalues.gopropagates by content at render time andflowfile/sensitive_log.goby name at lint time;docs/DSL.mdrecordssecret:taint as unbuilt. AnEvalStateObservercould mark derived values by observing which nodes touched a sensitive input, but the render-time set already catches most of it and the gain is confined to derived substrings. Not filed until a real file shows the gap. DeclareContextProtooverv1.Scope.run.proto'sScopeis one bucket per root, which is the shape cel-go'sDeclareContextProtodeclares from a descriptor; the values areflowstate.v1.Value, not CEL-native, so the declaration would type wrong without a projection. Recorded as a direction dsl: the reference model is re-derived by eighteen hand-kept lists across validator, LSP, debugger, flowtest, rewriter and CLI — derive them from one table #1437's root registry could take, not as a proposal; the narrower mechanism (cel.Typesfor the closedrun/triggerroots) is a comment on dsl: the reference model is re-derived by eighteen hand-kept lists across validator, LSP, debugger, flowtest, rewriter and CLI — derive them from one table #1437.- Identifier escape syntax for the four lexer-token step ids: a row in cel: what cel-go grew past the pin, screened by the profile's own admission rule —
json.parsewith a target type,hasValue(v),hasOnly/hasAny/hasAll/hasExactly, base64url, HMAC/JWT helpers, timestamp parsing,maps.merge, cross-type numerics #1487. - The compiled spec's size.
flow compile -o jsonof the two-file workload is 107 KB: every expression carries a fullSourceInfo(location: "<input>",lineOffsets,positions, emptymacroCalls), and the callee is inlined per call site. The encoding is decided (design: flowstate stores every payload as ProtoJSON, and nothing has decided that #911/payloadcodec: write payloads as binary protobuf, keep decoding both #1196); the<input>location is the one field worth a look when cel: a runtime expression failure reads "no such overload" and nothing else — cel-go'sErr.NodeID()is already in the error chain, the spec holds every node's offset, andPositionsholds the step's line #1551 decides where an expression's file name lives.
Already tracked and cited from the finding it touches: #1357, #1358, #1360 (fusion inside one evaluation is the decorator/optimizer seam), #1383, #1432, #1436, #1452, #1454, #1459, #1421, #1437, #1438, #1449, #1468, #1491, #1493, #729, #1225, #1285, #558, #1469, #1288, #545, #548, #326, #332, #911, #923, #1012, #1465, #184, #1439, #1290, #1293, #1426, #1471, #1497, #230.
Not verified
- The eval: one
steps.<id>.<output>read converts every prior step's every output into a CEL value — 21 ms and 24 MB for one field at 1,000 steps, linear in what the run has accumulated, on both drivers #1482 benchmark ran the evaluator path directly, not a durable run; the durable driver sharesScope.Activation, so the cost is expected to hold there, and the issue's acceptance criteria require the conformance case to prove it. - Switch to direct observation of costs on a per-operation basis cel-expr/cel-go#1430's effect on cel: the runtime cost observer makes every large comprehension quadratic in wall time — a 50k-element map costs ~2.5s CPU while the budget reads it as affordable #1357's shape is inferred from the removal of
refValStack, not re-measured; cel: the pin is google/cel-go v0.31.0; upstream moved to cel.dev/cel-go at v0.32.0 and thirty-one commits past it rewrote cost, memory and observers — decide the bump as a versioned interpreter change, after the pins #1480's acceptance criteria require the measurement. - flowtest: a
check:claim written with optional syntax is refused at load with "unsupported syntax '.?'" — claims are parsed against the library-less base environment and evaluated against the run's profile #1512's macro-bearing claims (sum,cel.bind) loaded and evaluated correctly in the probe; the mis-shaped tree the walkers see was established by reading the code, not by a witness that came out wrong. - validate: protovalidate is a second CEL evaluator in the binary — its own environment, no cost limit, no interrupt,
nowbound, cross-type numerics on — and plugin-authoredcelrules already run through it atflow validateand in the editor #1531's cost exposure was established by reading protovalidate's sources (no limit option exists), not by timing a pathological plugin rule; its acceptance criteria require that test. - ci: hover,
flow tasks, the MCP catalog anddocs/reference/tasks.mdare rendered from schema comments, andbuf lintdoes not require one — 72 of 606 fields inflowstate/v1have no comment, including every arm ofValue#1544's count comes from the embedded source-info descriptor set with map-entry fields excluded; abuf lintrun with theCOMMENTSrules may count oneofs and enum values differently, andbufis not installed in the audit environment. - The fifth pass ran the local driver only (no Temporal dev server in the audit environment). cel: a runtime expression failure reads "no such overload" and nothing else — cel-go's
Err.NodeID()is already in the error chain, the spec holds every node's offset, andPositionsholds the step's line #1551, cli/embed/mcp: a refusal at the submit boundary is an untypedfmt.Errorf—--output jsonprints prose and no document,embed.RunLocalclassifies "declared int but was given string" asInternal, and three verbs print a text trailer after their JSON report #1552 and mcp:flowstate_run_local,runandgetanswer with protojsonValuetrees ({"literal":{"int64Value":"2"}}) while--output jsonprojects the same run to plain JSON — the code says the two are "the same bytes", and they are two dialects #1553 name the durable driver in their acceptance criteria because the code paths are shared (ExpressionError,rundoc.go, the submit boundary), not because a durable run was observed.
Activity
steps.x.yis a three-candidate maybe-attribute, every call carries an empty overload id, and cel-go's overload-aware costing, plan-time regex compilation and static cost estimation are unreachable #1481steps.<id>.<output>read converts every prior step's every output into a CEL value — 21 ms and 24 MB for one field at 1,000 steps, linear in what the run has accumulated, on both drivers #1482Limitsis two knobs (cost, interrupt frequency); the pinned cel-go exposes nine bounds and the five policy compilers set three of their own — decide each bound once, including the memory bound no site has #1483steps.<id>.<output>trails an expression touches, dynamic indexes included #1484EvalDetails— actual cost is measured on every expression and reported nowhere, andOptTrackStatewould giveflow policy explain, DAPevaluateand check witnesses a real per-node trace instead of a re-evaluation #1485Env.ToConfigrenders the same environment as a YAMLenv.Config— pin 2026.1 as data, feed it to the REPL and the editor, and make #1358's "cel.md unchanged" a structural diff #1486json.parsewith a target type,hasValue(v),hasOnly/hasAny/hasAll/hasExactly, base64url, HMAC/JWT helpers, timestamp parsing,maps.merge, cross-type numerics #1487env.Checkruns none of cel-go's AST validators, so the run fails whereflow validatesaid ok #1500common/astis the one traversal, and three files already use it #15012 remaining items
celcompletesays "is not available" is whatdocs/reference/cel.mdprints in its Signature column #1506v1.Nodehas no position — the compiler'sPositionsside table already maps step ids to spans for the launched file, so line breakpoints and navigable frames are a lookup away #1507cel.dev/expralready ships the textproto cases for every library the profile admits, and it is already ingo.mod#1509check:claim written with optional syntax is refused at load with "unsupported syntax '.?'" — claims are parsed against the library-less base environment and evaluated against the run's profile #1512nowbound, cross-type numerics on — and plugin-authoredcelrules already run through it atflow validateand in the editor #1531buf.validate's names andmust:spells itsthis; decide whether a declaration becomes a descriptor #1534Node.Outputs.named_valuesisrequired, so the wire cannot say "a step produced no outputs" — the server substitutes an empty map for an absent signal payload, and four engine sites build the empty message the rule forbids #1537parse.go, a 65-entry LSP completion table and DSL.md, beside the schema's own comments — derive the key table, hover, a generated grammar reference and a JSON Schema fromworkflow.protothrough oneyamloption #1542Cancel,Terminate,SignalandRunlook identical toValidateto an agent's client; derivereadOnlyHint/destructiveHint/idempotentHintfrom method options and giveflow mcp servea read-only posture #1543flow tasks, the MCP catalog anddocs/reference/tasks.mdare rendered from schema comments, andbuf lintdoes not require one — 72 of 606 fields inflowstate/v1have no comment, including every arm ofValue#1544Err.NodeID()is already in the error chain, the spec holds every node's offset, andPositionsholds the step's line #1551with:binding of a bare${inputs.<name>}is not typed against the callee's declaration, and an enum output'svalues:is not checked against the literal domainswitch:already infers #1554must:that does not type-check is reported three times with cel-go's raw issue text (ERROR: <input>:1:6 …plus an ASCII caret) — and amust:whose regex literal cannot compile validates clean unless adefault:orexample:happens to evaluate it #1556picatz commentedon Sep 7, 2026
Sixth pass: the interpreter package against the durable model, and the agent surface
Read at
7530b29, against cel-gov0.31.0(pin) andaa72cde(master, 2026-09-04):interpreter/planner.go(decorators and observers),async.go,prune.go,frame.go,evalstate.go,dispatcher.go,runtimecost.go,runtimememory.go; and, outside cel-go,cel-expr/skills(an MCP server and two agent skills overcel.AuthoringPrompt,OptTrackStatecoverage andenv.Config) andcel-expr/cel-spec'sdoc/extensions/. Every behavioural claim below was reproduced withgo run ./cmd/flowon a throwaway file; nothing was posted upstream.Reproduced, security-relevant:
sensitive:input survivesupperAscii(),substring()andreverse()into alog:line and into run outputs in the clear — the lint follows shape and the value set follows content, and neither follows a content-preserving function; run outputs redact by name only #1868 — asensitive:input survivesupperAscii(),substring()andreverse()into alog:line and into run outputs in the clear; the lint follows shape, the value set follows content, and run outputs redact by name only (raw: ${inputs.token}prints). The "real file" the Recorded, not filed row above was waiting for; the planner's observer (planner.go:77-99) is the runtime taint hook.Decisions the slate did not have:
PruneAst) optimizers each have a Flowstate caller, the spec stores every expression as parsed, and positions,fmt, attestation and both drivers all depend on which form is persisted #1869 — where a rewritten AST may live: constant folding, inlining andPruneAst/ResidualAsteach have a caller; the spec stores every expression as parsed; positions,fmt, attestation and both drivers depend on which form persists.vars:are values,cel.bindis one expression,call:is a whole run; afunctions:block compiled by inlining keeps the IR plain2026.1CEL and both drivers untouched, and plugin- or operator-defined CEL functions stay refused #1871 — the language cannot name a reusable computation; afunctions:block compiled by inlining keeps the IR plain2026.1CEL, and plugin/operator CEL functions stay refused.AsyncBinding/LateFunctionBindingare refused in the workflow dialect by a guard test, and an external fact enters a policy through its subject (an attribute provider at the activation seam), never through a call #1872 — CEL functions are pure and synchronous on every surface:AsyncBinding/LateFunctionBindingrefused by a guard test; external facts enter a policy through its subject (attribute-provider seam), never through a call. Expands the one-sentence ruling in What holds.Capability, with prior art in the CEL project's own tooling:
flow expr eval/replverb and a local MCP tool over the oneEvaluator, with--type,--cost,--explainand a run's scope #1870 —flow expr eval/repland a local MCP tool over the oneEvaluator(cel_compile/cel_evaluate's shape, profile as the fixed environment).switch:arms and cannot see theif:that was never false, the? :arm that never ran or the||right side never evaluated — cel-go'sOptTrackStategives node and branch coverage per expression, as cel-expr/skills already reports it #1875 — expression node and branch coverage inflow testfromOptTrackState(cel-expr/skills'coverage.gois the hundred-line reference).condition,hitConditionandlogMessageare dropped without a word — the console already compilesbreak <id> if <expr>and evaluates it at arrival, the adapter reads onlyname; conditional breakpoints, hit counts and log points from the same expression dialect #1873 — DAP conditional breakpoints, hit counts and log points: the console compilesbreak <id> if <expr>already; the adapter dropscondition.semanticTokensProvideris advertised, while the server already holds the fence scanner, the profile's function index and a position per AST node; semantic tokens for roots, step ids, bindings, functions, operators and.?#1874 — LSP semantic tokens for fences: no grammar is contributed, nosemanticTokensProvideradvertised.Comments rather than issues: #1856 (policy subjects are
ext.NativeTypesstructs with 23cel:tags; the proto-first version of the same finding), #1852 and #1486 (cel-expr/skills consumescel.AuthoringPromptandenv.Configexactly as those issues propose; Flowstate should ship the two skills), #1850 (cel-spec's extension prose as the description source), #1862 (two ledger rows:ResolveNamecannot see the frame, and the spec prose as theextdocs source; async/memory/dispatcher read and not needed).Checked and not filed: the workflow-side deadlock budget vs the cost budget is already decided and measured (
size.go:481-493,engine/panicpolicy.go, #1769, #1357);evalContext()returningcontext.Background()on the workflow side is a recorded invariant with a guard test (engine/evalcontext.go); the nested stored-expression budget split is documented ateval.go:44-62and bounded by count, with the path unused by every execution path today;string.formatargument checking already runs at validate ("%d".format(["x"])is refused atflow validate); a runtime profile fingerprint fromDispatcher.OverloadIds()is #1486's document hashed, not a new mechanism.Not verified: the durable driver's rendering of derived sensitive outputs (#1868) was inferred from the shared
redactGetResponsepath, not observed on a Temporal run; cel-expr/skills was read atmainoverraw.githubusercontent.com(the tarball and API are outside this session's scope), not at a tagged release.Generated by Claude Code
picatz commentedon Sep 7, 2026
Seventh pass: the interpreter against the durable driver
Read at
7530b29:engine/evalcontext.go,engine/activities.go,engine/execute.go's value/switch/loop paths,engine/progress.go's query handlers,server/list.go,size.go's deadlock budget, and the async/width bounds. One measurement, one design record, two comments.value:steps, or aloop:of a thousand iterations over one, each inside every cost and element bound, run back-to-back without yielding and spend the 5 s deadlock budget deterministically, failing the durable run whileflow run localcompletes it #1882 — bug, measured: workflow-side wall time is bounded per expression and never per goroutine slice. A bounded 10k-element comprehension takes 85–187 ms here; a hundredvalue:steps or a 1,000-triploop:over one runs back-to-back without a yield and spends the 5 s deadlock budget deterministically, failing the durable run whileflow run localcompletes it. engine: an authored expression can spend 3 seconds under the CEL cost limit, two of them in one workflow task exceed the 5-second deadlock budget, and the durable run panics and retries forever whileflow run localcompletes it;lists.rangeand comprehensions bypass the element bound #204 placed on inputs #1769's fix bounded one expression; this is the count. Not reproduced on a Temporal worker; the arithmetic is in the issue.queries:(a CEL projection over live scope, no history event) andupdates:(a validated request with a reply) are the service-oriented half of the language, and an ad-hocflow get <id> --select '<expr>'is the operator's version of the same handler #1881 — decision: a run can be told things and cannot be asked things. Query handlers exist for progress and entity state only; namedqueries:,updates:with a validator and reply, and an ad-hocflow get <id> --select '<expr>'are the service-oriented half the language lacks. Pairs with decision: concurrency shapes at the step layer — the language already has structured concurrency (async:,parallel:,for_each,wait_for_signals:,concurrency:, entities) and forecloses select; what a Go-style model still lacks (a recorded-winnerselect:, bounded pipelines, a detached child run, real overlap on the local driver) and how each stays durable and deterministic #1876 (concurrency shapes).EvalDetails— actual cost is measured on every expression and reported nowhere, andOptTrackStatewould giveflow policy explain, DAPevaluateand check witnesses a real per-node trace instead of a re-evaluation #1485: carry the tenant on the cost metric; every evaluation site uses one process-wideLimits, and Multi-tenancy isolation: the four-tier model, the deployment matrix, and the ranked gap list (design record) #236's gap 6 declines a limiter, so the number is the instrument.Checked and not filed: expression failures on the activity side are non-retryable (
ErrorKind.Retryable()excludesexpression);flow server dev --dbgives single-player durability; typed signal payloads are a recorded refusal (outputs:on the wait shapes them,DSL.md:1742,:1822); thehttptask'sexpect:evaluates throughDefaultEvaluatorwith a hierarchical activation (one evaluator); cel-go recovers evaluation panics inprog.Eval(cel/program.go:376), so a cost or bound panic never escapes into the workflow goroutine.Generated by Claude Code