@mono-agent/agent-runtime 0.6.2 → 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 (39) 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 +2 -0
  6. package/src/agent/tools/shared/ripgrep.js +12 -8
  7. package/src/ai/index.js +8 -0
  8. package/src/ai/providers/claude-cli.js +109 -5
  9. package/src/ai/providers/claude-sandbox.js +71 -0
  10. package/src/ai/providers/claude-sdk-discovery-worker.js +53 -0
  11. package/src/ai/providers/claude-sdk-discovery.js +352 -0
  12. package/src/ai/providers/claude-sdk.js +313 -35
  13. package/src/ai/providers/codex-app.js +823 -78
  14. package/src/ai/providers/opencode-app.js +682 -96
  15. package/src/ai/providers/opencode-server.js +508 -0
  16. package/src/ai/runtime/capabilities.js +12 -0
  17. package/src/ai/runtime/context-windows.js +8 -0
  18. package/src/ai/runtime/registry.js +8 -2
  19. package/src/ai/runtime/router.js +627 -29
  20. package/src/ai/types.js +29 -2
  21. package/src/index.js +6 -0
  22. package/src/runtime.js +17 -1
  23. package/types/agent/approval.d.ts +4 -7
  24. package/types/agent/sandbox-seam.d.ts +5 -0
  25. package/types/ai/backend.d.ts +16 -0
  26. package/types/ai/index.d.ts +1 -0
  27. package/types/ai/providers/claude-cli.d.ts +116 -0
  28. package/types/ai/providers/claude-sandbox.d.ts +79 -0
  29. package/types/ai/providers/claude-sdk-discovery-worker.d.ts +1 -0
  30. package/types/ai/providers/claude-sdk-discovery.d.ts +97 -0
  31. package/types/ai/providers/claude-sdk.d.ts +81 -5
  32. package/types/ai/providers/codex-app.d.ts +11 -7
  33. package/types/ai/providers/opencode-app.d.ts +15 -16
  34. package/types/ai/providers/opencode-server.d.ts +20 -0
  35. package/types/ai/runtime/capabilities.d.ts +19 -0
  36. package/types/ai/runtime/context-windows.d.ts +1 -0
  37. package/types/ai/runtime/router.d.ts +24 -23
  38. package/types/ai/types.d.ts +75 -2
  39. package/types/index.d.ts +1 -0
package/README.md CHANGED
@@ -6,13 +6,15 @@ Category: `runtime`
6
6
 
7
7
  ## Responsibility
8
8
 
9
- Provides the multi-backend agent runtime bridges (Claude SDK, Claude Code CLI, Codex app-server, Pi SDK) with provider session support. This is the runtime layer that `@mono-agent/runtime-adapter` wraps behind runtime contracts, and it enforces an optional sandbox policy for runtime-owned tools through an injectable `RuntimeSandbox` seam (a fail-closed passthrough by default; `@mono-agent/runtime-adapter` injects the real sandbox implementation for mono-agent hosts).
9
+ Provides five runtime bridges (Claude SDK, Claude Code CLI, Codex app-server, OpenCode app-server, Pi SDK), with capabilities declared per bridge. This is the runtime layer that `@mono-agent/runtime-adapter` wraps behind runtime contracts. Pi enforces optional mono-agent sandbox policy for runtime-owned tools through an injectable `RuntimeSandbox` seam (a fail-closed passthrough by default; `@mono-agent/runtime-adapter` injects the real implementation). The router supports a compatibility-preserving uniform contract or explicit isolated per-route-native contracts; no provider route silently drops required capabilities.
10
10
 
11
11
  ## Public API
12
12
 
13
13
  - `createRuntime` — runtime factory dispatching to the backend bridges
14
14
  - `ai/runtime/model-refs.js` — `parseRuntimeModelReference`, `executionModeIncompatibilityReason`
15
15
  - `ai/runtime/registry.js` — `listRuntimeBridges`
16
+ - `ai/providers/claude-sdk-discovery.js` — isolated Claude SDK model discovery without importing ambient auth/config
17
+ - `createRouterRuntime({ chain, routeSafety, resolveAttempt })` — ordered fallback routing with exact route effort and bounded safety/failover telemetry
16
18
  - Provider bridges for `claude` (SDK + CLI), `codex` (app-server), `pi` (Pi SDK), and `opencode`
