@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
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.1",
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
  /**
@@ -18,8 +18,6 @@ import {
18
18
  writeToolImpl,
19
19
  } from "./index.js";
20
20
  import {
21
- createFileEditToolResultEvent,
22
- createFileEditToolUseEvent,
23
21
  fileChangeSummary,
24
22
  readFileChangeSnapshot,
25
23
  statsForCompletedChange,
@@ -165,19 +163,18 @@ function compactRawMcpResult(out) {
165
163
  };
166
164
  }
167
165
 
168
- /**
169
- * @param {any} change
170
- * @param {{status?: any, before?: any, after?: any, error?: any}} [options]
171
- */
172
- function fileEditPayload(change, { status, before, after, error } = {}) {
166
+ function writeFileChangeDetails(path, before, after) {
167
+ const change = {
168
+ path,
169
+ kind: before && before.exists ? "update" : "add",
170
+ };
173
171
  const lineStats = statsForCompletedChange(change, before, after);
174
172
  const completedChange = lineStats ? { ...change, line_stats: lineStats } : change;
175
173
  const summary = fileChangeSummary([completedChange]);
176
174
  return {
175
+ status: "completed",
177
176
  changes: [completedChange],
178
- status,
179
177
  ...(summary ? { summary } : {}),
180
- ...(error ? { error } : {}),
181
178
  };
182
179
  }
183
180
 
@@ -262,24 +259,8 @@ function createBuiltinTool(name, label, description, parameters, execute, { cwd,
262
259
  if (name === "Bash" && toolPolicy?.bashReadOnly && !isReadOnlyShellCommand(normalized.command)) {
263
260
  throw new Error("Error: Planning shell policy allows only read-only inspection commands.");
264
261
  }
265
- const isFileEdit = name === "Write" || name === "Edit";
266
- let editState = null;
267
- if (isFileEdit && normalized.file_path) {
268
- const before = readFileChangeSnapshot(normalized.file_path);
269
- editState = {
270
- path: normalized.file_path,
271
- before,
272
- change: {
273
- path: normalized.file_path,
274
- kind: name === "Write" && before && !before.exists ? "add" : "update",
275
- },
276
- };
277
- onEvent?.(createFileEditToolUseEvent(`file_edit:${toolCallId}`, {
278
- changes: [editState.change],
279
- status: "in_progress",
280
- }));
281
- }
282
-
262
+ const shouldTrackWrite = name === "Write" && typeof normalized.file_path === "string" && normalized.file_path.length > 0;
263
+ const beforeWrite = shouldTrackWrite ? readFileChangeSnapshot(normalized.file_path) : null;
283
264
  const raw = await execute(normalized, { signal, sandboxPolicy, sandboxEngine, ctx });
284
265
  // Image reads (e.g. Read on a .png) come back as a structured image
285
266
  // result so vision models see pixels; emit an image content block and let
@@ -288,22 +269,12 @@ function createBuiltinTool(name, label, description, parameters, execute, { cwd,
288
269
  return imageResult(raw.data, raw.mimeType, { tool: name, params: normalized });
289
270
  }
290
271
  const text = toolText(raw);
291
- if (isFileEdit && editState) {
292
- const failed = isErrorText(text);
293
- const after = readFileChangeSnapshot(editState.path);
294
- onEvent?.(createFileEditToolResultEvent(
295
- `file_edit:${toolCallId}`,
296
- fileEditPayload(editState.change, {
297
- status: failed ? "failed" : "completed",
298
- before: editState.before,
299
- after,
300
- error: failed ? text : null,
301
- }),
302
- { isError: failed },
303
- ));
304
- }
305
272
  if (isErrorText(text)) throw new Error(text);
306
- return textResult(text, { tool: name, params: normalized });
273
+ const details = { tool: name, params: normalized };
274
+ if (shouldTrackWrite) {
275
+ details.file_change = writeFileChangeDetails(normalized.file_path, beforeWrite, readFileChangeSnapshot(normalized.file_path));
276
+ }
277
+ return textResult(text, details);
307
278
  },
308
279
  };
309
280
  }
@@ -528,6 +499,7 @@ export function resolveMcpStdioCwd(cfg = {}, cwd = null) {
528
499
  export async function prepareMcpStdioCommand(cfg = {}, { cwd = null, sandboxPolicy = null, sandboxEngine = null, ctx = null } = {}) {
529
500
  const resolvedCtx = ctx ?? readToolRuntime();
530
501
  const sandbox = resolvedCtx.sandbox ?? passthroughSandbox;
502
+ const appOwnedLocalBinding = cfg[Symbol.for("@mono-agent/app-owned-local-binding")] === true;
531
503
  return sandbox.prepareCommand({
532
504
  policy: resolveSandboxPolicy(resolvedCtx, sandboxPolicy),
533
505
  engine: sandboxEngine ?? undefined,
@@ -536,6 +508,7 @@ export async function prepareMcpStdioCommand(cfg = {}, { cwd = null, sandboxPoli
536
508
  args: cfg.args || [],
537
509
  cwd: resolveMcpStdioCwd(cfg, cwd),
538
510
  ...(cfg.env && typeof cfg.env === "object" ? { env: cfg.env } : {}),
511
+ ...(appOwnedLocalBinding ? { allowLocalBinding: true } : {}),
539
512
  },
540
513
  });
541
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
  }
@@ -211,24 +211,3 @@ export function createFileChangePayload(raw, { cwd = process.cwd(), snapshots =
211
211
  ...(summary ? { summary } : {}),
212
212
  };
213
213
  }
214
-
215
- export function createFileEditToolUseEvent(id, payload) {
216
- return {
217
- type: "assistant",
218
- message: { content: [{ type: "tool_use", id, name: "file_edit", input: payload }] },
219
- };
220
- }
221
-
222
- export function createFileEditToolResultEvent(id, payload, { isError = false } = {}) {
223
- return {
224
- type: "user",
225
- message: {
226
- content: [{
227
- type: "tool_result",
228
- tool_use_id: id,
229
- content: payload,
230
- is_error: isError,
231
- }],
232
- },
233
- };
234
- }
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,