@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
+ }
@@ -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.9",
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": {