@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.
Files changed (43) hide show
  1. package/README.md +18 -0
  2. package/binding.gyp +12 -0
  3. package/dist/runtime/agent/budget.d.ts +5 -5
  4. package/dist/runtime/agent/budget.js +8 -8
  5. package/dist/runtime/agent/model_rates.js +4 -5
  6. package/dist/runtime/agent/secret_redactor.js +1 -1
  7. package/dist/runtime/browser_session.d.ts +59 -0
  8. package/dist/runtime/browser_session.js +188 -0
  9. package/dist/runtime/browser_session_backend.d.ts +44 -0
  10. package/dist/runtime/browser_session_backend.js +221 -0
  11. package/dist/runtime/cancel_watcher.js +1 -1
  12. package/dist/runtime/credit_watcher.d.ts +1 -1
  13. package/dist/runtime/credit_watcher.js +5 -5
  14. package/dist/runtime/freeze_coordinator.d.ts +6 -1
  15. package/dist/runtime/freeze_coordinator.js +18 -4
  16. package/dist/runtime/index.d.ts +11 -6
  17. package/dist/runtime/index.js +96 -41
  18. package/dist/runtime/leaf_executor.d.ts +2 -2
  19. package/dist/runtime/program_runner.d.ts +1 -1
  20. package/dist/runtime/program_runner.js +1 -1
  21. package/dist/runtime/program_worker.d.ts +9 -3
  22. package/dist/runtime/program_worker.js +18 -7
  23. package/dist/runtime/recording_secret_resolver.js +1 -1
  24. package/dist/runtime/run_abort.js +3 -3
  25. package/dist/runtime/runner_control_client.d.ts +29 -4
  26. package/dist/runtime/runner_control_client.js +76 -32
  27. package/dist/runtime/runtime_flusher.d.ts +1 -1
  28. package/dist/runtime/runtime_flusher.js +3 -3
  29. package/dist/runtime/support/index.js +5 -6
  30. package/dist/runtime/tools/artifacts.js +1 -1
  31. package/dist/runtime/tools/types.d.ts +5 -5
  32. package/dist/runtime/tools/types.js +7 -7
  33. package/dist/runtime/tools/web_search.js +5 -6
  34. package/dist/runtime/uniqueness_reseed.d.ts +7 -0
  35. package/dist/runtime/uniqueness_reseed.js +65 -0
  36. package/dist/runtime/wire/artifact_storage.js +2 -2
  37. package/dist/runtime/wire/inference_proxy.d.ts +1 -1
  38. package/dist/runtime/wire/inference_proxy.js +2 -2
  39. package/dist/runtime/workflow_host.d.ts +47 -1
  40. package/dist/runtime/workflow_host.js +100 -10
  41. package/native/reseed.c +52 -0
  42. package/package.json +15 -7
  43. package/prebuilds/linux-x64/@boardwalk-labs+runner.node +0 -0
