Skip to content

feat(core): engine run loop (1.N) + built-in ToolRegistry (1.T)#17

Merged
cemililik merged 16 commits into
mainfrom
development
Jun 13, 2026
Merged

feat(core): engine run loop (1.N) + built-in ToolRegistry (1.T)#17
cemililik merged 16 commits into
mainfrom
development

Conversation

@cemililik

@cemililik cemililik commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Two parallel Phase-1 @relavium/core engine workstreams, developed concurrently on development and reviewed independently — bundled here because they converge at the 1.O AgentRunner join and share the engine substrate.

  • 1.N — WorkflowEngine + RunEventBus (the run loop). Walks the 1.M RunPlan, dispatches ready nodes through an injected NodeExecutor seam (1.O/1.P fill it), and emits the canonical RunEvent stream with an exactly-one-terminal-event guarantee. New decision: ADR-0036 (run-loop substrate — in-house platform-free RunEventBus, the ExecutionHost seam, the terminal-event invariant).
  • 1.T — built-in ToolRegistry + dispatch. The engine-side tool registry/dispatcher behind a ToolHost seam, with the ADR-0029 guardrails. New decision: ADR-0037 (engine tool-execution boundary) + the tool-registry.md reference spec.

Both are pure-TypeScript (zero platform-specific imports), sit behind their seams, and add no vendor type across the @relavium/llm seam.

1.N — run loop (ADR-0036)

  • In-house, platform-free RunEventBus — not Node's node:events (the engine-purity fence bans it), not a new dependency. The single producer-side translation point: assigns the monotonic, gap-free sequenceNumber, validates against RunEventSchema, masks secrets, and stamps a secret-free correlationId on failures.
  • RunHandle.events — a bounded, producer-await (no-drop) async-iterable bridge; co-equal with .subscribe(); completes on the terminal event.
  • ExecutionHost seam — injected clock / id source / RunStore / abort controller, so the loop is identical across execution modes (local now, cloud later); a deterministic in-memory reference host + store.
  • WorkflowEngine.start/resume/cancel + reconcile — a serialized, completion-driven ready queue (no sleep/poll); condition skip-propagation (a fan-in joins over a skipped branch); the max_parallel cap; cooperative AbortSignal cancellation (cancel wins a late-failure race); exactly one terminal event (incl. uncaught-throw → run:failed{internal}, crash reconciliation, and store-fault robustness — #emitDurable is total: always delivers, so a persist fault never hangs the consumer stream); persist-before-deliver on the success path.
  • NodeExecutor / NodeOutcome / GateRequest seam for the 1.O AgentRunner and 1.P node handlers; typed EngineStateError for API-boundary misuse.
  • Adds an optional, secret-free correlationId to the @relavium/shared run-event error payload (additive, forward-compatible) + reworded the literal "EventEmitter" doc homes to the platform-free bus.

1.T — built-in ToolRegistry (ADR-0037)

  • The engine-side ToolRegistry + dispatcher behind the ToolHost seam; built-in tools exposed as canonical ToolDefs for the 1.E ToolNormalizer.
  • ADR-0029 guardrails enforced in-engine: exact-match tool resolution, allowlisted run_command, secret-free agent:tool_call events, bounded tool results, untrusted-input handling + SSRF posture.
  • New ADR-0037 (engine tool-execution boundary) + docs/reference/shared-core/tool-registry.md.

Review

Each workstream went through two adversarial, multi-dimensional review rounds (opus then sonnet, with a verify phase):

  • 1.N: fixed a store-fault zombie-run class (terminal / gate-pause persist failure left the stream hung) and a reconcile()-abandons-the-rest gap; folded all blocker/high/medium findings.
  • 1.T: round-1 security + result-bounding hardening, round-2 sonnet re-audit; findings folded.

Validation

  • pnpm turbo run lint typecheck test build green for @relavium/shared + @relavium/core (8/8).
  • Tests green — core: 439, shared: 235; engine ≥90% line + branch.
  • Zero platform-specific imports (both purity gates green); no vendor type across the seam; no new runtime dependency.
  • prettier --check clean across core / shared / docs.

Notes

  • ADR-0036 and ADR-0037 are append-only decision records.
  • The roadmap entries for 1.N and 1.T stay In-progress until this PR merges (per project convention — marked Done only after merge).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Full workflow runtime: serialized run loop with durable, gap‑free event delivery, exactly‑one terminal run outcome, resumable interruption semantics, backpressure‑aware run handles, typed in‑process event bus, and completed DAG/plan + expression sandbox.
    • Tool system: registry with 12 built‑ins, host‑mediated dispatch, strict guardrails, model‑facing result bounding with spill-to-store, and untrusted-result branding.
  • Documentation

    • New ADRs, expanded roadmap, architecture, standards, and contract/schema references for run-loop, event bus, tool-host, and execution boundaries.
  • Tests

    • Extensive test suites covering engine, eventing, host/store, run handles, tooling, bounding, and untrusted utilities.

cemililik and others added 11 commits June 13, 2026 09:04
…ne after PR #16

PR #16 (DAG builder + `RunPlan`, 1.M; QuickJS-wasm expression sandbox, 1.AB)
merged to main on 2026-06-13. Per the roadmap-done-after-merge convention,
flip both workstreams to ✅ Done and repoint the "next workstream" pointer to
1.N (WorkflowEngine + RunEventBus).

- phase-1-engine-and-llm.md: status block, the 1.M and 1.AB section headers,
  and the dependency table (M2-path cells annotated Done (PR #16)).
- current.md: bump Last updated to 2026-06-13; record 1.M/1.AB landed and set
  1.N as next.
- README.md / CLAUDE.md: refresh the engine-lane status sentence.

phases/README.md and roadmap/README.md unchanged — neither tracks per-workstream
status (the M2 milestone def still correctly lists 1.AB as required for M2).

Refs: ADR-0027, ADR-0029, PR #16

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ionHost, terminal-event invariant)

Records the three 1.N run-loop substrate decisions no existing ADR covers:
an in-house, platform-free typed RunEventBus (not node:events, not a new
runtime dependency); the injected ExecutionHost execution-mode seam (clock /
store / transport, so the loop never branches on local-vs-cloud); and the
exactly-one-terminal-event invariant with its cancel/timeout/budget precedence
and crash-reconciliation rule. Cites ADR-0003 (dispatch shape, derived
checkpoint) and ADR-0028 (governance terminality) rather than restating them.

Resolves the documented docs-vs-purity-fence contradiction (canonical prose
literally says "EventEmitter", which the engine-purity fence bans in
packages/core/src); the three literal-"EventEmitter" doc homes are reworded as
part of the 1.N implementation PR.

Refs: ADR-0036

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The engine run loop (ADR-0036): walks the 1.M RunPlan, dispatches every ready
node through the injected NodeExecutor seam (1.O/1.P fill it), and emits the
canonical RunEvent stream with the exactly-one-terminal-event guarantee.

- RunEventBus: an in-house, platform-free typed pub/sub over the @relavium/shared
  RunEvent union (NOT node:events — the engine-purity fence bans it). The single
  producer-side translation point: assigns the monotonic, gap-free sequenceNumber
  + timestamp, validates against RunEventSchema, isolates throwing subscribers,
  and stamps a secret-free correlationId on failures.
- RunHandle.events: a bounded, producer-await (no-drop) async-iterable bridge over
  the bus, co-equal with .subscribe(); completes on the terminal event.
- ExecutionHost seam: injected clock / id source / RunStore / abort controller so
  the loop is platform-free and identical across execution modes (local now, cloud
  later); a deterministic in-memory host + store for tests and the local reference.
- WorkflowEngine.start/resume/cancel + reconcile: a serialized, completion-driven
  ready queue (no sleep/poll); condition skip-propagation (a fan-in joins over a
  skipped branch instead of hanging); the max_parallel cap; cooperative AbortSignal
  cancellation (cancel wins a race with a late failure); exactly one terminal event
  (incl. uncaught-throw -> run:failed{internal} and crash-reconciliation ->
  run:failed, never a stuck run:started); persist-before-deliver at node boundaries.
- NodeExecutor / NodeOutcome / GateRequest seam for 1.O (AgentRunner) and 1.P
  (node handlers); typed EngineStateError for API-boundary misuse (narrow on code).

Adds an optional, secret-free correlationId to the @relavium/shared run-event error
payload (additive, forward-compatible). Rewords the literal "EventEmitter" in its
three doc homes to the platform-free typed bus the purity fence requires, fixes the
sse-event-schema canonical-home pointer, and pins the timer/persist/sequence/
terminality/skip/retry notes the run loop depends on.

47 engine unit tests; engine >=90% line+branch; zero platform imports (purity gate
green). Roadmap entry stays In-progress until the PR merges.

Refs: ADR-0036

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…l-execution boundary (1.T)

Record the engine→host tool-execution boundary that no existing doc covered: the
ToolRegistry owns tool policy + dispatch (pure), and every side effect goes through a
host-injected ToolHost capability seam — the ResolverCapabilities (1.L2) / ADR-0018
purity-seam pattern generalized. Sibling to ADR-0036's ExecutionHost.

- ADR-0037 (Proposed): the decision, the policy/mechanism split, the discriminated
  egress kinds (http/search/mcp), the host-resolved credential-ref boundary, model-facing
  result bounding, and the error→ErrorCode mapping (tool_denied fatal; cancelled on abort).
- tool-registry.md: the canonical ToolHost/ToolDef contract, the 8-step dispatch lifecycle
  (effective-args assembled+validated BEFORE the guardrail checks, so a tool node cannot
  bypass the allowlist), the guardrail table, result bounding, the untrusted-data taint.
- built-in-tools.md: config-only params, tool-result bounding, subprocess environment.
- Index rows in decisions/ and reference/shared-core/.

Refs: ADR-0037, ADR-0029

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…he ToolHost seam

The engine-side registry the AgentRunner (1.O) and AgentSession (1.V) invoke. Pure
(zero platform imports): resolution, the node-grant check (registered ≠ authorized),
effective-arg assembly + Zod validation + secret-taint re-check, the ADR-0029 guardrails
on the EFFECTIVE args (so a tool node whose args come from input_mapping cannot bypass the
command/domain allowlist), output_mapping on the full result, model-facing byte/line
bounding with spill-to-store, and untrusted-result branding. Every side effect goes through
the injected ToolHost (fs/process/egress/os/mcp/outputStore); the 12 built-in ToolDefs span
every policy class. Egress tools dispatch against a stub host now and ship gated at the
surfaces until the shared SSRF primitive lands (1.AE).

- tools/{types,errors,untrusted,bounding,registry,builtins}.ts + 81 tests
- tools coverage 98% line / 94% branch; full turbo lint+typecheck+test+build green
- No new dependency (Zod already in core); no vendor type crosses the @relavium/llm seam

Refs: ADR-0037, ADR-0029

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ngs)

Adversarial review found the exactly-one-terminal-event invariant breaks on a
store fault: #emitDurable stamped the event, and on a persistEvent rejection
NEVER delivered it — so the consumer stream never closed (a zombie run) and the
rejection escaped the fire-and-forget #loop as an unhandled rejection. This hit
the terminal #settle, #emitPausedOnce, node:started dispatch, and the begin()
double-failure paths.

Fix: make #emitDurable TOTAL. It persists, then ALWAYS delivers (keeping the
stream gap-free and guaranteeing a terminal always closes the consumer's
for-await), and never rejects. A persist failure of a NON-terminal event
additionally fails the run (we never report progress the durable log lacks); a
terminal whose write fails is still delivered in-process, with reconcile()
repairing the durable record on restart. Persist-before-deliver is preserved on
the success path.

