@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,278 @@
1
+ // Event channel: UNIX socketpair, HMAC-SHA256 authenticated.
2
+ //
3
+ // The workflow-process child sends InferenceEvents (high rate, plus
4
+ // the per-message `message.run.started` / `message.run.ended`
5
+ // brackets) to the supervisor. The supervisor verifies each frame's
6
+ // MAC and forwards into the hub via the existing `agent.event`
7
+ // session-channel path. The shared 32-byte HMAC key is minted by the
8
+ // supervisor at spawn time and passed to the child in spawn-time env.
9
+ // Both sides authenticate every frame: a malformed or tampered frame
10
+ // is a crash signal, not a recoverable error.
11
+ //
12
+ // Backpressure: the supervisor keeps a bounded userspace ring (default
13
+ // 1024 frames). On overrun the supervisor logs the saturation, fires
14
+ // the caller-supplied crash callback, and the workflow-process kills
15
+ // itself on the next signal cycle. Observability lost equals
16
+ // invariant violated equals crash; the audit chain cannot tolerate a
17
+ // silent drop, and a blocking writer would deadlock against the
18
+ // reactor's emit path.
19
+ //
20
+ // Mixing failure mode: the payload union here covers InferenceEvent
21
+ // shapes only. A "control message" structurally shaped as `drain` or
22
+ // `recycle` will not satisfy this union and the receiver will crash.
23
+ // The discriminated arktype validators in `control-channel.ts` and
24
+ // here are disjoint by construction.
25
+ import { type } from "arktype";
26
+ import { hexDecode, hexEncode } from "@intx/types";
27
+ import { InferenceEvent } from "@intx/types/runtime";
28
+ import { decodeEnvelope, encodeEnvelope, FrameEnvelope, MacedEnvelope, } from "./envelope.js";
29
+ import { signHmac, verifyHmac } from "./crypto.js";
30
+ /**
31
+ * The event channel additionally carries the two bracket events the
32
+ * reactor emits per message run. They are part of the InferenceEvent
33
+ * union upstream; re-exporting the union directly keeps this channel
34
+ * structurally identical to what the hub's `agent.event` frame
35
+ * carries today.
36
+ */
37
+ export const EventPayload = InferenceEvent;
38
+ /**
39
+ * Construct the child-side event-channel sender. The shared HMAC
40
+ * key lives in closure. The child mints monotonic seq values per
41
+ * channelId; the receiver enforces strict monotonicity.
42
+ */
43
+ export function createEventChannelSender(opts) {
44
+ let seq = 0;
45
+ // Serialize sends. `signHmac` is async, so without a lock two
46
+ // concurrent callers could each assign seq, suspend on the signer, and
47
+ // resume in HMAC-resolution order — writing frames out of seq order,
48
+ // which the receiver rejects as a gap and crashes the channel. The
49
+ // production caller fires events without awaiting (`void send(event)`),
50
+ // so this is the live case. The promise chain makes each send await the
51
+ // previous send's completion before it assigns seq, signs, and writes,
52
+ // keeping that critical section atomic. Mirrors the control channel's
53
+ // sender, whose Ed25519 signing has the same shape.
54
+ let tail = Promise.resolve();
55
+ return {
56
+ get seq() {
57
+ return seq;
58
+ },
59
+ send(payload) {
60
+ const previous = tail;
61
+ let release = () => undefined;
62
+ tail = new Promise((resolve) => {
63
+ release = resolve;
64
+ });
65
+ return (async () => {
66
+ await previous;
67
+ try {
68
+ seq += 1;
69
+ const envelope = {
70
+ seq,
71
+ channelId: opts.channelId,
72
+ payload,
73
+ };
74
+ const envelopeBytes = encodeEnvelope(envelope);
75
+ const tag = await signHmac(envelopeBytes, opts.hmacKey);
76
+ const maced = {
77
+ envelope,
78
+ mac: hexEncode(tag),
79
+ };
80
+ // Newline-delimit each frame. The channel rides a byte-stream
81
+ // pipe (fd3) where the kernel may coalesce successive writes into
82
+ // one read or split one write across reads -- the one-write-equals-
83
+ // one-frame assumption does not hold under the burst of events a
84
+ // real step emits. `JSON.stringify` never emits a literal newline
85
+ // (newlines inside strings are escaped as `\n`), so `\n` is an
86
+ // unambiguous frame terminator the receiver splits on, mirroring
87
+ // the control channel's NDJSON discipline.
88
+ await opts.writer.write(new TextEncoder().encode(`${JSON.stringify(maced)}\n`));
89
+ }
90
+ finally {
91
+ release();
92
+ }
93
+ })();
94
+ },
95
+ };
96
+ }
97
+ /**
98
+ * Construct the supervisor-side event-channel receiver. Yields one
99
+ * verified, in-order `EventPayload` per call. Any frame that fails
100
+ * HMAC verification, carries a non-current channelId, arrives out
101
+ * of order, or arrives faster than the consumer drains -- triggers
102
+ * `onCrash` and ends the iterator.
103
+ */
104
+ export async function* receiveEventChannel(opts) {
105
+ const limit = opts.bufferLimit ?? DEFAULT_EVENT_BUFFER_LIMIT;
106
+ let highestSeq = 0;
107
+ const buffer = [];
108
+ let crashed = false;
109
+ let producerDone = false;
110
+ let waiter = null;
111
+ function wake() {
112
+ const w = waiter;
113
+ waiter = null;
114
+ if (w)
115
+ w();
116
+ }
117
+ const pump = (async () => {
118
+ // The channel rides a byte-stream pipe: the kernel can coalesce
119
+ // several sender writes into one read chunk or split one write
120
+ // across chunks, so a chunk boundary is not a frame boundary. The
121
+ // sender newline-delimits every frame; this decoder accumulates raw
122
+ // bytes and splits on `\n` so each complete line is exactly one
123
+ // envelope, mirroring the control channel's NDJSON reader. A partial
124
+ // trailing line stays buffered until its terminator arrives.
125
+ const decoder = new TextDecoder("utf-8");
126
+ let pending = "";
127
+ try {
128
+ for await (const chunk of opts.reader.read()) {
129
+ if (crashed)
130
+ return;
131
+ pending += decoder.decode(chunk, { stream: true });
132
+ let nl = pending.indexOf("\n");
133
+ while (nl >= 0) {
134
+ const line = pending.slice(0, nl).replace(/\r$/, "");
135
+ pending = pending.slice(nl + 1);
136
+ nl = pending.indexOf("\n");
137
+ if (line.length === 0)
138
+ continue;
139
+ if (crashed)
140
+ return;
141
+ let raw;
142
+ try {
143
+ raw = JSON.parse(line);
144
+ }
145
+ catch (cause) {
146
+ crashed = true;
147
+ opts.onCrash(`event channel received non-JSON frame: ${errorMessage(cause)}`);
148
+ wake();
149
+ return;
150
+ }
151
+ const crashedOnLine = await processLine(raw);
152
+ if (crashedOnLine)
153
+ return;
154
+ }
155
+ }
156
+ // A non-empty trailing buffer at EOF is a truncated final frame:
157
+ // the sender always terminates a frame with `\n`, so unterminated
158
+ // bytes mean the writer died mid-frame. Surface it as a crash
159
+ // rather than silently dropping a partial envelope.
160
+ if (pending.length > 0) {
161
+ crashed = true;
162
+ opts.onCrash(`event channel received truncated frame at EOF (${String(pending.length)} bytes, no terminator)`);
163
+ wake();
164
+ return;
165
+ }
166
+ }
167
+ finally {
168
+ producerDone = true;
169
+ wake();
170
+ }
171
+ // Process one decoded frame through the verify/order/validate
172
+ // pipeline. Returns `true` when the frame tripped a crash (the
173
+ // caller must stop pumping), `false` on a clean buffered push.
174
+ async function processLine(raw) {
175
+ const maced = MacedEnvelope(raw);
176
+ if (maced instanceof type.errors) {
177
+ crashed = true;
178
+ opts.onCrash(`event channel envelope failed validation: ${maced.summary}`);
179
+ wake();
180
+ return true;
181
+ }
182
+ let envelopeBytes;
183
+ try {
184
+ envelopeBytes = encodeEnvelope(maced.envelope);
185
+ }
186
+ catch (cause) {
187
+ crashed = true;
188
+ opts.onCrash(`event channel envelope re-encode failed: ${errorMessage(cause)}`);
189
+ wake();
190
+ return true;
191
+ }
192
+ let macBytes;
193
+ try {
194
+ macBytes = hexDecode(maced.mac);
195
+ }
196
+ catch (cause) {
197
+ crashed = true;
198
+ opts.onCrash(`event channel MAC decode failed: ${errorMessage(cause)}`);
199
+ wake();
200
+ return true;
201
+ }
202
+ const ok = await verifyHmac(envelopeBytes, macBytes, opts.hmacKey);
203
+ if (!ok) {
204
+ crashed = true;
205
+ opts.onCrash(`event channel HMAC did not verify (seq=${String(maced.envelope.seq)}, channelId=${maced.envelope.channelId})`);
206
+ wake();
207
+ return true;
208
+ }
209
+ if (maced.envelope.channelId !== opts.channelId) {
210
+ crashed = true;
211
+ opts.onCrash(`event channel channelId mismatch: expected ${opts.channelId}, got ${maced.envelope.channelId} at seq=${String(maced.envelope.seq)}`);
212
+ wake();
213
+ return true;
214
+ }
215
+ if (maced.envelope.seq <= highestSeq) {
216
+ crashed = true;
217
+ opts.onCrash(`event channel out-of-order seq: expected > ${String(highestSeq)}, got ${String(maced.envelope.seq)}`);
218
+ wake();
219
+ return true;
220
+ }
221
+ if (maced.envelope.seq !== highestSeq + 1) {
222
+ crashed = true;
223
+ opts.onCrash(`event channel seq gap: expected ${String(highestSeq + 1)}, got ${String(maced.envelope.seq)}`);
224
+ wake();
225
+ return true;
226
+ }
227
+ highestSeq = maced.envelope.seq;
228
+ const payload = EventPayload(maced.envelope.payload);
229
+ if (payload instanceof type.errors) {
230
+ crashed = true;
231
+ opts.onCrash(`event channel payload failed validation: ${payload.summary}`);
232
+ wake();
233
+ return true;
234
+ }
235
+ if (buffer.length >= limit) {
236
+ crashed = true;
237
+ opts.onCrash(`event channel buffer overrun: ${String(buffer.length)} frames pending, limit ${String(limit)}`);
238
+ wake();
239
+ return true;
240
+ }
241
+ buffer.push(payload);
242
+ wake();
243
+ return false;
244
+ }
245
+ })();
246
+ try {
247
+ while (true) {
248
+ if (buffer.length > 0) {
249
+ const next = buffer.shift();
250
+ if (next === undefined) {
251
+ throw new Error("event channel receiver invariant: shift returned undefined despite non-empty buffer");
252
+ }
253
+ yield next;
254
+ continue;
255
+ }
256
+ if (crashed || producerDone) {
257
+ return;
258
+ }
259
+ await new Promise((resolve) => {
260
+ waiter = resolve;
261
+ });
262
+ }
263
+ }
264
+ finally {
265
+ await pump;
266
+ }
267
+ }
268
+ function errorMessage(cause) {
269
+ if (cause instanceof Error)
270
+ return cause.message;
271
+ return String(cause);
272
+ }
273
+ export const DEFAULT_EVENT_BUFFER_LIMIT = 1024;
274
+ /**
275
+ * Re-export the envelope decoder for callers that need raw access
276
+ * to a frame's envelope outside the receiver iterator.
277
+ */
278
+ export { decodeEnvelope };
@@ -0,0 +1,4 @@
1
+ export { ControlPayload, OutboundAttachmentPayload, OutboundMessagePayload, SourcesUpdatedData, createControlChannelSender, receiveControlChannel, type ControlChannelSender, type ControlChannelSenderOpts, type ControlChannelReceiverOpts, type NdjsonReader, type NdjsonWriter, } from "./control-channel.js";
2
+ export { DEFAULT_EVENT_BUFFER_LIMIT, EventPayload, createEventChannelSender, receiveEventChannel, type EventChannelSender, type EventChannelSenderOpts, type EventChannelReceiverOpts, type FrameReader, type FrameWriter, } from "./event-channel.js";
3
+ export { FrameEnvelope, MacedEnvelope, SignedEnvelope, decodeEnvelope, encodeEnvelope, } from "./envelope.js";
4
+ export { IPC_CRYPTO, generateChannelId, generateHmacKey, signEd25519, signHmac, verifyEd25519, verifyHmac, } from "./crypto.js";
@@ -0,0 +1,143 @@
1
+ // =============================================================
2
+ // THREAT MODEL -- workflow-process supervisor/child IPC
3
+ // =============================================================
4
+ //
5
+ // The workflow-process is a Bun child the supervisor spawns per
6
+ // active deployment. That child runs user-supplied workflow code:
7
+ // tools the operator deployed, director prompts the operator wrote,
8
+ // agent harnesses operating against external APIs. The supervisor
9
+ // lives in-sidecar, owns the mail-bus identity for the deployment's
10
+ // addresses, and holds the Ed25519 signing key used to commit
11
+ // authoritative records (`CancelRequested`, drain signatures, audit
12
+ // frames) on the deployment's behalf.
13
+ //
14
+ // The IPC channels between the supervisor and the child are the
15
+ // airlock between an authoritative-but-trusted process and a
16
+ // potentially-compromised one. Per-frame crypto is what makes the
17
+ // airlock seal: any frame the child receives whose signature does
18
+ // not verify under the supervisor's Ed25519 public key is dropped
19
+ // (and the receiver crashes); any frame the supervisor receives
20
+ // whose HMAC does not verify is dropped (and the receiver crashes).
21
+ //
22
+ // Design pieces, with the failure mode each one defends against:
23
+ //
24
+ // 1. Asymmetric crypto on the control channel, two keypairs per spawn.
25
+ // Both directions of the control channel carry Ed25519-signed
26
+ // frames; each direction uses its own keypair so neither end
27
+ // holds the private half of the keypair its peer signs with.
28
+ //
29
+ // Supervisor's keypair (downstream: supervisor -> child).
30
+ // The supervisor mints the keypair at spawn time, signs every
31
+ // downstream control frame (trigger.fire, signal.deliver, drain,
32
+ // recycle, shutdown, grants-updated, sources-updated) with the
33
+ // private half, and passes the corresponding 32-byte public half
34
+ // to the child in spawn-time env (`HOST_PUBKEY`). The
35
+ // SUPERVISOR'S PRIVATE KEY NEVER LEAVES THE SUPERVISOR'S ADDRESS
36
+ // SPACE. The spawn-time env carries pubkey + HMAC key + channelId
37
+ // and NEVER the supervisor's private key. A leak via fork() memory
38
+ // copy is impossible because the supervisor uses `spawn` with an
39
+ // explicitly constructed `env` object -- there is no inheritance
40
+ // of the supervisor's process env into the child by reference.
41
+ // A leak via accidental `process.env` propagation is prevented by
42
+ // constructing the child env as a fresh object containing only
43
+ // the documented variables (no `...process.env` spread). A leak
44
+ // via serialization is prevented by the supervisor never placing
45
+ // the private key in any IPC payload, log line, or audit-log
46
+ // frame -- it lives only as a 32-byte Uint8Array held in closure
47
+ // by the signing-key callback.
48
+ //
49
+ // Child's keypair (upstream: child -> supervisor).
50
+ // The child mints its own Ed25519 keypair at startup, signs every
51
+ // upstream control frame with the private half, and publishes the
52
+ // matching public half in the payload of the upstream `ready`
53
+ // frame as `childPublicKey` (hex-encoded). The supervisor's
54
+ // upstream receiver opens in bootstrap mode: it parses the first
55
+ // frame's envelope structurally, extracts `childPublicKey`,
56
+ // verifies the `ready` frame's signature against it, and uses the
57
+ // same key to verify every subsequent upstream frame. The CHILD'S
58
+ // PRIVATE KEY NEVER LEAVES THE CHILD'S ADDRESS SPACE. A
59
+ // compromised child cannot forge a frame the supervisor accepts
60
+ // under a different key: the bootstrap pins the verification key
61
+ // to the value the first `ready` frame published, and a frame
62
+ // that claims a different sender is one whose signature the
63
+ // receiver cannot verify.
64
+ //
65
+ // 2. HMAC-SHA256 on the event channel.
66
+ // Symmetric authentication is correct for the high-rate path
67
+ // (InferenceEvents stream at the reactor's emit cadence, including
68
+ // `message.run.started` / `message.run.ended` brackets). Both
69
+ // sides hold the same 32-byte key; both sides authenticate every
70
+ // frame. HMAC is roughly two orders of magnitude cheaper per byte
71
+ // than Ed25519, which is what makes per-frame authentication
72
+ // affordable at high rate. The cost saving holds across the full
73
+ // InferenceEvent volume profile (delta events at token rate).
74
+ //
75
+ // 3. ChannelId rotation on every spawn AND every recycle.
76
+ // The channelId is 16 bytes from `crypto.getRandomValues`, hex-encoded
77
+ // (interface-decisions Bonus 2). The supervisor mints it at every
78
+ // spawn, places it in spawn-time env (`IPC_CHANNEL_ID`), and mints
79
+ // a fresh one at every recycle. Receivers track the current
80
+ // channelId; any frame carrying a non-current channelId is a
81
+ // signal that the wire is still attached to a predecessor
82
+ // workflow-process (a recycled child's leftover state in some
83
+ // socket the supervisor failed to fully close, or an attacker who
84
+ // captured a frame from a previous spawn and is replaying it
85
+ // against the current one). The receiver crashes loudly rather
86
+ // than processing the stale frame. Crash-on-mismatch is the only
87
+ // honest response: a "stale frame" is by construction either a
88
+ // replay or a programming bug, and silently dropping it would
89
+ // paper over both.
90
+ //
91
+ // 4. Monotonic seq per channelId, crash on out-of-order.
92
+ // Every frame within a channelId's lifetime carries a strictly-
93
+ // increasing seq. The sender maintains a counter; the receiver
94
+ // tracks the highest seq seen and requires the next frame's seq
95
+ // to equal `highestSeq + 1` exactly (gap = drop, repeat = replay,
96
+ // decrease = replay). Any seq violation crashes the receiver.
97
+ // The combination of channelId rotation and monotonic seq
98
+ // prevents an attacker who captured a previous channelId's frame
99
+ // stream from replaying it against the current channel: the
100
+ // captured frames carry the wrong channelId. Within the current
101
+ // channelId, the monotonic-seq check prevents replay of an
102
+ // earlier frame in the same channel.
103
+ //
104
+ // 5. Crash-on-overrun on the event channel.
105
+ // The supervisor buffers in userspace with a bound (default 1024
106
+ // frames). On overrun, the supervisor logs the saturation and the
107
+ // workflow-process kills itself. The audit chain is built from
108
+ // forwarded InferenceEvents; a silent drop of a single event
109
+ // breaks the chain in a way no downstream consumer can detect.
110
+ // The choice is between two unrecoverable states: (a) corrupt
111
+ // audit chain that pretends to be correct, or (b) crash that
112
+ // advertises itself. Crash wins.
113
+ //
114
+ // 6. Clean control-vs-event boundary.
115
+ // The control channel carries `credentialsSnapshot` updates,
116
+ // drain, recycle, grants-updated, ready, shutdown -- shapes whose
117
+ // authority is the supervisor's identity and whose rate is low.
118
+ // The event channel carries InferenceEvents and the bracket
119
+ // events -- shapes whose authority is the deployment's identity
120
+ // (which the supervisor and child share via the HMAC key) and
121
+ // whose rate is high. The two channels' typed payload unions are
122
+ // DISJOINT BY CONSTRUCTION (the discriminated `ControlPayload`
123
+ // union in `control-channel.ts` does not overlap the
124
+ // `EventPayload` union in `event-channel.ts`). A control payload
125
+ // that included an inference-event shape would defeat the split;
126
+ // an inference-event payload that included a `drain` or `recycle`
127
+ // discriminator would let a compromised child issue control-plane
128
+ // commands the supervisor honors. Neither is possible at the
129
+ // type level.
130
+ //
131
+ // 7. Supervisor-minted channelId.
132
+ // The supervisor is the single source of truth for channelId.
133
+ // The child does NOT generate its own and never proposes a value.
134
+ // A protocol that let the child propose a channelId would let a
135
+ // compromised child negotiate a channelId an attacker had
136
+ // pre-captured frames for. The supervisor mints, the child reads
137
+ // from env, the supervisor enforces.
138
+ //
139
+ // =============================================================
140
+ export { ControlPayload, OutboundAttachmentPayload, OutboundMessagePayload, SourcesUpdatedData, createControlChannelSender, receiveControlChannel, } from "./control-channel.js";
141
+ export { DEFAULT_EVENT_BUFFER_LIMIT, EventPayload, createEventChannelSender, receiveEventChannel, } from "./event-channel.js";
142
+ export { FrameEnvelope, MacedEnvelope, SignedEnvelope, decodeEnvelope, encodeEnvelope, } from "./envelope.js";
143
+ export { IPC_CRYPTO, generateChannelId, generateHmacKey, signEd25519, signHmac, verifyEd25519, verifyHmac, } from "./crypto.js";
@@ -0,0 +1,30 @@
1
+ import type { HubTransport } from "@intx/mail-memory";
2
+ import type { MailBusBindings } from "../supervisor/types.js";
3
+ /**
4
+ * Returned shape: the `MailBusBindings` surface the supervisor
5
+ * consumes, plus a `routeInbound` method the host calls to deliver
6
+ * an inbound message to every handler subscribed at the named
7
+ * address. The split keeps the supervisor's subscription contract
8
+ * narrow while letting the host drive delivery through its existing
9
+ * transport plumbing.
10
+ */
11
+ export interface HubTransportMailBusAdapter extends MailBusBindings {
12
+ /**
13
+ * Fan a delivered message out to every handler subscribed at
14
+ * `address`. Returns immediately if no handler is registered --
15
+ * the supervisor's lifecycle (`subscribeMailForAddress` returns a
16
+ * disposer the supervisor calls on teardown) is the authoritative
17
+ * source of which addresses are live; addresses without an active
18
+ * subscriber drop the message silently.
19
+ */
20
+ routeInbound(address: string, message: Uint8Array): void;
21
+ }
22
+ /**
23
+ * Wrap an existing `HubTransport` instance as the supervisor-facing
24
+ * `MailBusBindings` shape. The `transport` argument is held only as
25
+ * a sink-side reference the adapter does not actively reach into
26
+ * today -- the sidecar's deploy router registers the head's address
27
+ * on the transport, and the adapter delivers inbound bytes through
28
+ * `routeInbound` directly into the per-address subscriber map below.
29
+ */
30
+ export declare function wrapHubTransportAsMailBus(transport: HubTransport): HubTransportMailBusAdapter;
@@ -0,0 +1,76 @@
1
+ // Adapter that surfaces an existing `HubTransport` instance as the
2
+ // supervisor-facing `MailBusBindings` shape. Hosts that already own
3
+ // a hub-mail transport (the sidecar, the integration test harness,
4
+ // any future alternative-sidecar implementation) wire the supervisor
5
+ // through this adapter instead of standing up a second bus.
6
+ //
7
+ // Per-address subscriber maps live inside the adapter: the
8
+ // supervisor's `subscribeMailForAddress` registers a handler here;
9
+ // the host's transport delivers messages via `routeInbound` and the
10
+ // adapter fans the bytes out to every subscribed handler. The
11
+ // transport itself is treated as a sink the host already owns -- the
12
+ // adapter does not register addresses on the transport on the
13
+ // supervisor's behalf (production sidecars register the head's address
14
+ // through the deploy router before the supervisor spawns) and does not
15
+ // double-deliver messages the transport itself routes.
16
+ /**
17
+ * Wrap an existing `HubTransport` instance as the supervisor-facing
18
+ * `MailBusBindings` shape. The `transport` argument is held only as
19
+ * a sink-side reference the adapter does not actively reach into
20
+ * today -- the sidecar's deploy router registers the head's address
21
+ * on the transport, and the adapter delivers inbound bytes through
22
+ * `routeInbound` directly into the per-address subscriber map below.
23
+ */
24
+ export function wrapHubTransportAsMailBus(transport) {
25
+ const subscribers = new Map();
26
+ return {
27
+ registerAddress(address) {
28
+ // The supervisor's address registration is the seam where the
29
+ // workflow-process child owns its own mailbox; production
30
+ // sidecars register the head's address through the deploy router
31
+ // before the supervisor spawns, so this method is intentionally
32
+ // inert. The `transport` reference is retained so a future
33
+ // routing change that wants the bus to own registration has
34
+ // the handle in scope.
35
+ void address;
36
+ void transport;
37
+ },
38
+ unregisterAddress(address) {
39
+ subscribers.delete(address);
40
+ },
41
+ subscribeMailForAddress(address, handler) {
42
+ let set = subscribers.get(address);
43
+ if (set === undefined) {
44
+ set = new Set();
45
+ subscribers.set(address, set);
46
+ }
47
+ set.add(handler);
48
+ return () => {
49
+ const current = subscribers.get(address);
50
+ current?.delete(handler);
51
+ };
52
+ },
53
+ routeInbound(address, message) {
54
+ const set = subscribers.get(address);
55
+ if (set === undefined)
56
+ return;
57
+ for (const handler of set)
58
+ handler(message);
59
+ },
60
+ async sendOutbound(senderAddress, message) {
61
+ // OUTBOUND half of mailbox ownership (§3a): route the agent's
62
+ // reply / mail-tool send through the host transport's signed-send
63
+ // path. `getTransportFor(senderAddress).send(message)` is the
64
+ // exact path the in-process agent uses today -- it signs with the
65
+ // `CryptoProvider` the host registered for `senderAddress` via
66
+ // `register(address, crypto)`, so the outbound mail carries the
67
+ // AGENT's signature and fires the same `addMessageSentHandler`
68
+ // audit hook the in-process path relies on. The supervisor never
69
+ // holds the agent's key; the host transport does. An unregistered
70
+ // sender throws inside `getTransportFor` rather than emitting
71
+ // unsigned mail.
72
+ const scoped = transport.getTransportFor(senderAddress);
73
+ return scoped.send(message);
74
+ },
75
+ };
76
+ }
@@ -0,0 +1 @@
1
+ export { wrapHubTransportAsMailBus, type HubTransportMailBusAdapter, } from "./hub-transport-adapter.js";
@@ -0,0 +1 @@
1
+ export { wrapHubTransportAsMailBus, } from "./hub-transport-adapter.js";
@@ -0,0 +1,3 @@
1
+ export { createWorkflowHostScheduler, type SchedulerHandle, type SchedulerOpts, } from "./scheduler.js";
2
+ export { adaptHostScheduler } from "./scheduler-adapter.js";
3
+ export { SignalReceivedEnvelope, createWorkflowHostSignalChannel, type SignalChannelHandle, type SignalChannelOpts, } from "./signal-channel.js";
@@ -0,0 +1,3 @@
1
+ export { createWorkflowHostScheduler, } from "./scheduler.js";
2
+ export { adaptHostScheduler } from "./scheduler-adapter.js";
3
+ export { SignalReceivedEnvelope, createWorkflowHostSignalChannel, } from "./signal-channel.js";
@@ -0,0 +1,3 @@
1
+ import type { Scheduler } from "@intx/workflow";
2
+ import type { SchedulerHandle } from "./scheduler.js";
3
+ export declare function adaptHostScheduler(handle: SchedulerHandle): Scheduler;
@@ -0,0 +1,24 @@
1
+ // Adapter from the host-singleton `SchedulerHandle` to the runtime's
2
+ // per-call `Scheduler` interface.
3
+ //
4
+ // The host's `SchedulerHandle` (from `seams/scheduler.ts`) tails
5
+ // `TimerSet` commits via `subscribeKind` and commits `TimerFired` at
6
+ // the wall-clock deadline. The runtime body (`@intx/workflow`'s
7
+ // `runtimeRun`) expects a `Scheduler.scheduleIn(runId, timerId,
8
+ // fireAt) => dispose` whose dispose cancels any pending `TimerFired`.
9
+ //
10
+ // The adapter routes `scheduleIn` through to the host scheduler's
11
+ // live ingest by relying on the supervisor's commit-then-subscribe
12
+ // ordering: by the time the runtime body has committed `TimerSet`
13
+ // and called `scheduleIn`, the host scheduler's `subscribeKind` tail
14
+ // will queue the timer. The returned disposer asks the host scheduler
15
+ // to drop the queued entry for `(runId, timerId)`.
16
+ export function adaptHostScheduler(handle) {
17
+ return {
18
+ scheduleIn(runId, timerId, _fireAt) {
19
+ return () => {
20
+ handle.cancelQueued(runId, timerId);
21
+ };
22
+ },
23
+ };
24
+ }