@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,126 @@
|
|
|
1
|
+
// IPC crypto primitives: raw Ed25519 sign/verify and HMAC-SHA256 sign/verify.
|
|
2
|
+
//
|
|
3
|
+
// The control channel uses Ed25519 because the supervisor is the only
|
|
4
|
+
// signer and the child must not be able to forge supervisor commands.
|
|
5
|
+
// The event channel uses HMAC-SHA256 because both sides hold the same
|
|
6
|
+
// 32-byte secret and the cost-per-frame of HMAC over per-frame Ed25519
|
|
7
|
+
// is what keeps the event stream affordable at InferenceEvent rates.
|
|
8
|
+
//
|
|
9
|
+
// Ed25519 sign/verify come from `@intx/crypto`, whose raw
|
|
10
|
+
// `signEd25519`/`verifyEd25519` primitives produce and check the bare
|
|
11
|
+
// 64-byte RFC 8032 signature without the PGP packet framing and ASCII
|
|
12
|
+
// armor the package's envelope helpers add — exactly the wire format
|
|
13
|
+
// this channel wants. HMAC-SHA256 uses the Web Crypto `subtle` API; its
|
|
14
|
+
// tag is verified by recomputing the tag with `subtle.sign` and
|
|
15
|
+
// comparing under an explicit constant-time XOR-accumulate rather than
|
|
16
|
+
// `subtle.verify`, because the Web Crypto spec does not guarantee
|
|
17
|
+
// `verify` runs in constant time and this channel keeps ownership of
|
|
18
|
+
// that property. The wire format here is the raw 64-byte Ed25519
|
|
19
|
+
// signature and the raw 32-byte HMAC-SHA256 tag concatenated with the
|
|
20
|
+
// canonical-JSON payload bytes. Anything fancier would just pay PGP
|
|
21
|
+
// overhead per frame.
|
|
22
|
+
import { signEd25519 as ed25519Sign, verifyEd25519 as ed25519Verify, } from "@intx/crypto";
|
|
23
|
+
import { hexEncode } from "@intx/types";
|
|
24
|
+
const ED25519_SIGNATURE_BYTES = 64;
|
|
25
|
+
const ED25519_KEY_BYTES = 32;
|
|
26
|
+
const HMAC_KEY_BYTES = 32;
|
|
27
|
+
const HMAC_TAG_BYTES = 32;
|
|
28
|
+
const CHANNEL_ID_BYTES = 16;
|
|
29
|
+
/**
|
|
30
|
+
* Mint a fresh control-channel HMAC key. Used by the supervisor at
|
|
31
|
+
* spawn time. The child never derives its own key; it receives the
|
|
32
|
+
* 32-byte secret in spawn-time env and never sees the Ed25519 private
|
|
33
|
+
* key the supervisor uses on the control channel.
|
|
34
|
+
*/
|
|
35
|
+
export function generateHmacKey() {
|
|
36
|
+
return crypto.getRandomValues(new Uint8Array(HMAC_KEY_BYTES));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Mint a fresh channelId per the channel-identity contract: 16 bytes
|
|
40
|
+
* from `crypto.getRandomValues`, hex-encoded. The supervisor mints one at
|
|
41
|
+
* every spawn and every recycle, passes it to the child in spawn-time
|
|
42
|
+
* env, and rotates it on the next respawn. The hex encoding keeps the
|
|
43
|
+
* value safe to log and round-trips cleanly through JSON.
|
|
44
|
+
*/
|
|
45
|
+
export function generateChannelId() {
|
|
46
|
+
return hexEncode(crypto.getRandomValues(new Uint8Array(CHANNEL_ID_BYTES)));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Sign the canonicalized envelope bytes with the supervisor's
|
|
50
|
+
* Ed25519 private key. Caller is responsible for canonicalization;
|
|
51
|
+
* this primitive does not see the structured envelope.
|
|
52
|
+
*
|
|
53
|
+
* The private-key bytes are the 32-byte Ed25519 seed. The raw signing
|
|
54
|
+
* primitive lives in `@intx/crypto`; this module wraps it with the
|
|
55
|
+
* channel's fixed-length validation.
|
|
56
|
+
*/
|
|
57
|
+
export async function signEd25519(bytes, privateKeySeed) {
|
|
58
|
+
if (privateKeySeed.length !== ED25519_KEY_BYTES) {
|
|
59
|
+
throw new Error(`IPC Ed25519 private key seed must be ${ED25519_KEY_BYTES} bytes, got ${privateKeySeed.length}`);
|
|
60
|
+
}
|
|
61
|
+
return ed25519Sign(privateKeySeed, bytes);
|
|
62
|
+
}
|
|
63
|
+
export async function verifyEd25519(bytes, signature, publicKey) {
|
|
64
|
+
if (signature.length !== ED25519_SIGNATURE_BYTES) {
|
|
65
|
+
throw new Error(`IPC Ed25519 signature must be ${ED25519_SIGNATURE_BYTES} bytes, got ${signature.length}`);
|
|
66
|
+
}
|
|
67
|
+
if (publicKey.length !== ED25519_KEY_BYTES) {
|
|
68
|
+
throw new Error(`IPC Ed25519 public key must be ${ED25519_KEY_BYTES} bytes, got ${publicKey.length}`);
|
|
69
|
+
}
|
|
70
|
+
return ed25519Verify(bytes, signature, publicKey);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Produce the 32-byte HMAC-SHA256 tag for the given canonicalized
|
|
74
|
+
* envelope bytes under the shared key. Same primitive on both sides
|
|
75
|
+
* of the event channel.
|
|
76
|
+
*/
|
|
77
|
+
export async function signHmac(bytes, key) {
|
|
78
|
+
if (key.length !== HMAC_KEY_BYTES) {
|
|
79
|
+
throw new Error(`IPC HMAC key must be ${HMAC_KEY_BYTES} bytes, got ${key.length}`);
|
|
80
|
+
}
|
|
81
|
+
const cryptoKey = await crypto.subtle.importKey("raw",
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- ArrayBuffer-backed at the call site; Web Crypto's BufferSource type rejects Uint8Array<ArrayBufferLike> under TS 5.9 (microsoft/TypeScript#62240)
|
|
83
|
+
key, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
84
|
+
const tag = await crypto.subtle.sign("HMAC", cryptoKey,
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- ArrayBuffer-backed at the call site; Web Crypto's BufferSource type rejects Uint8Array<ArrayBufferLike> under TS 5.9 (microsoft/TypeScript#62240)
|
|
86
|
+
bytes);
|
|
87
|
+
return new Uint8Array(tag);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Constant-time byte comparison. A non-constant comparison would leak
|
|
91
|
+
* the position of the first mismatched byte through a timing side
|
|
92
|
+
* channel. The XOR accumulate is branch-free over the byte range; the
|
|
93
|
+
* only early return is on a length mismatch, which is not secret-
|
|
94
|
+
* dependent.
|
|
95
|
+
*/
|
|
96
|
+
function constantTimeEqual(a, b) {
|
|
97
|
+
if (a.length !== b.length) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
let acc = 0;
|
|
101
|
+
for (let i = 0; i < a.length; i++) {
|
|
102
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- i is bounds-checked by the loop guard and a.length === b.length above
|
|
103
|
+
acc |= a[i] ^ b[i];
|
|
104
|
+
}
|
|
105
|
+
return acc === 0;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Verify an HMAC tag by recomputing it and comparing in constant time.
|
|
109
|
+
* Deliberately avoids `subtle.verify`, whose constant-time behavior the
|
|
110
|
+
* Web Crypto spec does not guarantee; this channel owns that property
|
|
111
|
+
* via `constantTimeEqual`.
|
|
112
|
+
*/
|
|
113
|
+
export async function verifyHmac(bytes, tag, key) {
|
|
114
|
+
if (tag.length !== HMAC_TAG_BYTES) {
|
|
115
|
+
throw new Error(`IPC HMAC tag must be ${HMAC_TAG_BYTES} bytes, got ${tag.length}`);
|
|
116
|
+
}
|
|
117
|
+
const expected = await signHmac(bytes, key);
|
|
118
|
+
return constantTimeEqual(expected, tag);
|
|
119
|
+
}
|
|
120
|
+
export const IPC_CRYPTO = Object.freeze({
|
|
121
|
+
ED25519_SIGNATURE_BYTES,
|
|
122
|
+
ED25519_KEY_BYTES,
|
|
123
|
+
HMAC_KEY_BYTES,
|
|
124
|
+
HMAC_TAG_BYTES,
|
|
125
|
+
CHANNEL_ID_BYTES,
|
|
126
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validator for the inner envelope shape. `payload` is `unknown` here
|
|
3
|
+
* because each channel narrows it further via its own typed schema
|
|
4
|
+
* (control payload union vs. InferenceEvent forwarded over event).
|
|
5
|
+
*/
|
|
6
|
+
export declare const FrameEnvelope: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
7
|
+
seq: number;
|
|
8
|
+
channelId: string;
|
|
9
|
+
payload: unknown;
|
|
10
|
+
}, {}>;
|
|
11
|
+
export type FrameEnvelope = typeof FrameEnvelope.infer;
|
|
12
|
+
/**
|
|
13
|
+
* Validator for the signed envelope wire shape. `sig` carries the
|
|
14
|
+
* hex-encoded Ed25519 signature (128 hex chars / 64 bytes).
|
|
15
|
+
*/
|
|
16
|
+
export declare const SignedEnvelope: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
17
|
+
envelope: {
|
|
18
|
+
seq: number;
|
|
19
|
+
channelId: string;
|
|
20
|
+
payload: unknown;
|
|
21
|
+
};
|
|
22
|
+
sig: string;
|
|
23
|
+
}, {}>;
|
|
24
|
+
export type SignedEnvelope = typeof SignedEnvelope.infer;
|
|
25
|
+
/**
|
|
26
|
+
* Validator for the MACed envelope wire shape. `mac` carries the
|
|
27
|
+
* hex-encoded HMAC-SHA256 tag (64 hex chars / 32 bytes).
|
|
28
|
+
*/
|
|
29
|
+
export declare const MacedEnvelope: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
30
|
+
envelope: {
|
|
31
|
+
seq: number;
|
|
32
|
+
channelId: string;
|
|
33
|
+
payload: unknown;
|
|
34
|
+
};
|
|
35
|
+
mac: string;
|
|
36
|
+
}, {}>;
|
|
37
|
+
export type MacedEnvelope = typeof MacedEnvelope.infer;
|
|
38
|
+
/**
|
|
39
|
+
* Produce the canonical byte serialization of an envelope. The
|
|
40
|
+
* sender signs these bytes; the receiver verifies against these
|
|
41
|
+
* bytes. Both sides reach the same bytestring deterministically
|
|
42
|
+
* because the JSON serialization runs in insertion order over a
|
|
43
|
+
* fixed-shape object.
|
|
44
|
+
*/
|
|
45
|
+
export declare function encodeEnvelope(envelope: FrameEnvelope): Uint8Array;
|
|
46
|
+
/**
|
|
47
|
+
* Parse a canonical envelope byte serialization back into the
|
|
48
|
+
* structured shape. Used on the receiver side after the per-frame
|
|
49
|
+
* MAC/signature check passes -- a structural failure on a frame
|
|
50
|
+
* whose authentication tag matched is a programming bug at the
|
|
51
|
+
* sender, not a tampering signal.
|
|
52
|
+
*/
|
|
53
|
+
export declare function decodeEnvelope(bytes: Uint8Array): FrameEnvelope;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Frame envelope shared by both IPC channels.
|
|
2
|
+
//
|
|
3
|
+
// Every signed/HMACed frame carries `{ seq, channelId, payload }`
|
|
4
|
+
// inside the bytes the receiver authenticates. `seq` is a monotonic
|
|
5
|
+
// u64 counter the sender maintains per channel; `channelId` is the
|
|
6
|
+
// 16-byte hex identity the supervisor mints at every spawn and rotates
|
|
7
|
+
// at every recycle; `payload` is the channel-specific JSON value.
|
|
8
|
+
//
|
|
9
|
+
// Wire shape on disk for the control channel: one NDJSON line per
|
|
10
|
+
// frame, each line `{ envelope: <canonical-json-of-envelope>, sig:
|
|
11
|
+
// <hex Ed25519 signature> }`. For the event channel: a 4-byte big-
|
|
12
|
+
// endian length prefix, then `{ envelope, mac }` JSON in the same
|
|
13
|
+
// shape with `mac` carrying the hex HMAC tag. Both wires sign the
|
|
14
|
+
// canonical-JSON serialization of the envelope as a single bytestring.
|
|
15
|
+
// The on-wire byte representation of the envelope is exactly what the
|
|
16
|
+
// signer signs and the verifier verifies.
|
|
17
|
+
//
|
|
18
|
+
// Canonical JSON: keys appear in fixed insertion order (seq,
|
|
19
|
+
// channelId, payload). The payload's internal structure is preserved
|
|
20
|
+
// as the sender produced it; no recursive canonicalization is
|
|
21
|
+
// performed because the verifier never compares two structurally
|
|
22
|
+
// different serializations of the same logical value. Senders and
|
|
23
|
+
// receivers see the same bytes by construction (sender computes the
|
|
24
|
+
// signature over the exact serialization it then transmits; receiver
|
|
25
|
+
// verifies the signature over the exact bytes it received).
|
|
26
|
+
import { type } from "arktype";
|
|
27
|
+
/**
|
|
28
|
+
* Validator for the inner envelope shape. `payload` is `unknown` here
|
|
29
|
+
* because each channel narrows it further via its own typed schema
|
|
30
|
+
* (control payload union vs. InferenceEvent forwarded over event).
|
|
31
|
+
*/
|
|
32
|
+
export const FrameEnvelope = type({
|
|
33
|
+
seq: "number",
|
|
34
|
+
channelId: "string",
|
|
35
|
+
payload: "unknown",
|
|
36
|
+
});
|
|
37
|
+
/**
|
|
38
|
+
* Validator for the signed envelope wire shape. `sig` carries the
|
|
39
|
+
* hex-encoded Ed25519 signature (128 hex chars / 64 bytes).
|
|
40
|
+
*/
|
|
41
|
+
export const SignedEnvelope = type({
|
|
42
|
+
envelope: FrameEnvelope,
|
|
43
|
+
sig: "string",
|
|
44
|
+
});
|
|
45
|
+
/**
|
|
46
|
+
* Validator for the MACed envelope wire shape. `mac` carries the
|
|
47
|
+
* hex-encoded HMAC-SHA256 tag (64 hex chars / 32 bytes).
|
|
48
|
+
*/
|
|
49
|
+
export const MacedEnvelope = type({
|
|
50
|
+
envelope: FrameEnvelope,
|
|
51
|
+
mac: "string",
|
|
52
|
+
});
|
|
53
|
+
/**
|
|
54
|
+
* Produce the canonical byte serialization of an envelope. The
|
|
55
|
+
* sender signs these bytes; the receiver verifies against these
|
|
56
|
+
* bytes. Both sides reach the same bytestring deterministically
|
|
57
|
+
* because the JSON serialization runs in insertion order over a
|
|
58
|
+
* fixed-shape object.
|
|
59
|
+
*/
|
|
60
|
+
export function encodeEnvelope(envelope) {
|
|
61
|
+
const ordered = {
|
|
62
|
+
seq: envelope.seq,
|
|
63
|
+
channelId: envelope.channelId,
|
|
64
|
+
payload: envelope.payload,
|
|
65
|
+
};
|
|
66
|
+
return new TextEncoder().encode(JSON.stringify(ordered));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Parse a canonical envelope byte serialization back into the
|
|
70
|
+
* structured shape. Used on the receiver side after the per-frame
|
|
71
|
+
* MAC/signature check passes -- a structural failure on a frame
|
|
72
|
+
* whose authentication tag matched is a programming bug at the
|
|
73
|
+
* sender, not a tampering signal.
|
|
74
|
+
*/
|
|
75
|
+
export function decodeEnvelope(bytes) {
|
|
76
|
+
let parsed;
|
|
77
|
+
try {
|
|
78
|
+
parsed = JSON.parse(new TextDecoder().decode(bytes));
|
|
79
|
+
}
|
|
80
|
+
catch (cause) {
|
|
81
|
+
throw new Error("IPC envelope bytes are not valid JSON", { cause });
|
|
82
|
+
}
|
|
83
|
+
const validated = FrameEnvelope(parsed);
|
|
84
|
+
if (validated instanceof type.errors) {
|
|
85
|
+
throw new Error(`IPC envelope failed validation: ${validated.summary}`);
|
|
86
|
+
}
|
|
87
|
+
return validated;
|
|
88
|
+
}
|