Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
625142b
feat(sdk): replace broker transport with node spawn
Nov 8, 2026
dad61da
docs: record approved node-spawn follow-ups
Nov 8, 2026
70ba476
fix(sdk): require Desktop service caller secret
Nov 8, 2026
8c2e44a
fix(sdk): harden node-spawn recovery
Nov 8, 2026
2aa4b8c
fix(sdk): keep node cleanup recoverable
khaliqgant Nov 8, 2026
cefb928
fix(sdk): preserve node-spawn cleanup recovery
Nov 8, 2026
1716b12
merge: integrate concurrent node-spawn hardening
Nov 8, 2026
32e71b2
fix(sdk): close node-spawn recovery gaps
khaliqgant Nov 8, 2026
7465cf5
fix(sdk): retain cleanup identity across roster outage
khaliqgant Nov 8, 2026
837a931
fix(sdk): close node-spawn review gaps
khaliqgant Nov 8, 2026
70dc70c
test(sdk): pin publisher discovery timeout
khaliqgant Nov 8, 2026
bdb05fe
fix(sdk): close node-spawn inbox transport
khaliqgant Nov 8, 2026
e48df15
fix(sdk): preserve node results when teardown fails
khaliqgant Nov 8, 2026
eeb765a
fix(sdk): share relay publisher runtime across tasks
khaliqgant Nov 8, 2026
fb6d2f9
test(sdk): typecheck shared runtime regression
khaliqgant Nov 8, 2026
4bbd567
fix(sdk): preserve results across iterator teardown
khaliqgant Nov 8, 2026
87a1c99
test(sdk): prove node spawn through live journal
khaliqgant Nov 8, 2026
f7be47d
test(sdk): type exact cleanup identity
khaliqgant Nov 8, 2026
e19dc92
test(sdk): pin node runtime teardown order
khaliqgant Nov 8, 2026
caa33dc
fix(sdk): contain synchronous inbox teardown errors
khaliqgant Nov 8, 2026
03850e3
fix(sdk): share concurrent worker shutdown
Nov 8, 2026
2304c93
test(sdk): stress shared node runtime drain
khaliqgant Nov 8, 2026
29fe24b
fix(sdk): share node runtime owner shutdown
khaliqgant Nov 8, 2026
b6ad369
fix(sdk): harden relay node task recovery
khaliqgant Nov 9, 2026
74537c9
fix(sdk): reconcile relay settlements after dispatch drain
khaliqgant Nov 9, 2026
de6dbe8
fix(sdk): gate dispatches behind settlement recovery
khaliqgant Nov 9, 2026
e07465c
fix(sdk): gate settlement retries by dispatch generation
khaliqgant Nov 9, 2026
fe7d2e0
fix(sdk): scope node results by spawn invocation
khaliqgant Nov 9, 2026
aacb6a1
feat(sdk): bind local node spawn to Desktop callers
khaliqgant Nov 9, 2026
6a610b1
fix(sdk): close Desktop node-spawn lifecycle gaps
khaliqgant Nov 9, 2026
e2e0b42
fix(sdk): bound Desktop result teardown
khaliqgant Nov 9, 2026
83033c9
fix(sdk): preserve live Desktop teardown state
khaliqgant Nov 9, 2026
849db39
fix(sdk): retain live inbox teardown barriers
khaliqgant Nov 9, 2026
64a89ec
fix(sdk): serialize live result settlement
khaliqgant Nov 9, 2026
611f8e1
fix(sdk): serialize node result observation
khaliqgant Nov 9, 2026
f329e45
fix(sdk): cancel queued claim execution
khaliqgant Nov 9, 2026
2df85dc
fix(sdk): serialize durable result claims
khaliqgant Nov 9, 2026
cdef5e6
test(sdk): prove canonical claim aliases
khaliqgant Nov 9, 2026
18e8116
fix(sdk): validate Desktop caller before admission
khaliqgant Nov 9, 2026
ec97042
fix(sdk): preserve exact caller capability paths
khaliqgant Nov 9, 2026
8c49413
fix(sdk): preserve relay deadlines and settlement retries
khaliqgant Nov 9, 2026
051b3ef
test(sdk): drain node spawn acknowledgement
khaliqgant Nov 9, 2026
03691e9
test(sdk): isolate live lifecycle fixtures
khaliqgant Nov 9, 2026
708e994
fix(sdk): pin the Desktop bridge type contract
khaliqgant Nov 9, 2026
6bbd670
fix(sdk): preserve relay harness model defaults
khaliqgant Nov 9, 2026
6ed9eed
fix(sdk): keep relay harness model defaults remote
khaliqgant Nov 9, 2026
fb4000e
fix(sdk): harden node-spawn recovery ownership
khaliqgant Nov 9, 2026
c142c0c
fix(sdk): preserve result schema claim identity
khaliqgant Nov 9, 2026
eb7caef
Merge main into codex/relayflows-node-spawn
khaliqgant Nov 9, 2026
5dfda36
fix(sdk): retain Relay 18 node-spawn compatibility
khaliqgant Nov 9, 2026
17fe0dd
fix(sdk): isolate lease snapshot cancellation
khaliqgant Nov 9, 2026
7897907
fix(sdk): preserve node spawn recovery state
khaliqgant Nov 9, 2026
d8c1a58
fix(sdk): require complete core bridge surface
khaliqgant Nov 10, 2026
ba0c633
test(sdk): typecheck core bridge contract
khaliqgant Nov 10, 2026
d34f6a7
fix(sdk): fence durable node spawn dispatch
khaliqgant Nov 10, 2026
5c7bde0
fix(sdk): reject malformed node result status
khaliqgant Nov 10, 2026
4f74d69
fix(sdk): reconcile late node cleanup
khaliqgant Nov 10, 2026
5818230
fix(sdk): preserve timely node results
khaliqgant Nov 10, 2026
801f37d
fix(sdk): keep foreign claims from creating authority
khaliqgant Nov 10, 2026
333d510
fix(sdk): fence custom ports before dispatch
khaliqgant Nov 10, 2026
6de14c2
docs(sdk): clarify custom port fence semantics
khaliqgant Nov 10, 2026
3df4bf4
refactor(sdk): narrow retryable port preflight
khaliqgant Nov 10, 2026
f7af0e8
fix(sdk): fingerprint retryable preflight methods
khaliqgant Nov 10, 2026
b5084e1
fix(sdk): avoid pre-fence port accessors
khaliqgant Nov 10, 2026
af36c14
test: pin public core bridge contract
Nov 11, 2026
d1799fb
fix(sdk): replay strict local spawn after lost receipt
khaliqgant Nov 11, 2026
e915701
test(sdk): add spawn response-loss proof kit
Nov 11, 2026
8ac91bb
test(sdk): allow coalesced spawn replay racers
Nov 11, 2026
4a8307c
test(sdk): cover proof runner defaults and redaction
Nov 11, 2026
0b68f37
test(sdk): prove cross-process replay claim retention
Nov 11, 2026
de51644
test(sdk): audit local spawn response loss
Nov 11, 2026
5a5de58
fix(sdk): bind local replay to authentic bridge
Nov 11, 2026
f136c1a
fix(sdk): gate local preflight on official bridge
Nov 11, 2026
64ccf2b
test(sdk): bind response-loss proof to consumer
Nov 11, 2026
e2dcdd0
fix(sdk): harden official local replay capability
Nov 11, 2026
feeba7e
fix(sdk): replay durable local spawn request
khaliqgant Nov 11, 2026
2dc9366
test(sdk): make late cleanup timeout deterministic
khaliqgant Nov 11, 2026
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
193 changes: 135 additions & 58 deletions docs/AGENT-RELAY-TRANSPORT.md
Original file line number Diff line number Diff line change
@@ -1,58 +1,135 @@
# Relay agent task transport