Also from the review:
- reconcile() now stamps a correlationId on its run:failed, matching #settle and
  node:failed.
- handle.cancel() is idempotent after termination (a best-effort surface action);
  engine.cancel(runId) keeps the strict throwing contract.
- docs: createAbortController matches native (a listener added post-abort never
  fires — check signal.aborted); the ExecutionHost timer port (gate/run timeout)
  is noted as deferred to 1.Q/1.AC.

Regression tests: a store whose every write rejects still terminates with exactly
one terminal (no hang); a terminal-only write failure still delivers run:completed;
gap-free sequenceNumber asserted across cancel / condition-skip / gate-resume; a
post-abort listener does not fire. 51 engine tests; engine >=90% line+branch.

Refs: ADR-0036

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…st gaps (sonnet review)

A second, sonnet-model multi-dimensional review found the 70e4442 fix left ONE
zombie-run path: #emitPausedOnce -> #emitDurable(run:paused). On a run:paused
persist failure the catch set #failure and aborted but did NOT re-enter the
scheduler — and #emitPausedOnce is the only #emitDurable call-site reached from
#step with no unconditional #schedule() afterward. So a gate-pause persist fault
set #failure yet never reached #settle: the consumer stream hung (a zombie),
re-created exactly on the gate path.

Fix: call this.#schedule() inside the #emitDurable catch (idempotent at every
other call-site via the #scheduling guard). Now any non-terminal persist failure
that fails the run always drives it to a single terminal.

Also from the review:
- reconcile() guards each run's write: a store fault reconciling one interrupted
  run no longer abandons the rest (best-effort, idempotent, retried next time).
- assertGapFreeSeq now checks the SET of sequenceNumbers is {0..n-1} (sorted), not
  delivery position — so it stays valid once 1.O streams tokens concurrently
  (delivery order may differ from emission order; the set must not gap). Added it
  to the max_parallel concurrent-fan-out test.
- refreshed the now-stale #onOutcome catch comment (#emitDurable is total, so a
  persist rejection no longer reaches it; it backstops an unexpected stamp throw).

Regression tests: a gate whose run:paused write fails still terminates with one
terminal (no zombie); an all-skipped fan-in is itself skipped; reconcile() skips a
write-failing run and still reconciles the others. 54 engine tests; >=90% line+branch.

Refs: ADR-0036

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wrap long import lists / type unions in engine.ts, node-executor.ts, and
event-bus.test.ts to satisfy `prettier --check` (CI format gate). No behavior
change; 54 engine tests still green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ity + bounding)

A 9-dimension adversarial review (185 agents, 3-lens verification) confirmed 43 findings
on the ToolRegistry/ToolHost seam: fix-then-keep, no blockers. This folds all High/Medium
+ the low-severity hardening:

- H1/M3: config-only params (endpoint/credentialRef/env/cwd) now come ONLY from
  ctx.config.parameters — a model- or input_mapping-supplied value is dropped in
  assembleArgs (closes the web_search secret-exfil + run_command env/cwd injection the
  contract forbade "by the engine, not by convention").
- H2: git_status `args` is now config-only (author-pinned), closing model-controlled git
  flag injection (`--no-index` arbitrary-read, `log -p --all` history dump).
- H3: readPath walks own data properties only (Object.hasOwn) — an output_mapping path
  naming __proto__/constructor returns undefined, never the prototype/constructor.
- H4/L4: the model-facing preview now honors BOTH the line and byte ceiling, with a
  code-point-safe byte slice (no lone surrogate).
- M1: git_commit inserts a `--` separator and rejects a leading-dash pathspec (no git
  option injection past the human gate).
- M2/L2/L3: the post-dispatch tail (output_mapping + bounding/spill) is under the
  cancellation/error ladder; a spill I/O failure degrades to a preview-only result while a
  spill-time abort surfaces as `cancelled`; abort-after-resolve classifies cancelled; the
  cancel ladder no longer double-wraps.
- proto: assembleArgs skips __proto__/constructor/prototype from every source.
- L5: host FQDN extraction fails closed on backslash/whitespace/control chars (hyphens OK).
- L6: glob collapses consecutive `*` (no ReDoS) and caches compiled patterns.
- L8: a provider-executed tool_call gets its own `provider_executed` deny reason.
- M4/M5/M6/L7/L9 docs: FsScopeTier imported from @relavium/shared (was 'project-scoped');
  secretArgKeys/policyTarget added + invokeAgent/limits marked optional in the spec; the
  "never buffered in the engine" claim corrected to the in-memory v1.0 reality (host-stream
  obligation recorded); git_commit "Stage files" + the return-shape column corrected.

+22 regression tests (103 tools tests; coverage 97% line / 93% branch). Full turbo
lint+typecheck+test+build green (core 461 tests). No new dependency; no vendor type
crosses the seam; engine purity held (no control bytes, no node:*/DOM globals).

Refs: ADR-0037, ADR-0029

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A second independent adversarial review (Sonnet, 81 agents, 3-lens verification) re-audited the
round-1 fixes and found two genuinely incomplete ones plus a new ReDoS and edge cases. All folded:

- H-1 (fix-incomplete): the round-1 abort guard was added after the dispatch await but NOT after
  boundForModel — its async fast path yields a microtask in which a cancel could slip through to a
  success return. Added the symmetric throwIfAborted after bounding (success-after-cancel window closed).
- M-2 (fix-incomplete): the proto-pollution defense covered copyArgs + readPath segments but not the
  output_mapping STATEKEY — a `__proto__` stateKey mutated the result object's prototype. applyOutputMapping
  now skips UNSAFE_ARG_KEYS.
- M-1 (new ReDoS): globMatch compiled `a*a*…X` to a backtracking RegExp (alternating-literal-star hangs).
  Replaced with a linear-time iterative matcher (single backtrack point); removed the RegExp + cache.
- L-1/L-2: utf8ByteLength now counts a lone high surrogate as 3 bytes without skipping the next char;
  sliceToBytes(fromEnd) accounts a lone low surrogate correctly.
- L-3: documented cancel-wins-all (ADR-0036 precedence) at the catch ladder.
- Docs: ADR-0037 + tool-registry.md flipped to Accepted/Stable (peer ADR-0036 parity); the spec's
  llmVisibleParams typed JsonSchema not JSONSchema7 (L-6); EgressCapability.fetch ctx.limits overclaim
  corrected (I-1); error-taxonomy + lifecycle pre-step now name provider_executed (I-2/I-3); SSRF test
  comment corrected (I-4).

+15 regression tests (118 tools tests; both abort guards, the proto stateKey, ReDoS-completes-fast +
glob correctness, the surrogate edges, git_status input_mapping strip, DEL smuggling char, git_commit
empty-files argv). Full turbo lint+typecheck+test+build green (core 461 → now more); no control bytes;
engine purity + seam held.

