Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ A run executes in one of **three execution modes** behind the one `LLMProvider`
engine is identical across all three. See [ADR-0012](docs/decisions/0012-managed-inference-dual-mode.md) to [ADR-0015](docs/decisions/0015-managed-mode-data-handling-and-compliance.md)
and [docs/architecture/managed-inference.md](docs/architecture/managed-inference.md).

**Status: Phase 1 in progress — milestone M1 (LLM seam proven) reached (PR #9, 2026-06-07); the `FallbackChain` runner (1.K) landed, completing 1.m2 with the cost tracker (PR #13, 2026-06-11); the run loop (1.N — `WorkflowEngine` + `RunEventBus`) landed (PR #17, 2026-06-13) **completing 1.m3** (parse → DAG → run loop emits the canonical event stream), with the built-in `ToolRegistry` (1.T, a 1.m4 component) landing alongside it as the other `AgentRunner` (1.O) join prerequisite; and the **`AgentRunner` (1.O) — per-node LLM execution behind the seam — landed (PR #18, 2026-06-14)**. The node-type handlers (1.P) are next, toward M2.**
**Status: Phase 1 in progress — milestone M1 (LLM seam proven) reached (PR #9, 2026-06-07); the `FallbackChain` runner (1.K) landed, completing 1.m2 with the cost tracker (PR #13, 2026-06-11); the run loop (1.N — `WorkflowEngine` + `RunEventBus`) landed (PR #17, 2026-06-13) **completing 1.m3** (parse → DAG → run loop emits the canonical event stream), with the built-in `ToolRegistry` (1.T, a 1.m4 component) landing alongside it as the other `AgentRunner` (1.O) join prerequisite; the **`AgentRunner` (1.O) — per-node LLM execution behind the seam — landed (PR #18, 2026-06-14)**; and the **node-type handlers (1.P) — the six non-agent `NodeExecutor` arms (condition / transform / fan_out / fan_in / input / output) behind a dispatching executor — landed (PR #20, 2026-06-14)**. The human gate (1.Q) is next, toward M2.**
Phase 0 (M0, 2026-06-04) landed the monorepo, strict toolchain + CI, `@relavium/shared` (the
full Zod contract set), the no-vendor-type seam fence, and `@relavium/db`. Phase 1 has since
landed `@relavium/llm` — the `LLMProvider` seam + all three adapters (Anthropic, OpenAI/DeepSeek,
Expand All @@ -51,10 +51,12 @@ reserved generator methods, shape-only, landed before the seam's exhaustive cons
**`WorkflowYAMLParser` (1.L, PR #14)**, the **`{{ … }}` interpolation engine + parse-time secret-taint
gate (1.L2, PR #15)**, the **DAG builder + `RunPlan` (1.M)** plus the **QuickJS-wasm expression
sandbox (1.AB)** (PR #16, 2026-06-13), and the **run loop — `WorkflowEngine` + `RunEventBus` (1.N)**
together with the **built-in `ToolRegistry` (1.T)** (PR #17, 2026-06-13), and the **`AgentRunner` (1.O)**
together with the **built-in `ToolRegistry` (1.T)** (PR #17, 2026-06-13), the **`AgentRunner` (1.O)**
join (PR #18, 2026-06-14 — host-injected provider resolution behind the seam, the correlation-agnostic
turn core, the tool-call loop, and the same-provider reasoning replay). Active work is now the remaining
1.m4 lane — the node-type handlers (1.P), the human gate (1.Q), checkpoint/resume (1.R), node retry
turn core, the tool-call loop, and the same-provider reasoning replay), and the **node-type handlers
(1.P)** (PR #20, 2026-06-14 — the six non-agent `NodeExecutor` arms behind a dispatching executor,
executor-only with a `secretInputNames` masking gate on `NodeExecContext`). Active work is now the
remaining 1.m4 lane — the human gate (1.Q), checkpoint/resume (1.R), node retry
(1.S), and the pre-egress budget governor (1.AC) — toward **M2**; see
[docs/roadmap/current.md](docs/roadmap/current.md). See [README.md](README.md) for the public overview.

Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ secret-taint gate** (1.L2, PR #15), and the **DAG builder + `RunPlan`** (1.M) to
**QuickJS-wasm expression sandbox** (1.AB) (PR #16, 2026-06-13) — all with zero platform imports.
The **run loop** (1.N — `WorkflowEngine` + `RunEventBus`) landed (PR #17, 2026-06-13), **completing
milestone 1.m3** (parse → DAG → run loop emits the canonical event stream); the **built-in
`ToolRegistry`** (1.T, a 1.m4 component) landed alongside it; and the **`AgentRunner`** (1.O —
per-node LLM execution behind the seam) landed (PR #18, 2026-06-14). Next on the critical path: the
**node-type handlers** (1.P), the human gate, checkpoint/resume and retry, plus the **AgentSession**
`ToolRegistry`** (1.T, a 1.m4 component) landed alongside it; the **`AgentRunner`** (1.O —
per-node LLM execution behind the seam) landed (PR #18, 2026-06-14); and the **node-type handlers**
(1.P — the six non-agent handlers behind a dispatching executor) landed (PR #20, 2026-06-14). Next on
the critical path: the **human gate** (1.Q), checkpoint/resume and retry, plus the **AgentSession**
runtime + export-to-workflow sub-spine. See
[docs/roadmap/current.md](docs/roadmap/current.md) for live status.
2 changes: 2 additions & 0 deletions docs/reference/contracts/sse-event-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ export interface HumanGatePausedEvent extends BaseEvent {

`agent:tool_call.toolInput` is sanitized (no secrets) and `agent:tool_result.outputSummary` is truncated. `run:started.inputs` carries workflow inputs, but any **secret-typed** input is **masked** — the value is replaced with `{ secret: true, ref }` (the keychain/env reference), never the raw value. API keys and other secrets never appear in any event payload — this holds across the in-process bus, HTTP SSE, and any persisted run log. (On the desktop the raw provider key never even reaches the WebView: egress is Rust-delegated, [ADR-0018](../../decisions/0018-desktop-execution-and-rust-egress.md).)

The same `{ secret: true, ref }` **`MaskedSecret`** marker can also appear in **`node:completed.output`** (for an `input` node, which emits the masked inputs) and therefore in **`run:completed.outputs`** / **`run:failed.partialOutputs`** wherever a `secret`-typed input would otherwise surface — the engine masks `secret` inputs at the ingress so a raw secret never reaches an output payload (see [run-plan.md §output capture](../shared-core/run-plan.md)). **Any surface rendering of node/run outputs must treat a `MaskedSecret` object as a redacted placeholder, not displayable data.**

## Consuming the stream

The consumer pattern is identical for every surface, local or cloud:
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/contracts/workflow-yaml-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ The cost cap is **pre-egress**: before each LLM call the engine checks `cumulati

## Edges

Edges are explicit directed connections. An edge from a `condition`/branching node uses the `nodeId:handleName` form to reference a named output handle/branch.
Edges are explicit directed connections. An edge from a `condition`/branching node uses the `nodeId:handleName` form to reference a named output handle/branch. **A plain (handle-less) edge whose `from` is a `condition` node is rejected at parse** — a `condition` routes only via its `branches[].target_node` (and the optional `nodeId:when` handle edge), so a handle-less edge from it is either redundant with a dependency already materialized from a branch target, or it names a downstream the branch selection never activates (a silently dead node). Use the `nodeId:when` form, or rely on `branches[].target_node` alone (the routing edge is materialized either way).

```yaml
edges:
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/shared-core/run-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ The builder owns the structural checks the pure parser defers (it has the full n

- **Cycle** — the dependency graph has a directed cycle; the message names it (`a → b → c → a`).
- **Unknown edge target** — an `edges[]` endpoint, a `condition` `branches[].target_node` / `default`, or a `parallel_of` member names a node that does not exist.
- **Invalid handle** — a `nodeId:handle` edge whose source is not a `condition`, or whose handle matches no branch `when` value (the only named output handles in v1.0; `fan_out` uses plain edges).
- **Invalid handle** — a `nodeId:handle` edge whose source is not a `condition`, or whose handle matches no branch `when` value (the only named output handles in v1.0; `fan_out` uses plain edges); **also** a *plain* (handle-less) edge whose `from` is a `condition` node — a condition routes only via `branches[].target_node` + the `nodeId:when` handle form, so a handle-less edge from it is rejected (redundant with a branch target, or a node the branch selection never activates).
- **Dangling ref** — an `agent_ref` resolves to no agent. Only checked when a **resolved-agent registry** is supplied (`agent_ref` resolution against the workspace registry is a host concern — the pure builder never reads files); otherwise resolution is deferred. When resolution was deferred and an `agent` vertex reaches dispatch with **no** `resolvedAgent`, the `AgentRunner` (1.O) fails the node with `code: 'validation'` naming the unresolved `agent_ref` — never a crash ([agent-runner.md](agent-runner.md)).

Separately, a resolved `$ref`/registry agent's `system_prompt` is re-run through the secret-taint gate (a `$ref` agent's prompt lives in another file the pure parser never reads): a secret reaching it throws **`WorkflowSecretLeakError`** (ADR-0029(c)), exactly as for an inline agent.
Expand Down
20 changes: 13 additions & 7 deletions docs/roadmap/current.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,16 @@ separate `AgentRunner` (1.O) join prerequisite — it does **not** close 1.m3. T
landed the **1.O `AgentRunner` join ✅ Done (PR #18, 2026-06-14)** — per-node LLM execution behind the
`@relavium/llm` seam: the host-injected provider-resolution boundary ([ADR-0038](../decisions/0038-agentrunner-llm-call-boundary.md)),
the correlation-agnostic turn core (reused by 1.V), the tool-call loop + classified failure ladder, and
the same-provider signed-reasoning replay ([ADR-0039](../decisions/0039-same-provider-reasoning-replay.md)).
The lane now continues at the remaining **1.m4** workstreams toward **M2** — the **node-type handlers
(1.P)**, the **human gate (1.Q)**, **checkpoint/resume (1.R)**, **node retry (1.S)**, and the
**pre-egress budget governor (1.AC)** — and the agent-first sub-spine (**1.V–1.AA**, Lane C) opens now
that 1.O exists.
the same-provider signed-reasoning replay ([ADR-0039](../decisions/0039-same-provider-reasoning-replay.md)) —
and the **node-type handlers (1.P) ✅ Done (PR #20, 2026-06-14)**: the six non-agent `NodeExecutor` arms
(condition / transform / fan_out / fan_in / input / output) composed by a `createDispatchingNodeExecutor`
alongside the 1.O agent arm — executor-only, no `engine.ts` change (the run loop already owns readiness,
skip-propagation, fan-in join scheduling, events, cancellation), `wait_first` executor-only (true
loser-cancel deferred), and a pre-merge BLOCKER secret-leak (the `input` handler emitting raw
`secret`-typed inputs into events) fixed via a `secretInputNames` masking gate on `NodeExecContext`.
The lane now continues at the remaining **1.m4** workstreams toward **M2** — the **human gate (1.Q)**,
**checkpoint/resume (1.R)**, **node retry (1.S)**, and the **pre-egress budget governor (1.AC)** — and the
agent-first sub-spine (**1.V–1.AA**, Lane C) opens now that 1.O exists.

> **Multimodal I/O — the shape is landed (1.AD ✅ Done, PR #11, 2026-06-10).** First-class
> image/audio/video I/O (input **and** output, incl. generate-media-by-rule) was decided on 2026-06-08:
Expand All @@ -171,8 +176,9 @@ that 1.O exists.
> `RunPlan`) and 1.AB (the expression sandbox) have since landed (PR #16, merged 2026-06-13)**; and
> **1.N (`WorkflowEngine` + `RunEventBus`) and 1.T (the built-in `ToolRegistry`) are ✅ Done (PR #17,
> merged 2026-06-13)** — **1.N closes 1.m3** (its last component); **1.T** (a 1.m4 component) is the
> other 1.O join prerequisite; and **the `AgentRunner` join (1.O) is ✅ Done (PR #18, 2026-06-14)**. The
> **node-type handlers (1.P)** are the next workstream.
> other 1.O join prerequisite; **the `AgentRunner` join (1.O) is ✅ Done (PR #18, 2026-06-14)**; and the
> **node-type handlers (1.P) are ✅ Done (PR #20, 2026-06-14)**. The **human gate (1.Q)** is the next
> workstream.

Carry-over hardening is tracked in [deferred-tasks.md](deferred-tasks.md) — pick items up as Phase 1
first touches each file.
Expand Down
26 changes: 20 additions & 6 deletions docs/roadmap/deferred-tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,12 +250,17 @@ Severity is the review's verified rating. Check an item off in the PR that resol
- [ ] **Multi-tool result ordering in the turn core** — `dispatchToolCalls` appends tool-result messages in
dispatch-completion order; for v1.0 (single tool call per `tool_use` stop) this is moot, but a parallel-tool
provider should order by the accumulator's `toolOrder` before 1.V reuses the core. *(low · packages/core/src/engine/agent-turn.ts; 1.V)*
- [ ] **Secret-into-`run.outputs` runtime taint (ADR-0029(c) follow-up)** — an `agent` node cannot launder a
- [x] **Secret-into-`run.outputs` runtime taint (ADR-0029(c) follow-up)** — an `agent` node cannot launder a
secret into `run.outputs` (it emits LLM text only), so this is **not** 1.O's to own; it belongs to the
`transform` / sandbox node (1.P / 1.AB) that can return a secret-derived value. 1.O's only obligation is to
refuse a tainted `{{ run.outputs[…] }}` reference *if* such a marker reaches it; the static parse-time
`analyzeSecretTaint` gate covers the authored template graph. Record as a scoped ADR-0029 amendment when 1.P/1.AB
lands. *(medium · packages/core/src/interpolation/analyze.ts; ADR-0029(c), 1.P/1.AB)*
lands. *(medium · packages/core/src/interpolation/analyze.ts; ADR-0029(c), 1.P/1.AB)* **✅ Closed at the source
by 1.P (PR #20):** `buildExpressionScope` (scope.ts) masks `secret`-typed inputs out of the sandbox scope, so a
`transform` / `condition` / fan_in `merge_fn` reads the `{ secret, ref }` marker — never the raw secret — and
therefore cannot derive a secret value to launder into `run.outputs`. The vector is cut at the read, so no runtime taint
on the output is needed. (The only remaining secret-into-egress path is the agent prompt — tracked separately
below as a 1.O policy item, and it is provider egress, not an event-payload leak.)

> **2026-06-14 (PR #18 final review follow-ups).** Confirmed by the multi-dimensional pre-merge review;
> non-blocking, recorded so they aren't dropped.
Expand Down Expand Up @@ -305,21 +310,29 @@ Severity is the review's verified rating. Check an item off in the PR that resol
`merge_fn` sandbox scope (and the AgentRunner's prompt `RunScope`) currently bind `ctx: {}` — the authored
`context:` namespace is not yet resolved and threaded to handlers (a `{{ctx.key}}` template still resolves,
but a bare `ctx.key` JS-expression read sees `{}`). A cross-cutting change for **both** 1.O and 1.P when the
engine resolves the workflow `context:` map. *(medium · packages/core/src/engine/node-handlers/scope.ts, packages/core/src/engine/agent-runner.ts)*
engine resolves the workflow `context:` map. **NB (2026-06-14 triage):** this is not a drive-by — `resolveContext`
is **async** (a context value may `read_file`), so it needs a new **run-start async resolution step** in the
engine (with resolver capabilities + a context-resolution-failure path) plus the `NodeExecContext` seam field.
Best done as its own focused task or folded into **1.Q/1.R** (which already touch the run lifecycle); it is the
highest-value open engine gap (a bare `ctx.key` silently reads `undefined` today — a mis-route risk). *(medium ·
packages/core/src/engine/engine.ts, node-handlers/scope.ts, agent-runner.ts; resolveContext is 1.L2)*
- [ ] **`secret`-typed input flowing into an agent prompt (1.O parallel to the 1.P fix)** — the AgentRunner
resolves `{{ inputs.<name> }}` in a `prompt_template` against the **raw** `RunScope` (agent-runner.ts), so a
`secret`-typed input interpolates raw into a USER message sent to the provider. This is provider **egress**
the author opted into (not an event-payload leak, so it does not violate the events rule the 1.P fix
enforces), but whether a `secret`-typed input should be silently interpolated into a prompt — vs masked /
rejected at parse — is a policy call. Evaluate alongside the secret-handling story; if masked, reuse
`maskSecretInputs`. *(low · packages/core/src/engine/agent-runner.ts; security-review.md)*
- [ ] **Reject a plain (handle-less) edge whose `from` is a `condition` node (1.M validation)** — a `condition`
- [x] **Reject a plain (handle-less) edge whose `from` is a `condition` node (1.M validation)** — a `condition`
routes only via `branches[].target_node`/`default` (materialized edges); a separately-authored plain edge
`from: <condition>` (no `:handle`) makes its target a dependent that the handler's `selected` never names, so
the run loop skip-propagates it — a silently-dead downstream rather than a parse error. Add a structural
validation in `dag.ts` (`validateStructuralEdge`) rejecting a handle-less edge out of a condition (reuse
`invalid_handle`, or a `condition_requires_handle` kind). Pre-existing 1.M edge-validation gap, not a 1.P
handler defect. *(low · packages/core/src/dag.ts; workflow-yaml-spec.md §edges)*
handler defect. *(low · packages/core/src/dag.ts; workflow-yaml-spec.md §edges)* **✅ Fixed (2026-06-14
hardening pass):** `validateStructuralEdge` rejects a handle-less edge from a `condition` with an
`invalid_handle` issue (no existing fixture/spec used one — the spec routes via `branches` + `nodeId:when`
handles); pinned by `dag.test.ts` and documented in workflow-yaml-spec.md §edges.

## Schema / validation hardening

Expand Down Expand Up @@ -489,7 +502,8 @@ Severity is the review's verified rating. Check an item off in the PR that resol

- [x] **`readBracket` cognitive complexity (1.L2)** — Sonar 17 > 15; extract the numeric-index vs
quoted-key branches into helpers. *(critical · packages/core/src/interpolation/path.ts:96)* **✅ Fixed:**
extracted `readQuotedKey` + `readNumericIndex`; `readBracket` is now a 3-line dispatcher.
extracted `readQuotedKey` + `readNumericIndex`; `readBracket` is now a thin dispatcher that delegates
to them (cognitive complexity well under the threshold).
- [x] **`splitTopLevel` cognitive complexity (1.L)** — Sonar 16 > 15; extract the quote/bracket
depth-tracking into a small state helper. *(critical · packages/core/src/interpolation/references.ts:217)*
**✅ Fixed:** extracted a `SplitState` + `splitStep`/`splitStepOutsideQuote` pair; the loop body is one call.
Expand Down
Loading
Loading