An agent step with `transport: 'relay'` waits for a durable final task receipt.
Spawn readiness and invocation acceptance do not complete the Flows step.

This adapter requires the task contract in AgentWorkforce/relaycast#436
(`c80202f2dfe7a15a6cef5b81f2452b1d7acd6add`) and its provider in
AgentWorkforce/relay#1772 (`6a06fd176df94ca1054d5b7965bd777b7d881eb6`;
subsequent proof-only commit `a8577736dd32c0bcf51a37f88ac0b04fb056da3f`).
These changes need to be admitted and deployed in that order before a live
Cloud proof can validate this adapter. The independent schema-validation fix in AgentWorkforce/flows#398 is also an
explicit validate dependency; its patch is not included here. Local wire fixtures do not establish
that the deployed service supports the contract.

Configure a pre-provisioned `RELAY_AGENT_TOKEN` for the caller in the provider's
workspace and, if needed, `RELAY_BASE_URL` (default `https://cast.agentrelay.com`).
A workspace API key cannot substitute for the agent token. HTTPS is required
except for literal loopback IP addresses used in local development. The Relay provider
must explicitly enable `AGENT_RELAY_TASK_PROVIDER=1` with persistent state.
The worker uses its injected `agent_result` tool to submit the final output or
failure and waits for the provider's durable acknowledgment before exiting.

