@warble/claude-agent-sdk 0.15.4 → 0.17.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/README.md CHANGED
@@ -93,6 +93,29 @@ over turn. `--stream-json` streams one `WarbleChatEvent` NDJSON line per event
93
93
  `{"t":"answer",…}` line; every turn also emits a `{"t":"session","id":…}` line — on success **and**
94
94
  on a failed turn — so a caller can resume that conversation with `--resume <session-id>`.
95
95
 
96
+ `chat --host-mcp-config <absolute path>` lets a host give the agent its own tools through one stdio
97
+ MCP server ([`src/hostMcp.ts`](./src/hostMcp.ts)). The file is a closed JSON object:
98
+
99
+ ```json
100
+ {
101
+ "name": "host_tools",
102
+ "command": "/absolute/path/to/server",
103
+ "args": ["--token-file", "/absolute/path/to/token"],
104
+ "tools": ["query"],
105
+ "instruction": "Optional single line appended to the system prompt."
106
+ }
107
+ ```
108
+
109
+ It must be an absolute path to a regular file (opened with `O_NOFOLLOW`, so a symlink is refused),
110
+ owned by the current user, with no group or other permission bits (`chmod 600`). `name` matches
111
+ `[a-z0-9_]{1,32}`, `tools` is a non-empty list of `[a-z0-9_]{1,64}`, `instruction` is one line of at
112
+ most 300 characters, and unknown keys are rejected. The server is passed to `query()` as a stdio
113
+ `mcpServers` entry; the read-only guardrail then allows exactly `mcp__<name>__<tool>` for the listed
114
+ tools and still refuses every other `mcp__*` tool, and the `wren`-only Bash rule is unchanged. Keep
115
+ secrets out of this file — pass a token file path in `args` instead; the CLI never prints the file,
116
+ its `args`, or the environment. Hybrid-staged plans refuse the flag. Without it, `chat` behaves
117
+ exactly as before.
118
+
96
119
  `list-models` emits exactly one versioned JSON object for the currently authenticated Claude
97
120
  subscription. It exposes only model ID, display name, and description; unavailable authentication,
98
121
  runtime, timeout, or protocol states are returned as sanitized JSON. It uses an empty SDK input and
@@ -299,6 +299,98 @@ function emitAgentModule(prepared, opts = {}) {
299
299
  ].join("\n");
300
300
  }
301
301
 
