You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(spec,service-automation): the wait executor reads its declared contract only (#4045) (#4161)
* fix(spec,service-automation): the wait executor reads its declared contract only (#4045)
`wait` keeps its contract in `waitEventConfig` — a declared, `.describe()`-annotated
block on `FlowNodeSchema` that sits in the authorable-field list, reaches the
generated reference, and is what the showcase actually authors. Its descriptor
publishes no `configSchema`, which is by design rather than the gap it looks like.
The executor nevertheless also read six loose `config` keys behind `wec.X ?? loose.X`,
two of them (`duration`, `signal`) spellings the spec never declared anywhere. That is
the `notify.source` shape #4050 retired: a second de-facto contract announced only by a
code comment, so an author who wrote it got a flow that worked forever and was never
steered to the declared spelling (PD #12). Nothing in-repo authors it.
- New ADR-0087 D2 conversion `flow-node-wait-event-config-lift` lifts
config.{eventType,timerDuration,duration,timeoutMs,signalName,signal} onto the
declared block, in the executor's own `??` precedence — a declared value wins and
its loose counterpart is left shadowed, as `renameConfigKey` treats a shadowed alias.
- `eventType` is stamped `'timer'` when the lift would otherwise leave the block
without one. Load-bearing, not tidiness: the loader parses the CONVERTED flow
(`applyConversionsToFlow` → `FlowSchema.parse`) and `waitEventConfig.eventType` is
required once the block exists, so a stored flow carrying only
`config: { duration: 'PT1M' }` would have gone from working to failing to load.
`'timer'` is the exact default the executor applied to that shape.
- The six `?? loose.*` fallbacks are deleted. The surviving `?? 'timer'` is not one:
`waitEventConfig` is itself optional, and a node without it is a valid timer wait.
Registered in both lists the layer keeps: `CONVERSIONS_BY_MAJOR[17]` and
`step17.conversionIds`. The first draft missed the second and the repo's own ratchet
caught it — `migrations.test.ts` replays every conversion fixture through the chain,
so an unwired conversion fails there rather than shipping inert.
Verified at the real seam: the new executor tests author the legacy shape and go
through `registerFlow`, which is what applies the conversion. Negative control run by
unregistering the conversion — two of the three fail without it; the third passes
either way because it pins the executor's half of the precedence, and its name says so.
A second negative control pins the `eventType` default: deleting it from the converted
output makes `FlowSchema.parse` throw.
Regenerated `spec-changes.json` and `docs/protocol-upgrade-guide.md` (the step
rationale is a generated-artifact input). All 12 spec gates and 8 root gates pass;
spec suite 7097 tests green.
Deliberately unchanged and filed as #4158: `waitEventConfig.timeoutMs` is declared a
timeout guard but read as a timer duration, and `onTimeout` has zero readers — `wait`
has no timeout implementation at all, while the showcase authors `onTimeout: 'continue'`.
Implementing or retracting that is a behaviour change, not a contract cleanup.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoA8AV99Ss1RRkLLAYmDzq
* fix(examples): the showcase authors `wait` through the declared block (#4045)
Corrects a factual claim in the previous commit. It said "Nothing in-repo authors
it" of the loose `config` back door — that is wrong. The showcase's own
`wait_revision` node authored exactly that shape:
config: { eventType: 'signal', signalName: 'budget_revision' }
so the back door was not hypothetical, and the example that demonstrates `wait`
was itself on the spelling this PR retires. It moves to `waitEventConfig`.
The conversion already handled this shape correctly — the executor's behaviour is
identical either way, and a new test pins the exact combination the showcase hit:
the DECLARED key names sitting in the UNDECLARED location, which is what the
candidate ordering has to get right (`signalName` before `signal`). It also asserts
the converted flow still parses, since the lift creates the required block.
Found while checking whether a stuck `objectstack verify` CI step could be caused
by this change rather than by the runner — it could not, but the audit that ruled
it out is what surfaced the false claim.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoA8AV99Ss1RRkLLAYmDzq
---------
Co-authored-by: Claude <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/protocol-upgrade-guide.md
+3Lines changed: 3 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -124,6 +124,8 @@ Finally it removes agent `tools` (#3894): the legacy inline `{type,name,descript
124
124
125
125
Beyond those spec-surface removals, it graduates the seven flow-node config key aliases the executors still tolerated (#3796): the CRUD nodes' `object` (use `objectName`) — the last tenant of the `readAliasedConfig` executor shim, which is deleted with it — plus the six open-coded fallbacks that never went through that shim: notify `to`/`subject`/`body`/`url` (use `recipients`/`title`/`message`/`actionUrl`) and script `functionName`/`input` (use `function`/`inputs`). All are pure key renames with unchanged values and replay losslessly. Like the sharing-rule access level above they keep a load-path acceptance window: none carried a prior deprecation warning, and `FlowNodeSchema.config` is an unconstrained record, so no schema tombstone can reject them — the conversion layer is the only seam that can declare, convert, and retire them.
126
126
127
+
The same graduation covers `wait`, whose fallback was not a config-to-config rename (#4045). `wait` keeps its contract in the declared `waitEventConfig` block, not in `config` at all — yet the executor also read six loose `config` keys, two of them (`duration`, `signal`) spellings the spec never declared. The conversion lifts them onto the declared block in the executor's own `??` precedence, so a value already declared wins and its loose counterpart is left shadowed. One wrinkle makes this a rewrite rather than a delete: `waitEventConfig.eventType` is required once the block exists, and the loader parses the CONVERTED flow — so a source carrying only `config: { duration }` is stamped with `eventType: 'timer'`, the exact default the executor applied to that shape. Behaviour-preserving in both directions.
128
+
127
129
And it removes the RLS-policy key `priority` (#3896 security audit): promised "conflict resolution" that cannot exist, because applicable policies OR-combine (most permissive wins) — there is never a conflict to order, and nothing ever read the key (call graph closed across the collection site, the projection round-trip and the compiler). A pure lossless delete: outcomes are identical with or without it; the schema tombstones the key with the same prescription.
128
130
129
131
The same close-out retires the four inert tool authoring keys (`category`, `permissions`, `active`, `builtIn`): none is part of AIToolDefinition and no execution path read them. Two were misleading in the dangerous direction — `permissions` promised an invocation gate nothing enforced, and `active: false` read as "withdrawn" while the tool kept reaching the LLM tool set. Lossless deletes; the strict ToolSchema rejects each with its prescription.
@@ -146,6 +148,7 @@ The close-out sweep finishes the enforce-or-remove worklist across the remaining
146
148
|`sharing-rule-access-level-full-to-edit`|`sharingRule.accessLevel`| sharing-rule accessLevel 'full' → 'edit' (#3865 — `full` never granted more than `edit`) | live — protocol 17 loader accepts the old shape |
147
149
|`flow-node-crud-object-alias`|`flow.node.config.objectName`| CRUD flow-node config key 'object' → 'objectName' (#3796 — `readAliasedConfig` shim graduation) | live — protocol 17 loader accepts the old shape |
148
150
|`flow-node-notify-config-aliases`|`flow.node.notify.config`| notify flow-node config keys 'to' → 'recipients', 'subject' → 'title', 'body' → 'message', 'url' → 'actionUrl' (#3796), and nested 'source: {object, id}' → 'sourceObject' / 'sourceId' (#4045) | live — protocol 17 loader accepts the old shape |
151
+
|`flow-node-wait-event-config-lift`|`flow.node.wait.waitEventConfig`| wait flow-node loose config keys → the declared `waitEventConfig` block: 'eventType', 'timerDuration'/'duration' → 'timerDuration', 'signalName'/'signal' → 'signalName', 'timeoutMs' (#4045) | live — protocol 17 loader accepts the old shape |
149
152
|`flow-node-script-config-aliases`|`flow.node.script.config`| script flow-node config keys 'functionName' → 'function', 'input' → 'inputs' (#3796) | live — protocol 17 loader accepts the old shape |
150
153
|`permission-rls-priority-removed`|`permission.rowLevelSecurity.priority`| RLS-policy key 'priority' removed (#3896 audit — policies OR-combine, so the promised conflict-resolution semantics cannot exist; dropping it changes no outcome) | retired — `migrate meta` only |
0 commit comments