Flows resolves the caller with `GET /v1/agent` and invokes
`POST /v1/actions/task.run/invoke`. The journal idempotency key identifies the
invocation; the input carries run, step, dispatch identity, model, working
directory, result schema, wake context, and a 24-hour task deadline. The adapter
uses the pinned engine's `action-invoke-v1` deterministic invocation ID so it
can reconcile a lost POST acknowledgment without another POST.

Before dispatch, Flows exclusively creates and syncs a private claim under its
durable data directory's `relay-tasks/`. It binds the canonical engine origin,
workspace, caller, invocation, journal identity, and full input. The data
directory must survive runner restarts; it includes the task prompt and needs
the same protection as the journal. Tokens are never stored in the claim.
A corrupt claim or changed caller, endpoint, or input fails closed. Token
rotation for the same caller remains possible.

Only the original claim creator sends a POST. Resumed or ambiguous dispatches
use `GET /v1/actions/task.run/invocations/:id` exclusively. If the task cannot be
confirmed, Flows reports an explicit failure; it never creates another task
under a replacement principal. A terminal failure is immutable for that
journal identity, including on a retry of the same dispatch. Starting a new
logical task requires a new journal identity.

While polling, Flows renews the existing worker lease. Lease loss aborts polling
and prevents a stale journal completion. Execution identity must remain stable across every observed receipt. Generation
and acceptance time may appear together once and must then remain stable; final receipts must match the caller, invocation,
input, run, step, and dispatch. A completed receipt supplies the exact JSON
output, including arrays, scalars, and null. A failed receipt supplies an
explicit failure reason. Receipt correlation is retained in the step's
trajectory metadata. Known credential values are redacted from error details.

Optional accounting counters `tokens_input` and `tokens_output` feed existing
model pricing. A priced task without the required usage still fails the
existing usage check. Cancellation of local polling does not invent a remote
cancellation API; the durable provider and engine task deadline govern the
remote execution. Historical terminal receipts can still be recovered after
the deadline.
# Agent Relay node-spawn transport

An agent step with `transport: 'relay'` runs through Agent Relay's native node
spawn control. Flows no longer invokes the Relaycast `task.run` action and does
not start, connect to, or embed the retired Relay broker.

## Runtime contract

The worker needs a dedicated publisher `RELAY_AGENT_TOKEN`; a workspace key
cannot replace it. `RELAY_BASE_URL` defaults to
`https://cast.agentrelay.com`. Optional placement constraints are
`RELAYFLOW_NODE_NAME`, `RELAYFLOW_NODE_REPO`, and
`RELAYFLOW_NODE_SANDBOX_ONLY=1`.

