@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,320 @@
1
+ import type { Principal, RepoId, RepoStore as SubstrateRepoStore } from "@intx/hub-sessions/substrate";
2
+ import type { DirectorRegistry } from "@intx/agent";
3
+ import type { AuthzCallResult } from "@intx/inference";
4
+ import type { RunResult, Scheduler, StepInvokeRequest, StepInvokeResult, SpawnChildWorkflow, WorkflowAuthorizeFn } from "@intx/workflow";
5
+ import { type WorkflowHostDrainController } from "../drain-controller.js";
6
+ import type { InferenceSource } from "@intx/types/runtime";
7
+ import { type ControlChannelSender, type ControlPayload, type EventPayload, type FrameWriter, type NdjsonReader, type NdjsonWriter } from "../ipc/index.js";
8
+ import type { CredentialsSnapshot } from "../supervisor/credentials.js";
9
+ import { hashGrants } from "../supervisor/credentials.js";
10
+ import type { SpawnTimeEnv } from "./env-bootstrap.js";
11
+ import type { ChildOutboundMailBridge } from "./outbound-mail-bridge.js";
12
+ import { type WarmAgentCache } from "./warm-agent-cache.js";
13
+ /**
14
+ * `WorkflowAuthorize` closure factory shape. The child's authorize
15
+ * evaluates a `(resource, action)` request against the active
16
+ * credentialsSnapshot for the originating step. The closure used here
17
+ * is intentionally permissive on missing context: the workflow runtime
18
+ * supplies `stepId` from the `AuthorizeContext` it threads through
19
+ * every step's invoker, so a bare-call without a step id is a
20
+ * programming error rather than a security-sensitive path. The closure
21
+ * surfaces it loudly.
22
+ *
23
+ * Read-site: the closure consults a mutable reference so a
24
+ * `grants-updated` control frame swaps the snapshot in place without
25
+ * the caller having to reconstruct the closure.
26
+ */
27
+ export type CredentialsSnapshotRef = {
28
+ current: CredentialsSnapshot | null;
29
+ };
30
+ /**
31
+ * Per-step inference-source table the build path reads through a mutable
32
+ * reference, keyed by stepId. Each value is the step's ordered failover
33
+ * chain (element 0 is the active source). The single-step build resolves
34
+ * its sources from `current` at build time, so a rotation that writes
35
+ * `current` before the first build is reflected in the built agent. A
36
+ * warm agent that is already built does not re-read this ref, so rotating
37
+ * its live sources is out of this ref's scope.
38
+ */
39
+ export type SourcesSnapshotRef = {
40
+ current: Record<string, InferenceSource[]>;
41
+ };
42
+ /**
43
+ * Construct the workflow-level authorize closure backed by a
44
+ * mutable credentialsSnapshot reference.
45
+ *
46
+ * The closure looks up the step's grants by `stepId`, then delegates
47
+ * to the caller-supplied grant evaluator. The evaluator slot exists so
48
+ * the host wires its own grant-rule semantics without leaking the
49
+ * grant-rule grammar into the workflow-host package; tests inject a
50
+ * spy that records inputs.
51
+ */
52
+ export type GrantEvaluator = (input: {
53
+ resource: string;
54
+ action: string;
55
+ stepId: string;
56
+ attempt: number | undefined;
57
+ runId: string | undefined;
58
+ grants: readonly unknown[];
59
+ }) => Promise<AuthzCallResult>;
60
+ export declare function createCredentialsBackedAuthorize(ref: CredentialsSnapshotRef, evaluate: GrantEvaluator): WorkflowAuthorizeFn;
61
+ /**
62
+ * The workflow-host child's drain controller is the production
63
+ * implementation defined in `../drain-controller.ts`. The control-loop
64
+ * calls `requestDrain()` on receipt of the supervisor's `drain`
65
+ * control mail; the controller flips its signal and the runtime body
66
+ * observes the change at its four observation points. The `behaviorFor`
67
+ * resolver consults the loaded `WorkflowDefinition`.
68
+ */
69
+ export type DrainController = WorkflowHostDrainController;
70
+ /**
71
+ * Step-invoker shape the child binds. Widens the workflow-runtime
72
+ * `StepInvoker` with an `onEvent` callback the harness fires for
73
+ * every `InferenceEvent` it emits during the step's run, plus the
74
+ * child's credentials-backed `authorize` closure so the step agent's
75
+ * tool-invocation gate evaluates the per-step grants the supervisor
76
+ * pushed (rather than the host minting its own authorize that cannot
77
+ * see the live credentials snapshot). The child's `buildRuntimeEnv`
78
+ * constructs the per-step `onEvent` closure (wrapping the
79
+ * HMAC-authenticated event-channel sender) and threads both here so
80
+ * every event reaches the supervisor over the wire and every tool
81
+ * call resolves against the agent's grants. The runtime-runtime
82
+ * `StepInvoker` exposed via `WorkflowRuntimeEnv` remains the narrower
83
+ * shape -- the child wraps this binding into a `StepInvoker` inside
84
+ * `buildRuntimeEnv` so the workflow-runtime never sees the
85
+ * host-specific surface.
86
+ *
87
+ * The `warmCache` argument carries the run-loop's per-deployment
88
+ * warm-agent cache (design §3b) when the deployment is a warm candidate,
89
+ * and `undefined` otherwise. The binding forwards it to the step-invoker
90
+ * adapter, which builds-or-reuses the cached agent on a warm cache and
91
+ * keeps instantiate-send-teardown when it is absent. The cache is owned
92
+ * by the run-loop (`runWorkflowChild`), not the binding: the binding
93
+ * only reads it through to the adapter.
94
+ */
95
+ export type ChildStepInvoker = (req: StepInvokeRequest, onEvent: (event: EventPayload) => void, authorize: WorkflowAuthorizeFn, warmCache: WarmAgentCache | undefined, sourcesRef: SourcesSnapshotRef) => Promise<StepInvokeResult>;
96
+ /**
97
+ * Bindings the binary owns: per-deployment substrate identity,
98
+ * principal credentials, the runtime-supplied callbacks the
99
+ * adapter-layer cannot construct from `process.env` alone. Tests
100
+ * supply a fully in-memory bindings object so `runWorkflowChild` runs
101
+ * without touching disk.
102
+ */
103
+ export interface RunWorkflowChildBindings {
104
+ /** Workflow-run substrate (per-deployment workflow-run repo). */
105
+ substrate: SubstrateRepoStore;
106
+ /** Per-deployment workflow-run repo identity. */
107
+ workflowRunRepoId: RepoId;
108
+ /** Workflow-run repo ref the child reads/writes. */
109
+ workflowRunRef: string;
110
+ /**
111
+ * Substrate-shaped principal the child presents on every workflow-run
112
+ * read/write. Per the IPC threat model the child holds no private
113
+ * key of its own; the principal here is a substrate-level identity
114
+ * the host's substrate accepts for `runs/<runId>/` writes.
115
+ */
116
+ principal: Principal;
117
+ /** Workflow-asset repo identity (used to load `workflow.json`). */
118
+ workflowDefinitionRepoId: RepoId;
119
+ /** Workflow-asset ref the deploy orchestrator wrote to. */
120
+ workflowDefinitionRef: string;
121
+ /**
122
+ * Step-invoker callback the runtime body invokes per step. The
123
+ * shape is the workflow-runtime `StepInvoker` widened with an
124
+ * `onEvent` slot so the harness can emit `InferenceEvent` frames
125
+ * up through the event channel for every step invocation. The
126
+ * production binary wires this against `createWorkflowStepInvoker`
127
+ * with the host's per-step env builder; tests inject a stub.
128
+ */
129
+ invokeStep: ChildStepInvoker;
130
+ /**
131
+ * Child-spawn callback the runtime body invokes for `childWorkflow`
132
+ * primitives. The production binary wires this against
133
+ * `createWorkflowSpawnChild`; tests inject a stub.
134
+ */
135
+ spawnChild: SpawnChildWorkflow;
136
+ /** Host-process scheduler singleton. The child consumes the same instance. */
137
+ scheduler: Scheduler;
138
+ /** Grant evaluator wired against the host's grant-rule grammar. */
139
+ evaluateGrants: GrantEvaluator;
140
+ /**
141
+ * Reclaim the local-disk scratch a run produced once the run has
142
+ * reached its terminal status. The host owns the on-disk layout
143
+ * (`<dataDir>/workflow-step-state/<repoId>/runs/<runId>/`), so the
144
+ * teardown lives next to the path construction in the substrate
145
+ * factory and the run-loop merely fires it at the run-completion
146
+ * moment it observes. Invoked ONLY on the cold (non-warm) path -- a
147
+ * warm deployment's single agent reuses one stable workspace across
148
+ * runs, so deleting per run would wipe a live conversation's files
149
+ * mid-stream. A cleanup failure is logged and swallowed: it is a
150
+ * disk-reclamation best-effort, never a correctness gate on the run's
151
+ * terminal status. Optional so tests and the recursive child-workflow
152
+ * adapter (which roots no per-run scratch of its own) can omit it.
153
+ */
154
+ cleanupRunStorage?: (runId: string) => Promise<void>;
155
+ /** Optional director registry; defaults to the canonical built-ins. */
156
+ directors?: DirectorRegistry;
157
+ /** Optional clock override; production wires `() => new Date()`. */
158
+ clock?: () => Date;
159
+ /** Optional id generator override; production wires a monotonic one. */
160
+ newId?: (prefix: string) => string;
161
+ /**
162
+ * Optional bootstrap credentialsSnapshot. The host's production
163
+ * wiring supplies this for multi-step deploys whose snapshot is
164
+ * baked at spawn time; tests can pre-seed it directly. Absent
165
+ * value defers to the first `grants-updated` control frame.
166
+ */
167
+ initialCredentialsSnapshot?: CredentialsSnapshot;
168
+ /**
169
+ * Bootstrap per-step inference-source table (keyed by stepId), parsed
170
+ * from the spawn env by the host's substrate factory. Seeds the
171
+ * mutable `sourcesRef` the build path reads. Absent value defers to an
172
+ * empty table, so a step with no pinned source fails loudly at build
173
+ * rather than resolving a default.
174
+ */
175
+ initialSources?: Record<string, InferenceSource[]>;
176
+ /**
177
+ * Optional override for the child's Ed25519 keypair factory. The
178
+ * child mints a fresh keypair at startup, holds the private half
179
+ * in its own address space, signs every upstream control frame
180
+ * with it, and publishes the public half in the `ready` frame so
181
+ * the supervisor can verify subsequent upstream frames. Production
182
+ * wires this against `@intx/crypto`'s `generateKeyPair`;
183
+ * tests inject a deterministic factory so they can assert on the
184
+ * published key. The supervisor's private key is NEVER threaded
185
+ * into the child -- the child holds only its own private half.
186
+ */
187
+ ipcChildKeyPairFactory?: () => Promise<{
188
+ privateKey: Uint8Array;
189
+ publicKey: Uint8Array;
190
+ }>;
191
+ }
192
+ export interface RunWorkflowChildOpts {
193
+ /** Parsed spawn-time env. */
194
+ env: SpawnTimeEnv;
195
+ /** Control-channel reader (supervisor -> child). */
196
+ controlReader: NdjsonReader;
197
+ /**
198
+ * Control-channel writer back to the supervisor. The child does not
199
+ * sign frames here today (the only upstream control frame, the
200
+ * `ready` signal, rides as an unsigned wire shape because the
201
+ * supervisor receives it on its trusted side). Future upstream
202
+ * frames will adopt the same envelope-and-signature contract the
203
+ * downstream side enforces; the writer slot exists today so the
204
+ * control-channel boundary is symmetric in shape.
205
+ */
206
+ controlWriter: NdjsonWriter;
207
+ /**
208
+ * Event-channel writer (child -> supervisor). The child publishes
209
+ * verified `InferenceEvent` frames the harness emits up through
210
+ * here. Tests inject an in-memory writer; production wires the
211
+ * inherited socketpair fd into a FrameWriter.
212
+ */
213
+ eventWriter: FrameWriter;
214
+ /** Bindings the binary or test harness owns. */
215
+ bindings: RunWorkflowChildBindings;
216
+ /**
217
+ * Optional pre-built upstream control sender the child uses to emit
218
+ * `ready` and (today) `pack.push.request` frames. Defaults to a
219
+ * sender minted internally against the child's own Ed25519 keypair.
220
+ * The process-shaped wrapper `runWorkflowChildFromProcessEnv`
221
+ * supplies a pre-built sender so the same Ed25519-signed surface is
222
+ * shared with the pack-push bridge it constructs against the
223
+ * substrate factory.
224
+ */
225
+ upstreamSender?: ControlChannelSender;
226
+ /**
227
+ * Optional substrate-write bridge whose `handleMergeRequest` and
228
+ * `handleWriteResponse` the child's control loop invokes when the
229
+ * matching downstream frames land. When omitted, inbound
230
+ * `substrate.merge.request` / `substrate.write.response` frames are
231
+ * logged at warn-level and dropped -- the wire shapes are
232
+ * well-formed but nobody on the child side asked for them. The
233
+ * pre-built bridge is the path the process-shaped wrapper takes so
234
+ * the substrate factory's proxy `RepoStore` can resolve writes
235
+ * against it.
236
+ */
237
+ substrateWriteBridge?: SubstrateWriteResponseSink;
238
+ /**
239
+ * Optional outbound-mail bridge (OUTBOUND half of mailbox ownership,
240
+ * §3a). The step agent's mail tools are backed by a transport whose
241
+ * `send` routes through this bridge: it emits an `outbound.message`
242
+ * upstream control frame and resolves the agent's mail-tool `send`
243
+ * once the supervisor's matching `outbound.result` lands. The
244
+ * control loop routes the downstream `outbound.result` frame to the
245
+ * bridge's `handleResult` and invokes `cancelAll` on any exit path so
246
+ * a pending send does not leak an awaiter after the supervisor tears
247
+ * the IPC down. When omitted, inbound `outbound.result` frames are
248
+ * logged at warn-level and dropped -- the wire shape is well-formed
249
+ * but no agent on the child side asked for an outbound send.
250
+ */
251
+ outboundMailBridge?: ChildOutboundMailBridge;
252
+ }
253
+ /**
254
+ * Narrow interface the child's control loop calls when downstream
255
+ * substrate-write frames arrive, plus the `cancelAll` shutdown
256
+ * surface the loop invokes on any exit path. Decouples the loop from
257
+ * the bridge's `submit` side so a test can drop in a recording sink
258
+ * without standing up the full bridge.
259
+ */
260
+ export interface SubstrateWriteResponseSink {
261
+ handleMergeRequest(data: Extract<ControlPayload, {
262
+ type: "substrate.merge.request";
263
+ }>["data"]): void;
264
+ handleWriteResponse(data: Extract<ControlPayload, {
265
+ type: "substrate.write.response";
266
+ }>["data"]): void;
267
+ cancelAll(reason: string): void;
268
+ }
269
+ /**
270
+ * Public result the test harness inspects. Production binaries discard
271
+ * the return value (the process exits when this function resolves);
272
+ * tests assert on the discovered-run ids and the active credentials
273
+ * snapshot to verify the loop's behaviour without scraping logs.
274
+ */
275
+ export interface RunWorkflowChildResult {
276
+ /** RunIds the child resumed at startup. */
277
+ resumedRunIds: readonly string[];
278
+ /** RunIds the child started from `trigger.fired` after `ready`. */
279
+ triggeredRunIds: readonly string[];
280
+ /** Snapshot active at function return. */
281
+ finalCredentialsSnapshot: CredentialsSnapshot | null;
282
+ }
283
+ /**
284
+ * Run the workflow-process child. Resolves once the control channel
285
+ * emits `shutdown` (or ends without a frame, in which case the loop
286
+ * exits cleanly).
287
+ */
288
+ export declare function runWorkflowChild(opts: RunWorkflowChildOpts): Promise<RunWorkflowChildResult>;
289
+ /**
290
+ * Mirror a run's terminal status back to the supervisor over the
291
+ * upstream control channel. Fired once per run from the resume and
292
+ * trigger.fire paths' `complete` continuation. The supervisor's
293
+ * per-cohort terminal broadcaster fans the event out to the dispatch
294
+ * loop and any armed drainTimeout accumulator subscribed for the
295
+ * runId.
296
+ *
297
+ * The frame mirrors the run's committed terminal event: every field --
298
+ * `kind`, `seq`, `at`, and (for `RunFailed`) `error.message` -- is
299
+ * sourced from that event, which is why the frame's `seq` matches the
300
+ * on-disk audit-log entry. `terminalStatus` is only the cross-check: the
301
+ * found event's `kind` must agree with it. A missing terminal event, or
302
+ * one whose kind disagrees, is a runtime producer bug (the runtime
303
+ * commits the terminal event last), and emitting a frame anyway would
304
+ * desync the supervisor from the durable log that `discoverInFlightRuns`
305
+ * reads on resume -- the supervisor would settle a run the on-disk log
306
+ * still shows in-flight. So this throws instead: no frame keeps the
307
+ * supervisor and the durable log agreeing that the run is unsettled, and
308
+ * the next recycle/restart resumes it. The throw propagates to the
309
+ * caller's `complete` continuation, which logs it.
310
+ *
311
+ * Errors flowing out of `upstreamSender.send` are a different case --
312
+ * a transport send failure, logged but not rethrown. The supervisor's
313
+ * dispatch loop is the authoritative settler through its cohort abort
314
+ * signal, so a lost frame surfaces structurally as a wedged dispatch
315
+ * rather than a silent lifecycle failure. The invariant throws above run
316
+ * before the send so that catch never swallows them.
317
+ */
318
+ export declare function emitTerminalEvent(upstreamSender: ControlChannelSender, result: RunResult): Promise<void>;
319
+ /** Re-export the hash helper so callers can verify the snapshot's pin. */
320
+ export { hashGrants };