@boardwalk-labs/runner 0.1.4 → 0.1.6

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 (47) hide show
  1. package/dist/daemon/daemon.js +6 -2
  2. package/dist/runtime/agent/budget.d.ts +2 -2
  3. package/dist/runtime/agent/budget.js +4 -4
  4. package/dist/runtime/agent/model_rates.js +1 -1
  5. package/dist/runtime/agent/secret_redactor.js +1 -2
  6. package/dist/runtime/broker_artifact_store.js +3 -3
  7. package/dist/runtime/broker_child_dispatcher.js +1 -1
  8. package/dist/runtime/broker_event_publisher.js +1 -1
  9. package/dist/runtime/broker_tool_host.js +2 -2
  10. package/dist/runtime/cancel_watcher.js +1 -1
  11. package/dist/runtime/credit_watcher.js +1 -2
  12. package/dist/runtime/direct_inference.d.ts +7 -1
  13. package/dist/runtime/direct_inference.js +10 -2
  14. package/dist/runtime/index.d.ts +2 -2
  15. package/dist/runtime/index.js +17 -12
  16. package/dist/runtime/inference_transport.js +1 -1
  17. package/dist/runtime/leaf_executor.d.ts +2 -2
  18. package/dist/runtime/leaf_executor.js +6 -3
  19. package/dist/runtime/lease_renewer.js +1 -1
  20. package/dist/runtime/main.js +1 -1
  21. package/dist/runtime/program_log_capture.d.ts +7 -1
  22. package/dist/runtime/program_log_capture.js +10 -3
  23. package/dist/runtime/program_runner.d.ts +2 -2
  24. package/dist/runtime/program_runner.js +14 -3
  25. package/dist/runtime/program_worker.d.ts +4 -4
  26. package/dist/runtime/program_worker.js +5 -3
  27. package/dist/runtime/recording_secret_resolver.js +1 -1
  28. package/dist/runtime/run_abort.js +1 -2
  29. package/dist/runtime/runner_control_client.d.ts +3 -3
  30. package/dist/runtime/runner_control_client.js +5 -5
  31. package/dist/runtime/runtime_flusher.js +1 -1
  32. package/dist/runtime/suspension.js +1 -1
  33. package/dist/runtime/testing_artifact_build.js +1 -1
  34. package/dist/runtime/tools/artifacts.js +4 -4
  35. package/dist/runtime/tools/types.d.ts +1 -1
  36. package/dist/runtime/tools/types.js +1 -1
  37. package/dist/runtime/tools/web_search.d.ts +1 -1
  38. package/dist/runtime/tools/web_search.js +2 -2
  39. package/dist/runtime/wire/artifact_storage.d.ts +1 -1
  40. package/dist/runtime/wire/artifact_storage.js +4 -4
  41. package/dist/runtime/wire/human_input.d.ts +1 -1
  42. package/dist/runtime/wire/human_input.js +1 -1
  43. package/dist/runtime/wire/inference_proxy.js +2 -2
  44. package/dist/runtime/workflow_host.d.ts +4 -4
  45. package/dist/runtime/workflow_host.js +3 -3
  46. package/dist/runtime/workspace_store.js +1 -1
  47. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- // The self-hosted runner daemon (docs/SELF_HOSTED_RUNNERS.md R4): poll → claim → spawn ONE run
2
+ // The self-hosted runner daemon (the self-hosted runner design): poll → claim → spawn ONE run
3
3
  // process → heartbeat while it runs → clean → poll. One run at a time per daemon (concurrency =
4
4
  // more daemons/machines); drain (Ctrl-C, admin drain, org toggle off) finishes the current run
5
5
  // then stops claiming. The per-run process is the SAME runtime a Boardwalk-hosted worker boots