The remote path uses `@agent-relay/sdk >=17.2.1 <19`:

1. `placement.spawn` selects a live node advertising `spawn:<harness>`.
2. Flows requests `confirm: true` and `verifyReady: true` and refuses an
unconfirmed task, identity, node, launch, or readiness result.
3. The spawned agent reports one versioned result envelope to the publisher's
durable inbox. Flows verifies the immutable sender id and name plus run,
step, and dispatch correlation.
4. Flows invokes the node's `release` capability on the same node with the
exact agent id, then records spawn, delivery, and release evidence.
5. Only after `stepComplete` is durably accepted by the journal does Flows ack
the inbox delivery. Relay is transport; the journal is execution authority.

Before the journal append, Flows records a settlement intent containing the
kernel attempt and expected completion reason. The journal's terminal fact
must contain that reason and the exact Relay delivery and message ids before a
restart can acknowledge the inbox item. Inbox acknowledgement and rejection
have independent deadlines; an unconfirmed operation remains in the durable
outbox. It is retried on worker attach only when admission already established
the exact matching publisher authority, then after dispatch drain and on
shutdown while that authority remains established. A durable claim never
creates authority or triggers credential discovery by itself.

One worker uses one lazy publisher connection across all of its concurrent
relay steps. Each step closes only its inbox iterator. The default runtime is
disconnected after worker shutdown drains every step; cancellation teardown is
best-effort. Injected runtimes remain caller-owned: their caller must
disconnect them after every worker sharing that runtime has drained.

Spawn readiness is not task completion. A result without the exact sender and
attempt correlation cannot complete a step.

## Replay and cancellation

Before dispatch, Flows creates and fsyncs a private claim under
`<data-dir>/node-spawns/`. It binds publisher identity, Relay origin, journal
identity, stable worker name, and full spawn input. Tokens are never stored.

The current fleet placement SDK has no caller-provided idempotency key for the
placement invocation itself. Flows forwards the durable key to the selected
node in the spawn action input. A version 3 claim uses a permanent sibling
dispatch fence: Flows writes and fsyncs a complete private marker, atomically
hard-links it into place, and fsyncs the directory immediately before local
`spawn.invoke` or remote `placement.spawn`. Competing workers may repeat
preflight, but only the fence winner may enter that irreversible call. A v3
claim with no fence proves dispatch was not entered and may retry preflight.
The exact built-in local Desktop port is the sole fenced-replay exception:
Core Bridge 18.1 or newer durably deduplicates `spawn.invoke` by the forwarded
idempotency key, so a restart may repeat that identical request and recover the
original invocation after its reply was lost. Flows admits that replay only for
the SDK runtime built from the exact module returned by `loadCoreBridge()`, with
the unchanged local port prototype and lifecycle methods, and re-verifies the
existing fence before entry. The durable claim retains the original rendered
task and exact local node name/id. Replay uses those stored bytes; a changed
node identity fails closed, while mutable current directory-scope or live-target
preflight is not reapplied. A new unfenced dispatch still applies all current
caller scope and target checks. A missing receipt from remote placement, an
injected bridge, a custom or overridden port, or a legacy version 2 claim
remains ambiguous and fails closed without another dispatch. Malformed,
mismatched, or unreadable fence state also fails closed. A recorded invocation
is reconciled with `commands.getInvocation`. The stable worker name provides
additional collision protection at the node.

Custom `NodeSpawnPort` implementations must declare `dispatchFenceVersion: 1`
and accept the supplied `beforeDispatch` callback. Because Flows cannot audit
arbitrary port code, it publishes and fsyncs the fence before entering a custom
`spawn`; an error from that call is permanently ambiguous and cannot be retried.
The callback remains part of the protocol and is idempotent after publication.
Only the SDK runtime constructed from its internally loaded Core Bridge module
and exact built-in local lifecycle method/state set may use
`NodeSpawnPreDispatchError` to retain retryable target preflight. Replacing or
overriding a relevant method removes the narrow trust and fences it before entry.
Existing one-argument `spawn` implementations are not compatible
even though JavaScript can ignore extra arguments: the required protocol field
makes them a type error, and the runtime refuses a missing or unknown version
before creating a durable claim.