302
+ // src/hostMcp.ts
303
+ import { closeSync, constants, fstatSync, openSync, readFileSync } from "fs";
304
+ import { isAbsolute } from "path";
305
+ var NAME_RE = /^[a-z0-9_]{1,32}$/;
306
+ var TOOL_RE = /^[a-z0-9_]{1,64}$/;
307
+ var LINE_BREAK_RE = /[\r\n\u2028\u2029]/;
308
+ var MAX_INSTRUCTION = 300;
309
+ var ALLOWED_KEYS = /* @__PURE__ */ new Set(["name", "command", "args", "tools", "instruction"]);
310
+ function reject(reason) {
311
+ throw new DispatchError(`--host-mcp-config: ${reason}`);
312
+ }
313
+ function readTrustedFile(path) {
314
+ if (!isAbsolute(path)) reject("the path must be absolute");
315
+ const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
316
+ if (uid === void 0) reject("file ownership cannot be verified on this platform");
317
+ let fd;
318
+ try {
319
+ fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
320
+ } catch (e) {
321
+ const code = e.code;
322
+ if (code === "ELOOP") reject("the path must not be a symbolic link");
323
+ reject(`the file cannot be opened (${code ?? "unknown error"})`);
324
+ }
325
+ try {
326
+ const st = fstatSync(fd);
327
+ if (!st.isFile()) reject("the path must be a regular file");
328
+ if (st.uid !== uid) reject("the file must be owned by the current user");
329
+ if ((st.mode & 63) !== 0) {
330
+ reject("the file must not be accessible by group or others (chmod 600)");
331
+ }
332
+ return readFileSync(fd, "utf8");
333
+ } finally {
334
+ closeSync(fd);
335
+ }
336
+ }
337
+ function parseHostMcpConfig(value) {
338
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
339
+ reject("the file must contain a JSON object");
340
+ }
341
+ const obj = value;
342
+ for (const key of Object.keys(obj)) {
343
+ if (!ALLOWED_KEYS.has(key)) reject(`unknown key '${key.slice(0, 64)}'`);
344
+ }
345
+ const { name, command, args, tools, instruction } = obj;
346
+ if (typeof name !== "string" || !NAME_RE.test(name)) reject("'name' must match [a-z0-9_]{1,32}");
347
+ if (typeof command !== "string" || !isAbsolute(command)) {
348
+ reject("'command' must be an absolute path");
349
+ }
350
+ if (!Array.isArray(args) || !args.every((a) => typeof a === "string")) {
351
+ reject("'args' must be an array of strings");
352
+ }
353
+ if (!Array.isArray(tools) || tools.length === 0) reject("'tools' must be a non-empty array");
354
+ for (const t of tools) {
355
+ if (typeof t !== "string" || !TOOL_RE.test(t)) {
356
+ reject("each of 'tools' must match [a-z0-9_]{1,64}");
357
+ }
358
+ }
359
+ if (new Set(tools).size !== tools.length) reject("'tools' must not repeat a tool");
360
+ if (instruction !== void 0) {
361
+ if (typeof instruction !== "string" || instruction.trim().length === 0) {
362
+ reject("'instruction' must be a non-empty string");
363
+ }
364
+ if (instruction.length > MAX_INSTRUCTION) {
365
+ reject(`'instruction' must be at most ${MAX_INSTRUCTION} characters`);
366
+ }
367
+ if (LINE_BREAK_RE.test(instruction)) reject("'instruction' must be a single line");
368
+ }
369
+ return {
370
+ name,
371
+ command,
372
+ args: [...args],
373
+ tools: [...tools],
374
+ ...instruction !== void 0 ? { instruction } : {}
375
+ };
376
+ }
377
+ function loadHostMcpConfig(path) {
378
+ const raw = readTrustedFile(path);
379
+ let parsed;
380
+ try {
381
+ parsed = JSON.parse(raw);
382
+ } catch {
383
+ reject("the file is not valid JSON");
384
+ }
385
+ return parseHostMcpConfig(parsed);
386
+ }
387
+ function hostMcpToolNames(cfg) {
388
+ return cfg.tools.map((tool3) => `mcp__${cfg.name}__${tool3}`);
389
+ }
390
+ function hostMcpServerEntry(cfg) {
391
+ return { type: "stdio", command: cfg.command, args: [...cfg.args] };
392
+ }
393
+
302
394
  // src/closure.ts
