@mono-agent/agent-runtime 0.6.2 → 0.9.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 +7 -0
- package/src/agent/tools/shared/ripgrep.js +12 -8
- 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 +313 -35
- 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/turn-runner.js +8 -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/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/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/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.9.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) {
|
|
@@ -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
|
}
|
|
@@ -783,6 +785,11 @@ export async function initPiMcpTools(mcpConfig, reservedNames = new Set(), {
|
|
|
783
785
|
details: {
|
|
784
786
|
server: serverName,
|
|
785
787
|
tool: sourceTool.name,
|
|
788
|
+
// pi-agent-core treats a resolved execute() call as successful.
|
|
789
|
+
// Preserve the MCP protocol's explicit error bit in details so
|
|
790
|
+
// the harness after-tool hook can replace that default without
|
|
791
|
+
// throwing away the bounded content or structuredContent below.
|
|
792
|
+
...(out?.isError === true ? { mcp_result_is_error: true } : {}),
|
|
786
793
|
mcp_call_duration_ms: mcpCallDurationMs,
|
|
787
794
|
result_truncated: mcpContentWasTruncated(out, { textLimit, imageInlineMaxBytes }),
|
|
788
795
|
raw: compactRawMcpResult(out),
|
|
@@ -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
|
}
|
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
|
|
375
|
-
//
|
|
376
|
-
// pass an already-normalized reasoning level (low/medium/high/xhigh/none).
|
|
430
|
+
// Effort arrives pre-normalized (low/medium/high/xhigh/max/none); per-CLI
|
|
431
|
+
// ceilings are clamped below rather than by callers.
|
|
377
432
|
const normalizedEffort = typeof effort === "string" && effort.trim() ? effort : null;
|
|
378
433
|
if (sdk === "claude-code") {
|
|
379
434
|
const nativeAgents = claudeNativeAgentDefinitions(nativeSubagents);
|
|
@@ -435,8 +490,10 @@ export function buildCliCommand({
|
|
|
435
490
|
if (permissionMode === "bypassPermissions") args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
436
491
|
else if (permissionMode === "acceptEdits" || permissionMode === "auto") args.push("--full-auto");
|
|
437
492
|
else if (permissionMode === "plan") args.push("--sandbox", "read-only");
|
|
438
|
-
|
|
439
|
-
|
|
493
|
+
// codex has no "max" reasoning tier; clamp to its ceiling instead of crashing the CLI.
|
|
494
|
+
const codexEffort = normalizedEffort === "max" ? "xhigh" : normalizedEffort;
|
|
495
|
+
if (codexEffort) args.push("--config", `model_reasoning_effort=${codexEffort}`);
|
|
496
|
+
if (codexEffort !== "none") args.push("--config", `model_reasoning_summary=${tomlValue("auto")}`);
|
|
440
497
|
if (hasEntries(mcpServers)) args.push(...codexMcpConfigArgs(mcpServers));
|
|
441
498
|
args.push([systemPrompt, prompt].filter((part) => String(part || "").trim()).join("\n\n"));
|
|
442
499
|
return { command: "codex", args, cwd };
|
|
@@ -445,6 +502,53 @@ export function buildCliCommand({
|
|
|
445
502
|
export async function generateCliResponse(systemPrompt, options = {}) {
|
|
446
503
|
const start = Date.now();
|
|
447
504
|
const resolved = options.model;
|
|
505
|
+
if (resolved?.sdk === "claude-code" && claudeSandboxPolicyProblem(options)) {
|
|
506
|
+
const providerSessionId = (typeof options.sessionId === "string" && options.sessionId.trim())
|
|
507
|
+
|| (typeof options.providerSessionId === "string" && options.providerSessionId.trim())
|
|
508
|
+
|| null;
|
|
509
|
+
return claudeSandboxCapabilityMismatchResult({
|
|
510
|
+
model: resolved.reference || `claude:${resolved.model}`,
|
|
511
|
+
effort: options.effort,
|
|
512
|
+
sdk: "claude-code",
|
|
513
|
+
providerSessionId,
|
|
514
|
+
durationMs: Date.now() - start,
|
|
515
|
+
outputSchema: options.outputSchema,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
if (resolved?.sdk === "claude-code" && Array.isArray(options.allowedTools) && options.allowedTools.length === 0) {
|
|
519
|
+
const providerSessionId = (typeof options.sessionId === "string" && options.sessionId.trim())
|
|
520
|
+
|| (typeof options.providerSessionId === "string" && options.providerSessionId.trim())
|
|
521
|
+
|| null;
|
|
522
|
+
return claudeCapabilityMismatchResult({
|
|
523
|
+
model: resolved.reference || `claude:${resolved.model}`,
|
|
524
|
+
effort: options.effort,
|
|
525
|
+
sdk: "claude-code",
|
|
526
|
+
providerSessionId,
|
|
527
|
+
durationMs: Date.now() - start,
|
|
528
|
+
outputSchema: options.outputSchema,
|
|
529
|
+
error: CLAUDE_CLI_EMPTY_TOOL_POLICY_UNSUPPORTED,
|
|
530
|
+
errorCode: "claude_cli_empty_tool_policy_unsupported",
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
if (resolved?.sdk !== "claude-code") {
|
|
534
|
+
if (resolveSandboxPolicy(options.toolContext, options.sandboxPolicy) !== undefined) {
|
|
535
|
+
return codexCliCapabilityMismatchResult(
|
|
536
|
+
options,
|
|
537
|
+
CODEX_CLI_SANDBOX_POLICY_UNSUPPORTED,
|
|
538
|
+
"codex_sandbox_policy_unsupported",
|
|
539
|
+
start,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
const toolPolicyProblem = codexCliToolPolicyProblem(options);
|
|
543
|
+
if (toolPolicyProblem !== null) {
|
|
544
|
+
return codexCliCapabilityMismatchResult(
|
|
545
|
+
options,
|
|
546
|
+
toolPolicyProblem,
|
|
547
|
+
"codex_tool_policy_unsupported",
|
|
548
|
+
start,
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
448
552
|
const prompt = promptFromMessages(options.messages);
|
|
449
553
|
const dir = mkdtempSync(join(tmpdir(), (options.toolContext?.runtimeBrand ?? readRuntimeBrand()).tempdirPrefix));
|
|
450
554
|
const schemaPath = options.outputSchema ? join(dir, "output-schema.json") : null;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { buildCapabilitiesUsed } from "../runtime/capabilities-used.js";
|
|
2
|
+
import { resolveSandboxPolicy } from "../../agent/tools/shared/tool-context.js";
|
|
3
|
+
|
|
4
|
+
export const CLAUDE_SANDBOX_POLICY_UNSUPPORTED =
|
|
5
|
+
"Claude SDK/CLI cannot enforce mono-agent's native srt sandbox scopes. Remove the mono-agent sandbox policy or use a Pi runtime for exact readableRoots, writableRoots, denyWrite, and network rules.";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Claude owns its built-in tool subprocesses, so mono-agent's runtime tool
|
|
9
|
+
* context cannot wrap them with the configured srt engine. An explicit `off`
|
|
10
|
+
* policy is inert and remains valid; every enforcing mono-agent mode must fail
|
|
11
|
+
* before the provider starts instead of silently running outside that policy.
|
|
12
|
+
*/
|
|
13
|
+
export function claudeSandboxPolicyProblem(options) {
|
|
14
|
+
const effectivePolicy = resolveSandboxPolicy(
|
|
15
|
+
options?.toolContext,
|
|
16
|
+
options?.sandboxPolicy,
|
|
17
|
+
);
|
|
18
|
+
return effectivePolicy !== undefined
|
|
19
|
+
? CLAUDE_SANDBOX_POLICY_UNSUPPORTED
|
|
20
|
+
: null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Typed provider result used by Claude bridges for fail-closed capability paths. */
|
|
24
|
+
export function claudeCapabilityMismatchResult({
|
|
25
|
+
model,
|
|
26
|
+
effort,
|
|
27
|
+
sdk,
|
|
28
|
+
providerSessionId = null,
|
|
29
|
+
durationMs = 0,
|
|
30
|
+
outputSchema,
|
|
31
|
+
error,
|
|
32
|
+
errorCode,
|
|
33
|
+
}) {
|
|
34
|
+
return {
|
|
35
|
+
text: null,
|
|
36
|
+
structuredResult: undefined,
|
|
37
|
+
structuredResultSource: null,
|
|
38
|
+
events: [],
|
|
39
|
+
usage: {},
|
|
40
|
+
durationMs,
|
|
41
|
+
numTurns: 0,
|
|
42
|
+
model,
|
|
43
|
+
effort: effort || null,
|
|
44
|
+
sdk,
|
|
45
|
+
providerSessionId,
|
|
46
|
+
provider_session_id: providerSessionId,
|
|
47
|
+
cancelled: false,
|
|
48
|
+
error,
|
|
49
|
+
failureKind: "skipped_capability_mismatch",
|
|
50
|
+
diagnostics: { claude_error_code: errorCode },
|
|
51
|
+
capabilitiesUsed: buildCapabilitiesUsed({
|
|
52
|
+
promptCacheActive: null,
|
|
53
|
+
thinkingEnabled: null,
|
|
54
|
+
structuredOutputEnforced: !!outputSchema,
|
|
55
|
+
subagentInvoked: null,
|
|
56
|
+
mcpServersUsed: [],
|
|
57
|
+
nativeSubagentsUsed: [],
|
|
58
|
+
toolCompactionApplied: false,
|
|
59
|
+
contextCompactionApplied: null,
|
|
60
|
+
}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Typed provider result used by both Claude bridges for the sandbox path. */
|
|
65
|
+
export function claudeSandboxCapabilityMismatchResult(options) {
|
|
66
|
+
return claudeCapabilityMismatchResult({
|
|
67
|
+
...options,
|
|
68
|
+
error: CLAUDE_SANDBOX_POLICY_UNSUPPORTED,
|
|
69
|
+
errorCode: "claude_sandbox_policy_unsupported",
|
|
70
|
+
});
|
|
71
|
+
}
|