@boardwalk-labs/runner 0.2.16 → 0.3.0-alpha.1

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 (39) hide show
  1. package/README.md +1 -1
  2. package/dist/bin.js +5 -0
  3. package/dist/runtime/agent/budget.d.ts +43 -24
  4. package/dist/runtime/agent/budget.js +108 -57
  5. package/dist/runtime/agent/events.d.ts +1 -1
  6. package/dist/runtime/broker_child_dispatcher.d.ts +5 -2
  7. package/dist/runtime/broker_child_dispatcher.js +24 -6
  8. package/dist/runtime/budget_gate.d.ts +58 -21
  9. package/dist/runtime/budget_gate.js +227 -46
  10. package/dist/runtime/host_capabilities.d.ts +4 -0
  11. package/dist/runtime/host_capabilities.js +25 -0
  12. package/dist/runtime/host_server.d.ts +137 -0
  13. package/dist/runtime/host_server.js +562 -0
  14. package/dist/runtime/identity_relay.js +19 -9
  15. package/dist/runtime/index.d.ts +38 -0
  16. package/dist/runtime/index.js +109 -35
  17. package/dist/runtime/leaf_executor.d.ts +7 -4
  18. package/dist/runtime/leaf_executor.js +11 -6
  19. package/dist/runtime/program_runner.d.ts +62 -42
  20. package/dist/runtime/program_runner.js +156 -101
  21. package/dist/runtime/program_worker.d.ts +22 -11
  22. package/dist/runtime/program_worker.js +26 -48
  23. package/dist/runtime/python_program.d.ts +68 -0
  24. package/dist/runtime/python_program.js +270 -0
  25. package/dist/runtime/run_context.d.ts +13 -0
  26. package/dist/runtime/run_context.js +77 -0
  27. package/dist/runtime/runner_control_client.d.ts +23 -0
  28. package/dist/runtime/runner_control_client.js +69 -147
  29. package/dist/runtime/screen_capture_backend.d.ts +3 -0
  30. package/dist/runtime/screen_capture_backend.js +17 -2
  31. package/dist/runtime/shell_exec.d.ts +17 -0
  32. package/dist/runtime/shell_exec.js +144 -0
  33. package/dist/runtime/support/index.d.ts +18 -0
  34. package/dist/runtime/support/index.js +38 -6
  35. package/dist/runtime/wire/inference_proxy.js +1 -1
  36. package/dist/runtime/wire/run.d.ts +5 -2
  37. package/dist/runtime/workflow_host.d.ts +53 -7
  38. package/dist/runtime/workflow_host.js +82 -42
  39. package/package.json +4 -3
@@ -16,9 +16,9 @@ export type RunActor = {
16
16
  type: "event";
17
17
  subscription_id: string;
18
18
  source_run_id: string;
19
- event: string;
19
+ source_workflow_id: string;
20
+ event_type: string;
20
21
  event_chain_depth: number;
21
- user_id?: string;
22
22
  };
