@boardwalk-labs/runner 0.1.11 → 0.1.13
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/README.md +18 -0
- package/binding.gyp +12 -0
- package/dist/runtime/agent/budget.d.ts +5 -5
- package/dist/runtime/agent/budget.js +8 -8
- package/dist/runtime/agent/model_rates.js +4 -5
- package/dist/runtime/agent/secret_redactor.js +1 -1
- package/dist/runtime/browser_session.d.ts +59 -0
- package/dist/runtime/browser_session.js +188 -0
- package/dist/runtime/browser_session_backend.d.ts +44 -0
- package/dist/runtime/browser_session_backend.js +221 -0
- package/dist/runtime/cancel_watcher.js +1 -1
- package/dist/runtime/credit_watcher.d.ts +1 -1
- package/dist/runtime/credit_watcher.js +5 -5
- package/dist/runtime/freeze_coordinator.d.ts +6 -1
- package/dist/runtime/freeze_coordinator.js +18 -4
- package/dist/runtime/index.d.ts +11 -6
- package/dist/runtime/index.js +96 -41
- package/dist/runtime/leaf_executor.d.ts +2 -2
- package/dist/runtime/program_runner.d.ts +1 -1
- package/dist/runtime/program_runner.js +1 -1
- package/dist/runtime/program_worker.d.ts +9 -3
- package/dist/runtime/program_worker.js +18 -7
- package/dist/runtime/recording_secret_resolver.js +1 -1
- package/dist/runtime/run_abort.js +3 -3
- package/dist/runtime/runner_control_client.d.ts +29 -4
- package/dist/runtime/runner_control_client.js +76 -32
- package/dist/runtime/runtime_flusher.d.ts +1 -1
- package/dist/runtime/runtime_flusher.js +3 -3
- package/dist/runtime/support/index.js +5 -6
- package/dist/runtime/tools/artifacts.js +1 -1
- package/dist/runtime/tools/types.d.ts +5 -5
- package/dist/runtime/tools/types.js +7 -7
- package/dist/runtime/tools/web_search.js +5 -6
- package/dist/runtime/uniqueness_reseed.d.ts +7 -0
- package/dist/runtime/uniqueness_reseed.js +65 -0
- package/dist/runtime/wire/artifact_storage.js +2 -2
- package/dist/runtime/wire/inference_proxy.d.ts +1 -1
- package/dist/runtime/wire/inference_proxy.js +2 -2
- package/dist/runtime/workflow_host.d.ts +47 -1
- package/dist/runtime/workflow_host.js +100 -10
- package/native/reseed.c +52 -0
- package/package.json +15 -7
- package/prebuilds/linux-x64/@boardwalk-labs+runner.node +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { WorkflowHost, AgentOptions, ArtifactBody, ArtifactRef, CallOptions, HumanInputOptions, HumanInputResult, PhaseOptions, SleepArg } from "@boardwalk-labs/workflow/runtime";
|
|
1
|
+
import type { WorkflowHost, AgentOptions, ArtifactBody, ArtifactRef, BrowserSession, BrowserSessionOptions, CallOptions, HumanInputOptions, HumanInputResult, PhaseOptions, SleepArg } from "@boardwalk-labs/workflow/runtime";
|
|
2
|
+
import type { BrowserSessionManager } from "./browser_session.js";
|
|
2
3
|
import { type LeafResume } from "@boardwalk-labs/engine/core";
|
|
3
4
|
import { type JournalSeam, type SuspendSignal } from "./suspension.js";
|
|
4
5
|
import type { FreezeCoordinator } from "./freeze_coordinator.js";
|
|
@@ -52,6 +53,13 @@ export interface ChildDispatcher {
|
|
|
52
53
|
export interface SecretAccessor {
|
|
53
54
|
get(name: string): Promise<string>;
|
|
54
55
|
}
|
|
56
|
+
/** Register-without-release: register a HELD HITL gate so it is
|
|
57
|
+
* answerable while the run keeps running, and poll for the answer. Backed by the broker's
|
|
58
|
+
* `inputs` endpoints. Absent ⇒ a held gate is only answerable once it freezes. */
|
|
59
|
+
export interface HeldInputPort {
|
|
60
|
+
register(seq: number, gate: SuspendSignal["humanInput"]): Promise<unknown>;
|
|
61
|
+
poll(seq: number): Promise<Record<string, unknown>>;
|
|
62
|
+
}
|
|
55
63
|
/** Holds the process for `ms` milliseconds. The seam exists so tests don't wait on real time. An
|
|
56
64
|
* abort fires the hold REJECT (with the signal's RunAbortedError) and clears the timer — so a
|
|
57
65
|
* multi-day sleep aborted early doesn't leave a live timer pinning the event loop open. */
|
|
@@ -96,6 +104,10 @@ export interface WorkerWorkflowHostDeps {
|
|
|
96
104
|
/** Persists a file artifact for the run (→ broker artifact store); resolves to its id + signed
|
|
97
105
|
* download URL. Absent ⇒ artifacts.write is unsupported and the host method rejects clearly. */
|
|
98
106
|
writeArtifact?: (name: string, contentType: string, body: ArtifactBody, metadata: Record<string, unknown> | undefined) => Promise<ArtifactRef>;
|
|
107
|
+
/** Per-run browser-session manager (computer use, the browser tier). Absent ⇒ `computer.openBrowser`
|
|
108
|
+
* is unsupported (no desktop/browser backend) and the host method rejects clearly. When present,
|
|
109
|
+
* `agent({ session })` binds the session's in-VM Playwright MCP to the leaf. */
|
|
110
|
+
browserSessions?: BrowserSessionManager;
|
|
99
111
|
/** Cooperative-cancellation signal for the run (credit exhaustion today; user cancel later). Every
|
|
100
112
|
* hook checks it at entry and unwinds (throws RunAbortedError); the spending/blocking hooks
|
|
101
113
|
* (`agent`/`sleep`/`callWorkflow`) thread it down so an in-flight op stops promptly. Absent ⇒ no
|
|
@@ -135,6 +147,11 @@ export interface WorkerWorkflowHostDeps {
|
|
|
135
147
|
* stream, and work arriving while a freeze is pending queues until the wake.
|
|
136
148
|
*/
|
|
137
149
|
freeze?: FreezeCoordinator;
|
|
150
|
+
/** Register-without-release for HELD human-input gates. Only
|
|
151
|
+
* meaningful alongside `freeze`; absent ⇒ a held gate is answerable only once it freezes. */
|
|
152
|
+
heldInput?: HeldInputPort;
|
|
153
|
+
/** Poll interval for a held gate's answer (default 3s). */
|
|
154
|
+
heldPollIntervalMs?: number;
|
|
138
155
|
/**
|
|
139
156
|
* The highest journaled seq at claim (0 on a fresh run). While re-running seams up to this
|
|
140
157
|
* frontier on a resume, the host is REPLAYING and observability (phase markers, program logs) is
|
|
@@ -147,6 +164,7 @@ export declare class WorkerWorkflowHost implements WorkflowHost {
|
|
|
147
164
|
private readonly sleeper;
|
|
148
165
|
private readonly now;
|
|
149
166
|
private readonly maxSleepMs;
|
|
167
|
+
private readonly heldPollIntervalMs;
|
|
150
168
|
/** Synchronous durable-seam counter + silent-replay live flag (the durable-suspension design). */
|
|
151
169
|
private readonly seq;
|
|
152
170
|
/** Run context + on-demand public-API bearer the SDK `runtime` accessor reads off the host. */
|
|
@@ -167,6 +185,25 @@ export declare class WorkerWorkflowHost implements WorkflowHost {
|
|
|
167
185
|
/** A suspending seam's freeze wait: step out of the "work" count around the park (the wait itself
|
|
168
186
|
* is what the gate waits FOR, not work that blocks it), then rejoin on resume. */
|
|
169
187
|
private freezeWait;
|
|
188
|
+
/**
|
|
189
|
+
* Snapshot-substrate `humanInput()` with REGISTER-WITHOUT-RELEASE.
|
|
190
|
+
* A gate reached while a sibling seam is still in flight HOLDS (the quiescence gate won't freeze
|
|
191
|
+
* yet), but a human must still be able to answer during that hold. So: register the gate with the
|
|
192
|
+
* broker immediately (it surfaces in the inbox/API at once), then race two outcomes —
|
|
193
|
+
* - the answer arrives (brokered poll) while holding ⇒ WITHDRAW the freeze wait and resolve
|
|
194
|
+
* in-process (the run never froze); or
|
|
195
|
+
* - quiescence is reached with no answer ⇒ the wait freezes, and the wake carries the answer.
|
|
196
|
+
* Once frozen the poll is frozen too (same process), so the race is only live during the hold.
|
|
197
|
+
* A register/poll failure degrades to the plain freeze wait — the gate still works, just without
|
|
198
|
+
* the answerable-while-held property.
|
|
199
|
+
*/
|
|
200
|
+
private freezeHumanInput;
|
|
201
|
+
/** Poll the broker for a held gate's answer until it arrives or the wait withdraws. Resolves with
|
|
202
|
+
* the answer value; never rejects the run (a transient poll error just retries next tick). Runs
|
|
203
|
+
* OUTSIDE the quiescence gate (it is not run work — it must not block a freeze). */
|
|
204
|
+
private pollHeldAnswer;
|
|
205
|
+
/** Map a froze/aborted freeze outcome to the gate's answer (or throw on an unexpected abort). */
|
|
206
|
+
private resolveFreezeAnswer;
|
|
170
207
|
/** The wake's answer for one gate key. A wake whose value is missing the parked gate means the
|
|
171
208
|
* control plane and the snapshot disagree about what this run was waiting for — a platform bug,
|
|
172
209
|
* failed loudly, never a retry. */
|
|
@@ -201,6 +238,15 @@ export declare class WorkerWorkflowHost implements WorkflowHost {
|
|
|
201
238
|
scheduleWorkflow(slug: string, input: unknown, opts: ScheduleOptions): Promise<string>;
|
|
202
239
|
getSecret(name: string): Promise<string>;
|
|
203
240
|
writeArtifact(name: string, contentType: string, body: ArtifactBody, metadata: Record<string, unknown> | undefined): Promise<ArtifactRef>;
|
|
241
|
+
/** `computer.openBrowser()`: open a program-owned, in-VM browser session (the browser tier of
|
|
242
|
+
* computer use). Not a durable seam — a session is a live resource, reaped at run end, never
|
|
243
|
+
* journaled/replayed. Absent backend ⇒ a clear "not available" error. */
|
|
244
|
+
openBrowserSession(opts: BrowserSessionOptions | undefined): Promise<BrowserSession>;
|
|
245
|
+
/** Translate `agent({ session })` into the leaf's `mcp`: append the browser session's in-VM
|
|
246
|
+
* Playwright MCP (its http ref, which passes assertHostedMcpAllowed and reaches localhost without
|
|
247
|
+
* the egress proxy) and strip the `session` handle (the engine doesn't understand it). No-op when
|
|
248
|
+
* no session is bound. */
|
|
249
|
+
private bindBrowserSession;
|
|
204
250
|
sleep(arg: SleepArg): Promise<void>;
|
|
205
251
|
/** A short sleep HOLDS the task in-process (cheaper than a release + replay round-trip); a long one
|
|
206
252
|
* (≥ {@link SUSPEND_THRESHOLD_MS}) SUSPENDS — releases the task, and a timer re-dispatches the run
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// WorkerWorkflowHost — the real WorkflowHost the worker installs onto @boardwalk-labs/workflow
|
|
2
2
|
// before running a program (the workflow runtime design). The program's hooks delegate here:
|
|
3
3
|
//
|
|
4
|
-
// agent(prompt, opts) → an ephemeral
|
|
4
|
+
// agent(prompt, opts) → an ephemeral agent leaf (the demoted agent loop)
|
|
5
5
|
// sleep(arg) → an IN-PROCESS hold (hold-and-pay; no checkpoint, no exit)
|
|
6
6
|
// workflows.call(slug, in) → a durable child run (parent holds while it runs)
|
|
7
7
|
// secrets.get(name) → the run's fail-closed secret resolver
|
|
@@ -67,6 +67,7 @@ export class WorkerWorkflowHost {
|
|
|
67
67
|
sleeper;
|
|
68
68
|
now;
|
|
69
69
|
maxSleepMs;
|
|
70
|
+
heldPollIntervalMs;
|
|
70
71
|
/** Synchronous durable-seam counter + silent-replay live flag (the durable-suspension design). */
|
|
71
72
|
seq;
|
|
72
73
|
/** Run context + on-demand public-API bearer the SDK `runtime` accessor reads off the host. */
|
|
@@ -76,6 +77,7 @@ export class WorkerWorkflowHost {
|
|
|
76
77
|
this.sleeper = deps.sleeper ?? new TimerSleepController();
|
|
77
78
|
this.now = deps.now ?? Date.now;
|
|
78
79
|
this.maxSleepMs = deps.maxSleepMs ?? MAX_SLEEP_MS;
|
|
80
|
+
this.heldPollIntervalMs = deps.heldPollIntervalMs ?? 3_000;
|
|
79
81
|
this.seq = new SeamSequencer(deps.replayFrontier ?? 0);
|
|
80
82
|
this.runtime = deps.runtime;
|
|
81
83
|
}
|
|
@@ -118,15 +120,78 @@ export class WorkerWorkflowHost {
|
|
|
118
120
|
}
|
|
119
121
|
/** A suspending seam's freeze wait: step out of the "work" count around the park (the wait itself
|
|
120
122
|
* is what the gate waits FOR, not work that blocks it), then rejoin on resume. */
|
|
121
|
-
async freezeWait(freeze, signal) {
|
|
123
|
+
async freezeWait(freeze, signal, abort) {
|
|
122
124
|
freeze.endWork();
|
|
123
125
|
try {
|
|
124
|
-
return await freeze.suspendingWait(signal);
|
|
126
|
+
return await freeze.suspendingWait(signal, abort);
|
|
125
127
|
}
|
|
126
128
|
finally {
|
|
127
129
|
freeze.beginWork();
|
|
128
130
|
}
|
|
129
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Snapshot-substrate `humanInput()` with REGISTER-WITHOUT-RELEASE.
|
|
134
|
+
* A gate reached while a sibling seam is still in flight HOLDS (the quiescence gate won't freeze
|
|
135
|
+
* yet), but a human must still be able to answer during that hold. So: register the gate with the
|
|
136
|
+
* broker immediately (it surfaces in the inbox/API at once), then race two outcomes —
|
|
137
|
+
* - the answer arrives (brokered poll) while holding ⇒ WITHDRAW the freeze wait and resolve
|
|
138
|
+
* in-process (the run never froze); or
|
|
139
|
+
* - quiescence is reached with no answer ⇒ the wait freezes, and the wake carries the answer.
|
|
140
|
+
* Once frozen the poll is frozen too (same process), so the race is only live during the hold.
|
|
141
|
+
* A register/poll failure degrades to the plain freeze wait — the gate still works, just without
|
|
142
|
+
* the answerable-while-held property.
|
|
143
|
+
*/
|
|
144
|
+
async freezeHumanInput(freeze, signal, key) {
|
|
145
|
+
const held = this.deps.heldInput;
|
|
146
|
+
if (held === undefined) {
|
|
147
|
+
// No held-input port wired: the gate can only be answered once it freezes.
|
|
148
|
+
return this.resolveFreezeAnswer(await this.freezeWait(freeze, signal), key);
|
|
149
|
+
}
|
|
150
|
+
await held.register(signal.seq, signal.humanInput);
|
|
151
|
+
const withdraw = new AbortController();
|
|
152
|
+
const poll = this.pollHeldAnswer(held, signal.seq, key, withdraw.signal);
|
|
153
|
+
const outcome = await Promise.race([
|
|
154
|
+
poll.then((answer) => ({ kind: "answered", answer })),
|
|
155
|
+
this.freezeWait(freeze, signal, withdraw.signal).then((o) => ({ kind: "froze", o })),
|
|
156
|
+
]);
|
|
157
|
+
if (outcome.kind === "answered") {
|
|
158
|
+
withdraw.abort(); // withdraw the still-holding freeze wait (no-op if it already froze)
|
|
159
|
+
return normalizeHumanInputResult(outcome.answer);
|
|
160
|
+
}
|
|
161
|
+
withdraw.abort(); // stop the poll loop; the wake path carries the answer
|
|
162
|
+
return this.resolveFreezeAnswer(outcome.o, key);
|
|
163
|
+
}
|
|
164
|
+
/** Poll the broker for a held gate's answer until it arrives or the wait withdraws. Resolves with
|
|
165
|
+
* the answer value; never rejects the run (a transient poll error just retries next tick). Runs
|
|
166
|
+
* OUTSIDE the quiescence gate (it is not run work — it must not block a freeze). */
|
|
167
|
+
async pollHeldAnswer(held, seq, key, abort) {
|
|
168
|
+
for (;;) {
|
|
169
|
+
if (abort.aborted)
|
|
170
|
+
return await new Promise(() => undefined); // frozen path won: never resolve
|
|
171
|
+
try {
|
|
172
|
+
const answers = await held.poll(seq);
|
|
173
|
+
if (key in answers)
|
|
174
|
+
return answers[key];
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
/* transient — retry next tick */
|
|
178
|
+
}
|
|
179
|
+
await new Promise((resolve) => {
|
|
180
|
+
const t = setTimeout(resolve, this.heldPollIntervalMs);
|
|
181
|
+
abort.addEventListener("abort", () => {
|
|
182
|
+
clearTimeout(t);
|
|
183
|
+
resolve();
|
|
184
|
+
}, { once: true });
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/** Map a froze/aborted freeze outcome to the gate's answer (or throw on an unexpected abort). */
|
|
189
|
+
resolveFreezeAnswer(outcome, key) {
|
|
190
|
+
if (outcome.kind !== "wake") {
|
|
191
|
+
throw new AppError(ErrorCode.INTERNAL_ERROR, `Freeze wait for a human-input gate ended unexpectedly (${outcome.kind})`, { kind: "unexpected_freeze_outcome", key });
|
|
192
|
+
}
|
|
193
|
+
return normalizeHumanInputResult(this.gateAnswer(outcome.wake, key));
|
|
194
|
+
}
|
|
130
195
|
/** The wake's answer for one gate key. A wake whose value is missing the parked gate means the
|
|
131
196
|
* control plane and the snapshot disagree about what this run was waiting for — a platform bug,
|
|
132
197
|
* failed loudly, never a retry. */
|
|
@@ -172,9 +237,13 @@ export class WorkerWorkflowHost {
|
|
|
172
237
|
resume = leafResumeSchema.parse(existing.result);
|
|
173
238
|
}
|
|
174
239
|
}
|
|
240
|
+
// Bind a computer-use session's tools (browser tier ⇒ its in-VM Playwright MCP) if one was passed.
|
|
241
|
+
// Done AFTER the fingerprint so the injected MCP never perturbs determinism (the fingerprint keys
|
|
242
|
+
// on provider/model/prompt/schema only — an mcp/session change must not invalidate a journal hit).
|
|
243
|
+
const effectiveOpts = this.bindBrowserSession(opts);
|
|
175
244
|
for (;;) {
|
|
176
245
|
try {
|
|
177
|
-
const result = await this.deps.leaf.run(prompt,
|
|
246
|
+
const result = await this.deps.leaf.run(prompt, effectiveOpts, this.deps.signal, resume);
|
|
178
247
|
await this.deps.journal?.put({
|
|
179
248
|
seq,
|
|
180
249
|
kind: "agent",
|
|
@@ -256,12 +325,7 @@ export class WorkerWorkflowHost {
|
|
|
256
325
|
};
|
|
257
326
|
const freeze = this.deps.freeze;
|
|
258
327
|
if (freeze !== undefined) {
|
|
259
|
-
|
|
260
|
-
const outcome = await this.freezeWait(freeze, signal);
|
|
261
|
-
if (outcome.kind !== "wake") {
|
|
262
|
-
throw new AppError(ErrorCode.INTERNAL_ERROR, "Suspend aborted surfaced to a human-input seam (the coordinator retries these)", { kind: "unexpected_abort", seq });
|
|
263
|
-
}
|
|
264
|
-
return normalizeHumanInputResult(this.gateAnswer(outcome.wake, key));
|
|
328
|
+
return await this.freezeHumanInput(freeze, signal, key);
|
|
265
329
|
}
|
|
266
330
|
return this.suspend(signal);
|
|
267
331
|
}
|
|
@@ -399,6 +463,32 @@ export class WorkerWorkflowHost {
|
|
|
399
463
|
return ref;
|
|
400
464
|
});
|
|
401
465
|
}
|
|
466
|
+
/** `computer.openBrowser()`: open a program-owned, in-VM browser session (the browser tier of
|
|
467
|
+
* computer use). Not a durable seam — a session is a live resource, reaped at run end, never
|
|
468
|
+
* journaled/replayed. Absent backend ⇒ a clear "not available" error. */
|
|
469
|
+
openBrowserSession(opts) {
|
|
470
|
+
return this.guarded(async () => {
|
|
471
|
+
if (this.deps.browserSessions === undefined) {
|
|
472
|
+
throw new AppError(ErrorCode.VALIDATION_FAILED, "computer.openBrowser is not available in this runtime (no browser backend)");
|
|
473
|
+
}
|
|
474
|
+
return await this.deps.browserSessions.open(opts);
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
/** Translate `agent({ session })` into the leaf's `mcp`: append the browser session's in-VM
|
|
478
|
+
* Playwright MCP (its http ref, which passes assertHostedMcpAllowed and reaches localhost without
|
|
479
|
+
* the egress proxy) and strip the `session` handle (the engine doesn't understand it). No-op when
|
|
480
|
+
* no session is bound. */
|
|
481
|
+
bindBrowserSession(opts) {
|
|
482
|
+
if (opts === undefined || opts.session === undefined)
|
|
483
|
+
return opts;
|
|
484
|
+
const ref = this.deps.browserSessions?.mcpRefFor(opts.session);
|
|
485
|
+
if (ref === null || ref === undefined) {
|
|
486
|
+
throw new AppError(ErrorCode.VALIDATION_FAILED, "agent({ session }) received a browser session that is not open in this run", { kind: "browser_session_not_open" });
|
|
487
|
+
}
|
|
488
|
+
const rest = { ...opts };
|
|
489
|
+
delete rest.session;
|
|
490
|
+
return { ...rest, mcp: [...(rest.mcp ?? []), ref] };
|
|
491
|
+
}
|
|
402
492
|
sleep(arg) {
|
|
403
493
|
return this.guarded(() => this.sleepSeam(arg));
|
|
404
494
|
}
|
package/native/reseed.c
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* bw_reseed — the clause-3 userspace-CSPRNG reseed (SNAPSHOT_UNIQUENESS_CONTRACT).
|
|
3
|
+
*
|
|
4
|
+
* A memory snapshot freezes OpenSSL's DRBG state, so two clones of one base snapshot hand every
|
|
5
|
+
* run byte-identical `crypto.*` output. The kernel CSPRNG diverges across clones (VMGenID), but
|
|
6
|
+
* that reseed never reaches OpenSSL. A pure-JS monkeypatch was proven insufficient (named ESM
|
|
7
|
+
* imports of node:crypto bypass it), so this native step reseeds OpenSSL's DRBG UNDERNEATH every
|
|
8
|
+
* caller — the only robust fix.
|
|
9
|
+
*
|
|
10
|
+
* OpenSSL 3 DRBG chain: the primary DRBG seeds from the OS entropy source; the public + private
|
|
11
|
+
* DRBGs (what RAND_bytes / Node crypto actually draw from) seed from the primary. We reseed the
|
|
12
|
+
* primary first (pulling the now-diverged OS entropy) then the public + private so the fresh
|
|
13
|
+
* entropy propagates immediately, and call RAND_poll() as a belt. Everything is guarded on NULL so
|
|
14
|
+
* a non-default RAND provider degrades to whatever succeeded rather than crashing.
|
|
15
|
+
*
|
|
16
|
+
* Symbols (RAND_get0_*, EVP_RAND_reseed, RAND_poll) come from the OpenSSL that Node links; on a
|
|
17
|
+
* Node build that does not export them the addon fails to load and the JS layer no-ops with a warn
|
|
18
|
+
* (uniqueness_reseed.ts). Linux-only in practice (the snapshot substrate is Linux microVMs).
|
|
19
|
+
*/
|
|
20
|
+
#include <node_api.h>
|
|
21
|
+
#include <openssl/rand.h>
|
|
22
|
+
#include <openssl/evp.h>
|
|
23
|
+
|
|
24
|
+
static int reseed_ctx(EVP_RAND_CTX *ctx) {
|
|
25
|
+
if (ctx == NULL) return 1; /* absent layer: nothing to do, not a failure */
|
|
26
|
+
return EVP_RAND_reseed(ctx, /*prediction_resistance=*/0, NULL, 0, NULL, 0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static napi_value Reseed(napi_env env, napi_callback_info info) {
|
|
30
|
+
int ok = 1;
|
|
31
|
+
/* Reseed the primary FIRST (it pulls fresh OS entropy), then the layers that serve draws. */
|
|
32
|
+
if (!reseed_ctx(RAND_get0_primary(NULL))) ok = 0;
|
|
33
|
+
if (!reseed_ctx(RAND_get0_public(NULL))) ok = 0;
|
|
34
|
+
if (!reseed_ctx(RAND_get0_private(NULL))) ok = 0;
|
|
35
|
+
/* Legacy belt: also nudge the default method's entropy pool. */
|
|
36
|
+
RAND_poll();
|
|
37
|
+
|
|
38
|
+
napi_value result;
|
|
39
|
+
napi_get_boolean(env, ok ? true : false, &result);
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static napi_value Init(napi_env env, napi_value exports) {
|
|
44
|
+
napi_value fn;
|
|
45
|
+
if (napi_create_function(env, "reseed", NAPI_AUTO_LENGTH, Reseed, NULL, &fn) != napi_ok) {
|
|
46
|
+
return exports;
|
|
47
|
+
}
|
|
48
|
+
napi_set_named_property(env, exports, "reseed", fn);
|
|
49
|
+
return exports;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boardwalk-labs/runner",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
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": {
|
|
@@ -37,7 +37,10 @@
|
|
|
37
37
|
"files": [
|
|
38
38
|
"dist",
|
|
39
39
|
"README.md",
|
|
40
|
-
"LICENSE"
|
|
40
|
+
"LICENSE",
|
|
41
|
+
"native",
|
|
42
|
+
"binding.gyp",
|
|
43
|
+
"prebuilds"
|
|
41
44
|
],
|
|
42
45
|
"scripts": {
|
|
43
46
|
"build": "tsc -p tsconfig.build.json",
|
|
@@ -46,19 +49,24 @@
|
|
|
46
49
|
"format": "prettier --write .",
|
|
47
50
|
"format:check": "prettier --check .",
|
|
48
51
|
"test": "vitest run --coverage",
|
|
49
|
-
"test:watch": "vitest"
|
|
52
|
+
"test:watch": "vitest",
|
|
53
|
+
"prebuild": "prebuildify --napi --strip -t 24.0.0"
|
|
50
54
|
},
|
|
51
55
|
"dependencies": {
|
|
52
|
-
"
|
|
53
|
-
"@boardwalk-labs/
|
|
54
|
-
"@
|
|
55
|
-
"
|
|
56
|
+
"@boardwalk-labs/engine": "^0.1.31",
|
|
57
|
+
"@boardwalk-labs/workflow": "^0.1.23",
|
|
58
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
59
|
+
"node-gyp-build": "^4.8.4",
|
|
60
|
+
"tar": "^7.5.16",
|
|
61
|
+
"zod": "^4.0.0"
|
|
56
62
|
},
|
|
57
63
|
"devDependencies": {
|
|
58
64
|
"@eslint/js": "^9.18.0",
|
|
59
65
|
"@types/node": "^24.0.0",
|
|
60
66
|
"@vitest/coverage-v8": "^3.2.6",
|
|
61
67
|
"eslint": "^9.18.0",
|
|
68
|
+
"node-gyp": "^11.0.0",
|
|
69
|
+
"prebuildify": "^6.0.1",
|
|
62
70
|
"prettier": "^3.4.0",
|
|
63
71
|
"typescript": "^5.6.0",
|
|
64
72
|
"typescript-eslint": "^8.20.0",
|
|
Binary file
|