@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.
Files changed (81) hide show
  1. package/LICENSE +176 -0
  2. package/README.md +287 -0
  3. package/dist/adapters/blob-substrate.d.ts +49 -0
  4. package/dist/adapters/blob-substrate.js +140 -0
  5. package/dist/adapters/repo-store.d.ts +39 -0
  6. package/dist/adapters/repo-store.js +344 -0
  7. package/dist/adapters/spawn-child.d.ts +74 -0
  8. package/dist/adapters/spawn-child.js +152 -0
  9. package/dist/adapters/step-invoker.d.ts +114 -0
  10. package/dist/adapters/step-invoker.js +360 -0
  11. package/dist/child/env-bootstrap.d.ts +56 -0
  12. package/dist/child/env-bootstrap.js +120 -0
  13. package/dist/child/from-process-env.d.ts +127 -0
  14. package/dist/child/from-process-env.js +183 -0
  15. package/dist/child/index.d.ts +9 -0
  16. package/dist/child/index.js +9 -0
  17. package/dist/child/outbound-mail-bridge.d.ts +36 -0
  18. package/dist/child/outbound-mail-bridge.js +143 -0
  19. package/dist/child/proxy-repo-store.d.ts +27 -0
  20. package/dist/child/proxy-repo-store.js +200 -0
  21. package/dist/child/run-child.d.ts +320 -0
  22. package/dist/child/run-child.js +900 -0
  23. package/dist/child/self-discovery.d.ts +29 -0
  24. package/dist/child/self-discovery.js +57 -0
  25. package/dist/child/substrate-write-bridge.d.ts +72 -0
  26. package/dist/child/substrate-write-bridge.js +188 -0
  27. package/dist/child/supervisor-backed-transport.d.ts +10 -0
  28. package/dist/child/supervisor-backed-transport.js +113 -0
  29. package/dist/child/warm-agent-cache.d.ts +78 -0
  30. package/dist/child/warm-agent-cache.js +112 -0
  31. package/dist/drain-controller.d.ts +37 -0
  32. package/dist/drain-controller.js +46 -0
  33. package/dist/index.d.ts +10 -0
  34. package/dist/index.js +10 -0
  35. package/dist/ipc/control-channel.d.ts +336 -0
  36. package/dist/ipc/control-channel.js +532 -0
  37. package/dist/ipc/crypto.d.ts +46 -0
  38. package/dist/ipc/crypto.js +126 -0
  39. package/dist/ipc/envelope.d.ts +53 -0
  40. package/dist/ipc/envelope.js +88 -0
  41. package/dist/ipc/event-channel.d.ts +677 -0
  42. package/dist/ipc/event-channel.js +278 -0
  43. package/dist/ipc/index.d.ts +4 -0
  44. package/dist/ipc/index.js +143 -0
  45. package/dist/mail-bus/hub-transport-adapter.d.ts +30 -0
  46. package/dist/mail-bus/hub-transport-adapter.js +76 -0
  47. package/dist/mail-bus/index.d.ts +1 -0
  48. package/dist/mail-bus/index.js +1 -0
  49. package/dist/seams/index.d.ts +3 -0
  50. package/dist/seams/index.js +3 -0
  51. package/dist/seams/scheduler-adapter.d.ts +3 -0
  52. package/dist/seams/scheduler-adapter.js +24 -0
  53. package/dist/seams/scheduler.d.ts +94 -0
  54. package/dist/seams/scheduler.js +397 -0
  55. package/dist/seams/signal-channel.d.ts +74 -0
  56. package/dist/seams/signal-channel.js +304 -0
  57. package/dist/supervisor/cancel-signing.d.ts +68 -0
  58. package/dist/supervisor/cancel-signing.js +144 -0
  59. package/dist/supervisor/child-termination.d.ts +51 -0
  60. package/dist/supervisor/child-termination.js +76 -0
  61. package/dist/supervisor/credentials.d.ts +101 -0
  62. package/dist/supervisor/credentials.js +153 -0
  63. package/dist/supervisor/dispatch-attribution.d.ts +37 -0
  64. package/dist/supervisor/dispatch-attribution.js +114 -0
  65. package/dist/supervisor/drain-timeout.d.ts +127 -0
  66. package/dist/supervisor/drain-timeout.js +231 -0
  67. package/dist/supervisor/index.d.ts +7 -0
  68. package/dist/supervisor/index.js +6 -0
  69. package/dist/supervisor/recycle.d.ts +212 -0
  70. package/dist/supervisor/recycle.js +440 -0
  71. package/dist/supervisor/run-event-compaction.d.ts +34 -0
  72. package/dist/supervisor/run-event-compaction.js +115 -0
  73. package/dist/supervisor/spawn-env.d.ts +39 -0
  74. package/dist/supervisor/spawn-env.js +36 -0
  75. package/dist/supervisor/supervisor.d.ts +202 -0
  76. package/dist/supervisor/supervisor.js +2244 -0
  77. package/dist/supervisor/terminal-broadcaster.d.ts +45 -0
  78. package/dist/supervisor/terminal-broadcaster.js +184 -0
  79. package/dist/supervisor/types.d.ts +542 -0
  80. package/dist/supervisor/types.js +10 -0
  81. 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;