23
23
  export interface Run {
24
24
  id: string;
@@ -26,6 +26,9 @@ export interface Run {
26
26
  workflowId: string;
27
27
  workflowVersionId: string;
28
28
  environmentId: string | null;
29
+ /** 1-based crash-restart-from-top counter (context.attempt). Optional: an older backend's
30
+ * claim payload may predate the column; the honest fallback is 1. */
31
+ attempt?: number;
29
32
  parentRunId: string | null;
30
33
  actor: RunActor;
31
34
  triggerKind: string;
@@ -1,4 +1,5 @@
1
- import type { WorkflowHost, AgentOptions, ArtifactBody, ArtifactRef, BrowserSession, BrowserSessionOptions, CallOptions, HumanInputOptions, HumanInputResult, PhaseOptions, SleepArg } from "@boardwalk-labs/workflow/runtime";
1
+ import type { AgentOptions, ArtifactBody, ArtifactRef, BrowserSession, BrowserSessionOptions, CallOptions, HumanInputOptions, HumanInputResult, PhaseOptions, ShellResult, SleepArg, UsageSnapshot } from "@boardwalk-labs/workflow/runtime";
2
+ import type { ShellOptions } from "@boardwalk-labs/workflow";
2
3
  import type { BrowserSessionManager } from "./browser_session.js";
3
4
  import { type LeafResume } from "@boardwalk-labs/engine/core";
4
5
  import { type SuspendSignal } from "./suspension.js";
@@ -30,11 +31,19 @@ export interface ScheduleOptions {
30
31
  timezone?: string;
31
32
  idempotencyKey?: string;
32
33
  }
33
- /** A child run's terminal-relevant state, as the start/poll seams return it. */
34
+ /** A child run's terminal-relevant state, as the start/poll seams return it. `outputSchema` is
35
+ * the callee's PINNED version's stored `output_schema` (what lets the SDK revive a typed child's
36
+ * return); null = untyped callee or a payload that predates the field. */
34
37
  export interface ChildResult {
35
38
  childRunId: string;
36
39
  status: string;
37
40
  output: unknown;
41
+ outputSchema: Record<string, unknown> | null;
42
+ }
43
+ /** What `workflows.call` resolves: the completed child's output + the callee's output schema. */
44
+ export interface ChildCallOutput {
45
+ output: unknown;
46
+ outputSchema: Record<string, unknown> | null;
38
47
  }
39
48
  /** Dispatches child runs: `call` holds in-process + returns the completed child's output (the
40
49
  * hold path); `start`/`poll` back the snapshot-substrate callWorkflow seam (start once, freeze
@@ -42,7 +51,11 @@ export interface ChildResult {
42
51
  * provisions a durable future/recurring run → schedule id. The `signal` lets a hold/start abort
43
52
  * promptly when the parent run is cancelled. */
44
53
  export interface ChildDispatcher {
45
- call(slug: string, input: unknown, opts: CallOptions | undefined, signal?: AbortSignal): Promise<unknown>;
54
+ call(slug: string, input: unknown, opts: CallOptions | undefined, signal?: AbortSignal): Promise<ChildCallOutput>;
55
+ /** Poll a child run's current state (used by the freeze-wake path to fetch the callee's
56
+ * output schema — the wake payload carries the output but not the schema); null = not this
57
+ * run's child. */
58
+ poll(childRunId: string): Promise<ChildResult | null>;
46
59
  /** Start (or idempotently re-attach to) a child run; resolves its current state. */
47
60
  start(slug: string, input: unknown, opts: CallOptions | undefined, signal?: AbortSignal): Promise<ChildResult>;
48
61
  run(slug: string, input: unknown, opts: CallOptions | undefined): Promise<string>;
@@ -145,8 +158,26 @@ export interface WorkerWorkflowHostDeps {
145
158
  heldInput?: HeldInputPort;
146
159
  /** Poll interval for a held gate's answer (default 3s). */
147
160
  heldPollIntervalMs?: number;
161
+ /** Backs the protocol's `shell` capability (shell_exec's runShell in production). Absent ⇒
162
+ * `shell()` is unsupported in this runtime and rejects clearly. */
163
+ shell?: (cmd: string, opts: ShellOptions | undefined) => Promise<ShellResult>;
164
+ /** Live budget state for `usage.get` (the BudgetMeter's usageSnapshot in production). Absent ⇒
165
+ * `usage.get()` is unsupported in this runtime and rejects clearly. */
166
+ usage?: () => UsageSnapshot;
167
+ /**
168
+ * Budget clearance (docs/SUSPEND_POLICY.md Decision 3), awaited at the host's blocking/spending
169
+ * capability seams — `sleep`, `shell`, `workflows.call` — in addition to the leaf executor's
170
+ * `streamModel` seam. `usd`/`tokens` only move at model calls, but `max_compute_seconds` burns
171
+ * continuously, so a breach between model calls parks at whichever capability the program touches
172
+ * next (the park-at-next-seam contract; see budget_gate.ts). Runs INSIDE `guarded()` so the park's
173
+ * `budgetClearance` work-accounting (endWork around the freeze wait) balances, exactly like the
174
+ * leaf seam. Absent ⇒ no host-seam parks (tests / legacy paths).
175
+ */
176
+ budgetGate?: {
177
+ clear(): Promise<void>;
178
+ };
148
179
  }
149
- export declare class WorkerWorkflowHost implements WorkflowHost {
180
+ export declare class WorkerWorkflowHost {
150
181
  private readonly deps;
151
182
  private readonly sleeper;
152
183
  private readonly now;
@@ -207,10 +238,16 @@ export declare class WorkerWorkflowHost implements WorkflowHost {
207
238
  * host always implements it. */
208
239
  humanInput(opts: HumanInputOptions): Promise<HumanInputResult>;
209
240
  private humanInputSeam;
241
+ /** Wait a registered gate out on whichever substrate the run has: freeze in place on the
242
+ * snapshot fleet, or (self-hosted runner / local dev) hold the live process and poll until a
243
+ * person answers. The one place the freeze-vs-hold decision for a gate is made. */
244
+ private awaitGate;
210
245
  /**
211
246
  * Park the run at a BUDGET gate and resolve with the responder's answer (docs/SUSPEND_POLICY.md
212
- * Decision 3). Called by the leaf executor's `streamModel` seam when the run's `max_usd` cap is
213
- * breached, i.e. from INSIDE an in-flight `agent()` which drives two deliberate differences from
247
+ * Decision 3). Called when any budget cap (`max_usd` / `max_tokens` / `max_compute_seconds`) is
248
+ * breached by the leaf executor's `streamModel` seam (from INSIDE an in-flight `agent()`) and by
249
+ * the host's own `sleep`/`shell`/`workflows.call` seams (from inside their `guarded()` bodies).
250
+ * Either way the caller is already work-tracked, which drives two deliberate differences from
214
251
  * {@link humanInputSeam}:
215
252
  *
216
253
  * - **No `guarded()` wrapper.** The enclosing `agent()` seam already counted this leaf as work via
@@ -232,7 +269,7 @@ export declare class WorkerWorkflowHost implements WorkflowHost {
232
269
  }): Promise<HumanInputResult>;
233
270
  /** Absolute wake time for a `humanInput({ timeout })`, or null when there is none / it's unparseable. */
234
271
  private timeoutExpiry;
235
- callWorkflow(slug: string, input: unknown, opts: CallOptions | undefined): Promise<unknown>;
272
+ callWorkflow(slug: string, input: unknown, opts: CallOptions | undefined): Promise<ChildCallOutput>;
236
273
  /** The `workflows.call` seam: start the child once (idempotently — the child's run row is the
237
274
  * durable memo, so a crash-restarted parent re-attaches instead of re-spawning). A non-terminal
238
275
  * child suspends the parent `waiting_for_child` on the snapshot substrate (the wake carries the
@@ -260,6 +297,15 @@ export declare class WorkerWorkflowHost implements WorkflowHost {
260
297
  * wake resolves this very await, heap intact. Without a freeze substrate EVERY sleep holds,
261
298
  * whatever its length (the no-substrate rule: snapshot or hold, never replay). */
262
299
  private sleepSeam;
300
+ /** The protocol's `shell` capability. Runs under the same abort/freeze gate as every hook, so
301
+ * a freeze never snapshots around an unguarded seam and an aborted run rejects promptly. */
302
+ shell(cmd: string, opts: ShellOptions | undefined): Promise<ShellResult>;
303
+ /** Live budget state for `usage.get` — every dimension `{spent, cap, remaining}`. */
304
+ usage(): Promise<UsageSnapshot>;
305
+ /** `auth.idToken(audience)` — minted per call by the broker via {@link RuntimeContext}. */
306
+ idToken(audience: string): Promise<string>;
307
+ /** `auth.apiToken()` — the run's short-lived, manifest-scoped public-API bearer. */
308
+ apiToken(): Promise<string>;
263
309
  /** Resolve any {@link SleepArg} shape to a millisecond duration from now. */
264
310
  private resolveSleepMs;
265
311
  }
@@ -1,5 +1,5 @@
1
- // WorkerWorkflowHost — the real WorkflowHost the worker installs onto @boardwalk-labs/workflow
2
- // before running a program (the workflow runtime design). The program's hooks delegate here:
1
+ // WorkerWorkflowHost — the runner's capability layer behind the host protocol (the
2
+ // workflow-format redesign). The protocol server's `HostCapabilities` seam delegates here:
3
3
  //
4
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)
@@ -281,11 +281,11 @@ export class WorkerWorkflowHost {
281
281
  humanInput(opts) {
282
282
  return this.guarded(() => this.humanInputSeam(opts));
283
283
  }
284
- async humanInputSeam(opts) {
284
+ humanInputSeam(opts) {
285
285
  const seq = this.seq.next();
286
286
  const key = opts.key ?? `seam-${String(seq)}`;
287
287
  const expiresAt = this.timeoutExpiry(opts.timeout);
288
- const signal = {
288
+ return this.awaitGate({
289
289
  reason: "human_input",
290
290
  seq,
291
291
  humanInput: {
@@ -296,18 +296,24 @@ export class WorkerWorkflowHost {
296
296
  // A timeout only matters with a wake to fire at; carry onTimeout alongside expiresAt.
297
297
  ...(expiresAt !== null ? { expiresAt, onTimeout: opts.onTimeout ?? "fail" } : {}),
298
298
  },
299
- };
299
+ });
300
+ }
301
+ /** Wait a registered gate out on whichever substrate the run has: freeze in place on the
302
+ * snapshot fleet, or (self-hosted runner / local dev) hold the live process and poll until a
303
+ * person answers. The one place the freeze-vs-hold decision for a gate is made. */
304
+ async awaitGate(signal) {
300
305
  const freeze = this.deps.freeze;
301
306
  if (freeze !== undefined) {
302
- return await this.freezeHumanInput(freeze, signal, key);
307
+ return await this.freezeHumanInput(freeze, signal, signal.humanInput.key);
303
308
  }
304
- // Hold path: register the gate and poll until a person answers.
305
- return normalizeHumanInputResult(await this.holdForAnswer(seq, signal.humanInput));
309
+ return normalizeHumanInputResult(await this.holdForAnswer(signal.seq, signal.humanInput));
306
310
  }
307
311
  /**
308
312
  * Park the run at a BUDGET gate and resolve with the responder's answer (docs/SUSPEND_POLICY.md
309
- * Decision 3). Called by the leaf executor's `streamModel` seam when the run's `max_usd` cap is
310
- * breached, i.e. from INSIDE an in-flight `agent()` which drives two deliberate differences from
313
+ * Decision 3). Called when any budget cap (`max_usd` / `max_tokens` / `max_compute_seconds`) is
314
+ * breached by the leaf executor's `streamModel` seam (from INSIDE an in-flight `agent()`) and by
315
+ * the host's own `sleep`/`shell`/`workflows.call` seams (from inside their `guarded()` bodies).
316
+ * Either way the caller is already work-tracked, which drives two deliberate differences from
311
317
  * {@link humanInputSeam}:
312
318
  *
313
319
  * - **No `guarded()` wrapper.** The enclosing `agent()` seam already counted this leaf as work via
@@ -323,20 +329,12 @@ export class WorkerWorkflowHost {
323
329
  * the inbox, and is answered by the same machinery as any other gate. No timeout: an unanswered
324
330
  * budget gate is aged out by the control plane's inactive-cancel reaper, not by a wake we schedule.
325
331
  */
326
- async budgetClearance(gate) {
327
- const seq = this.seq.next();
328
- const key = BUDGET_GATE_KEY;
329
- const signal = {
332
+ budgetClearance(gate) {
333
+ return this.awaitGate({
330
334
  reason: "budget",
331
- seq,
332
- humanInput: { key, prompt: gate.prompt, inputSpec: gate.inputSpec },
333
- };
334
- const freeze = this.deps.freeze;
335
- if (freeze !== undefined) {
336
- return await this.freezeHumanInput(freeze, signal, key);
337
- }
338
- // No freeze substrate (self-hosted runner / local dev): hold the live process until answered.
339
- return normalizeHumanInputResult(await this.holdForAnswer(seq, signal.humanInput));
335
+ seq: this.seq.next(),
336
+ humanInput: { key: BUDGET_GATE_KEY, prompt: gate.prompt, inputSpec: gate.inputSpec },
337
+ });
340
338
  }
341
339
  /** Absolute wake time for a `humanInput({ timeout })`, or null when there is none / it's unparseable. */
342
340
  timeoutExpiry(timeout) {
@@ -344,7 +342,12 @@ export class WorkerWorkflowHost {
344
342
  return ms === null ? null : this.now() + ms;
345
343
  }
346
344
  callWorkflow(slug, input, opts) {
347
- return this.guarded(() => this.callWorkflowSeam(slug, input, opts));
345
+ return this.guarded(async () => {
346
+ // Park BEFORE dispatching the child: a budget-breached parent doesn't get to spawn more
347
+ // (org-billed) work until a responder clears the breach.
348
+ await this.deps.budgetGate?.clear();
349
+ return this.callWorkflowSeam(slug, input, opts);
350
+ });
348
351
  }
349
352
  /** The `workflows.call` seam: start the child once (idempotently — the child's run row is the
350
353
  * durable memo, so a crash-restarted parent re-attaches instead of re-spawning). A non-terminal
@@ -358,7 +361,7 @@ export class WorkerWorkflowHost {
358
361
  }
359
362
  const child = await this.deps.children.start(slug, input, opts, this.deps.signal);
360
363
  if (child.status === "completed")
361
- return child.output;
364
+ return { output: child.output, outputSchema: child.outputSchema };
362
365
  if (child.status === "failed" || child.status === "cancelled") {
363
366
  throw new AppError(ErrorCode.VALIDATION_FAILED, `Called workflow "${slug}" ${child.status} (run ${child.childRunId})`, { childRunId: child.childRunId, status: child.status });
364
367
  }
@@ -377,38 +380,39 @@ export class WorkerWorkflowHost {
377
380
  if (outcome.wake.kind !== "workflow_call" || woken === undefined) {
378
381
  throw new AppError(ErrorCode.INTERNAL_ERROR, `Wake value does not carry the awaited child run (got kind "${outcome.wake.kind}")`, { kind: "wake_mismatch", childRunId: child.childRunId });
379
382
  }
380
- if (woken.status === "completed")
381
- return woken.output;
383
+ if (woken.status === "completed") {
384
+ // The wake payload carries the finalized child's OUTPUT but not the callee's schema; one
385
+ // post-wake poll fetches it so a typed child's return still revives. Best-effort: a poll
386
+ // failure degrades to plain JSON (null schema), never fails the parent.
387
+ let outputSchema = null;
388
+ try {
389
+ outputSchema = (await this.deps.children.poll(woken.run_id))?.outputSchema ?? null;
390
+ }
391
+ catch {
392
+ outputSchema = null;
393
+ }
394
+ return { output: woken.output, outputSchema };
395
+ }
382
396
  throw new AppError(ErrorCode.VALIDATION_FAILED, `Called workflow "${slug}" ${woken.status} (run ${woken.run_id})`, { childRunId: woken.run_id, status: woken.status });
383
397
  }
384
398
  /** Fire-and-forget trigger of another workflow; resolves to the new run's id (no hold/poll). */
385
399
  runWorkflow(slug, input, opts) {
386
- return this.guarded(async () => {
387
- const id = await this.deps.children.run(slug, input, opts);
388
- return id;
389
- });
400
+ return this.guarded(() => this.deps.children.run(slug, input, opts));
390
401
  }
391
402
  /** Provision a durable schedule (one-shot/recurring) that fires the target later; resolves to the
392
403
  * new schedule's id WITHOUT running it now. Satisfies the SDK's optional `scheduleWorkflow`. */
393
404
  scheduleWorkflow(slug, input, opts) {
394
- return this.guarded(async () => {
395
- const id = await this.deps.children.schedule(slug, input, opts);
396
- return id;
397
- });
405
+ return this.guarded(() => this.deps.children.schedule(slug, input, opts));
398
406
  }
399
407
  getSecret(name) {
400
- return this.guarded(async () => {
401
- const value = await this.deps.secrets.get(name);
402
- return value;
403
- });
408
+ return this.guarded(() => this.deps.secrets.get(name));
404
409
  }
405
410
  writeArtifact(name, contentType, body, metadata) {
406
411
  return this.guarded(async () => {
407
412
  if (this.deps.writeArtifact === undefined) {
408
413
  throw new AppError(ErrorCode.VALIDATION_FAILED, "artifacts.write is not available in this runtime");
409
414
  }
410
- const ref = await this.deps.writeArtifact(name, contentType, body, metadata);
411
- return ref;
415
+ return await this.deps.writeArtifact(name, contentType, body, metadata);
412
416
  });
413
417
  }
414
418
  /** `computer.openBrowser()`: open a program-owned, in-VM browser session (the browser tier of
@@ -438,7 +442,12 @@ export class WorkerWorkflowHost {
438
442
  return { ...rest, mcp: [...(rest.mcp ?? []), ref] };
439
443
  }
440
444
  sleep(arg) {
441
- return this.guarded(() => this.sleepSeam(arg));
445
+ return this.guarded(async () => {
446
+ // A compute-breached run parks HERE rather than sleeping first and parking later — no forward
447
+ // progress while a budget cap is breached (the budget-gate park-at-next-seam contract).
448
+ await this.deps.budgetGate?.clear();
449
+ return this.sleepSeam(arg);
450
+ });
442
451
  }
443
452
  /** A short sleep HOLDS the task in-process (cheaper than a snapshot round-trip); a long one
444
453
  * (≥ {@link SUSPEND_THRESHOLD_MS}) SUSPENDS on the snapshot substrate — the VM freezes and the
@@ -476,6 +485,37 @@ export class WorkerWorkflowHost {
476
485
  await this.deps.onBeforeSleep();
477
486
  await this.sleeper.hold(holdMs, this.deps.signal);
478
487
  }
488
+ /** The protocol's `shell` capability. Runs under the same abort/freeze gate as every hook, so
489
+ * a freeze never snapshots around an unguarded seam and an aborted run rejects promptly. */
490
+ shell(cmd, opts) {
491
+ return this.guarded(async () => {
492
+ if (this.deps.shell === undefined) {
493
+ throw new AppError(ErrorCode.VALIDATION_FAILED, "shell is not available in this runtime");
494
+ }
495
+ // Compute burns hardest in long shell commands — a breach parks before starting another one.
496
+ await this.deps.budgetGate?.clear();
497
+ return await this.deps.shell(cmd, opts);
498
+ });
499
+ }
500
+ /** Live budget state for `usage.get` — every dimension `{spent, cap, remaining}`. */
501
+ usage() {
502
+ return this.guarded(async () => {
503
+ if (this.deps.usage === undefined) {
504
+ throw new AppError(ErrorCode.VALIDATION_FAILED, "usage.get is not available in this runtime");
505
+ }
506
+ // Promise.resolve keeps the arrow's async-without-await acceptable to require-await; the
507
+ // arrow stays async so the guard above REJECTS (guarded's contract) rather than throwing.
508
+ return Promise.resolve(this.deps.usage());
509
+ });
510
+ }
511
+ /** `auth.idToken(audience)` — minted per call by the broker via {@link RuntimeContext}. */
512
+ idToken(audience) {
513
+ return this.guarded(() => this.deps.runtime.idToken(audience));
514
+ }
515
+ /** `auth.apiToken()` — the run's short-lived, manifest-scoped public-API bearer. */
516
+ apiToken() {
517
+ return this.guarded(() => this.deps.runtime.apiToken());
518
+ }
479
519
  /** Resolve any {@link SleepArg} shape to a millisecond duration from now. */
480
520
  resolveSleepMs(arg) {
481
521
  if (typeof arg === "number")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boardwalk-labs/runner",
3
- "version": "0.2.16",
3
+ "version": "0.3.0-alpha.1",
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": {
@@ -53,9 +53,10 @@
53
53
  "prebuild": "prebuildify --napi --strip -t 24.0.0"
54
54
  },
55
55
  "dependencies": {
56
- "@boardwalk-labs/engine": "0.2.12",
57
- "@boardwalk-labs/workflow": "0.2.6",
56
+ "@boardwalk-labs/engine": "0.3.0-alpha.2",
57
+ "@boardwalk-labs/workflow": "0.3.0-alpha.7",
58
58
  "@modelcontextprotocol/sdk": "^1.29.0",
59
+ "ajv": "^8.20.0",
59
60
  "node-gyp-build": "^4.8.4",
60
61
  "tar": "^7.5.16",
61
62
  "undici": "^6.27.0",