@@ -19,20 +19,41 @@ export class RunnerControlClient {
19
19
  /** The live bearer. Mutable: on the snapshot substrate a wake carries a FRESH run token (the
20
20
  * frozen one expired while suspended) and the worker swaps it at runtime. */
21
21
  runToken;
22
+ controlTimeoutMs;
23
+ bulkTimeoutMs;
22
24
  constructor(cfg) {
23
25
  this.cfg = cfg;
24
26
  this.base = cfg.baseUrl.replace(/\/+$/, "");
25
27
  this.fetchImpl = cfg.fetchImpl ?? fetch;
26
28
  this.runToken = cfg.runToken;
29
+ this.controlTimeoutMs = cfg.controlTimeoutMs ?? 30_000;
30
+ this.bulkTimeoutMs = cfg.bulkTimeoutMs ?? 300_000;
27
31
  }
28
32
  /** Swap the bearer for a fresh run token (the wake path). Every subsequent call uses it. */
29
33
  swapRunToken(token) {
30
34
  this.runToken = token;
31
35
  }
36
+ /**
37
+ * Every SHORT control call (claim / renew / cancel / credit / journal / …) goes through here so
38
+ * it carries a hard timeout. Without one, a poll frozen mid-flight on the snapshot substrate
39
+ * hangs FOREVER on restore (the socket is dead but never reset), and since a watcher serializes
40
+ * its ticks, one hung tick wedges that watcher — the dead-connections gotcha for the
41
+ * background pollers (lease/cancel/credit), which run on untracked timers the quiescence gate
42
+ * doesn't cover. Also plain robustness: no broker call should hang on a network blip. The
43
+ * streaming inference call is the ONE exception (long-lived NDJSON) and bypasses this.
44
+ */
45
+ controlFetch(url, init) {
46
+ return this.fetchImpl(url, { ...init, signal: AbortSignal.timeout(this.controlTimeoutMs) });
47
+ }
48
+ /** Bulk transfers (artifact + workspace up/download over presigned S3) — a much larger ceiling
49
+ * than a control call, but still bounded so a dead socket can't hang the run. */
50
+ bulkFetch(url, init) {
51
+ return this.fetchImpl(url, { ...init, signal: AbortSignal.timeout(this.bulkTimeoutMs) });
52
+ }
32
53
  /** Claim the run's lease. Returns the run on success, or null when it isn't claimable (409 —
33
54
  * another worker has it, or it isn't pending), which the worker treats as "claim lost". */
34
55
  async claim(workerId, leaseSeconds) {
35
- const res = await this.fetchImpl(this.url("claim"), {
56
+ const res = await this.controlFetch(this.url("claim"), {
36
57
  method: "POST",
37
58
  headers: this.headers(true),
38
59
  body: JSON.stringify({ workerId, leaseSeconds }),
@@ -54,7 +75,7 @@ export class RunnerControlClient {
54
75
  * `leaseUntil`, or null when the lease was lost (409 — another worker reclaimed the run), which
55
76
  * the worker treats as "stop". */
56
77
  async renewLease(workerId, leaseSeconds) {
57
- const res = await this.fetchImpl(this.url("renew"), {
78
+ const res = await this.controlFetch(this.url("renew"), {
58
79
  method: "POST",
59
80
  headers: this.headers(true),
60
81
  body: JSON.stringify({ workerId, leaseSeconds }),
@@ -70,7 +91,7 @@ export class RunnerControlClient {
70
91
  * whose lease expired and whose run was reclaimed + re-dispatched to a new owner), so a
71
92
  * hung/partitioned worker that later recovers can't clobber the live run or revive a terminal one. */
72
93
  async finalize(status, output, workerId) {
73
- const res = await this.fetchImpl(this.url("finalize"), {
94
+ const res = await this.controlFetch(this.url("finalize"), {
74
95
  method: "POST",
75
96
  headers: this.headers(true),
76
97
  body: JSON.stringify({ status, output, workerId }),
@@ -81,7 +102,7 @@ export class RunnerControlClient {
81
102
  /** Look up a durable-seam journal entry by its seq (the durable-suspension design), or null on a replay miss
82
103
  * (404). The broker joins a parked agent leaf's answers into the result server-side. */
83
104
  async journalGet(seq) {
84
- const res = await this.fetchImpl(this.url(`journal/${encodeURIComponent(String(seq))}`), {
105
+ const res = await this.controlFetch(this.url(`journal/${encodeURIComponent(String(seq))}`), {
85
106
  method: "GET",
86
107
  headers: this.headers(false),
87
108
  });
@@ -94,7 +115,7 @@ export class RunnerControlClient {
94
115
  /** Record a RESOLVED seam result (idempotent on the run + seq server-side; a resolved entry is
95
116
  * immutable). The broker writes the memoized value the next replay returns. */
96
117
  async journalPut(entry) {
97
- const res = await this.fetchImpl(this.url("journal"), {
118
+ const res = await this.controlFetch(this.url("journal"), {
98
119
  method: "POST",
99
120
  headers: this.headers(true),
100
121
  body: JSON.stringify(entry),
@@ -106,7 +127,7 @@ export class RunnerControlClient {
106
127
  * entry + a human-input request row for HITL, or the wake time for a long sleep), flips the run to
107
128
  * its suspended status, and releases the lease — transactionally. No finalize; a wake re-dispatches. */
108
129
  async suspend(signal, workerId) {
109
- const res = await this.fetchImpl(this.url("suspend"), {
130
+ const res = await this.controlFetch(this.url("suspend"), {
110
131
  method: "POST",
111
132
  headers: this.headers(true),
112
133
  body: JSON.stringify({ ...signal, workerId }),
@@ -123,7 +144,7 @@ export class RunnerControlClient {
123
144
  }
124
145
  /** Fetch the run's pinned manifest + program source, or null when the version is missing (404). */
125
146
  async getVersion() {
126
- const res = await this.fetchImpl(this.url("version"), {
147
+ const res = await this.controlFetch(this.url("version"), {
127
148
  method: "GET",
128
149
  headers: this.headers(false),
129
150
  });
@@ -136,7 +157,7 @@ export class RunnerControlClient {
136
157
  /** Book a runtime-seconds DELTA (the worker's RuntimeFlusher → broker). `identifier` makes a
137
158
  * retried/duplicate flush idempotent; distinct per-flush ids sum into the run's runtime total. */
138
159
  async reportUsage(runtimeSeconds, identifier) {
139
- const res = await this.fetchImpl(this.url("usage"), {
160
+ const res = await this.controlFetch(this.url("usage"), {
140
161
  method: "POST",
141
162
  headers: this.headers(true),
142
163
  body: JSON.stringify({ runtimeSeconds, identifier }),
@@ -145,10 +166,10 @@ export class RunnerControlClient {
145
166
  throw await brokerError(res, "usage");
146
167
  }
147
168
  /** Report a token-usage delta for incremental in-run metering (the usage flusher → broker). The
148
- * broker gates on the run's per-connection `billed_by_boardwalk` server-side + meters to Stripe;
149
- * `identifier` makes a retried/duplicate flush idempotent. Satisfies {@link TokenUsageReporter}. */
169
+ * broker gates on the run's per-connection `billed_by_boardwalk` server-side + meters usage to the
170
+ * platform; `identifier` makes a retried/duplicate flush idempotent. Satisfies {@link TokenUsageReporter}. */
150
171
  async meterTokens(input) {
151
- const res = await this.fetchImpl(this.url("usage/tokens"), {
172
+ const res = await this.controlFetch(this.url("usage/tokens"), {
152
173
  method: "POST",
153
174
  headers: this.headers(true),
154
175
  body: JSON.stringify(input),
@@ -157,9 +178,9 @@ export class RunnerControlClient {
157
178
  throw await brokerError(res, "usage/tokens");
158
179
  }
159
180
  /** Check whether the run's org is still funded (the CreditWatcher → broker). The broker reads the
160
- * live Stripe balance server-side; `false` means out of credit (the watcher then aborts the run). */
181
+ * live billing balance server-side; `false` means out of credit (the watcher then aborts the run). */
161
182
  async checkCredit() {
162
- const res = await this.fetchImpl(this.url("credit"), {
183
+ const res = await this.controlFetch(this.url("credit"), {
163
184
  method: "GET",
164
185
  headers: this.headers(false),
165
186
  });
@@ -171,7 +192,7 @@ export class RunnerControlClient {
171
192
  * cancelled the run (the broker flipped it to `cancelling`/`cancelled`); the watcher then aborts the
172
193
  * run. Brokered because the runner holds no DB/Redis — this replaces the unreachable Redis channel. */
173
194
  async checkCancelled() {
174
- const res = await this.fetchImpl(this.url("cancel"), {
195
+ const res = await this.controlFetch(this.url("cancel"), {
175
196
  method: "GET",
176
197
  headers: this.headers(false),
177
198
  });
@@ -179,10 +200,33 @@ export class RunnerControlClient {
179
200
  throw await brokerError(res, "cancel");
180
201
  return (await res.json()).cancelRequested;
181
202
  }
203
+ /** Register-without-release: register a HELD HITL gate's request row
204
+ * so it is answerable while the run keeps running — no suspend. Idempotent. Returns whether a new
205
+ * gate was registered. */
206
+ async registerInput(seq, gate) {
207
+ const res = await this.controlFetch(this.url("inputs"), {
208
+ method: "POST",
209
+ headers: this.headers(true),
210
+ body: JSON.stringify({ seq, humanInput: gate }),
211
+ });
212
+ if (res.status !== 200)
213
+ throw await brokerError(res, "register-input");
214
+ return (await res.json()).registered;
215
+ }
216
+ /** Poll the resolved answers for a held gate at `seq` (empty until a human responds). */
217
+ async pollInputAnswers(seq) {
218
+ const res = await this.controlFetch(this.url(`inputs/${encodeURIComponent(String(seq))}`), {
219
+ method: "GET",
220
+ headers: this.headers(false),
221
+ });
222
+ if (res.status !== 200)
223
+ throw await brokerError(res, "poll-inputs");
224
+ return (await res.json()).answers;
225
+ }
182
226
  /** Mint a presigned GET URL to restore this workflow's last `/workspace` snapshot (workspace
183
- * persistence, §5). `null` when the run isn't eligible (not opted-in, or self-hosted). */
227
+ * persistence). `null` when the run isn't eligible (not opted-in, or self-hosted). */
184
228
  async workspaceHydrateUrl() {
185
- const res = await this.fetchImpl(this.url("workspace/hydrate-url"), {
229
+ const res = await this.controlFetch(this.url("workspace/hydrate-url"), {
186
230
  method: "POST",
187
231
  headers: this.headers(false),
188
232
  });
@@ -195,7 +239,7 @@ export class RunnerControlClient {
195
239
  * (the worker tars BEFORE requesting the URL) — the broker records it for the org storage counter +
196
240
  * daily meter; the snapshot overwrites one per-workflow key, so it's the workflow's full footprint. */
197
241
  async workspacePersistUrl(sizeBytes) {
198
- const res = await this.fetchImpl(this.url("workspace/persist-url"), {
242
+ const res = await this.controlFetch(this.url("workspace/persist-url"), {
199
243
  method: "POST",
200
244
  headers: this.headers(true),
201
245
  body: JSON.stringify({ sizeBytes }),
@@ -208,7 +252,7 @@ export class RunnerControlClient {
208
252
  /** Download bytes from a presigned S3 URL (workspace hydrate). `null` on 404 (no snapshot yet —
209
253
  * e.g. the workflow's first run); throws on any other non-2xx. Goes straight to S3, not the broker. */
210
254
  async downloadBytes(url) {
211
- const res = await this.fetchImpl(url, { method: "GET" });
255
+ const res = await this.bulkFetch(url, { method: "GET" });
212
256
  if (res.status === 404)
213
257
  return null;
214
258
  if (!res.ok)
@@ -219,7 +263,7 @@ export class RunnerControlClient {
219
263
  * third-party-verifiable token (gated server-side on `permissions.id_token: "write"`) — used to
220
264
  * federate into the org's OWN cloud (AWS/GCP). DIFFERENT from this client's run token. */
221
265
  async requestOidcToken(audience) {
222
- const res = await this.fetchImpl(this.url("oidc/token"), {
266
+ const res = await this.controlFetch(this.url("oidc/token"), {
223
267
  method: "POST",
224
268
  headers: this.headers(true),
225
269
  body: JSON.stringify({ audience }),
@@ -231,7 +275,7 @@ export class RunnerControlClient {
231
275
  /** Publish a batch of live agent-event frames (the SSE live-tail source) — the broker publishes
232
276
  * them to the run's Redis channel server-side, so the runner holds no Redis credential. */
233
277
  async publishTelemetry(frames) {
234
- const res = await this.fetchImpl(this.url("telemetry"), {
278
+ const res = await this.controlFetch(this.url("telemetry"), {
235
279
  method: "POST",
236
280
  headers: this.headers(true),
237
281
  body: JSON.stringify({ frames }),
@@ -242,7 +286,7 @@ export class RunnerControlClient {
242
286
  /** Store a run artifact through the broker (which holds the S3 credential + neutralizes the served
243
287
  * content type server-side). Returns the catalog id + a signed download URL. */
244
288
  async writeArtifact(input) {
245
- const res = await this.fetchImpl(this.url("artifacts"), {
289
+ const res = await this.controlFetch(this.url("artifacts"), {
246
290
  method: "POST",
247
291
  headers: this.headers(true),
248
292
  body: JSON.stringify(input),
@@ -255,7 +299,7 @@ export class RunnerControlClient {
255
299
  * broker derives the S3 key + neutralizes/pins the served content type; it returns the upload URL +
256
300
  * required headers + the `s3Key` to echo back at commit. No catalog row exists yet. */
257
301
  async presignArtifact(input) {
258
- const res = await this.fetchImpl(this.url("artifacts/presign"), {
302
+ const res = await this.controlFetch(this.url("artifacts/presign"), {
259
303
  method: "POST",
260
304
  headers: this.headers(true),
261
305
  body: JSON.stringify(input),
@@ -268,7 +312,7 @@ export class RunnerControlClient {
268
312
  * presign response and MUST be sent verbatim — the content type is pinned into the signature, so
269
313
  * S3 rejects a mismatch. This call goes straight to S3, not the broker. */
270
314
  async uploadBytes(url, headers, body) {
271
- const res = await this.fetchImpl(url, { method: "PUT", headers, body });
315
+ const res = await this.bulkFetch(url, { method: "PUT", headers, body });
272
316
  if (!res.ok)
273
317
  throw await brokerError(res, "artifacts-upload");
274
318
  }
@@ -277,7 +321,7 @@ export class RunnerControlClient {
277
321
  * broker re-validates the run prefix + re-neutralizes the content type, then returns the catalog id
278
322
  * + a signed download URL. */
279
323
  async commitArtifact(input) {
280
- const res = await this.fetchImpl(this.url("artifacts/commit"), {
324
+ const res = await this.controlFetch(this.url("artifacts/commit"), {
281
325
  method: "POST",
282
326
  headers: this.headers(true),
283
327
  body: JSON.stringify(input),
@@ -288,7 +332,7 @@ export class RunnerControlClient {
288
332
  }
289
333
  /** List the artifacts this run has produced. */
290
334
  async listArtifacts() {
291
- const res = await this.fetchImpl(this.url("artifacts"), {
335
+ const res = await this.controlFetch(this.url("artifacts"), {
292
336
  method: "GET",
293
337
  headers: this.headers(false),
294
338
  });
@@ -298,7 +342,7 @@ export class RunnerControlClient {
298
342
  }
299
343
  /** Mint a fresh signed download URL for one of this run's artifacts. */
300
344
  async signArtifactUrl(artifactId, ttlSeconds) {
301
- const res = await this.fetchImpl(this.url(`artifacts/${encodeURIComponent(artifactId)}/signed-url`), {
345
+ const res = await this.controlFetch(this.url(`artifacts/${encodeURIComponent(artifactId)}/signed-url`), {
302
346
  method: "POST",
303
347
  headers: this.headers(true),
304
348
  body: JSON.stringify({ ttlSeconds }),
@@ -310,7 +354,7 @@ export class RunnerControlClient {
310
354
  /** Resolve an org secret the run's manifest allows (the program's `secrets.get`). The broker
311
355
  * enforces the allowlist + returns the value; a forbidden/missing secret surfaces as a throw. */
312
356
  async resolveSecret(name) {
313
- const res = await this.fetchImpl(this.url("secrets/resolve"), {
357
+ const res = await this.controlFetch(this.url("secrets/resolve"), {
314
358
  method: "POST",
315
359
  headers: this.headers(true),
316
360
  body: JSON.stringify({ name }),
@@ -325,7 +369,7 @@ export class RunnerControlClient {
325
369
  * A 403 (no active connection / non-allowlisted host) degrades to `{ accessToken: null, hint }` so
326
370
  * the engine surfaces a clean failure instead of a thrown 500 mid-run; the token is never logged. */
327
371
  async mcpToken(serverUrl, invalidateToken) {
328
- const res = await this.fetchImpl(this.url("mcp/token"), {
372
+ const res = await this.controlFetch(this.url("mcp/token"), {
329
373
  method: "POST",
330
374
  headers: this.headers(true),
331
375
  body: JSON.stringify({
@@ -344,7 +388,7 @@ export class RunnerControlClient {
344
388
  /** Proxy a web_search through the broker (which holds the Tavily key) — the runner sends the
345
389
  * query, the broker calls Tavily and returns the results. */
346
390
  async webSearch(input) {
347
- const res = await this.fetchImpl(this.url("tools/web_search"), {
391
+ const res = await this.controlFetch(this.url("tools/web_search"), {
348
392
  method: "POST",
349
393
  headers: this.headers(true),
350
394
  body: JSON.stringify(input),
@@ -355,7 +399,7 @@ export class RunnerControlClient {
355
399
  }
356
400
  /** Create (or idempotently re-attach to) a child run for `workflows.call`. */
357
401
  async startChild(slug, input) {
358
- const res = await this.fetchImpl(this.url("children"), {
402
+ const res = await this.controlFetch(this.url("children"), {
359
403
  method: "POST",
360
404
  headers: this.headers(true),
361
405
  body: JSON.stringify({ slug, input }),
@@ -367,7 +411,7 @@ export class RunnerControlClient {
367
411
  }
368
412
  /** Provision a durable schedule for `workflows.schedule`; returns the new schedule's id. */
369
413
  async scheduleWorkflow(slug, input, spec) {
370
- const res = await this.fetchImpl(this.url("schedules"), {
414
+ const res = await this.controlFetch(this.url("schedules"), {
371
415
  method: "POST",
372
416
  headers: this.headers(true),
373
417
  body: JSON.stringify({ slug, input, ...spec }),
@@ -378,7 +422,7 @@ export class RunnerControlClient {
378
422
  }
379
423
  /** Poll a child run's status/output, or null when it isn't this run's child (404). */
380
424
  async getChild(childRunId) {
381
- const res = await this.fetchImpl(this.url(`children/${encodeURIComponent(childRunId)}`), {
425
+ const res = await this.controlFetch(this.url(`children/${encodeURIComponent(childRunId)}`), {
382
426
  method: "GET",
383
427
  headers: this.headers(false),
384
428
  });
@@ -30,7 +30,7 @@ export declare class RuntimeFlusher {
30
30
  private excludedMs;
31
31
  constructor(deps: RuntimeFlusherDeps);
32
32
  /** Book everything unbilled right now (the pre-freeze flush: suspended time must never appear as
33
- * billed runtime, so the tail is booked BEFORE the snapshot — SUSPEND_POLICY billing rule). */
33
+ * billed runtime, so the tail is booked BEFORE the snapshot — the suspension billing rule). */
34
34
  flushNow(): Promise<void>;
35
35
  /** Exclude `ms` of wall-clock from billing — the frozen window on a wake (computed from the wake's
36
36
  * authoritative wall clock, since the guest's own clock was stopped). Never lets elapsed go
@@ -1,10 +1,10 @@
1
1
  // RuntimeFlusher — meters a run's held-task runtime as periodic DELTAS, instead of one charge at
2
- // terminal (the platform spec). The heartbeat counterpart to the credit / cancel
2
+ // terminal. The heartbeat counterpart to the credit / cancel
3
3
  // / lease watchers.
4
4
  //
5
5
  // Why: runtime used to be booked once, at terminal (`now - sessionStart`). That left two holes — a run
6
6
  // that never terminates (a perpetual loop) NEVER billed its runtime (and the credit watcher, reading
7
- // Stripe, never saw the burn so it couldn't stop it); and a session that crashed before finalizing had
7
+ // the billing balance, never saw the burn so it couldn't stop it); and a session that crashed before finalizing had
8
8
  // its whole runtime lost. Metering on a timer closes both: runtime is billed as it accrues, the credit
9
9
  // watcher sees it, and a crashed session keeps the deltas it already flushed (the fresh session bills
10
10
  // its own — distinct ids sum, so the restart isn't double-charged or under-charged for the overlap-free
@@ -36,7 +36,7 @@ export class RuntimeFlusher {
36
36
  this.deps = deps;
37
37
  }
38
38
  /** Book everything unbilled right now (the pre-freeze flush: suspended time must never appear as
39
- * billed runtime, so the tail is booked BEFORE the snapshot — SUSPEND_POLICY billing rule). */
39
+ * billed runtime, so the tail is booked BEFORE the snapshot — the suspension billing rule). */
40
40
  async flushNow() {
41
41
  await this.flush(false);
42
42
  }
@@ -1,11 +1,10 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- // Runtime support shims — the small slice of the platform's `@boardwalk/common` +
3
- // `domain/authz` the worker runtime depends on, ported verbatim where it matters
4
- // (AppError taxonomy) and minimally where it doesn't (the logger: structured JSON to
5
- // stdout, same call surface as the platform's Powertools child logger).
2
+ // Runtime support shims — a small, self-contained slice of the error taxonomy and logging the
3
+ // worker runtime depends on. The AppError taxonomy mirrors the platform's error codes so brokered
4
+ // responses map cleanly; the logger emits structured JSON to stdout.
6
5
  import { randomUUID } from "node:crypto";
7
- // ---- errors (ported from boardwalk-backend src/common/errors.ts; keep in sync until the
8
- // backend consumes this package and the copy there is deleted) ----
6
+ // ---- errors (the shared AppError taxonomy mirrors the platform's error codes so brokered
7
+ // responses deserialize to the same shape) ----
9
8
  export var ErrorCode;
10
9
  (function (ErrorCode) {
11
10
  ErrorCode["VALIDATION_FAILED"] = "VALIDATION_FAILED";
@@ -1,4 +1,4 @@
1
- // artifacts — persist + reference run outputs (the platform spec). Available to any agent
1
+ // artifacts — persist + reference run outputs. Available to any agent
2
2
  // (no sandbox required). Three operations:
3
3
  // * write(name, content_type, body) → store a file under the run's prefix; returns id + a signed URL.
4
4
  // * list() → artifacts produced in THIS run.
@@ -3,7 +3,7 @@ import type { AuthContext } from "../support/index.js";
3
3
  import type { SecretRefManifest } from "../wire/manifest.js";
4
4
  /**
5
5
  * Per-invocation context the worker threads through to a tool. Equivalent to
6
- * Strands' `ToolContext` but typed in Boardwalk's idiom (AuthContext + run id +
6
+ * a tool framework's `ToolContext` but typed in Boardwalk's idiom (AuthContext + run id +
7
7
  * secret resolver scoped to this run's permissions).
8
8
  */
9
9
  export interface ToolContext {
@@ -27,7 +27,7 @@ export interface SecretResolver {
27
27
  * * Return a `ToolReturn` body — synchronous "normal" tools (echo, http,
28
28
  * web_search). The body lands in the conversation as the assistant's
29
29
  * tool-result block.
30
- * * Return a `ToolControlSignal` — the legacy Strands-level sleep/workflows.call path. The
30
+ * * Return a `ToolControlSignal` — the legacy tool-level sleep/workflows.call path. The
31
31
  * current JS-body worker exposes these as program SDK hooks instead; agent() leaves strip
32
32
  * control-flow tools before registering model-callable tools.
33
33
  */
@@ -41,8 +41,8 @@ export interface BoardwalkTool<TInput = unknown, TOutput = unknown> {
41
41
  /**
42
42
  * Zod schema for the tool's SUCCESS output (the `TOutput` shape — never the
43
43
  * control-signal branch). The adapter validates the tool's return value
44
- * against this before it lands in the LLM conversation, per the code standards
45
- * §2.1/§8.3 (LLM-facing output is treated like untrusted external input).
44
+ * against this before it lands in the LLM conversation
45
+ * (LLM-facing output is treated like untrusted external input).
46
46
  */
47
47
  readonly outputSchema: z.ZodType<TOutput>;
48
48
  /**
@@ -64,7 +64,7 @@ export interface BoardwalkTool<TInput = unknown, TOutput = unknown> {
64
64
  normalizeInput?(raw: unknown): unknown;
65
65
  /**
66
66
  * Invoke the tool. `input` (LLM-supplied) comes first, `ctx` (worker-supplied)
67
- * second — see the `Tool` naming note in SPEC §9. Returns either the typed
67
+ * second — see the `Tool` naming note in SPEC.md. Returns either the typed
68
68
  * `TOutput` body or a `ToolControlSignal` (sleep / workflows.call) the worker
69
69
  * intercepts before serialization.
70
70
  */
@@ -1,15 +1,15 @@
1
1
  // Boardwalk's internal Tool contract. Concrete tools (`echo`, `http`, `web_search`, `sleep`,
2
- // `workflows.call`, …) implement this interface; the worker registers a `ZodTool`-flavored
3
- // adapter into Strands at agent construction time.
2
+ // `workflows.call`, …) implement this interface; the worker registers a schema-typed
3
+ // adapter into the agent leaf at construction time.
4
4
  //
5
- // Why a Boardwalk-side interface instead of using `ZodTool` directly?
6
- // * Tests can drive tool logic without instantiating Strands.
5
+ // Why a Boardwalk-side interface instead of the leaf's native tool type?
6
+ // * Tests can drive tool logic without instantiating the agent leaf.
7
7
  // * Control-signal tools (`sleep`, `workflows.call`) need to return a
8
8
  // specially-shaped value the worker interprets — they don't really "run";
9
- // they bubble a signal up to the engine. Our adapter (lands with the worker
10
- // in Phase 10.5) translates between this interface and the Strands surface.
9
+ // they bubble a signal up to the engine. Our adapter translates between
10
+ // this interface and the agent leaf's tool surface.
11
11
  //
12
- // Per the platform spec: tools NEVER see secret values. Secrets resolve to
12
+ // Tools NEVER see secret values. Secrets resolve to
13
13
  // short-lived bearer tokens, ARNs, etc. via the injected `SecretResolver`.
14
14
  export function isControlSignal(value) {
15
15
  return (typeof value === "object" &&
@@ -1,12 +1,11 @@
1
1
  // web_search — wraps the Tavily API (https://docs.tavily.com/docs/api-reference).
2
2
  //
3
- // Per the platform spec: Tavily is Boardwalk's default web-search provider. API
4
- // key fetched from Secrets Manager (`boardwalk/<stage>/tavily/api-key`) and
5
- // cached at the module level per container, NEVER appearing in the agent's
6
- // conversation.
3
+ // The platform's default web-search provider. API
4
+ // key fetched from the org's secret store and cached at the module level per
5
+ // container, NEVER appearing in the agent's conversation.
7
6
  //
8
- // 429 from Tavily → `AppError(RATE_LIMIT)` so Strands' retry strategy handles
9
- // backoff. 5xx → `AppError(TOOL_ERROR)`. Network-level fetch failures fall
7
+ // 429 from the provider → `AppError(RATE_LIMIT)` so the agent loop's retry strategy
8
+ // handles backoff. 5xx → `AppError(TOOL_ERROR)`. Network-level fetch failures fall
10
9
  // back to `TOOL_ERROR`.
11
10
  import { z } from "zod";
12
11
  import { AppError, ErrorCode } from "../support/index.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Reseed the userspace CSPRNG at a restore boundary (fresh-run identity injection AND every wake —
3
+ * clause 3), before any run code draws randomness. Idempotent and side-effect-free beyond the
4
+ * reseed; safe to call repeatedly. Returns whether the reseed ran (false = addon unavailable, the
5
+ * degraded no-op path). Never throws.
6
+ */
7
+ export declare function reseedUserspaceCsprng(): boolean;
@@ -0,0 +1,65 @@
1
+ // Snapshot-uniqueness reseed — clause 3 of the SNAPSHOT_UNIQUENESS_CONTRACT, the one sliver of
2
+ // the determinism contract that survives the journal's deletion.
3
+ //
4
+ // A memory snapshot freezes OpenSSL's DRBG, so two clones of one base snapshot hand every run
5
+ // byte-identical `crypto.*` output (proven, contract). The kernel CSPRNG diverges across
6
+ // clones (VMGenID) but that reseed never reaches OpenSSL, and a pure-JS monkeypatch was proven
7
+ // insufficient (named ESM imports of `node:crypto` bypass it — measured 2026-07-09). So the robust
8
+ // fix is a native step that reseeds OpenSSL's DRBG UNDERNEATH every caller: the `bw_reseed` addon
9
+ // (native/reseed.c) chains EVP_RAND_reseed over the primary/public/private DRBGs from the
10
+ // (VMGenID-diverged) OS entropy.
11
+ //
12
+ // This module is the platform-owned JS entry point. It loads the prebuilt addon opportunistically
13
+ // and NO-OPS with a warning when it is unavailable (a platform with no prebuild — e.g. the ARM64
14
+ // Fargate worker, where there is no snapshot and thus nothing to reseed; or a self-host on an
15
+ // unsupported arch). It never throws: a reseed that cannot run must not fail a run.
16
+ import { createRequire } from "node:module";
17
+ import { fileURLToPath } from "node:url";
18
+ import { dirname, join } from "node:path";
19
+ import { createLogger } from "./support/index.js";
20
+ const log = createLogger("uniqueness_reseed");
21
+ /** undefined = not yet attempted; null = unavailable (no prebuild for this platform). */
22
+ let addon;
23
+ function loadAddon() {
24
+ if (addon !== undefined)
25
+ return addon;
26
+ try {
27
+ const require = createRequire(import.meta.url);
28
+ // node-gyp-build resolves prebuilds/<platform>-<arch>/*.node (production) or build/Release
29
+ // (a local dev build), throwing when neither exists for this platform.
30
+ const nodeGypBuild = require("node-gyp-build");
31
+ // dist/runtime/uniqueness_reseed.js → the package root two levels up (holds prebuilds/ + build/).
32
+ const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
33
+ addon = nodeGypBuild(pkgRoot);
34
+ }
35
+ catch (err) {
36
+ addon = null;
37
+ log.warn("uniqueness_reseed_addon_unavailable", {
38
+ error: err instanceof Error ? err.message : String(err),
39
+ });
40
+ }
41
+ return addon;
42
+ }
43
+ /**
44
+ * Reseed the userspace CSPRNG at a restore boundary (fresh-run identity injection AND every wake —
45
+ * clause 3), before any run code draws randomness. Idempotent and side-effect-free beyond the
46
+ * reseed; safe to call repeatedly. Returns whether the reseed ran (false = addon unavailable, the
47
+ * degraded no-op path). Never throws.
48
+ */
49
+ export function reseedUserspaceCsprng() {
50
+ const a = loadAddon();
51
+ if (a === null)
52
+ return false;
53
+ try {
54
+ const ok = a.reseed();
55
+ if (!ok)
56
+ log.warn("uniqueness_reseed_partial", {});
57
+ return ok;
58
+ }
59
+ catch (err) {
60
+ log.error("uniqueness_reseed_failed", {
61
+ error: err instanceof Error ? err.message : String(err),
62
+ });
63
+ return false;
64
+ }
65
+ }
@@ -1,5 +1,5 @@
1
1
  // Artifact storage policy — the server-side rules for turning an agent's artifact-write request into
2
- // a stored S3 object (the platform spec). These used to live in the `artifacts` TOOL,
2
+ // a stored S3 object. These used to live in the `artifacts` TOOL,
3
3
  // which runs on the UNTRUSTED runner; under the Runner Credential Broker (the Runner Credential Broker model) the
4
4
  // broker owns them so a malicious runner can't bypass content-type neutralization or escape its
5
5
  // run's key prefix. Pure functions — unit-tested exhaustively.
@@ -10,7 +10,7 @@ import * as nodePath from "node:path";
10
10
  export function workspaceS3Key(orgId, workflowId) {
11
11
  return `orgs/${orgId}/workflows/${workflowId}/workspace.tar.gz`;
12
12
  }
13
- // ---- run-artifact retention tag (Phase 18.5 S3 lifecycle safety net) ----
13
+ // ---- run-artifact retention tag (S3 lifecycle safety net) ----
14
14
  //
15
15
  // Run artifacts share the `orgs/{org}/...` prefix with workspace snapshots (`.../workflows/...`) and
16
16
  // program artifacts (`.../program-artifacts/...`), and S3 lifecycle prefix filters can't express
@@ -57,7 +57,7 @@ export declare function parseInferenceRequest(body: string): InferenceProxyReque
57
57
  /** Serialize one streamed text delta as a single NDJSON line (trailing "\n" included). */
58
58
  export declare function serializeDeltaFrame(text: string): string;
59
59
  /** Serialize the single terminal turn result as one NDJSON line. `costMicros` is the turn's EXACT
60
- * upstream cost (OpenRouter `usage.cost` × 1e6) on the managed lane — 0 for BYO or when unavailable.
60
+ * upstream cost (the managed provider's per-request cost × 1e6) on the managed lane — 0 for BYO or when unavailable.
61
61
  * The worker feeds it to the budget guardrail so `max_usd` tracks real spend, not a token estimate. */
62
62
  export declare function serializeResultFrame(turn: ChatTurn, modelRef: string, costMicros?: number): string;
63
63
  /** Serialize a terminal error as a single NDJSON line. */
@@ -13,7 +13,7 @@
13
13
  //
14
14
  // Transport: the response is **newline-delimited JSON** (one frame per line). A model turn streams
15
15
  // many text deltas; NDJSON lets the runner consume them incrementally over the raw socket (the
16
- // buffered REST path can't stream — the platform spec). Each frame is one of:
16
+ // buffered REST path can't stream). Each frame is one of:
17
17
  // { "t": "delta", "text": <string> } — a streamed assistant-text chunk
18
18
  // { "t": "result", "turn": <ChatTurn>, "modelRef": <string>, "costMicros": <number> } — terminal turn
19
19
  // { "t": "error", "error": { code, message } } — a terminal model/broker error
@@ -77,7 +77,7 @@ export function serializeDeltaFrame(text) {
77
77
  return `${JSON.stringify({ t: "delta", text })}\n`;
78
78
  }
79
79
  /** Serialize the single terminal turn result as one NDJSON line. `costMicros` is the turn's EXACT
80
- * upstream cost (OpenRouter `usage.cost` × 1e6) on the managed lane — 0 for BYO or when unavailable.
80
+ * upstream cost (the managed provider's per-request cost × 1e6) on the managed lane — 0 for BYO or when unavailable.
81
81
  * The worker feeds it to the budget guardrail so `max_usd` tracks real spend, not a token estimate. */
82
82
  export function serializeResultFrame(turn, modelRef, costMicros = 0) {
83
83
  return `${JSON.stringify({ t: "result", turn, modelRef, costMicros })}\n`;