@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 +23 -0
- package/dist/{chunk-XH2CFK2E.js → chunk-74JAAZB7.js} +143 -14
- package/dist/chunk-74JAAZB7.js.map +1 -0
- package/dist/cli.js +10 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +33 -1
- package/dist/index.js +7 -1
- package/package.json +3 -3
- package/dist/chunk-XH2CFK2E.js.map +0 -1
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.
|
|
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 =
|
|
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 (
|
|
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
|
|
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
|
|
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 (
|
|
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.
|
|
4044
|
-
var MAX_SUPPORTED_IR_VERSION = "0.
|
|
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((_,
|
|
4414
|
+
new Promise((_, reject2) => {
|
|
4289
4415
|
timer = setTimeout(() => {
|
|
4290
4416
|
abortController.abort();
|
|
4291
|
-
|
|
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-
|
|
4665
|
+
//# sourceMappingURL=chunk-74JAAZB7.js.map
|