@nanhara/hara 0.131.1 → 0.132.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/CHANGELOG.md +23 -0
- package/README.md +15 -0
- package/dist/config.js +3 -2
- package/dist/gateway/feishu.js +9 -1
- package/dist/gateway/runtime-state.js +247 -0
- package/dist/gateway/serve.js +176 -4
- package/dist/gateway/weixin.js +26 -4
- package/dist/index.js +185 -43
- package/dist/org-fleet/enroll.js +173 -24
- package/dist/security/secrets.js +2 -2
- package/dist/serve/protocol.js +7 -0
- package/dist/serve/server.js +56 -1
- package/dist/tools/web.js +234 -15
- package/package.json +6 -2
package/dist/serve/server.js
CHANGED
|
@@ -787,7 +787,9 @@ export async function startServe(opts, deps) {
|
|
|
787
787
|
"session.list", "session.create", "session.resume", "session.send", "session.steer", "session.interrupt", "session.set-model",
|
|
788
788
|
"session.rename", "session.archive", "session.compact", "session.rewind", "session.context", "session.delete", "session.fork",
|
|
789
789
|
"approval.reply", "plugins.list", "plugins.set", "skills.list", "models.list", "files.search", "project.panels",
|
|
790
|
-
"settings.providers.list", "settings.providers.test", "settings.providers.save",
|
|
790
|
+
"settings.providers.list", "settings.providers.test", "settings.providers.save", "settings.gateways.list",
|
|
791
|
+
"settings.organizations.list", "settings.organizations.enroll", "settings.organizations.use",
|
|
792
|
+
"settings.organizations.remove", "settings.organizations.check",
|
|
791
793
|
"automation.list", "automation.add", "automation.toggle", "automation.delete",
|
|
792
794
|
"artifact.import", "artifact.commit", "artifact.revert",
|
|
793
795
|
"artifact.list", "artifact.get", "artifact.revisions",
|
|
@@ -1031,6 +1033,59 @@ export async function startServe(opts, deps) {
|
|
|
1031
1033
|
const targetCwd = typeof p.cwd === "string" && p.cwd ? p.cwd : opts.cwd;
|
|
1032
1034
|
return reply(rpcResult(id, redactSensitiveValue(deps.providerSettings(targetCwd)).value));
|
|
1033
1035
|
}
|
|
1036
|
+
case "settings.gateways.list": {
|
|
1037
|
+
if (!deps.gatewayStatuses)
|
|
1038
|
+
return reply(rpcError(id, ERR.METHOD, "gateway status not supported by this server"));
|
|
1039
|
+
const gateways = await deps.gatewayStatuses();
|
|
1040
|
+
return reply(rpcResult(id, { gateways: redactSensitiveValue(gateways).value }));
|
|
1041
|
+
}
|
|
1042
|
+
case "settings.organizations.list": {
|
|
1043
|
+
if (!deps.organizationConnections)
|
|
1044
|
+
return reply(rpcError(id, ERR.METHOD, "organization settings not supported by this server"));
|
|
1045
|
+
const targetCwd = typeof p.cwd === "string" && p.cwd ? p.cwd : opts.cwd;
|
|
1046
|
+
return reply(rpcResult(id, redactSensitiveValue(deps.organizationConnections(targetCwd)).value));
|
|
1047
|
+
}
|
|
1048
|
+
case "settings.organizations.enroll": {
|
|
1049
|
+
if (!deps.enrollOrganizationConnection)
|
|
1050
|
+
return reply(rpcError(id, ERR.METHOD, "organization enrollment not supported by this server"));
|
|
1051
|
+
if (typeof p.id !== "string" ||
|
|
1052
|
+
typeof p.gatewayUrl !== "string" ||
|
|
1053
|
+
typeof p.code !== "string" ||
|
|
1054
|
+
(p.label !== undefined && typeof p.label !== "string") ||
|
|
1055
|
+
(p.activate !== undefined && typeof p.activate !== "boolean")) {
|
|
1056
|
+
return reply(rpcError(id, ERR.PARAMS, "id + gatewayUrl + code required; optional label/activate have invalid types"));
|
|
1057
|
+
}
|
|
1058
|
+
const targetCwd = typeof p.cwd === "string" && p.cwd ? p.cwd : opts.cwd;
|
|
1059
|
+
const input = {
|
|
1060
|
+
id: p.id,
|
|
1061
|
+
gatewayUrl: p.gatewayUrl,
|
|
1062
|
+
code: p.code,
|
|
1063
|
+
...(p.label !== undefined ? { label: p.label } : {}),
|
|
1064
|
+
...(p.activate !== undefined ? { activate: p.activate } : {}),
|
|
1065
|
+
};
|
|
1066
|
+
const result = await deps.enrollOrganizationConnection(input, targetCwd);
|
|
1067
|
+
return reply(rpcResult(id, redactSensitiveValue(result, [p.code]).value));
|
|
1068
|
+
}
|
|
1069
|
+
case "settings.organizations.use":
|
|
1070
|
+
case "settings.organizations.remove":
|
|
1071
|
+
case "settings.organizations.check": {
|
|
1072
|
+
if (typeof p.id !== "string")
|
|
1073
|
+
return reply(rpcError(id, ERR.PARAMS, "organization connection id required"));
|
|
1074
|
+
const targetCwd = typeof p.cwd === "string" && p.cwd ? p.cwd : opts.cwd;
|
|
1075
|
+
if (req.method === "settings.organizations.use") {
|
|
1076
|
+
if (!deps.useOrganizationConnection)
|
|
1077
|
+
return reply(rpcError(id, ERR.METHOD, "organization switching not supported by this server"));
|
|
1078
|
+
return reply(rpcResult(id, redactSensitiveValue(deps.useOrganizationConnection(p.id, targetCwd)).value));
|
|
1079
|
+
}
|
|
1080
|
+
if (req.method === "settings.organizations.remove") {
|
|
1081
|
+
if (!deps.removeOrganizationConnection)
|
|
1082
|
+
return reply(rpcError(id, ERR.METHOD, "organization removal not supported by this server"));
|
|
1083
|
+
return reply(rpcResult(id, redactSensitiveValue(deps.removeOrganizationConnection(p.id, targetCwd)).value));
|
|
1084
|
+
}
|
|
1085
|
+
if (!deps.checkOrganizationConnection)
|
|
1086
|
+
return reply(rpcError(id, ERR.METHOD, "organization connection check not supported by this server"));
|
|
1087
|
+
return reply(rpcResult(id, redactSensitiveValue(await deps.checkOrganizationConnection(p.id, targetCwd)).value));
|
|
1088
|
+
}
|
|
1034
1089
|
case "settings.providers.test":
|
|
1035
1090
|
case "settings.providers.save": {
|
|
1036
1091
|
const callback = req.method === "settings.providers.test" ? deps.testProviderSettings : deps.saveProviderSettings;
|
package/dist/tools/web.js
CHANGED
|
@@ -7,7 +7,9 @@ import { lookup } from "node:dns/promises";
|
|
|
7
7
|
import { isIP } from "node:net";
|
|
8
8
|
import { request as httpRequest } from "node:http";
|
|
9
9
|
import { request as httpsRequest } from "node:https";
|
|
10
|
+
import { ProxyAgent, fetch as undiciFetch, request as undiciRequest } from "undici";
|
|
10
11
|
import { wrapUntrusted } from "../security/external-content.js";
|
|
12
|
+
import { loadConfig } from "../config.js";
|
|
11
13
|
const MAX = 60_000;
|
|
12
14
|
const SEARCH_ATTEMPT_MS = 8_000;
|
|
13
15
|
const SEARCH_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/124.0 Safari/537.36";
|
|
@@ -116,9 +118,179 @@ export async function resolvePublicHost(hostname, resolver = lookup) {
|
|
|
116
118
|
const chosen = addrs[0];
|
|
117
119
|
return { address: chosen.address, family: chosen.family };
|
|
118
120
|
}
|
|
121
|
+
function nonBlank(value) {
|
|
122
|
+
return value?.trim() || undefined;
|
|
123
|
+
}
|
|
124
|
+
function defaultPort(url) {
|
|
125
|
+
return url.port || (url.protocol === "https:" ? "443" : "80");
|
|
126
|
+
}
|
|
127
|
+
/** Common NO_PROXY matching: exact host, domain suffix (`.example.com` / `*.example.com`), optional port,
|
|
128
|
+
* and `*`. This decision always uses the original public hostname, never the later pinned IP. */
|
|
129
|
+
export function bypassesWebProxy(url, noProxyValue) {
|
|
130
|
+
const hostname = url.hostname.replace(/^\[|\]$/g, "").replace(/\.$/, "").toLowerCase();
|
|
131
|
+
const port = defaultPort(url);
|
|
132
|
+
for (const raw of noProxyValue?.split(/[\s,]+/u) ?? []) {
|
|
133
|
+
let rule = raw.trim().toLowerCase();
|
|
134
|
+
if (!rule)
|
|
135
|
+
continue;
|
|
136
|
+
if (rule === "*")
|
|
137
|
+
return true;
|
|
138
|
+
try {
|
|
139
|
+
if (/^https?:\/\//u.test(rule)) {
|
|
140
|
+
const parsed = new URL(rule);
|
|
141
|
+
if (parsed.port && parsed.port !== port)
|
|
142
|
+
continue;
|
|
143
|
+
rule = parsed.hostname.toLowerCase();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
let rulePort;
|
|
150
|
+
if (rule.startsWith("[")) {
|
|
151
|
+
const match = /^\[([^\]]+)\](?::(\d+))?$/u.exec(rule);
|
|
152
|
+
if (!match)
|
|
153
|
+
continue;
|
|
154
|
+
rule = match[1];
|
|
155
|
+
rulePort = match[2];
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
const match = /^([^:]+):(\d+)$/u.exec(rule);
|
|
159
|
+
if (match) {
|
|
160
|
+
rule = match[1];
|
|
161
|
+
rulePort = match[2];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (rulePort && rulePort !== port)
|
|
165
|
+
continue;
|
|
166
|
+
rule = rule.replace(/\.$/u, "");
|
|
167
|
+
const suffix = rule.startsWith("*.") ? rule.slice(2) : rule.startsWith(".") ? rule.slice(1) : "";
|
|
168
|
+
if (suffix ? hostname === suffix || hostname.endsWith(`.${suffix}`) : hostname === rule)
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
function normalizedProxyUri(value) {
|
|
174
|
+
try {
|
|
175
|
+
const parsed = new URL(value);
|
|
176
|
+
if ((parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
177
|
+
|| !parsed.hostname
|
|
178
|
+
|| parsed.pathname !== "/"
|
|
179
|
+
|| parsed.search
|
|
180
|
+
|| parsed.hash) {
|
|
181
|
+
throw new Error("unsupported proxy URL");
|
|
182
|
+
}
|
|
183
|
+
return parsed.href;
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
throw new Error("web proxy configuration is invalid; use an HTTP(S) proxy URL without a path, query, or fragment");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/** Resolve one proxy without mutating Undici's global dispatcher. Standard lowercase variables take
|
|
190
|
+
* precedence over uppercase, and HTTPS falls back to HTTP_PROXY as documented by EnvHttpProxyAgent. */
|
|
191
|
+
export function selectWebProxy(url, configuredProxy, env = process.env) {
|
|
192
|
+
const noProxy = nonBlank(env.no_proxy) ?? nonBlank(env.NO_PROXY);
|
|
193
|
+
if (bypassesWebProxy(url, noProxy))
|
|
194
|
+
return undefined;
|
|
195
|
+
const haraProxy = nonBlank(env.HARA_WEB_PROXY);
|
|
196
|
+
if (haraProxy)
|
|
197
|
+
return { uri: normalizedProxyUri(haraProxy), source: "hara-env" };
|
|
198
|
+
const protocolProxy = url.protocol === "https:"
|
|
199
|
+
? nonBlank(env.https_proxy) ?? nonBlank(env.HTTPS_PROXY) ?? nonBlank(env.http_proxy) ?? nonBlank(env.HTTP_PROXY)
|
|
200
|
+
: nonBlank(env.http_proxy) ?? nonBlank(env.HTTP_PROXY);
|
|
201
|
+
if (protocolProxy)
|
|
202
|
+
return { uri: normalizedProxyUri(protocolProxy), source: "environment" };
|
|
203
|
+
return configuredProxy ? { uri: normalizedProxyUri(configuredProxy), source: "config" } : undefined;
|
|
204
|
+
}
|
|
205
|
+
function webProxy(url, cwd) {
|
|
206
|
+
return selectWebProxy(url, loadConfig({ cwd }).proxy);
|
|
207
|
+
}
|
|
208
|
+
function proxySafeError(error) {
|
|
209
|
+
const candidate = error;
|
|
210
|
+
const code = typeof candidate?.code === "string"
|
|
211
|
+
? candidate.code
|
|
212
|
+
: typeof candidate?.cause?.code === "string"
|
|
213
|
+
? candidate.cause.code
|
|
214
|
+
: undefined;
|
|
215
|
+
const suffix = code && /^[A-Z][A-Z0-9_]{1,40}$/u.test(code) ? ` (${code})` : "";
|
|
216
|
+
const result = new Error(`proxy request failed${suffix}`);
|
|
217
|
+
result.name = typeof candidate?.name === "string" && /^(?:Abort|Timeout)Error$/u.test(candidate.name)
|
|
218
|
+
? candidate.name
|
|
219
|
+
: "Error";
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
function safeNetworkError(error) {
|
|
223
|
+
const candidate = error;
|
|
224
|
+
if (candidate?.name === "AbortError" || candidate?.name === "TimeoutError")
|
|
225
|
+
return "timed out (30s)";
|
|
226
|
+
const rawCode = typeof candidate?.code === "string" ? candidate.code : candidate?.cause?.code;
|
|
227
|
+
const code = typeof rawCode === "string" && /^[A-Z][A-Z0-9_]{1,40}$/u.test(rawCode) ? ` [${rawCode}]` : "";
|
|
228
|
+
const message = String(candidate?.message ?? "network request failed")
|
|
229
|
+
.replace(/([a-z][a-z0-9+.-]*:\/\/)[^\s/@]+@/giu, "$1[credentials]@")
|
|
230
|
+
.replace(/[\u0000-\u001f\u007f]/gu, " ")
|
|
231
|
+
.replace(/\s+/gu, " ")
|
|
232
|
+
.slice(0, 240);
|
|
233
|
+
return `${message}${code}`;
|
|
234
|
+
}
|
|
235
|
+
function pinnedUrl(url, pinned) {
|
|
236
|
+
const result = new URL(url.href);
|
|
237
|
+
result.hostname = pinned.family === 6 ? `[${pinned.address}]` : pinned.address;
|
|
238
|
+
result.username = "";
|
|
239
|
+
result.password = "";
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
function responseHeaders(values) {
|
|
243
|
+
const headers = new Headers();
|
|
244
|
+
for (const [name, value] of Object.entries(values)) {
|
|
245
|
+
if (Array.isArray(value))
|
|
246
|
+
for (const item of value)
|
|
247
|
+
headers.append(name, item);
|
|
248
|
+
else if (value !== undefined)
|
|
249
|
+
headers.append(name, String(value));
|
|
250
|
+
}
|
|
251
|
+
return headers;
|
|
252
|
+
}
|
|
119
253
|
/** One HTTP hop whose TCP socket is pinned to the address approved above. `Host` and TLS SNI retain the
|
|
120
254
|
* original hostname, so virtual hosting/certificate checks work without a second DNS lookup. */
|
|
121
|
-
async function requestPinned(url, pinned, signal) {
|
|
255
|
+
export async function requestPinned(url, pinned, signal, proxy) {
|
|
256
|
+
if (proxy) {
|
|
257
|
+
const servername = url.hostname.replace(/^\[|\]$/g, "");
|
|
258
|
+
const agent = new ProxyAgent({
|
|
259
|
+
uri: proxy.uri,
|
|
260
|
+
proxyTunnel: true,
|
|
261
|
+
...(url.protocol === "https:" && !isIP(servername) ? { requestTls: { servername } } : {}),
|
|
262
|
+
});
|
|
263
|
+
try {
|
|
264
|
+
const response = await undiciRequest(pinnedUrl(url, pinned), {
|
|
265
|
+
dispatcher: agent,
|
|
266
|
+
method: "GET",
|
|
267
|
+
signal,
|
|
268
|
+
headers: {
|
|
269
|
+
host: url.host,
|
|
270
|
+
"user-agent": "hara-cli",
|
|
271
|
+
accept: "text/html,text/plain,application/json,*/*",
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
let released = false;
|
|
275
|
+
return {
|
|
276
|
+
status: response.statusCode,
|
|
277
|
+
headers: responseHeaders(response.headers),
|
|
278
|
+
body: response.body,
|
|
279
|
+
async release(discard = false) {
|
|
280
|
+
if (released)
|
|
281
|
+
return;
|
|
282
|
+
released = true;
|
|
283
|
+
if (discard)
|
|
284
|
+
response.body.destroy();
|
|
285
|
+
await agent.close();
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
await agent.destroy().catch(() => { });
|
|
291
|
+
throw proxySafeError(error);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
122
294
|
return new Promise((resolve, reject) => {
|
|
123
295
|
const request = (url.protocol === "https:" ? httpsRequest : httpRequest)({
|
|
124
296
|
protocol: url.protocol,
|
|
@@ -138,7 +310,15 @@ async function requestPinned(url, pinned, signal) {
|
|
|
138
310
|
const headers = new Headers();
|
|
139
311
|
for (let i = 0; i < body.rawHeaders.length; i += 2)
|
|
140
312
|
headers.append(body.rawHeaders[i], body.rawHeaders[i + 1]);
|
|
141
|
-
resolve({
|
|
313
|
+
resolve({
|
|
314
|
+
status: body.statusCode ?? 0,
|
|
315
|
+
headers,
|
|
316
|
+
body,
|
|
317
|
+
async release(discard = false) {
|
|
318
|
+
if (discard)
|
|
319
|
+
body.destroy();
|
|
320
|
+
},
|
|
321
|
+
});
|
|
142
322
|
});
|
|
143
323
|
request.once("error", reject);
|
|
144
324
|
request.end();
|
|
@@ -354,12 +534,34 @@ function interrupted(label) {
|
|
|
354
534
|
error.name = "AbortError";
|
|
355
535
|
return error;
|
|
356
536
|
}
|
|
357
|
-
|
|
537
|
+
const searchProxyAgents = new Map();
|
|
538
|
+
function searchProxyAgent(uri) {
|
|
539
|
+
let agent = searchProxyAgents.get(uri);
|
|
540
|
+
if (!agent) {
|
|
541
|
+
agent = new ProxyAgent({ uri, proxyTunnel: true });
|
|
542
|
+
searchProxyAgents.set(uri, agent);
|
|
543
|
+
}
|
|
544
|
+
return agent;
|
|
545
|
+
}
|
|
546
|
+
async function searchFetch(url, init, parentSignal, cwd) {
|
|
358
547
|
if (parentSignal?.aborted)
|
|
359
548
|
throw interrupted("web search");
|
|
360
549
|
const attemptSignal = AbortSignal.timeout(SEARCH_ATTEMPT_MS);
|
|
361
550
|
const signal = parentSignal ? AbortSignal.any([parentSignal, attemptSignal]) : attemptSignal;
|
|
362
|
-
|
|
551
|
+
const proxy = webProxy(new URL(url), cwd);
|
|
552
|
+
if (!proxy)
|
|
553
|
+
return fetch(url, { ...init, signal });
|
|
554
|
+
try {
|
|
555
|
+
const response = await undiciFetch(url, {
|
|
556
|
+
...init,
|
|
557
|
+
dispatcher: searchProxyAgent(proxy.uri),
|
|
558
|
+
signal,
|
|
559
|
+
});
|
|
560
|
+
return response;
|
|
561
|
+
}
|
|
562
|
+
catch (error) {
|
|
563
|
+
throw proxySafeError(error);
|
|
564
|
+
}
|
|
363
565
|
}
|
|
364
566
|
async function firstSuccessfulSearch(attempts, failures, signal) {
|
|
365
567
|
// Try one provider at a time. Search terms can be sensitive; a successful request must not be mirrored
|
|
@@ -422,7 +624,7 @@ registerTool({
|
|
|
422
624
|
method: "POST",
|
|
423
625
|
headers: { "content-type": "application/json" },
|
|
424
626
|
body: JSON.stringify({ api_key: key, query: q, max_results: limit }),
|
|
425
|
-
}, ctx.signal);
|
|
627
|
+
}, ctx.signal, ctx.cwd);
|
|
426
628
|
if (!res.ok)
|
|
427
629
|
throw new Error(`HTTP ${res.status}`);
|
|
428
630
|
const j = (await res.json());
|
|
@@ -437,7 +639,7 @@ registerTool({
|
|
|
437
639
|
method: "GET",
|
|
438
640
|
redirect: "follow",
|
|
439
641
|
headers: { "user-agent": SEARCH_UA, accept: "text/html" },
|
|
440
|
-
}, ctx.signal);
|
|
642
|
+
}, ctx.signal, ctx.cwd);
|
|
441
643
|
if (!res.ok)
|
|
442
644
|
throw new Error(`HTTP ${res.status}`);
|
|
443
645
|
return parseBingSearchResults(await res.text(), limit);
|
|
@@ -456,7 +658,7 @@ registerTool({
|
|
|
456
658
|
method: "GET",
|
|
457
659
|
redirect: "follow",
|
|
458
660
|
headers: { "user-agent": SEARCH_UA, accept: "text/html" },
|
|
459
|
-
}, ctx.signal);
|
|
661
|
+
}, ctx.signal, ctx.cwd);
|
|
460
662
|
if (!res.ok)
|
|
461
663
|
throw new Error(`HTTP ${res.status}`);
|
|
462
664
|
return parseBaiduSearchResults(await res.text(), limit);
|
|
@@ -469,7 +671,7 @@ registerTool({
|
|
|
469
671
|
method: "GET",
|
|
470
672
|
redirect: "follow",
|
|
471
673
|
headers: { "user-agent": SEARCH_UA, accept: "text/html" },
|
|
472
|
-
}, ctx.signal);
|
|
674
|
+
}, ctx.signal, ctx.cwd);
|
|
473
675
|
if (!res.ok)
|
|
474
676
|
throw new Error(`HTTP ${res.status}`);
|
|
475
677
|
return parseGoogleSearchResults(await res.text(), limit);
|
|
@@ -483,7 +685,7 @@ registerTool({
|
|
|
483
685
|
redirect: "follow",
|
|
484
686
|
headers: { "user-agent": SEARCH_UA, "content-type": "application/x-www-form-urlencoded", accept: "text/html" },
|
|
485
687
|
body: `q=${encodeURIComponent(q)}`,
|
|
486
|
-
}, ctx.signal);
|
|
688
|
+
}, ctx.signal, ctx.cwd);
|
|
487
689
|
if (!res.ok)
|
|
488
690
|
throw new Error(`HTTP ${res.status}`);
|
|
489
691
|
return parseSearchResults(await res.text(), limit);
|
|
@@ -518,10 +720,12 @@ registerTool({
|
|
|
518
720
|
url = new URL(input.url);
|
|
519
721
|
}
|
|
520
722
|
catch {
|
|
521
|
-
return
|
|
723
|
+
return "Error: invalid URL.";
|
|
522
724
|
}
|
|
523
725
|
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
524
726
|
return "Error: only http/https URLs are supported.";
|
|
727
|
+
if (url.username || url.password)
|
|
728
|
+
return "Error: URL-embedded credentials are not supported.";
|
|
525
729
|
const cap = Math.min(Math.max(1000, input.max_chars ?? MAX), 200_000);
|
|
526
730
|
const ctrl = new AbortController();
|
|
527
731
|
const timer = setTimeout(() => ctrl.abort(), 30_000);
|
|
@@ -536,24 +740,36 @@ registerTool({
|
|
|
536
740
|
const pinned = await resolvePublicHost(current.hostname);
|
|
537
741
|
if (ctx.signal?.aborted)
|
|
538
742
|
throw interrupted("web fetch");
|
|
539
|
-
res = await requestPinned(current, pinned, signal);
|
|
743
|
+
res = await requestPinned(current, pinned, signal, webProxy(current, ctx.cwd));
|
|
540
744
|
const loc = res.status >= 300 && res.status < 400 ? res.headers.get("location") : null;
|
|
541
745
|
if (!loc || hop >= 5)
|
|
542
746
|
break;
|
|
543
747
|
const next = new URL(loc, current);
|
|
544
748
|
if (next.protocol !== "http:" && next.protocol !== "https:") {
|
|
545
|
-
res.
|
|
749
|
+
await res.release(true);
|
|
546
750
|
return "Error: redirect to a non-http(s) URL was blocked.";
|
|
547
751
|
}
|
|
752
|
+
if (next.username || next.password) {
|
|
753
|
+
await res.release(true);
|
|
754
|
+
return "Error: redirect with URL-embedded credentials was blocked.";
|
|
755
|
+
}
|
|
548
756
|
// We never consume redirect bodies. Destroy the socket before following the next pinned hop so a
|
|
549
757
|
// server cannot accumulate idle response streams across a redirect chain.
|
|
550
|
-
res.
|
|
758
|
+
await res.release(true);
|
|
551
759
|
if (ctx.signal?.aborted)
|
|
552
760
|
throw interrupted("web fetch");
|
|
553
761
|
current = next;
|
|
554
762
|
}
|
|
555
763
|
const ct = res.headers.get("content-type") ?? "";
|
|
556
|
-
|
|
764
|
+
let raw;
|
|
765
|
+
let bodyRead = false;
|
|
766
|
+
try {
|
|
767
|
+
raw = await readPinnedCapped(res.body, cap * 4); // byte ceiling (HTML→text shrinks; cap*4 leaves headroom)
|
|
768
|
+
bodyRead = true;
|
|
769
|
+
}
|
|
770
|
+
finally {
|
|
771
|
+
await res.release(!bodyRead);
|
|
772
|
+
}
|
|
557
773
|
if (ctx.signal?.aborted)
|
|
558
774
|
throw interrupted("web fetch");
|
|
559
775
|
let text = /html/i.test(ct) ? htmlToText(raw) : raw;
|
|
@@ -569,7 +785,10 @@ registerTool({
|
|
|
569
785
|
catch (e) {
|
|
570
786
|
if (ctx.signal?.aborted)
|
|
571
787
|
throw interrupted("web fetch");
|
|
572
|
-
|
|
788
|
+
const display = new URL(url.href);
|
|
789
|
+
display.username = "";
|
|
790
|
+
display.password = "";
|
|
791
|
+
return `Error fetching ${display.href}: ${safeNetworkError(e)}`;
|
|
573
792
|
}
|
|
574
793
|
finally {
|
|
575
794
|
clearTimeout(timer);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanhara/hara",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.132.0",
|
|
4
4
|
"description": "hara — a coding agent CLI that runs like an engineering org.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"hara": "runtime-bootstrap.cjs"
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"ink": "^6.8.0",
|
|
60
60
|
"openai": "^6.44.0",
|
|
61
61
|
"react": "^19.2.7",
|
|
62
|
+
"undici": "^7.28.0",
|
|
62
63
|
"ws": "^8.19.0"
|
|
63
64
|
},
|
|
64
65
|
"devDependencies": {
|
|
@@ -77,6 +78,9 @@
|
|
|
77
78
|
"@larksuiteoapi/node-sdk": {
|
|
78
79
|
"axios": "1.18.1",
|
|
79
80
|
"protobufjs": "7.6.5"
|
|
80
|
-
}
|
|
81
|
+
},
|
|
82
|
+
"@hono/node-server": "2.0.11",
|
|
83
|
+
"hono": "4.12.31",
|
|
84
|
+
"fast-uri": "3.1.3"
|
|
81
85
|
}
|
|
82
86
|
}
|