@asterxsk/kiln 0.4.0 → 0.4.2
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 +155 -155
- package/agent/AGENTS.md +67 -67
- package/agent/README.md +5 -5
- package/agent/extensions/ask-user/index.ts +418 -418
- package/agent/extensions/ask-user/install.ps1 +27 -27
- package/agent/extensions/ask-user/install.sh +24 -24
- package/agent/extensions/ask-user/package-lock.json +769 -769
- package/agent/extensions/ask-user/package.json +19 -19
- package/agent/extensions/ask-user/prompt.ts +45 -45
- package/agent/extensions/ask-user/tsconfig.json +7 -7
- package/agent/extensions/background-terminals/docs/implementation-guide.md +942 -942
- package/agent/extensions/background-terminals/index.ts +627 -627
- package/agent/extensions/background-terminals/install.ps1 +27 -27
- package/agent/extensions/background-terminals/install.sh +24 -24
- package/agent/extensions/background-terminals/manager.test.ts +735 -735
- package/agent/extensions/background-terminals/output.test.ts +109 -109
- package/agent/extensions/background-terminals/package-lock.json +769 -769
- package/agent/extensions/background-terminals/package.json +17 -17
- package/agent/extensions/background-terminals/prompt.test.ts +125 -125
- package/agent/extensions/background-terminals/ps.test.ts +82 -82
- package/agent/extensions/background-terminals/result-delivery.test.ts +44 -44
- package/agent/extensions/background-terminals/src/domain.ts +87 -87
- package/agent/extensions/background-terminals/src/manager.ts +907 -907
- package/agent/extensions/background-terminals/src/output.ts +84 -84
- package/agent/extensions/background-terminals/src/prompt.ts +142 -142
- package/agent/extensions/background-terminals/src/result-delivery.ts +27 -27
- package/agent/extensions/background-terminals/src/runtime.ts +36 -36
- package/agent/extensions/background-terminals/src/ui/output-view.ts +79 -79
- package/agent/extensions/background-terminals/src/ui/ps.ts +621 -621
- package/agent/extensions/background-terminals/tsconfig.json +7 -7
- package/agent/extensions/destructive/README.md +31 -31
- package/agent/extensions/destructive/index.ts +88 -88
- package/agent/extensions/destructive/install.ps1 +27 -27
- package/agent/extensions/destructive/install.sh +24 -24
- package/agent/extensions/file-search/index.spec.ts +443 -443
- package/agent/extensions/file-search/index.ts +459 -459
- package/agent/extensions/file-search/install.ps1 +27 -27
- package/agent/extensions/file-search/install.sh +24 -24
- package/agent/extensions/file-search/package-lock.json +2253 -2253
- package/agent/extensions/file-search/package.json +23 -23
- package/agent/extensions/file-search/src/args.ts +122 -122
- package/agent/extensions/file-search/src/binaries.ts +422 -422
- package/agent/extensions/file-search/src/output.ts +126 -126
- package/agent/extensions/file-search/src/process.ts +146 -146
- package/agent/extensions/file-search/src/prompt.ts +52 -52
- package/agent/extensions/file-search/tsconfig.json +7 -7
- package/agent/extensions/goal/README.md +50 -50
- package/agent/extensions/goal/index.ts +155 -155
- package/agent/extensions/goal/install.ps1 +27 -27
- package/agent/extensions/goal/install.sh +24 -24
- package/agent/extensions/kiln-update/README.md +38 -25
- package/agent/extensions/kiln-update/index.ts +177 -99
- package/agent/extensions/modelconf/PLAN.md +915 -915
- package/agent/extensions/modelconf/README.md +66 -66
- package/agent/extensions/modelconf/index.ts +296 -296
- package/agent/extensions/modelconf/install.ps1 +27 -27
- package/agent/extensions/modelconf/install.sh +24 -24
- package/agent/extensions/modelconf/package-lock.json +1809 -1809
- package/agent/extensions/modelconf/package.json +13 -13
- package/agent/extensions/modelconf/src/fuzzy.ts +26 -26
- package/agent/extensions/modelconf/src/glob.ts +13 -13
- package/agent/extensions/modelconf/src/persistence.test.ts +46 -46
- package/agent/extensions/modelconf/src/persistence.ts +164 -164
- package/agent/extensions/modelconf/src/ui/ModelConfView.test.ts +75 -75
- package/agent/extensions/modelconf/src/ui/ModelConfView.ts +1101 -1101
- package/agent/extensions/modelconf/tsconfig.json +15 -15
- package/agent/extensions/pi-web-access/CHANGELOG.md +690 -690
- package/agent/extensions/pi-web-access/LICENSE +21 -21
- package/agent/extensions/pi-web-access/README.md +470 -470
- package/agent/extensions/pi-web-access/SECURITY.md +5 -5
- package/agent/extensions/pi-web-access/activity.ts +101 -101
- package/agent/extensions/pi-web-access/auth-fetch.ts +148 -148
- package/agent/extensions/pi-web-access/brightdata-unlocker.ts +272 -272
- package/agent/extensions/pi-web-access/chrome-cookies.ts +669 -669
- package/agent/extensions/pi-web-access/content-find.ts +139 -139
- package/agent/extensions/pi-web-access/credential-source.ts +191 -191
- package/agent/extensions/pi-web-access/data-uri-sanitize.ts +406 -406
- package/agent/extensions/pi-web-access/datalab-pdf-extract.ts +568 -568
- package/agent/extensions/pi-web-access/declared-web-links.ts +173 -173
- package/agent/extensions/pi-web-access/evidence/CONTRACT-EVIDENCE.md +496 -496
- package/agent/extensions/pi-web-access/evidence/contract-probe.mjs +140 -140
- package/agent/extensions/pi-web-access/exa.ts +526 -526
- package/agent/extensions/pi-web-access/extract.ts +1196 -1196
- package/agent/extensions/pi-web-access/feature-config.ts +29 -29
- package/agent/extensions/pi-web-access/fetch-params.ts +111 -111
- package/agent/extensions/pi-web-access/gemini-adc.ts +298 -298
- package/agent/extensions/pi-web-access/gemini-api.ts +353 -353
- package/agent/extensions/pi-web-access/gemini-pdf-extract.ts +108 -108
- package/agent/extensions/pi-web-access/gemini-search.ts +21 -21
- package/agent/extensions/pi-web-access/gemini-url-context.ts +128 -128
- package/agent/extensions/pi-web-access/gemini-web-config.ts +101 -101
- package/agent/extensions/pi-web-access/gemini-web.ts +487 -487
- package/agent/extensions/pi-web-access/github-api.ts +197 -197
- package/agent/extensions/pi-web-access/github-extract.ts +746 -746
- package/agent/extensions/pi-web-access/github-issue-pr.ts +700 -700
- package/agent/extensions/pi-web-access/index.ts +1737 -1737
- package/agent/extensions/pi-web-access/package-lock.json +5808 -5808
- package/agent/extensions/pi-web-access/package.json +64 -64
- package/agent/extensions/pi-web-access/page-query.ts +96 -96
- package/agent/extensions/pi-web-access/pdf-extract.ts +409 -409
- package/agent/extensions/pi-web-access/promise-try.d.ts +7 -7
- package/agent/extensions/pi-web-access/query-rewrite.ts +51 -51
- package/agent/extensions/pi-web-access/render-search-error.ts +170 -170
- package/agent/extensions/pi-web-access/rsc-extract.ts +338 -338
- package/agent/extensions/pi-web-access/search-types.ts +20 -20
- package/agent/extensions/pi-web-access/source-check.ts +282 -282
- package/agent/extensions/pi-web-access/ssrf-protection.ts +526 -526
- package/agent/extensions/pi-web-access/storage.ts +521 -521
- package/agent/extensions/pi-web-access/summary-model-scope.ts +125 -125
- package/agent/extensions/pi-web-access/test/auth-fetch.test.mjs +208 -208
- package/agent/extensions/pi-web-access/test/brightdata-unlocker.test.mjs +840 -840
- package/agent/extensions/pi-web-access/test/chrome-cookie-extraction.test.mjs +441 -441
- package/agent/extensions/pi-web-access/test/config-path.test.mjs +283 -283
- package/agent/extensions/pi-web-access/test/content-find.test.mjs +25 -25
- package/agent/extensions/pi-web-access/test/credential-source.test.mjs +118 -118
- package/agent/extensions/pi-web-access/test/data-uri-sanitize.test.mjs +210 -210
- package/agent/extensions/pi-web-access/test/datalab-pdf-extract.test.mjs +552 -552
- package/agent/extensions/pi-web-access/test/declared-web-links.test.mjs +212 -212
- package/agent/extensions/pi-web-access/test/fetch-answer-storage.test.mjs +40 -40
- package/agent/extensions/pi-web-access/test/fetch-cache-storage.test.mjs +334 -334
- package/agent/extensions/pi-web-access/test/fetch-content-domain-policy.test.mjs +95 -95
- package/agent/extensions/pi-web-access/test/fetch-modes.test.mjs +53 -53
- package/agent/extensions/pi-web-access/test/fetch-not-found-guidance.test.mjs +92 -92
- package/agent/extensions/pi-web-access/test/fetch-params.test.mjs +86 -86
- package/agent/extensions/pi-web-access/test/fetch-render-call.test.mjs +34 -34
- package/agent/extensions/pi-web-access/test/fetch-routing.test.mjs +173 -173
- package/agent/extensions/pi-web-access/test/gemini-adc-auth.test.mjs +257 -257
- package/agent/extensions/pi-web-access/test/gemini-api-transport.test.mjs +170 -170
- package/agent/extensions/pi-web-access/test/gemini-pdf-extract.test.mjs +133 -133
- package/agent/extensions/pi-web-access/test/gemini-web-cookie-opt-in.test.mjs +178 -178
- package/agent/extensions/pi-web-access/test/gemini-web-header-overflow.test.mjs +148 -148
- package/agent/extensions/pi-web-access/test/get-search-content.test.mjs +223 -223
- package/agent/extensions/pi-web-access/test/github-extract.test.mjs +378 -378
- package/agent/extensions/pi-web-access/test/github-issue-pr.test.mjs +565 -565
- package/agent/extensions/pi-web-access/test/inline-content-config.test.mjs +99 -99
- package/agent/extensions/pi-web-access/test/lazy-extract-load.test.mjs +118 -118
- package/agent/extensions/pi-web-access/test/local-video-oversize.test.mjs +52 -52
- package/agent/extensions/pi-web-access/test/package-typebox-dependency.test.mjs +50 -50
- package/agent/extensions/pi-web-access/test/page-query.test.mjs +51 -51
- package/agent/extensions/pi-web-access/test/pdf-config.test.mjs +140 -140
- package/agent/extensions/pi-web-access/test/pdf-extract.test.mjs +500 -500
- package/agent/extensions/pi-web-access/test/proxy-transport.test.mjs +286 -286
- package/agent/extensions/pi-web-access/test/query-rewrite.test.mjs +52 -52
- package/agent/extensions/pi-web-access/test/rsc-fallback.test.mjs +102 -102
- package/agent/extensions/pi-web-access/test/search-error-render.test.mjs +152 -152
- package/agent/extensions/pi-web-access/test/search-providers.test.mjs +274 -274
- package/agent/extensions/pi-web-access/test/source-check.test.mjs +179 -179
- package/agent/extensions/pi-web-access/test/ssrf-allow-ranges-config.test.mjs +205 -205
- package/agent/extensions/pi-web-access/test/ssrf-protection.test.mjs +456 -456
- package/agent/extensions/pi-web-access/test/summary-model-scope.test.mjs +106 -106
- package/agent/extensions/pi-web-access/test/tool-registration-config.test.mjs +182 -182
- package/agent/extensions/pi-web-access/test/web-search-answer-render.test.mjs +66 -66
- package/agent/extensions/pi-web-access/test/youtube-extract-errors.test.mjs +64 -64
- package/agent/extensions/pi-web-access/tsconfig.json +11 -11
- package/agent/extensions/pi-web-access/utils.ts +451 -451
- package/agent/extensions/pi-web-access/video-extract.ts +392 -392
- package/agent/extensions/pi-web-access/youtube-extract.ts +328 -328
- package/agent/extensions/shared/activity-status.ts +31 -31
- package/agent/extensions/shared/child-session.test.ts +270 -270
- package/agent/extensions/shared/child-session.ts +148 -148
- package/agent/extensions/shared/context-utilization.test.ts +48 -48
- package/agent/extensions/shared/context-utilization.ts +47 -47
- package/agent/extensions/shared/dashboard-state.ts +99 -99
- package/agent/extensions/shared/install.ps1 +27 -27
- package/agent/extensions/shared/install.sh +24 -24
- package/agent/extensions/shared/tool-call-timeout.test.ts +117 -117
- package/agent/extensions/shared/tool-call-timeout.ts +104 -104
- package/agent/extensions/skillsconf/README.md +74 -74
- package/agent/extensions/skillsconf/index.ts +92 -92
- package/agent/extensions/skillsconf/install.ps1 +27 -27
- package/agent/extensions/skillsconf/install.sh +24 -24
- package/agent/extensions/skillsconf/package-lock.json +1809 -1809
- package/agent/extensions/skillsconf/package.json +18 -18
- package/agent/extensions/skillsconf/src/delete-skill.test.ts +64 -64
- package/agent/extensions/skillsconf/src/delete-skill.ts +45 -45
- package/agent/extensions/skillsconf/src/filter.test.ts +80 -80
- package/agent/extensions/skillsconf/src/filter.ts +74 -74
- package/agent/extensions/skillsconf/src/fuzzy.ts +26 -26
- package/agent/extensions/skillsconf/src/persistence.test.ts +72 -72
- package/agent/extensions/skillsconf/src/persistence.ts +169 -169
- package/agent/extensions/skillsconf/src/ui/SkillConfView.test.ts +337 -337
- package/agent/extensions/skillsconf/src/ui/SkillConfView.ts +758 -758
- package/agent/extensions/skillsconf/src/ui/text-input.ts +91 -91
- package/agent/extensions/skillsconf/src/ui/tui-helpers.ts +144 -144
- package/agent/extensions/skillsconf/tsconfig.json +15 -15
- package/agent/extensions/statusline/index.ts +273 -282
- package/agent/extensions/statusline/install.ps1 +27 -27
- package/agent/extensions/statusline/install.sh +24 -24
- package/agent/extensions/subagents/by-the-way.test.ts +29 -29
- package/agent/extensions/subagents/claude.test.ts +119 -119
- package/agent/extensions/subagents/codex.test.ts +102 -102
- package/agent/extensions/subagents/context-usage.test.ts +107 -107
- package/agent/extensions/subagents/docs/design-plan.md +568 -568
- package/agent/extensions/subagents/docs/effect-v4-extension-guide.md +354 -354
- package/agent/extensions/subagents/docs/effect-v4-notes.md +571 -571
- package/agent/extensions/subagents/index.ts +779 -779
- package/agent/extensions/subagents/install.ps1 +27 -27
- package/agent/extensions/subagents/install.sh +24 -24
- package/agent/extensions/subagents/manager.test.ts +276 -276
- package/agent/extensions/subagents/package-lock.json +2244 -2244
- package/agent/extensions/subagents/package.json +19 -19
- package/agent/extensions/subagents/result-delivery.test.ts +27 -27
- package/agent/extensions/subagents/src/backend.ts +73 -73
- package/agent/extensions/subagents/src/backends/claude.ts +701 -701
- package/agent/extensions/subagents/src/backends/codex.ts +1060 -1060
- package/agent/extensions/subagents/src/backends/pi.ts +575 -575
- package/agent/extensions/subagents/src/backends/stub.ts +300 -300
- package/agent/extensions/subagents/src/by-the-way.ts +21 -21
- package/agent/extensions/subagents/src/domain.ts +253 -253
- package/agent/extensions/subagents/src/format.ts +74 -74
- package/agent/extensions/subagents/src/manager.ts +736 -736
- package/agent/extensions/subagents/src/prompt.ts +92 -92
- package/agent/extensions/subagents/src/result-delivery.ts +20 -20
- package/agent/extensions/subagents/src/runtime.ts +53 -53
- package/agent/extensions/subagents/src/ui/takeover.ts +583 -583
- package/agent/extensions/subagents/src/ui/transcript.ts +201 -201
- package/agent/extensions/subagents/takeover.test.ts +29 -29
- package/agent/extensions/subagents/tsconfig.json +7 -7
- package/agent/extensions/todo/AGENTS.md +38 -38
- package/agent/extensions/todo/LICENSE +21 -21
- package/agent/extensions/todo/config.ts +55 -55
- package/agent/extensions/todo/index.ts +151 -151
- package/agent/extensions/todo/install.ps1 +27 -27
- package/agent/extensions/todo/install.sh +24 -24
- package/agent/extensions/todo/locales/de.json +17 -17
- package/agent/extensions/todo/locales/en.json +15 -15
- package/agent/extensions/todo/locales/es.json +17 -17
- package/agent/extensions/todo/locales/fr.json +17 -17
- package/agent/extensions/todo/locales/pt-BR.json +17 -17
- package/agent/extensions/todo/locales/pt.json +17 -17
- package/agent/extensions/todo/locales/ru.json +17 -17
- package/agent/extensions/todo/locales/uk.json +17 -17
- package/agent/extensions/todo/locales/zh.json +17 -17
- package/agent/extensions/todo/package-lock.json +3358 -3358
- package/agent/extensions/todo/package.json +67 -67
- package/agent/extensions/todo/state/i18n-bridge.ts +64 -64
- package/agent/extensions/todo/state/invariants.ts +20 -20
- package/agent/extensions/todo/state/replay.ts +38 -38
- package/agent/extensions/todo/state/selectors.ts +107 -107
- package/agent/extensions/todo/state/state-reducer.ts +326 -326
- package/agent/extensions/todo/state/state.ts +18 -18
- package/agent/extensions/todo/state/store.ts +82 -82
- package/agent/extensions/todo/state/task-graph.ts +57 -57
- package/agent/extensions/todo/todo-overlay.ts +200 -200
- package/agent/extensions/todo/todo.ts +155 -155
- package/agent/extensions/todo/tool/response-envelope.ts +109 -109
- package/agent/extensions/todo/tool/types.ts +206 -206
- package/agent/extensions/todo/view/format.ts +177 -177
- package/agent/extensions/trim-context/README.md +54 -54
- package/agent/extensions/trim-context/index.ts +487 -487
- package/agent/extensions/trim-context/install.ps1 +27 -27
- package/agent/extensions/trim-context/install.sh +24 -24
- package/agent/keybindings.json +7 -7
- package/agent/version.txt +1 -1
- package/bin/kiln.js +20 -13
- package/package.json +1 -1
- package/agent/extensions/taste/index.ts +0 -443
- package/agent/extensions/taste/install.ps1 +0 -27
- package/agent/extensions/taste/install.sh +0 -24
|
@@ -1,286 +1,286 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdtemp, readFile, rm, writeFile, chmod } from "node:fs/promises";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { delimiter, join } from "node:path";
|
|
5
|
-
import { test } from "node:test";
|
|
6
|
-
|
|
7
|
-
import initializeExtension from "../index.ts";
|
|
8
|
-
import { getActiveProxy, installGlobalProxyFetch, runWithProxy } from "../utils.ts";
|
|
9
|
-
|
|
10
|
-
const originalFetch = globalThis.fetch;
|
|
11
|
-
const originalPath = process.env.PATH;
|
|
12
|
-
const originalAgentDir = process.env.PI_CODING_AGENT_DIR;
|
|
13
|
-
const originalNoProxy = process.env.NO_PROXY;
|
|
14
|
-
const originalNoProxyLower = process.env.no_proxy;
|
|
15
|
-
|
|
16
|
-
// Fake shebang executables cannot run on Windows: extensionless scripts are not
|
|
17
|
-
// resolvable/spawnable via CreateProcess, and `.cmd` shims are refused by
|
|
18
|
-
// child_process without `shell: true` (post CVE-2024-27980 hardening). Tests
|
|
19
|
-
// below that depend on the fake curl's canned responses skip there; they still
|
|
20
|
-
// run on POSIX CI. Behavior-only proxy tests are unaffected and keep running.
|
|
21
|
-
const SKIP_FAKE_CURL = process.platform === "win32"
|
|
22
|
-
? "requires a runnable fake curl executable (unavailable on Windows)"
|
|
23
|
-
: false;
|
|
24
|
-
|
|
25
|
-
async function withFakeCurl(t, routes, fn) {
|
|
26
|
-
const dir = await mkdtemp(join(tmpdir(), "pi-proxy-test-"));
|
|
27
|
-
const logPath = join(dir, "curl-args.jsonl");
|
|
28
|
-
const curlPath = join(dir, "curl");
|
|
29
|
-
await writeFile(curlPath, `#!/usr/bin/env node
|
|
30
|
-
const fs = require("node:fs");
|
|
31
|
-
const routes = JSON.parse(process.env.PI_PROXY_TEST_ROUTES);
|
|
32
|
-
const args = process.argv.slice(2);
|
|
33
|
-
fs.appendFileSync(process.env.PI_PROXY_TEST_LOG, JSON.stringify(args) + "\\n");
|
|
34
|
-
function valueAfter(flag) {
|
|
35
|
-
const index = args.indexOf(flag);
|
|
36
|
-
return index === -1 ? null : args[index + 1];
|
|
37
|
-
}
|
|
38
|
-
const url = args[args.length - 1];
|
|
39
|
-
const route = routes[url];
|
|
40
|
-
if (!route) throw new Error("unexpected url " + url);
|
|
41
|
-
fs.writeFileSync(valueAfter("-D"), "HTTP/1.1 " + route.status + " " + route.statusText + "\\r\\n" + (route.location ? "Location: " + route.location + "\\r\\n" : "") + "\\r\\n");
|
|
42
|
-
fs.writeFileSync(valueAfter("--output"), route.body || "");
|
|
43
|
-
process.stdout.write(JSON.stringify({ url_effective: url, num_redirects: 0 }));
|
|
44
|
-
`);
|
|
45
|
-
await chmod(curlPath, 0o755);
|
|
46
|
-
if (process.platform === "win32") {
|
|
47
|
-
// Windows CreateProcess resolves executables via PATHEXT, so the
|
|
48
|
-
// extensionless shebang script above is invisible to spawn("curl").
|
|
49
|
-
// A curl.cmd shim in the same directory makes the fake win resolution.
|
|
50
|
-
await writeFile(join(dir, "curl.cmd"), `@echo off\r\nnode "%~dp0curl" %*\r\n`);
|
|
51
|
-
}
|
|
52
|
-
process.env.PATH = `${dir}${delimiter}${originalPath ?? ""}`;
|
|
53
|
-
process.env.PI_PROXY_TEST_LOG = logPath;
|
|
54
|
-
process.env.PI_PROXY_TEST_ROUTES = JSON.stringify(routes);
|
|
55
|
-
process.env.NO_PROXY = "";
|
|
56
|
-
process.env.no_proxy = "";
|
|
57
|
-
globalThis.fetch = originalFetch;
|
|
58
|
-
installGlobalProxyFetch();
|
|
59
|
-
t.after(async () => {
|
|
60
|
-
globalThis.fetch = originalFetch;
|
|
61
|
-
if (originalPath === undefined) delete process.env.PATH;
|
|
62
|
-
else process.env.PATH = originalPath;
|
|
63
|
-
if (originalNoProxy === undefined) delete process.env.NO_PROXY;
|
|
64
|
-
else process.env.NO_PROXY = originalNoProxy;
|
|
65
|
-
if (originalNoProxyLower === undefined) delete process.env.no_proxy;
|
|
66
|
-
else process.env.no_proxy = originalNoProxyLower;
|
|
67
|
-
delete process.env.PI_PROXY_TEST_LOG;
|
|
68
|
-
delete process.env.PI_PROXY_TEST_ROUTES;
|
|
69
|
-
await rm(dir, { recursive: true, force: true });
|
|
70
|
-
});
|
|
71
|
-
const result = await fn(logPath);
|
|
72
|
-
return result;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function readCurlCalls(logPath) {
|
|
76
|
-
return (await readFile(logPath, "utf8"))
|
|
77
|
-
.trim()
|
|
78
|
-
.split("\n")
|
|
79
|
-
.filter(Boolean)
|
|
80
|
-
.map((line) => JSON.parse(line));
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function headerValues(args) {
|
|
84
|
-
const values = [];
|
|
85
|
-
for (let index = 0; index < args.length; index++) {
|
|
86
|
-
if (args[index] === "-H") values.push(args[index + 1]);
|
|
87
|
-
}
|
|
88
|
-
return values;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function registerSourceCheck() {
|
|
92
|
-
const tools = [];
|
|
93
|
-
initializeExtension({
|
|
94
|
-
registerTool(tool) { tools.push(tool); },
|
|
95
|
-
registerCommand() {},
|
|
96
|
-
registerShortcut() {},
|
|
97
|
-
on() {},
|
|
98
|
-
appendEntry() {},
|
|
99
|
-
});
|
|
100
|
-
return tools.find((tool) => tool.name === "source_check");
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function registerFetchContent() {
|
|
104
|
-
const tools = [];
|
|
105
|
-
initializeExtension({
|
|
106
|
-
registerTool(tool) { tools.push(tool); },
|
|
107
|
-
registerCommand() {},
|
|
108
|
-
registerShortcut() {},
|
|
109
|
-
on() {},
|
|
110
|
-
appendEntry() {},
|
|
111
|
-
});
|
|
112
|
-
return tools.find((tool) => tool.name === "fetch_content");
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function proxyArg(args) {
|
|
116
|
-
const index = args.indexOf("-x");
|
|
117
|
-
return index === -1 ? undefined : args[index + 1];
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
test("proxy curl redirects strip caller headers across origins", { skip: SKIP_FAKE_CURL }, async (t) => {
|
|
121
|
-
await withFakeCurl(t, {
|
|
122
|
-
"https://origin.example/start": { status: 302, statusText: "Found", location: "https://other.example/final" },
|
|
123
|
-
"https://other.example/final": { status: 200, statusText: "OK", body: "ok" },
|
|
124
|
-
}, async (logPath) => {
|
|
125
|
-
const response = await runWithProxy("http://proxy.example:8080", () => fetch("https://origin.example/start", {
|
|
126
|
-
headers: {
|
|
127
|
-
Authorization: "Bearer secret",
|
|
128
|
-
Cookie: "session=secret",
|
|
129
|
-
"X-Api-Key": "secret",
|
|
130
|
-
Accept: "text/html",
|
|
131
|
-
},
|
|
132
|
-
}));
|
|
133
|
-
|
|
134
|
-
assert.equal(await response.text(), "ok");
|
|
135
|
-
assert.equal(response.url, "https://other.example/final");
|
|
136
|
-
assert.equal(response.redirected, true);
|
|
137
|
-
const calls = await readCurlCalls(logPath);
|
|
138
|
-
assert.equal(calls.length, 2);
|
|
139
|
-
assert.ok(headerValues(calls[0]).some((header) => /^authorization:/i.test(header)));
|
|
140
|
-
assert.deepEqual(headerValues(calls[1]), []);
|
|
141
|
-
assert.ok(calls.every((args) => !args.includes("--location")));
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
test("omitted proxy uses global config while empty string forces direct access", async (t) => {
|
|
146
|
-
const dir = await mkdtemp(join(tmpdir(), "pi-proxy-config-test-"));
|
|
147
|
-
await writeFile(join(dir, "web-search.json"), JSON.stringify({ proxy: "http://global-proxy.example:8080" }));
|
|
148
|
-
process.env.PI_CODING_AGENT_DIR = dir;
|
|
149
|
-
t.after(async () => {
|
|
150
|
-
if (originalAgentDir === undefined) delete process.env.PI_CODING_AGENT_DIR;
|
|
151
|
-
else process.env.PI_CODING_AGENT_DIR = originalAgentDir;
|
|
152
|
-
await rm(dir, { recursive: true, force: true });
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
assert.equal(runWithProxy(undefined, () => getActiveProxy()), "http://global-proxy.example:8080/");
|
|
156
|
-
assert.equal(runWithProxy("", () => getActiveProxy()), null);
|
|
157
|
-
assert.equal(runWithProxy("http://call-proxy.example:8080", () => getActiveProxy()), "http://call-proxy.example:8080/");
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
test("invalid configured proxy fails closed instead of direct fetching", async (t) => {
|
|
161
|
-
const dir = await mkdtemp(join(tmpdir(), "pi-proxy-invalid-config-test-"));
|
|
162
|
-
await writeFile(join(dir, "web-search.json"), JSON.stringify({ proxy: "socks5://proxy.example:1080" }));
|
|
163
|
-
process.env.PI_CODING_AGENT_DIR = dir;
|
|
164
|
-
t.after(async () => {
|
|
165
|
-
if (originalAgentDir === undefined) delete process.env.PI_CODING_AGENT_DIR;
|
|
166
|
-
else process.env.PI_CODING_AGENT_DIR = originalAgentDir;
|
|
167
|
-
await rm(dir, { recursive: true, force: true });
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
assert.throws(() => getActiveProxy(), /proxy.*must use the http:\/\/ or https:\/\/ scheme/);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
test("proxy transport does not spawn curl for pre-aborted requests", async (t) => {
|
|
174
|
-
await withFakeCurl(t, {
|
|
175
|
-
"https://origin.example/abort": { status: 200, statusText: "OK", body: "late" },
|
|
176
|
-
}, async (logPath) => {
|
|
177
|
-
const controller = new AbortController();
|
|
178
|
-
controller.abort();
|
|
179
|
-
|
|
180
|
-
await assert.rejects(
|
|
181
|
-
runWithProxy("http://proxy.example:8080", () => fetch("https://origin.example/abort", { signal: controller.signal })),
|
|
182
|
-
/error.*abort/i,
|
|
183
|
-
);
|
|
184
|
-
await assert.rejects(readFile(logPath, "utf8"), /ENOENT/);
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
test("proxy transport errors redact proxy credentials", async (t) => {
|
|
189
|
-
await withFakeCurl(t, {}, async () => {
|
|
190
|
-
await assert.rejects(
|
|
191
|
-
runWithProxy("http://user:secret@proxy.example:8080", () => fetch("https://origin.example/missing")),
|
|
192
|
-
(error) => {
|
|
193
|
-
assert.match(error.message, /http:\/\/redacted:redacted@proxy\.example:8080\//);
|
|
194
|
-
assert.doesNotMatch(error.message, /user:secret/);
|
|
195
|
-
return true;
|
|
196
|
-
},
|
|
197
|
-
);
|
|
198
|
-
});
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
test("proxy curl redirects keep caller headers on the same origin", { skip: SKIP_FAKE_CURL }, async (t) => {
|
|
202
|
-
await withFakeCurl(t, {
|
|
203
|
-
"https://origin.example/start": { status: 302, statusText: "Found", location: "/final" },
|
|
204
|
-
"https://origin.example/final": { status: 200, statusText: "OK", body: "ok" },
|
|
205
|
-
}, async (logPath) => {
|
|
206
|
-
await runWithProxy("http://proxy.example:8080", () => fetch("https://origin.example/start", {
|
|
207
|
-
headers: { Authorization: "Bearer secret" },
|
|
208
|
-
}));
|
|
209
|
-
|
|
210
|
-
const calls = await readCurlCalls(logPath);
|
|
211
|
-
assert.equal(calls.length, 2);
|
|
212
|
-
assert.ok(headerValues(calls[1]).some((header) => /^authorization:/i.test(header)));
|
|
213
|
-
});
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
test("proxy curl keeps manual redirects as redirect responses", { skip: SKIP_FAKE_CURL }, async (t) => {
|
|
217
|
-
await withFakeCurl(t, {
|
|
218
|
-
"https://origin.example/start": { status: 302, statusText: "Found", location: "https://other.example/final" },
|
|
219
|
-
}, async (logPath) => {
|
|
220
|
-
const response = await runWithProxy("http://proxy.example:8080", () => fetch("https://origin.example/start", { redirect: "manual" }));
|
|
221
|
-
|
|
222
|
-
assert.equal(response.status, 302);
|
|
223
|
-
assert.equal(response.headers.get("location"), "https://other.example/final");
|
|
224
|
-
assert.equal((await readCurlCalls(logPath)).length, 1);
|
|
225
|
-
});
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
test("source_check fetchContent uses the explicit proxy for result pages", { skip: SKIP_FAKE_CURL }, async (t) => {
|
|
229
|
-
const previousKey = process.env.EXA_API_KEY;
|
|
230
|
-
process.env.EXA_API_KEY = "source-check-proxy-test-key";
|
|
231
|
-
t.after(() => {
|
|
232
|
-
if (previousKey === undefined) delete process.env.EXA_API_KEY;
|
|
233
|
-
else process.env.EXA_API_KEY = previousKey;
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
await withFakeCurl(t, {
|
|
237
|
-
"https://api.exa.ai/answer": {
|
|
238
|
-
status: 200,
|
|
239
|
-
statusText: "OK",
|
|
240
|
-
body: JSON.stringify({
|
|
241
|
-
answer: "The API docs are available.",
|
|
242
|
-
citations: [{ title: "API docs", url: "https://example.com/api" }],
|
|
243
|
-
}),
|
|
244
|
-
},
|
|
245
|
-
"https://example.com/api": { status: 200, statusText: "OK", body: "<html><title>API docs</title><body>The API docs are available.</body></html>" },
|
|
246
|
-
}, async (logPath) => {
|
|
247
|
-
const tool = registerSourceCheck();
|
|
248
|
-
assert.ok(tool);
|
|
249
|
-
const response = await tool.execute("call", {
|
|
250
|
-
claim: "API docs",
|
|
251
|
-
fetchContent: true,
|
|
252
|
-
proxy: "http://call-proxy.example:8080",
|
|
253
|
-
}, undefined, undefined, { modelRegistry: {} });
|
|
254
|
-
|
|
255
|
-
assert.equal(response.details.sourceCount, 1);
|
|
256
|
-
const calls = await readCurlCalls(logPath);
|
|
257
|
-
const apiCall = calls.find((args) => args.at(-1) === "https://api.exa.ai/answer");
|
|
258
|
-
const pageCall = calls.find((args) => args.at(-1) === "https://example.com/api");
|
|
259
|
-
assert.ok(apiCall);
|
|
260
|
-
assert.ok(pageCall);
|
|
261
|
-
assert.ok(["http://call-proxy.example:8080", "http://call-proxy.example:8080/"].includes(proxyArg(apiCall)));
|
|
262
|
-
assert.ok(["http://call-proxy.example:8080", "http://call-proxy.example:8080/"].includes(proxyArg(pageCall)));
|
|
263
|
-
});
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
test("fetch_content passes the explicit proxy through queued extraction", { skip: SKIP_FAKE_CURL }, async (t) => {
|
|
267
|
-
await withFakeCurl(t, {
|
|
268
|
-
"https://example.com/page": {
|
|
269
|
-
status: 200,
|
|
270
|
-
statusText: "OK",
|
|
271
|
-
body: "<html><title>Proxy page</title><body>Fetched through the requested proxy.</body></html>",
|
|
272
|
-
},
|
|
273
|
-
}, async (logPath) => {
|
|
274
|
-
const tool = registerFetchContent();
|
|
275
|
-
assert.ok(tool);
|
|
276
|
-
const response = await tool.execute("call", {
|
|
277
|
-
url: "https://example.com/page",
|
|
278
|
-
proxy: "http://call-proxy.example:8080",
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
assert.equal(response.details.successful, 1);
|
|
282
|
-
const pageCall = (await readCurlCalls(logPath)).find((args) => args.at(-1) === "https://example.com/page");
|
|
283
|
-
assert.ok(pageCall);
|
|
284
|
-
assert.ok(["http://call-proxy.example:8080", "http://call-proxy.example:8080/"].includes(proxyArg(pageCall)));
|
|
285
|
-
});
|
|
286
|
-
});
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, readFile, rm, writeFile, chmod } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { delimiter, join } from "node:path";
|
|
5
|
+
import { test } from "node:test";
|
|
6
|
+
|
|
7
|
+
import initializeExtension from "../index.ts";
|
|
8
|
+
import { getActiveProxy, installGlobalProxyFetch, runWithProxy } from "../utils.ts";
|
|
9
|
+
|
|
10
|
+
const originalFetch = globalThis.fetch;
|
|
11
|
+
const originalPath = process.env.PATH;
|
|
12
|
+
const originalAgentDir = process.env.PI_CODING_AGENT_DIR;
|
|
13
|
+
const originalNoProxy = process.env.NO_PROXY;
|
|
14
|
+
const originalNoProxyLower = process.env.no_proxy;
|
|
15
|
+
|
|
16
|
+
// Fake shebang executables cannot run on Windows: extensionless scripts are not
|
|
17
|
+
// resolvable/spawnable via CreateProcess, and `.cmd` shims are refused by
|
|
18
|
+
// child_process without `shell: true` (post CVE-2024-27980 hardening). Tests
|
|
19
|
+
// below that depend on the fake curl's canned responses skip there; they still
|
|
20
|
+
// run on POSIX CI. Behavior-only proxy tests are unaffected and keep running.
|
|
21
|
+
const SKIP_FAKE_CURL = process.platform === "win32"
|
|
22
|
+
? "requires a runnable fake curl executable (unavailable on Windows)"
|
|
23
|
+
: false;
|
|
24
|
+
|
|
25
|
+
async function withFakeCurl(t, routes, fn) {
|
|
26
|
+
const dir = await mkdtemp(join(tmpdir(), "pi-proxy-test-"));
|
|
27
|
+
const logPath = join(dir, "curl-args.jsonl");
|
|
28
|
+
const curlPath = join(dir, "curl");
|
|
29
|
+
await writeFile(curlPath, `#!/usr/bin/env node
|
|
30
|
+
const fs = require("node:fs");
|
|
31
|
+
const routes = JSON.parse(process.env.PI_PROXY_TEST_ROUTES);
|
|
32
|
+
const args = process.argv.slice(2);
|
|
33
|
+
fs.appendFileSync(process.env.PI_PROXY_TEST_LOG, JSON.stringify(args) + "\\n");
|
|
34
|
+
function valueAfter(flag) {
|
|
35
|
+
const index = args.indexOf(flag);
|
|
36
|
+
return index === -1 ? null : args[index + 1];
|
|
37
|
+
}
|
|
38
|
+
const url = args[args.length - 1];
|
|
39
|
+
const route = routes[url];
|
|
40
|
+
if (!route) throw new Error("unexpected url " + url);
|
|
41
|
+
fs.writeFileSync(valueAfter("-D"), "HTTP/1.1 " + route.status + " " + route.statusText + "\\r\\n" + (route.location ? "Location: " + route.location + "\\r\\n" : "") + "\\r\\n");
|
|
42
|
+
fs.writeFileSync(valueAfter("--output"), route.body || "");
|
|
43
|
+
process.stdout.write(JSON.stringify({ url_effective: url, num_redirects: 0 }));
|
|
44
|
+
`);
|
|
45
|
+
await chmod(curlPath, 0o755);
|
|
46
|
+
if (process.platform === "win32") {
|
|
47
|
+
// Windows CreateProcess resolves executables via PATHEXT, so the
|
|
48
|
+
// extensionless shebang script above is invisible to spawn("curl").
|
|
49
|
+
// A curl.cmd shim in the same directory makes the fake win resolution.
|
|
50
|
+
await writeFile(join(dir, "curl.cmd"), `@echo off\r\nnode "%~dp0curl" %*\r\n`);
|
|
51
|
+
}
|
|
52
|
+
process.env.PATH = `${dir}${delimiter}${originalPath ?? ""}`;
|
|
53
|
+
process.env.PI_PROXY_TEST_LOG = logPath;
|
|
54
|
+
process.env.PI_PROXY_TEST_ROUTES = JSON.stringify(routes);
|
|
55
|
+
process.env.NO_PROXY = "";
|
|
56
|
+
process.env.no_proxy = "";
|
|
57
|
+
globalThis.fetch = originalFetch;
|
|
58
|
+
installGlobalProxyFetch();
|
|
59
|
+
t.after(async () => {
|
|
60
|
+
globalThis.fetch = originalFetch;
|
|
61
|
+
if (originalPath === undefined) delete process.env.PATH;
|
|
62
|
+
else process.env.PATH = originalPath;
|
|
63
|
+
if (originalNoProxy === undefined) delete process.env.NO_PROXY;
|
|
64
|
+
else process.env.NO_PROXY = originalNoProxy;
|
|
65
|
+
if (originalNoProxyLower === undefined) delete process.env.no_proxy;
|
|
66
|
+
else process.env.no_proxy = originalNoProxyLower;
|
|
67
|
+
delete process.env.PI_PROXY_TEST_LOG;
|
|
68
|
+
delete process.env.PI_PROXY_TEST_ROUTES;
|
|
69
|
+
await rm(dir, { recursive: true, force: true });
|
|
70
|
+
});
|
|
71
|
+
const result = await fn(logPath);
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function readCurlCalls(logPath) {
|
|
76
|
+
return (await readFile(logPath, "utf8"))
|
|
77
|
+
.trim()
|
|
78
|
+
.split("\n")
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
.map((line) => JSON.parse(line));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function headerValues(args) {
|
|
84
|
+
const values = [];
|
|
85
|
+
for (let index = 0; index < args.length; index++) {
|
|
86
|
+
if (args[index] === "-H") values.push(args[index + 1]);
|
|
87
|
+
}
|
|
88
|
+
return values;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function registerSourceCheck() {
|
|
92
|
+
const tools = [];
|
|
93
|
+
initializeExtension({
|
|
94
|
+
registerTool(tool) { tools.push(tool); },
|
|
95
|
+
registerCommand() {},
|
|
96
|
+
registerShortcut() {},
|
|
97
|
+
on() {},
|
|
98
|
+
appendEntry() {},
|
|
99
|
+
});
|
|
100
|
+
return tools.find((tool) => tool.name === "source_check");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function registerFetchContent() {
|
|
104
|
+
const tools = [];
|
|
105
|
+
initializeExtension({
|
|
106
|
+
registerTool(tool) { tools.push(tool); },
|
|
107
|
+
registerCommand() {},
|
|
108
|
+
registerShortcut() {},
|
|
109
|
+
on() {},
|
|
110
|
+
appendEntry() {},
|
|
111
|
+
});
|
|
112
|
+
return tools.find((tool) => tool.name === "fetch_content");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function proxyArg(args) {
|
|
116
|
+
const index = args.indexOf("-x");
|
|
117
|
+
return index === -1 ? undefined : args[index + 1];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
test("proxy curl redirects strip caller headers across origins", { skip: SKIP_FAKE_CURL }, async (t) => {
|
|
121
|
+
await withFakeCurl(t, {
|
|
122
|
+
"https://origin.example/start": { status: 302, statusText: "Found", location: "https://other.example/final" },
|
|
123
|
+
"https://other.example/final": { status: 200, statusText: "OK", body: "ok" },
|
|
124
|
+
}, async (logPath) => {
|
|
125
|
+
const response = await runWithProxy("http://proxy.example:8080", () => fetch("https://origin.example/start", {
|
|
126
|
+
headers: {
|
|
127
|
+
Authorization: "Bearer secret",
|
|
128
|
+
Cookie: "session=secret",
|
|
129
|
+
"X-Api-Key": "secret",
|
|
130
|
+
Accept: "text/html",
|
|
131
|
+
},
|
|
132
|
+
}));
|
|
133
|
+
|
|
134
|
+
assert.equal(await response.text(), "ok");
|
|
135
|
+
assert.equal(response.url, "https://other.example/final");
|
|
136
|
+
assert.equal(response.redirected, true);
|
|
137
|
+
const calls = await readCurlCalls(logPath);
|
|
138
|
+
assert.equal(calls.length, 2);
|
|
139
|
+
assert.ok(headerValues(calls[0]).some((header) => /^authorization:/i.test(header)));
|
|
140
|
+
assert.deepEqual(headerValues(calls[1]), []);
|
|
141
|
+
assert.ok(calls.every((args) => !args.includes("--location")));
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("omitted proxy uses global config while empty string forces direct access", async (t) => {
|
|
146
|
+
const dir = await mkdtemp(join(tmpdir(), "pi-proxy-config-test-"));
|
|
147
|
+
await writeFile(join(dir, "web-search.json"), JSON.stringify({ proxy: "http://global-proxy.example:8080" }));
|
|
148
|
+
process.env.PI_CODING_AGENT_DIR = dir;
|
|
149
|
+
t.after(async () => {
|
|
150
|
+
if (originalAgentDir === undefined) delete process.env.PI_CODING_AGENT_DIR;
|
|
151
|
+
else process.env.PI_CODING_AGENT_DIR = originalAgentDir;
|
|
152
|
+
await rm(dir, { recursive: true, force: true });
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
assert.equal(runWithProxy(undefined, () => getActiveProxy()), "http://global-proxy.example:8080/");
|
|
156
|
+
assert.equal(runWithProxy("", () => getActiveProxy()), null);
|
|
157
|
+
assert.equal(runWithProxy("http://call-proxy.example:8080", () => getActiveProxy()), "http://call-proxy.example:8080/");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("invalid configured proxy fails closed instead of direct fetching", async (t) => {
|
|
161
|
+
const dir = await mkdtemp(join(tmpdir(), "pi-proxy-invalid-config-test-"));
|
|
162
|
+
await writeFile(join(dir, "web-search.json"), JSON.stringify({ proxy: "socks5://proxy.example:1080" }));
|
|
163
|
+
process.env.PI_CODING_AGENT_DIR = dir;
|
|
164
|
+
t.after(async () => {
|
|
165
|
+
if (originalAgentDir === undefined) delete process.env.PI_CODING_AGENT_DIR;
|
|
166
|
+
else process.env.PI_CODING_AGENT_DIR = originalAgentDir;
|
|
167
|
+
await rm(dir, { recursive: true, force: true });
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
assert.throws(() => getActiveProxy(), /proxy.*must use the http:\/\/ or https:\/\/ scheme/);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("proxy transport does not spawn curl for pre-aborted requests", async (t) => {
|
|
174
|
+
await withFakeCurl(t, {
|
|
175
|
+
"https://origin.example/abort": { status: 200, statusText: "OK", body: "late" },
|
|
176
|
+
}, async (logPath) => {
|
|
177
|
+
const controller = new AbortController();
|
|
178
|
+
controller.abort();
|
|
179
|
+
|
|
180
|
+
await assert.rejects(
|
|
181
|
+
runWithProxy("http://proxy.example:8080", () => fetch("https://origin.example/abort", { signal: controller.signal })),
|
|
182
|
+
/error.*abort/i,
|
|
183
|
+
);
|
|
184
|
+
await assert.rejects(readFile(logPath, "utf8"), /ENOENT/);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("proxy transport errors redact proxy credentials", async (t) => {
|
|
189
|
+
await withFakeCurl(t, {}, async () => {
|
|
190
|
+
await assert.rejects(
|
|
191
|
+
runWithProxy("http://user:secret@proxy.example:8080", () => fetch("https://origin.example/missing")),
|
|
192
|
+
(error) => {
|
|
193
|
+
assert.match(error.message, /http:\/\/redacted:redacted@proxy\.example:8080\//);
|
|
194
|
+
assert.doesNotMatch(error.message, /user:secret/);
|
|
195
|
+
return true;
|
|
196
|
+
},
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("proxy curl redirects keep caller headers on the same origin", { skip: SKIP_FAKE_CURL }, async (t) => {
|
|
202
|
+
await withFakeCurl(t, {
|
|
203
|
+
"https://origin.example/start": { status: 302, statusText: "Found", location: "/final" },
|
|
204
|
+
"https://origin.example/final": { status: 200, statusText: "OK", body: "ok" },
|
|
205
|
+
}, async (logPath) => {
|
|
206
|
+
await runWithProxy("http://proxy.example:8080", () => fetch("https://origin.example/start", {
|
|
207
|
+
headers: { Authorization: "Bearer secret" },
|
|
208
|
+
}));
|
|
209
|
+
|
|
210
|
+
const calls = await readCurlCalls(logPath);
|
|
211
|
+
assert.equal(calls.length, 2);
|
|
212
|
+
assert.ok(headerValues(calls[1]).some((header) => /^authorization:/i.test(header)));
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("proxy curl keeps manual redirects as redirect responses", { skip: SKIP_FAKE_CURL }, async (t) => {
|
|
217
|
+
await withFakeCurl(t, {
|
|
218
|
+
"https://origin.example/start": { status: 302, statusText: "Found", location: "https://other.example/final" },
|
|
219
|
+
}, async (logPath) => {
|
|
220
|
+
const response = await runWithProxy("http://proxy.example:8080", () => fetch("https://origin.example/start", { redirect: "manual" }));
|
|
221
|
+
|
|
222
|
+
assert.equal(response.status, 302);
|
|
223
|
+
assert.equal(response.headers.get("location"), "https://other.example/final");
|
|
224
|
+
assert.equal((await readCurlCalls(logPath)).length, 1);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("source_check fetchContent uses the explicit proxy for result pages", { skip: SKIP_FAKE_CURL }, async (t) => {
|
|
229
|
+
const previousKey = process.env.EXA_API_KEY;
|
|
230
|
+
process.env.EXA_API_KEY = "source-check-proxy-test-key";
|
|
231
|
+
t.after(() => {
|
|
232
|
+
if (previousKey === undefined) delete process.env.EXA_API_KEY;
|
|
233
|
+
else process.env.EXA_API_KEY = previousKey;
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
await withFakeCurl(t, {
|
|
237
|
+
"https://api.exa.ai/answer": {
|
|
238
|
+
status: 200,
|
|
239
|
+
statusText: "OK",
|
|
240
|
+
body: JSON.stringify({
|
|
241
|
+
answer: "The API docs are available.",
|
|
242
|
+
citations: [{ title: "API docs", url: "https://example.com/api" }],
|
|
243
|
+
}),
|
|
244
|
+
},
|
|
245
|
+
"https://example.com/api": { status: 200, statusText: "OK", body: "<html><title>API docs</title><body>The API docs are available.</body></html>" },
|
|
246
|
+
}, async (logPath) => {
|
|
247
|
+
const tool = registerSourceCheck();
|
|
248
|
+
assert.ok(tool);
|
|
249
|
+
const response = await tool.execute("call", {
|
|
250
|
+
claim: "API docs",
|
|
251
|
+
fetchContent: true,
|
|
252
|
+
proxy: "http://call-proxy.example:8080",
|
|
253
|
+
}, undefined, undefined, { modelRegistry: {} });
|
|
254
|
+
|
|
255
|
+
assert.equal(response.details.sourceCount, 1);
|
|
256
|
+
const calls = await readCurlCalls(logPath);
|
|
257
|
+
const apiCall = calls.find((args) => args.at(-1) === "https://api.exa.ai/answer");
|
|
258
|
+
const pageCall = calls.find((args) => args.at(-1) === "https://example.com/api");
|
|
259
|
+
assert.ok(apiCall);
|
|
260
|
+
assert.ok(pageCall);
|
|
261
|
+
assert.ok(["http://call-proxy.example:8080", "http://call-proxy.example:8080/"].includes(proxyArg(apiCall)));
|
|
262
|
+
assert.ok(["http://call-proxy.example:8080", "http://call-proxy.example:8080/"].includes(proxyArg(pageCall)));
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("fetch_content passes the explicit proxy through queued extraction", { skip: SKIP_FAKE_CURL }, async (t) => {
|
|
267
|
+
await withFakeCurl(t, {
|
|
268
|
+
"https://example.com/page": {
|
|
269
|
+
status: 200,
|
|
270
|
+
statusText: "OK",
|
|
271
|
+
body: "<html><title>Proxy page</title><body>Fetched through the requested proxy.</body></html>",
|
|
272
|
+
},
|
|
273
|
+
}, async (logPath) => {
|
|
274
|
+
const tool = registerFetchContent();
|
|
275
|
+
assert.ok(tool);
|
|
276
|
+
const response = await tool.execute("call", {
|
|
277
|
+
url: "https://example.com/page",
|
|
278
|
+
proxy: "http://call-proxy.example:8080",
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
assert.equal(response.details.successful, 1);
|
|
282
|
+
const pageCall = (await readCurlCalls(logPath)).find((args) => args.at(-1) === "https://example.com/page");
|
|
283
|
+
assert.ok(pageCall);
|
|
284
|
+
assert.ok(["http://call-proxy.example:8080", "http://call-proxy.example:8080/"].includes(proxyArg(pageCall)));
|
|
285
|
+
});
|
|
286
|
+
});
|