ocuclaw 1.3.3 → 1.3.4
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 +29 -1
- package/dist/config/runtime-config-session-title-model.test.js +0 -3
- package/dist/config/runtime-config.js +22 -33
- package/dist/domain/activity-status-adapter.js +0 -7
- package/dist/domain/activity-status-arbiter.js +3 -27
- package/dist/domain/activity-status-labels.js +8 -38
- package/dist/domain/code-span-regions.js +4 -24
- package/dist/domain/constant-time-equal.js +9 -0
- package/dist/domain/constant-time-equal.test.js +28 -0
- package/dist/domain/conversation-state.js +27 -138
- package/dist/domain/debug-bundle-cache.js +52 -0
- package/dist/domain/debug-bundle-format.js +60 -0
- package/dist/domain/debug-bundle-preview.js +123 -0
- package/dist/domain/debug-bundle-redaction.js +182 -0
- package/dist/domain/debug-bundle-save.js +11 -0
- package/dist/domain/debug-bundle-zip.js +15 -0
- package/dist/domain/debug-bundle.js +97 -0
- package/dist/domain/debug-store.js +6 -17
- package/dist/domain/debug-upload-preset.js +27 -0
- package/dist/domain/glasses-display-system-prompt.js +0 -5
- package/dist/domain/glasses-display-system-prompt.test.js +1 -1
- package/dist/domain/glasses-ui-content-summary.js +0 -6
- package/dist/domain/glasses-ui-system-prompt.test.js +1 -2
- package/dist/domain/message-emoji-allowlist.js +0 -7
- package/dist/domain/message-emoji-filter.js +3 -9
- package/dist/domain/neural-emoji-reactor-tag-config.js +3 -3
- package/dist/domain/prompt-channel-fragments.js +1 -10
- package/dist/domain/tagged-span-parser.js +3 -26
- package/dist/domain/tagged-span-strip.js +0 -7
- package/dist/even-ai/even-ai-endpoint.js +77 -24
- package/dist/even-ai/even-ai-run-waiter.js +0 -1
- package/dist/even-ai/even-ai-settings-store.js +11 -0
- package/dist/gateway/gateway-bridge.js +8 -9
- package/dist/gateway/gateway-timing-ledger.js +8 -6
- package/dist/gateway/openclaw-client.js +97 -297
- package/dist/gateway/sanitize-connect-reason.js +10 -0
- package/dist/gateway/sanitize-connect-reason.test.js +34 -0
- package/dist/index.js +3 -3
- package/dist/runtime/channel-two-hook.js +1 -6
- package/dist/runtime/container-env.js +1 -5
- package/dist/runtime/debug-bundle-handler.js +159 -0
- package/dist/runtime/display-toggle-states.js +6 -17
- package/dist/runtime/downstream-handler.js +682 -508
- package/dist/runtime/glasses-backpressure-latch.js +2 -24
- package/dist/runtime/ocuclaw-settings-store.js +10 -1
- package/dist/runtime/openclaw-host-version.js +5 -0
- package/dist/runtime/plugin-version-service.js +13 -6
- package/dist/runtime/provider-usage-select.js +0 -6
- package/dist/runtime/register-session-title-distiller.js +14 -16
- package/dist/runtime/relay-core.js +601 -290
- package/dist/runtime/relay-service.js +19 -47
- package/dist/runtime/relay-worker-approval-replay-cache.js +1 -1
- package/dist/runtime/relay-worker-entry.js +1 -2
- package/dist/runtime/relay-worker-health.js +2 -10
- package/dist/runtime/relay-worker-protocol.js +6 -1
- package/dist/runtime/relay-worker-supervisor.js +103 -41
- package/dist/runtime/relay-worker-transport.js +150 -17
- package/dist/runtime/session-context-service.js +5 -45
- package/dist/runtime/session-service.js +157 -175
- package/dist/runtime/session-title-distiller-budget.js +1 -5
- package/dist/runtime/session-title-distiller-helpers.js +14 -24
- package/dist/runtime/session-title-distiller.js +109 -122
- package/dist/runtime/session-title-record.js +0 -6
- package/dist/runtime/stable-prompt-snapshot.js +3 -14
- package/dist/runtime/upstream-runtime.js +600 -103
- package/dist/tools/device-info-tool.js +4 -21
- package/dist/tools/glasses-ui-cron.js +22 -77
- package/dist/tools/glasses-ui-descriptors.js +4 -33
- package/dist/tools/glasses-ui-limits.js +0 -13
- package/dist/tools/glasses-ui-paint-floor.js +5 -39
- package/dist/tools/glasses-ui-recipes.js +92 -101
- package/dist/tools/glasses-ui-surfaces.js +31 -163
- package/dist/tools/glasses-ui-template.js +7 -22
- package/dist/tools/glasses-ui-tool-description.test.js +2 -2
- package/dist/tools/glasses-ui-tool.js +87 -451
- package/dist/tools/glasses-ui-voicemail.js +6 -63
- package/dist/tools/glasses-ui-wake.js +9 -76
- package/dist/tools/session-title-tool.js +2 -7
- package/dist/tools/session-title-tool.test.js +1 -1
- package/dist/version.js +3 -2
- package/openclaw.plugin.json +60 -13
- package/package.json +3 -2
- package/dist/runtime/protocol-adapter.js +0 -387
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
// Recipe executors for glasses_ui_refresh. Kinds: http (in-process fetch),
|
|
2
|
-
// llm (two HTTP API backends behind one dispatcher), and system-stats
|
|
3
|
-
// (in-process node:os reads). All return either { output } or
|
|
4
|
-
// { error: <string> }. The shell (spawn bash) and llm claude-cli
|
|
5
|
-
// (spawn claude) backends were removed to drop the plugin's last
|
|
6
|
-
// child_process spawn — see the backends comment in config/runtime-config.ts.
|
|
7
|
-
//
|
|
8
|
-
// output is `string` for plain text and `object` for JSON. The cron engine
|
|
9
|
-
// hands `output` to the template engine which handles both shapes.
|
|
10
|
-
|
|
11
1
|
import { totalmem, freemem, loadavg, cpus } from "node:os";
|
|
12
2
|
import * as dns from "node:dns";
|
|
13
3
|
import { Agent } from "undici";
|
|
@@ -46,11 +36,6 @@ function checkIpv4Tuple(a, b) {
|
|
|
46
36
|
return null;
|
|
47
37
|
}
|
|
48
38
|
|
|
49
|
-
// Classify a resolved IP literal (as returned by dns.lookup). Reused both
|
|
50
|
-
// by the URL-form check below and by the per-connection lookup that fires
|
|
51
|
-
// when undici opens the socket — see safeLookup. For IPv4 we route through
|
|
52
|
-
// checkIpv4Tuple; for IPv6 we additionally peel IPv4-mapped (::ffff:a.b.c.d)
|
|
53
|
-
// and the hex-compressed IPv4-compat form that Node sometimes returns.
|
|
54
39
|
function checkResolvedIp(address, family) {
|
|
55
40
|
if (typeof address !== "string") return null;
|
|
56
41
|
if (family === 4) {
|
|
@@ -78,24 +63,12 @@ function checkResolvedIp(address, family) {
|
|
|
78
63
|
const r = checkIpv4Tuple((high >> 8) & 0xff, high & 0xff);
|
|
79
64
|
return r ? `IPv4-compatible IPv6 (${r})` : null;
|
|
80
65
|
}
|
|
81
|
-
|
|
82
|
-
// so the first byte is always 0xfe and the second byte ranges 0x80-0xbf.
|
|
83
|
-
// The canonical first hextet is therefore fe[89ab][0-9a-f] (the leading
|
|
84
|
-
// 'fe' is fixed, the third hex char is 8/9/a/b for the high two bits 10).
|
|
85
|
-
// The old startsWith("fe80:") check missed fe81::* through febf::*.
|
|
66
|
+
|
|
86
67
|
if (/^fe[89ab][0-9a-f]:/.test(addr)) return "IPv6 link-local blocked";
|
|
87
68
|
if (/^f[cd][0-9a-f]{2}:/.test(addr)) return "IPv6 ULA blocked";
|
|
88
69
|
return null;
|
|
89
70
|
}
|
|
90
71
|
|
|
91
|
-
// Per-connection DNS lookup that undici calls just before opening the socket.
|
|
92
|
-
// We resolve the hostname via the same resolver fetch would use, then check
|
|
93
|
-
// EVERY returned address: if any resolves into a private/loopback/link-local
|
|
94
|
-
// range, reject the whole hostname. Strict — split-horizon DNS that returns
|
|
95
|
-
// public IPs at one moment and private at another can't slip past, because
|
|
96
|
-
// we only ever connect via this lookup's returned address (no TOCTOU). TLS
|
|
97
|
-
// SNI and certificate verification keep using the URL hostname downstream,
|
|
98
|
-
// so https://public.example still validates against its public cert.
|
|
99
72
|
export function makeSafeLookup(dnsLookup) {
|
|
100
73
|
return function safeLookup(hostname, opts, cb) {
|
|
101
74
|
const family = opts && typeof opts.family === "number" ? opts.family : 0;
|
|
@@ -127,26 +100,6 @@ const ssrfSafeDispatcher = new Agent({
|
|
|
127
100
|
connect: { lookup: makeSafeLookup(dns.promises.lookup) },
|
|
128
101
|
});
|
|
129
102
|
|
|
130
|
-
// Block direct-IP requests to loopback, link-local (incl. 169.254.169.254
|
|
131
|
-
// cloud-metadata IPs), RFC1918 private space, IPv6 ULA/link-local. This is
|
|
132
|
-
// the cheap URL-form check that runs before any DNS — it rejects literal IPs
|
|
133
|
-
// (including IPv4-mapped/compat IPv6) without a round-trip.
|
|
134
|
-
//
|
|
135
|
-
// Hostnames pass through this URL-form check by design; the second layer is
|
|
136
|
-
// safeLookup() above, which undici invokes just before opening the socket.
|
|
137
|
-
// That layer closes the real attack surface here:
|
|
138
|
-
// 1. Static A record. evil.example with `A 127.0.0.1` (or 169.254.169.254,
|
|
139
|
-
// or an RFC1918 IP) — no DNS rebinding required, the agent just owns
|
|
140
|
-
// a domain.
|
|
141
|
-
// 2. Operator's resolver context. evenclaw deployments may resolve names
|
|
142
|
-
// like grafana.internal or vault to private services the operator
|
|
143
|
-
// didn't realize the plugin could reach.
|
|
144
|
-
// 3. Cloud-metadata uniformity. evil.example → 169.254.169.254 works on
|
|
145
|
-
// every AWS/GCP/Azure VM running OcuClaw.
|
|
146
|
-
// safeLookup rejects the hostname if ANY resolved IP is private — strict, so
|
|
147
|
-
// split-horizon DNS can't slip past either.
|
|
148
|
-
//
|
|
149
|
-
// Non-http(s) schemes (file:, gopher:, ftp:, data:) are rejected outright.
|
|
150
103
|
function isForbiddenHttpDestination(urlString) {
|
|
151
104
|
let parsed;
|
|
152
105
|
try {
|
|
@@ -158,33 +111,28 @@ function isForbiddenHttpDestination(urlString) {
|
|
|
158
111
|
if (proto !== "http:" && proto !== "https:") {
|
|
159
112
|
return `disallowed scheme: ${proto}`;
|
|
160
113
|
}
|
|
161
|
-
|
|
162
|
-
// versions; normalize by stripping them ourselves so the IPv6 checks below
|
|
163
|
-
// work regardless.
|
|
114
|
+
|
|
164
115
|
const rawHost = parsed.hostname.toLowerCase();
|
|
165
116
|
const host = rawHost.startsWith("[") && rawHost.endsWith("]")
|
|
166
117
|
? rawHost.slice(1, -1)
|
|
167
118
|
: rawHost;
|
|
168
|
-
|
|
119
|
+
|
|
169
120
|
if (host === "localhost" || host === "ip6-localhost" || host === "ip6-loopback") {
|
|
170
121
|
return "loopback hostname blocked";
|
|
171
122
|
}
|
|
172
|
-
|
|
123
|
+
|
|
173
124
|
const v4 = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
174
125
|
if (v4) {
|
|
175
126
|
return checkIpv4Tuple(Number(v4[1]), Number(v4[2]));
|
|
176
127
|
}
|
|
177
|
-
|
|
128
|
+
|
|
178
129
|
if (/^[0-9a-f.x]+$/.test(host) && /^\d/.test(host) && !host.includes(":")) {
|
|
179
130
|
return "ambiguous numeric host blocked";
|
|
180
131
|
}
|
|
181
|
-
|
|
132
|
+
|
|
182
133
|
if (host.includes(":")) {
|
|
183
134
|
if (host === "::" || host === "::1") return "IPv6 loopback/unspecified blocked";
|
|
184
|
-
|
|
185
|
-
// ::ffff:XXXX:YYYY (hex). Also IPv4-compatible (deprecated): ::a.b.c.d.
|
|
186
|
-
// All of these resolve to an IPv4 destination — extract the embedded
|
|
187
|
-
// IPv4 and run it through the IPv4 rules so the bypass closes.
|
|
135
|
+
|
|
188
136
|
const mappedDotted = host.match(/^::(?:ffff:)?(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
189
137
|
if (mappedDotted) {
|
|
190
138
|
const reason = checkIpv4Tuple(Number(mappedDotted[1]), Number(mappedDotted[2]));
|
|
@@ -200,11 +148,7 @@ function isForbiddenHttpDestination(urlString) {
|
|
|
200
148
|
if (reason) return `IPv4-mapped IPv6 blocked (${reason})`;
|
|
201
149
|
return null;
|
|
202
150
|
}
|
|
203
|
-
|
|
204
|
-
// Node's URL parser normalizes ::a.b.c.d to this compressed-hex form, so
|
|
205
|
-
// the dotted regex above never fires for compat addresses — handle it
|
|
206
|
-
// here. Excludes the IPv4-mapped case (caught above) and the IPv6
|
|
207
|
-
// loopback ::1 (caught earlier as an exact match).
|
|
151
|
+
|
|
208
152
|
const compatHex = host.match(/^::([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
|
|
209
153
|
if (compatHex) {
|
|
210
154
|
const high = parseInt(compatHex[1], 16);
|
|
@@ -214,7 +158,7 @@ function isForbiddenHttpDestination(urlString) {
|
|
|
214
158
|
if (reason) return `IPv4-compatible IPv6 blocked (${reason})`;
|
|
215
159
|
return null;
|
|
216
160
|
}
|
|
217
|
-
|
|
161
|
+
|
|
218
162
|
if (/^fe[89ab][0-9a-f]:/.test(host)) return "IPv6 link-local blocked";
|
|
219
163
|
if (/^f[cd][0-9a-f]{2}:/.test(host)) return "IPv6 ULA blocked";
|
|
220
164
|
return null;
|
|
@@ -224,7 +168,7 @@ function isForbiddenHttpDestination(urlString) {
|
|
|
224
168
|
|
|
225
169
|
function resolveJsonPath(value, jsonPath) {
|
|
226
170
|
if (!jsonPath || typeof jsonPath !== "string") return value;
|
|
227
|
-
|
|
171
|
+
|
|
228
172
|
const expr = jsonPath.trim();
|
|
229
173
|
if (!expr.startsWith("$")) return value;
|
|
230
174
|
const rest = expr.slice(1).replace(/\[(\d+)\]/g, ".$1");
|
|
@@ -244,23 +188,80 @@ function resolveJsonPath(value, jsonPath) {
|
|
|
244
188
|
return cursor;
|
|
245
189
|
}
|
|
246
190
|
|
|
191
|
+
export function normalizeHttpAllowHosts(list) {
|
|
192
|
+
if (!Array.isArray(list)) return [];
|
|
193
|
+
return list
|
|
194
|
+
.filter((p) => typeof p === "string")
|
|
195
|
+
.map((p) => p.trim().toLowerCase().replace(/\.+$/, ""))
|
|
196
|
+
.filter((p) => p.length > 0);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function isHttpHostAllowed(hostname, allowList) {
|
|
200
|
+
if (!Array.isArray(allowList) || allowList.length === 0) return false;
|
|
201
|
+
if (typeof hostname !== "string" || !hostname) return false;
|
|
202
|
+
const host = hostname.trim().toLowerCase().replace(/\.+$/, "");
|
|
203
|
+
if (!host) return false;
|
|
204
|
+
return allowList.some((p) =>
|
|
205
|
+
p.startsWith(".") ? host === p.slice(1) || host.endsWith(p) : host === p,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const CROSS_ORIGIN_STRIP_HEADERS = new Set([
|
|
210
|
+
"authorization",
|
|
211
|
+
"cookie",
|
|
212
|
+
"proxy-authorization",
|
|
213
|
+
"x-api-key",
|
|
214
|
+
"api-key",
|
|
215
|
+
"x-auth-token",
|
|
216
|
+
"x-access-token",
|
|
217
|
+
"x-amz-security-token",
|
|
218
|
+
]);
|
|
219
|
+
const CREDENTIALISH_HEADER_RE = /(^|-)(api|auth|access|secret|session)(-|key|token|$)/i;
|
|
220
|
+
|
|
221
|
+
function stripCrossOriginHeaders(headers) {
|
|
222
|
+
const out = {};
|
|
223
|
+
for (const key of Object.keys(headers || {})) {
|
|
224
|
+
const lower = key.toLowerCase();
|
|
225
|
+
if (CROSS_ORIGIN_STRIP_HEADERS.has(lower)) continue;
|
|
226
|
+
if (CREDENTIALISH_HEADER_RE.test(lower)) continue;
|
|
227
|
+
out[key] = headers[key];
|
|
228
|
+
}
|
|
229
|
+
return out;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function sameOrigin(a, b) {
|
|
233
|
+
try {
|
|
234
|
+
return new URL(a).origin === new URL(b).origin;
|
|
235
|
+
} catch (_) {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
247
240
|
export async function executeHttpRecipe(params, opts) {
|
|
248
241
|
const url = params && typeof params.url === "string" ? params.url : "";
|
|
249
242
|
if (!url) return { error: "http recipe missing url" };
|
|
250
|
-
|
|
251
|
-
// engine invokes executeHttpRecipe(recipe) with no second argument). Tests
|
|
252
|
-
// that spin up their own loopback HTTP server opt out via
|
|
253
|
-
// executeHttpRecipe(recipe, { allowPrivateNetworks: true }). The recipe
|
|
254
|
-
// schema deliberately does NOT include this flag, so agents can't bypass.
|
|
243
|
+
|
|
255
244
|
if (!(opts && opts.allowPrivateNetworks === true)) {
|
|
256
245
|
const forbidden = isForbiddenHttpDestination(url);
|
|
257
246
|
if (forbidden) {
|
|
258
247
|
return { error: `http recipe destination blocked: ${forbidden}` };
|
|
259
248
|
}
|
|
260
249
|
}
|
|
250
|
+
|
|
251
|
+
const allowHosts = opts && Array.isArray(opts.allowHosts) ? opts.allowHosts : null;
|
|
252
|
+
if (allowHosts) {
|
|
253
|
+
let initialHost = "";
|
|
254
|
+
try { initialHost = new URL(url).hostname; } catch (_) {}
|
|
255
|
+
if (!isHttpHostAllowed(initialHost, allowHosts)) {
|
|
256
|
+
return { error: `http recipe destination not in allowlist: ${initialHost || url}` };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
261
259
|
const method = params && typeof params.method === "string" ? params.method.toUpperCase() : "GET";
|
|
262
260
|
const headers = params && params.headers && typeof params.headers === "object" ? params.headers : {};
|
|
263
261
|
const body = method !== "GET" && method !== "HEAD" ? params && params.body : undefined;
|
|
262
|
+
|
|
263
|
+
let requestHeaders = headers;
|
|
264
|
+
let requestBody = body;
|
|
264
265
|
const timeoutMs = Number.isFinite(params && params.timeoutMs)
|
|
265
266
|
? params.timeoutMs
|
|
266
267
|
: DEFAULT_TIMEOUT_MS;
|
|
@@ -270,12 +271,7 @@ export async function executeHttpRecipe(params, opts) {
|
|
|
270
271
|
const jsonPath = params && typeof params.jsonPath === "string" ? params.jsonPath : "";
|
|
271
272
|
|
|
272
273
|
const fetchFn = opts && typeof opts.fetch === "function" ? opts.fetch : fetch;
|
|
273
|
-
|
|
274
|
-
// resolves hostnames and blocks any that point at private IPs — see the
|
|
275
|
-
// comment block on isForbiddenHttpDestination. Production callers default
|
|
276
|
-
// to the module-level ssrfSafeDispatcher; the test seam accepts an
|
|
277
|
-
// injected dispatcher (e.g. one built with a canned lookup) so the lookup
|
|
278
|
-
// check can be exercised without real DNS.
|
|
274
|
+
|
|
279
275
|
const dispatcher =
|
|
280
276
|
opts && opts.dispatcher !== undefined ? opts.dispatcher : ssrfSafeDispatcher;
|
|
281
277
|
const controller = new AbortController();
|
|
@@ -283,24 +279,19 @@ export async function executeHttpRecipe(params, opts) {
|
|
|
283
279
|
const allowPrivate = opts && opts.allowPrivateNetworks === true;
|
|
284
280
|
const MAX_REDIRECTS = 3;
|
|
285
281
|
try {
|
|
286
|
-
|
|
287
|
-
// public attacker-controlled domain return a 302 → http://169.254.169.254/
|
|
288
|
-
// after the one-shot URL check. Validate every hop. Capped at 3 to
|
|
289
|
-
// bound effort and prevent loops.
|
|
282
|
+
|
|
290
283
|
let currentUrl = url;
|
|
291
284
|
let response = null;
|
|
292
285
|
let hop = 0;
|
|
293
286
|
while (true) {
|
|
294
287
|
const fetchInit = {
|
|
295
288
|
method,
|
|
296
|
-
headers,
|
|
297
|
-
body,
|
|
289
|
+
headers: requestHeaders,
|
|
290
|
+
body: requestBody,
|
|
298
291
|
signal: controller.signal,
|
|
299
292
|
redirect: "manual",
|
|
300
293
|
};
|
|
301
|
-
|
|
302
|
-
// tests that pass `dispatcher: null` explicitly opt out (e.g. to use
|
|
303
|
-
// a mock fetch with no networking at all).
|
|
294
|
+
|
|
304
295
|
if (dispatcher) fetchInit.dispatcher = dispatcher;
|
|
305
296
|
response = await fetchFn(currentUrl, fetchInit);
|
|
306
297
|
if (response.status >= 300 && response.status < 400) {
|
|
@@ -323,7 +314,20 @@ export async function executeHttpRecipe(params, opts) {
|
|
|
323
314
|
return { error: `http recipe redirect destination blocked: ${forbidden}` };
|
|
324
315
|
}
|
|
325
316
|
}
|
|
326
|
-
|
|
317
|
+
|
|
318
|
+
if (allowHosts) {
|
|
319
|
+
let nextHost = "";
|
|
320
|
+
try { nextHost = new URL(nextUrl).hostname; } catch (_) {}
|
|
321
|
+
if (!isHttpHostAllowed(nextHost, allowHosts)) {
|
|
322
|
+
return { error: `http recipe redirect destination not in allowlist: ${nextHost || nextUrl}` };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (!sameOrigin(currentUrl, nextUrl)) {
|
|
327
|
+
requestHeaders = stripCrossOriginHeaders(requestHeaders);
|
|
328
|
+
requestBody = undefined;
|
|
329
|
+
}
|
|
330
|
+
|
|
327
331
|
try { await response.arrayBuffer(); } catch (_) {}
|
|
328
332
|
currentUrl = nextUrl;
|
|
329
333
|
hop += 1;
|
|
@@ -375,10 +379,7 @@ export async function executeHttpRecipe(params, opts) {
|
|
|
375
379
|
if (err && err.name === "AbortError") {
|
|
376
380
|
return { error: `http recipe timeout after ${timeoutMs}ms` };
|
|
377
381
|
}
|
|
378
|
-
|
|
379
|
-
// as `TypeError: fetch failed` with the real reason on err.cause. Surface
|
|
380
|
-
// the cause so SSRF-guard rejections are visible to the operator and
|
|
381
|
-
// testable.
|
|
382
|
+
|
|
382
383
|
const msg = err && err.message ? err.message : String(err);
|
|
383
384
|
const causeMsg = err && err.cause && err.cause.message ? err.cause.message : "";
|
|
384
385
|
const full = causeMsg ? `${msg}: ${causeMsg}` : msg;
|
|
@@ -397,12 +398,6 @@ function stripModelProviderPrefix(modelRef) {
|
|
|
397
398
|
return idx === -1 ? modelRef : modelRef.slice(idx + 1);
|
|
398
399
|
}
|
|
399
400
|
|
|
400
|
-
// Both CLI-spawn backends (codex-cli, claude-cli) were removed to eliminate
|
|
401
|
-
// the plugin's last child_process spawn — see the backends comment in
|
|
402
|
-
// config/runtime-config.ts for the rationale and the deferred native-delegation
|
|
403
|
-
// track. Operators who want those providers point an *-api backend at the
|
|
404
|
-
// provider endpoint (key resolved via the host modelAuth, tool-less).
|
|
405
|
-
|
|
406
401
|
async function runAnthropicApi(params, deps) {
|
|
407
402
|
const fetchFn = deps && deps.fetch ? deps.fetch : fetch;
|
|
408
403
|
if (!params.apiKey) return { error: "anthropic-api: missing api key" };
|
|
@@ -516,10 +511,6 @@ export function executeLlmRecipe(recipe, ctx) {
|
|
|
516
511
|
return executeLlmRecipeWithDeps(recipe, ctx, {});
|
|
517
512
|
}
|
|
518
513
|
|
|
519
|
-
// L0' tier: host RAM/CPU via in-process node:os reads. No shell (a sandboxed
|
|
520
|
-
// `free -m` would report the container's view, not the host's). CPU% is a delta
|
|
521
|
-
// over a short window — os.loadavg() is run-queue length, not utilization. Deps
|
|
522
|
-
// (totalmem/freemem/loadavg/cpus/sleep) are injectable for deterministic tests.
|
|
523
514
|
export function computeCpuPct(t0, t1) {
|
|
524
515
|
let idleDelta = 0;
|
|
525
516
|
let totalDelta = 0;
|