303
395
  function freezeArray(values) {
304
396
  return Object.freeze(values);
@@ -422,7 +514,7 @@ var COMPONENT_TYPES = [
422
514
  ];
423
515
  var TRIGGER_KINDS = ["one_shot", "scheduled", "event"];
424
516
  var OUTCOME_KINDS = ["none", "assertion", "mutation", "dispatch"];
425
- var SUPPORTED_IR_VERSIONS = ["0.8"];
517
+ var SUPPORTED_IR_VERSIONS = ["0.9"];
426
518
  function assertSupportedIrVersion(version) {
427
519
  if (!SUPPORTED_IR_VERSIONS.includes(version)) {
428
520
  throw new DispatchError(
@@ -751,6 +843,9 @@ function parseIr(json2) {
751
843
  const obj = requireObject(root, "<root>");
752
844
  const version = requireString(obj, "warble_ir_version", "<root>");
753
845
  assertSupportedIrVersion(version);
846
+ if (obj["context_binding"] === null) {
847
+ fail("context-free IR is not supported by claude-agent-sdk:local");
848
+ }
754
849
  const configRaw = obj["config"];
755
850
  const config = {};
756
851
  if (configRaw !== void 0) {
@@ -1902,12 +1997,12 @@ import {
1902
1997
  existsSync,
1903
1998
  lstatSync,
1904
1999
  mkdirSync,
1905
- readFileSync,
2000
+ readFileSync as readFileSync2,
1906
2001
  realpathSync,
1907
2002
  statSync,
1908
2003
  writeFileSync
1909
2004
  } from "fs";
1910
- import { dirname, isAbsolute, join, parse, resolve, sep } from "path";
2005
+ import { dirname, isAbsolute as isAbsolute2, join, parse, resolve, sep } from "path";
1911
2006
  function assetDirForIr(irPath) {
1912
2007
  const { dir, name } = parse(resolve(irPath));
1913
2008
  return join(dir, `${name}.assets`);
@@ -1932,7 +2027,7 @@ function prepareAssetLanding(ir, irPath, cwd) {
1932
2027
  `asset '${asset.path}' of component '${node.id}' is not a regular file. Reading a pipe or device would block the dispatch indefinitely instead of failing, so anything that is not a plain file is refused.`
1933
2028
  );
1934
2029
  }
1935
- const data = readFileSync(source);
2030
+ const data = readFileSync2(source);
1936
2031
  const actual = `sha256:${createHash("sha256").update(data).digest("hex")}`;
1937
2032
  if (actual !== asset.hash) {
1938
2033
  throw new DispatchError(
@@ -1959,7 +2054,7 @@ function landAssets(ir, irPath, cwd) {
1959
2054
  }
1960
2055
  function assertContainedRelativePath(relative, componentId) {
1961
2056
  const climbs = relative.split(/[\\/]/).includes("..");
1962
- if (isAbsolute(relative) || climbs || relative.startsWith(sep)) {
2057
+ if (isAbsolute2(relative) || climbs || relative.startsWith(sep)) {
1963
2058
  throw new DispatchError(
1964
2059
  `asset path '${relative}' of component '${componentId}' must be a relative path with no '..' segments. A manifest naming a path outside the directory it lands in would be an arbitrary file write, so it is refused rather than resolved.`
1965
2060
  );
@@ -2162,6 +2257,9 @@ function makeReadOnlyGuard(cfg) {
2162
2257
  denials.push({ tool: toolName, reason: reason2 });
2163
2258
  return deny(reason2);
2164
2259
  }
2260
+ if (cfg.hostMcpTools?.includes(toolName)) {
2261
+ return allow(input);
2262
+ }
2165
2263
  const reason = `tool '${toolName}' is not permitted for this component.`;
2166
2264
  denials.push({ tool: toolName, reason });
2167
2265
  return deny(reason);
@@ -2603,6 +2701,27 @@ function aggregateTrace(messages, meta, denials) {
2603
2701
  denials
2604
2702
  };
2605
2703
  }
2704
+ function applyHostMcp(options, hostMcp) {
2705
+ if (options.mcpServers && hostMcp.name in options.mcpServers) {
2706
+ throw new DispatchError(
2707
+ `--host-mcp-config: MCP server name '${hostMcp.name}' is already used by this plan`
2708
+ );
2709
+ }
2710
+ const next = {
2711
+ ...options,
2712
+ mcpServers: { ...options.mcpServers, [hostMcp.name]: hostMcpServerEntry(hostMcp) }
2713
+ };
2714
+ if (hostMcp.instruction !== void 0) {
2715
+ if (typeof options.systemPrompt !== "string") {
2716
+ throw new DispatchError(
2717
+ "--host-mcp-config: 'instruction' needs a plain-string system prompt, which this plan does not have"
2718
+ );
2719
+ }
2720
+ next.systemPrompt = `${options.systemPrompt}
2721
+ ${hostMcp.instruction}`;
2722
+ }
2723
+ return next;
2724
+ }
2606
2725
  var DispatchSessionError = class extends DispatchError {
2607
2726
  constructor(message, sessionId) {
2608
2727
  super(message);
@@ -2642,6 +2761,11 @@ async function runDispatch(plan, cfg) {
2642
2761
  );
2643
2762
  }
2644
2763
  if (plan.meta.mode === "hybrid-staged") {
2764
+ if (cfg.hostMcp) {
2765
+ throw new DispatchError(
2766
+ "--host-mcp-config is not supported for a hybrid-staged plan (steps span providers)"
2767
+ );
2768
+ }
2645
2769
  return process.env["WARBLE_HYBRID_MODE"] === "tool" ? runHybridTool(plan, cfg) : runHybridStaged(plan, cfg);
2646
2770
  }
2647
2771
  mkdirSync3(cfg.outDir, { recursive: true });
@@ -2652,12 +2776,13 @@ async function runDispatch(plan, cfg) {
2652
2776
  readOnly: plan.meta.readOnly,
2653
2777
  writeScope,
2654
2778
  cwd,
2655
- setupScope: plan.meta.setupScope
2779
+ setupScope: plan.meta.setupScope,
2780
+ ...cfg.hostMcp ? { hostMcpTools: hostMcpToolNames(cfg.hostMcp) } : {}
2656
2781
  });
2657
2782
  const venvBin = join4(cwd, ".venv", "bin");
2658
2783
  const pathEnv = existsSync3(venvBin) ? `${venvBin}:${process.env.PATH ?? ""}` : process.env.PATH ?? "";
2659
2784
  const env = { ...process.env, PATH: pathEnv };
2660
- const options = {
2785
+ const baseOptions = {
2661
2786
  ...plan.options,
2662
2787
  canUseTool: composeCanUseTool(plan.options.canUseTool, canUseTool),
2663
2788
  // Read never reaches `canUseTool` for an in-cwd path in the real SDK (see guardrails.ts); this
@@ -2666,6 +2791,7 @@ async function runDispatch(plan, cfg) {
2666
2791
  env,
2667
2792
  ...cfg.resume ? { resume: cfg.resume } : {}
2668
2793
  };
2794
+ const options = cfg.hostMcp ? applyHostMcp(baseOptions, cfg.hostMcp) : baseOptions;
2669
2795
  if (cfg.assets) landAssets(cfg.assets.ir, cfg.assets.irPath, cwd);
2670
2796
  reportPromptFingerprint(cfg, options);
2671
2797
  const mapper = new ChatEventMapper(plan.meta.verb);
@@ -3797,7 +3923,7 @@ async function runComposedDispatch(plan, cfg) {
3797
3923
  }
3798
3924
 
3799
3925
  // src/dispatch.ts
3800
- import { dirname as dirname2, isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
3926
+ import { dirname as dirname2, isAbsolute as isAbsolute3, resolve as resolve2 } from "path";
3801
3927
  var UNAVAILABLE_COMPONENT_REASON = "component is unavailable on the configured runtime";
3802
3928
  function deepFreeze(value) {
3803
3929
  if (typeof value !== "object" || value === null || Object.isFrozen(value)) return value;
@@ -3842,7 +3968,7 @@ function assertInvocationRealization(closure, reports, target) {
3842
3968
  function resolveProjectCwd(node, opts) {
3843
3969
  if (opts.project) return resolve2(opts.project);
3844
3970
  const p = node.context_binding.project;
3845
- if (isAbsolute2(p)) return p;
3971
+ if (isAbsolute3(p)) return p;
3846
3972
  const baseDir = opts.irPath ? dirname2(resolve2(opts.irPath)) : process.cwd();
3847
3973
  return resolve2(baseDir, p);
3848
3974
  }
@@ -4040,8 +4166,8 @@ async function dispatch(input, runCfg) {
4040
4166
 
4041
4167
  // src/manifest.ts
4042
4168
  var MANIFEST_VERSION = "0.3";
4043
- var MIN_SUPPORTED_IR_VERSION = "0.8";
4044
- var MAX_SUPPORTED_IR_VERSION = "0.8";
4169
+ var MIN_SUPPORTED_IR_VERSION = "0.9";
4170
+ var MAX_SUPPORTED_IR_VERSION = "0.9";
4045
4171
  var DEFAULT_MAX_ATTEMPTS = 1;
4046
4172
  function classifyStep(node, stepIndex) {
4047
4173
  const when = node.llm_calls[stepIndex].when;
@@ -4285,10 +4411,10 @@ async function withinTimeout(operation, timeoutMs, abortController) {
4285
4411
  try {
4286
4412
  return await Promise.race([
4287
4413
  operation,
4288
- new Promise((_, reject) => {
4414
+ new Promise((_, reject2) => {
4289
4415
  timer = setTimeout(() => {
4290
4416
  abortController.abort();
4291
- reject(new Error("model catalog timed out"));
4417
+ reject2(new Error("model catalog timed out"));
4292
4418
  }, timeoutMs);
4293
4419
  })
4294
4420
  ]);
@@ -4450,6 +4576,9 @@ function createChatSession(plan, runCfg, initialResumeSessionId) {
4450
4576
  export {
4451
4577
  DispatchError,
4452
4578
  emitAgentModule,
4579
+ parseHostMcpConfig,
4580
+ loadHostMcpConfig,
4581
+ hostMcpToolNames,
4453
4582
  componentDependencies,
4454
4583
  resolveComponentClosure,
4455
4584
  REALIZATION_KINDS,
@@ -4533,4 +4662,4 @@ export {
4533
4662
  ChatSession,
4534
4663
  createChatSession
4535
4664
  };
4536
- //# sourceMappingURL=chunk-XH2CFK2E.js.map
4665
+ //# sourceMappingURL=chunk-74JAAZB7.js.map