@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
@@ -0,0 +1,221 @@
1
+ // Production seams for the browser tier — the guest-coupled halves of browser_session.ts's injected
2
+ // contracts: the `BrowserBackend` (spawn a program-owned CDP Chromium on the guest display + a
3
+ // per-session Playwright MCP HTTP server attached to it) and the program's MCP client factory
4
+ // (`connectSessionMcp`). See docs/COMPUTER_USE_SESSION.md.
5
+ //
6
+ // These only run where the runner IMAGE ships the browser stack (Chromium + a pre-installed
7
+ // Playwright MCP + an X display). The composition root gates on `browserTierEnabled(env)` and only
8
+ // constructs a BrowserSessionManager with this backend when the image opts in — elsewhere
9
+ // `computer.openBrowser()` fails with a clear "not available on this runner image".
10
+ //
11
+ // Config proven against Playwright MCP 0.0.77 (spike, 2026-07-09):
12
+ // - HTTP mode (`--port`) + `--cdp-endpoint` attach + a StreamableHTTP client all work.
13
+ // - The agent's + the program's MCP URL MUST use `localhost` (not 127.0.0.1): the `/mcp` endpoint
14
+ // enforces a literal `localhost` Host as DNS-rebinding protection, and `--allowed-hosts '*'`
15
+ // does NOT lift it. So `mcpUrl` is built with `localhost`; the engine binds the same ref.
16
+ // - `capabilities:['core']` does NOT drop `browser_evaluate` / `browser_run_code_unsafe` — they
17
+ // are core tools. Excluding them from the AGENT (they stay available to the trusted program's
18
+ // `eval()`) is an engine-level `excludeTools` follow-up, tracked in COMPUTER_USE_SESSION.md.
19
+ import { spawn } from "node:child_process";
20
+ import { createServer } from "node:net";
21
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
22
+ import { tmpdir } from "node:os";
23
+ import { join } from "node:path";
24
+ import { setTimeout as delay } from "node:timers/promises";
25
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
26
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
27
+ import { AppError, ErrorCode, createLogger } from "./support/index.js";
28
+ const log = createLogger("browser_backend");
29
+ const DEFAULT_MCP_PACKAGE = "@playwright/mcp@0.0.77";
30
+ /** True when the runner image declares the browser stack present (BOARDWALK_BROWSER_TIER=1). */
31
+ export function browserTierEnabled(env) {
32
+ return env.BOARDWALK_BROWSER_TIER === "1";
33
+ }
34
+ /** Read the guest browser config from env, or null when the tier is disabled / no Chrome path is set. */
35
+ export function loadGuestBrowserConfig(env) {
36
+ if (!browserTierEnabled(env))
37
+ return null;
38
+ const chromePath = env.BOARDWALK_BROWSER_CHROME_PATH?.trim();
39
+ if (chromePath === undefined || chromePath.length === 0) {
40
+ log.warn("browser_tier_missing_chrome_path");
41
+ return null;
42
+ }
43
+ // A custom command (a direct bin) takes no npx prefix; the default is `npx --no-install <pkg>`.
44
+ const command = env.BOARDWALK_BROWSER_MCP_COMMAND?.trim();
45
+ const baseArgs = command === undefined || command.length === 0
46
+ ? ["--yes", "--no-install", env.BOARDWALK_BROWSER_MCP_PACKAGE?.trim() || DEFAULT_MCP_PACKAGE]
47
+ : (env.BOARDWALK_BROWSER_MCP_ARGS ?? "").split(/\s+/).filter((a) => a.length > 0);
48
+ const timeout = Number(env.BOARDWALK_BROWSER_READY_TIMEOUT_MS);
49
+ return {
50
+ chromePath,
51
+ display: env.DISPLAY?.trim() || ":0",
52
+ mcpCommand: command !== undefined && command.length > 0 ? command : "npx",
53
+ mcpBaseArgs: baseArgs,
54
+ readyTimeoutMs: Number.isFinite(timeout) && timeout > 0 ? timeout : 30_000,
55
+ };
56
+ }
57
+ /** Bind an ephemeral port, read it, release it — then hand it to the child. A tiny TOCTOU window, but
58
+ * each run owns its VM/container so nothing else competes for the loopback port. */
59
+ export async function freePort() {
60
+ return await new Promise((resolve, reject) => {
61
+ const srv = createServer();
62
+ srv.once("error", reject);
63
+ srv.listen(0, "127.0.0.1", () => {
64
+ const addr = srv.address();
65
+ if (addr === null || typeof addr === "string") {
66
+ srv.close(() => reject(new Error("could not allocate a port")));
67
+ return;
68
+ }
69
+ const { port } = addr;
70
+ srv.close(() => resolve(port));
71
+ });
72
+ });
73
+ }
74
+ /** Poll an HTTP URL until it answers (any status < 500 counts as "up" — Playwright MCP's localhost
75
+ * guard returns 403 to a bare GET, which still proves the server is listening). */
76
+ export async function waitForHttp(url, timeoutMs) {
77
+ const deadline = Date.now() + timeoutMs;
78
+ let lastErr = "no response";
79
+ while (Date.now() < deadline) {
80
+ try {
81
+ const res = await fetch(url);
82
+ if (res.status < 500)
83
+ return;
84
+ lastErr = `status ${String(res.status)}`;
85
+ }
86
+ catch (err) {
87
+ lastErr = err instanceof Error ? err.message : String(err);
88
+ }
89
+ await delay(200);
90
+ }
91
+ throw new AppError(ErrorCode.INTERNAL_ERROR, `browser backend: ${url} never came up (${lastErr})`, {
92
+ kind: "browser_backend_timeout",
93
+ });
94
+ }
95
+ function killProc(proc) {
96
+ try {
97
+ if (proc.exitCode === null && proc.signalCode === null)
98
+ proc.kill("SIGKILL");
99
+ }
100
+ catch {
101
+ // best-effort: a dead child must not throw out of teardown.
102
+ }
103
+ }
104
+ /**
105
+ * Build the production BrowserBackend. Each `launch` allocates two loopback ports, spawns a
106
+ * program-owned Chromium (CDP endpoint, headful on the guest display, isolated profile) and a
107
+ * per-session Playwright MCP HTTP server attached to it, waits for both to answer, and returns the
108
+ * handle the manager wraps. On any failure it tears down everything it started (no leaked processes /
109
+ * temp profiles).
110
+ */
111
+ export function makeGuestBrowserBackend(cfg) {
112
+ return {
113
+ async launch(opts) {
114
+ const cdpPort = await freePort();
115
+ const mcpPort = await freePort();
116
+ const profileDir = await mkdtemp(join(tmpdir(), "bw-browser-"));
117
+ const cdpUrl = `http://127.0.0.1:${String(cdpPort)}`;
118
+ // localhost (NOT 127.0.0.1) — the Playwright MCP /mcp endpoint rejects any other Host (spike).
119
+ const mcpUrl = `http://localhost:${String(mcpPort)}/mcp`;
120
+ const started = [];
121
+ const cleanup = async () => {
122
+ for (const p of started)
123
+ killProc(p);
124
+ await rm(profileDir, { recursive: true, force: true }).catch(() => undefined);
125
+ };
126
+ try {
127
+ // 1) Program-owned Chromium with a CDP endpoint, headful on the guest display so the desktop
128
+ // tier + recording can mirror it. The isolated profile dir is this session's alone.
129
+ const chrome = spawn(cfg.chromePath, [
130
+ `--remote-debugging-port=${String(cdpPort)}`,
131
+ `--user-data-dir=${profileDir}`,
132
+ "--no-first-run",
133
+ "--no-default-browser-check",
134
+ "--disable-features=Translate",
135
+ ...(opts?.startUrl !== undefined ? [opts.startUrl] : ["about:blank"]),
136
+ ], { stdio: "ignore", env: { ...process.env, DISPLAY: cfg.display } });
137
+ started.push(chrome);
138
+ chrome.once("error", (err) => {
139
+ log.error("browser_chrome_spawn_error", { error: err.message });
140
+ });
141
+ await waitForHttp(`${cdpUrl}/json/version`, cfg.readyTimeoutMs);
142
+ // 2) Per-session Playwright MCP in HTTP mode, attached to the CDP browser. `sharedBrowserContext`
143
+ // so the program's client and the agent's engine connection drive the SAME browser context.
144
+ const configPath = join(profileDir, "pw-mcp.json");
145
+ await writeFile(configPath, JSON.stringify({ capabilities: ["core"], sharedBrowserContext: true }));
146
+ const mcp = spawn(cfg.mcpCommand, [
147
+ ...cfg.mcpBaseArgs,
148
+ "--port",
149
+ String(mcpPort),
150
+ "--host",
151
+ "127.0.0.1",
152
+ // Only loopback clients exist inside the VM; disable the DNS-rebinding host list (the
153
+ // literal-localhost guard on /mcp still applies, which is why mcpUrl uses `localhost`).
154
+ "--allowed-hosts",
155
+ "*",
156
+ "--cdp-endpoint",
157
+ cdpUrl,
158
+ "--config",
159
+ configPath,
160
+ ], { stdio: ["ignore", "ignore", "inherit"], env: { ...process.env, DISPLAY: cfg.display } });
161
+ started.push(mcp);
162
+ mcp.once("error", (err) => {
163
+ log.error("browser_mcp_spawn_error", { error: err.message });
164
+ });
165
+ await waitForHttp(`http://127.0.0.1:${String(mcpPort)}/mcp`, cfg.readyTimeoutMs);
166
+ let killed = false;
167
+ return {
168
+ cdpUrl,
169
+ mcpUrl,
170
+ kill: async () => {
171
+ if (killed)
172
+ return;
173
+ killed = true;
174
+ await cleanup();
175
+ },
176
+ };
177
+ }
178
+ catch (err) {
179
+ await cleanup();
180
+ throw err;
181
+ }
182
+ },
183
+ };
184
+ }
185
+ /** Adapt one MCP tool-result content block (the SDK's loose shape) to the subset browser_session.ts reads. */
186
+ export function toContentBlock(block) {
187
+ if (block === null || typeof block !== "object")
188
+ return { type: "unknown" };
189
+ const b = block;
190
+ const out = { type: typeof b.type === "string" ? b.type : "unknown" };
191
+ if (typeof b.text === "string")
192
+ out.text = b.text;
193
+ if (typeof b.data === "string")
194
+ out.data = b.data;
195
+ if (typeof b.mimeType === "string")
196
+ out.mimeType = b.mimeType;
197
+ return out;
198
+ }
199
+ /**
200
+ * The program's MCP client factory: open a StreamableHTTP client to a session's Playwright MCP server
201
+ * (the trusted PROGRAM's channel, distinct from the engine's separate connection the AGENT uses). Wraps
202
+ * the SDK Client behind the manager's `SessionMcpCaller` seam. `mcpUrl` must be the `localhost` form.
203
+ */
204
+ export async function connectSessionMcp(mcpUrl) {
205
+ const client = new Client({ name: "boardwalk-runner", version: "1" });
206
+ // The SDK's concrete transport types `sessionId` as `string | undefined`, which trips our
207
+ // exactOptionalPropertyTypes against the `Transport` interface's optional `sessionId?`. Cast at
208
+ // this one seam — a pure type-strictness artifact, not a runtime mismatch.
209
+ const transport = new StreamableHTTPClientTransport(new URL(mcpUrl));
210
+ await client.connect(transport);
211
+ return {
212
+ callTool: async (name, args) => {
213
+ const result = await client.callTool({ name, arguments: args });
214
+ const content = Array.isArray(result.content) ? result.content.map(toContentBlock) : [];
215
+ return { content, isError: result.isError === true };
216
+ },
217
+ close: async () => {
218
+ await client.close();
219
+ },
220
+ };
221
+ }
@@ -1,4 +1,4 @@
1
- // CancelWatcher — stops a run when the user cancels it mid-flight (the platform spec).
1
+ // CancelWatcher — stops a run when the user cancels it mid-flight.
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
@@ -4,7 +4,7 @@ export interface CreditWatcherDeps {
4
4
  /** The run being watched (for correlation/logging). */
5
5
  runId: string;
6
6
  /** Resolves true while the org can keep spending; false once it's out of credit. Brokered
7
- * (`RunnerControlClient.checkCredit` → `GET /credit`), so the runner never reads Stripe. */
7
+ * (`RunnerControlClient.checkCredit` → `GET /credit`), so the runner never reads billing state directly. */
8
8
  isFunded: () => Promise<boolean>;
9
9
  /** Fired exactly once when the org is first seen to be out of credit. */
10
10
  onExhausted: () => void;
@@ -1,14 +1,14 @@
1
- // CreditWatcher — stops a run when its org runs out of prepaid credit mid-flight (the platform spec).
1
+ // CreditWatcher — stops a run when its org runs out of prepaid credit mid-flight.
2
2
  //
3
3
  // One per run session. On a timer it asks — through the broker (`GET /credit`, since the runner holds
4
- // no Stripe credential) — whether the org is still funded; the FIRST time it isn't, it fires
4
+ // no billing credential) — whether the org is still funded; the FIRST time it isn't, it fires
5
5
  // `onExhausted` once (the orchestrator wires this to `AbortController.abort`, which the WorkflowHost
6
6
  // honors cooperatively) and stops checking. This is the org-level counterpart to the per-run
7
- // BudgetMeter cap: the run can't see Stripe balances, so funding is enforced here, out of band,
7
+ // BudgetMeter cap: the run can't see its billing balance, so funding is enforced here, out of band,
8
8
  // against the live balance that incremental token metering keeps fresh.
9
9
  //
10
- // "Prompt, not instant": Stripe meter aggregation is eventually-consistent and the host honors the
11
- // abort at the next hook boundary, so a run stops within ~one check interval (plus Stripe's lag) of
10
+ // "Prompt, not instant": the platform's usage metering is eventually-consistent and the host honors the
11
+ // abort at the next hook boundary, so a run stops within ~one check interval (plus metering lag) of
12
12
  // going unfunded — bounding overshoot, not eliminating it. Applies to MANAGED and BYOK runs alike,
13
13
  // since runtime always burns credit (the gate requires the runtime floor regardless of token billing).
14
14
  import { createLogger } from "./support/index.js";
@@ -51,6 +51,11 @@ export type FreezeOutcome = {
51
51
  } | {
52
52
  kind: "aborted";
53
53
  reason: string;
54
+ }
55
+ /** The caller withdrew the wait via its abort signal BEFORE it froze (register-without-release:
56
+ * a held gate got its answer during the hold, so it resolves in-process instead of freezing). */
57
+ | {
58
+ kind: "withdrawn";
54
59
  };
55
60
  export interface FreezeCoordinatorHooks {
56
61
  /** Runs at quiescence, immediately before the freeze is requested: flush billable runtime
@@ -104,7 +109,7 @@ export declare class FreezeCoordinator {
104
109
  * the abort (the seam then holds in-process); other reasons retry the freeze after a
105
110
  * backoff. Concurrent suspending waits serialize through an internal chain.
106
111
  */
107
- suspendingWait(signal: SuspendSignal): Promise<FreezeOutcome>;
112
+ suspendingWait(signal: SuspendSignal, abort?: AbortSignal): Promise<FreezeOutcome>;
108
113
  /** Relay handler: a wake injection landed. Validates, runs the after-wake hook (token
109
114
  * swap + meter rebase), confirms to init, and resolves the parked seam. A wake with no
110
115
  * parked seam is a duplicate delivery — re-confirm (idempotent), never crash. */
@@ -126,7 +126,7 @@ export class FreezeCoordinator {
126
126
  * the abort (the seam then holds in-process); other reasons retry the freeze after a
127
127
  * backoff. Concurrent suspending waits serialize through an internal chain.
128
128
  */
129
- suspendingWait(signal) {
129
+ suspendingWait(signal, abort) {
130
130
  const turn = this.chain;
131
131
  let release = () => undefined;
132
132
  this.chain = new Promise((resolve) => {
@@ -137,7 +137,14 @@ export class FreezeCoordinator {
137
137
  try {
138
138
  let backoff = ABORT_RETRY_INITIAL_MS;
139
139
  for (;;) {
140
- await this.awaitQuiescence();
140
+ // Withdraw window: the abort can cancel the wait any time BEFORE it sends
141
+ // suspend_request (once sent, the VM freezes and the abort is moot — the process is
142
+ // paused). This is register-without-release: a held gate answered during the hold.
143
+ // awaitQuiescence short-circuits when already aborted, so one check after it covers
144
+ // both loop entry and a mid-hold abort.
145
+ await this.awaitQuiescence(abort);
146
+ if (abort?.aborted === true)
147
+ return { kind: "withdrawn" };
141
148
  try {
142
149
  await this.hooks.onBeforeFreeze?.();
143
150
  }
@@ -167,6 +174,10 @@ export class FreezeCoordinator {
167
174
  this.releaseGate();
168
175
  if (outcome.kind === "wake")
169
176
  return outcome;
177
+ // The park only ever resolves wake or aborted (never withdrawn — that returns before the
178
+ // park). Narrow explicitly so `.reason` is safe and a stray outcome propagates.
179
+ if (outcome.kind !== "aborted")
180
+ return outcome;
170
181
  if (signal.reason === "sleep")
171
182
  return outcome;
172
183
  log.warn("suspend_aborted_retrying", {
@@ -228,11 +239,14 @@ export class FreezeCoordinator {
228
239
  this.parked = null;
229
240
  resolve({ kind: "aborted", reason });
230
241
  }
231
- awaitQuiescence() {
232
- if (this.inFlight === 0)
242
+ awaitQuiescence(abort) {
243
+ if (this.inFlight === 0 || abort?.aborted === true)
233
244
  return Promise.resolve();
234
245
  return new Promise((resolve) => {
246
+ // Resolve on quiescence OR abort (a held wait wakes immediately to withdraw). Resolving
247
+ // twice is harmless; endWork may still call the queued waiter later.
235
248
  this.quiescenceWaiters.push(resolve);
249
+ abort?.addEventListener("abort", () => resolve(), { once: true });
236
250
  });
237
251
  }
238
252
  releaseGate() {
@@ -2,12 +2,13 @@ import type { AuthContext } from "./support/index.js";
2
2
  import type { Run } from "./wire/run.js";
3
3
  import { type IdentityRelay } from "./identity_relay.js";
4
4
  import type { ByoInferenceProvider } from "../contract.js";
5
+ import { type BrowserBackend } from "./browser_session.js";
5
6
  import { type ProgramWorkerDeps } from "./program_worker.js";
6
7
  /** Constructed primitives the pure assembly consumes (real ones in main(), fakes in tests). The
7
8
  * runner holds NO platform credential — only the per-run control-plane handle (run token + broker
8
9
  * URL); everything else is reached through the Runner Control API (the Runner Credential Broker model). */
9
10
  export interface WorkerRuntime {
10
- /** Stable worker identity stamped onto the lease (ECS task arn / hostname / run-derived). */
11
+ /** Stable worker identity stamped onto the lease (task ARN / hostname / run-derived). */
11
12
  workerId: string;
12
13
  /** Sandbox workspace root for filesystem/shell/git tools. */
13
14
  workspaceRoot: string;
@@ -32,14 +33,18 @@ export interface WorkerRuntime {
32
33
  * boot). When set, the worker suspends by FREEZING — the FreezeCoordinator parks seams over this
33
34
  * relay's suspend/wake channel — instead of the exit-and-replay path. */
34
35
  freezeRelay?: IdentityRelay;
36
+ /** The browser tier's process backend, present ONLY when the runner IMAGE ships the browser stack
37
+ * (Chromium + a pre-installed Playwright MCP + an X display; gated by BOARDWALK_BROWSER_TIER).
38
+ * When absent, `computer.openBrowser()` fails with a clear "not available on this runner image". */
39
+ browserBackend?: BrowserBackend;
35
40
  }
36
- /** Durable events are deferred (run_step_events table) — live fan-out via the broker only. */
41
+ /** Durable events are deferred (durable event storage) — live fan-out via the broker only. */
37
42
  /** Synthesize the run's AuthContext. The run was already authorized at trigger time; the
38
43
  * worker acts on the org's behalf, so it carries the org + an owner role. Tool-level
39
44
  * boundaries (the broker's server-side manifest allowlist) are the real guard.
40
45
  *
41
46
  * source='workflow' (NOT 'session_jwt'): the program must never perform SESSION_JWT_ONLY
42
- * credential mutations (§12.11), so a tool that ever exposed such a service is denied by
47
+ * credential mutations, so a tool that ever exposed such a service is denied by
43
48
  * construction regardless of the owner role. */
44
49
  export declare function workerAuthContext(run: Run): AuthContext;
45
50
  /**
@@ -57,10 +62,10 @@ export declare function workerAuthContext(run: Run): AuthContext;
57
62
  */
58
63
  export declare function tokenMeterIdentifiers(runId: string, sessionId: string): (leafIndex: number) => string;
59
64
  /** Pure wiring: build the full ProgramWorkerDeps from the control-plane handle. The runner reaches
60
- * every privileged seam through the broker over its run token — no DB / Redis / Stripe / model creds. */
65
+ * every privileged seam through the broker over its run token — no database, cache, billing, or model creds. */
61
66
  export declare function assembleWorkerDeps(runtime: WorkerRuntime): ProgramWorkerDeps;
62
67
  /**
63
- * The per-run platform values the dispatcher injects as container env (ecs_run_task_client.ts). They
68
+ * The per-run platform values the dispatcher injects as container env. They
64
69
  * are captured into private worker state at bootstrap and DELETED from `process.env` before any user
65
70
  * program / agent leaf / subprocess can run — the run token + API token are credentials, and nothing
66
71
  * untrusted run code touches should inherit them (the run env/credential rules). The user owns the rest
@@ -78,7 +83,7 @@ export interface PlatformContext {
78
83
  vcpus: number;
79
84
  }
80
85
  /** The public-API origin a run uses for raw API / MCP / CLI calls. The broker (Runner Control API)
81
- * is hosted on the api-server, so the public API shares its origin; the program appends `/v1` or
86
+ * shares an origin with the public API, so the program appends `/v1` or
82
87
  * `/mcp/v1`. Falls back to the broker URL unchanged if it can't be parsed. */
83
88
  export declare function publicApiOrigin(controlPlaneBaseUrl: string): string;
84
89
  /**