Refs: ADR-0037, ADR-0029

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Format packages/core/src/tools/* (registry, builtins, types, errors, bounding,
untrusted + tests) to satisfy `prettier --check` (CI format gate). No behavior
change; 118 tool tests still green. Completes the joint 1.N + 1.T branch's format
compliance before the PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @cemililik, your pull request is larger than the review limit of 150000 diff characters

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 30f73f43-a803-4cc4-8e21-ea72d40e8628

📥 Commits

Reviewing files that changed from the base of the PR and between 6ec6eb3 and bb1b821.

📒 Files selected for processing (3)
  • docs/reference/contracts/sse-event-schema.md
  • docs/reference/shared-core/built-in-tools.md
  • packages/core/src/tools/registry.ts

📝 Walkthrough

Walkthrough

Adds a platform-free WorkflowEngine run loop, typed RunEventBus and RunHandle streams, ExecutionHost/in-memory store, node-executor contracts, a ToolRegistry with built-ins, result bounding and untrusted branding, ADRs/docs, and comprehensive tests.

Changes

Run-Loop Infrastructure and Tool Execution

Layer / File(s) Summary
Docs, ADRs, roadmap, and SSE schema
docs/**, docs/decisions/**, docs/reference/**
Adds ADR-0036/0037, updates execution-model, roadmap, SSE event schema, error-handling standards, and shared-core reference rows to record run-loop and tool-host decisions.
Shared RunEvent schema
packages/shared/src/run-event.ts, packages/shared/src/run-event.test.ts, docs/reference/contracts/sse-event-schema.md
Extends shared RunEvent error shape with optional correlationId and aligns docs/tests and transport notes to use the in-process RunEventBus.
RunEventBus implementation & tests
packages/core/src/engine/event-bus.ts, packages/core/src/engine/event-bus.test.ts
Introduces platform-free typed RunEventBus with draft stamping, per-correlation gap-free sequenceNumber, optional validation, delivery/fan-out, and listener-error isolation plus tests.
ExecutionHost and InMemoryRunStore
packages/core/src/engine/execution-host.ts, packages/core/src/engine/execution-host.test.ts
Defines ExecutionHost seam (Clock, IdSource, AbortControllerLike, RunStore), implements InMemoryRunStore and createInMemoryHost with deterministic clock/ids and interrupted-run reconciliation tests.
RunHandle / RunEventStream
packages/core/src/engine/run-handle.ts, packages/core/src/engine/run-handle.test.ts
Adds single-consumer bounded async iterator bridging RunEventBus to RunHandle.events, backpressure/drain semantics, scoped subscribe/cancel, and tests.
Engine contracts
packages/core/src/engine/errors.ts, packages/core/src/engine/node-executor.ts
Adds EngineStateError discriminants/class and NodeExecutor boundary types (NodeOutcome, NodeStreamEvent, NodeExecContext, GateRequest, GateType).
WorkflowEngine runtime and tests
packages/core/src/engine/engine.ts, packages/core/src/engine/engine.test.ts
Implements WorkflowEngine scheduling loop, per-vertex execution, skip-propagation, durability (#emitDurable persistence-before-delivery), start/resume/cancel/reconcile APIs, and extensive behavioral tests.
Tool types, untrusted, bounding
packages/core/src/tools/types.ts, packages/core/src/tools/untrusted.ts, packages/core/src/tools/bounding.ts, tests
Defines ToolHost capability seam, tool domain types, Untrusted branding, utf8ByteLength and boundForModel with UTF-8-safe preview, spill-to-store semantics and tests.
Tool dispatch errors & registry
packages/core/src/tools/errors.ts, packages/core/src/tools/registry.ts, packages/core/src/tools/registry.test.ts
Adds typed ToolDispatchError hierarchy, implements createToolRegistry dispatch pipeline (effective args, secret-taint, validation, guardrails, host call, output_mapping, bounding, sanitization, cancellation), and comprehensive hardening tests.
Built-in tools
packages/core/src/tools/builtins.ts, packages/core/src/tools/builtins.test.ts
Registers 12 built-in tools (fs/process/egress/mcp/os), host-capability guards, web_search URL rules, git hardening and exports, with tests covering dispatch and edge cases.
Public exports
packages/core/src/index.ts
Expands @relavium/core public surface to re-export engine, RunEventBus/RunHandle, ExecutionHost helpers, node-executor boundaries, tool registry/builtins/types/errors/untrusted/bounding.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant WorkflowEngine
  participant ExecutionHost
  participant RunEventBus
  Client->>WorkflowEngine: start(input)
  WorkflowEngine->>ExecutionHost: resolveWorkflowId / persistEvent(run:started)
  WorkflowEngine->>WorkflowEngine: schedule / claimReady
  WorkflowEngine->>ExecutionHost: persistEvent(node:completed|node:failed)
  WorkflowEngine->>RunEventBus: emit(draft event)
  RunEventBus->>RunEventBus: stamp timestamp & sequenceNumber
  RunEventBus->>Client: deliver(event) (subscribers / RunHandle)
Loading

"🐇 I stitched a loop that hums and runs,
Events stamped neat like ordered drums,
Tools spill large outputs, boxed and signed,
Docs and tests keep every path defined,
One tidy terminal — resilient and kind."

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch development

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements the core engine run loop (WorkflowEngine and RunEventBus under workstream 1.N) and the engine-side tool execution boundary (ToolRegistry and ToolHost under workstream 1.T), aligning with ADR-0036 and ADR-0037. It introduces a platform-free, in-house typed event bus, an execution host seam for persistence and clock injection, and a secure tool dispatch lifecycle with result bounding and untrusted-data branding. Feedback on the changes identifies a potential bug in the web_search tool where query parameters are unconditionally appended with a '?' separator, which can result in malformed URLs if the configured endpoint already contains query parameters.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread packages/core/src/tools/builtins.ts Outdated
Comment on lines +326 to +327
const endpoint = args.endpoint ?? '';
const url = `${endpoint}?q=${encodeURIComponent(args.query)}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In the web_search tool, the URL is constructed by unconditionally appending ?q=... to the endpoint. If the configured endpoint already contains query parameters (for example, https://api.searxng.org/search?format=json), this will result in a malformed URL with multiple ? characters (e.g., https://api.searxng.org/search?format=json?q=...).

We should dynamically determine the query separator (? or &) based on whether the endpoint already contains a ? character.

    const endpoint = args.endpoint ?? '';
    const separator = endpoint.includes('?') ? '&' : '?';
    const url = endpoint + separator + "q=" + encodeURIComponent(args.query);

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (3)
packages/core/src/tools/builtins.ts (1)

46-61: 🏗️ Heavy lift

Avoid erasing the tool arg type with as ToolDef.

This helper sits in front of every built-in, so the cast severs the compile-time link between parseArgs, policyTarget, and dispatch for all 12 tools. That conflicts with the repo rule against unsafe as assertions and makes a mismatched tool spec type-check when it should fail. Please move the widening to the catalog boundary instead of inside the helper.

As per coding guidelines, "**/*.{ts,tsx}: Use TypeScript with strict mode; no any or unsafe as type assertions`."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/tools/builtins.ts` around lines 46 - 61, The helper
defineBuiltin currently erases the Args generic by returning def as ToolDef,
which unsafely severs the compile-time link between parseArgs, policyTarget, and
dispatch; change defineBuiltin to preserve the generic (return ToolDef<A> or
infer and export the generic ToolDef type) so the created def retains its A type
(keep the object fields as-is: id, source, description, parseArgs,
llmVisibleParams, policy, configOnlyParams, policyTarget, dispatch) and remove
the unsafe "as ToolDef" cast, and then perform the necessary widening only at
the catalog/registry boundary where heterogeneous ToolDef[] is built (i.e.,
replace the cast there with an explicit, local, and documented widening
operation), ensuring all call sites that collect builtins into a ToolDef[] do
the single, controlled cast while defineBuiltin and individual builtins remain
type-safe.

Source: Coding guidelines

packages/core/src/engine/run-handle.test.ts (1)

36-147: ⚡ Quick win

Add a multi-run isolation regression test.

