@mono-agent/agent-runtime 0.6.1 → 0.8.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 +36 -16
- package/package.json +14 -7
- package/src/agent/approval.js +52 -17
- package/src/agent/sandbox-seam.js +1 -0
- package/src/agent/tools/pi-bridge.js +15 -42
- package/src/agent/tools/shared/ripgrep.js +12 -8
- package/src/ai/file-change-stats.js +0 -21
- package/src/ai/index.js +8 -0
- package/src/ai/providers/claude-cli.js +109 -5
- package/src/ai/providers/claude-sandbox.js +71 -0
- package/src/ai/providers/claude-sdk-discovery-worker.js +53 -0
- package/src/ai/providers/claude-sdk-discovery.js +352 -0
- package/src/ai/providers/claude-sdk.js +315 -163
- package/src/ai/providers/codex-app.js +823 -78
- package/src/ai/providers/opencode-app.js +682 -96
- package/src/ai/providers/opencode-server.js +508 -0
- package/src/ai/providers/pi-native/stream-subscriber.js +9 -0
- package/src/ai/runtime/capabilities.js +12 -0
- package/src/ai/runtime/context-windows.js +8 -0
- package/src/ai/runtime/registry.js +8 -2
- package/src/ai/runtime/router.js +627 -29
- package/src/ai/streaming/codex-events.js +7 -15
- package/src/ai/types.js +29 -2
- package/src/index.js +6 -0
- package/src/runtime.js +17 -1
- package/types/agent/approval.d.ts +4 -7
- package/types/agent/sandbox-seam.d.ts +5 -0
- package/types/ai/backend.d.ts +16 -0
- package/types/ai/file-change-stats.d.ts +0 -24
- package/types/ai/index.d.ts +1 -0
- package/types/ai/providers/claude-cli.d.ts +116 -0
- package/types/ai/providers/claude-sandbox.d.ts +79 -0
- package/types/ai/providers/claude-sdk-discovery-worker.d.ts +1 -0
- package/types/ai/providers/claude-sdk-discovery.d.ts +97 -0
- package/types/ai/providers/claude-sdk.d.ts +81 -5
- package/types/ai/providers/codex-app.d.ts +11 -7
- package/types/ai/providers/opencode-app.d.ts +15 -16
- package/types/ai/providers/opencode-server.d.ts +20 -0
- package/types/ai/runtime/capabilities.d.ts +19 -0
- package/types/ai/runtime/context-windows.d.ts +1 -0
- package/types/ai/runtime/router.d.ts +24 -23
- package/types/ai/streaming/codex-events.d.ts +15 -6
- package/types/ai/types.d.ts +75 -2
- package/types/index.d.ts +1 -0
|
@@ -15,6 +15,62 @@ import {
|
|
|
15
15
|
claudeNativeAgentDefinitions,
|
|
16
16
|
resolveClaudeAllowedTools,
|
|
17
17
|
} from "./claude-subagents.js";
|
|
18
|
+
import {
|
|
19
|
+
claudeCapabilityMismatchResult,
|
|
20
|
+
claudeSandboxCapabilityMismatchResult,
|
|
21
|
+
claudeSandboxPolicyProblem,
|
|
22
|
+
} from "./claude-sandbox.js";
|
|
23
|
+
import { resolveSandboxPolicy } from "../../agent/tools/shared/tool-context.js";
|
|
24
|
+
|
|
25
|
+
const CODEX_CLI_SANDBOX_POLICY_UNSUPPORTED =
|
|
26
|
+
"Direct Codex CLI cannot enforce mono-agent's native srt sandbox scopes. Remove the mono-agent sandbox policy or use a Pi runtime for exact readableRoots, writableRoots, denyWrite, and network rules.";
|
|
27
|
+
const CLAUDE_CLI_EMPTY_TOOL_POLICY_UNSUPPORTED =
|
|
28
|
+
"Claude Code CLI cannot enforce an explicit empty allowedTools list: omitting --tools would restore Claude Code's default toolset. Use a specific non-empty allowlist, a denylist, or the Claude SDK for a no-tools run.";
|
|
29
|
+
|
|
30
|
+
function codexCliToolPolicyProblem(options) {
|
|
31
|
+
const allowedTools = Array.isArray(options.allowedTools) ? options.allowedTools : null;
|
|
32
|
+
const disallowedTools = Array.isArray(options.disallowedTools) ? options.disallowedTools : [];
|
|
33
|
+
const exactAllowAll = allowedTools === null
|
|
34
|
+
|| (allowedTools.length === 1 && allowedTools[0] === "*");
|
|
35
|
+
return exactAllowAll && disallowedTools.length === 0
|
|
36
|
+
? null
|
|
37
|
+
: "Direct Codex CLI cannot enforce allowedTools/disallowedTools. Use exact allow-all ([\"*\"] with no disallowedTools) or another runtime.";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function codexCliCapabilityMismatchResult(options, error, codexErrorCode, start) {
|
|
41
|
+
const resolved = options.model;
|
|
42
|
+
const providerSessionId = (typeof options.sessionId === "string" && options.sessionId.trim())
|
|
43
|
+
|| (typeof options.providerSessionId === "string" && options.providerSessionId.trim())
|
|
44
|
+
|| null;
|
|
45
|
+
return {
|
|
46
|
+
text: null,
|
|
47
|
+
structuredResult: undefined,
|
|
48
|
+
structuredResultSource: null,
|
|
49
|
+
events: [],
|
|
50
|
+
usage: {},
|
|
51
|
+
durationMs: Date.now() - start,
|
|
52
|
+
numTurns: 0,
|
|
53
|
+
model: resolved?.reference || `codex:${resolved?.model || ""}`,
|
|
54
|
+
effort: options.effort || null,
|
|
55
|
+
sdk: "codex",
|
|
56
|
+
providerSessionId,
|
|
57
|
+
provider_session_id: providerSessionId,
|
|
58
|
+
cancelled: false,
|
|
59
|
+
error,
|
|
60
|
+
failureKind: "skipped_capability_mismatch",
|
|
61
|
+
diagnostics: { codex_error_code: codexErrorCode },
|
|
62
|
+
capabilitiesUsed: buildCapabilitiesUsed({
|
|
63
|
+
promptCacheActive: null,
|
|
64
|
+
thinkingEnabled: null,
|
|
65
|
+
structuredOutputEnforced: !!options.outputSchema,
|
|
66
|
+
subagentInvoked: null,
|
|
67
|
+
mcpServersUsed: [],
|
|
68
|
+
nativeSubagentsUsed: [],
|
|
69
|
+
toolCompactionApplied: false,
|
|
70
|
+
contextCompactionApplied: null,
|
|
71
|
+
}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
18
74
|
|
|
19
75
|
const DORMANT_CLI_CAPABILITIES = {
|
|
20
76
|
streaming: true,
|
|
@@ -371,9 +427,8 @@ export function buildCliCommand({
|
|
|
371
427
|
nativeSubagents,
|
|
372
428
|
contextWindow,
|
|
373
429
|
}) {
|
|
374
|
-
// Effort
|
|
375
|
-
//
|
|
376
|
-
// pass an already-normalized reasoning level (low/medium/high/xhigh/none).
|
|
430
|
+
// Effort arrives pre-normalized (low/medium/high/xhigh/max/none); per-CLI
|
|
431
|
+
// ceilings are clamped below rather than by callers.
|
|
377
432
|
const normalizedEffort = typeof effort === "string" && effort.trim() ? effort : null;
|
|
378
433
|
if (sdk === "claude-code") {
|
|
379
434
|
const nativeAgents = claudeNativeAgentDefinitions(nativeSubagents);
|
|
@@ -435,8 +490,10 @@ export function buildCliCommand({
|
|
|
435
490
|
if (permissionMode === "bypassPermissions") args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
436
491
|
else if (permissionMode === "acceptEdits" || permissionMode === "auto") args.push("--full-auto");
|
|
437
492
|
else if (permissionMode === "plan") args.push("--sandbox", "read-only");
|
|
438
|
-
|
|
439
|
-
|
|
493
|
+
// codex has no "max" reasoning tier; clamp to its ceiling instead of crashing the CLI.
|
|
494
|
+
const codexEffort = normalizedEffort === "max" ? "xhigh" : normalizedEffort;
|
|
495
|
+
if (codexEffort) args.push("--config", `model_reasoning_effort=${codexEffort}`);
|
|
496
|
+
if (codexEffort !== "none") args.push("--config", `model_reasoning_summary=${tomlValue("auto")}`);
|
|
440
497
|
if (hasEntries(mcpServers)) args.push(...codexMcpConfigArgs(mcpServers));
|
|
441
498
|
args.push([systemPrompt, prompt].filter((part) => String(part || "").trim()).join("\n\n"));
|
|
442
499
|
return { command: "codex", args, cwd };
|
|
@@ -445,6 +502,53 @@ export function buildCliCommand({
|
|
|
445
502
|
export async function generateCliResponse(systemPrompt, options = {}) {
|
|
446
503
|
const start = Date.now();
|
|
447
504
|
const resolved = options.model;
|
|
505
|
+
if (resolved?.sdk === "claude-code" && claudeSandboxPolicyProblem(options)) {
|
|
506
|
+
const providerSessionId = (typeof options.sessionId === "string" && options.sessionId.trim())
|
|
507
|
+
|| (typeof options.providerSessionId === "string" && options.providerSessionId.trim())
|
|
508
|
+
|| null;
|
|
509
|
+
return claudeSandboxCapabilityMismatchResult({
|
|
510
|
+
model: resolved.reference || `claude:${resolved.model}`,
|
|
511
|
+
effort: options.effort,
|
|
512
|
+
sdk: "claude-code",
|
|
513
|
+
providerSessionId,
|
|
514
|
+
durationMs: Date.now() - start,
|
|
515
|
+
outputSchema: options.outputSchema,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
if (resolved?.sdk === "claude-code" && Array.isArray(options.allowedTools) && options.allowedTools.length === 0) {
|
|
519
|
+
const providerSessionId = (typeof options.sessionId === "string" && options.sessionId.trim())
|
|
520
|
+
|| (typeof options.providerSessionId === "string" && options.providerSessionId.trim())
|
|
521
|
+
|| null;
|
|
522
|
+
return claudeCapabilityMismatchResult({
|
|
523
|
+
model: resolved.reference || `claude:${resolved.model}`,
|
|
524
|
+
effort: options.effort,
|
|
525
|
+
sdk: "claude-code",
|
|
526
|
+
providerSessionId,
|
|
527
|
+
durationMs: Date.now() - start,
|
|
528
|
+
outputSchema: options.outputSchema,
|
|
529
|
+
error: CLAUDE_CLI_EMPTY_TOOL_POLICY_UNSUPPORTED,
|
|
530
|
+
errorCode: "claude_cli_empty_tool_policy_unsupported",
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
if (resolved?.sdk !== "claude-code") {
|
|
534
|
+
if (resolveSandboxPolicy(options.toolContext, options.sandboxPolicy) !== undefined) {
|
|
535
|
+
return codexCliCapabilityMismatchResult(
|
|
536
|
+
options,
|
|
537
|
+
CODEX_CLI_SANDBOX_POLICY_UNSUPPORTED,
|
|
538
|
+
"codex_sandbox_policy_unsupported",
|
|
539
|
+
start,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
const toolPolicyProblem = codexCliToolPolicyProblem(options);
|
|
543
|
+
if (toolPolicyProblem !== null) {
|
|
544
|
+
return codexCliCapabilityMismatchResult(
|
|
545
|
+
options,
|
|
546
|
+
toolPolicyProblem,
|
|
547
|
+
"codex_tool_policy_unsupported",
|
|
548
|
+
start,
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
448
552
|
const prompt = promptFromMessages(options.messages);
|
|
449
553
|
const dir = mkdtempSync(join(tmpdir(), (options.toolContext?.runtimeBrand ?? readRuntimeBrand()).tempdirPrefix));
|
|
450
554
|
const schemaPath = options.outputSchema ? join(dir, "output-schema.json") : null;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { buildCapabilitiesUsed } from "../runtime/capabilities-used.js";
|
|
2
|
+
import { resolveSandboxPolicy } from "../../agent/tools/shared/tool-context.js";
|
|
3
|
+
|
|
4
|
+
export const CLAUDE_SANDBOX_POLICY_UNSUPPORTED =
|
|
5
|
+
"Claude SDK/CLI cannot enforce mono-agent's native srt sandbox scopes. Remove the mono-agent sandbox policy or use a Pi runtime for exact readableRoots, writableRoots, denyWrite, and network rules.";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Claude owns its built-in tool subprocesses, so mono-agent's runtime tool
|
|
9
|
+
* context cannot wrap them with the configured srt engine. An explicit `off`
|
|
10
|
+
* policy is inert and remains valid; every enforcing mono-agent mode must fail
|
|
11
|
+
* before the provider starts instead of silently running outside that policy.
|
|
12
|
+
*/
|
|
13
|
+
export function claudeSandboxPolicyProblem(options) {
|
|
14
|
+
const effectivePolicy = resolveSandboxPolicy(
|
|
15
|
+
options?.toolContext,
|
|
16
|
+
options?.sandboxPolicy,
|
|
17
|
+
);
|
|
18
|
+
return effectivePolicy !== undefined
|
|
19
|
+
? CLAUDE_SANDBOX_POLICY_UNSUPPORTED
|
|
20
|
+
: null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Typed provider result used by Claude bridges for fail-closed capability paths. */
|
|
24
|
+
export function claudeCapabilityMismatchResult({
|
|
25
|
+
model,
|
|
26
|
+
effort,
|
|
27
|
+
sdk,
|
|
28
|
+
providerSessionId = null,
|
|
29
|
+
durationMs = 0,
|
|
30
|
+
outputSchema,
|
|
31
|
+
error,
|
|
32
|
+
errorCode,
|
|
33
|
+
}) {
|
|
34
|
+
return {
|
|
35
|
+
text: null,
|
|
36
|
+
structuredResult: undefined,
|
|
37
|
+
structuredResultSource: null,
|
|
38
|
+
events: [],
|
|
39
|
+
usage: {},
|
|
40
|
+
durationMs,
|
|
41
|
+
numTurns: 0,
|
|
42
|
+
model,
|
|
43
|
+
effort: effort || null,
|
|
44
|
+
sdk,
|
|
45
|
+
providerSessionId,
|
|
46
|
+
provider_session_id: providerSessionId,
|
|
47
|
+
cancelled: false,
|
|
48
|
+
error,
|
|
49
|
+
failureKind: "skipped_capability_mismatch",
|
|
50
|
+
diagnostics: { claude_error_code: errorCode },
|
|
51
|
+
capabilitiesUsed: buildCapabilitiesUsed({
|
|
52
|
+
promptCacheActive: null,
|
|
53
|
+
thinkingEnabled: null,
|
|
54
|
+
structuredOutputEnforced: !!outputSchema,
|
|
55
|
+
subagentInvoked: null,
|
|
56
|
+
mcpServersUsed: [],
|
|
57
|
+
nativeSubagentsUsed: [],
|
|
58
|
+
toolCompactionApplied: false,
|
|
59
|
+
contextCompactionApplied: null,
|
|
60
|
+
}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Typed provider result used by both Claude bridges for the sandbox path. */
|
|
65
|
+
export function claudeSandboxCapabilityMismatchResult(options) {
|
|
66
|
+
return claudeCapabilityMismatchResult({
|
|
67
|
+
...options,
|
|
68
|
+
error: CLAUDE_SANDBOX_POLICY_UNSUPPORTED,
|
|
69
|
+
errorCode: "claude_sandbox_policy_unsupported",
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import { normalizeClaudeSdkCatalog } from "./claude-sdk-discovery.js";
|
|
3
|
+
|
|
4
|
+
const abortController = new AbortController();
|
|
5
|
+
let activeQuery = null;
|
|
6
|
+
|
|
7
|
+
async function* emptyInput() {
|
|
8
|
+
// Initialization is the operation. An empty async input keeps the SDK from
|
|
9
|
+
// executing a paid model turn while still opening the control channel.
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function abort() {
|
|
13
|
+
abortController.abort();
|
|
14
|
+
try { activeQuery?.close?.(); } catch { /* best effort */ }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
process.on("message", (message) => {
|
|
18
|
+
if (message && typeof message === "object" && /** @type {any} */ (message).type === "abort") abort();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
async function main() {
|
|
22
|
+
try {
|
|
23
|
+
activeQuery = query({
|
|
24
|
+
prompt: emptyInput(),
|
|
25
|
+
options: /** @type {any} */ ({
|
|
26
|
+
cwd: process.cwd(),
|
|
27
|
+
abortController,
|
|
28
|
+
persistSession: false,
|
|
29
|
+
settingSources: [],
|
|
30
|
+
tools: [],
|
|
31
|
+
mcpServers: {},
|
|
32
|
+
strictMcpConfig: true,
|
|
33
|
+
env: {
|
|
34
|
+
...process.env,
|
|
35
|
+
MCP_CONNECTION_NONBLOCKING: "0",
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
39
|
+
const initialization = await activeQuery.initializationResult();
|
|
40
|
+
const models = normalizeClaudeSdkCatalog(initialization?.models, "discovered");
|
|
41
|
+
process.send?.({ type: "claude_catalog", models });
|
|
42
|
+
} catch {
|
|
43
|
+
// Raw SDK errors may contain paths or account detail. The parent needs
|
|
44
|
+
// only a typed failure signal so it can use the curated cache.
|
|
45
|
+
process.send?.({ type: "claude_catalog_error" });
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
} finally {
|
|
48
|
+
try { activeQuery?.close?.(); } catch { /* best effort */ }
|
|
49
|
+
try { process.disconnect?.(); } catch { /* best effort */ }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
void main();
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { fork } from "node:child_process";
|
|
2
|
+
import { chmod, mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_TIMEOUT_MS = 5_000;
|
|
8
|
+
const CHILD_STOP_TIMEOUT_MS = 300;
|
|
9
|
+
const MAX_MODELS = 64;
|
|
10
|
+
const MAX_DESCRIPTION_CHARS = 320;
|
|
11
|
+
const MODEL_ALIASES = new Set(["default", "opus", "sonnet", "haiku", "fable", "mythos", "inherit"]);
|
|
12
|
+
const SUPPORTED_EFFORTS = new Set(["low", "medium", "high", "xhigh", "max"]);
|
|
13
|
+
const WORKER_PATH = fileURLToPath(new URL("./claude-sdk-discovery-worker.js", import.meta.url));
|
|
14
|
+
|
|
15
|
+
export const CLAUDE_SDK_CATALOG_VERSION = "claude-agent-sdk-0.3.206";
|
|
16
|
+
|
|
17
|
+
/** @typedef {"low"|"medium"|"high"|"xhigh"|"max"} ClaudeSdkEffort */
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {Object} ClaudeSdkCatalogModel
|
|
20
|
+
* @property {string} model Exact model id accepted by the SDK.
|
|
21
|
+
* @property {`claude:${string}`} reference Canonical mono-agent reference.
|
|
22
|
+
* @property {string} displayName
|
|
23
|
+
* @property {string} description
|
|
24
|
+
* @property {readonly ClaudeSdkEffort[]} supportedEfforts
|
|
25
|
+
* @property {boolean} supportsAdaptiveThinking
|
|
26
|
+
* @property {boolean} supportsFastMode
|
|
27
|
+
* @property {"discovered"|"cached"} source
|
|
28
|
+
* @property {typeof CLAUDE_SDK_CATALOG_VERSION} catalogVersion
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const CURATED_CATALOG = Object.freeze([
|
|
32
|
+
curatedModel("claude-sonnet-5", "Claude Sonnet 5", "Efficient for routine tasks", {
|
|
33
|
+
supportedEfforts: ["low", "medium", "high", "xhigh", "max"],
|
|
34
|
+
supportsAdaptiveThinking: true,
|
|
35
|
+
}),
|
|
36
|
+
curatedModel("claude-opus-4-8[1m]", "Claude Opus 4.8 (1M context)", "Opus 4.8 with the 1M context window", {
|
|
37
|
+
supportedEfforts: ["low", "medium", "high", "xhigh", "max"],
|
|
38
|
+
supportsAdaptiveThinking: true,
|
|
39
|
+
supportsFastMode: true,
|
|
40
|
+
}),
|
|
41
|
+
curatedModel("claude-haiku-4-5-20251001", "Claude Haiku 4.5", "Fastest for quick answers"),
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {string} model
|
|
46
|
+
* @param {string} displayName
|
|
47
|
+
* @param {string} description
|
|
48
|
+
* @param {{supportedEfforts?: ClaudeSdkEffort[], supportsAdaptiveThinking?: boolean, supportsFastMode?: boolean}} [capabilities]
|
|
49
|
+
* @returns {Readonly<ClaudeSdkCatalogModel>}
|
|
50
|
+
*/
|
|
51
|
+
function curatedModel(model, displayName, description, capabilities = {}) {
|
|
52
|
+
return Object.freeze({
|
|
53
|
+
model,
|
|
54
|
+
reference: `claude:${model}`,
|
|
55
|
+
displayName,
|
|
56
|
+
description,
|
|
57
|
+
supportedEfforts: Object.freeze([...(capabilities.supportedEfforts || [])]),
|
|
58
|
+
supportsAdaptiveThinking: capabilities.supportsAdaptiveThinking === true,
|
|
59
|
+
supportsFastMode: capabilities.supportsFastMode === true,
|
|
60
|
+
source: "cached",
|
|
61
|
+
catalogVersion: CLAUDE_SDK_CATALOG_VERSION,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Return the versioned, SDK-matched fallback without exposing mutable shared
|
|
67
|
+
* objects to callers.
|
|
68
|
+
*/
|
|
69
|
+
export function curatedClaudeSdkModels() {
|
|
70
|
+
return CURATED_CATALOG.map((entry) => ({ ...entry, supportedEfforts: [...entry.supportedEfforts] }));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Normalize only exact Claude model ids. CLI convenience aliases are rejected
|
|
75
|
+
* so persisted configuration never changes meaning when an alias advances.
|
|
76
|
+
* Exact dated ids and a canonical `[1m]` suffix are preserved.
|
|
77
|
+
* @param {unknown} value
|
|
78
|
+
* @returns {string|null}
|
|
79
|
+
*/
|
|
80
|
+
export function normalizeClaudeSdkModelId(value) {
|
|
81
|
+
let model = String(value ?? "").trim().toLowerCase();
|
|
82
|
+
if (!model || model.length > 160) return null;
|
|
83
|
+
if (model.startsWith("claude:")) model = model.slice("claude:".length);
|
|
84
|
+
if (MODEL_ALIASES.has(model)) return null;
|
|
85
|
+
|
|
86
|
+
const contextSuffix = model.endsWith("[1m]") ? "[1m]" : "";
|
|
87
|
+
if (contextSuffix) model = model.slice(0, -contextSuffix.length);
|
|
88
|
+
if (!/^claude-(?:opus|sonnet|haiku|fable|mythos)-\d+(?:-\d+)*$/u.test(model)) return null;
|
|
89
|
+
return `${model}${contextSuffix}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function displayNameForModel(model) {
|
|
93
|
+
const oneMillion = model.endsWith("[1m]");
|
|
94
|
+
const base = oneMillion ? model.slice(0, -4) : model;
|
|
95
|
+
const match = /^claude-([a-z]+)-(.+)$/u.exec(base);
|
|
96
|
+
if (!match) return model;
|
|
97
|
+
const family = `${match[1][0].toUpperCase()}${match[1].slice(1)}`;
|
|
98
|
+
const version = match[2].replace(/-20\d{6}$/u, "").replace(/-/g, ".");
|
|
99
|
+
return `Claude ${family} ${version}${oneMillion ? " (1M context)" : ""}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function boundedCatalogText(value, limit) {
|
|
103
|
+
const text = String(value ?? "")
|
|
104
|
+
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ")
|
|
105
|
+
.replace(/\s+/g, " ")
|
|
106
|
+
.trim();
|
|
107
|
+
return text.length <= limit ? text : `${text.slice(0, Math.max(0, limit - 1))}…`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function normalizedEfforts(entry) {
|
|
111
|
+
const values = Array.isArray(entry?.supportedEffortLevels)
|
|
112
|
+
? entry.supportedEffortLevels
|
|
113
|
+
: Array.isArray(entry?.supportedEfforts)
|
|
114
|
+
? entry.supportedEfforts
|
|
115
|
+
: [];
|
|
116
|
+
return [...new Set(values.map((value) => String(value)).filter((value) => SUPPORTED_EFFORTS.has(value)))];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Whitelist the SDK initialization catalog into the public mono-agent shape.
|
|
121
|
+
* No account, organization, token source, raw error, or unknown SDK field can
|
|
122
|
+
* cross this boundary.
|
|
123
|
+
* @param {unknown} rows
|
|
124
|
+
* @param {"discovered"|"cached"} [source]
|
|
125
|
+
* @returns {ClaudeSdkCatalogModel[]}
|
|
126
|
+
*/
|
|
127
|
+
export function normalizeClaudeSdkCatalog(rows, source = "discovered") {
|
|
128
|
+
if (!Array.isArray(rows)) return [];
|
|
129
|
+
const byModel = new Map();
|
|
130
|
+
for (const raw of rows.slice(0, MAX_MODELS * 4)) {
|
|
131
|
+
if (!raw || typeof raw !== "object") continue;
|
|
132
|
+
const row = /** @type {any} */ (raw);
|
|
133
|
+
const model = normalizeClaudeSdkModelId(
|
|
134
|
+
row.resolvedModel ?? row.model ?? row.reference ?? row.value,
|
|
135
|
+
);
|
|
136
|
+
if (!model) continue;
|
|
137
|
+
const supportedEfforts = /** @type {ClaudeSdkEffort[]} */ (normalizedEfforts(row));
|
|
138
|
+
const description = boundedCatalogText(row.description, MAX_DESCRIPTION_CHARS)
|
|
139
|
+
|| `Claude ${displayNameForModel(model).replace(/^Claude /u, "")}`;
|
|
140
|
+
const current = byModel.get(model);
|
|
141
|
+
/** @type {ClaudeSdkCatalogModel} */
|
|
142
|
+
const normalized = {
|
|
143
|
+
model,
|
|
144
|
+
reference: `claude:${model}`,
|
|
145
|
+
displayName: displayNameForModel(model),
|
|
146
|
+
description,
|
|
147
|
+
supportedEfforts,
|
|
148
|
+
supportsAdaptiveThinking: row.supportsAdaptiveThinking === true,
|
|
149
|
+
supportsFastMode: row.supportsFastMode === true,
|
|
150
|
+
source,
|
|
151
|
+
catalogVersion: CLAUDE_SDK_CATALOG_VERSION,
|
|
152
|
+
};
|
|
153
|
+
if (!current) {
|
|
154
|
+
byModel.set(model, normalized);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
byModel.set(model, {
|
|
158
|
+
...current,
|
|
159
|
+
description: current.description.length >= normalized.description.length
|
|
160
|
+
? current.description
|
|
161
|
+
: normalized.description,
|
|
162
|
+
supportedEfforts: [...new Set([...current.supportedEfforts, ...supportedEfforts])],
|
|
163
|
+
supportsAdaptiveThinking: current.supportsAdaptiveThinking || normalized.supportsAdaptiveThinking,
|
|
164
|
+
supportsFastMode: current.supportsFastMode || normalized.supportsFastMode,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return [...byModel.values()].slice(0, MAX_MODELS);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function authoredCatalogRows(references) {
|
|
171
|
+
return (Array.isArray(references) ? references : []).map((reference) => ({ reference }));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** @param {...ClaudeSdkCatalogModel[]} catalogs @returns {ClaudeSdkCatalogModel[]} */
|
|
175
|
+
function mergeCatalogs(...catalogs) {
|
|
176
|
+
const byModel = new Map();
|
|
177
|
+
for (const catalog of catalogs) {
|
|
178
|
+
for (const entry of catalog) {
|
|
179
|
+
const existing = byModel.get(entry.model);
|
|
180
|
+
if (!existing || (existing.source === "cached" && entry.source === "discovered")) {
|
|
181
|
+
byModel.set(entry.model, entry);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return [...byModel.values()].slice(0, MAX_MODELS);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function safeDiscoveryEnvironment(baseEnvironment = process.env) {
|
|
189
|
+
const env = {};
|
|
190
|
+
for (const key of [
|
|
191
|
+
"PATH", "LANG", "LC_ALL", "LC_CTYPE", "SHELL",
|
|
192
|
+
"SystemRoot", "WINDIR", "ComSpec", "PATHEXT",
|
|
193
|
+
]) {
|
|
194
|
+
const value = baseEnvironment[key];
|
|
195
|
+
if (typeof value === "string" && value) env[key] = value;
|
|
196
|
+
}
|
|
197
|
+
return env;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function privateDirectory(parent, name) {
|
|
201
|
+
const path = join(parent, name);
|
|
202
|
+
await mkdir(path, { mode: 0o700 });
|
|
203
|
+
if (process.platform !== "win32") await chmod(path, 0o700);
|
|
204
|
+
return path;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** @internal Exported for deterministic isolation tests. */
|
|
208
|
+
export async function createClaudeSdkDiscoveryIsolation({ baseEnvironment = process.env } = {}) {
|
|
209
|
+
const root = await mkdtemp(join(tmpdir(), "mono-agent-claude-discovery-"));
|
|
210
|
+
try {
|
|
211
|
+
if (process.platform !== "win32") await chmod(root, 0o700);
|
|
212
|
+
const home = await privateDirectory(root, "home");
|
|
213
|
+
const claudeConfig = await privateDirectory(root, "secure-storage");
|
|
214
|
+
const config = await privateDirectory(root, "xdg-config");
|
|
215
|
+
const cache = await privateDirectory(root, "xdg-cache");
|
|
216
|
+
const data = await privateDirectory(root, "xdg-data");
|
|
217
|
+
const state = await privateDirectory(root, "xdg-state");
|
|
218
|
+
const temp = await privateDirectory(root, "tmp");
|
|
219
|
+
const cwd = await privateDirectory(root, "cwd");
|
|
220
|
+
const env = {
|
|
221
|
+
...safeDiscoveryEnvironment(baseEnvironment),
|
|
222
|
+
HOME: home,
|
|
223
|
+
CLAUDE_CONFIG_DIR: claudeConfig,
|
|
224
|
+
XDG_CONFIG_HOME: config,
|
|
225
|
+
XDG_CACHE_HOME: cache,
|
|
226
|
+
XDG_DATA_HOME: data,
|
|
227
|
+
XDG_STATE_HOME: state,
|
|
228
|
+
TMPDIR: temp,
|
|
229
|
+
TMP: temp,
|
|
230
|
+
TEMP: temp,
|
|
231
|
+
CLAUDE_AGENT_SDK_CLIENT_APP: "mono-agent-model-discovery/0.6",
|
|
232
|
+
MCP_CONNECTION_NONBLOCKING: "0",
|
|
233
|
+
};
|
|
234
|
+
return {
|
|
235
|
+
root,
|
|
236
|
+
cwd,
|
|
237
|
+
env,
|
|
238
|
+
cleanup: async () => {
|
|
239
|
+
await rm(root, { recursive: true, force: true }).catch(() => undefined);
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
} catch (error) {
|
|
243
|
+
await rm(root, { recursive: true, force: true }).catch(() => undefined);
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function waitForWorker(child, timeoutMs) {
|
|
249
|
+
return new Promise((resolve, reject) => {
|
|
250
|
+
let settled = false;
|
|
251
|
+
const finish = (callback, value) => {
|
|
252
|
+
if (settled) return;
|
|
253
|
+
settled = true;
|
|
254
|
+
clearTimeout(timer);
|
|
255
|
+
child.removeListener?.("message", onMessage);
|
|
256
|
+
child.removeListener?.("error", onError);
|
|
257
|
+
child.removeListener?.("exit", onExit);
|
|
258
|
+
callback(value);
|
|
259
|
+
};
|
|
260
|
+
const onMessage = (message) => {
|
|
261
|
+
if (message?.type === "claude_catalog") finish(resolve, message.models);
|
|
262
|
+
else if (message?.type === "claude_catalog_error") finish(reject, new Error("Claude catalog worker failed"));
|
|
263
|
+
};
|
|
264
|
+
const onError = () => finish(reject, new Error("Claude catalog worker failed"));
|
|
265
|
+
const onExit = (code) => {
|
|
266
|
+
if (code !== 0) finish(reject, new Error("Claude catalog worker exited before returning a catalog"));
|
|
267
|
+
};
|
|
268
|
+
const timer = setTimeout(() => {
|
|
269
|
+
try {
|
|
270
|
+
if (child.connected !== false) child.send?.({ type: "abort" }, () => undefined);
|
|
271
|
+
} catch { /* best effort */ }
|
|
272
|
+
finish(reject, new Error("Claude catalog discovery timed out"));
|
|
273
|
+
}, timeoutMs);
|
|
274
|
+
timer.unref?.();
|
|
275
|
+
child.on("message", onMessage);
|
|
276
|
+
child.on("error", onError);
|
|
277
|
+
child.on("exit", onExit);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function stopWorker(child) {
|
|
282
|
+
if (!child || child.exitCode != null || child.signalCode != null) return;
|
|
283
|
+
await new Promise((resolve) => {
|
|
284
|
+
let settled = false;
|
|
285
|
+
let timer;
|
|
286
|
+
const finish = () => {
|
|
287
|
+
if (settled) return;
|
|
288
|
+
settled = true;
|
|
289
|
+
clearTimeout(timer);
|
|
290
|
+
child.removeListener?.("exit", finish);
|
|
291
|
+
child.removeListener?.("error", finish);
|
|
292
|
+
resolve(undefined);
|
|
293
|
+
};
|
|
294
|
+
child.once?.("exit", finish);
|
|
295
|
+
child.once?.("error", finish);
|
|
296
|
+
try {
|
|
297
|
+
if (child.connected !== false) child.send?.({ type: "abort" }, () => undefined);
|
|
298
|
+
} catch { /* best effort */ }
|
|
299
|
+
try { child.disconnect?.(); } catch { /* best effort */ }
|
|
300
|
+
timer = setTimeout(() => {
|
|
301
|
+
try { child.kill?.("SIGKILL"); } catch { /* best effort */ }
|
|
302
|
+
finish();
|
|
303
|
+
}, CHILD_STOP_TIMEOUT_MS);
|
|
304
|
+
timer.unref?.();
|
|
305
|
+
try { child.kill?.("SIGTERM"); } catch { finish(); }
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Discover Claude's current model catalog in a throwaway, no-auth process.
|
|
311
|
+
* Failure is deliberately non-fatal: the exact SDK-versioned curated catalog
|
|
312
|
+
* remains available with `source: "cached"`.
|
|
313
|
+
*
|
|
314
|
+
* @param {Object} [options]
|
|
315
|
+
* @param {number} [options.timeoutMs]
|
|
316
|
+
* @param {readonly string[]} [options.authoredModelRefs]
|
|
317
|
+
* @param {typeof fork} [options.forkProcess]
|
|
318
|
+
* @param {(isolation: Awaited<ReturnType<typeof createClaudeSdkDiscoveryIsolation>>) => void} [options.onIsolation]
|
|
319
|
+
* @returns {Promise<ClaudeSdkCatalogModel[]>}
|
|
320
|
+
*/
|
|
321
|
+
export async function discoverClaudeSdkModels({
|
|
322
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
323
|
+
authoredModelRefs = [],
|
|
324
|
+
forkProcess = fork,
|
|
325
|
+
onIsolation,
|
|
326
|
+
} = {}) {
|
|
327
|
+
const cached = curatedClaudeSdkModels();
|
|
328
|
+
const authored = normalizeClaudeSdkCatalog(authoredCatalogRows(authoredModelRefs), "cached");
|
|
329
|
+
let isolation;
|
|
330
|
+
let child;
|
|
331
|
+
try {
|
|
332
|
+
isolation = await createClaudeSdkDiscoveryIsolation();
|
|
333
|
+
onIsolation?.(isolation);
|
|
334
|
+
child = forkProcess(WORKER_PATH, [], {
|
|
335
|
+
cwd: isolation.cwd,
|
|
336
|
+
env: isolation.env,
|
|
337
|
+
// Do not inherit caller preload/debug/input-type flags into the
|
|
338
|
+
// credential-isolated discovery process.
|
|
339
|
+
execArgv: [],
|
|
340
|
+
stdio: ["ignore", "ignore", "ignore", "ipc"],
|
|
341
|
+
serialization: "json",
|
|
342
|
+
});
|
|
343
|
+
const raw = await waitForWorker(child, Math.max(1, Math.min(30_000, Number(timeoutMs) || DEFAULT_TIMEOUT_MS)));
|
|
344
|
+
const discovered = normalizeClaudeSdkCatalog(raw, "discovered");
|
|
345
|
+
return mergeCatalogs(cached, authored, discovered);
|
|
346
|
+
} catch {
|
|
347
|
+
return mergeCatalogs(cached, authored);
|
|
348
|
+
} finally {
|
|
349
|
+
await stopWorker(child);
|
|
350
|
+
await isolation?.cleanup();
|
|
351
|
+
}
|
|
352
|
+
}
|