17
19
  - Provider session support: bridges accept `sessionId` in run options and report `provider_session_id`; the runtime exposes `disposeSession` / `disposeAllSessions`
18
20
  - Sandbox-aware built-in tools and stdio MCP startup through an injectable `RuntimeSandbox` seam (`agent/sandbox-seam.js`) — no direct dependency on `@mono-agent/runtime-adapter`
@@ -35,12 +37,13 @@ pnpm --filter @mono-agent/agent-runtime run test
35
37
 
36
38
  ## Overview
37
39
 
38
- Generic agent runtime that supports four backends out of the box:
40
+ Generic agent runtime that supports five bridges out of the box:
39
41
 
40
- - **Claude SDK** (`@anthropic-ai/claude-agent-sdk`)
42
+ - **Claude SDK** (`@anthropic-ai/claude-agent-sdk` 0.3.206)
41
43
  - **Claude Code CLI** (the `claude` binary)
42
44
  - **Pi SDK** (`@earendil-works/pi-agent-core`, used for OpenAI / Codex / Gemini / OpenRouter / Ollama / etc. via Pi providers)
43
45
  - **Codex CLI** (the `codex` app-server)
46
+ - **OpenCode CLI** (an isolated `opencode` app-server driven through `@opencode-ai/sdk/v2`)
44
47
 
45
48
  Hosts wire in their own pricing, persistence, and credential callbacks (plus an `onCompactionRecorded` hook that fires on every automatic compaction — proactive or reactive — the pi bridge drives; see "Context compaction"). The runtime returns raw text + raw structured output; hosts that want a domain-specific contract parse it on their end.
46
49
 
@@ -57,10 +60,11 @@ npm install @mono-agent/agent-runtime
57
60
 
58
61
  Peer requirements:
59
62
 
60
- - Node.js ≥ 20
63
+ - Node.js ≥ 22.19.0
61
64
  - `claude` CLI on PATH (only for `executionMode: "cli"` with `claude` SDK)
62
65
  - `codex` CLI on PATH (only for `executionMode: "cli"` with `codex` SDK; override via the `codexAppServerCommand` option)
63
- - `ripgrep` on PATH (or supplied via `ripgrepPath`) — required for the `Glob` and `Grep` built-in tools
66
+ - stable `opencode` CLI >= 1.15.0 on PATH (only for direct `opencode:<provider>:<model>` refs)
67
+ - `Glob` and `Grep` use the packaged `@vscode/ripgrep` binary on supported platforms. An explicit `ripgrepPath` is authoritative and PATH remains a fallback; provide one of those when optional dependencies are omitted or the platform is unsupported.
64
68
 
65
69
  ## Quick start
66
70
 
@@ -91,7 +95,7 @@ console.log(result.text);
91
95
  `@mono-agent/agent-runtime` is purpose-built for **autonomous, long-running agent work** with provider portability and operational resilience as first-class concerns. It is *not* a streaming-chat UI kit. Where each peer fits:
92
96
 
93
97
  - **Vercel AI SDK** — best when you're building a chat / generative-UI experience inside a React or Next.js app. `useChat`, `useCompletion`, streaming server components, and edge-runtime compatibility are their strengths. Their provider list is curated (Anthropic, OpenAI, Google, etc., via `@ai-sdk/*` packages); there's no Pi gateway, no Claude Code CLI, no Codex CLI app-server, and no per-call provider fallback. If you're rendering a streaming chat into a browser, use them. If you're orchestrating multi-turn autonomous work that must survive a rate-limited primary provider, use us.
