@mono-agent/agent-runtime 0.6.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -16
- package/package.json +14 -7
- package/src/agent/approval.js +52 -17
- package/src/agent/sandbox-seam.js +1 -0
- package/src/agent/tools/pi-bridge.js +15 -42
- package/src/agent/tools/shared/ripgrep.js +12 -8
- package/src/ai/file-change-stats.js +0 -21
- package/src/ai/index.js +8 -0
- package/src/ai/providers/claude-cli.js +109 -5
- package/src/ai/providers/claude-sandbox.js +71 -0
- package/src/ai/providers/claude-sdk-discovery-worker.js +53 -0
- package/src/ai/providers/claude-sdk-discovery.js +352 -0
- package/src/ai/providers/claude-sdk.js +315 -163
- package/src/ai/providers/codex-app.js +823 -78
- package/src/ai/providers/opencode-app.js +682 -96
- package/src/ai/providers/opencode-server.js +508 -0
- package/src/ai/providers/pi-native/stream-subscriber.js +9 -0
- package/src/ai/runtime/capabilities.js +12 -0
- package/src/ai/runtime/context-windows.js +8 -0
- package/src/ai/runtime/registry.js +8 -2
- package/src/ai/runtime/router.js +627 -29
- package/src/ai/streaming/codex-events.js +7 -15
- package/src/ai/types.js +29 -2
- package/src/index.js +6 -0
- package/src/runtime.js +17 -1
- package/types/agent/approval.d.ts +4 -7
- package/types/agent/sandbox-seam.d.ts +5 -0
- package/types/ai/backend.d.ts +16 -0
- package/types/ai/file-change-stats.d.ts +0 -24
- package/types/ai/index.d.ts +1 -0
- package/types/ai/providers/claude-cli.d.ts +116 -0
- package/types/ai/providers/claude-sandbox.d.ts +79 -0
- package/types/ai/providers/claude-sdk-discovery-worker.d.ts +1 -0
- package/types/ai/providers/claude-sdk-discovery.d.ts +97 -0
- package/types/ai/providers/claude-sdk.d.ts +81 -5
- package/types/ai/providers/codex-app.d.ts +11 -7
- package/types/ai/providers/opencode-app.d.ts +15 -16
- package/types/ai/providers/opencode-server.d.ts +20 -0
- package/types/ai/runtime/capabilities.d.ts +19 -0
- package/types/ai/runtime/context-windows.d.ts +1 -0
- package/types/ai/runtime/router.d.ts +24 -23
- package/types/ai/streaming/codex-events.d.ts +15 -6
- package/types/ai/types.d.ts +75 -2
- package/types/index.d.ts +1 -0
package/README.md
CHANGED
|
@@ -6,13 +6,15 @@ Category: `runtime`
|
|
|
6
6
|
|
|
7
7
|
## Responsibility
|
|
8
8
|
|
|
9
|
-
Provides
|
|
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
|
|
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 ≥
|
|
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
|
-
- `
|
|
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
|
|
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 | ✓ (
|
|
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
|
|
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
|
|
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-
|
|
309
|
-
{ sdk: "
|
|
310
|
-
{ model: { sdk: "pi", provider: "
|
|
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).
|
|
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.
|
|
4
|
-
"description": "Agent runtime supporting Claude SDK
|
|
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": ">=
|
|
130
|
+
"node": ">=22.19.0"
|
|
127
131
|
},
|
|
128
132
|
"dependencies": {
|
|
129
|
-
"@anthropic-ai/claude-agent-sdk": "
|
|
130
|
-
"@
|
|
131
|
-
"@earendil-works/pi-
|
|
132
|
-
"@
|
|
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": {
|
package/src/agent/approval.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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"
|
|
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
|
-
|
|
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 {
|
|
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) {
|
|
@@ -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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
266
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
39
|
+
function packagedRgPath() {
|
|
40
40
|
try {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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 =
|
|
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,
|