The claim also fixes the task's original start time. Resume never grants a new
execution deadline. Once a correlated result was durably observed before that
deadline, the validated envelope is part of the claim and can be replayed to
the journal after any restart delay. Recovery cannot use that fact to spawn or
extend remote execution.

Lease loss or timeout aborts the inbox wait and attempts exact release. A
post-spawn verification failure carries any exact identity already proven into
the same cleanup path. Spawn and release action inputs carry their stable
idempotency keys. Every release attempt has an independent deadline. A failed
or unconfirmed cleanup is recorded as pending in the durable claim,
the attempt remains incomplete for kernel redispatch, and the result remains
unacked. Kernel redelivery therefore reopens the same claim and retries exact
release before it can accept a result or issue another spawn. A result delivery
stays unacked until its terminal journal append succeeds, so a worker crash
cannot turn Relay acknowledgement into fabricated execution truth.

## Local Desktop bridge

The SDK contains a local `NodeSpawnPort` adapter for Desktop's schema-v2
service-caller bridge. The caller is an explicit private `caller.json` path
supplied with `--service-caller-file`, the SDK `serviceCallerFile` option, or
`RELAYFLOW_SERVICE_CALLER_FILE`; Flows never scans Desktop's app-data
directory. Optional `@agent-relay/core-bridge` `>=18.1.0 <19` validates the
handoff and local socket or named pipe, challenge-authenticates a fresh
connection, verifies the bound publisher/workspace/node identity, and then
calls `spawn.targets`, `spawn.invoke`, `spawn.status`, and `spawn.release` without sending a caller
name or secret. The same authenticated bridge provides the invocation-scoped
result inbox and its ack/fail/close/disconnect lifecycle.

An explicit caller handoff is bound before the local worker registers with the
journal, so a missing, expired, malformed, revoked or identity-mismatched file
cannot admit a run and fail only at its first Relay step. The bridge package
owns platform-specific socket and Windows named-pipe trust checks; a bridge
version hello by itself is not authentication.
The current bridge does not accept model, repository, or sandbox placement
overrides, so the local adapter refuses those fields instead of dropping them.

The CLI selects this adapter only when the caller file is explicit and
absolute. It is mutually exclusive with `RELAY_AGENT_TOKEN`; Flows does not
borrow another session's name and does not invent or provision authority.
Desktop creates, rotates and revokes the private handoff.

See [the migration note](MIGRATION-NODE-SPAWN.md) for removed broker surfaces.
98 changes: 98 additions & 0 deletions docs/MIGRATION-NODE-SPAWN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Migrating from Relay broker execution to node spawn

This release removes the frozen `@agent-relay/harness-driver` dependency and
all broker/PTy launch paths from `@relayflows/sdk`.

## One-shot remote agents

Keep `transport: 'relay'`, install `@agent-relay/sdk` in the supported range
`>=17.2.1 <19`, and provide a dedicated publisher `RELAY_AGENT_TOKEN`. The
transport now uses fleet node placement and the durable result inbox instead
of Relaycast `task.run`.

Custom executable paths and arbitrary injected environment variables do not
carry over. The selected node must advertise `spawn:<harness>` for the declared
harness. Use `RELAYFLOW_NODE_NAME` for an exact node or
`RELAYFLOW_NODE_REPO` for repository placement.

## Removed surfaces

- `transport: 'broker'`, `keepAlive`, `f.message`, and `f.release` for retained
sessions fail before child-run admission.
- YAML with non-empty `communication.links` fails preflight. An empty list
declares no linked session and remains a no-op.
- `examples/issue-swarm` is retained only as a labelled migration reference;
it is not runnable on this release.
- `RELAYFLOW_RELAY_BROKER_BIN`, broker connection files, helper sockets, and
broker health checks are no longer read.

