@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.
Files changed (44) hide show
  1. package/README.md +36 -16
  2. package/package.json +14 -7
  3. package/src/agent/approval.js +52 -17
  4. package/src/agent/sandbox-seam.js +1 -0
  5. package/src/agent/tools/pi-bridge.js +15 -42
  6. package/src/agent/tools/shared/ripgrep.js +12 -8
  7. package/src/ai/file-change-stats.js +0 -21
  8. package/src/ai/index.js +8 -0
  9. package/src/ai/providers/claude-cli.js +109 -5
  10. package/src/ai/providers/claude-sandbox.js +71 -0
  11. package/src/ai/providers/claude-sdk-discovery-worker.js +53 -0
  12. package/src/ai/providers/claude-sdk-discovery.js +352 -0
  13. package/src/ai/providers/claude-sdk.js +315 -163
  14. package/src/ai/providers/codex-app.js +823 -78
  15. package/src/ai/providers/opencode-app.js +682 -96
  16. package/src/ai/providers/opencode-server.js +508 -0
  17. package/src/ai/providers/pi-native/stream-subscriber.js +9 -0
  18. package/src/ai/runtime/capabilities.js +12 -0
  19. package/src/ai/runtime/context-windows.js +8 -0
  20. package/src/ai/runtime/registry.js +8 -2
  21. package/src/ai/runtime/router.js +627 -29
  22. package/src/ai/streaming/codex-events.js +7 -15
  23. package/src/ai/types.js +29 -2
  24. package/src/index.js +6 -0
  25. package/src/runtime.js +17 -1
  26. package/types/agent/approval.d.ts +4 -7
  27. package/types/agent/sandbox-seam.d.ts +5 -0
  28. package/types/ai/backend.d.ts +16 -0
  29. package/types/ai/file-change-stats.d.ts +0 -24
  30. package/types/ai/index.d.ts +1 -0
  31. package/types/ai/providers/claude-cli.d.ts +116 -0
  32. package/types/ai/providers/claude-sandbox.d.ts +79 -0
  33. package/types/ai/providers/claude-sdk-discovery-worker.d.ts +1 -0
  34. package/types/ai/providers/claude-sdk-discovery.d.ts +97 -0
  35. package/types/ai/providers/claude-sdk.d.ts +81 -5
  36. package/types/ai/providers/codex-app.d.ts +11 -7
  37. package/types/ai/providers/opencode-app.d.ts +15 -16
  38. package/types/ai/providers/opencode-server.d.ts +20 -0
  39. package/types/ai/runtime/capabilities.d.ts +19 -0
  40. package/types/ai/runtime/context-windows.d.ts +1 -0
  41. package/types/ai/runtime/router.d.ts +24 -23
  42. package/types/ai/streaming/codex-events.d.ts +15 -6
  43. package/types/ai/types.d.ts +75 -2
  44. 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 is expected to be pre-normalized by core/ai.js#generateResponse
375
- // before reaching this provider. Direct callers of buildCliCommand must
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
- if (normalizedEffort) args.push("--config", `model_reasoning_effort=${normalizedEffort}`);
439
- if (normalizedEffort !== "none") args.push("--config", `model_reasoning_summary=${tomlValue("auto")}`);
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
+ }