Please add one test where the same bus feeds two handles (run-1, run-2) and assert each handle only receives its own events and only closes on its own terminal event.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/engine/run-handle.test.ts` around lines 36 - 147, Add a new
test to verify multi-run isolation: using bus(), create two handles via
createRunHandle(b, 'run-1', ...) and createRunHandle(b, 'run-2', ...), emit
run-scoped events with started(...) and completed() intended for each run (emit
started for run-1 and run-2 separately), then await drain(handle.events) (or use
the async iterator) for each handle and assert that each handle.events only
contains its own events (types and sequenceNumbers) and that when you emit the
terminal completed() for run-1 only run-1's stream closes while run-2 remains
open until you emit its completed(), after which run-2 closes; use the existing
helpers started(), completed(), drain(), and subscribe/assert patterns from the
file to implement these checks.
packages/core/src/engine/event-bus.test.ts (1)

61-66: ⚡ Quick win

Unsafe test fixture assertions are repeated across files; replace with checked typing.
The shared root cause is use of unsafe as assertions where typed fixtures can be expressed directly, reducing safety and masking schema drift.

  • packages/core/src/engine/event-bus.test.ts#L61-L66: replace the orphan fixture with a directly-typed RunEventDraft negative case (no double-cast).
  • packages/core/src/engine/event-bus.test.ts#L71-L80: type bad directly as RunEventDraft and keep only the runtime-invalid payload (costMicrocents: -1).
  • packages/core/src/engine/execution-host.test.ts#L49-L57: define sessionEvent with satisfies RunEvent (or equivalent checked typing) instead of as RunEvent.
  • packages/core/src/engine/execution-host.test.ts#L61-L69: define runEvent with checked typing instead of as RunEvent.

As per coding guidelines, **/*.{ts,tsx} must avoid unsafe as assertions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/engine/event-bus.test.ts` around lines 61 - 66, For each
failing test fixture, remove the unsafe "as" casts and replace with checked
typing: in packages/core/src/engine/event-bus.test.ts (lines 61-66) replace the
orphan double-cast with an object literal explicitly typed as RunEventDraft
(declare const orphan: RunEventDraft = { ... }) instead of using as unknown as;
in packages/core/src/engine/event-bus.test.ts (lines 71-80) declare const bad:
RunEventDraft = { ... } and only keep the runtime-invalid payload
(costMicrocents: -1) rather than casting; in
packages/core/src/engine/execution-host.test.ts (lines 49-57) define
sessionEvent using a checked typing (e.g., const sessionEvent = { ... }
satisfies RunEvent or declare const sessionEvent: RunEvent = { ... }) instead of
using as RunEvent; and in packages/core/src/engine/execution-host.test.ts (lines
61-69) define runEvent with checked typing (satisfies RunEvent or explicit
RunEvent type) instead of as RunEvent. Ensure all fixtures conform to the
RunEvent/RunEventDraft shapes at compile time and retain the same runtime
invalid scenarios for the negative tests.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/engine/engine.ts`:
- Around line 810-816: reconcile() currently skips interrupted runs with
run.resumable but never hydrates them into the engine's in-memory `#runs`, so
subsequent resume() can't find them and throws unknown_run; fix by either (A)
hydrating resumable interrupted runs into active RunExecution instances during
reconcile/startup: for each interrupted run where run.resumable is true,
reconstruct the corresponding RunExecution (using the RunExecution
constructor/initializer and the persisted run metadata and state) and insert it
into this.#runs with the correct execution state so resume() can operate
normally, or (B) if full hydration isn't yet supported, implement a clear
fallback in reconcile(): for resumable interrupted runs, update their durable
state to failed/terminal (or enqueue them for durable retry) so they are not
left orphaned—pick one approach and apply it consistently in reconcile() and any
startup rehydration path so resume() no longer encounters missing runs.

