@boardwalk-labs/runner 0.1.9 → 0.1.10
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.
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Duplex } from "node:stream";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
/** Env var naming the inherited relay fd. Set by the guest init; absent everywhere else
|
|
4
|
+
* (Fargate, self-hosted daemon), where the worker env-boots as always. */
|
|
5
|
+
export declare const RELAY_FD_ENV = "BOARDWALK_IDENTITY_RELAY_FD";
|
|
6
|
+
/** The identity payload — the env contract's fields, as JSON instead of env vars. */
|
|
7
|
+
export declare const relayIdentitySchema: z.ZodObject<{
|
|
8
|
+
run_id: z.ZodString;
|
|
9
|
+
control_plane_url: z.ZodString;
|
|
10
|
+
run_token: z.ZodString;
|
|
11
|
+
api_token: z.ZodOptional<z.ZodString>;
|
|
12
|
+
task_cpu_units: z.ZodOptional<z.ZodNumber>;
|
|
13
|
+
byo_providers: z.ZodOptional<z.ZodUnknown>;
|
|
14
|
+
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
15
|
+
}, z.core.$strip>;
|
|
16
|
+
export type RelayIdentity = z.infer<typeof relayIdentitySchema>;
|
|
17
|
+
/**
|
|
18
|
+
* Read the relay fd from `env`, deleting the key (bootstrap-only plumbing; run code has no
|
|
19
|
+
* business seeing it). Returns null when unset — the normal env-boot path.
|
|
20
|
+
*/
|
|
21
|
+
export declare function relayFdFromEnv(env: NodeJS.ProcessEnv): number | null;
|
|
22
|
+
/**
|
|
23
|
+
* Map a relayed identity onto `env` for `capturePlatformContext`. User env lands FIRST and
|
|
24
|
+
* the platform keys LAST, so nothing user-supplied can shadow a platform value.
|
|
25
|
+
*/
|
|
26
|
+
export declare function applyIdentityToEnv(identity: RelayIdentity, env: NodeJS.ProcessEnv): void;
|
|
27
|
+
/** One end of the init↔worker relay. Wraps any Duplex so tests run over in-memory streams. */
|
|
28
|
+
export declare class IdentityRelay {
|
|
29
|
+
private readonly stream;
|
|
30
|
+
private buffer;
|
|
31
|
+
private closed;
|
|
32
|
+
private wake;
|
|
33
|
+
constructor(stream: Duplex);
|
|
34
|
+
/** Announce the pre-identity park. Init forwards this as the base-snapshot gate. */
|
|
35
|
+
announceReady(): void;
|
|
36
|
+
/**
|
|
37
|
+
* Block until init relays the run identity. THIS is the point the base snapshot freezes:
|
|
38
|
+
* everything before it is generic warm-up shared by every run; everything after belongs
|
|
39
|
+
* to one run. Unknown message types are ignored (forward-compatible), malformed lines are
|
|
40
|
+
* logged and skipped (init is trusted; a torn line must not kill PID 1's only worker), but
|
|
41
|
+
* a malformed IDENTITY payload is a hard error — same as a missing env var today.
|
|
42
|
+
*/
|
|
43
|
+
awaitIdentity(): Promise<RelayIdentity>;
|
|
44
|
+
/** Confirm capture. Init acks its host only after this arrives. */
|
|
45
|
+
acceptIdentity(): void;
|
|
46
|
+
private writeLine;
|
|
47
|
+
private readLine;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Open the relay over the inherited fd. Wrapping the fd in a net.Socket also marks it
|
|
51
|
+
* close-on-exec (libuv does this on adoption), so subprocesses the run later spawns cannot
|
|
52
|
+
* inherit the relay and speak to init.
|
|
53
|
+
*/
|
|
54
|
+
export declare function connectIdentityRelayFd(fd: number): IdentityRelay;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// Identity relay — the microVM bootstrap boundary.
|
|
2
|
+
//
|
|
3
|
+
// On Boardwalk's snapshot-based microVM substrate the worker starts BEFORE it has a run: the
|
|
4
|
+
// guest init (PID 1) spawns it warm (Node up, this code loaded, parked pre-identity), the
|
|
5
|
+
// platform snapshots the whole VM, and every run restores from that snapshot and INJECTS the
|
|
6
|
+
// run identity over an in-guest relay instead of container env. The relay is an inherited
|
|
7
|
+
// AF_UNIX socketpair; init tells us its fd via `BOARDWALK_IDENTITY_RELAY_FD`. Wire: one JSON
|
|
8
|
+
// object per LF-terminated line —
|
|
9
|
+
//
|
|
10
|
+
// {"type":"worker_ready"} worker → init the pre-identity park is reached
|
|
11
|
+
// {"type":"identity","payload":{...}} init → worker the run identity (see schema below)
|
|
12
|
+
// {"type":"identity_accepted"} worker → init captured; init acks its host
|
|
13
|
+
//
|
|
14
|
+
// The payload carries exactly the platform env contract (snake_case) plus the resolved user
|
|
15
|
+
// env; `applyIdentityToEnv` maps it onto `process.env` so `capturePlatformContext` runs
|
|
16
|
+
// UNCHANGED afterward — same fields, same capture-and-delete discipline, two transports.
|
|
17
|
+
// The stream stays open after identity: later platform phases speak suspend/wake over it.
|
|
18
|
+
import { Socket } from "node:net";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
import { createLogger } from "./support/index.js";
|
|
21
|
+
const log = createLogger("identity_relay");
|
|
22
|
+
/** Env var naming the inherited relay fd. Set by the guest init; absent everywhere else
|
|
23
|
+
* (Fargate, self-hosted daemon), where the worker env-boots as always. */
|
|
24
|
+
export const RELAY_FD_ENV = "BOARDWALK_IDENTITY_RELAY_FD";
|
|
25
|
+
/** The identity payload — the env contract's fields, as JSON instead of env vars. */
|
|
26
|
+
export const relayIdentitySchema = z.object({
|
|
27
|
+
run_id: z.string().min(1),
|
|
28
|
+
control_plane_url: z.string().min(1),
|
|
29
|
+
run_token: z.string().min(1),
|
|
30
|
+
api_token: z.string().optional(),
|
|
31
|
+
task_cpu_units: z.number().int().positive().optional(),
|
|
32
|
+
/** The BYO inference provider registry, verbatim (stringified into BOARDWALK_BYO_PROVIDERS). */
|
|
33
|
+
byo_providers: z.unknown().optional(),
|
|
34
|
+
/** Resolved NON-secret user env. Platform keys always win over these (applied last). */
|
|
35
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
36
|
+
});
|
|
37
|
+
const relayMessageSchema = z.object({
|
|
38
|
+
type: z.string(),
|
|
39
|
+
payload: z.unknown().optional(),
|
|
40
|
+
});
|
|
41
|
+
/**
|
|
42
|
+
* Read the relay fd from `env`, deleting the key (bootstrap-only plumbing; run code has no
|
|
43
|
+
* business seeing it). Returns null when unset — the normal env-boot path.
|
|
44
|
+
*/
|
|
45
|
+
export function relayFdFromEnv(env) {
|
|
46
|
+
const raw = env[RELAY_FD_ENV];
|
|
47
|
+
Reflect.deleteProperty(env, RELAY_FD_ENV);
|
|
48
|
+
if (raw === undefined || raw.trim().length === 0)
|
|
49
|
+
return null;
|
|
50
|
+
const fd = Number(raw);
|
|
51
|
+
if (!Number.isInteger(fd) || fd < 3) {
|
|
52
|
+
throw new Error(`${RELAY_FD_ENV} must name an inherited fd (>= 3), got ${raw}`);
|
|
53
|
+
}
|
|
54
|
+
return fd;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Map a relayed identity onto `env` for `capturePlatformContext`. User env lands FIRST and
|
|
58
|
+
* the platform keys LAST, so nothing user-supplied can shadow a platform value.
|
|
59
|
+
*/
|
|
60
|
+
export function applyIdentityToEnv(identity, env) {
|
|
61
|
+
for (const [key, value] of Object.entries(identity.env ?? {})) {
|
|
62
|
+
env[key] = value;
|
|
63
|
+
}
|
|
64
|
+
env.RUN_ID = identity.run_id;
|
|
65
|
+
env.BOARDWALK_CONTROL_PLANE_URL = identity.control_plane_url;
|
|
66
|
+
env.BOARDWALK_RUN_TOKEN = identity.run_token;
|
|
67
|
+
if (identity.api_token !== undefined && identity.api_token.length > 0) {
|
|
68
|
+
env.BOARDWALK_API_KEY = identity.api_token;
|
|
69
|
+
}
|
|
70
|
+
if (identity.task_cpu_units !== undefined) {
|
|
71
|
+
env.BOARDWALK_TASK_CPU_UNITS = String(identity.task_cpu_units);
|
|
72
|
+
}
|
|
73
|
+
if (identity.byo_providers !== undefined) {
|
|
74
|
+
env.BOARDWALK_BYO_PROVIDERS = JSON.stringify(identity.byo_providers);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** One end of the init↔worker relay. Wraps any Duplex so tests run over in-memory streams. */
|
|
78
|
+
export class IdentityRelay {
|
|
79
|
+
stream;
|
|
80
|
+
buffer = "";
|
|
81
|
+
closed = false;
|
|
82
|
+
wake = null;
|
|
83
|
+
constructor(stream) {
|
|
84
|
+
this.stream = stream;
|
|
85
|
+
// One persistent listener for the relay's lifetime: a flowing stream DROPS chunks
|
|
86
|
+
// emitted while nobody listens, so attach-per-read would lose lines between reads.
|
|
87
|
+
stream.on("data", (chunk) => {
|
|
88
|
+
this.buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
89
|
+
this.wake?.();
|
|
90
|
+
});
|
|
91
|
+
const onClose = () => {
|
|
92
|
+
this.closed = true;
|
|
93
|
+
this.wake?.();
|
|
94
|
+
};
|
|
95
|
+
stream.on("end", onClose);
|
|
96
|
+
stream.on("error", onClose);
|
|
97
|
+
}
|
|
98
|
+
/** Announce the pre-identity park. Init forwards this as the base-snapshot gate. */
|
|
99
|
+
announceReady() {
|
|
100
|
+
this.writeLine({ type: "worker_ready" });
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Block until init relays the run identity. THIS is the point the base snapshot freezes:
|
|
104
|
+
* everything before it is generic warm-up shared by every run; everything after belongs
|
|
105
|
+
* to one run. Unknown message types are ignored (forward-compatible), malformed lines are
|
|
106
|
+
* logged and skipped (init is trusted; a torn line must not kill PID 1's only worker), but
|
|
107
|
+
* a malformed IDENTITY payload is a hard error — same as a missing env var today.
|
|
108
|
+
*/
|
|
109
|
+
async awaitIdentity() {
|
|
110
|
+
for (;;) {
|
|
111
|
+
const line = await this.readLine();
|
|
112
|
+
let parsed;
|
|
113
|
+
try {
|
|
114
|
+
parsed = JSON.parse(line);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
log.warn("relay_malformed_line_skipped", { length: line.length });
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const message = relayMessageSchema.safeParse(parsed);
|
|
121
|
+
if (!message.success) {
|
|
122
|
+
log.warn("relay_malformed_message_skipped", {});
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (message.data.type !== "identity") {
|
|
126
|
+
log.warn("relay_unexpected_type_ignored", { type: message.data.type });
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const identity = relayIdentitySchema.safeParse(message.data.payload);
|
|
130
|
+
if (!identity.success) {
|
|
131
|
+
throw new Error(`Relayed identity payload is invalid: ${identity.error.message}`);
|
|
132
|
+
}
|
|
133
|
+
return identity.data;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** Confirm capture. Init acks its host only after this arrives. */
|
|
137
|
+
acceptIdentity() {
|
|
138
|
+
this.writeLine({ type: "identity_accepted" });
|
|
139
|
+
}
|
|
140
|
+
writeLine(message) {
|
|
141
|
+
this.stream.write(JSON.stringify(message) + "\n");
|
|
142
|
+
}
|
|
143
|
+
async readLine() {
|
|
144
|
+
for (;;) {
|
|
145
|
+
const newline = this.buffer.indexOf("\n");
|
|
146
|
+
if (newline >= 0) {
|
|
147
|
+
const line = this.buffer.slice(0, newline);
|
|
148
|
+
this.buffer = this.buffer.slice(newline + 1);
|
|
149
|
+
return line;
|
|
150
|
+
}
|
|
151
|
+
if (this.closed) {
|
|
152
|
+
throw new Error("identity relay closed before the run identity arrived");
|
|
153
|
+
}
|
|
154
|
+
await new Promise((resolve) => {
|
|
155
|
+
this.wake = resolve;
|
|
156
|
+
});
|
|
157
|
+
this.wake = null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Open the relay over the inherited fd. Wrapping the fd in a net.Socket also marks it
|
|
163
|
+
* close-on-exec (libuv does this on adoption), so subprocesses the run later spawns cannot
|
|
164
|
+
* inherit the relay and speak to init.
|
|
165
|
+
*/
|
|
166
|
+
export function connectIdentityRelayFd(fd) {
|
|
167
|
+
return new IdentityRelay(new Socket({ fd, readable: true, writable: true }));
|
|
168
|
+
}
|
package/dist/runtime/index.js
CHANGED
|
@@ -35,6 +35,7 @@ import { SecretRedactor } from "./agent/secret_redactor.js";
|
|
|
35
35
|
import { RecordingSecretResolver } from "./recording_secret_resolver.js";
|
|
36
36
|
import { EngineLeafExecutor } from "./leaf_executor.js";
|
|
37
37
|
import { parseByoProviders } from "./direct_inference.js";
|
|
38
|
+
import { applyIdentityToEnv, connectIdentityRelayFd, relayFdFromEnv } from "./identity_relay.js";
|
|
38
39
|
import { WorkerWorkflowHost } from "./workflow_host.js";
|
|
39
40
|
import { runProgramWorker, } from "./program_worker.js";
|
|
40
41
|
import { RunnerControlClient } from "./runner_control_client.js";
|
|
@@ -484,6 +485,20 @@ export function capturePlatformContext(env) {
|
|
|
484
485
|
return { runId, controlPlane, vcpus };
|
|
485
486
|
}
|
|
486
487
|
export async function main() {
|
|
488
|
+
// Relay-mode bootstrap (the snapshot-based microVM substrate): when the guest init handed
|
|
489
|
+
// us an identity relay fd, park here — warm, generic, pre-identity; this await is what the
|
|
490
|
+
// base snapshot freezes — until the run's identity arrives, then map it onto process.env
|
|
491
|
+
// so the capture below is transport-agnostic. Everywhere else (Fargate, self-hosted
|
|
492
|
+
// daemon) the fd is absent and the worker env-boots exactly as before. The post-restore
|
|
493
|
+
// uniqueness reseed will hook in at this boundary, before any run code executes.
|
|
494
|
+
const relayFd = relayFdFromEnv(process.env);
|
|
495
|
+
if (relayFd !== null) {
|
|
496
|
+
const relay = connectIdentityRelayFd(relayFd);
|
|
497
|
+
relay.announceReady();
|
|
498
|
+
const identity = await relay.awaitIdentity();
|
|
499
|
+
applyIdentityToEnv(identity, process.env);
|
|
500
|
+
relay.acceptIdentity();
|
|
501
|
+
}
|
|
487
502
|
// Capture the platform context into private state and remove it from process.env BEFORE anything
|
|
488
503
|
// else — so no user program / agent tool / subprocess we later spawn can read the run token or
|
|
489
504
|
// API token (the run env/credential rules). WORKER_ID / WORKSPACE_ROOT are non-secret infra knobs.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boardwalk-labs/runner",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Boardwalk self-hosted runner: execute cloud-scheduled runs on your own machines. Currently: the canonical runner contract types.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|