@workerdeck/core 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.mjs CHANGED
@@ -410,6 +410,8 @@ var SessionRunner = class {
410
410
  id;
411
411
  createdAt;
412
412
  #config;
413
+ /** {@link SessionRunnerConfig.cwd}, checked once in the constructor. */
414
+ #cwd;
413
415
  #events = [];
414
416
  #listeners = /* @__PURE__ */ new Set();
415
417
  #seq = 0;
@@ -434,6 +436,8 @@ var SessionRunner = class {
434
436
  #closed = false;
435
437
  #runPromise;
436
438
  constructor(config, id = randomUUID()) {
439
+ if (!config.cwd) throw new Error("the claude engine requires a cwd");
440
+ this.#cwd = config.cwd;
437
441
  this.#config = config;
438
442
  this.#permissionMode = config.permissionMode;
439
443
  this.id = id;
@@ -460,7 +464,7 @@ var SessionRunner = class {
460
464
  id: this.id,
461
465
  sdkSessionId: this.#sdkSessionId,
462
466
  status: this.#status,
463
- cwd: this.#config.cwd,
467
+ cwd: this.#cwd,
464
468
  profile: this.#config.profile,
465
469
  engine: "claude",
466
470
  capabilities: ENGINE_CAPABILITIES.claude,
@@ -473,6 +477,7 @@ var SessionRunner = class {
473
477
  activityCount: this.#activityCount,
474
478
  pendingPermissionCount: this.#pending.size,
475
479
  meta: this.#config.meta,
480
+ scope: this.#config.scope,
476
481
  title: this.#title(),
477
482
  totalCostUsd: this.#totalCostUsd,
478
483
  numTurns: this.#numTurns,
@@ -665,7 +670,7 @@ var SessionRunner = class {
665
670
  const historyFn = c.historyFn ?? ((sessionId, options) => getSessionMessages(sessionId, options));
666
671
  let messages;
667
672
  try {
668
- messages = await historyFn(c.resume, { dir: c.cwd });
673
+ messages = await historyFn(c.resume, { dir: this.#cwd });
669
674
  } catch {
670
675
  return;
671
676
  }
@@ -690,7 +695,7 @@ var SessionRunner = class {
690
695
  #buildOptions() {
691
696
  const c = this.#config;
692
697
  return {
693
- cwd: c.cwd,
698
+ cwd: this.#cwd,
694
699
  permissionMode: c.permissionMode,
695
700
  allowedTools: c.allowedTools,
696
701
  disallowedTools: c.disallowedTools,
@@ -1099,7 +1104,7 @@ var AiSdkRunner = class {
1099
1104
  return {
1100
1105
  id: this.id,
1101
1106
  status: this.#status,
1102
- cwd: this.#config.cwd ?? process.cwd(),
1107
+ cwd: this.#config.cwd ?? "",
1103
1108
  profile: this.#config.profile,
1104
1109
  engine: "provider",
1105
1110
  capabilities: ENGINE_CAPABILITIES.provider,
@@ -1110,6 +1115,7 @@ var AiSdkRunner = class {
1110
1115
  activityCount: this.#activityCount,
1111
1116
  pendingPermissionCount: 0,
1112
1117
  meta: this.#config.meta,
1118
+ scope: this.#config.scope,
1113
1119
  title: this.#title(),
1114
1120
  numTurns: this.#numTurns || void 0,
1115
1121
  lastActivityAt: this.#lastActivityAt
@@ -1694,6 +1700,17 @@ var AiSdkRunner = class {
1694
1700
  if (!prompt) return void 0;
1695
1701
  return prompt.length > 80 ? prompt.slice(0, 77) + "…" : prompt;
1696
1702
  }
1703
+ /**
1704
+ * This session's MCP servers, as the host assembled them.
1705
+ *
1706
+ * Always answers — an empty list when no MCP was wired — because the
1707
+ * alternative (undefined, which the server turns into a 501) says "this
1708
+ * engine cannot tell you", and this engine can: the host that built the
1709
+ * session is the only party who knows, and it has been asked.
1710
+ */
1711
+ async mcpServers() {
1712
+ return await this.#config.reportMcpServers?.() ?? [];
1713
+ }
1697
1714
  /** Host-facing rename: writes `meta.title`, which `#title()` prefers. Clearing
1698
1715
  * it (undefined) restores the derived title. The engine is never told. */
1699
1716
  setTitle(title) {
@@ -2327,21 +2344,50 @@ function createToolContext(options) {
2327
2344
  /** Add host-side MCP tools to a context. They are ALWAYS authoritative: they run
2328
2345
  * server-side with server credentials, and must never be handed to a browser. */
2329
2346
  function withMcpTools(context, mcpTools) {
2347
+ return withHostTools(context, Object.fromEntries(Object.entries(mcpTools).map(([name, mcpTool]) => [name, {
2348
+ tool: mcpTool,
2349
+ trust: "authoritative"
2350
+ }])), "MCP tool");
2351
+ }
2352
+ /**
2353
+ * Add host-supplied tools to a context at an explicit trust level.
2354
+ *
2355
+ * The trust level is the whole point of the seam: {@link withMcpTools} can only
2356
+ * produce authoritative tools, so a host tool that *should* be sandboxed — and
2357
+ * therefore executable in the browser tab that asked for it — had no way to be
2358
+ * expressed at all. Here the host says which it is, and the contradictions are
2359
+ * refused rather than silently resolved:
2360
+ *
2361
+ * - a `sandboxed` tool carrying `execute` would run inline in this process with
2362
+ * the gateway's ambient authority, which is exactly what sandboxing it was
2363
+ * meant to prevent;
2364
+ * - an `authoritative` tool *without* `execute` would park the turn on a call no
2365
+ * executor claims, and the session would simply stop.
2366
+ */
2367
+ function withHostTools(context, hostTools, kind = "host tool") {
2368
+ const entries = Object.entries(hostTools);
2369
+ if (entries.length === 0) return context;
2330
2370
  const definitions = [...context.definitions];
2331
2371
  const tools = { ...context.tools };
2332
- for (const [name, mcpTool] of Object.entries(mcpTools)) {
2333
- if (context.sandboxedToolNames.includes(name)) throw new Error(`MCP tool '${name}' collides with a sandboxed tool of the same name`);
2372
+ const sandboxedToolNames = [...context.sandboxedToolNames];
2373
+ for (const [name, { tool: hostTool, trust }] of entries) {
2374
+ if (name in tools) throw new Error(`${kind} '${name}' collides with an existing tool of the same name`);
2375
+ const executes = typeof hostTool.execute === "function";
2376
+ if (trust === "sandboxed" && executes) throw new Error(`${kind} '${name}' is declared sandboxed but has an \`execute\` — it would run in this process with full authority. Drop \`execute\` so it rides the ToolExecutor seam.`);
2377
+ if (trust === "authoritative" && !executes) throw new Error(`${kind} '${name}' is declared authoritative but has no \`execute\` — nothing would ever answer its calls and the turn would stall.`);
2334
2378
  definitions.push({
2335
2379
  name,
2336
- trust: "authoritative",
2337
- tool: mcpTool
2380
+ trust,
2381
+ tool: hostTool
2338
2382
  });
2339
- tools[name] = mcpTool;
2383
+ tools[name] = hostTool;
2384
+ if (trust === "sandboxed") sandboxedToolNames.push(name);
2340
2385
  }
2341
2386
  return {
2342
2387
  ...context,
2343
2388
  tools,
2344
- definitions
2389
+ definitions,
2390
+ sandboxedToolNames
2345
2391
  };
2346
2392
  }
2347
2393
  function truncate(text) {
@@ -2581,7 +2627,7 @@ const CAPABILITY_TOOLS = {
2581
2627
  * the host wired, which is what a host that ignores profiles gets.
2582
2628
  */
2583
2629
  function createEngineSession(options) {
2584
- const vfs = options.config.vfs ?? createVfs(options.config.restore?.vfs);
2630
+ const vfs = options.config.vfs ?? createVfs(options.config.restore ? options.config.restore.vfs : options.seedVfs);
2585
2631
  const executor = options.selectExecutor();
2586
2632
  const granted = options.config.capabilities ?? options.profile?.session?.capabilities;
2587
2633
  const isGranted = (key) => granted === void 0 || granted.includes(CAPABILITY_TOOLS[key]);
@@ -2602,8 +2648,12 @@ function createEngineSession(options) {
2602
2648
  webFetch,
2603
2649
  onFileDelivered: options.capabilities?.deliverFiles === false || !isGranted("deliverFiles") ? void 0 : (file) => runner?.emitFileDelivered(file)
2604
2650
  });
2605
- const mcpTools = selectMcpTools(options.mcpTools, options.profile?.session?.mcpServers);
2606
- const context = mcpTools ? withMcpTools(base, mcpTools) : base;
2651
+ const declaredServers = options.profile?.session?.mcpServers;
2652
+ const connected = options.mcp?.tools ?? options.mcpTools;
2653
+ requireDeclaredServers(options.profile?.name ?? "(unnamed)", declaredServers, options.mcp, connected);
2654
+ const mcpTools = selectMcpTools(connected, declaredServers);
2655
+ const withMcp = mcpTools ? withMcpTools(base, mcpTools) : base;
2656
+ const context = options.tools ? withHostTools(withMcp, options.tools) : withMcp;
2607
2657
  runner = new AiSdkRunner({
2608
2658
  ...options.config,
2609
2659
  languageModel: options.resolveModel(options.profile, options.config),
@@ -2613,11 +2663,42 @@ function createEngineSession(options) {
2613
2663
  executor,
2614
2664
  executableTools: context.sandboxedToolNames,
2615
2665
  executionBackend: options.backend ?? "server",
2616
- executionLimits: options.executionLimits
2617
- });
2666
+ executionLimits: options.executionLimits,
2667
+ reportMcpServers: options.mcp ? () => Promise.resolve(declaredServers === void 0 ? options.mcp.servers : options.mcp.servers.filter((s) => declaredServers.includes(s.name))) : void 0
2668
+ }, options.id);
2618
2669
  return runner;
2619
2670
  }
2620
2671
  /**
2672
+ * Refuse to build a session whose profile names an MCP server that isn't there.
2673
+ *
2674
+ * A profile's `mcpServers` list is a **declaration**, not a filter: an embedder
2675
+ * who wrote it meant the agent to have those tools. Honouring it partially is
2676
+ * the worst failure mode this engine has — the session starts, reports healthy,
2677
+ * and the agent apologises its way through every request that needed the server,
2678
+ * with one warning line in a log nobody is reading.
2679
+ *
2680
+ * With a {@link McpConnection} the check is exact (did this server connect?).
2681
+ * With a bare tool set all we can see is whether any tool carries the server's
2682
+ * namespace, so a genuinely tool-less server would trip it — the fix there is to
2683
+ * pass `mcp` rather than to weaken this.
2684
+ */
2685
+ function requireDeclaredServers(profileName, declared, mcp, tools) {
2686
+ if (!declared || declared.length === 0) return;
2687
+ const missing = declared.filter((name) => {
2688
+ if (mcp) {
2689
+ const server = mcp.servers.find((s) => s.name === name);
2690
+ return !server || server.status !== "connected";
2691
+ }
2692
+ return !Object.keys(tools ?? {}).some((tool) => tool.split("__")[0] === name);
2693
+ });
2694
+ if (missing.length === 0) return;
2695
+ const reasons = missing.map((name) => {
2696
+ const error = mcp?.servers.find((s) => s.name === name)?.error;
2697
+ return error ? `${name} (${error})` : name;
2698
+ }).join(", ");
2699
+ throw new Error(`profile '${profileName}' declares MCP server(s) that are not connected: ${reasons}. A session missing a declared server is a session whose agent silently cannot do its job.`);
2700
+ }
2701
+ /**
2621
2702
  * Restrict a connected tool set to the MCP servers a profile grants, by the
2622
2703
  * `<server>__<tool>` namespace {@link connectMcpTools} assigns. Undefined `servers`
2623
2704
  * = no declaration, so every connected server passes through.
@@ -2637,31 +2718,88 @@ function selectMcpTools(tools, servers) {
2637
2718
  * Server-side only, with server credentials: these tools are authoritative and
2638
2719
  * must never be bridged to a browser. `@ai-sdk/mcp` is imported lazily and is an
2639
2720
  * optional dependency — an operator who wires no MCP servers never needs it.
2721
+ *
2722
+ * **A stateless MCP server must answer `GET` with 405.** The client opens the
2723
+ * SSE stream with a `GET` before it sends anything, and a POST-only server
2724
+ * mounted under a framework's default 404 makes the whole connect fail with an
2725
+ * error that names neither the method nor the route. This is the single most
2726
+ * common way an otherwise-correct MCP mount fails.
2640
2727
  */
2641
2728
  async function connectMcpTools(servers, options = {}) {
2642
2729
  const entries = Object.entries(servers);
2643
2730
  if (entries.length === 0) return {
2644
2731
  tools: {},
2732
+ servers: [],
2645
2733
  close: async () => {}
2646
2734
  };
2647
2735
  const { createMCPClient } = await import("@ai-sdk/mcp");
2648
2736
  const clients = [];
2649
2737
  const tools = {};
2650
- for (const [name, server] of entries) try {
2651
- const client = await createMCPClient({
2652
- transport: toTransport(server),
2653
- onUncaughtError: (error) => options.onError?.(name, error)
2654
- });
2655
- clients.push(client);
2656
- for (const [toolName, mcpTool] of Object.entries(await client.tools())) tools[`${name}__${toolName}`] = mcpTool;
2657
- } catch (error) {
2658
- options.onError?.(name, error);
2738
+ const statuses = [];
2739
+ const closeAll = async () => {
2740
+ await Promise.allSettled(clients.map((c) => c.close()));
2741
+ };
2742
+ for (const [name, server] of entries) {
2743
+ const identity = describeServer(server);
2744
+ try {
2745
+ const client = await createMCPClient({
2746
+ transport: toTransport(server),
2747
+ onUncaughtError: (error) => options.onError?.(name, error)
2748
+ });
2749
+ clients.push(client);
2750
+ const connected = await client.tools();
2751
+ for (const [toolName, mcpTool] of Object.entries(connected)) tools[`${name}__${toolName}`] = mcpTool;
2752
+ statuses.push({
2753
+ name,
2754
+ status: "connected",
2755
+ ...identity,
2756
+ tools: Object.entries(connected).map(([toolName, mcpTool]) => toToolInfo(toolName, mcpTool))
2757
+ });
2758
+ } catch (error) {
2759
+ const message = error instanceof Error ? error.message : String(error);
2760
+ statuses.push({
2761
+ name,
2762
+ status: "failed",
2763
+ error: message,
2764
+ ...identity
2765
+ });
2766
+ options.onError?.(name, error);
2767
+ if (options.required) {
2768
+ await closeAll();
2769
+ throw new Error(`MCP server '${name}' failed to connect: ${message}`);
2770
+ }
2771
+ }
2659
2772
  }
2660
2773
  return {
2661
2774
  tools,
2662
- close: async () => {
2663
- await Promise.allSettled(clients.map((c) => c.close()));
2664
- }
2775
+ servers: statuses,
2776
+ close: closeAll
2777
+ };
2778
+ }
2779
+ /** The connection's identity, minus its secrets — `headers` never travel. */
2780
+ function describeServer(server) {
2781
+ if ("url" in server) return {
2782
+ transport: server.type === "sse" ? "sse" : "http",
2783
+ url: server.url
2784
+ };
2785
+ return {
2786
+ transport: "stdio",
2787
+ command: server.command,
2788
+ args: server.args
2789
+ };
2790
+ }
2791
+ /**
2792
+ * The AI SDK hands back its own `Tool`, whose `inputSchema` may be a zod schema
2793
+ * or a `jsonSchema()` wrapper. Only the latter carries a JSON Schema document,
2794
+ * so that is the only case where parameters are reported — `McpServerToolInfo`
2795
+ * models the absence deliberately, and inventing one here would be worse.
2796
+ */
2797
+ function toToolInfo(name, mcpTool) {
2798
+ const { description, inputSchema } = mcpTool ?? {};
2799
+ return {
2800
+ name,
2801
+ description: typeof description === "string" ? description : void 0,
2802
+ inputSchema: inputSchema?.jsonSchema
2665
2803
  };
2666
2804
  }
2667
2805
  /**
@@ -2797,9 +2935,9 @@ const claudeAdapter = {
2797
2935
  };
2798
2936
  return { available: "unknown" };
2799
2937
  },
2800
- createRunner({ config, restore }) {
2938
+ createRunner({ config, restore, id }) {
2801
2939
  if (restore) throw new Error("the Claude engine cannot rebuild a parked session");
2802
- return new SessionRunner(config);
2940
+ return new SessionRunner(config, id);
2803
2941
  },
2804
2942
  /**
2805
2943
  * The Agent SDK's on-disk session store, mapped browser-safe. The SDK reads
@@ -3345,6 +3483,8 @@ var CodexRunner = class {
3345
3483
  id;
3346
3484
  createdAt;
3347
3485
  #config;
3486
+ /** {@link CodexRunnerConfig.cwd}, checked once in the constructor. */
3487
+ #cwd;
3348
3488
  #events = [];
3349
3489
  #listeners = /* @__PURE__ */ new Set();
3350
3490
  #seq = 0;
@@ -3407,6 +3547,8 @@ var CodexRunner = class {
3407
3547
  const mode = config.permissionMode ?? "default";
3408
3548
  if (!ENGINE_CAPABILITIES.codex.permissionModes.includes(mode)) throw new Error(`permission mode '${mode}' is not supported by the codex engine`);
3409
3549
  if (config.forkSession) throw new Error("the codex engine cannot fork a resumed thread");
3550
+ if (!config.cwd) throw new Error("the codex engine requires a cwd");
3551
+ this.#cwd = config.cwd;
3410
3552
  this.#config = config;
3411
3553
  this.#permissionMode = mode;
3412
3554
  this.#model = config.model;
@@ -3442,7 +3584,7 @@ var CodexRunner = class {
3442
3584
  id: this.id,
3443
3585
  sdkSessionId: this.#sdkSessionId,
3444
3586
  status: this.#status,
3445
- cwd: this.#config.cwd,
3587
+ cwd: this.#cwd,
3446
3588
  profile: this.#config.profile,
3447
3589
  engine: "codex",
3448
3590
  capabilities: ENGINE_CAPABILITIES.codex,
@@ -3454,6 +3596,7 @@ var CodexRunner = class {
3454
3596
  activityCount: this.#activityCount,
3455
3597
  pendingPermissionCount: this.#approvals.size,
3456
3598
  meta: this.#config.meta,
3599
+ scope: this.#config.scope,
3457
3600
  title: this.#title(),
3458
3601
  totalCostUsd: this.#totalCostUsd,
3459
3602
  numTurns: this.#numTurns || void 0,
@@ -3738,7 +3881,7 @@ var CodexRunner = class {
3738
3881
  }
3739
3882
  if (!this.#threadLoaded) {
3740
3883
  const options = {
3741
- cwd: this.#config.cwd,
3884
+ cwd: this.#cwd,
3742
3885
  approvalPolicy: APPROVAL_POLICY_BY_MODE[this.#permissionMode],
3743
3886
  sandbox: THREAD_SANDBOX_BY_MODE[this.#permissionMode]
3744
3887
  };
@@ -3779,7 +3922,7 @@ var CodexRunner = class {
3779
3922
  if (this.#skillsRefresh) return this.#skillsRefresh;
3780
3923
  const run = (async () => {
3781
3924
  try {
3782
- const result = await connection.request("skills/list", { cwds: [this.#config.cwd] });
3925
+ const result = await connection.request("skills/list", { cwds: [this.#cwd] });
3783
3926
  if (this.#closed) return;
3784
3927
  const entries = Array.isArray(result?.data) ? result.data : [];
3785
3928
  const seen = /* @__PURE__ */ new Set();
@@ -3981,7 +4124,7 @@ var CodexRunner = class {
3981
4124
  const params = {
3982
4125
  threadId: this.#sdkSessionId,
3983
4126
  input: turn.input,
3984
- cwd: this.#config.cwd,
4127
+ cwd: this.#cwd,
3985
4128
  approvalPolicy: APPROVAL_POLICY_BY_MODE[this.#permissionMode],
3986
4129
  sandboxPolicy: TURN_SANDBOX_BY_MODE[this.#permissionMode]
3987
4130
  };
@@ -4905,7 +5048,7 @@ const codexAdapter = {
4905
5048
  capabilities: ENGINE_CAPABILITIES.codex,
4906
5049
  catalog: CODEX_CATALOG,
4907
5050
  checkAvailability: (profile, env) => checkCodexAvailability(profile, env),
4908
- createRunner({ config, profile, restore }) {
5051
+ createRunner({ config, profile, restore, id }) {
4909
5052
  if (restore) throw new Error("the codex engine cannot rebuild a parked session");
4910
5053
  const executable = config.codexPathOverride ?? resolveBundledCodexExecutable();
4911
5054
  if (!executable) throw new Error(NOT_INSTALLED);
@@ -4916,7 +5059,7 @@ const codexAdapter = {
4916
5059
  executable,
4917
5060
  ...options
4918
5061
  })
4919
- });
5062
+ }, id);
4920
5063
  },
4921
5064
  async listSessions(options) {
4922
5065
  const executable = resolveBundledCodexExecutable();
@@ -4976,6 +5119,6 @@ function getEngineAdapter(engine) {
4976
5119
  return ADAPTERS[engine ?? "claude"];
4977
5120
  }
4978
5121
  //#endregion
4979
- export { AiSdkRunner, BrowserBridgeExecutor, CLAUDE_CATALOG, CODEX_CATALOG, CodexRunner, DeferredExecutor, InputQueue, JsonRpcError, JsonRpcStdioConnection, PendingRequestRegistry, QuickJsExecutor, SUPPORTED_ATTACHMENT_TYPES, SessionRunner, attachmentContentBlocks, attachmentKind, attachmentRef, checkClaudeAuth, claudeAdapter, codexAdapter, connectAppServer, connectMcpTools, createEngineSession, createToolContext, createWebFetch, getEngineAdapter, htmlToMarkdown, isHostAllowed, isPrivateAddress, listCodexSessions, mcpStatusInfo, modelOptionsFromSdk, normalizeMediaType, normalizeSdkMessage, providerAdapter, resolveBundledClaudeExecutable, resolveBundledCodexExecutable, toApiMessage, toExecutionResult, withMcpTools };
5122
+ export { AiSdkRunner, BrowserBridgeExecutor, CLAUDE_CATALOG, CODEX_CATALOG, CodexRunner, DeferredExecutor, InputQueue, JsonRpcError, JsonRpcStdioConnection, PendingRequestRegistry, QuickJsExecutor, SUPPORTED_ATTACHMENT_TYPES, SessionRunner, attachmentContentBlocks, attachmentKind, attachmentRef, checkClaudeAuth, claudeAdapter, codexAdapter, connectAppServer, connectMcpTools, createEngineSession, createToolContext, createWebFetch, getEngineAdapter, htmlToMarkdown, isHostAllowed, isPrivateAddress, listCodexSessions, mcpStatusInfo, modelOptionsFromSdk, normalizeMediaType, normalizeSdkMessage, providerAdapter, resolveBundledClaudeExecutable, resolveBundledCodexExecutable, toApiMessage, toExecutionResult, withHostTools, withMcpTools };
4980
5123
 
4981
5124
  //# sourceMappingURL=index.mjs.map