94
- - **Claude Agent SDK** (`@anthropic-ai/claude-agent-sdk`) — first-party Anthropic SDK. Tight integration with Claude features (canUseTool, sub-agents, hooks, MCP). We *wrap* it as one of our four backends and add transcript-resume across provider drops, a 22-kind failure taxonomy, a tool-bloat guard with artifact persistence, and a provider fallback router. Context/window handling stays with the provider — the runtime does not run its own in-loop summarization pass. Reach for the bare Anthropic SDK when you only ever talk to Claude and don't need cross-provider portability or resume.
98
+ - **Claude Agent SDK** (`@anthropic-ai/claude-agent-sdk`) — first-party Anthropic SDK. Tight integration with Claude features (canUseTool, sub-agents, hooks, MCP). We *wrap* it as one of our five bridges and add transcript-resume across provider drops, a 22-kind failure taxonomy, a tool-bloat guard with artifact persistence, and a provider fallback router. Context/window handling stays with the provider — the runtime does not run its own in-loop summarization pass. Reach for the bare Anthropic SDK when you only ever talk to Claude and don't need cross-provider portability or resume.
95
99
  - **Mastra** — a workflow engine + memory + RAG stack. Different category: it's the layer *above* a runtime. You can layer Mastra workflows on top of `@mono-agent/agent-runtime` if you want both.
96
100
  - **OpenAI Agents SDK** — first-party OpenAI SDK. Same trade-off as the Claude Agent SDK: tight integration with OpenAI, no other providers. Pi providers in our runtime cover OpenAI plus a dozen others through a single API.
97
101
  - **LangChain.js** — kitchen sink with deep abstraction stacks. We're deliberately lean; if you want chains, agents, vector stores, and parsers under one umbrella, LangChain is built for that. If you want a focused runtime kernel, use us.
@@ -101,6 +105,7 @@ console.log(result.text);
101
105
  - Anthropic Claude via the Claude Agent SDK (`claude` SDK).
102
106
  - Anthropic Claude via the `claude` Code CLI binary.
103
107
  - OpenAI's Codex via the `codex` app-server CLI.
108
+ - OpenCode providers via an isolated, password-authenticated `opencode` app-server.
104
109
  - OpenAI, Google Gemini, AWS Bedrock, OpenRouter, xAI, Groq, Mistral, Perplexity, DeepSeek, Ollama, LlamaCPP, GLM, Vercel AI Gateway, GitHub Copilot, Gemini CLI — all through the Pi (`@earendil-works/pi-ai`) provider gateway, which our SDK adapter speaks directly.
105
110
 
106
111
  **At-a-glance:**
@@ -108,8 +113,8 @@ console.log(result.text);
108
113
  | Need | Use this | Use Vercel AI SDK | Use Claude Agent SDK |
109
114
  |---|---|---|---|
110
115
  | Streaming chat UI in React/Next | ✗ | ✓ | ✗ |
111
- | Multi-provider portability | ✓ (4 backends, 15+ providers) | partial | ✗ |
112
- | CLI providers (claude/codex binaries) | ✓ | ✗ | ✗ |
116
+ | Multi-provider portability | ✓ (5 bridges, 15+ providers) | partial | ✗ |
117
+ | CLI providers (claude/codex/opencode binaries) | ✓ | ✗ | ✗ |
113
118
  | Provider fallback on rate limit / overload | ✓ (`createRouterRuntime`) | ✗ | ✗ |
114
119
  | Context handling delegated to the provider (no host auto-summarization) | ✓ | ✓ | ✓ |
115
120
  | Transcript-tail resume after provider drops | ✓ | ✗ | ✗ |
@@ -131,6 +136,7 @@ The runtime picks a backend from `options.model` + `options.executionMode`:
131
136
  | `"claude"` | `"cli"` | `claude` CLI |
132
137
  | `"pi"` | any | Pi SDK |
133
138
  | `"codex"` | `"cli"` | Codex app-server CLI |
139
+ | `"opencode"` | `"cli"` | Isolated OpenCode app-server CLI |
134
140
 
135
141
  A `model` reference can be the parsed shape `{ sdk, model, provider? }` or a string (`"pi:openai:gpt-5.5"`, `"claude:claude-sonnet-4-6"`, etc.) that you parse with the package's `parseRuntimeModelReference` helper.
136
142
 
