@warble/claude-agent-sdk 0.15.3 → 0.16.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-6GZGHFX7.js} +137 -11
- package/dist/chunk-6GZGHFX7.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 +1 -1
- 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);
|
|
@@ -1902,12 +1994,12 @@ import {
|
|
|
1902
1994
|
existsSync,
|
|
1903
1995
|
lstatSync,
|
|
1904
1996
|
mkdirSync,
|
|
1905
|
-
readFileSync,
|
|
1997
|
+
readFileSync as readFileSync2,
|
|
1906
1998
|
realpathSync,
|
|
1907
1999
|
statSync,
|
|
1908
2000
|
writeFileSync
|
|
1909
2001
|
} from "fs";
|
|
1910
|
-
import { dirname, isAbsolute, join, parse, resolve, sep } from "path";
|
|
2002
|
+
import { dirname, isAbsolute as isAbsolute2, join, parse, resolve, sep } from "path";
|
|
1911
2003
|
function assetDirForIr(irPath) {
|
|
1912
2004
|
const { dir, name } = parse(resolve(irPath));
|
|
1913
2005
|
return join(dir, `${name}.assets`);
|
|
@@ -1932,7 +2024,7 @@ function prepareAssetLanding(ir, irPath, cwd) {
|
|
|
1932
2024
|
`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
2025
|
);
|
|
1934
2026
|
}
|
|
1935
|
-
const data =
|
|
2027
|
+
const data = readFileSync2(source);
|
|
1936
2028
|
const actual = `sha256:${createHash("sha256").update(data).digest("hex")}`;
|
|
1937
2029
|
if (actual !== asset.hash) {
|
|
1938
2030
|
throw new DispatchError(
|
|
@@ -1959,7 +2051,7 @@ function landAssets(ir, irPath, cwd) {
|
|
|
1959
2051
|
}
|
|
1960
2052
|
function assertContainedRelativePath(relative, componentId) {
|
|
1961
2053
|
const climbs = relative.split(/[\\/]/).includes("..");
|
|
1962
|
-
if (
|
|
2054
|
+
if (isAbsolute2(relative) || climbs || relative.startsWith(sep)) {
|
|
1963
2055
|
throw new DispatchError(
|
|
1964
2056
|
`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
2057
|
);
|
|
@@ -2162,6 +2254,9 @@ function makeReadOnlyGuard(cfg) {
|
|
|
2162
2254
|
denials.push({ tool: toolName, reason: reason2 });
|
|
2163
2255
|
return deny(reason2);
|
|
2164
2256
|
}
|
|
2257
|
+
if (cfg.hostMcpTools?.includes(toolName)) {
|
|
2258
|
+
return allow(input);
|
|
2259
|
+
}
|
|
2165
2260
|
const reason = `tool '${toolName}' is not permitted for this component.`;
|
|
2166
2261
|
denials.push({ tool: toolName, reason });
|
|
2167
2262
|
return deny(reason);
|
|
@@ -2603,6 +2698,27 @@ function aggregateTrace(messages, meta, denials) {
|
|
|
2603
2698
|
denials
|
|
2604
2699
|
};
|
|
2605
2700
|
}
|
|
2701
|
+
function applyHostMcp(options, hostMcp) {
|
|
2702
|
+
if (options.mcpServers && hostMcp.name in options.mcpServers) {
|
|
2703
|
+
throw new DispatchError(
|
|
2704
|
+
`--host-mcp-config: MCP server name '${hostMcp.name}' is already used by this plan`
|
|
2705
|
+
);
|
|
2706
|
+
}
|
|
2707
|
+
const next = {
|
|
2708
|
+
...options,
|
|
2709
|
+
mcpServers: { ...options.mcpServers, [hostMcp.name]: hostMcpServerEntry(hostMcp) }
|
|
2710
|
+
};
|
|
2711
|
+
if (hostMcp.instruction !== void 0) {
|
|
2712
|
+
if (typeof options.systemPrompt !== "string") {
|
|
2713
|
+
throw new DispatchError(
|
|
2714
|
+
"--host-mcp-config: 'instruction' needs a plain-string system prompt, which this plan does not have"
|
|
2715
|
+
);
|
|
2716
|
+
}
|
|
2717
|
+
next.systemPrompt = `${options.systemPrompt}
|
|
2718
|
+
${hostMcp.instruction}`;
|
|
2719
|
+
}
|
|
2720
|
+
return next;
|
|
2721
|
+
}
|
|
2606
2722
|
var DispatchSessionError = class extends DispatchError {
|
|
2607
2723
|
constructor(message, sessionId) {
|
|
2608
2724
|
super(message);
|
|
@@ -2642,6 +2758,11 @@ async function runDispatch(plan, cfg) {
|
|
|
2642
2758
|
);
|
|
2643
2759
|
}
|
|
2644
2760
|
if (plan.meta.mode === "hybrid-staged") {
|
|
2761
|
+
if (cfg.hostMcp) {
|
|
2762
|
+
throw new DispatchError(
|
|
2763
|
+
"--host-mcp-config is not supported for a hybrid-staged plan (steps span providers)"
|
|
2764
|
+
);
|
|
2765
|
+
}
|
|
2645
2766
|
return process.env["WARBLE_HYBRID_MODE"] === "tool" ? runHybridTool(plan, cfg) : runHybridStaged(plan, cfg);
|
|
2646
2767
|
}
|
|
2647
2768
|
mkdirSync3(cfg.outDir, { recursive: true });
|
|
@@ -2652,12 +2773,13 @@ async function runDispatch(plan, cfg) {
|
|
|
2652
2773
|
readOnly: plan.meta.readOnly,
|
|
2653
2774
|
writeScope,
|
|
2654
2775
|
cwd,
|
|
2655
|
-
setupScope: plan.meta.setupScope
|
|
2776
|
+
setupScope: plan.meta.setupScope,
|
|
2777
|
+
...cfg.hostMcp ? { hostMcpTools: hostMcpToolNames(cfg.hostMcp) } : {}
|
|
2656
2778
|
});
|
|
2657
2779
|
const venvBin = join4(cwd, ".venv", "bin");
|
|
2658
2780
|
const pathEnv = existsSync3(venvBin) ? `${venvBin}:${process.env.PATH ?? ""}` : process.env.PATH ?? "";
|
|
2659
2781
|
const env = { ...process.env, PATH: pathEnv };
|
|
2660
|
-
const
|
|
2782
|
+
const baseOptions = {
|
|
2661
2783
|
...plan.options,
|
|
2662
2784
|
canUseTool: composeCanUseTool(plan.options.canUseTool, canUseTool),
|
|
2663
2785
|
// Read never reaches `canUseTool` for an in-cwd path in the real SDK (see guardrails.ts); this
|
|
@@ -2666,6 +2788,7 @@ async function runDispatch(plan, cfg) {
|
|
|
2666
2788
|
env,
|
|
2667
2789
|
...cfg.resume ? { resume: cfg.resume } : {}
|
|
2668
2790
|
};
|
|
2791
|
+
const options = cfg.hostMcp ? applyHostMcp(baseOptions, cfg.hostMcp) : baseOptions;
|
|
2669
2792
|
if (cfg.assets) landAssets(cfg.assets.ir, cfg.assets.irPath, cwd);
|
|
2670
2793
|
reportPromptFingerprint(cfg, options);
|
|
2671
2794
|
const mapper = new ChatEventMapper(plan.meta.verb);
|
|
@@ -3797,7 +3920,7 @@ async function runComposedDispatch(plan, cfg) {
|
|
|
3797
3920
|
}
|
|
3798
3921
|
|
|
3799
3922
|
// src/dispatch.ts
|
|
3800
|
-
import { dirname as dirname2, isAbsolute as
|
|
3923
|
+
import { dirname as dirname2, isAbsolute as isAbsolute3, resolve as resolve2 } from "path";
|
|
3801
3924
|
var UNAVAILABLE_COMPONENT_REASON = "component is unavailable on the configured runtime";
|
|
3802
3925
|
function deepFreeze(value) {
|
|
3803
3926
|
if (typeof value !== "object" || value === null || Object.isFrozen(value)) return value;
|
|
@@ -3842,7 +3965,7 @@ function assertInvocationRealization(closure, reports, target) {
|
|
|
3842
3965
|
function resolveProjectCwd(node, opts) {
|
|
3843
3966
|
if (opts.project) return resolve2(opts.project);
|
|
3844
3967
|
const p = node.context_binding.project;
|
|
3845
|
-
if (
|
|
3968
|
+
if (isAbsolute3(p)) return p;
|
|
3846
3969
|
const baseDir = opts.irPath ? dirname2(resolve2(opts.irPath)) : process.cwd();
|
|
3847
3970
|
return resolve2(baseDir, p);
|
|
3848
3971
|
}
|
|
@@ -4285,10 +4408,10 @@ async function withinTimeout(operation, timeoutMs, abortController) {
|
|
|
4285
4408
|
try {
|
|
4286
4409
|
return await Promise.race([
|
|
4287
4410
|
operation,
|
|
4288
|
-
new Promise((_,
|
|
4411
|
+
new Promise((_, reject2) => {
|
|
4289
4412
|
timer = setTimeout(() => {
|
|
4290
4413
|
abortController.abort();
|
|
4291
|
-
|
|
4414
|
+
reject2(new Error("model catalog timed out"));
|
|
4292
4415
|
}, timeoutMs);
|
|
4293
4416
|
})
|
|
4294
4417
|
]);
|
|
@@ -4450,6 +4573,9 @@ function createChatSession(plan, runCfg, initialResumeSessionId) {
|
|
|
4450
4573
|
export {
|
|
4451
4574
|
DispatchError,
|
|
4452
4575
|
emitAgentModule,
|
|
4576
|
+
parseHostMcpConfig,
|
|
4577
|
+
loadHostMcpConfig,
|
|
4578
|
+
hostMcpToolNames,
|
|
4453
4579
|
componentDependencies,
|
|
4454
4580
|
resolveComponentClosure,
|
|
4455
4581
|
REALIZATION_KINDS,
|
|
@@ -4533,4 +4659,4 @@ export {
|
|
|
4533
4659
|
ChatSession,
|
|
4534
4660
|
createChatSession
|
|
4535
4661
|
};
|
|
4536
|
-
//# sourceMappingURL=chunk-
|
|
4662
|
+
//# sourceMappingURL=chunk-6GZGHFX7.js.map
|