@intx/workflow-host 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +176 -0
- package/README.md +287 -0
- package/dist/adapters/blob-substrate.d.ts +49 -0
- package/dist/adapters/blob-substrate.js +140 -0
- package/dist/adapters/repo-store.d.ts +39 -0
- package/dist/adapters/repo-store.js +344 -0
- package/dist/adapters/spawn-child.d.ts +74 -0
- package/dist/adapters/spawn-child.js +152 -0
- package/dist/adapters/step-invoker.d.ts +114 -0
- package/dist/adapters/step-invoker.js +360 -0
- package/dist/child/env-bootstrap.d.ts +56 -0
- package/dist/child/env-bootstrap.js +120 -0
- package/dist/child/from-process-env.d.ts +127 -0
- package/dist/child/from-process-env.js +183 -0
- package/dist/child/index.d.ts +9 -0
- package/dist/child/index.js +9 -0
- package/dist/child/outbound-mail-bridge.d.ts +36 -0
- package/dist/child/outbound-mail-bridge.js +143 -0
- package/dist/child/proxy-repo-store.d.ts +27 -0
- package/dist/child/proxy-repo-store.js +200 -0
- package/dist/child/run-child.d.ts +320 -0
- package/dist/child/run-child.js +900 -0
- package/dist/child/self-discovery.d.ts +29 -0
- package/dist/child/self-discovery.js +57 -0
- package/dist/child/substrate-write-bridge.d.ts +72 -0
- package/dist/child/substrate-write-bridge.js +188 -0
- package/dist/child/supervisor-backed-transport.d.ts +10 -0
- package/dist/child/supervisor-backed-transport.js +113 -0
- package/dist/child/warm-agent-cache.d.ts +78 -0
- package/dist/child/warm-agent-cache.js +112 -0
- package/dist/drain-controller.d.ts +37 -0
- package/dist/drain-controller.js +46 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/ipc/control-channel.d.ts +336 -0
- package/dist/ipc/control-channel.js +532 -0
- package/dist/ipc/crypto.d.ts +46 -0
- package/dist/ipc/crypto.js +126 -0
- package/dist/ipc/envelope.d.ts +53 -0
- package/dist/ipc/envelope.js +88 -0
- package/dist/ipc/event-channel.d.ts +677 -0
- package/dist/ipc/event-channel.js +278 -0
- package/dist/ipc/index.d.ts +4 -0
- package/dist/ipc/index.js +143 -0
- package/dist/mail-bus/hub-transport-adapter.d.ts +30 -0
- package/dist/mail-bus/hub-transport-adapter.js +76 -0
- package/dist/mail-bus/index.d.ts +1 -0
- package/dist/mail-bus/index.js +1 -0
- package/dist/seams/index.d.ts +3 -0
- package/dist/seams/index.js +3 -0
- package/dist/seams/scheduler-adapter.d.ts +3 -0
- package/dist/seams/scheduler-adapter.js +24 -0
- package/dist/seams/scheduler.d.ts +94 -0
- package/dist/seams/scheduler.js +397 -0
- package/dist/seams/signal-channel.d.ts +74 -0
- package/dist/seams/signal-channel.js +304 -0
- package/dist/supervisor/cancel-signing.d.ts +68 -0
- package/dist/supervisor/cancel-signing.js +144 -0
- package/dist/supervisor/child-termination.d.ts +51 -0
- package/dist/supervisor/child-termination.js +76 -0
- package/dist/supervisor/credentials.d.ts +101 -0
- package/dist/supervisor/credentials.js +153 -0
- package/dist/supervisor/dispatch-attribution.d.ts +37 -0
- package/dist/supervisor/dispatch-attribution.js +114 -0
- package/dist/supervisor/drain-timeout.d.ts +127 -0
- package/dist/supervisor/drain-timeout.js +231 -0
- package/dist/supervisor/index.d.ts +7 -0
- package/dist/supervisor/index.js +6 -0
- package/dist/supervisor/recycle.d.ts +212 -0
- package/dist/supervisor/recycle.js +440 -0
- package/dist/supervisor/run-event-compaction.d.ts +34 -0
- package/dist/supervisor/run-event-compaction.js +115 -0
- package/dist/supervisor/spawn-env.d.ts +39 -0
- package/dist/supervisor/spawn-env.js +36 -0
- package/dist/supervisor/supervisor.d.ts +202 -0
- package/dist/supervisor/supervisor.js +2244 -0
- package/dist/supervisor/terminal-broadcaster.d.ts +45 -0
- package/dist/supervisor/terminal-broadcaster.js +184 -0
- package/dist/supervisor/types.d.ts +542 -0
- package/dist/supervisor/types.js +10 -0
- package/package.json +35 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The required spawn-time env keys, named once so the supervisor-side
|
|
3
|
+
* producer (`buildChildSpawnEnv`) and this child-side parser share a
|
|
4
|
+
* single required-key contract. `WARM_KEEP` is optional and lives only in
|
|
5
|
+
* the shape below. The producer types its output against this list
|
|
6
|
+
* (`Record<RequiredSpawnEnvKey, string>`), so omitting a listed key is a
|
|
7
|
+
* compile error. That this list stays in step with the validator shape
|
|
8
|
+
* below -- a hand-maintained arktype object -- is covered by the recycle
|
|
9
|
+
* env-contract regression test, which drives the real producer through
|
|
10
|
+
* this parser. This is the contract whose drift once omitted `STEP_COUNT`
|
|
11
|
+
* from the recycle env and broke every recycle.
|
|
12
|
+
*/
|
|
13
|
+
export declare const REQUIRED_SPAWN_ENV_KEYS: readonly ["IPC_CHANNEL_ID", "IPC_HMAC_KEY", "HOST_PUBKEY", "DEPLOYMENT_ID", "DEFINITION_HASH", "MAILBOX_ADDRESS", "STEP_COUNT"];
|
|
14
|
+
export type RequiredSpawnEnvKey = (typeof REQUIRED_SPAWN_ENV_KEYS)[number];
|
|
15
|
+
/**
|
|
16
|
+
* Parsed and validated spawn-time env. The hex-encoded trust anchors
|
|
17
|
+
* decode to their raw byte representations so the IPC channel
|
|
18
|
+
* constructors can consume them without re-validating the hex shape.
|
|
19
|
+
*/
|
|
20
|
+
export interface SpawnTimeEnv {
|
|
21
|
+
/** Channel identifier minted by the supervisor for this spawn. */
|
|
22
|
+
channelId: string;
|
|
23
|
+
/** 32-byte shared HMAC key for the event channel. */
|
|
24
|
+
hmacKey: Uint8Array;
|
|
25
|
+
/** Supervisor's 32-byte Ed25519 public key for control-frame verification. */
|
|
26
|
+
hostPublicKey: Uint8Array;
|
|
27
|
+
/** Deployment identity the supervisor manages. */
|
|
28
|
+
deploymentId: string;
|
|
29
|
+
/** Content hash of the deployed `WorkflowDefinition`. */
|
|
30
|
+
definitionHash: string;
|
|
31
|
+
/** Mail address the deployment registered on the bus. */
|
|
32
|
+
mailboxAddress: string;
|
|
33
|
+
/**
|
|
34
|
+
* Number of steps in the deployed `WorkflowDefinition`
|
|
35
|
+
* (`stepOrder.length`). Selects the head/step collapse in the sidecar's
|
|
36
|
+
* `resolveStepAddress`: a single-step deployment reads its deploy tree
|
|
37
|
+
* at the head, a multi-step deployment at the per-step address.
|
|
38
|
+
*/
|
|
39
|
+
stepCount: number;
|
|
40
|
+
/**
|
|
41
|
+
* Whether this deployment's agent is warm-kept across messages (design
|
|
42
|
+
* §3b). True only for the single-step long-lived deployment the deploy
|
|
43
|
+
* projection marked a warm candidate; the run-loop builds a warm-agent
|
|
44
|
+
* cache when set and keeps cold instantiate-send-teardown otherwise.
|
|
45
|
+
*/
|
|
46
|
+
warmKeep: boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Parse and validate `process.env`-shaped input into the typed
|
|
50
|
+
* `SpawnTimeEnv` struct. Any missing key, malformed hex, or off-size
|
|
51
|
+
* byte payload throws so the binary aborts before opening IPC.
|
|
52
|
+
*
|
|
53
|
+
* The validator runs at the boundary; downstream consumers trust the
|
|
54
|
+
* parsed struct without re-checking.
|
|
55
|
+
*/
|
|
56
|
+
export declare function parseSpawnTimeEnv(rawEnv: Record<string, string | undefined>): SpawnTimeEnv;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Spawn-time env parser for the workflow-process child.
|
|
2
|
+
//
|
|
3
|
+
// The supervisor's spawn path constructs a fresh env object carrying
|
|
4
|
+
// only the IPC trust anchors plus a tightly-scoped set of deployment
|
|
5
|
+
// identifiers. The binary parses `process.env` once at start and hands
|
|
6
|
+
// the validated struct to `runWorkflowChild`. The struct shape is the
|
|
7
|
+
// only env-shaped surface the runtime body sees; everything else flows
|
|
8
|
+
// through IPC frames.
|
|
9
|
+
//
|
|
10
|
+
// The IPC trust anchors carried here are public-half values: the
|
|
11
|
+
// supervisor's Ed25519 PUBLIC key (`HOST_PUBKEY`) plus the shared HMAC
|
|
12
|
+
// key (`IPC_HMAC_KEY`) the supervisor minted at spawn time. The
|
|
13
|
+
// supervisor's Ed25519 PRIVATE key never appears in env per the IPC
|
|
14
|
+
// threat model; the child verifies but never signs control frames.
|
|
15
|
+
import { type } from "arktype";
|
|
16
|
+
import { hexDecode } from "@intx/types";
|
|
17
|
+
import { IPC_CRYPTO } from "../ipc/index.js";
|
|
18
|
+
/**
|
|
19
|
+
* The required spawn-time env keys, named once so the supervisor-side
|
|
20
|
+
* producer (`buildChildSpawnEnv`) and this child-side parser share a
|
|
21
|
+
* single required-key contract. `WARM_KEEP` is optional and lives only in
|
|
22
|
+
* the shape below. The producer types its output against this list
|
|
23
|
+
* (`Record<RequiredSpawnEnvKey, string>`), so omitting a listed key is a
|
|
24
|
+
* compile error. That this list stays in step with the validator shape
|
|
25
|
+
* below -- a hand-maintained arktype object -- is covered by the recycle
|
|
26
|
+
* env-contract regression test, which drives the real producer through
|
|
27
|
+
* this parser. This is the contract whose drift once omitted `STEP_COUNT`
|
|
28
|
+
* from the recycle env and broke every recycle.
|
|
29
|
+
*/
|
|
30
|
+
export const REQUIRED_SPAWN_ENV_KEYS = [
|
|
31
|
+
"IPC_CHANNEL_ID",
|
|
32
|
+
"IPC_HMAC_KEY",
|
|
33
|
+
"HOST_PUBKEY",
|
|
34
|
+
"DEPLOYMENT_ID",
|
|
35
|
+
"DEFINITION_HASH",
|
|
36
|
+
"MAILBOX_ADDRESS",
|
|
37
|
+
"STEP_COUNT",
|
|
38
|
+
];
|
|
39
|
+
/**
|
|
40
|
+
* Required env keys carried by the supervisor at spawn time. The
|
|
41
|
+
* validator surface is intentionally narrow: every key documented at
|
|
42
|
+
* the supervisor's `spawn(opts)` method is represented here, and
|
|
43
|
+
* anything the supervisor did not place in the env causes a targeted
|
|
44
|
+
* failure rather than a silent fallback.
|
|
45
|
+
*/
|
|
46
|
+
const SpawnTimeEnvShape = type({
|
|
47
|
+
IPC_CHANNEL_ID: "string > 0",
|
|
48
|
+
IPC_HMAC_KEY: "string > 0",
|
|
49
|
+
HOST_PUBKEY: "string > 0",
|
|
50
|
+
DEPLOYMENT_ID: "string > 0",
|
|
51
|
+
DEFINITION_HASH: "string > 0",
|
|
52
|
+
MAILBOX_ADDRESS: "string > 0",
|
|
53
|
+
// Step count of the deployed `WorkflowDefinition` (`stepOrder.length`),
|
|
54
|
+
// stringified by the supervisor. The child's deploy-tree read collapses
|
|
55
|
+
// onto the head for a single-step deployment (`resolveStepAddress`), so
|
|
56
|
+
// producer and consumer never derive divergent step addresses. Parsed to
|
|
57
|
+
// a positive integer below; a non-integer or non-positive value throws.
|
|
58
|
+
STEP_COUNT: "string > 0",
|
|
59
|
+
// Warm-keep signal (design §3b). The supervisor sets this to the
|
|
60
|
+
// string `"true"` only for the single-step long-lived deployment the
|
|
61
|
+
// deploy projection marked a warm candidate; any other value (or the
|
|
62
|
+
// key's absence) means cold instantiate-send-teardown per message.
|
|
63
|
+
// Carried explicitly rather than re-derived heuristically in the child
|
|
64
|
+
// so the warm-keep decision is deterministic and a multi-step agent is
|
|
65
|
+
// never warm-kept by a silent default.
|
|
66
|
+
"WARM_KEEP?": "string",
|
|
67
|
+
}).onUndeclaredKey("ignore");
|
|
68
|
+
/**
|
|
69
|
+
* Parse and validate `process.env`-shaped input into the typed
|
|
70
|
+
* `SpawnTimeEnv` struct. Any missing key, malformed hex, or off-size
|
|
71
|
+
* byte payload throws so the binary aborts before opening IPC.
|
|
72
|
+
*
|
|
73
|
+
* The validator runs at the boundary; downstream consumers trust the
|
|
74
|
+
* parsed struct without re-checking.
|
|
75
|
+
*/
|
|
76
|
+
export function parseSpawnTimeEnv(rawEnv) {
|
|
77
|
+
const present = {};
|
|
78
|
+
for (const [key, value] of Object.entries(rawEnv)) {
|
|
79
|
+
if (value !== undefined)
|
|
80
|
+
present[key] = value;
|
|
81
|
+
}
|
|
82
|
+
const validated = SpawnTimeEnvShape(present);
|
|
83
|
+
if (validated instanceof type.errors) {
|
|
84
|
+
throw new Error(`workflow-child spawn-time env failed validation: ${validated.summary}`);
|
|
85
|
+
}
|
|
86
|
+
const hmacKey = hexDecode(validated.IPC_HMAC_KEY);
|
|
87
|
+
if (hmacKey.length !== IPC_CRYPTO.HMAC_KEY_BYTES) {
|
|
88
|
+
throw new Error(`workflow-child IPC_HMAC_KEY must decode to ${String(IPC_CRYPTO.HMAC_KEY_BYTES)} bytes; got ${String(hmacKey.length)}`);
|
|
89
|
+
}
|
|
90
|
+
const hostPublicKey = hexDecode(validated.HOST_PUBKEY);
|
|
91
|
+
if (hostPublicKey.length !== IPC_CRYPTO.ED25519_KEY_BYTES) {
|
|
92
|
+
throw new Error(`workflow-child HOST_PUBKEY must decode to ${String(IPC_CRYPTO.ED25519_KEY_BYTES)} bytes; got ${String(hostPublicKey.length)}`);
|
|
93
|
+
}
|
|
94
|
+
// The channelId is supervisor-minted and the receiver compares it
|
|
95
|
+
// byte-for-byte against incoming frames. Hex-decoding here would
|
|
96
|
+
// surface a malformed value but the IPC primitives expect the
|
|
97
|
+
// hex-encoded string form, so we only verify the encoded length
|
|
98
|
+
// matches the documented channelId byte width.
|
|
99
|
+
const expectedChannelIdHex = IPC_CRYPTO.CHANNEL_ID_BYTES * 2;
|
|
100
|
+
if (validated.IPC_CHANNEL_ID.length !== expectedChannelIdHex) {
|
|
101
|
+
throw new Error(`workflow-child IPC_CHANNEL_ID must be ${String(expectedChannelIdHex)} hex chars; got ${String(validated.IPC_CHANNEL_ID.length)}`);
|
|
102
|
+
}
|
|
103
|
+
const stepCount = Number(validated.STEP_COUNT);
|
|
104
|
+
if (!Number.isInteger(stepCount) || stepCount <= 0) {
|
|
105
|
+
throw new Error(`workflow-child STEP_COUNT must be a positive integer; got ${JSON.stringify(validated.STEP_COUNT)}`);
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
channelId: validated.IPC_CHANNEL_ID,
|
|
109
|
+
hmacKey,
|
|
110
|
+
hostPublicKey,
|
|
111
|
+
deploymentId: validated.DEPLOYMENT_ID,
|
|
112
|
+
definitionHash: validated.DEFINITION_HASH,
|
|
113
|
+
mailboxAddress: validated.MAILBOX_ADDRESS,
|
|
114
|
+
stepCount,
|
|
115
|
+
// Strict `=== "true"` so any other value (including the key's
|
|
116
|
+
// absence) reads false. Warm-keep is opt-in and deterministic; a
|
|
117
|
+
// typo'd or partial value must not silently enable it.
|
|
118
|
+
warmKeep: validated.WARM_KEEP === "true",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { type SpawnTimeEnv } from "./env-bootstrap.js";
|
|
2
|
+
import { type RunWorkflowChildBindings, type RunWorkflowChildResult } from "./run-child.js";
|
|
3
|
+
import { type ChildSubstrateWriteBridge } from "./substrate-write-bridge.js";
|
|
4
|
+
import { type ChildOutboundMailBridge } from "./outbound-mail-bridge.js";
|
|
5
|
+
import { type FrameWriter, type NdjsonReader, type NdjsonWriter } from "../ipc/index.js";
|
|
6
|
+
/**
|
|
7
|
+
* File descriptor the supervisor's `Bun.spawn` wires the
|
|
8
|
+
* event-channel socketpair onto. The supervisor's spawn convention
|
|
9
|
+
* inherits stdio 0/1/2 for stdin/stdout/stderr and the
|
|
10
|
+
* event-channel write side at fd 3 in the child's address space.
|
|
11
|
+
* The wrapper opens fd 3 as the child's `FrameWriter`.
|
|
12
|
+
*/
|
|
13
|
+
export declare const EVENT_CHANNEL_FD = 3;
|
|
14
|
+
/**
|
|
15
|
+
* Substrate-config env keys the host promises to its factory. The
|
|
16
|
+
* supervisor's spawn-time env always carries the IPC trust anchors
|
|
17
|
+
* plus the deployment identifiers (parsed via `parseSpawnTimeEnv`).
|
|
18
|
+
* `substrateConfig` carries every key the host placed in
|
|
19
|
+
* `WorkflowSupervisorBindings.substrateEnv` -- the host's own narrow
|
|
20
|
+
* struct (data-dir, signing-key paths, definition-repo identifiers)
|
|
21
|
+
* lives there, and the factory narrows it again on the way in.
|
|
22
|
+
*
|
|
23
|
+
* The factory does NOT receive `NodeJS.ProcessEnv` directly. The
|
|
24
|
+
* surface is intentionally narrow so a future env-shaped surface
|
|
25
|
+
* (a CLI launcher that prepends keys, a test harness that injects
|
|
26
|
+
* extra knobs) crosses this contract explicitly rather than via
|
|
27
|
+
* an opaque process-shaped slot.
|
|
28
|
+
*/
|
|
29
|
+
export interface SubstrateFactoryEnv {
|
|
30
|
+
/** Parsed spawn-time env (IPC trust anchors + deployment ids). */
|
|
31
|
+
readonly spawn: SpawnTimeEnv;
|
|
32
|
+
/**
|
|
33
|
+
* Substrate-config keys the host placed in
|
|
34
|
+
* `WorkflowSupervisorBindings.substrateEnv`. The factory narrows
|
|
35
|
+
* its own required-key shape against this record at the boundary.
|
|
36
|
+
*/
|
|
37
|
+
readonly substrateConfig: Readonly<Record<string, string>>;
|
|
38
|
+
/**
|
|
39
|
+
* Child-side IPC bridge over the upstream control channel for the
|
|
40
|
+
* workflow-run substrate-write surface. The substrate factory uses
|
|
41
|
+
* this to construct its proxy `RepoStore`, whose
|
|
42
|
+
* `writeTreePreservingPrefix` forwards over IPC into the
|
|
43
|
+
* supervisor's substrate. The bridge's `submit` sends a
|
|
44
|
+
* `substrate.write.request` upstream control frame; the supervisor
|
|
45
|
+
* runs its own underlying `writeTreePreservingPrefix` (which fires
|
|
46
|
+
* the boot-edge pack-push wrap on success) and replies with a
|
|
47
|
+
* matching `substrate.write.response` the bridge resolves the
|
|
48
|
+
* awaiter against. The supervisor's merge callback runs as a
|
|
49
|
+
* `substrate.merge.request` / `substrate.merge.response` pair so
|
|
50
|
+
* the child's per-write merge closure stays in the child's address
|
|
51
|
+
* space.
|
|
52
|
+
*/
|
|
53
|
+
readonly substrateWriteBridge: ChildSubstrateWriteBridge;
|
|
54
|
+
/**
|
|
55
|
+
* Child-side IPC bridge over the upstream control channel for the
|
|
56
|
+
* OUTBOUND half of mailbox ownership (§3a). The substrate factory uses
|
|
57
|
+
* this to construct the supervisor-backed `MessageTransport` it
|
|
58
|
+
* supplies as the step agent's `env.transport`: the transport's
|
|
59
|
+
* `send` calls `bridge.submit`, which emits an `outbound.message`
|
|
60
|
+
* upstream frame and resolves once the supervisor's matching
|
|
61
|
+
* `outbound.result` lands. The supervisor performs the actual signed
|
|
62
|
+
* send through the host transport, so outbound mail carries the
|
|
63
|
+
* agent's signature without the child ever holding the agent's key.
|
|
64
|
+
*/
|
|
65
|
+
readonly outboundMailBridge: ChildOutboundMailBridge;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Substrate-factory callback the host supplies to
|
|
69
|
+
* `runWorkflowChildFromProcessEnv`. The factory constructs the
|
|
70
|
+
* `RunWorkflowChildBindings` the runtime body consumes:
|
|
71
|
+
* substrate-shaped `RepoStore`, principal, per-deployment repo ids,
|
|
72
|
+
* scheduler, step invoker, child spawner, grant evaluator. The
|
|
73
|
+
* factory owns every concrete dependency the runtime body needs to
|
|
74
|
+
* see; the wrapper itself depends on nothing host-specific.
|
|
75
|
+
*/
|
|
76
|
+
export type SubstrateFactory = (env: SubstrateFactoryEnv) => Promise<RunWorkflowChildBindings>;
|
|
77
|
+
/**
|
|
78
|
+
* Optional overrides for the process-shaped surfaces the wrapper
|
|
79
|
+
* crosses. Production hosts use the defaults; tests can inject
|
|
80
|
+
* in-memory streams. The fields exist so a host that wants to
|
|
81
|
+
* compose the wrapper around its own logging layer or telemetry
|
|
82
|
+
* surface can pass through, without exposing `process.env` to the
|
|
83
|
+
* factory.
|
|
84
|
+
*/
|
|
85
|
+
export interface RunWorkflowChildFromProcessEnvOpts {
|
|
86
|
+
/** Override the raw env record (defaults to `process.env`). */
|
|
87
|
+
rawEnv?: Readonly<Record<string, string | undefined>>;
|
|
88
|
+
/** Override the control-channel reader (defaults to `process.stdin`). */
|
|
89
|
+
controlReader?: NdjsonReader;
|
|
90
|
+
/** Override the control-channel writer (defaults to `process.stdout`). */
|
|
91
|
+
controlWriter?: NdjsonWriter;
|
|
92
|
+
/** Override the event-channel writer (defaults to a wrap of fd 3). */
|
|
93
|
+
eventWriter?: FrameWriter;
|
|
94
|
+
/**
|
|
95
|
+
* Override which env keys are forwarded to the factory's
|
|
96
|
+
* `substrateConfig`. Keys not in this allowlist are filtered out.
|
|
97
|
+
* Production hosts list their own substrate-config keys here so the
|
|
98
|
+
* factory never sees spawn-time IPC keys (those flow through the
|
|
99
|
+
* typed `spawn` slot) or unrelated process env. The default is the
|
|
100
|
+
* empty allowlist -- a host that wants its factory to receive
|
|
101
|
+
* substrate-config keys MUST name them here.
|
|
102
|
+
*/
|
|
103
|
+
substrateConfigKeys?: readonly string[];
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Process-boundary wrapper around `runWorkflowChild`. The wrapper
|
|
107
|
+
* parses `process.env` into the typed `SpawnTimeEnv` plus a narrow
|
|
108
|
+
* substrate-config record, opens stdin/stdout for the control
|
|
109
|
+
* channel, wraps the inherited event-channel fd into a
|
|
110
|
+
* `FrameWriter`, hands the typed env to the host's substrate
|
|
111
|
+
* factory to mint the runtime body's bindings, and invokes
|
|
112
|
+
* `runWorkflowChild`.
|
|
113
|
+
*
|
|
114
|
+
* Failures surface loudly:
|
|
115
|
+
* - missing or malformed spawn-time env throws via
|
|
116
|
+
* `parseSpawnTimeEnv`;
|
|
117
|
+
* - a substrate-config key listed in `substrateConfigKeys` whose
|
|
118
|
+
* value is missing or empty throws;
|
|
119
|
+
* - factory rejection propagates;
|
|
120
|
+
* - `runWorkflowChild` rejection propagates.
|
|
121
|
+
*
|
|
122
|
+
* The wrapper does not catch or coerce failures. The host's binary
|
|
123
|
+
* is the layer that decides what to do with a thrown error; the
|
|
124
|
+
* convention shown in the documentation is `process.exit(1)` with
|
|
125
|
+
* a stderr message.
|
|
126
|
+
*/
|
|
127
|
+
export declare function runWorkflowChildFromProcessEnv(factory: SubstrateFactory, opts?: RunWorkflowChildFromProcessEnvOpts): Promise<RunWorkflowChildResult>;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// Process-shaped convenience wrapper around `runWorkflowChild`.
|
|
2
|
+
//
|
|
3
|
+
// The wrapper crosses the only boundary that touches `process.env`,
|
|
4
|
+
// `process.stdin`/`process.stdout`, and the inherited event-channel
|
|
5
|
+
// file descriptor. Each host ships a ~5-line entry script
|
|
6
|
+
// (`#!/usr/bin/env bun` + an `import` + an `await` of this function)
|
|
7
|
+
// against a substrate-factory of its own; the factory consumes a
|
|
8
|
+
// narrow typed env struct rather than `NodeJS.ProcessEnv`, and the
|
|
9
|
+
// runtime body never sees the process boundary.
|
|
10
|
+
//
|
|
11
|
+
// The factory's typed env carries the spawn-time keys the supervisor
|
|
12
|
+
// promises plus the substrate-config keys the host injected on top.
|
|
13
|
+
// The supervisor's `WorkflowSupervisorBindings.substrateEnv` is the
|
|
14
|
+
// only documented surface for the substrate-config slot; the host
|
|
15
|
+
// places its own data-dir / signing-key / definition-repo keys there
|
|
16
|
+
// at supervisor-construction time and reads them back here.
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import { type } from "arktype";
|
|
19
|
+
import { generateKeyPair } from "@intx/crypto";
|
|
20
|
+
import { parseSpawnTimeEnv } from "./env-bootstrap.js";
|
|
21
|
+
import { runWorkflowChild, } from "./run-child.js";
|
|
22
|
+
import { createChildSubstrateWriteBridge, } from "./substrate-write-bridge.js";
|
|
23
|
+
import { createChildOutboundMailBridge, } from "./outbound-mail-bridge.js";
|
|
24
|
+
import { createControlChannelSender, } from "../ipc/index.js";
|
|
25
|
+
/**
|
|
26
|
+
* File descriptor the supervisor's `Bun.spawn` wires the
|
|
27
|
+
* event-channel socketpair onto. The supervisor's spawn convention
|
|
28
|
+
* inherits stdio 0/1/2 for stdin/stdout/stderr and the
|
|
29
|
+
* event-channel write side at fd 3 in the child's address space.
|
|
30
|
+
* The wrapper opens fd 3 as the child's `FrameWriter`.
|
|
31
|
+
*/
|
|
32
|
+
export const EVENT_CHANNEL_FD = 3;
|
|
33
|
+
/**
|
|
34
|
+
* Process-boundary wrapper around `runWorkflowChild`. The wrapper
|
|
35
|
+
* parses `process.env` into the typed `SpawnTimeEnv` plus a narrow
|
|
36
|
+
* substrate-config record, opens stdin/stdout for the control
|
|
37
|
+
* channel, wraps the inherited event-channel fd into a
|
|
38
|
+
* `FrameWriter`, hands the typed env to the host's substrate
|
|
39
|
+
* factory to mint the runtime body's bindings, and invokes
|
|
40
|
+
* `runWorkflowChild`.
|
|
41
|
+
*
|
|
42
|
+
* Failures surface loudly:
|
|
43
|
+
* - missing or malformed spawn-time env throws via
|
|
44
|
+
* `parseSpawnTimeEnv`;
|
|
45
|
+
* - a substrate-config key listed in `substrateConfigKeys` whose
|
|
46
|
+
* value is missing or empty throws;
|
|
47
|
+
* - factory rejection propagates;
|
|
48
|
+
* - `runWorkflowChild` rejection propagates.
|
|
49
|
+
*
|
|
50
|
+
* The wrapper does not catch or coerce failures. The host's binary
|
|
51
|
+
* is the layer that decides what to do with a thrown error; the
|
|
52
|
+
* convention shown in the documentation is `process.exit(1)` with
|
|
53
|
+
* a stderr message.
|
|
54
|
+
*/
|
|
55
|
+
export async function runWorkflowChildFromProcessEnv(factory, opts = {}) {
|
|
56
|
+
const rawEnv = opts.rawEnv ?? process.env;
|
|
57
|
+
const spawn = parseSpawnTimeEnv(rawEnv);
|
|
58
|
+
const substrateConfig = filterSubstrateConfig(rawEnv, opts.substrateConfigKeys ?? []);
|
|
59
|
+
const controlReader = opts.controlReader ?? defaultControlReader();
|
|
60
|
+
const controlWriter = opts.controlWriter ?? defaultControlWriter();
|
|
61
|
+
const eventWriter = opts.eventWriter ?? defaultEventWriter();
|
|
62
|
+
// Mint the child's upstream-signing keypair here so the wrapper can
|
|
63
|
+
// construct the upstream sender before invoking the substrate
|
|
64
|
+
// factory: the factory consumes the sender via the pack-push bridge
|
|
65
|
+
// to build its `ChildHubPackSink`, and the same sender carries the
|
|
66
|
+
// `ready` frame `runWorkflowChild` emits.
|
|
67
|
+
const childKeyPair = await generateKeyPair();
|
|
68
|
+
const upstreamSender = createControlChannelSender({
|
|
69
|
+
privateKeySeed: childKeyPair.privateKey,
|
|
70
|
+
channelId: spawn.channelId,
|
|
71
|
+
writer: controlWriter,
|
|
72
|
+
});
|
|
73
|
+
const substrateWriteBridge = createChildSubstrateWriteBridge({
|
|
74
|
+
upstreamSender,
|
|
75
|
+
});
|
|
76
|
+
const outboundMailBridge = createChildOutboundMailBridge({
|
|
77
|
+
upstreamSender,
|
|
78
|
+
});
|
|
79
|
+
const bindings = await factory({
|
|
80
|
+
spawn,
|
|
81
|
+
substrateConfig,
|
|
82
|
+
substrateWriteBridge,
|
|
83
|
+
outboundMailBridge,
|
|
84
|
+
});
|
|
85
|
+
return runWorkflowChild({
|
|
86
|
+
env: spawn,
|
|
87
|
+
controlReader,
|
|
88
|
+
controlWriter,
|
|
89
|
+
eventWriter,
|
|
90
|
+
bindings: {
|
|
91
|
+
...bindings,
|
|
92
|
+
// The child key pair is minted at this layer so the upstream
|
|
93
|
+
// sender and the `ready` frame's `childPublicKey` come from one
|
|
94
|
+
// keypair. Override the factory's path so `runWorkflowChild`
|
|
95
|
+
// does not re-mint a different key and break verification.
|
|
96
|
+
ipcChildKeyPairFactory: () => Promise.resolve(childKeyPair),
|
|
97
|
+
},
|
|
98
|
+
upstreamSender,
|
|
99
|
+
substrateWriteBridge,
|
|
100
|
+
outboundMailBridge,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const SubstrateConfigValue = type("string > 0");
|
|
104
|
+
function filterSubstrateConfig(rawEnv, keys) {
|
|
105
|
+
const out = {};
|
|
106
|
+
for (const key of keys) {
|
|
107
|
+
const value = rawEnv[key];
|
|
108
|
+
if (value === undefined) {
|
|
109
|
+
throw new Error(`workflow-child substrate-config env: required key ${key} is unset`);
|
|
110
|
+
}
|
|
111
|
+
const validated = SubstrateConfigValue(value);
|
|
112
|
+
if (validated instanceof type.errors) {
|
|
113
|
+
throw new Error(`workflow-child substrate-config env: ${key} failed validation: ${validated.summary}`);
|
|
114
|
+
}
|
|
115
|
+
out[key] = validated;
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
function defaultControlReader() {
|
|
120
|
+
return {
|
|
121
|
+
read() {
|
|
122
|
+
return readNdjsonLines(process.stdin);
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function defaultControlWriter() {
|
|
127
|
+
return {
|
|
128
|
+
write(line) {
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
process.stdout.write(line, (err) => {
|
|
131
|
+
if (err)
|
|
132
|
+
reject(err);
|
|
133
|
+
else
|
|
134
|
+
resolve();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function defaultEventWriter() {
|
|
141
|
+
// The supervisor inherits the event-channel write side on fd 3 in
|
|
142
|
+
// the child's address space. Wrap it as a Node writable so the
|
|
143
|
+
// wire matches the supervisor's `FrameReader` half. Failing to
|
|
144
|
+
// open fd 3 surfaces loudly: the child cannot publish
|
|
145
|
+
// InferenceEvents without it.
|
|
146
|
+
const stream = fs.createWriteStream("", { fd: EVENT_CHANNEL_FD });
|
|
147
|
+
return {
|
|
148
|
+
write(bytes) {
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
stream.write(bytes, (err) => {
|
|
151
|
+
if (err)
|
|
152
|
+
reject(err);
|
|
153
|
+
else
|
|
154
|
+
resolve();
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
async function* readNdjsonLines(source) {
|
|
161
|
+
// Buffered line splitter over the source stream. Yields one JSON
|
|
162
|
+
// line per iteration; trailing newlines are stripped so callers
|
|
163
|
+
// see exactly what the sender wrote without a wire-shape
|
|
164
|
+
// re-decode.
|
|
165
|
+
const decoder = new TextDecoder("utf-8");
|
|
166
|
+
let pending = "";
|
|
167
|
+
for await (const chunk of source) {
|
|
168
|
+
const text = typeof chunk === "string"
|
|
169
|
+
? chunk
|
|
170
|
+
: decoder.decode(chunk, { stream: true });
|
|
171
|
+
pending += text;
|
|
172
|
+
let nl = pending.indexOf("\n");
|
|
173
|
+
while (nl >= 0) {
|
|
174
|
+
const line = pending.slice(0, nl).replace(/\r$/, "");
|
|
175
|
+
pending = pending.slice(nl + 1);
|
|
176
|
+
if (line.length > 0)
|
|
177
|
+
yield line;
|
|
178
|
+
nl = pending.indexOf("\n");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (pending.length > 0)
|
|
182
|
+
yield pending;
|
|
183
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { createCredentialsBackedAuthorize, hashGrants, runWorkflowChild, type ChildStepInvoker, type CredentialsSnapshotRef, type DrainController, type GrantEvaluator, type RunWorkflowChildBindings, type RunWorkflowChildOpts, type RunWorkflowChildResult, type SourcesSnapshotRef, type SubstrateWriteResponseSink, } from "./run-child.js";
|
|
2
|
+
export { createChildSubstrateWriteBridge, type ChildSubstrateWriteBridge, type CreateChildSubstrateWriteBridgeOpts, type SubstrateWriteRequest, } from "./substrate-write-bridge.js";
|
|
3
|
+
export { createChildOutboundMailBridge, type ChildOutboundMailBridge, type CreateChildOutboundMailBridgeOpts, } from "./outbound-mail-bridge.js";
|
|
4
|
+
export { createSupervisorBackedTransport } from "./supervisor-backed-transport.js";
|
|
5
|
+
export { createProxyWorkflowRunRepoStore, type CreateProxyWorkflowRunRepoStoreOpts, } from "./proxy-repo-store.js";
|
|
6
|
+
export { parseSpawnTimeEnv, type SpawnTimeEnv } from "./env-bootstrap.js";
|
|
7
|
+
export { discoverInFlightRuns, type DiscoverRunsOpts, type DiscoveredRun, } from "./self-discovery.js";
|
|
8
|
+
export { createWarmAgentCache, type WarmAgentCache, type WarmEventSinkRef, } from "./warm-agent-cache.js";
|
|
9
|
+
export { EVENT_CHANNEL_FD, runWorkflowChildFromProcessEnv, type RunWorkflowChildFromProcessEnvOpts, type SubstrateFactory, type SubstrateFactoryEnv, } from "./from-process-env.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { createCredentialsBackedAuthorize, hashGrants, runWorkflowChild, } from "./run-child.js";
|
|
2
|
+
export { createChildSubstrateWriteBridge, } from "./substrate-write-bridge.js";
|
|
3
|
+
export { createChildOutboundMailBridge, } from "./outbound-mail-bridge.js";
|
|
4
|
+
export { createSupervisorBackedTransport } from "./supervisor-backed-transport.js";
|
|
5
|
+
export { createProxyWorkflowRunRepoStore, } from "./proxy-repo-store.js";
|
|
6
|
+
export { parseSpawnTimeEnv } from "./env-bootstrap.js";
|
|
7
|
+
export { discoverInFlightRuns, } from "./self-discovery.js";
|
|
8
|
+
export { createWarmAgentCache, } from "./warm-agent-cache.js";
|
|
9
|
+
export { EVENT_CHANNEL_FD, runWorkflowChildFromProcessEnv, } from "./from-process-env.js";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { OutboundMessage, SendReceipt } from "@intx/types/runtime";
|
|
2
|
+
import type { ControlChannelSender, ControlPayload } from "../ipc/control-channel.js";
|
|
3
|
+
/**
|
|
4
|
+
* Bridge surface the child's supervisor-backed transport reaches into.
|
|
5
|
+
* `submit` sends an `outbound.message` upstream and resolves once the
|
|
6
|
+
* supervisor's matching `outbound.result` lands. `handleResult` is the
|
|
7
|
+
* receiver-side entry point the child's control loop invokes when the
|
|
8
|
+
* downstream `outbound.result` frame arrives. `cancelAll` is the
|
|
9
|
+
* cleanup hook the control loop invokes on any exit path so a pending
|
|
10
|
+
* send does not leak an awaiter when the supervisor has torn the IPC
|
|
11
|
+
* down.
|
|
12
|
+
*/
|
|
13
|
+
export interface ChildOutboundMailBridge {
|
|
14
|
+
submit(senderAddress: string, message: OutboundMessage): Promise<SendReceipt>;
|
|
15
|
+
handleResult(data: Extract<ControlPayload, {
|
|
16
|
+
type: "outbound.result";
|
|
17
|
+
}>["data"]): void;
|
|
18
|
+
cancelAll(reason: string): void;
|
|
19
|
+
readonly pendingCount: number;
|
|
20
|
+
}
|
|
21
|
+
export interface CreateChildOutboundMailBridgeOpts {
|
|
22
|
+
upstreamSender: ControlChannelSender;
|
|
23
|
+
/**
|
|
24
|
+
* Optional `requestId` allocator. Production wires a per-instance
|
|
25
|
+
* monotonic counter plus a random suffix; tests inject a
|
|
26
|
+
* deterministic factory so the upstream frame's `requestId` is
|
|
27
|
+
* predictable.
|
|
28
|
+
*/
|
|
29
|
+
allocateRequestId?: () => string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Construct the child-side outbound-mail bridge. Pending sends live in
|
|
33
|
+
* a map keyed by `requestId`; the bridge resolves the awaiter when the
|
|
34
|
+
* supervisor's matching `outbound.result` lands.
|
|
35
|
+
*/
|
|
36
|
+
export declare function createChildOutboundMailBridge(opts: CreateChildOutboundMailBridgeOpts): ChildOutboundMailBridge;
|