@@ -151,7 +157,7 @@ createRuntime({
151
157
  // -- tool runtime context (process-level config for the tool kernel) --
152
158
  workspace, // primary allowed root for path-based tools
153
159
  repoRoot, // secondary allowed root
154
- ripgrepPath, // explicit path to `rg`; falls back to vendored binary, then PATH
160
+ ripgrepPath, // explicit path to `rg`; falls back to packaged binary, then PATH
155
161
  qaOutputDir, // fallback dir for Playwright MCP filename routing
156
162
  sandboxPolicy, // optional SandboxPolicy for tools and stdio MCP (enforced
157
163
  // through the injectable RuntimeSandbox seam, not a bundled dep)
@@ -297,23 +303,23 @@ The package does **not** validate `structuredResult` against your schema — it
297
303
 
298
304
  ## Provider fallback router
299
305
 
300
- `createRouterRuntime({ host, chain })` wraps the standard runtime with an ordered chain of model references. On a retryable provider failure (rate limit, overload, network blip classified via the same taxonomy as `retryableProviderFailureInfo`), it retries the same logical run against the next chain entry, replaying the transcript-tail snapshot of the previous attempt so the next provider continues rather than starts over.
306
+ `createRouterRuntime({ host, chain, routeSafety, resolveAttempt })` wraps the standard runtime with an ordered chain of model references. On a retryable provider/auth failure it retries the logical run against the next entry with one bounded transcript-tail snapshot. A chain is stateless across provider sessions. Entry `effort` is tri-state: a string fixes that route, `null` asks for provider default, and omission inherits the legacy per-run effort.
301
307
 
302
308
  ```js
303
309
  import { createRouterRuntime } from "@mono-agent/agent-runtime";
304
310
 
305
311
  const router = createRouterRuntime({
306
312
  host: { /* same shape as createRuntime */ },
313
+ routeSafety: "per-route-native",
307
314
  chain: [
308
- { sdk: "claude", model: "claude-opus-4-7" },
309
- { sdk: "claude", model: "claude-sonnet-4-6" },
310
- { model: { sdk: "pi", provider: "openai", model: "gpt-5.5" }, requires: { structured_output: true } },
315
+ { model: { sdk: "claude", model: "claude-sonnet-5" }, effort: "high" },
316
+ { model: { sdk: "codex", model: "gpt-5.6-sol" }, effort: "xhigh" },
317
+ { model: { sdk: "pi", provider: "ollama", model: "gemma4:31b" }, effort: null },
311
318
  ],
312
319
  });
313
320
 
314
321
  const result = await router.run("...", { /* same shape as runtime.run */ });
315
- console.log(result.failoverHistory);
316
- // [{ model, failureKind, requestId, retryableSubkind }, ...] one entry per attempt that didn't succeed.
322
+ console.log(result.failoverHistory, result.routeSafetyHistory);
317
323
  ```
318
324
 
319
325
  Behaviour:
@@ -324,6 +330,18 @@ Behaviour:
324
330
  - Malformed request/config/billing-type non-retryable failure → returns immediately with `failoverHistory` containing the one attempt.
325
331
  - Cancellation → returns immediately.
326
332
  - Chain exhausted → `failureKind: "provider_unavailable_exhausted"`, `failoverHistory` lists every attempt.
333
+ - `uniform` safety keeps the shared monotonic runtime; `per-route-native` isolates
334
+ route runtimes and records each bounded safety contract/status.
335
+ - Pi route telemetry distinguishes `disabled`, fail-closed `mono-agent-srt`,
336
+ and `mono-agent-srt-unsafe-host-fallback`; the last describes a configured
337
+ policy that prefers SRT but permits host execution, not which branch ran.
338
+ - A resolver-supplied Pi runtime may own provider credentials and lifecycle,
339
+ but must expose `configureTools()`: before every attempt the router replaces
340
+ its mutable tool context with the router's effective host/configured safety
341
+ inputs, while request-scoped overrides remain on that exact run. A runtime
342
+ that cannot accept this projection fails closed as `safety_unavailable`.
343
+ - Attempt-resolver failures are sanitized to `safety_unavailable`; resolver
344
+ credentials/options never enter result telemetry.
327
345
 
328
346
  Chain entries can require backend capabilities via `requires: { structured_output: true, supports_mcp: true, ... }`; entries that don't satisfy the requirements are skipped (logged in `failoverHistory` as `failureKind: "skipped_capability_mismatch"`).
329
347
 
@@ -393,7 +411,9 @@ Responses:
393
411
  - `{ decision: "deny", reason? }` — block; the agent receives a tool error.
394
412
  - `{ decision: "always" }` — allow + session-allowlist for the run.
395
413
 
396
- Backend coverage: Claude SDK (via `canUseTool`) and Pi SDK (via tool dispatch wrapping). Claude CLI and Codex CLI bridge into their backend's own approval models (`permissionMode` / `approvalPolicy`) per-call runtime gates aren't available there.
414
+ Backend coverage: Claude SDK (via `canUseTool`) and Pi SDK (via tool dispatch wrapping). Direct OpenCode projects `permissionMode` into its SDK rules and forwards native permission events through the callback; `default`/`acceptEdits` ask for reads, dynamic/custom permission names require explicit host approval in attended modes, and unsupported live-question/subagent permissions are always denied. OpenCode `plan` is read-only but not a secret boundary because path rules follow symlinks; use Pi plus native `srt` for filesystem confinement. Claude CLI and Codex app-server use their backend-native `permissionMode` / `approvalPolicy` instead of the per-call gate.
415
+
416
+ Direct OpenCode uses a password-authenticated ephemeral loopback server and a unique private database for every run; that database is deleted after the server closes, so user sessions and saved approvals are never imported. Session resume and MCP injection are intentionally unsupported. Repo/global config and external plugins/skills are disabled, and the provider shell inherits only a narrow non-secret environment; built-in providers use the normal OpenCode auth store so token rotation persists. `OPENCODE_AUTH_CONTENT` is rejected, stable OpenCode CLI >=1.15.0 is required, and the user's native DB migration marker must pre-exist. Provider replies are always one-shot—even a host `always` decision stays only in the current mono-agent run. Positive `maxTurns`, explicit effort, structured output, live input, fast mode, native subagents, and runtime skill metadata fail with typed capability mismatches before startup rather than being silently ignored.
397
417
 
398
418
  Approval lifecycle is observable via `onEvent`:
399
419
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mono-agent/agent-runtime",
3
- "version": "0.6.2",
4
- "description": "Agent runtime supporting Claude SDK, Claude CLI, Codex CLI, and PI SDK out of the box",
3
+ "version": "0.8.0",
4
+ "description": "Agent runtime supporting Claude SDK/CLI, Codex, OpenCode, and Pi SDK bridges out of the box",
5
5
  "type": "module",
6
6
  "license": "GPL-3.0-only",
7
7
  "keywords": [
@@ -70,6 +70,10 @@
70
70
  "types": "./types/ai/providers/claude-sdk.d.ts",
71
71
  "default": "./src/ai/providers/claude-sdk.js"
72
72
  },
73
+ "./ai/providers/claude-sdk-discovery.js": {
74
+ "types": "./types/ai/providers/claude-sdk-discovery.d.ts",
75
+ "default": "./src/ai/providers/claude-sdk-discovery.js"
76
+ },
73
77
  "./ai/providers/claude-cli.js": {
74
78
  "types": "./types/ai/providers/claude-cli.d.ts",
75
79
  "default": "./src/ai/providers/claude-cli.js"
@@ -123,14 +127,17 @@
123
127
  "LICENSE"
124
128
  ],
125
129
  "engines": {
126
- "node": ">=20"
130
+ "node": ">=22.19.0"
127
131
  },
128
132
  "dependencies": {
129
- "@anthropic-ai/claude-agent-sdk": "^0.1.0",
130
- "@earendil-works/pi-agent-core": "^0.80.3",
131
- "@earendil-works/pi-ai": "^0.80.3",
132
- "@modelcontextprotocol/sdk": "^1.12.0",
133
+ "@anthropic-ai/claude-agent-sdk": "0.3.206",
134
+ "@anthropic-ai/sdk": "^0.110.0",
135
+ "@earendil-works/pi-agent-core": "^0.80.5",
136
+ "@earendil-works/pi-ai": "^0.80.5",
137
+ "@modelcontextprotocol/sdk": "^1.29.0",
133
138
  "@opencode-ai/sdk": "^1.15.13",
139
+ "@vscode/ripgrep": "1.18.0",
140
+ "cross-spawn": "^7.0.6",
134
141
  "zod": "^4.3.6"
135
142
  },
136
143
  "scripts": {
@@ -30,7 +30,7 @@ export const RISK_TIERS = Object.freeze(["low", "medium", "high"]);
30
30
  const DEFAULT_TIMEOUT_MS = 60_000;
31
31
 
32
32
  /**
33
- * @param {{onToolApprovalRequest?: any, defaultRiskTier?: string, timeoutMs?: number, onEvent?: (event: any) => void, riskTiersByTool?: any, alwaysAllowTools?: any}} [options]
33
+ * @param {{onToolApprovalRequest?: any, defaultRiskTier?: string, timeoutMs?: number, onEvent?: (event: any) => void, riskTiersByTool?: any, alwaysAllowTools?: any, autoApproveLowRisk?: boolean}} [options]
34
34
  */
35
35
  export function createApprovalManager({
36
36
  onToolApprovalRequest = null,
@@ -39,6 +39,7 @@ export function createApprovalManager({
39
39
  onEvent = () => {},
40
40
  riskTiersByTool = {},
41
41
  alwaysAllowTools = [],
42
+ autoApproveLowRisk = true,
42
43
  } = {}) {
43
44
  const sessionAllowlist = new Set(normaliseList(alwaysAllowTools));
44
45
  const normalisedTiersByTool = Object.fromEntries(
@@ -63,24 +64,34 @@ export function createApprovalManager({
63
64
  const toolName = String(toolCall.toolName || toolCall.name || "");
64
65
  const toolUseId = toolCall.toolUseId || toolCall.id || null;
65
66
  const tier = riskTierFor(toolName);
67
+ const requestId = toolCall.requestId || randomUUID();
66
68
 
67
- if (tier === "low") {
68
- return { decision: "approve", reason: "low_risk", riskTier: tier };
69
+ if (tier === "low" && autoApproveLowRisk !== false) {
70
+ emitGrant({ requestId, toolName, toolUseId, tier, decision: "approve", reason: "low_risk" });
71
+ return { decision: "approve", reason: "low_risk", requestId, riskTier: tier };
69
72
  }
70
73
 
71
74
  if (sessionAllowlist.has(toolName)) {
72
- return { decision: "approve", reason: "session_allowed", riskTier: tier };
75
+ emitGrant({ requestId, toolName, toolUseId, tier, decision: "approve", reason: "session_allowed" });
76
+ return { decision: "approve", reason: "session_allowed", requestId, riskTier: tier };
73
77
  }
74
78
 
75
79
  if (typeof onToolApprovalRequest !== "function") {
76
80
  if (tier === "high") {
77
- emitDenial({ toolName, toolUseId, tier, reason: "no_host_callback_for_high_risk" });
78
- return { decision: "deny", reason: "no_host_callback_for_high_risk", riskTier: tier };
81
+ emitDenial({ requestId, toolName, toolUseId, tier, reason: "no_host_callback_for_high_risk" });
82
+ return { decision: "deny", reason: "no_host_callback_for_high_risk", requestId, riskTier: tier };
79
83
  }
80
- return { decision: "approve", reason: "no_host_callback_medium_auto_approve", riskTier: tier };
84
+ emitGrant({
85
+ requestId,
86
+ toolName,
87
+ toolUseId,
88
+ tier,
89
+ decision: "approve",
90
+ reason: "no_host_callback_medium_auto_approve",
91
+ });
92
+ return { decision: "approve", reason: "no_host_callback_medium_auto_approve", requestId, riskTier: tier };
81
93
  }
82
94
 
83
- const requestId = toolCall.requestId || randomUUID();
84
95
  const argumentsSummary = redactSecrets(stringifyShort(toolCall.input || toolCall.arguments || {}));
85
96
  const payload = {
86
97
  requestId,
@@ -95,18 +106,26 @@ export function createApprovalManager({
95
106
 
96
107
  let response;
97
108
  let timedOut = false;
109
+ let timer;
98
110
  try {
99
111
  response = await Promise.race([
100
112
  Promise.resolve().then(() => onToolApprovalRequest(payload)),
101
- new Promise((_, reject) => setTimeout(() => {
102
- timedOut = true;
103
- reject(new Error("approval_timeout"));
104
- }, timeout).unref?.()),
113
+ new Promise((_, reject) => {
114
+ timer = setTimeout(() => {
115
+ timedOut = true;
116
+ reject(new Error("approval_timeout"));
117
+ }, timeout);
118
+ timer.unref?.();
119
+ }),
105
120
  ]);
106
121
  } catch (err) {
107
- const reason = timedOut || err?.message === "approval_timeout" ? "approval_timeout" : `host_error:${err?.message || err}`;
122
+ const reason = timedOut || err?.message === "approval_timeout"
123
+ ? "approval_timeout"
124
+ : `host_error:${redactSecrets(stringifyShort(err?.message || err))}`;
108
125
  emitDenial({ requestId, toolName, toolUseId, tier, reason });
109
126
  return { decision: "deny", reason, requestId, riskTier: tier };
127
+ } finally {
128
+ if (timer !== undefined) clearTimeout(timer);
110
129
  }
111
130
 
112
131
  const normalised = normaliseResponse(response, tier);
@@ -116,14 +135,13 @@ export function createApprovalManager({
116
135
  if (normalised.decision === "deny") {
117
136
  emitDenial({ requestId, toolName, toolUseId, tier, reason: normalised.reason });
118
137
  } else {
119
- emit({
120
- type: "tool_approval_granted",
138
+ emitGrant({
121
139
  requestId,
122
140
  toolName,
123
141
  toolUseId,
142
+ tier,
124
143
  decision: normalised.decision,
125
144
  reason: normalised.reason,
126
- riskTier: tier,
127
145
  });
128
146
  }
129
147
  return { ...normalised, requestId, riskTier: tier };
@@ -141,6 +159,18 @@ export function createApprovalManager({
141
159
  });
142
160
  }
143
161
 
162
+ function emitGrant({ requestId, toolName, toolUseId = null, tier, decision, reason }) {
163
+ emit({
164
+ type: "tool_approval_granted",
165
+ requestId,
166
+ toolName,
167
+ toolUseId,
168
+ decision,
169
+ reason,
170
+ riskTier: tier,
171
+ });
172
+ }
173
+
144
174
  return {
145
175
  request,
146
176
  riskTierFor,
@@ -157,7 +187,12 @@ function normaliseResponse(response, tier) {
157
187
  const decision = APPROVAL_DECISIONS.includes(response.decision)
158
188
  ? response.decision
159
189
  : (tier === "high" ? "deny" : "approve");
160
- return { decision, reason: typeof response.reason === "string" ? response.reason : null };
190
+ return {
191
+ decision,
192
+ reason: typeof response.reason === "string"
193
+ ? redactSecrets(response.reason).slice(0, 2000)
194
+ : null,
195
+ };
161
196
  }
162
197
 
163
198
  function normaliseList(value) {
@@ -49,6 +49,7 @@
49
49
  * @property {ReadonlyArray<string>} [args]
50
50
  * @property {string} [cwd]
51
51
  * @property {Object<string, string|undefined>} [env]
52
+ * @property {boolean} [allowLocalBinding] Trusted per-command capability.
52
53
  */
53
54
 
54
55
  /**
@@ -499,6 +499,7 @@ export function resolveMcpStdioCwd(cfg = {}, cwd = null) {
499
499
  export async function prepareMcpStdioCommand(cfg = {}, { cwd = null, sandboxPolicy = null, sandboxEngine = null, ctx = null } = {}) {
500
500
  const resolvedCtx = ctx ?? readToolRuntime();
501
501
  const sandbox = resolvedCtx.sandbox ?? passthroughSandbox;
502
+ const appOwnedLocalBinding = cfg[Symbol.for("@mono-agent/app-owned-local-binding")] === true;
502
503
  return sandbox.prepareCommand({
503
504
  policy: resolveSandboxPolicy(resolvedCtx, sandboxPolicy),
504
505
  engine: sandboxEngine ?? undefined,
@@ -507,6 +508,7 @@ export async function prepareMcpStdioCommand(cfg = {}, { cwd = null, sandboxPoli
507
508
  args: cfg.args || [],
508
509
  cwd: resolveMcpStdioCwd(cfg, cwd),
509
510
  ...(cfg.env && typeof cfg.env === "object" ? { env: cfg.env } : {}),
511
+ ...(appOwnedLocalBinding ? { allowLocalBinding: true } : {}),
510
512
  },
511
513
  });
512
514
  }
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { createRequire } from "node:module";
3
- import { delimiter, dirname, join } from "node:path";
3
+ import { delimiter, join } from "node:path";
4
4
  import {
5
5
  DEFAULT_EXCLUDED_DIRS,
6
6
  DEFAULT_EXCLUDED_FILES,
@@ -36,14 +36,18 @@ export function ripgrepMissingMessage(ctx) {
36
36
  // larger change with no real-world payoff today.
37
37
  export const cachedRgPath = { value: undefined };
38
38
 
39
- function vendoredRgPath() {
39
+ function packagedRgPath() {
40
40
  try {
41
- const sdkPkg = requireFromHere.resolve("@anthropic-ai/claude-agent-sdk/package.json");
42
- const platform = process.platform === "win32" ? "win32" : process.platform;
43
- const arch = process.arch === "x64" ? "x64" : process.arch === "arm64" ? "arm64" : null;
44
- if (!arch) return null;
41
+ // Resolve the platform package relative to @vscode/ripgrep itself. pnpm's
42
+ // strict layout does not expose this optional transitive dependency from
43
+ // agent-runtime, and importing the wrapper eagerly would throw when a
44
+ // consumer intentionally installs with optional dependencies omitted.
45
+ const wrapperEntry = requireFromHere.resolve("@vscode/ripgrep");
46
+ const requireFromRipgrep = createRequire(wrapperEntry);
47
+ const arch = process.env.npm_config_arch || process.arch;
45
48
  const binaryName = process.platform === "win32" ? "rg.exe" : "rg";
46
- const candidate = join(dirname(sdkPkg), "vendor", "ripgrep", `${arch}-${platform}`, binaryName);
49
+ const platformPackage = `@vscode/ripgrep-${process.platform}-${arch}`;
50
+ const candidate = requireFromRipgrep.resolve(`${platformPackage}/bin/${binaryName}`);
47
51
  return existsSync(candidate) ? candidate : null;
48
52
  } catch {
49
53
  return null;
@@ -73,7 +77,7 @@ export function resolveRgPath({ refresh = false, ctx } = {}) {
73
77
  if (ripgrepPath) {
74
78
  cachedRgPath.value = existsSync(ripgrepPath) ? ripgrepPath : null;
75
79
  } else {
76
- cachedRgPath.value = vendoredRgPath() || rgFromPath() || null;
80
+ cachedRgPath.value = packagedRgPath() || rgFromPath() || null;
77
81
  }
78
82
  return cachedRgPath.value;
79
83
  }
package/src/ai/index.js CHANGED
@@ -10,6 +10,14 @@ export {
10
10
  } from "./runtime/sessions.js";
11
11
  export { createMetricsObserver, createObserverHub } from "./observer.js";
12
12
  export { generatePiNativeResponse, piNativeRuntimeBridge } from "./providers/pi-native.js";
13
+ export {
14
+ CLAUDE_SDK_CATALOG_VERSION,
15
+ createClaudeSdkDiscoveryIsolation,
16
+ curatedClaudeSdkModels,
17
+ discoverClaudeSdkModels,
18
+ normalizeClaudeSdkCatalog,
19
+ normalizeClaudeSdkModelId,
20
+ } from "./providers/claude-sdk-discovery.js";
13
21
  export {
14
22
  buildCapabilitiesUsed,
15
23
  toolCompactionAppliedFromWarnings,
@@ -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
+ }