In `@packages/core/src/engine/event-bus.ts`:
- Around line 130-133: In `#reportListenerError`, prevent exceptions from the
user-provided this.#onListenerError from bubbling into deliver() by wrapping the
this.#onListenerError(error, event) call in a try/catch; inside the catch,
forward the secondary failure out-of-band (if you have an existing sink like
this.#onListenerErrorSink call it with the caught error and event), otherwise
log the secondary error (e.g., console.error or internal logger) and return — do
not rethrow; ensure callers like deliver() rely on `#reportListenerError` to
isolate listener-handler failures.

In `@packages/core/src/engine/run-handle.ts`:
- Around line 156-167: The RunHandle is currently receiving all bus events;
scope events to the current runId by filtering both the internal subscription
and the exposed subscribe function: change the bus.subscribe callback that
pushes into primary to only handle events where event.runId === runId (and still
close primary/unsubscribe when a TERMINAL_TYPES event for this runId is seen),
and change the returned subscribe implementation to return
bus.subscribe(listenerFiltered) where listenerFiltered only forwards events with
event.runId === runId (so external subscribers only see events for this run).
Ensure references: RunHandle/runId, primary, bus.subscribe, subscribe,
TERMINAL_TYPES are patched accordingly.
- Around line 114-119: The current return() implementation bypasses close() and
so can leave a waiting next() unresolved; change return() to delegate to and
return this.close() (removing the duplicate logic that sets `#closed`, clears
`#buffer`, and calls `#wakeDrainWaiters`) so that any parked next() is settled
deterministically by the existing close() logic in RunHandle.

In `@packages/core/src/tools/bounding.test.ts`:
- Line 1: Run Prettier on the test file packages/core/src/tools/bounding.test.ts
to fix formatting (CI flagged line 1); simply format the file (e.g., via
prettier --write packages/core/src/tools/bounding.test.ts or your editor/IDE
formatter) and commit the formatted file so the import line and whole file
comply with project style.
- Around line 117-120: The test's `signal` variable currently uses `as never`,
violating the AbortSignalLike contract; replace it with a real stub implementing
AbortSignalLike (i.e., include `aborted: true` plus no-op `addEventListener` and
`removeEventListener` methods) so the test for `boundForModel` ("rethrows an
abort that occurs during spill (cancel precedence) (M2)") satisfies the type
checker and behaves correctly.

In `@packages/core/src/tools/builtins.ts`:
- Around line 312-330: The dispatch implementation currently accepts an optional
endpoint and forwards credentialRef to requireEgress.fetch; change the config to
make endpoint a required config-only value and update dispatch (the dispatch
function and its use of requireEgress) to validate the resolved endpoint is an
absolute HTTPS URL and reject any request that is not HTTPS or that pairs a
credentialRef with a non-HTTPS/invalid endpoint before calling
requireEgress(host, 'web_search').fetch; specifically, locate the dispatch
function and replace the endpoint fallback (endpoint = args.endpoint ?? '') with
a required check that throws/returns an error when args.endpoint is missing or
not starting with "https://", and ensure that if args.credentialRef is present
you only proceed when the endpoint is a valid HTTPS URL (otherwise reject and do
not pass credentialRef to the host), and update any tests that assumed optional
endpoints to expect the new required/validated behavior.
- Around line 309-327: The schema exposes maxResults but dispatch (function
dispatch, args) never uses it, so either remove it from the
schema/llmVisibleParams or thread it into the request URL; to fix, update
dispatch to check args.maxResults (validated as positive int) and, if present,
append it to the endpoint query (e.g., add &maxResults=<encoded int>) when
building url (ensure encodeURIComponent or numeric serialization is used), or
alternatively remove maxResults from the Zod schema and llmVisibleParams if the
provider does not support it.

In `@packages/core/src/tools/errors.ts`:
- Line 1: The file fails Prettier formatting; run Prettier (or your project's
format script) against the file packages/core/src/tools/errors.ts and update the
file with the formatted output, then stage and commit the change so the CI
Prettier check passes.

In `@packages/core/src/tools/untrusted.ts`:
- Around line 35-40: isUntrusted currently uses an unsafe cast (value as
Record<symbol, unknown>) to access the UNTRUSTED property; replace that cast
with a safe reflective lookup: after confirming typeof value === 'object' &&
value !== null, use Reflect.get(value, UNTRUSTED) === true to perform the
runtime check against the UNTRUSTED symbol (function name: isUntrusted, symbol:
UNTRUSTED) so no type assertion is needed and behavior remains the same.

---

Nitpick comments:
In `@packages/core/src/engine/event-bus.test.ts`:
- Around line 61-66: For each failing test fixture, remove the unsafe "as" casts
and replace with checked typing: in packages/core/src/engine/event-bus.test.ts
(lines 61-66) replace the orphan double-cast with an object literal explicitly
typed as RunEventDraft (declare const orphan: RunEventDraft = { ... }) instead
of using as unknown as; in packages/core/src/engine/event-bus.test.ts (lines
71-80) declare const bad: RunEventDraft = { ... } and only keep the
runtime-invalid payload (costMicrocents: -1) rather than casting; in
packages/core/src/engine/execution-host.test.ts (lines 49-57) define
sessionEvent using a checked typing (e.g., const sessionEvent = { ... }
satisfies RunEvent or declare const sessionEvent: RunEvent = { ... }) instead of
using as RunEvent; and in packages/core/src/engine/execution-host.test.ts (lines
61-69) define runEvent with checked typing (satisfies RunEvent or explicit
RunEvent type) instead of as RunEvent. Ensure all fixtures conform to the
RunEvent/RunEventDraft shapes at compile time and retain the same runtime
invalid scenarios for the negative tests.

In `@packages/core/src/engine/run-handle.test.ts`:
- Around line 36-147: Add a new test to verify multi-run isolation: using bus(),
create two handles via createRunHandle(b, 'run-1', ...) and createRunHandle(b,
'run-2', ...), emit run-scoped events with started(...) and completed() intended
for each run (emit started for run-1 and run-2 separately), then await
drain(handle.events) (or use the async iterator) for each handle and assert that
each handle.events only contains its own events (types and sequenceNumbers) and
that when you emit the terminal completed() for run-1 only run-1's stream closes
while run-2 remains open until you emit its completed(), after which run-2
closes; use the existing helpers started(), completed(), drain(), and
subscribe/assert patterns from the file to implement these checks.

In `@packages/core/src/tools/builtins.ts`:
- Around line 46-61: The helper defineBuiltin currently erases the Args generic
by returning def as ToolDef, which unsafely severs the compile-time link between
parseArgs, policyTarget, and dispatch; change defineBuiltin to preserve the
generic (return ToolDef<A> or infer and export the generic ToolDef type) so the
created def retains its A type (keep the object fields as-is: id, source,
description, parseArgs, llmVisibleParams, policy, configOnlyParams,
policyTarget, dispatch) and remove the unsafe "as ToolDef" cast, and then
perform the necessary widening only at the catalog/registry boundary where
heterogeneous ToolDef[] is built (i.e., replace the cast there with an explicit,
local, and documented widening operation), ensuring all call sites that collect
builtins into a ToolDef[] do the single, controlled cast while defineBuiltin and
individual builtins remain type-safe.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: de003f9d-19e5-44a1-b94e-fb04ef67b3dd

📥 Commits

Reviewing files that changed from the base of the PR and between 0c62f24 and 980dadf.

📒 Files selected for processing (39)
  • CLAUDE.md
  • README.md
  • docs/architecture/execution-model.md
  • docs/architecture/shared-core-engine.md
  • docs/decisions/0036-run-loop-substrate-event-bus-and-execution-host.md
  • docs/decisions/0037-engine-tool-execution-boundary.md
  • docs/decisions/README.md
  • docs/reference/contracts/sse-event-schema.md
  • docs/reference/shared-core/README.md
  • docs/reference/shared-core/built-in-tools.md
  • docs/reference/shared-core/node-types.md
  • docs/reference/shared-core/run-plan.md
  • docs/reference/shared-core/tool-registry.md
  • docs/roadmap/current.md
  • docs/roadmap/phases/phase-1-engine-and-llm.md
  • docs/standards/error-handling.md
  • packages/core/src/engine/engine.test.ts
  • packages/core/src/engine/engine.ts
  • packages/core/src/engine/errors.test.ts
  • packages/core/src/engine/errors.ts
  • packages/core/src/engine/event-bus.test.ts
  • packages/core/src/engine/event-bus.ts
  • packages/core/src/engine/execution-host.test.ts
  • packages/core/src/engine/execution-host.ts
  • packages/core/src/engine/node-executor.ts
  • packages/core/src/engine/run-handle.test.ts
  • packages/core/src/engine/run-handle.ts
  • packages/core/src/index.ts
  • packages/core/src/tools/bounding.test.ts
  • packages/core/src/tools/bounding.ts
  • packages/core/src/tools/builtins.test.ts
  • packages/core/src/tools/builtins.ts
  • packages/core/src/tools/errors.ts
  • packages/core/src/tools/registry.test.ts
  • packages/core/src/tools/registry.ts
  • packages/core/src/tools/types.ts
  • packages/core/src/tools/untrusted.test.ts
  • packages/core/src/tools/untrusted.ts
  • packages/shared/src/run-event.ts

Comment on lines +810 to +816
async reconcile(): Promise<readonly RunEvent[]> {
const interrupted = await this.#host.store.listInterruptedRuns();
const reconciled: RunEvent[] = [];
for (const run of interrupted) {
if (run.resumable) {
continue;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Resumable interrupted runs are orphaned after restart.

reconcile() deliberately skips run.resumable, but the engine never hydrates those runs into #runs. On restart, resume() (Line 788-Line 790) can only see in-memory runs and throws unknown_run, so gate-paused interrupted runs cannot actually be resumed.

This needs either (a) persisted-state hydration into active RunExecution entries, or (b) explicit fallback behavior (e.g., reconcile-to-failed until durable resume hydration exists) so resumable runs don’t remain indefinitely stranded.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/engine/engine.ts` around lines 810 - 816, reconcile()
currently skips interrupted runs with run.resumable but never hydrates them into
the engine's in-memory `#runs`, so subsequent resume() can't find them and throws
unknown_run; fix by either (A) hydrating resumable interrupted runs into active
RunExecution instances during reconcile/startup: for each interrupted run where
run.resumable is true, reconstruct the corresponding RunExecution (using the
RunExecution constructor/initializer and the persisted run metadata and state)
and insert it into this.#runs with the correct execution state so resume() can
operate normally, or (B) if full hydration isn't yet supported, implement a
clear fallback in reconcile(): for resumable interrupted runs, update their
durable state to failed/terminal (or enqueue them for durable retry) so they are
not left orphaned—pick one approach and apply it consistently in reconcile() and
any startup rehydration path so resume() no longer encounters missing runs.

Comment thread packages/core/src/engine/event-bus.ts Outdated
Comment thread packages/core/src/engine/run-handle.ts
Comment thread packages/core/src/engine/run-handle.ts
Comment thread packages/core/src/tools/bounding.test.ts
Comment thread packages/core/src/tools/bounding.test.ts Outdated
Comment thread packages/core/src/tools/builtins.ts Outdated
Comment thread packages/core/src/tools/builtins.ts Outdated
Comment thread packages/core/src/tools/errors.ts
Comment thread packages/core/src/tools/untrusted.ts Outdated
cemililik and others added 2 commits June 13, 2026 19:51
Inline review:
- run-handle: return() now delegates to close() so a parked next() is settled
  deterministically (was duplicate logic that skipped the waiting-pull resolve).
- run-handle: scope both the primary subscription and the exposed subscribe() to
  the handle's runId, so on a bus shared across runs/sessions (the ADR-0036 "one
  bus, two namespaces" model) a handle only ever sees its own run's events.
- event-bus: isolate a throwing onListenerError — wrap it so a sink failure can't
  bubble into deliver() and break the producer/sibling subscribers; surface it
  out-of-band instead.

Skipped (with reason):
- reconcile() not hydrating a resumable interrupted run: rehydrating a gate-parked
  run's RunExecution from persisted state is the Checkpointer's job (1.R); failing
  it here would destroy the resumability 1.R restores. Documented in-code; resume()
  throws unknown_run by design until 1.R lands.

Sonar:
- #onOutcome split into #settleCompleted/#settleFailed/#settlePaused/#failNodeInternal
  (cognitive complexity 27 -> well under 15); #step's idle handling extracted to
  #handleIdle (16 -> under 15). Behavior unchanged — 55 engine tests still green.
- optional chains (#claimReady, #failNodeInternal); flipped negated optional-field
  ternaries (resume/#settle/#settlePaused) to positive; execution-host uses
  events.at(-1). The #nodeEmit per-type cases are KEPT (each narrows `event` to a
  distinct union member so the spread keeps its fields; merging needs an unsafe
  cast) — documented in-code rather than collapsed.

Tests: removed a dead `events` collection (unknown-gate test); hoisted seedStarted
to module scope; added gap-free-sequence assertions and a multi-run-isolation test
that locks in the runId scoping. engine >=90% line+branch.

Refs: ADR-0036

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
web_search (builtins): made `endpoint` a REQUIRED config-only value validated as an
absolute https:// URL before any credentialRef is attached (no exfil to a missing/
insecure target); threaded the previously-unused `maxResults` into the request URL;
chose the query separator dynamically (`&` when the endpoint already has a query
string) to avoid a double `?`. The https/absolute check uses a string/regex test —
NOT the `URL` global (absent from the engine-purity lib, rule 5); deep parsing + the
SSRF range checks remain behind the egress host capability (ADR-0029(d)).

Type-safety / Sonar:
- defineBuiltin preserves the Args generic (ToolDef<A>) instead of erasing it with a
  cast; the single controlled widening moved to the BUILTIN_TOOLS catalog boundary.
- untrusted: isUntrusted uses Reflect.get(value, UNTRUSTED) === true (no cast).
- explicit String.localeCompare comparators on id sorts (registry, errors, builtins);
  Object.hasOwn over Object.prototype.hasOwnProperty.call; toSorted/String.raw;
  String#codePointAt where it preserves byte semantics; removed a redundant type alias.

Skipped (with reason): charCodeAt sites in bounding's WTF-8 byte counter and lone-
surrogate handling stay as-is — codePointAt merges surrogate pairs and would change
the byte-width / lone-surrogate semantics those paths (and their tests) rely on.

Updated the no-secret-exfil registry test for the now-required config endpoint: it
pins a real endpoint + credentialRef and asserts the model's malicious overrides are
dropped. 483 core tests green; engine + tools type-clean under both tsconfig gates.

Refs: ADR-0037

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@cemililik

Copy link
Copy Markdown
Contributor Author

@coderabbitai review all

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

@cemililik Sure! I'll kick off a full review of all the changes in PR #17 right away.

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/architecture/execution-model.md (1)

141-143: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove the stray + before run_events.

That reads like a diff marker that leaked into the prose and breaks the checkpoint sentence. The ADR-0036 note below is fine.

Suggested cleanup
- ... from `step_executions`
- + `run_events` (+ `messages` for an orchestrator's history), all defined in
+ ... from `step_executions`, `run_events` (+ `messages` for an orchestrator's history), all defined in
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/architecture/execution-model.md` around lines 141 - 143, The sentence
describing the reconstructed checkpoint contains a stray '+' before the symbol
run_events; remove that plus so the prose reads "...from `step_executions`
`run_events` ( + `messages` for an orchestrator's history)..." or better combine
clearly as "...from `step_executions`, `run_events` (and `messages` for an
orchestrator's history)..." so the `Checkpointer`/`run_events` phrase is normal
prose rather than a diff marker.
🧹 Nitpick comments (3)
packages/core/src/engine/execution-host.test.ts (1)

74-95: ⚡ Quick win

Consider adding test for interrupted run detection.

The test validates that completed runs are excluded from listInterruptedRuns(), but there's no test showing that an incomplete run (one with run:started but no terminal event) IS returned. This would strengthen confidence in the reconciliation logic.

Suggested additional test
it('includes an incomplete run in the interrupted set', async () => {
  const store = new InMemoryRunStore();
  await store.persistEvent({
    runId: 'r1',
    timestamp: '2026-06-13T00:00:00.000Z',
    type: 'run:started',
    sequenceNumber: 0,
    workflowId: '00000000-0000-4000-8000-000000000001',
    inputs: {},
    executionMode: 'local',
  } as RunEvent);
  const interrupted = await store.listInterruptedRuns();
  expect(interrupted).toHaveLength(1);
  expect(interrupted[0]?.runId).toBe('r1');
  expect(interrupted[0]?.resumable).toBe(false); // not paused at a gate
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/engine/execution-host.test.ts` around lines 74 - 95, Add a
complementary test that verifies listInterruptedRuns() returns incomplete runs:
create an InMemoryRunStore, call persistEvent(...) with a single run:started
event (use the same runId and fields as the existing test), then call await
store.listInterruptedRuns() and assert length is 1, interrupted[0].runId ===
'r1' and interrupted[0].resumable === false; place this new it(...) alongside
the existing tests in execution-host.test.ts so it exercises
InMemoryRunStore.persistEvent and listInterruptedRuns behavior for non-terminal
runs.
packages/core/src/engine/execution-host.ts (1)

181-184: 💤 Low value

Redundant type check after find.

The started.type !== 'run:started' check on line 182 is redundant since the find on line 181 already filters for e.type === 'run:started'. However, TypeScript's type narrowing through find callbacks can be imprecise, so this serves as a type guard. Consider using a type predicate in the find callback for cleaner narrowing:

-     const started = events.find((e) => e.type === 'run:started');
-     if (started === undefined || started.type !== 'run:started') {
+     const started = events.find((e): e is RunEvent & { type: 'run:started' } => e.type === 'run:started');
+     if (started === undefined) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/engine/execution-host.ts` around lines 181 - 184, The code
performs a redundant runtime/type check "started.type !== 'run:started'" after
using events.find(e => e.type === 'run:started'); replace the find callback with
a proper type predicate so TypeScript narrows the result and you can drop the
redundant check: define a predicate like "function isRunStartedEvent(e: Event):
e is RunStartedEvent { return e.type === 'run:started'; }" (or use an inline
arrow predicate "(e): e is RunStartedEvent => e.type === 'run:started'") and
call events.find(isRunStartedEvent) to let "started" be typed as RunStartedEvent
and remove the extra started.type comparison. Ensure RunStartedEvent is the
specific union subtype used by your codebase.
packages/core/src/tools/builtins.ts (1)

56-57: 💤 Low value

Static analysis: prefer positive condition for conditional spreading.

SonarCloud flags the negated conditions. This pattern is functionally correct but can be simplified.

♻️ Suggested refactor
-    ...(spec.configOnlyParams !== undefined ? { configOnlyParams: spec.configOnlyParams } : {}),
-    ...(spec.policyTarget !== undefined ? { policyTarget: spec.policyTarget } : {}),
+    ...(spec.configOnlyParams ? { configOnlyParams: spec.configOnlyParams } : {}),
+    ...(spec.policyTarget ? { policyTarget: spec.policyTarget } : {}),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/tools/builtins.ts` around lines 56 - 57, Replace the
conditional ternary spreads that use negated checks with positive-condition
logical-AND spreads: instead of ...(spec.configOnlyParams !== undefined ? {
configOnlyParams: spec.configOnlyParams } : {} ) and ...(spec.policyTarget !==
undefined ? { policyTarget: spec.policyTarget } : {} ), use
...(spec.configOnlyParams && { configOnlyParams: spec.configOnlyParams }) and
...(spec.policyTarget && { policyTarget: spec.policyTarget }) so the object only
spreads when the property is present (refer to spec.configOnlyParams and
spec.policyTarget in builtins).

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/decisions/0037-engine-tool-execution-boundary.md`:
- Around line 35-37: The ADR contains an empty blank line inside the blockquote:
locate the blockquote that begins with "**`invoke_agent` is not a `ToolHost` I/O
capability.**" and remove the stray blank line so the bullets/paragraphs inside
the blockquote are contiguous (collapse the blank line between the
paragraphs/bullets that include the "Result bounding + spill — the deferred
tool-output-gate..." paragraph and the following "Typed errors map to the closed
`ErrorCode`." paragraph), ensuring no extra blank line remains inside the
blockquote.

In `@docs/reference/contracts/sse-event-schema.md`:
- Line 4: Decide and state a single contract authority: if the runtime Zod
schema is authoritative, update the text to say the Zod schema RunEvent in
`@relavium/shared` (run-event.ts) is the canonical, runtime-validated source of
truth (with `@relavium/core` consuming/re-exporting it) and remove or change the
later note that says the docs win; alternatively, if the docs should win,
explicitly state that this documentation is the authoritative contract and
change the earlier line that references the Zod schema to say the schema mirrors
the docs and is derived from them. Ensure all references to RunEvent,
`@relavium/shared`, run-event.ts, and `@relavium/core` are consistent with the
chosen authority.

In `@docs/reference/shared-core/run-plan.md`:
- Around line 50-52: The sentence claiming "1.N treats a node failure as
terminal for the run" is too broad; change it to say 1.N treats a node failure
as terminal only for the current attempt/step (not the overall run) and that
node-level retries are still handled by phase 1.S which reads retry_config from
the authored node (config.node) after provider fallback; preserve the note that
retry_config is not copied onto the vertex and that fallbackChain is lifted onto
AgentPlanConfig for run-loop convenience.

In `@packages/core/src/engine/engine.test.ts`:
- Around line 77-83: Replace unsafe "as" casts on thrown errors with proper
type-narrowing checks: in the helper expectThrowsCode and other test sites where
you do (error as EngineStateError).code or (caught as EngineStateError).code,
remove the cast and use an instanceof guard (e.g., if (error instanceof
EngineStateError) { expect(error.code).toBe(code) } else { throw error }) so
TypeScript is properly narrowed; also keep the existing
expect(...).toBeInstanceOf(EngineStateError) assertion but follow it with the
instanceof branch for accessing .code. For the invalid decision literal
currently written as "{ decision: 'maybe' } as never", replace the cast with a
compile-time suppression and a realistic shape: use a // `@ts-expect-error`
comment and pass an object including the required decidedBy field (e.g., {
decision: 'maybe', decidedBy: 't' }) so the test expresses an intentionally
invalid decision without unsafe casting. Ensure these changes are applied in the
tests that reference expectThrowsCode and the mentioned invalid decision case.

In `@packages/core/src/tools/bounding.ts`:
- Around line 180-193: The preview can exceed limits.maxLines because headLines
+ tailLines may equal limits.maxLines and you then insert an extra "\n…\n" line;
modify the allocation logic in the function that computes headLines/tailLines so
you reserve one line for the ellipsis when truncation is needed (e.g., compute
headLines = Math.max(1, Math.floor((limits.maxLines - 1) * 0.7)) and tailLines =
Math.max(1, limits.maxLines - 1 - headLines) or otherwise ensure headLines +
tailLines + 1 <= limits.maxLines), then build headSrc/tailSrc and return the
joined string as before (head, tail and the ellipsis) so the final preview never
exceeds limits.maxLines; adjust only the headLines/tailLines calculation and
leave the return construction using `${head}\n…\n${tail}` unchanged.

---

Outside diff comments:
In `@docs/architecture/execution-model.md`:
- Around line 141-143: The sentence describing the reconstructed checkpoint
contains a stray '+' before the symbol run_events; remove that plus so the prose
reads "...from `step_executions` `run_events` ( + `messages` for an
orchestrator's history)..." or better combine clearly as "...from
`step_executions`, `run_events` (and `messages` for an orchestrator's
history)..." so the `Checkpointer`/`run_events` phrase is normal prose rather
than a diff marker.

---

Nitpick comments:
In `@packages/core/src/engine/execution-host.test.ts`:
- Around line 74-95: Add a complementary test that verifies
listInterruptedRuns() returns incomplete runs: create an InMemoryRunStore, call
persistEvent(...) with a single run:started event (use the same runId and fields
as the existing test), then call await store.listInterruptedRuns() and assert
length is 1, interrupted[0].runId === 'r1' and interrupted[0].resumable ===
false; place this new it(...) alongside the existing tests in
execution-host.test.ts so it exercises InMemoryRunStore.persistEvent and
listInterruptedRuns behavior for non-terminal runs.

In `@packages/core/src/engine/execution-host.ts`:
- Around line 181-184: The code performs a redundant runtime/type check
"started.type !== 'run:started'" after using events.find(e => e.type ===
'run:started'); replace the find callback with a proper type predicate so
TypeScript narrows the result and you can drop the redundant check: define a
predicate like "function isRunStartedEvent(e: Event): e is RunStartedEvent {
return e.type === 'run:started'; }" (or use an inline arrow predicate "(e): e is
RunStartedEvent => e.type === 'run:started'") and call
events.find(isRunStartedEvent) to let "started" be typed as RunStartedEvent and
remove the extra started.type comparison. Ensure RunStartedEvent is the specific
union subtype used by your codebase.

In `@packages/core/src/tools/builtins.ts`:
- Around line 56-57: Replace the conditional ternary spreads that use negated
checks with positive-condition logical-AND spreads: instead of
...(spec.configOnlyParams !== undefined ? { configOnlyParams:
spec.configOnlyParams } : {} ) and ...(spec.policyTarget !== undefined ? {
policyTarget: spec.policyTarget } : {} ), use ...(spec.configOnlyParams && {
configOnlyParams: spec.configOnlyParams }) and ...(spec.policyTarget && {
policyTarget: spec.policyTarget }) so the object only spreads when the property
is present (refer to spec.configOnlyParams and spec.policyTarget in builtins).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 581ea4fd-bfe6-48cf-b6c3-8c4b3ef414ff

📥 Commits

Reviewing files that changed from the base of the PR and between 0c62f24 and cbfcef4.

📒 Files selected for processing (39)
  • CLAUDE.md
  • README.md
  • docs/architecture/execution-model.md
  • docs/architecture/shared-core-engine.md
  • docs/decisions/0036-run-loop-substrate-event-bus-and-execution-host.md
  • docs/decisions/0037-engine-tool-execution-boundary.md
  • docs/decisions/README.md
  • docs/reference/contracts/sse-event-schema.md
  • docs/reference/shared-core/README.md
  • docs/reference/shared-core/built-in-tools.md
  • docs/reference/shared-core/node-types.md
  • docs/reference/shared-core/run-plan.md
  • docs/reference/shared-core/tool-registry.md
  • docs/roadmap/current.md
  • docs/roadmap/phases/phase-1-engine-and-llm.md
  • docs/standards/error-handling.md
  • packages/core/src/engine/engine.test.ts
  • packages/core/src/engine/engine.ts
  • packages/core/src/engine/errors.test.ts
  • packages/core/src/engine/errors.ts
  • packages/core/src/engine/event-bus.test.ts
  • packages/core/src/engine/event-bus.ts
  • packages/core/src/engine/execution-host.test.ts
  • packages/core/src/engine/execution-host.ts
  • packages/core/src/engine/node-executor.ts
  • packages/core/src/engine/run-handle.test.ts
  • packages/core/src/engine/run-handle.ts
  • packages/core/src/index.ts
  • packages/core/src/tools/bounding.test.ts
  • packages/core/src/tools/bounding.ts
  • packages/core/src/tools/builtins.test.ts
  • packages/core/src/tools/builtins.ts
  • packages/core/src/tools/errors.ts
  • packages/core/src/tools/registry.test.ts
  • packages/core/src/tools/registry.ts
  • packages/core/src/tools/types.ts
  • packages/core/src/tools/untrusted.test.ts
  • packages/core/src/tools/untrusted.ts
  • packages/shared/src/run-event.ts

Comment thread docs/decisions/0037-engine-tool-execution-boundary.md
Comment thread docs/reference/contracts/sse-event-schema.md Outdated
Comment thread docs/reference/shared-core/run-plan.md Outdated
Comment on lines +53 to +61
/**
* Where this tool's guardrail target lives in the effective args, so the registry enforces the
* allowlist generically: a `spawnsProcess` tool returns the resolved `command` string the
* `allowedCommands` allowlist matches; an `http` egress tool returns the `url`. Omitted ⇒ no target
* (the generic allowlist check is skipped — e.g. a pre-approved `git_status` or an `os`/delegate tool).
* SECURITY: a `spawnsProcess` / `egress:'http'` tool that omits this has its allowlist check silently
* skipped (fail-open) — supply it, or pin the model-controlled args as config-only.
*/
readonly policyTarget?: (args: Args) => PolicyTarget; // { command?: string; url?: string }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Make allowlist targets impossible to omit for guarded tools.

As written, a missing policyTarget silently skips the allowlist check for spawnsProcess/http tools. That is a fail-open path: one forgotten field can turn a guarded tool into an unguarded one. Please split the tool shapes or add registry-time validation so allowlist-bearing tools cannot be registered without a target.

Based on the registry contract in the provided context, policy is enforced on the effective args, so omission should fail closed, not be skipped.

Comment thread packages/core/src/engine/engine.test.ts
Comment thread packages/core/src/tools/bounding.ts
cemililik and others added 2 commits June 13, 2026 20:32
Real bug (1.T):
- bounding: the head+tail preview could be maxLines+1 lines (head+tail == maxLines,
  then the inserted `…` line). Reserve one line for the ellipsis so it never exceeds.

Docs:
- sse-event-schema: resolve the contract-authority contradiction — the document is
  the authoritative contract, the @relavium/shared Zod schema is its runtime
  implementation (consistent with the "spec wins" note); the schema lives in shared
  and is consumed/re-exported by core.
- run-plan: a node failure is terminal only for that attempt (which, without 1.S,
  fails the run); node-level retry above the provider chain is layered by 1.S.
- execution-model: drop the stray leading `+` so the checkpoint-sources phrase reads
  as prose (step_executions, run_events, and messages).

1.N type-safety / readability:
- engine.test: narrow thrown errors with instanceof (no `as EngineStateError`); the
  invalid-decision case uses @ts-expect-error + a realistic shape, not `as never`.
- execution-host: a `run:started` type-predicate find drops the redundant re-check.
- engine: optional chain in #hasLiveEdge. Added an InMemoryRunStore.listInterruptedRuns
  positive test (started-but-unfinished → interrupted, resumable:false).

1.T:
- builtins: flip defineBuiltin's negated optional-field spreads to positive; hoist the
  test's captureFetch helper to module scope.

Skipped (with reason):
- #nodeEmit duplicate cases: each narrows `event` to a distinct union member so the
  spread keeps its fields; merging needs an unsafe cast (documented in-code).
- ADR-0037 MD028: no blockquote exists (the content is a contiguous bulleted list).
- types.ts `ToolId = string`: an exported semantic registry-key alias used across the
  module — intentional, removing it churns call sites for no gain.
- registry glob `star !== -1`: a sentinel check in the backtracking matcher; inverting
  the if/else-if chain risks a bug for no readability gain.
- bounding charCodeAt sites: code-unit reads for WTF-8 byte-width / lone-surrogate
  handling — codePointAt would change those semantics.

484 core tests; turbo lint/typecheck/test/build green; >=90% line+branch.

Refs: ADR-0036, ADR-0037

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…PR-17 final review)

The final multi-dimensional pre-merge review surfaced one real latent concurrency
bug and a set of missing regression tests on the critical paths.

Real bug (1.N) — gap-free / no-drop under an async store:
- #emitDurable assigned the gap-free sequenceNumber synchronously but delivered only
  after `await persistEvent`. With concurrent leaf nodes and an ASYNC store (1.R
  SQLite, cloud), persists can resolve out of order, so a higher-seq event (or the
  terminal) could deliver first, close the stream, and DROP the slower lower-seq
  event — violating the ADR-0036 gap-free/no-drop primary-stream contract.
  InMemoryRunStore resolves synchronously, which masks it today; the seam exists so
  an async store plugs in unchanged, and it would fire on the very next workstream.
  Fix: a per-run #deliveryTail chains each deliver in sequenceNumber order (next()
  assigns seq synchronously in call order), so a higher-seq event always waits for the
  lower-seq deliver. Persists stay concurrent; only delivery is serialized. Regression
  test: an async store that resolves the first parallel leaf's write slower than the
  second, asserting the delivered set stays gap-free.

Doc:
- sse-event-schema: session:turn_completed error shape now lists correlationId? to
  match eventErrorFields (the shared shape it governs) and the node:failed/run:failed rows.

Coverage (the two test/standards block verdicts):
- node:failed now asserts its stamped correlationId (the join field's primary carrier).
- assertGapFreeSeq added to every failure / store-fault delivery path (uncaught throw,
  classified failure, mid-run persist reject, every-write-rejects, terminal-only-write,
  gate-pause, cancel, async-ordering) — and now checks the SET, not delivery position.
- new tests: cancel-wins-a-racing-node-failure (-> run:cancelled); the settle-time
  backstop (#failNodeInternal, no node:failed); run:failed partialOutputs contents;
  InMemoryRunStore resumable:true for all RESUMABLE_LAST_TYPES; the no-sink throwing-
  subscriber out-of-band surfacing; a multi-hop dead-chain skip (anti-topological, to
  exercise the #propagateSkips fixpoint loop); the shared correlationId accept/reject
  schema cases; registry result outputSummary wiring; makeSummary cap + collapse.

1.T:
- bounding makePreview emits head-only when maxLines < 3 (the head\n…\ntail shape needs
  3 lines), so the preview never exceeds the line ceiling even at a tiny limit.

491 core + 238 shared tests; turbo lint/typecheck/test/build green; >=90% line+branch;
both purity gates clean; prettier clean.

Refs: ADR-0036, ADR-0037

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@cemililik

Copy link
Copy Markdown
Contributor Author

@coderabbitai review all

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

@cemililik Sure! I'll kick off another full review of all the changes in PR #17 right away.

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
docs/reference/contracts/sse-event-schema.md (1)

4-4: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep one contract authority.

Selected definitions reintroduces “source of truth” language for the schema after the top metadata already makes the document the canonical contract. That leaves the schema/doc relationship ambiguous again.

♻️ Proposed fix
- The enforced, runtime-validated source of truth is the Zod schema set in `@relavium/shared` (`run-event.ts`), from which the TS types are inferred.
+ The enforced runtime validator is the Zod schema set in `@relavium/shared` (`run-event.ts`), from which the TS types are inferred.

Also applies to: 86-90

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/reference/contracts/sse-event-schema.md` at line 4, The document
currently contradicts itself by first declaring the doc as the canonical
contract and later reintroducing the schema (RunEvent in run-event.ts under
`@relavium/shared`, re-exported by `@relavium/core`) as a “source of truth”; remove
or reword the later sentence in the "Selected definitions" section that claims
the schema is the source of truth so the document remains the sole authority,
and instead state that the RunEvent Zod schema is the runtime-validated
implementation of the document (and will be corrected to match the doc when they
diverge).
docs/reference/shared-core/tool-registry.md (1)

53-61: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Make allowlist-bearing tools fail closed.

policyTarget is still optional here, so a guarded spawnsProcess / http tool can skip the allowlist check entirely. That keeps the fail-open path the earlier review already called out. Please require a target at registration time or split the guarded shapes from the unguarded ones.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/reference/shared-core/tool-registry.md` around lines 53 - 61, The
current optional policyTarget allows guarded tools to skip allowlist checks; fix
by making policyTarget required for any tool shape that can carry an allowlist
(e.g., tools with kind 'spawnsProcess' or egress:'http'): either (a) split the
tool types into UnguardedTool and GuardedTool interfaces and make
GuardedTool.policyTarget mandatory, updating registrations to use the
appropriate interface, or (b) keep the single tool type but add a
registration-time validation in the ToolRegistry (or the function that registers
tools) that throws/error when a tool with kind 'spawnsProcess' or egress:'http'
is registered without a policyTarget; reference policyTarget, spawnsProcess,
egress:'http', and the tool registration path (ToolRegistry/registerTool or
equivalent) so callers and type declarations are updated accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/reference/shared-core/built-in-tools.md`:
- Around line 37-38: The documentation currently references the stale phase
identifier "1.AE" in the sentence about return shapes for the egress tools;
update that sentence (the paragraph beginning "**Return shapes.** ...
`http_request` / `web_search` / `mcp_call` / `git_status` dispatch the **raw
host response**") to remove or replace the phase number: either drop "1.AE"
entirely and say "until a provider-specific normalizer lands" or replace "1.AE"
with the correct normalization milestone name (or a neutral phrase like "the
normalization milestone") so the reference to the egress tools' normalization
target is accurate.

In `@docs/reference/shared-core/tool-registry.md`:
- Around line 35-37: Remove the stray blank line inside the blockquote that
currently separates the two quoted paragraphs—specifically delete the empty line
between the paragraph starting "(1.E) lowers to each provider's wire shape; a
model can supply only these..." and the following paragraph that begins "Typed
`JsonSchema` (= `Readonly<Record<string, unknown>>`,..." so the two lines are
contiguous in the same blockquote (no blank line).

---

Duplicate comments:
In `@docs/reference/contracts/sse-event-schema.md`:
- Line 4: The document currently contradicts itself by first declaring the doc
as the canonical contract and later reintroducing the schema (RunEvent in
run-event.ts under `@relavium/shared`, re-exported by `@relavium/core`) as a “source
of truth”; remove or reword the later sentence in the "Selected definitions"
section that claims the schema is the source of truth so the document remains
the sole authority, and instead state that the RunEvent Zod schema is the
runtime-validated implementation of the document (and will be corrected to match
the doc when they diverge).

In `@docs/reference/shared-core/tool-registry.md`:
- Around line 53-61: The current optional policyTarget allows guarded tools to
skip allowlist checks; fix by making policyTarget required for any tool shape
that can carry an allowlist (e.g., tools with kind 'spawnsProcess' or
egress:'http'): either (a) split the tool types into UnguardedTool and
GuardedTool interfaces and make GuardedTool.policyTarget mandatory, updating
registrations to use the appropriate interface, or (b) keep the single tool type
but add a registration-time validation in the ToolRegistry (or the function that
registers tools) that throws/error when a tool with kind 'spawnsProcess' or
egress:'http' is registered without a policyTarget; reference policyTarget,
spawnsProcess, egress:'http', and the tool registration path
(ToolRegistry/registerTool or equivalent) so callers and type declarations are
updated accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 290d2bca-7a94-469d-ae2a-6809a9610539

📥 Commits

Reviewing files that changed from the base of the PR and between 0c62f24 and 6ec6eb3.

📒 Files selected for processing (40)
  • CLAUDE.md
  • README.md
  • docs/architecture/execution-model.md
  • docs/architecture/shared-core-engine.md
  • docs/decisions/0036-run-loop-substrate-event-bus-and-execution-host.md
  • docs/decisions/0037-engine-tool-execution-boundary.md
  • docs/decisions/README.md
  • docs/reference/contracts/sse-event-schema.md
  • docs/reference/shared-core/README.md
  • docs/reference/shared-core/built-in-tools.md
  • docs/reference/shared-core/node-types.md
  • docs/reference/shared-core/run-plan.md
  • docs/reference/shared-core/tool-registry.md
  • docs/roadmap/current.md
  • docs/roadmap/phases/phase-1-engine-and-llm.md
  • docs/standards/error-handling.md
  • packages/core/src/engine/engine.test.ts
  • packages/core/src/engine/engine.ts
  • packages/core/src/engine/errors.test.ts
  • packages/core/src/engine/errors.ts
  • packages/core/src/engine/event-bus.test.ts
  • packages/core/src/engine/event-bus.ts
  • packages/core/src/engine/execution-host.test.ts
  • packages/core/src/engine/execution-host.ts
  • packages/core/src/engine/node-executor.ts
  • packages/core/src/engine/run-handle.test.ts
  • packages/core/src/engine/run-handle.ts
  • packages/core/src/index.ts
  • packages/core/src/tools/bounding.test.ts
  • packages/core/src/tools/bounding.ts
  • packages/core/src/tools/builtins.test.ts
  • packages/core/src/tools/builtins.ts
  • packages/core/src/tools/errors.ts
  • packages/core/src/tools/registry.test.ts
  • packages/core/src/tools/registry.ts
  • packages/core/src/tools/types.ts
  • packages/core/src/tools/untrusted.test.ts
  • packages/core/src/tools/untrusted.ts
  • packages/shared/src/run-event.test.ts
  • packages/shared/src/run-event.ts

Comment thread docs/reference/shared-core/built-in-tools.md Outdated
Comment on lines +35 to +37
* (1.E) lowers to each provider's wire shape; a model can supply only these. Derived from the
* LLM-visible subset of the args schema (an in-house emitter or a hand-maintained projection),
* excluding every `configOnlyParams` entry. Typed `JsonSchema` (= `Readonly<Record<string, unknown>>`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove the blank line inside this blockquote.

markdownlint MD028 flags the empty line between the two quoted paragraphs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/reference/shared-core/tool-registry.md` around lines 35 - 37, Remove the
stray blank line inside the blockquote that currently separates the two quoted
paragraphs—specifically delete the empty line between the paragraph starting
"(1.E) lowers to each provider's wire shape; a model can supply only these..."
and the following paragraph that begins "Typed `JsonSchema` (=
`Readonly<Record<string, unknown>>`,..." so the two lines are contiguous in the
same blockquote (no blank line).

Source: Linters/SAST tools

…y wording (PR-17 review)

Three valid findings from the latest review batch; two skipped.

- built-in-tools "Return shapes": drop the "(the egress tools at 1.AE)" parenthetical.
  1.AE is the multimodal media-INPUT adapter milestone (the OpenAI textOf flatten fix in
  @relavium/llm), not the egress tools' output normalizer — a mis-attribution, not just a
  stale number. Replaced with the neutral "until a provider-specific normalizer lands".
- registry.ts: same wrong "1.AE" ref on the host-egress SSRF range-block comment, removed
  for the same reason (comment-only; both typecheck configs + lint stay clean).
- sse-event-schema "Selected definitions": the note still called the Zod schema the
  "source of truth", contradicting the L4 framing that the DOC is authoritative and the
  schema its runtime-validated implementation. Reworded to "implementation" so the two
  agree (doc wins on divergence).

Skipped (verified not still-valid):
- tool-registry "blank line in a blockquote": that region is a continuous JSDoc comment
  inside a ```ts block (not a blockquote) with no blank line between the sentences.
- "make policyTarget mandatory for spawnsProcess/egress:http tools": git_status and
  git_commit are spawnsProcess:true but deliberately omit policyTarget (subcommand-
  constrained + config-pinned flags / human-gate-protected). A blanket registration guard
  would break both, and the registry cannot distinguish a safe omission from an unsafe one
  without the very policyTarget that's absent — the optional + SECURITY-note contract is
  the deliberate ADR-0037 design.

Refs: ADR-0037

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@cemililik
cemililik merged commit 5700690 into main Jun 13, 2026
5 of 6 checks passed
@sonarqubecloud

Copy link
Copy Markdown

cemililik added a commit that referenced this pull request Jun 14, 2026
PR #17 (1.N WorkflowEngine + RunEventBus, 1.T built-in ToolRegistry) merged to main
(2026-06-13). Reflect completion across the status surfaces:

- phase-1: 1.N and 1.T section headers + dependency-matrix rows marked ✅ Done (PR #17);
  the 1.m3 milestone row marked ✅ (all components — 1.L.0/1.L/1.L2/1.M/1.N — landed);
  intro blockquote updated (engine lane now converges at the 1.O AgentRunner join, fully
  unblocked now that 1.K, 1.N, 1.T are all done).
- current.md: immediate-next-steps + the review-pass follow-up note updated — 1.N/1.T done,
  1.m3 reached, 1.O is the next workstream.
- CLAUDE.md / README.md: status lines advanced from "1.N is next" to "1.N + 1.T landed,
  1.m3 complete; 1.O next".

1.m4 (1.O/1.P/1.Q/1.R/1.S/1.AC) stays open — 1.T and 1.AB are its only landed components.
Docs-only; prettier clean.

Refs: ADR-0036, ADR-0037

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant