@mono-agent/agent-runtime 0.1.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/ARCHITECTURE.md +219 -0
- package/LICENSE +674 -0
- package/README.md +430 -0
- package/package.json +46 -0
- package/src/agent/allowlists.js +49 -0
- package/src/agent/approval.js +211 -0
- package/src/agent/compaction.js +752 -0
- package/src/agent/index.js +40 -0
- package/src/agent/prompt/skill-index.js +66 -0
- package/src/agent/tool-bloat.js +164 -0
- package/src/agent/tools/bash.js +156 -0
- package/src/agent/tools/edit.js +15 -0
- package/src/agent/tools/glob.js +71 -0
- package/src/agent/tools/grep.js +84 -0
- package/src/agent/tools/index.js +17 -0
- package/src/agent/tools/pi-bridge.js +638 -0
- package/src/agent/tools/read.js +39 -0
- package/src/agent/tools/shared/constants.js +21 -0
- package/src/agent/tools/shared/dedup.js +31 -0
- package/src/agent/tools/shared/output-truncation.js +54 -0
- package/src/agent/tools/shared/path-resolver.js +156 -0
- package/src/agent/tools/shared/ripgrep.js +130 -0
- package/src/agent/tools/shared/runtime-context.js +69 -0
- package/src/agent/tools/web-fetch.js +59 -0
- package/src/agent/tools/web-search.js +21 -0
- package/src/agent/tools/write.js +14 -0
- package/src/agent/transcript.js +227 -0
- package/src/ai/backend.js +17 -0
- package/src/ai/cost.js +164 -0
- package/src/ai/failure.js +165 -0
- package/src/ai/file-change-stats.js +234 -0
- package/src/ai/index.js +16 -0
- package/src/ai/live-input-prompt.js +15 -0
- package/src/ai/observer.js +233 -0
- package/src/ai/providers/claude-cli.js +694 -0
- package/src/ai/providers/claude-sdk.js +864 -0
- package/src/ai/providers/claude-subagents.js +67 -0
- package/src/ai/providers/codex-app.js +1045 -0
- package/src/ai/providers/opencode-app.js +356 -0
- package/src/ai/providers/opencode-discovery.js +39 -0
- package/src/ai/providers/pi-events.js +62 -0
- package/src/ai/providers/pi-messages.js +68 -0
- package/src/ai/providers/pi-models.js +111 -0
- package/src/ai/providers/pi-sdk.js +1310 -0
- package/src/ai/registry.js +5 -0
- package/src/ai/runtime/capabilities-used.js +56 -0
- package/src/ai/runtime/capabilities.js +44 -0
- package/src/ai/runtime/context-windows.js +38 -0
- package/src/ai/runtime/fast-mode.js +8 -0
- package/src/ai/runtime/model-refs.js +144 -0
- package/src/ai/runtime/registry.js +57 -0
- package/src/ai/runtime/router.js +214 -0
- package/src/ai/runtime/sessions.js +126 -0
- package/src/ai/streaming/codex-events.js +139 -0
- package/src/ai/streaming/opencode-events.js +54 -0
- package/src/ai/types.js +70 -0
- package/src/index.js +23 -0
- package/src/pi-auth.js +80 -0
- package/src/runtime-brand.js +32 -0
- package/src/runtime.js +104 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// Approval manager: gates individual tool calls behind an optional host
|
|
2
|
+
// callback that can approve, deny, or "always approve" (session-scoped
|
|
3
|
+
// allowlist). Adapted from zeroclaw's ApprovalManager pattern.
|
|
4
|
+
//
|
|
5
|
+
// Hosts opt in by passing `onToolApprovalRequest` to createRuntime. When the
|
|
6
|
+
// callback is not supplied, the gate falls back to per-tier defaults:
|
|
7
|
+
// low risk → auto-approve
|
|
8
|
+
// medium risk → auto-approve (no host means "don't pause")
|
|
9
|
+
// high risk → deny (fail closed)
|
|
10
|
+
//
|
|
11
|
+
// Risk tiers are configured per tool via `riskTiersByTool`; tools missing from
|
|
12
|
+
// the map use `defaultRiskTier` ("medium"). A `Bash` tier of "high" is the
|
|
13
|
+
// canonical example.
|
|
14
|
+
//
|
|
15
|
+
// The manager emits structured events through the supplied `onEvent`:
|
|
16
|
+
// - tool_approval_pending — before calling the host
|
|
17
|
+
// - tool_approval_granted — host approved (or session allowlist hit)
|
|
18
|
+
// - tool_approval_denied — host denied, timed out, or host threw
|
|
19
|
+
//
|
|
20
|
+
// Approval responses from the host should look like
|
|
21
|
+
// { decision: "approve" | "deny" | "always", reason?: string }
|
|
22
|
+
// "always" approves this call and adds the tool to the session allowlist.
|
|
23
|
+
// Anything else is treated as `tier === "high" ? deny : approve`.
|
|
24
|
+
|
|
25
|
+
import { randomUUID } from "node:crypto";
|
|
26
|
+
|
|
27
|
+
export const APPROVAL_DECISIONS = Object.freeze(["approve", "deny", "always"]);
|
|
28
|
+
export const RISK_TIERS = Object.freeze(["low", "medium", "high"]);
|
|
29
|
+
|
|
30
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
31
|
+
|
|
32
|
+
export function createApprovalManager({
|
|
33
|
+
onToolApprovalRequest = null,
|
|
34
|
+
defaultRiskTier = "medium",
|
|
35
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
36
|
+
onEvent = () => {},
|
|
37
|
+
riskTiersByTool = {},
|
|
38
|
+
alwaysAllowTools = [],
|
|
39
|
+
} = {}) {
|
|
40
|
+
const sessionAllowlist = new Set(normaliseList(alwaysAllowTools));
|
|
41
|
+
const normalisedTiersByTool = Object.fromEntries(
|
|
42
|
+
Object.entries(riskTiersByTool || {})
|
|
43
|
+
.filter(([, v]) => RISK_TIERS.includes(v))
|
|
44
|
+
.map(([k, v]) => [String(k), v]),
|
|
45
|
+
);
|
|
46
|
+
const normalisedDefault = RISK_TIERS.includes(defaultRiskTier) ? defaultRiskTier : "medium";
|
|
47
|
+
const timeout = Number.isFinite(Number(timeoutMs)) && Number(timeoutMs) > 0
|
|
48
|
+
? Number(timeoutMs)
|
|
49
|
+
: DEFAULT_TIMEOUT_MS;
|
|
50
|
+
|
|
51
|
+
function riskTierFor(toolName) {
|
|
52
|
+
return normalisedTiersByTool[toolName] || normalisedDefault;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function emit(event) {
|
|
56
|
+
try { onEvent(event); } catch { /* host emit errors must not escape */ }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function request(toolCall = {}) {
|
|
60
|
+
const toolName = String(toolCall.toolName || toolCall.name || "");
|
|
61
|
+
const toolUseId = toolCall.toolUseId || toolCall.id || null;
|
|
62
|
+
const tier = riskTierFor(toolName);
|
|
63
|
+
|
|
64
|
+
if (tier === "low") {
|
|
65
|
+
return { decision: "approve", reason: "low_risk", riskTier: tier };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (sessionAllowlist.has(toolName)) {
|
|
69
|
+
return { decision: "approve", reason: "session_allowed", riskTier: tier };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (typeof onToolApprovalRequest !== "function") {
|
|
73
|
+
if (tier === "high") {
|
|
74
|
+
emitDenial({ toolName, toolUseId, tier, reason: "no_host_callback_for_high_risk" });
|
|
75
|
+
return { decision: "deny", reason: "no_host_callback_for_high_risk", riskTier: tier };
|
|
76
|
+
}
|
|
77
|
+
return { decision: "approve", reason: "no_host_callback_medium_auto_approve", riskTier: tier };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const requestId = toolCall.requestId || randomUUID();
|
|
81
|
+
const argumentsSummary = redactSecrets(stringifyShort(toolCall.input || toolCall.arguments || {}));
|
|
82
|
+
const payload = {
|
|
83
|
+
requestId,
|
|
84
|
+
toolName,
|
|
85
|
+
toolUseId,
|
|
86
|
+
argumentsSummary,
|
|
87
|
+
riskTier: tier,
|
|
88
|
+
model: toolCall.model || null,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
emit({ type: "tool_approval_pending", ...payload });
|
|
92
|
+
|
|
93
|
+
let response;
|
|
94
|
+
let timedOut = false;
|
|
95
|
+
try {
|
|
96
|
+
response = await Promise.race([
|
|
97
|
+
Promise.resolve().then(() => onToolApprovalRequest(payload)),
|
|
98
|
+
new Promise((_, reject) => setTimeout(() => {
|
|
99
|
+
timedOut = true;
|
|
100
|
+
reject(new Error("approval_timeout"));
|
|
101
|
+
}, timeout).unref?.()),
|
|
102
|
+
]);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
const reason = timedOut || err?.message === "approval_timeout" ? "approval_timeout" : `host_error:${err?.message || err}`;
|
|
105
|
+
emitDenial({ requestId, toolName, toolUseId, tier, reason });
|
|
106
|
+
return { decision: "deny", reason, requestId, riskTier: tier };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const normalised = normaliseResponse(response, tier);
|
|
110
|
+
if (normalised.decision === "always") {
|
|
111
|
+
sessionAllowlist.add(toolName);
|
|
112
|
+
}
|
|
113
|
+
if (normalised.decision === "deny") {
|
|
114
|
+
emitDenial({ requestId, toolName, toolUseId, tier, reason: normalised.reason });
|
|
115
|
+
} else {
|
|
116
|
+
emit({
|
|
117
|
+
type: "tool_approval_granted",
|
|
118
|
+
requestId,
|
|
119
|
+
toolName,
|
|
120
|
+
toolUseId,
|
|
121
|
+
decision: normalised.decision,
|
|
122
|
+
reason: normalised.reason,
|
|
123
|
+
riskTier: tier,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return { ...normalised, requestId, riskTier: tier };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function emitDenial({ requestId = null, toolName, toolUseId = null, tier, reason }) {
|
|
130
|
+
emit({
|
|
131
|
+
type: "tool_approval_denied",
|
|
132
|
+
requestId,
|
|
133
|
+
toolName,
|
|
134
|
+
toolUseId,
|
|
135
|
+
decision: "deny",
|
|
136
|
+
reason,
|
|
137
|
+
riskTier: tier,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
request,
|
|
143
|
+
riskTierFor,
|
|
144
|
+
sessionAllowlist: () => new Set(sessionAllowlist),
|
|
145
|
+
isAlwaysAllowed: (toolName) => sessionAllowlist.has(String(toolName)),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function normaliseResponse(response, tier) {
|
|
150
|
+
if (!response || typeof response !== "object") {
|
|
151
|
+
const fallback = tier === "high" ? "deny" : "approve";
|
|
152
|
+
return { decision: fallback, reason: "invalid_host_response" };
|
|
153
|
+
}
|
|
154
|
+
const decision = APPROVAL_DECISIONS.includes(response.decision)
|
|
155
|
+
? response.decision
|
|
156
|
+
: (tier === "high" ? "deny" : "approve");
|
|
157
|
+
return { decision, reason: typeof response.reason === "string" ? response.reason : null };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function normaliseList(value) {
|
|
161
|
+
return Array.isArray(value)
|
|
162
|
+
? [...new Set(value.filter((v) => typeof v === "string" && v.trim()).map((v) => v.trim()))]
|
|
163
|
+
: [];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function stringifyShort(value) {
|
|
167
|
+
try {
|
|
168
|
+
const s = typeof value === "string" ? value : JSON.stringify(value);
|
|
169
|
+
return s.length > 2000 ? `${s.slice(0, 2000)}…` : s;
|
|
170
|
+
} catch {
|
|
171
|
+
return String(value || "");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Redact obvious secrets in argument summaries before we hand them to the
|
|
176
|
+
// host's approval UI. Hosts that need raw arguments should read them from
|
|
177
|
+
// their own audit log, not from the approval payload.
|
|
178
|
+
function redactSecrets(text) {
|
|
179
|
+
const s = String(text || "");
|
|
180
|
+
return s
|
|
181
|
+
.replace(/\b(?:sk|pk)[-_][A-Za-z0-9]{16,}\b/g, "[REDACTED]")
|
|
182
|
+
.replace(/\bBearer\s+[A-Za-z0-9._-]{12,}/gi, "Bearer [REDACTED]")
|
|
183
|
+
.replace(/("(?:api[_-]?key|token|secret|password|authorization)"\s*:\s*)"[^"]+"/gi, '$1"[REDACTED]"');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Wrap a list of Pi-bridge-style tools so each `execute(toolCallId, params, signal)`
|
|
187
|
+
// first checks the approval manager. Denied calls throw with a structured
|
|
188
|
+
// message so the agent receives a tool_result.is_error and can adapt.
|
|
189
|
+
export function wrapToolsWithApprovalGate(tools, approvalManager, { model = null } = {}) {
|
|
190
|
+
const list = Array.isArray(tools) ? tools : [];
|
|
191
|
+
if (!approvalManager) return list;
|
|
192
|
+
return list.map((tool) => {
|
|
193
|
+
if (!tool || typeof tool.execute !== "function") return tool;
|
|
194
|
+
const originalExecute = tool.execute.bind(tool);
|
|
195
|
+
return {
|
|
196
|
+
...tool,
|
|
197
|
+
async execute(toolCallId, params, signal) {
|
|
198
|
+
const decision = await approvalManager.request({
|
|
199
|
+
toolName: tool.name,
|
|
200
|
+
toolUseId: toolCallId,
|
|
201
|
+
input: params,
|
|
202
|
+
model,
|
|
203
|
+
});
|
|
204
|
+
if (decision.decision === "deny") {
|
|
205
|
+
throw new Error(`Tool call denied (${decision.reason || "no reason"}): ${tool.name}`);
|
|
206
|
+
}
|
|
207
|
+
return originalExecute(toolCallId, params, signal);
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
});
|
|
211
|
+
}
|