There is deliberately no compatibility fallback to broker 17.2.1. A local
Desktop run uses a separately approved schema-v2 service caller. Pass its
private file explicitly with
`--local-agent --service-caller-file /absolute/caller.json`,
the SDK `serviceCallerFile` option, or `RELAYFLOW_SERVICE_CALLER_FILE`. Flows
never scans Desktop's app-data directory. It securely parses and
challenge-authenticates the handoff through `@agent-relay/core-bridge` 18.1 or
newer. Install a compatible version
(`npm install @agent-relay/core-bridge@^18.1.0`) when using Desktop mode; the
peer is optional only when Desktop mode is not used.
Flows then sends no caller name or secret
on spawn or inbox calls. The handoff's harness and directory scope is enforced
before dispatch. A local relay step with no authored `cwd` spawns in that run's
root; remote relay placement still receives no local directory.

`RELAYFLOW_SERVICE_CALLER_FILE` and `RELAY_AGENT_TOKEN` are mutually exclusive:
the former is local Desktop authority and the latter is remote fleet authority.
Setting both fails closed. A caller file whose publisher or workspace differs
from the authenticated core identity is also refused; rotate or reprovision it
in Desktop rather than editing the file.

## Custom `NodeSpawnPort` implementations

Ports must declare `dispatchFenceVersion: 1` and accept the supplied
`beforeDispatch` callback. Flows cannot prove that arbitrary port preflight is
side-effect-free, so it durably fences a custom port before entering `spawn`;
any error from that call is permanently ambiguous and is not retried. Calling
`beforeDispatch` remains required for forward compatibility and is idempotent
after that fence. `NodeSpawnPreDispatchError` preserves retry only for the SDK
runtime built from its internally loaded Core Bridge module and exact local
method/state identity, whose target preflight is audited;
it does not reopen any other port's fenced claim. Existing one-argument `spawn`
methods must be migrated even though JavaScript permits them to ignore the
callback. The required version field makes an unchanged port a type error, and
runtime use fails before claim creation. Overriding the built-in local port's
`spawn` or `targets` method also removes that narrow trust and fences it before
entry.

The exact built-in local Desktop port has one additional recovery contract.
Core Bridge 18.1 or newer persists `spawn.invoke` idempotency keys, so after a
durable fence and a lost reply Flows may repeat the identical local request and
recover the original invocation even when mutable current scope or target
preflight would no longer admit a new dispatch. That exemption is registered
only when the SDK runtime loaded the exact Core Bridge module and the local
prototype and lifecycle method identities remain unchanged. Injected bridges,
wrappers, subclasses, overrides, accessors, custom ports, remote placement and
legacy version 2 claims remain fail-closed. New unfenced local dispatches still
enforce current caller scope and target eligibility. Fenced replay uses the
durable original rendered task and exact local node name/id; a changed node
identity is refused before replay.

## Hosted and self-hosted execution

Hosted workers use the agent-token fleet path. Self-hosted workers use the
same path for remote nodes. The local Desktop mode requires a Desktop release
whose `hello.methods` contains the complete service-caller challenge, identity,
spawn, invocation-scoped inbox, settlement, close and disconnect contract.
Each result subscription uses its own authenticated connection. Flows closes
that iterator and connection before settling the item through another fresh
authenticated connection. Older Desktop builds fail closed before dispatch.

Self-hosted Flows licensing is owned by relaycontrol's signed offline licence
design. This SDK does not invent an entitlement store; deployments must fail
closed until a signed `flows` entitlement attestation is available. Scheduled
self-hosted submissions remain Kubernetes CronJobs, not `relaycron`.

Relay CLI migration remains a separate, approved follow-up: `relay flows`
already consumes the v2 SDK, while legacy `node workflow run` and the cloud
worker move together in their own stacked draft after the broker-retirement
change. This package is not published as part of either draft.
Loading
Loading