@@ -24,7 +24,7 @@ export function runProcessEnv(claim, opts) {
24
24
  BOARDWALK_RUN_TOKEN: claim.control_plane.run_token,
25
25
  BOARDWALK_API_KEY: claim.control_plane.api_token,
26
26
  BOARDWALK_TASK_CPU_UNITS: String(os.cpus().length * 1024),
27
- // The org's BYO inference providers, as data (plan D7) — consumed by the runtime's direct
27
+ // The org's BYO inference providers, as data (the runner-direct BYO design) — consumed by the runtime's direct
28
28
  // model path. Names + endpoints + secret NAMES only; never a credential value.
29
29
  BOARDWALK_BYO_PROVIDERS: JSON.stringify(claim.byo_providers),
30
30
  WORKER_ID: opts.runnerId,
@@ -92,6 +92,10 @@ export function startDaemon(deps) {
92
92
  }
93
93
  const done = (async () => {
94
94
  log.info("daemon_started", { runnerId: deps.runnerId, workDir: deps.workDir });
95
+ // Reclaim run dirs a previous daemon left behind (crash / SIGKILL / force-quit): a completed
96
+ // run always cleans its own dir, so anything here is orphaned and may hold a workspace the
97
+ // crashed run wrote. Best-effort; a failure must not stop the daemon.
98
+ await rm(path.join(deps.workDir, "runs"), { recursive: true, force: true }).catch(() => undefined);
95
99
  while (!draining) {
96
100
  deps.onIdle?.();
97
101
  try {
@@ -20,7 +20,7 @@ export interface UsageDelta {
20
20
  * Cumulative usage carried forward from PRIOR worker sessions of the same run. Seeded from the
21
21
  * checkpoint on resume so budget caps bound the WHOLE run, not just the current session — a run
22
22
  * that sleeps / waits on a child / recovers from a crash must not get a fresh budget each time
23
- * (review #2). Zero for a fresh run.
23
+ *. Zero for a fresh run.
24
24
  */
25
25
  export interface PriorUsage {
26
26
  inputTokens: number;
@@ -91,7 +91,7 @@ export declare class BudgetMeter {
91
91
  * RUN-CUMULATIVE snapshot: this session's usage PLUS the prior sessions' usage seeded at
92
92
  * construction. Cap enforcement ({@link assertWithinCaps}) and checkpoint persistence use
93
93
  * this so a run that resumes after a sleep/wait/crash is bounded by its declared caps across
94
- * the whole run — not once per session (review #2). `snapshot()` stays session-local because
94
+ * the whole run — not once per session. `snapshot()` stays session-local because
95
95
  * per-session token metering reports token deltas; seeding it would double-report to Stripe.
96
96
  */
97
97
  cumulative(): BudgetSnapshot;
@@ -10,7 +10,7 @@
10
10
  // not a hand-rolled token estimate. Only a BYO turn (no upstream price) or a managed turn whose cost
11
11
  // the broker couldn't read falls back to a single flat representative rate
12
12
  // (`model_rates.ts::BUDGET_GUARDRAIL_RATE`), since a per-`agent()`-model run has no one model. Actual
13
- // billing remains per-request cost pass-through via Stripe meters (MASTER_SPEC §15.2). The meter takes
13
+ // billing remains per-request cost pass-through via Stripe meters (the platform spec). The meter takes
14
14
  // the fallback rate as a constructor arg so tests inject a fixed rate.
15
15
  import { AppError, ErrorCode } from "../support/index.js";
16
16
  const ZERO_PRIOR = {
@@ -77,7 +77,7 @@ export class BudgetMeter {
77
77
  * RUN-CUMULATIVE snapshot: this session's usage PLUS the prior sessions' usage seeded at
78
78
  * construction. Cap enforcement ({@link assertWithinCaps}) and checkpoint persistence use
79
79
  * this so a run that resumes after a sleep/wait/crash is bounded by its declared caps across
80
- * the whole run — not once per session (review #2). `snapshot()` stays session-local because
80
+ * the whole run — not once per session. `snapshot()` stays session-local because
81
81
  * per-session token metering reports token deltas; seeding it would double-report to Stripe.
82
82
  */
83
83
  cumulative() {
@@ -100,11 +100,11 @@ export class BudgetMeter {
100
100
  assertWithinCaps() {
101
101
  if (this.budget === undefined)
102
102
  return;
103
- // Cumulative across resume sessions — caps bound the whole run, not each session (review #2).
103
+ // Cumulative across resume sessions — caps bound the whole run, not each session.
104
104
  const snap = this.cumulative();
105
105
  // `max_tokens` deliberately bounds CONVERSATION tokens (input + output) only; cache-read /
106
106
  // cache-write tokens are tracked + fully billed via costFor() and are bounded by `max_usd`,
107
- // not by this cap (review #19). totalTokens (snapshot/cumulative) reflects that choice.
107
+ // not by this cap. totalTokens (snapshot/cumulative) reflects that choice.
108
108
  if (this.budget.max_tokens !== undefined && snap.totalTokens > this.budget.max_tokens) {
109
109
  throw new AppError(ErrorCode.BUDGET_EXCEEDED, `Token cap exceeded: ${snap.totalTokens.toString()} > ${this.budget.max_tokens.toString()}`, { kind: "tokens", used: snap.totalTokens, cap: this.budget.max_tokens });
110
110
  }
@@ -6,7 +6,7 @@
6
6
  // no upstream cost: a BYO-provider turn (the org pays its own key), or a managed turn whose cost the
7
7
  // broker couldn't read. It bounds runaway loops; it is NOT the bill. Actual billing is per-request
8
8
  // COST PASS-THROUGH (OpenRouter's reported cost × margin) via Stripe meters (domain/billing +
9
- // run_usage_service). MASTER_SPEC §15.2.
9
+ // run_usage_service). the platform spec
10
10
  //
11
11
  // Deliberately NOT a per-model table with a silent fallback: a lookup that returns a Sonnet-class
12
12
  // default for any unknown id only created the ILLUSION of precision. The real per-request cost (above)
@@ -1,5 +1,4 @@
1
- // SecretRedactor — scrubs known secret values from everything bound for an LLM (MASTER_SPEC §12,
2
- // docs/WORKFLOW_RUNTIME.md §6.2, plan #9).
1
+ // SecretRedactor — scrubs known secret values from everything bound for an LLM (the platform spec).
3
2
  //
4
3
  // The architectural guarantee is structural: secrets live ONLY in the workflow PROGRAM (the trusted
5
4
  // deterministic tool layer), which holds them via `secrets.get` / `ctx.secrets.resolve`. The
@@ -1,10 +1,10 @@
1
1
  // BrokerArtifactStore — the ArtifactStore the `artifacts` tool uses under the Runner Credential
2
- // Broker (docs/RUNNER_BROKER.md §4 Artifacts). It forwards every op to the Runner Control API:
3
- // the broker computes the S3 key, neutralizes the served content type (review #32), PUTs with its
2
+ // Broker (the Runner Credential Broker model). It forwards every op to the Runner Control API:
3
+ // the broker computes the S3 key, neutralizes the served content type, PUTs with its
4
4
  // own creds, and records the catalog row — so the untrusted runner holds no S3 credential and can't
5
5
  // bypass those server-side rules. A thin adapter over the run-bound RunnerControlClient.
6
6
  //
7
- // Writes route by size (docs/RUNNER_BROKER.md §7 step 4): a SMALL body is proxied inline through the
7
+ // Writes route by size (the Runner Credential Broker model): a SMALL body is proxied inline through the
8
8
  // broker (`POST .../artifacts`); a LARGE one takes the presigned-PUT path — the broker signs an S3
9
9
  // PUT (still owning the key + content-type neutralization + catalog row) and the runner streams the
10
10
  // bytes straight to S3, so they never pass through the control plane (no body cap).
@@ -1,4 +1,4 @@
1
- // BrokerChildDispatcher — the broker-backed `workflows.call` (docs/RUNNER_BROKER.md). The host's
1
+ // BrokerChildDispatcher — the broker-backed `workflows.call` (the Runner Credential Broker model). The host's
2
2
  // ChildDispatcher under the broker model: instead of touching the DB/SQS directly (the
3
3
  // WorkerChildDispatcher path), it asks the Runner Control API to create the child (resolve + the
4
4
  // `callable_by` gate + idempotent re-attach happen server-side) and then HOLDS the parent task,
@@ -1,5 +1,5 @@
1
1
  // BrokerEventPublisher — a `RedisPublisher` that ships the agent event-stream through the broker
2
- // instead of publishing to Redis directly (docs/RUNNER_BROKER.md §4 Telemetry).
2
+ // instead of publishing to Redis directly (the Runner Credential Broker model).
3
3
  //
4
4
  // The run-event live channel is a per-event `redis.publish(run:<id>, frame)`. Under the broker the
5
5
  // runner holds no Redis credential, so this stand-in BUFFERS frames and POSTs them in batches to the
@@ -1,9 +1,9 @@
1
1
  // BrokerToolHost — the engine's `ToolHost` (the infrastructure seam for the host-backed built-in
2
2
  // coding tools `webfetch` / `http` / `web_search` / `artifacts`) wired over the Runner Credential Broker
3
- // (docs/RUNNER_BROKER.md). `@boardwalk-labs/engine` registers those three tools ONLY when the leaf's
3
+ // (the Runner Credential Broker model). `@boardwalk-labs/engine` registers those three tools ONLY when the leaf's
4
4
  // `ToolSetContext.host` provides the backing hook; we supply one here so they light up on hosted
5
5
  // runs. Each hook delegates the privileged capability to whoever holds the credential/network/storage
6
- // — exactly as the broker model requires (security review #1): the untrusted worker holds no Tavily
6
+ // — exactly as the broker model requires (security): the untrusted worker holds no Tavily
7
7
  // key, no S3 credential, and only reaches the public network through the egress proxy.
8
8
  //
9
9
  // web_search → broker /tools/web_search (the broker holds the Tavily key; the worker just forwards
@@ -1,4 +1,4 @@
1
- // CancelWatcher — stops a run when the user cancels it mid-flight (MASTER_SPEC §6 + docs/RUNNER_BROKER.md).
1
+ // CancelWatcher — stops a run when the user cancels it mid-flight (the platform spec).
2
2
  //
3
3
  // The user-cancel counterpart to CreditWatcher. One per run session. On a timer it asks — through the
4
4
  // broker (`GET /cancel`, since the runner holds no DB/Redis credential) — whether the run has been
@@ -1,5 +1,4 @@
1
- // CreditWatcher — stops a run when its org runs out of prepaid credit mid-flight (MASTER_SPEC §15 +
2
- // docs/CODING_AGENT.md §6/§10).
1
+ // CreditWatcher — stops a run when its org runs out of prepaid credit mid-flight (the platform spec).
3
2
  //
4
3
  // One per run session. On a timer it asks — through the broker (`GET /credit`, since the runner holds
5
4
  // no Stripe credential) — whether the org is still funded; the FIRST time it isn't, it fires
@@ -23,7 +23,13 @@ export interface DirectInferenceDeps {
23
23
  }
24
24
  /** One model turn, straight to the org's endpoint. Returns the ChatTurn + canonical model ref;
25
25
  * BYO carries no platform cost (costMicros stays 0 — BYO is never metered). */
26
- export declare function streamDirectTurn(deps: DirectInferenceDeps, entry: ByoInferenceProvider, req: DirectTurnRequest, onDelta: ((text: string) => void) | undefined): Promise<{
26
+ export declare function streamDirectTurn(deps: DirectInferenceDeps, entry: ByoInferenceProvider, req: DirectTurnRequest, onDelta: ((text: string) => void) | undefined,
27
+ /** Register the resolved key with the CURRENT leaf's engine redactor, before the model call.
28
+ * The key resolves mid-leaf (after the leaf's redactor snapshot is seeded), so without this a
29
+ * provider that echoes the Authorization header in an error body would leak it into that
30
+ * leaf's error run event. The run-level redactor already has it via resolveSecret; this closes
31
+ * the per-leaf gap. */
32
+ registerSecret?: (value: string) => void): Promise<{
27
33
  turn: ChatTurn;
28
34
  modelRef: string;
29
35
  }>;
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- // Runner-direct BYO inference (docs/SELF_HOSTED_RUNNERS.md D7): the managed lane stays
2
+ // Runner-direct BYO inference (the self-hosted runner design): the managed lane stays
3
3
  // brokered (the platform key never leaves the platform), but a BYO provider is the ORG'S OWN
4
4
  // endpoint + key — class-3 material the runner may hold — so the runtime calls it directly
5
5
  // with the SAME engine adapters the broker uses. This is what makes a LAN-only model work on
@@ -53,8 +53,16 @@ export function directProviderFor(registry, provider) {
53
53
  }
54
54
  /** One model turn, straight to the org's endpoint. Returns the ChatTurn + canonical model ref;
55
55
  * BYO carries no platform cost (costMicros stays 0 — BYO is never metered). */
56
- export async function streamDirectTurn(deps, entry, req, onDelta) {
56
+ export async function streamDirectTurn(deps, entry, req, onDelta,
57
+ /** Register the resolved key with the CURRENT leaf's engine redactor, before the model call.
58
+ * The key resolves mid-leaf (after the leaf's redactor snapshot is seeded), so without this a
59
+ * provider that echoes the Authorization header in an error body would leak it into that
60
+ * leaf's error run event. The run-level redactor already has it via resolveSecret; this closes
61
+ * the per-leaf gap. */
62
+ registerSecret) {
57
63
  const apiKey = entry.auth_secret_name === null ? null : await deps.resolveSecret(entry.auth_secret_name);
64
+ if (apiKey !== null)
65
+ registerSecret?.(apiKey);
58
66
  if (entry.base_url === null) {
59
67
  throw new Error(`BYO provider '${entry.name}' has no base_url`);
60
68
  }
@@ -4,7 +4,7 @@ import type { ByoInferenceProvider } from "../contract.js";
4
4
  import { type ProgramWorkerDeps } from "./program_worker.js";
5
5
  /** Constructed primitives the pure assembly consumes (real ones in main(), fakes in tests). The
6
6
  * runner holds NO platform credential — only the per-run control-plane handle (run token + broker
7
- * URL); everything else is reached through the Runner Control API (docs/RUNNER_BROKER.md). */
7
+ * URL); everything else is reached through the Runner Control API (the Runner Credential Broker model). */
8
8
  export interface WorkerRuntime {
9
9
  /** Stable worker identity stamped onto the lease (ECS task arn / hostname / run-derived). */
10
10
  workerId: string;
@@ -58,7 +58,7 @@ export declare function assembleWorkerDeps(runtime: WorkerRuntime): ProgramWorke
58
58
  * The per-run platform values the dispatcher injects as container env (ecs_run_task_client.ts). They
59
59
  * are captured into private worker state at bootstrap and DELETED from `process.env` before any user
60
60
  * program / agent leaf / subprocess can run — the run token + API token are credentials, and nothing
61
- * untrusted run code touches should inherit them (docs/RUN_ENV_AND_CREDS.md). The user owns the rest
61
+ * untrusted run code touches should inherit them (the run env/credential rules). The user owns the rest
62
62
  * of `process.env` outright.
63
63
  */
64
64
  export declare const PLATFORM_ENV_KEYS: readonly ["RUN_ID", "BOARDWALK_CONTROL_PLANE_URL", "BOARDWALK_RUN_TOKEN", "BOARDWALK_API_KEY", "BOARDWALK_TASK_CPU_UNITS"];
@@ -1,9 +1,9 @@
1
- // Worker composition root (docs/WORKFLOW_RUNTIME.md + docs/RUNNER_BROKER.md). The Boardwalk-hosted
1
+ // Worker composition root (the workflow runtime design). The Boardwalk-hosted
2
2
  // worker container runs `node dist/fargate/worker/index.js`; the dispatcher launches it per run with
3
3
  // RUN_ID + the per-run control-plane handle. This file assembles real implementations behind every
4
4
  // seam `runProgramWorker(runId, deps)` injects, then runs the one workflow program to terminal.
5
5
  //
6
- // BROKERED-ONLY (security review #1): the runner is UNTRUSTED and holds NO platform credential — only
6
+ // BROKERED-ONLY (security): the runner is UNTRUSTED and holds NO platform credential — only
7
7
  // its short-lived run token. EVERY privileged seam goes through the Runner Control API (the broker):
8
8
  // - agent() → EngineLeafExecutor: the OSS engine's loop (runAgentLeaf) over a broker-backed
9
9
  // LeafIo — the model turn streams through /inference (invoked server-side).
@@ -90,7 +90,7 @@ export function assembleWorkerDeps(runtime) {
90
90
  runId: runtime.runId,
91
91
  });
92
92
  log.info("runner_control_enabled", { runId: runtime.runId });
93
- // The replay frontier (the highest journaled seq at claim) drives SILENT REPLAY (docs/SUSPENSION.md):
93
+ // The replay frontier (the highest journaled seq at claim) drives SILENT REPLAY (the durable-suspension design):
94
94
  // a resumed run suppresses observability for seams it already ran. Captured at claim, read by
95
95
  // buildHost (which runs right after claim) when constructing the per-run host.
96
96
  let replayFrontier = 0;
@@ -109,7 +109,7 @@ export function assembleWorkerDeps(runtime) {
109
109
  // bootstrap captured + scrubbed it (capturePlatformContext) so the agent's bash / subprocesses
110
110
  // can't read it. The program reaches it ONLY via `runtime.apiToken()` (built below); we still
111
111
  // record it in each run's redactor so a value the program threads into a tool can't echo back out
112
- // of a tool result. Absent in the dev no-signing-key path. (docs/RUN_ENV_AND_CREDS.md)
112
+ // of a tool result. Absent in the dev no-signing-key path. (the run env/credential rules)
113
113
  const runApiKey = runtime.controlPlane.apiToken;
114
114
  // The broker (Runner Control API) is hosted on the api-server, so the public API shares its
115
115
  // origin; `runtime.apiUrl` exposes it and the program appends `/v1` or `/mcp/v1`.
@@ -139,7 +139,7 @@ export function assembleWorkerDeps(runtime) {
139
139
  const nextMeterIdentifier = tokenMeterIdentifiers(run.id, meteringSessionId);
140
140
  // Per-run secret boundary: every value resolved (program `secrets.get` OR a tool's
141
141
  // ctx.secrets.resolve) is recorded into one shared redactor; the leaf seeds a fresh engine
142
- // Redactor from it so the loop scrubs those values out of all model-bound content. MASTER_SPEC §12.
142
+ // Redactor from it so the loop scrubs those values out of all model-bound content. the platform spec
143
143
  const redactor = new SecretRedactor();
144
144
  // Record the run's own API key so a prompt-injected agent can't echo it back to the model.
145
145
  if (runApiKey !== undefined && runApiKey !== "")
@@ -227,7 +227,7 @@ export function assembleWorkerDeps(runtime) {
227
227
  workspaceRoot: runtime.workspaceRoot,
228
228
  })
229
229
  : undefined;
230
- // Durable suspension (docs/SUSPENSION.md): the host raises a suspend OUT OF BAND (onSuspend +
230
+ // Durable suspension (the durable-suspension design): the host raises a suspend OUT OF BAND (onSuspend +
231
231
  // a never-resolving seam), so a program's own try/catch can't swallow it; the program runner
232
232
  // races `suspendSignal` against the body. One deferred per run — the first seam to suspend wins.
233
233
  let resolveSuspend = () => undefined;
@@ -289,7 +289,7 @@ export function assembleWorkerDeps(runtime) {
289
289
  : {}),
290
290
  phases: phaseTracker,
291
291
  });
292
- // Hand the redactor back too: the worker scrubs a terminal error's message with it (review #5);
292
+ // Hand the redactor back too: the worker scrubs a terminal error's message with it;
293
293
  // workspace (when opted in) hydrates at start + persists at terminal.
294
294
  return Promise.resolve({
295
295
  host,
@@ -299,7 +299,12 @@ export function assembleWorkerDeps(runtime) {
299
299
  // throws) so no language-server process leaks past the run.
300
300
  lsp: lspService,
301
301
  // The orchestrator emits the program's declared output onto the wire (`output` kind).
302
- activity: { output: (value) => void runEvents.emit({ kind: "output", value }) },
302
+ // Redact the declared-output EVENT (observability): a secret the program put in output()
303
+ // must not surface in the run's event stream. The FUNCTIONAL output program_runner returns
304
+ // for `workflows.call` is a separate path and stays raw, so cross-workflow data flow is intact.
305
+ activity: {
306
+ output: (value) => void runEvents.emit({ kind: "output", value: redactor.redactValue(value) }),
307
+ },
303
308
  // Filled by the runner once the artifact extracts; the leaf's `skillsDir` thunk reads it.
304
309
  setProgramDir: (dir) => {
305
310
  programDir = dir;
@@ -349,7 +354,7 @@ export function assembleWorkerDeps(runtime) {
349
354
  finalizer: {
350
355
  finalize: (_id, status, output) => broker.finalize(status, output, runtime.workerId),
351
356
  },
352
- // Durable suspension (docs/SUSPENSION.md): persist the wake condition through the broker (journal
357
+ // Durable suspension (the durable-suspension design): persist the wake condition through the broker (journal
353
358
  // entry + a HITL request row, or the wake time for a long sleep) and release the lease — no
354
359
  // finalize. A wake (an answer or a timer) re-dispatches the run.
355
360
  suspender: {
@@ -424,7 +429,7 @@ export function assembleWorkerDeps(runtime) {
424
429
  * The per-run platform values the dispatcher injects as container env (ecs_run_task_client.ts). They
425
430
  * are captured into private worker state at bootstrap and DELETED from `process.env` before any user
426
431
  * program / agent leaf / subprocess can run — the run token + API token are credentials, and nothing
427
- * untrusted run code touches should inherit them (docs/RUN_ENV_AND_CREDS.md). The user owns the rest
432
+ * untrusted run code touches should inherit them (the run env/credential rules). The user owns the rest
428
433
  * of `process.env` outright.
429
434
  */
430
435
  export const PLATFORM_ENV_KEYS = [
@@ -458,7 +463,7 @@ export function capturePlatformContext(env) {
458
463
  }
459
464
  return value.trim();
460
465
  };
461
- // The dispatcher always injects the control-plane handle (docs/RUNNER_BROKER.md). The runner is
466
+ // The dispatcher always injects the control-plane handle (the Runner Credential Broker model). The runner is
462
467
  // brokered-only — there is no DB/Redis/Stripe fallback — so a missing handle is a hard config error.
463
468
  const runId = read("RUN_ID");
464
469
  const apiToken = env.BOARDWALK_API_KEY?.trim();
@@ -481,7 +486,7 @@ export function capturePlatformContext(env) {
481
486
  export async function main() {
482
487
  // Capture the platform context into private state and remove it from process.env BEFORE anything
483
488
  // else — so no user program / agent tool / subprocess we later spawn can read the run token or
484
- // API token (docs/RUN_ENV_AND_CREDS.md). WORKER_ID / WORKSPACE_ROOT are non-secret infra knobs.
489
+ // API token (the run env/credential rules). WORKER_ID / WORKSPACE_ROOT are non-secret infra knobs.
485
490
  const platform = capturePlatformContext(process.env);
486
491
  const runId = platform.runId;
487
492
  const byoProviders = parseByoProviders(process.env.BOARDWALK_BYO_PROVIDERS);
@@ -1,5 +1,5 @@
1
1
  // The broker inference transport the engine-backed leaf streams a model turn through
2
- // (docs/RUNNER_BROKER.md §4 "Inference (class 2)").
2
+ // (the Runner Credential Broker model)").
3
3
  //
4
4
  // Under the Runner Credential Broker the runner invokes NO model directly: it holds neither the
5
5
  // managed-inference key nor any BYO provider key. So the `agent()` leaf's `LeafIo.streamModel`
@@ -21,7 +21,7 @@ export interface MeterUsageInput {
21
21
  export interface EngineLeafExecutorDeps {
22
22
  /** Streams one model turn through the broker (RunnerControlClient satisfies it). */
23
23
  inference: InferenceProxyTransport;
24
- /** Runner-direct BYO inference (docs/SELF_HOSTED_RUNNERS.md D7): when a per-`agent()` provider
24
+ /** Runner-direct BYO inference (the self-hosted runner design): when a per-`agent()` provider
25
25
  * matches this registry (key-based HTTP sources only), the turn goes STRAIGHT to the org's
26
26
  * endpoint with the engine adapters — the managed lane and bedrock stay brokered. Omitted ⇒
27
27
  * everything brokered (legacy dispatch without a registry). */
@@ -32,7 +32,7 @@ export interface EngineLeafExecutorDeps {
32
32
  budget: BudgetMeter;
33
33
  /** RUN-level redactor (shared with the secret resolver): every resolved secret value is recorded
34
34
  * here. We seed a fresh engine `Redactor` from it per leaf so the loop scrubs known values out of
35
- * the prompt, tool args/results, and transcript before they reach the model (MASTER_SPEC §12). */
35
+ * the prompt, tool args/results, and transcript before they reach the model (the platform spec). */
36
36
  redactor: SecretRedactor;
37
37
  /** Builds this leaf's event sink; `leafIndex` (1-based) is only a metering identifier — the sink
38
38
  * owns the run-global cursor. `identity` names the leaf on its turn frames. */
@@ -105,7 +105,7 @@ export class EngineLeafExecutor {
105
105
  // through providerIo.onDelta and return the terminal turn. An aborted run rejects in-flight.
106
106
  streamModel: async (req, providerIo) => {
107
107
  throwIfAborted(signal);
108
- return this.streamModel(req, providerIo, signal, (costMicros) => {
108
+ return this.streamModel(req, providerIo, signal, redactor, (costMicros) => {
109
109
  pendingCostMicros = (pendingCostMicros ?? 0) + costMicros;
110
110
  });
111
111
  },
@@ -193,7 +193,7 @@ export class EngineLeafExecutor {
193
193
  /** POST one model turn to the broker's `/inference` and adapt its NDJSON stream into the engine's
194
194
  * ModelTurnResult: each `delta` frame drives providerIo.onDelta; the terminal `result` frame is
195
195
  * the turn. An `error` frame throws (the broker already classified it). An abort mid-stream throws. */
196
- async streamModel(req, providerIo, signal, onCost) {
196
+ async streamModel(req, providerIo, signal, redactor, onCost) {
197
197
  // Runner-direct BYO (D7): the org's own endpoint + key, called with the same engine adapters
198
198
  // the broker uses. No platform cost (BYO is never metered) — onCost stays untouched.
199
199
  // A direct turn needs an explicit model (an omitted model means the managed auto lane,
@@ -209,7 +209,10 @@ export class EngineLeafExecutor {
209
209
  messages: req.messages,
210
210
  tools: req.tools,
211
211
  ...(req.reasoning !== undefined ? { reasoning: req.reasoning } : {}),
212
- }, providerIo.onDelta);
212
+ }, providerIo.onDelta,
213
+ // Register the org's key with THIS leaf's redactor before the model call, so an error
214
+ // body echoing it is scrubbed from the leaf's run events (not just the terminal error).
215
+ (value) => redactor.add("byo-key", value));
213
216
  throwIfAborted(signal);
214
217
  return { turn: out.turn, modelRef: out.modelRef };
215
218
  }
@@ -1,5 +1,5 @@
1
1
  // LeaseRenewer — keeps a long run's lease fresh so the recovery sweep doesn't reclaim a STILL-ALIVE
2
- // worker (docs/RUNNER_BROKER.md; MASTER_SPEC §6).
2
+ // worker (the Runner Credential Broker model).
3
3
  //
4
4
  // A run is claimed with a fixed lease (DEFAULT_LEASE_MS, 5 min). The worker never used to renew it,
5
5
  // so any run longer than the lease (an Opus agentic loop is routinely 7+ min) had its lease expire
@@ -2,6 +2,6 @@
2
2
  // Process entrypoint for one run: `node .../runtime/main.js` with the platform env contract
3
3
  // (RUN_ID + BOARDWALK_CONTROL_PLANE_URL + BOARDWALK_RUN_TOKEN [+ BOARDWALK_API_KEY]). Used by
4
4
  // the Boardwalk-hosted Fargate container AND spawned per-run by the self-hosted daemon —
5
- // one worker, two homes (docs/RUNNER_BROKER.md §6). Import `./index.js` for the library.
5
+ // one worker, two homes (the Runner Credential Broker model). Import `./index.js` for the library.
6
6
  import { main } from "./index.js";
7
7
  void main();
@@ -4,7 +4,13 @@ export type LogStream = "stdout" | "stderr";
4
4
  * Patch the global console so each call is forwarded to the original AND handed (formatted) to
5
5
  * `sink`. Returns a restore function — ALWAYS call it (the worker runs `restore()` in a finally).
6
6
  */
7
- export declare function captureConsole(sink: (stream: LogStream, text: string) => void): () => void;
7
+ export declare function captureConsole(sink: (stream: LogStream, text: string) => void,
8
+ /** Scrub known secret values from each formatted line before it reaches EITHER sink. A program
9
+ * that `console.log`s a resolved secret must not leak it — to the run's `program_output` events
10
+ * OR to container stdout (CloudWatch). We format+redact once and print the SAME redacted string
11
+ * to the original console (equivalent output; `util.format` is what console does internally).
12
+ * Defaults to identity (tests/local). */
13
+ redact?: (text: string) => string): () => void;
8
14
  export interface ProgramLogSinkOptions {
9
15
  /** The run's shared event emitter — program logs ride the one ordered stream (`log` channel). */
10
16
  sink: TurnEventSink;
@@ -24,7 +24,13 @@ const STREAM_BY_METHOD = {
24
24
  * Patch the global console so each call is forwarded to the original AND handed (formatted) to
25
25
  * `sink`. Returns a restore function — ALWAYS call it (the worker runs `restore()` in a finally).
26
26
  */
27
- export function captureConsole(sink) {
27
+ export function captureConsole(sink,
28
+ /** Scrub known secret values from each formatted line before it reaches EITHER sink. A program
29
+ * that `console.log`s a resolved secret must not leak it — to the run's `program_output` events
30
+ * OR to container stdout (CloudWatch). We format+redact once and print the SAME redacted string
31
+ * to the original console (equivalent output; `util.format` is what console does internally).
32
+ * Defaults to identity (tests/local). */
33
+ redact = (t) => t) {
28
34
  const console_ = globalThis.console;
29
35
  const originals = {};
30
36
  for (const [method, stream] of Object.entries(STREAM_BY_METHOD)) {
@@ -33,9 +39,10 @@ export function captureConsole(sink) {
33
39
  continue;
34
40
  originals[method] = original;
35
41
  console_[method] = (...args) => {
36
- original.apply(console_, args); // still print to the container stdout (CloudWatch)
42
+ const text = redact(format(...args));
43
+ original.call(console_, text); // print the REDACTED line to container stdout (CloudWatch)
37
44
  try {
38
- sink(stream, format(...args));
45
+ sink(stream, text);
39
46
  }
40
47
  catch {
41
48
  // best-effort — a telemetry hiccup must never break the program's own logging
@@ -48,7 +48,7 @@ export interface ProgramRunnerDeps {
48
48
  * Scrubs known secret values out of a string (the run's `SecretRedactor.redactText`). Applied to a
49
49
  * top-level throw's message before it is logged AND before it is returned to the worker — a program
50
50
  * that resolves a secret and then throws it in an error message must NOT land that secret raw in the
51
- * logs or the finalized run output (MASTER_SPEC §12, review #5). Defaults to identity (tests/local).
51
+ * logs or the finalized run output (the platform spec). Defaults to identity (tests/local).
52
52
  */
53
53
  redactText?: (text: string) => string;
54
54
  /**
@@ -59,7 +59,7 @@ export interface ProgramRunnerDeps {
59
59
  */
60
60
  onOutput?: (value: unknown) => void;
61
61
  /**
62
- * Resolves when a host seam SUSPENDS the run (docs/SUSPENSION.md) — the worker wires it to the
62
+ * Resolves when a host seam SUSPENDS the run (the durable-suspension design) — the worker wires it to the
63
63
  * host's `onSuspend` so a suspend is surfaced OUT OF BAND, racing the program body. The program's
64
64
  * own `try/catch` can't swallow a suspend this way (the suspending seam never resolves; this signal
65
65
  * short-circuits at the runner). Absent ⇒ no suspension wired (a seam that suspends throws
@@ -1,4 +1,4 @@
1
- // WorkflowProgramRunner — executes a workflow program (the JS-body model, docs/WORKFLOW_RUNTIME.md).
1
+ // WorkflowProgramRunner — executes a workflow program (the JS-body model, the workflow runtime design).
2
2
  //
3
3
  // A run is the execution of a built program ARTIFACT (§3.9): the worker is handed the VERIFIED tarball
4
4
  // (its sha256 already checked against the pinned digest by the orchestrator) plus the entry module
@@ -20,13 +20,13 @@
20
20
  // Durability: the body runs once, in-process. `sleep`/`workflows.call` hold in-process via the host
21
21
  // (no checkpoint, no exit). A crash mid-run restarts the run from the top (handled by the
22
22
  // worker/scheduler-sweep, not here). Output capture is deferred (v0 returns null).
23
- import { mkdir, writeFile, rm, symlink } from "node:fs/promises";
23
+ import { mkdir, writeFile, rm, symlink, lstat } from "node:fs/promises";
24
24
  import { dirname, join } from "node:path";
25
25
  import { createRequire } from "node:module";
26
26
  import { pathToFileURL } from "node:url";
27
27
  import { randomUUID } from "node:crypto";
28
28
  import { installHost, installInput, installConfig, takeDeclaredOutput, resetRuntime, } from "@boardwalk-labs/workflow/runtime";
29
- import { createLogger } from "./support/index.js";
29
+ import { AppError, ErrorCode, createLogger } from "./support/index.js";
30
30
  import { SuspendError } from "./suspension.js";
31
31
  const log = createLogger("ProgramRunner");
32
32
  /** Subdirectory (under the work root) that holds transient extracted program trees. */
@@ -41,6 +41,17 @@ const RUN_DIR = ".bw-runs";
41
41
  * still exist.
42
42
  */
43
43
  export async function ensureSdkLink(execDir) {
44
+ // A program tarball that ships its own `node_modules/@boardwalk-labs/workflow` would shadow the
45
+ // runtime's copy — the host adapter is installed on OUR instance, so the program's hooks would
46
+ // silently throw "no host installed". Fail loudly instead of link-then-EEXIST-swallow. Only a
47
+ // REAL entry is a vendored copy; a symlink is our own link (idempotent re-invocation), left be.
48
+ const linkPath = join(execDir, "node_modules", "@boardwalk-labs", "workflow");
49
+ const existing = await lstat(linkPath).catch(() => null);
50
+ if (existing !== null) {
51
+ if (existing.isSymbolicLink())
52
+ return;
53
+ throw new AppError(ErrorCode.VALIDATION_FAILED, "Program bundles its own @boardwalk-labs/workflow; the runtime provides it (do not vendor it).");
54
+ }
44
55
  try {
45
56
  // Resolve via the MAIN entry (the SDK's export map exposes no "./package.json") and cut the
46
57
  // path back to the package root.
@@ -44,7 +44,7 @@ export type RuntimeMeterStarter = (args: {
44
44
  export interface RunFinalizer {
45
45
  finalize(runId: string, status: "completed" | "failed", output: unknown): Promise<void>;
46
46
  }
47
- /** Persists a durable SUSPENSION (docs/SUSPENSION.md): the broker records the wake condition (a
47
+ /** Persists a durable SUSPENSION (the durable-suspension design): the broker records the wake condition (a
48
48
  * pending/suspended journal entry + a human-input request row for HITL, or the wake time for a long
49
49
  * sleep), flips the run to its suspended status, and releases the lease — all transactionally. The
50
50
  * run is NOT finalized; a wake (an answer, a child finalize, or a timer) re-dispatches it. */
@@ -74,7 +74,7 @@ export interface RunOutputHandle {
74
74
  /** Builds the per-run host (leaf + sleep + children + secrets) for a claimed run. Receives the run's
75
75
  * cooperative-cancellation `signal` so every host hook honors it (credit exhaustion / cancel).
76
76
  * Returns the run's `SecretRedactor` alongside the host so the orchestrator can scrub a terminal
77
- * error with the SAME instance every resolved secret was recorded into (review #5); `readUsage` — a
77
+ * error with the SAME instance every resolved secret was recorded into; `readUsage` — a
78
78
  * sampler over the run-level BudgetMeter the token flusher meters from; and an optional `workspace`
79
79
  * handle (when the workflow opted into persistence) the orchestrator hydrates at start + persists at
80
80
  * terminal (the host's `sleep` also persists, wired inside buildHost). */
@@ -94,7 +94,7 @@ export type ProgramHostBuilder = (run: Run, manifest: WorkflowManifest, signal:
94
94
  * it to the runner's `onExtracted`. Optional — absent on paths that don't surface bundled files. */
95
95
  setProgramDir?: (dir: string) => void;
96
96
  /** Resolves when a host seam SUSPENDS the run — wired to the host's `onSuspend`, threaded into the
97
- * program runner so a suspend short-circuits the body out of band (docs/SUSPENSION.md). Absent ⇒
97
+ * program runner so a suspend short-circuits the body out of band (the durable-suspension design). Absent ⇒
98
98
  * no durable suspension on this path (a suspend then surfaces as a thrown SuspendError). */
99
99
  suspendSignal?: Promise<SuspendSignal>;
100
100
  }>;
@@ -141,7 +141,7 @@ export interface ProgramWorkerDeps {
141
141
  /** Periodic runtime metering (optional — absent disables it, e.g. the local/test path). */
142
142
  startRuntimeFlush?: RuntimeMeterStarter;
143
143
  finalizer: RunFinalizer;
144
- /** Persists a durable suspension (docs/SUSPENSION.md). Absent ⇒ no suspension support: a run that
144
+ /** Persists a durable suspension (the durable-suspension design). Absent ⇒ no suspension support: a run that
145
145
  * reaches a suspend fails cleanly rather than stranding (the brokered worker always wires it). */
146
146
  suspender?: RunSuspender;
147
147
  buildHost: ProgramHostBuilder;
@@ -1,4 +1,4 @@
1
- // program_worker — the JS-body worker orchestration (docs/WORKFLOW_RUNTIME.md).
1
+ // program_worker — the JS-body worker orchestration (the workflow runtime design).
2
2
  //
3
3
  // Replaces the checkpoint-and-resume run_worker. One run, claim to terminal:
4
4
  // 1. Race-safe claim (pending → running). Lose the race → exit.
@@ -53,7 +53,7 @@ export async function runProgramWorker(runId, deps) {
53
53
  }
54
54
  // Fetch + verify the program ARTIFACT before any work (pre-flight; an integrity failure = no charge,
55
55
  // no run). The bytes are the EXACT artifact the manifest was derived from at deploy; verifying the
56
- // sha256 here means a tampered/corrupted object can never be imported (docs/WORKFLOW_RUNTIME.md §3.9).
56
+ // sha256 here means a tampered/corrupted object can never be imported (the workflow runtime design).
57
57
  let tarball;
58
58
  try {
59
59
  tarball = await deps.fetchProgram(loaded.program.downloadUrl);
@@ -162,7 +162,9 @@ export async function runProgramWorker(runId, deps) {
162
162
  // by `flushFinal()` after the body (on every path except a lease_lost handoff).
163
163
  const runtimeFlush = deps.startRuntimeFlush?.({ run: claimed, startedAtMs: sessionStartMs });
164
164
  // Capture the program's console.* as `log` run-events for the duration of the body (best-effort).
165
- const restoreConsole = deps.onProgramLog !== undefined ? captureConsole(deps.onProgramLog) : () => undefined;
165
+ const restoreConsole = deps.onProgramLog !== undefined
166
+ ? captureConsole(deps.onProgramLog, (text) => redactor.redactText(text))
167
+ : () => undefined;
166
168
  let result;
167
169
  try {
168
170
  result = await runWorkflowProgram({
@@ -1,4 +1,4 @@
1
- // RecordingSecretResolver — the redaction feeder (MASTER_SPEC §12, plan #9).
1
+ // RecordingSecretResolver — the redaction feeder (the platform spec).
2
2
  //
3
3
  // Decorates any SecretResolver so EVERY value a run resolves — whether the workflow program asked
4
4
  // via `secrets.get(name)` or a tool asked via `ctx.secrets.resolve(ref)` — is recorded into the
@@ -1,5 +1,4 @@
1
- // run_abort — the provider-agnostic cooperative-cancellation substrate for a run (docs/RUNNER_BROKER.md
2
- // §15 credit watching; the foundation user-initiated cancel will reuse).
1
+ // run_abort — the provider-agnostic cooperative-cancellation substrate for a run (the Runner Credential Broker model).
3
2
  //
4
3
  // The cancellation primitive is a Web-standard `AbortSignal` — NOT a Strands/model concept. The worker
5
4
  // owns one `AbortController` per run session; a watcher (credit, later user-cancel) calls
@@ -60,7 +60,7 @@ export declare class RunnerControlClient {
60
60
  * whose lease expired and whose run was reclaimed + re-dispatched to a new owner), so a
61
61
  * hung/partitioned worker that later recovers can't clobber the live run or revive a terminal one. */
62
62
  finalize(status: "completed" | "failed", output: unknown, workerId: string): Promise<void>;
63
- /** Look up a durable-seam journal entry by its seq (docs/SUSPENSION.md), or null on a replay miss
63
+ /** Look up a durable-seam journal entry by its seq (the durable-suspension design), or null on a replay miss
64
64
  * (404). The broker joins a parked agent leaf's answers into the result server-side. */
65
65
  journalGet(seq: number): Promise<JournalLookup | null>;
66
66
  /** Record a RESOLVED seam result (idempotent on the run + seq server-side; a resolved entry is
@@ -129,7 +129,7 @@ export declare class RunnerControlClient {
129
129
  /** Store a run artifact through the broker (which holds the S3 credential + neutralizes the served
130
130
  * content type server-side). Returns the catalog id + a signed download URL. */
131
131
  writeArtifact(input: ArtifactWriteInput): Promise<ArtifactWriteResult>;
132
- /** Phase 1 of the LARGE-artifact path (docs/RUNNER_BROKER.md §7 step 4): presign an S3 PUT. The
132
+ /** Phase 1 of the LARGE-artifact path (the Runner Credential Broker model): presign an S3 PUT. The
133
133
  * broker derives the S3 key + neutralizes/pins the served content type; it returns the upload URL +
134
134
  * required headers + the `s3Key` to echo back at commit. No catalog row exists yet. */
135
135
  presignArtifact(input: ArtifactPresignInput): Promise<ArtifactPresignResult>;
@@ -168,7 +168,7 @@ export declare class RunnerControlClient {
168
168
  output: unknown;
169
169
  } | null>;
170
170
  /**
171
- * Proxy one model turn through the broker (docs/RUNNER_BROKER.md §4 Inference). POSTs the
171
+ * Proxy one model turn through the broker (the Runner Credential Broker model). POSTs the
172
172
  * neutral conversation; the broker resolves the REAL model server-side (the runner holds no model
173
173
  * creds), invokes the matching engine adapter, and relays the model's stream back as NDJSON
174
174
  * `InferenceFrame`s (delta / result / error). Yields each frame; the engine-backed leaf surfaces
@@ -1,4 +1,4 @@
1
- // RunnerControlClient — the worker's HTTP client for the Runner Control API (docs/RUNNER_BROKER.md).
1
+ // RunnerControlClient — the worker's HTTP client for the Runner Control API (the Runner Credential Broker model).
2
2
  //
3
3
  // Under the broker model the worker reaches run lifecycle through the api-server, not the DB directly:
4
4
  // it presents its per-run token (BOARDWALK_RUN_TOKEN) as a bearer credential and calls the broker for
@@ -37,7 +37,7 @@ export class RunnerControlClient {
37
37
  return {
38
38
  run: body.run,
39
39
  lastEventCursor: body.lastEventCursor ?? 0,
40
- // The replay frontier for silent replay (docs/SUSPENSION.md): the highest journaled seq, so a
40
+ // The replay frontier for silent replay (the durable-suspension design): the highest journaled seq, so a
41
41
  // resumed run knows which seams already ran (suppress their re-emitted observability).
42
42
  lastJournalSeq: body.lastJournalSeq ?? 0,
43
43
  };
@@ -70,7 +70,7 @@ export class RunnerControlClient {
70
70
  if (res.status !== 204)
71
71
  throw await brokerError(res, "finalize");
72
72
  }
73
- /** Look up a durable-seam journal entry by its seq (docs/SUSPENSION.md), or null on a replay miss
73
+ /** Look up a durable-seam journal entry by its seq (the durable-suspension design), or null on a replay miss
74
74
  * (404). The broker joins a parked agent leaf's answers into the result server-side. */
75
75
  async journalGet(seq) {
76
76
  const res = await this.fetchImpl(this.url(`journal/${encodeURIComponent(String(seq))}`), {
@@ -243,7 +243,7 @@ export class RunnerControlClient {
243
243
  throw await brokerError(res, "artifacts");
244
244
  return (await res.json());
245
245
  }
246
- /** Phase 1 of the LARGE-artifact path (docs/RUNNER_BROKER.md §7 step 4): presign an S3 PUT. The
246
+ /** Phase 1 of the LARGE-artifact path (the Runner Credential Broker model): presign an S3 PUT. The
247
247
  * broker derives the S3 key + neutralizes/pins the served content type; it returns the upload URL +
248
248
  * required headers + the `s3Key` to echo back at commit. No catalog row exists yet. */
249
249
  async presignArtifact(input) {
@@ -381,7 +381,7 @@ export class RunnerControlClient {
381
381
  return (await res.json());
382
382
  }
383
383
  /**
384
- * Proxy one model turn through the broker (docs/RUNNER_BROKER.md §4 Inference). POSTs the
384
+ * Proxy one model turn through the broker (the Runner Credential Broker model). POSTs the
385
385
  * neutral conversation; the broker resolves the REAL model server-side (the runner holds no model
386
386
  * creds), invokes the matching engine adapter, and relays the model's stream back as NDJSON
387
387
  * `InferenceFrame`s (delta / result / error). Yields each frame; the engine-backed leaf surfaces
@@ -1,5 +1,5 @@
1
1
  // RuntimeFlusher — meters a run's held-task runtime as periodic DELTAS, instead of one charge at
2
- // terminal (MASTER_SPEC §15; docs/RUNNER_BROKER.md). The heartbeat counterpart to the credit / cancel
2
+ // terminal (the platform spec). 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
@@ -1,4 +1,4 @@
1
- // Durable-suspension primitives for the JS-body worker (docs/SUSPENSION.md).
1
+ // Durable-suspension primitives for the JS-body worker (the durable-suspension design).
2
2
  //
3
3
  // A run SUSPENDS at a durable seam — a long `sleep`, a `humanInput()` gate, or the in-leaf
4
4
  // `human_input` tool — by releasing its Fargate task and re-acquiring one on wake. The mechanism is
@@ -1,4 +1,4 @@
1
- // Server-side single-file artifact build (web / MCP deploy surface, docs/WORKFLOW_RUNTIME.md §3.9).
1
+ // Server-side single-file artifact build (web / MCP deploy surface, the workflow runtime design).
2
2
  //
3
3
  // The CLI bundles packages locally; web + MCP submit a single TS/JS file with no client bundler, so
4
4
  // the api-server builds the IDENTICAL artifact shape: type-strip the source → `index.mjs`, pack a
@@ -1,4 +1,4 @@
1
- // artifacts — persist + reference run outputs (MASTER_SPEC §9.1 + §13.6). Available to any agent
1
+ // artifacts — persist + reference run outputs (the platform spec). 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.
@@ -6,13 +6,13 @@
6
6
  //
7
7
  // This tool is a THIN agent-facing surface: it validates input + formats output and delegates the
8
8
  // actual storage to an injected `ArtifactStore`. Under the Runner Credential Broker
9
- // (docs/RUNNER_BROKER.md) the store is broker-backed — the broker computes the S3 key, neutralizes
10
- // the content type (review #32), PUTs with its own creds, and records the catalog row, so the
9
+ // (the Runner Credential Broker model) the store is broker-backed — the broker computes the S3 key, neutralizes
10
+ // the content type, PUTs with its own creds, and records the catalog row, so the
11
11
  // untrusted runner holds no S3 credential and can't bypass those server-side rules.
12
12
  import { z } from "zod";
13
13
  import { ARTIFACT_MAX_BYTES } from "../wire/artifact_storage.js";
14
14
  // Body cap: a small write travels through the broker's buffered Runner Control endpoint (≤5 MiB
15
- // wire); a large one takes the presigned-PUT path (docs/RUNNER_BROKER.md §7 step 4) that streams the
15
+ // wire); a large one takes the presigned-PUT path (the Runner Credential Broker model) that streams the
16
16
  // body straight to S3. Either way the artifact is bounded by ARTIFACT_MAX_BYTES — the cap here is on
17
17
  // the body STRING, sized for a full-size binary artifact carried as base64 (~4/3 inflation). The
18
18
  // broker re-validates the true byte size server-side (the authoritative check).
@@ -41,7 +41,7 @@ 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 CODE_QUALITY
44
+ * against this before it lands in the LLM conversation, per the code standards
45
45
  * §2.1/§8.3 (LLM-facing output is treated like untrusted external input).
46
46
  */
47
47
  readonly outputSchema: z.ZodType<TOutput>;
@@ -9,7 +9,7 @@
9
9
  // they bubble a signal up to the engine. Our adapter (lands with the worker
10
10
  // in Phase 10.5) translates between this interface and the Strands surface.
11
11
  //
12
- // Per MASTER_SPEC §12: tools NEVER see secret values. Secrets resolve to
12
+ // Per the platform spec: 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" &&
@@ -48,7 +48,7 @@ export interface WebSearchDeps {
48
48
  }
49
49
  export declare function makeWebSearchTool(deps: WebSearchDeps): BoardwalkTool<WebSearchInput, WebSearchOutput>;
50
50
  /**
51
- * Broker-backed web_search for the runner (docs/RUNNER_BROKER.md): identical name/schemas/behavior
51
+ * Broker-backed web_search for the runner (the Runner Credential Broker model): identical name/schemas/behavior
52
52
  * to {@link makeWebSearchTool}, but the Tavily call runs in the broker (which holds the platform key)
53
53
  * — the runner just forwards the query, so it needs NO Tavily secret (`secretsRequired: []`). Wired on
54
54
  * the worker when the control plane is on; the direct tool above is the pre-broker/local path.
@@ -1,6 +1,6 @@
1
1
  // web_search — wraps the Tavily API (https://docs.tavily.com/docs/api-reference).
2
2
  //
3
- // Per MASTER_SPEC §13.5: Tavily is Boardwalk's default web-search provider. API
3
+ // Per the platform spec: Tavily is Boardwalk's default web-search provider. API
4
4
  // key fetched from Secrets Manager (`boardwalk/<stage>/tavily/api-key`) and
5
5
  // cached at the module level per container, NEVER appearing in the agent's
6
6
  // conversation.
@@ -105,7 +105,7 @@ export function makeWebSearchTool(deps) {
105
105
  };
106
106
  }
107
107
  /**
108
- * Broker-backed web_search for the runner (docs/RUNNER_BROKER.md): identical name/schemas/behavior
108
+ * Broker-backed web_search for the runner (the Runner Credential Broker model): identical name/schemas/behavior
109
109
  * to {@link makeWebSearchTool}, but the Tavily call runs in the broker (which holds the platform key)
110
110
  * — the runner just forwards the query, so it needs NO Tavily secret (`secretsRequired: []`). Wired on
111
111
  * the worker when the control plane is on; the direct tool above is the pre-broker/local path.
@@ -17,7 +17,7 @@ export declare function extFor(name: string, contentType: string): string;
17
17
  /**
18
18
  * Map an agent-supplied content type to one safe to serve inline from a Boardwalk origin. Active
19
19
  * types are forced to text/plain so the browser renders them as inert text instead of executing
20
- * embedded script (review #32). The base type (before any `;` params) is what's matched. The body is
20
+ * embedded script. The base type (before any `;` params) is what's matched. The body is
21
21
  * never altered — only the type the object is SERVED as.
22
22
  *
23
23
  * MUST run server-side (in the broker), never on the untrusted runner: the served content type is
@@ -1,6 +1,6 @@
1
1
  // Artifact storage policy — the server-side rules for turning an agent's artifact-write request into
2
- // a stored S3 object (MASTER_SPEC §13.6, review #32). These used to live in the `artifacts` TOOL,
3
- // which runs on the UNTRUSTED runner; under the Runner Credential Broker (docs/RUNNER_BROKER.md) the
2
+ // a stored S3 object (the platform spec). These used to live in the `artifacts` TOOL,
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.
6
6
  import * as nodePath from "node:path";
@@ -60,7 +60,7 @@ const NEUTRAL_CONTENT_TYPE = "text/plain; charset=utf-8";
60
60
  /**
61
61
  * Map an agent-supplied content type to one safe to serve inline from a Boardwalk origin. Active
62
62
  * types are forced to text/plain so the browser renders them as inert text instead of executing
63
- * embedded script (review #32). The base type (before any `;` params) is what's matched. The body is
63
+ * embedded script. The base type (before any `;` params) is what's matched. The body is
64
64
  * never altered — only the type the object is SERVED as.
65
65
  *
66
66
  * MUST run server-side (in the broker), never on the untrusted runner: the served content type is
@@ -74,7 +74,7 @@ export function neutralizeActiveContentType(contentType) {
74
74
  //
75
75
  // The proxy path (`POST .../artifacts`) buffers the base64 body through the Runner Control body
76
76
  // limit (5 MiB wire), so it's bounded WELL under that. Anything larger takes the presigned-PUT path
77
- // (`POST .../artifacts/presign`, docs/RUNNER_BROKER.md §7 step 4): the broker signs an S3 PUT and the
77
+ // (`POST .../artifacts/presign`, the Runner Credential Broker model): the broker signs an S3 PUT and the
78
78
  // runner streams the bytes straight to S3, so the body never passes through the control plane.
79
79
  /** Largest raw artifact body the broker will accept INLINE (proxied). Comfortably under the 5 MiB
80
80
  * control-plane body cap so the base64 string + JSON envelope fit. Above this ⇒ presigned PUT. */
@@ -34,7 +34,7 @@ export declare function normalizeHumanInputResult(raw: unknown): HumanInputResul
34
34
  * interface with optional props, which TS won't structurally accept as JsonValue; an explicit object
35
35
  * literal IS assignable to the index signature). The inverse of {@link normalizeHumanInputResult}. */
36
36
  export declare function humanInputResultToJson(result: HumanInputResult): JsonValue;
37
- /** What to do when a gate's `timeout` elapses with no answer (docs/SUSPENSION.md §4.3). Mirrors the
37
+ /** What to do when a gate's `timeout` elapses with no answer (the durable-suspension design). Mirrors the
38
38
  * SDK's `HumanInputOptions.onTimeout`: fail the run, or resolve the gate with a default value. */
39
39
  export type OnTimeoutPolicy = {
40
40
  kind: "fail";
@@ -1,4 +1,4 @@
1
- // Human-input form spec + response validation (docs/SUSPENSION.md §4.3).
1
+ // Human-input form spec + response validation (the durable-suspension design).
2
2
  //
3
3
  // The `HumanInputSpec` is the single source of truth for BOTH the UI form and server-side
4
4
  // validation, so a `humanInput()` gate needs no JSON Schema. A responder's raw submission (from
@@ -1,4 +1,4 @@
1
- // Inference proxy protocol (docs/RUNNER_BROKER.md §4 "Inference (class 2)").
1
+ // Inference proxy protocol (the Runner Credential Broker model)").
2
2
  //
3
3
  // The runner never invokes a model directly: under the Runner Credential Broker it holds no
4
4
  // managed-inference creds and no BYO provider key. Instead the `agent()` leaf's model turn (the
@@ -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 — MASTER_SPEC §4.2). Each frame is one of:
16
+ // buffered REST path can't stream — the platform spec). 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
@@ -72,7 +72,7 @@ export declare class TimerSleepController implements SleepController {
72
72
  * `import { runtime } from "@boardwalk-labs/workflow"`. MIRRORS the SDK's `RuntimeContext` — defined
73
73
  * locally so the host compiles BEFORE the @boardwalk-labs/workflow bump that adds the optional
74
74
  * `runtime` member to `WorkflowHost`; once that lands, the host's `runtime` satisfies it
75
- * structurally. Platform credentials are NEVER placed in `process.env` (docs/RUN_ENV_AND_CREDS.md):
75
+ * structurally. Platform credentials are NEVER placed in `process.env` (the run env/credential rules):
76
76
  * trusted program code reaches the public-API bearer ONLY through `apiToken()`, which is redacted
77
77
  * from all LLM context.
78
78
  */
@@ -113,7 +113,7 @@ export interface WorkerWorkflowHostDeps {
113
113
  /** Override the 7-day hold ceiling (tests). */
114
114
  maxSleepMs?: number;
115
115
  /**
116
- * Durable-suspension journal (docs/SUSPENSION.md): the host memoizes each `agent`/`step`/`sleep`/
116
+ * Durable-suspension journal (the durable-suspension design): the host memoizes each `agent`/`step`/`sleep`/
117
117
  * `humanInput` seam here, so a resumed run replays journaled seams instantly instead of re-running
118
118
  * them. Backed by the broker over the run token on hosted runs. Absent ⇒ no memoization (the
119
119
  * local/test path): seams run live every time and a suspend can't be resumed.
@@ -138,7 +138,7 @@ export declare class WorkerWorkflowHost implements WorkflowHost {
138
138
  private readonly sleeper;
139
139
  private readonly now;
140
140
  private readonly maxSleepMs;
141
- /** Synchronous durable-seam counter + silent-replay live flag (docs/SUSPENSION.md). */
141
+ /** Synchronous durable-seam counter + silent-replay live flag (the durable-suspension design). */
142
142
  private readonly seq;
143
143
  /** Run context + on-demand public-API bearer the SDK `runtime` accessor reads off the host. */
144
144
  readonly runtime: RuntimeContext;
@@ -171,7 +171,7 @@ export declare class WorkerWorkflowHost implements WorkflowHost {
171
171
  step(name: string, fn: () => unknown): Promise<unknown>;
172
172
  private stepSeam;
173
173
  callWorkflow(slug: string, input: unknown, opts: CallOptions | undefined): Promise<unknown>;
174
- /** The `workflows.call` durable seam (docs/SUSPENSION.md): start the child once + memoize its
174
+ /** The `workflows.call` durable seam (the durable-suspension design): start the child once + memoize its
175
175
  * output; a non-terminal child SUSPENDS the parent (`waiting_for_child`, the child's id journaled)
176
176
  * — the parent releases its task and is woken when the child finalizes. On resume the seam polls
177
177
  * the journaled child and returns its output (or throws on a failed child). Without a journal (the
@@ -1,5 +1,5 @@
1
1
  // WorkerWorkflowHost — the real WorkflowHost the worker installs onto @boardwalk-labs/workflow
2
- // before running a program (docs/WORKFLOW_RUNTIME.md §3.8). The program's hooks delegate here:
2
+ // before running a program (the workflow runtime design). The program's hooks delegate here:
3
3
  //
4
4
  // agent(prompt, opts) → an ephemeral Strands agent leaf (the demoted agent loop)
5
5
  // sleep(arg) → an IN-PROCESS hold (hold-and-pay; no checkpoint, no exit)
@@ -67,7 +67,7 @@ export class WorkerWorkflowHost {
67
67
  sleeper;
68
68
  now;
69
69
  maxSleepMs;
70
- /** Synchronous durable-seam counter + silent-replay live flag (docs/SUSPENSION.md). */
70
+ /** Synchronous durable-seam counter + silent-replay live flag (the durable-suspension design). */
71
71
  seq;
72
72
  /** Run context + on-demand public-API bearer the SDK `runtime` accessor reads off the host. */
73
73
  runtime;
@@ -240,7 +240,7 @@ export class WorkerWorkflowHost {
240
240
  callWorkflow(slug, input, opts) {
241
241
  return this.guarded(() => this.callWorkflowSeam(slug, input, opts));
242
242
  }
243
- /** The `workflows.call` durable seam (docs/SUSPENSION.md): start the child once + memoize its
243
+ /** The `workflows.call` durable seam (the durable-suspension design): start the child once + memoize its
244
244
  * output; a non-terminal child SUSPENDS the parent (`waiting_for_child`, the child's id journaled)
245
245
  * — the parent releases its task and is woken when the child finalizes. On resume the seam polls
246
246
  * the journaled child and returns its output (or throws on a failed child). Without a journal (the
@@ -1,4 +1,4 @@
1
- // WorkspaceStore — per-workflow persistent /workspace, broker-mediated (docs/RUNNER_BROKER.md §5).
1
+ // WorkspaceStore — per-workflow persistent /workspace, broker-mediated (the Runner Credential Broker model).
2
2
  //
3
3
  // Hosted runs that opt in (manifest `workspace.persist`) get a `/workspace` that survives ACROSS runs
4
4
  // of the workflow + across a crash-restart. It is NOT a mounted shared filesystem — that would be
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boardwalk-labs/runner",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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": {