agentroam 0.2.0-preview.3 → 0.2.0-preview.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/dist/cli.js +1 -1
- package/dist/cloudflared/bundled-asset.test.js +1 -1
- package/dist/cloudflared/manifest.js +1 -1
- package/dist/tunnel/cloudflare-provider.js +17 -10
- package/dist/tunnel/cloudflare-provider.test.js +33 -0
- package/dist/tunnel/proxy-settings.js +141 -0
- package/dist/tunnel/proxy-settings.test.js +111 -0
- package/dist/tunnel/public-readiness.js +60 -36
- package/dist/tunnel/public-readiness.test.js +80 -0
- package/package.json +27 -9
- package/runtime/.next/BUILD_ID +1 -1
- package/runtime/.next/app-build-manifest.json +4 -4
- package/runtime/.next/app-path-routes-manifest.json +1 -1
- package/runtime/.next/build-manifest.json +7 -7
- package/runtime/.next/prerender-manifest.json +1 -1
- package/runtime/.next/react-loadable-manifest.json +1 -1
- package/runtime/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/runtime/.next/server/app/_not-found.html +1 -1
- package/runtime/.next/server/app/_not-found.rsc +1 -1
- package/runtime/.next/server/app/api/agent/abort/route.js +1 -1
- package/runtime/.next/server/app/api/agent/answer/route.js +1 -1
- package/runtime/.next/server/app/api/agent/model/route.js +1 -0
- package/runtime/.next/server/app/api/agent/model/route.js.nft.json +1 -0
- package/runtime/.next/server/app/api/agent/model.body +1 -0
- package/runtime/.next/server/app/api/agent/model.meta +1 -0
- package/runtime/.next/server/app/api/agent/run/route.js +1 -1
- package/runtime/.next/server/app/api/agent/steer/route.js +1 -0
- package/runtime/.next/server/app/api/agent/steer/route.js.nft.json +1 -0
- package/runtime/.next/server/app/api/agent/stream/route.js +3 -2
- package/runtime/.next/server/app/api/mcp/connect/route.js +1 -1
- package/runtime/.next/server/app/api/plugins/load/route.js +1 -1
- package/runtime/.next/server/app/api/remote-tools/register/route.js +1 -1
- package/runtime/.next/server/app/api/sessions/[id]/route.js +1 -1
- package/runtime/.next/server/app/api/sessions/route.js +1 -1
- package/runtime/.next/server/app/api/skills/route.js +1 -1
- package/runtime/.next/server/app/api/tools/route.js +1 -1
- package/runtime/.next/server/app/api/web-auth/login/route.js +1 -1
- package/runtime/.next/server/app/api/web-auth/logout/route.js +1 -1
- package/runtime/.next/server/app/api/web-auth/revoke-all/route.js +1 -1
- package/runtime/.next/server/app/api/web-auth/setup/route.js +1 -1
- package/runtime/.next/server/app/api/web-auth/status/route.js +1 -1
- package/runtime/.next/server/app/api/web-console/bootstrap/route.js +1 -1
- package/runtime/.next/server/app/api/web-console/device-state/route.js +1 -1
- package/runtime/.next/server/app/api/web-console/history/route.js +1 -1
- package/runtime/.next/server/app/api/web-console/history/route.js.nft.json +1 -1
- package/runtime/.next/server/app/api/web-console/preferences/route.js +1 -1
- package/runtime/.next/server/app/web/page.js +10 -10
- package/runtime/.next/server/app/web/page_client-reference-manifest.js +1 -1
- package/runtime/.next/server/app/web.html +1 -1
- package/runtime/.next/server/app/web.rsc +2 -2
- package/runtime/.next/server/app-paths-manifest.json +13 -11
- package/runtime/.next/server/chunks/322.js +28 -12
- package/runtime/.next/server/middleware-build-manifest.js +1 -1
- package/runtime/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/runtime/.next/server/pages/404.html +1 -1
- package/runtime/.next/server/pages/500.html +1 -1
- package/runtime/.next/server/server-reference-manifest.json +1 -1
- package/runtime/.next/static/chunks/982.9e87a5cc0e6b13cf.js +1 -0
- package/runtime/.next/static/chunks/app/web/page-02e0df331bb14489.js +1 -0
- package/runtime/.next/static/chunks/{main-a7dd8c98b48af038.js → main-30618c50527ce4e9.js} +1 -1
- package/runtime/.next/static/chunks/{webpack-d91bc49de770fd58.js → webpack-0c872a6ee48cbbdf.js} +1 -1
- package/runtime/node_modules/.package-lock.json +3 -3
- package/runtime/node_modules/@agent/core/dist/domain/context/ContextLoader.d.ts +2 -0
- package/runtime/node_modules/@agent/core/dist/domain/context/ContextLoader.d.ts.map +1 -1
- package/runtime/node_modules/@agent/core/dist/index.js +114 -12
- package/runtime/node_modules/@agent/core/dist/index.js.map +7 -6
- package/runtime/node_modules/@agent/core/dist/infrastructure/SQLiteAnonymousWebStore.d.ts +13 -0
- package/runtime/node_modules/@agent/core/dist/infrastructure/SQLiteAnonymousWebStore.d.ts.map +1 -0
- package/runtime/node_modules/@agent/core/dist/infrastructure/SQLiteAnonymousWebStore.test.d.ts +2 -0
- package/runtime/node_modules/@agent/core/dist/infrastructure/SQLiteAnonymousWebStore.test.d.ts.map +1 -0
- package/runtime/node_modules/@agent/core/dist/infrastructure/SQLiteDatabase.d.ts.map +1 -1
- package/runtime/node_modules/@agent/core/dist/infrastructure/SQLiteSessionStore.d.ts.map +1 -1
- package/runtime/node_modules/@agent/core/dist/infrastructure/index.d.ts +1 -0
- package/runtime/node_modules/@agent/core/dist/infrastructure/index.d.ts.map +1 -1
- package/runtime/node_modules/node-abi/abi_registry.json +7 -0
- package/runtime/node_modules/node-abi/package.json +1 -1
- package/runtime/package-lock.json +3 -3
- package/runtime/webapp/dist/assets/App-lXIeMH5u.js +52 -0
- package/runtime/webapp/dist/assets/index-BAFMhDtY.js +42 -0
- package/runtime/webapp/dist/assets/index-Beg9M2xI.css +1 -0
- package/runtime/webapp/dist/index.html +17 -0
- package/runtime/webapp/dist/pcm-audio-worklet.js +126 -0
- package/runtime/ws-server.mjs +148 -18
- package/runtime/.next/static/chunks/982.bf2e6e7fe55cd5ca.js +0 -1
- package/runtime/.next/static/chunks/app/web/page-0ee508ac538f72d3.js +0 -1
- /package/runtime/.next/static/{OszWLAB2kMAnNfSwJzpyw → dclalTmecO-HMVQvSKG7g}/_buildManifest.js +0 -0
- /package/runtime/.next/static/{OszWLAB2kMAnNfSwJzpyw → dclalTmecO-HMVQvSKG7g}/_ssgManifest.js +0 -0
package/dist/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ import { detectPlatform } from "./platform.js";
|
|
|
9
9
|
import { renderQr } from "./qr.js";
|
|
10
10
|
import { RuntimeManager } from "./runtime-manager.js";
|
|
11
11
|
import { selectRelay } from "./tunnel/relay-orchestrator.js";
|
|
12
|
-
const VERSION = "0.2.0-preview.
|
|
12
|
+
const VERSION = "0.2.0-preview.4";
|
|
13
13
|
export async function main(argv) {
|
|
14
14
|
const options = parseArgs(argv);
|
|
15
15
|
const target = detectPlatform();
|
|
@@ -10,7 +10,7 @@ describe("resolveBundledCloudflared", () => {
|
|
|
10
10
|
const archivePath = resolve(root, "cloudflared.tgz");
|
|
11
11
|
await writeFile(archivePath, "archive");
|
|
12
12
|
await writeFile(manifestPath, JSON.stringify({
|
|
13
|
-
packageVersion: "0.2.0-preview.
|
|
13
|
+
packageVersion: "0.2.0-preview.4",
|
|
14
14
|
upstreamVersion: "2026.8.2",
|
|
15
15
|
target: "darwin-arm64",
|
|
16
16
|
fileName: "cloudflared",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const CLOUDFLARED_VERSION = "2026.8.2";
|
|
2
|
-
export const CLOUDFLARED_PACKAGE_VERSION = "0.2.0-preview.
|
|
2
|
+
export const CLOUDFLARED_PACKAGE_VERSION = "0.2.0-preview.4";
|
|
3
3
|
export const CLOUDFLARED_PACKAGES = {
|
|
4
4
|
"darwin-arm64": "agentroam-cloudflared-darwin-arm64",
|
|
5
5
|
};
|
|
@@ -16,7 +16,7 @@ export class CloudflareTunnelProvider {
|
|
|
16
16
|
if (signal.aborted)
|
|
17
17
|
abort();
|
|
18
18
|
try {
|
|
19
|
-
const publicUrl = await
|
|
19
|
+
const publicUrl = await waitForConnectedUrl(child, log, 30_000);
|
|
20
20
|
const exited = new Promise((resolvePromise) => child.once("exit", (code, childSignal) => resolvePromise({ code, signal: childSignal })));
|
|
21
21
|
return { publicUrl, exited, close: () => stopChildProcess(child) };
|
|
22
22
|
}
|
|
@@ -26,9 +26,11 @@ export class CloudflareTunnelProvider {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
function
|
|
29
|
+
export function waitForConnectedUrl(child, log, timeoutMs) {
|
|
30
30
|
return new Promise((resolvePromise, reject) => {
|
|
31
|
-
|
|
31
|
+
const buffers = { stdout: "", stderr: "" };
|
|
32
|
+
let publicUrl = null;
|
|
33
|
+
let connected = false;
|
|
32
34
|
let settled = false;
|
|
33
35
|
const finish = (error, url) => {
|
|
34
36
|
if (settled)
|
|
@@ -40,20 +42,25 @@ function waitForUrl(child, log, timeoutMs) {
|
|
|
40
42
|
else
|
|
41
43
|
resolvePromise(url);
|
|
42
44
|
};
|
|
43
|
-
const timer = setTimeout(() => finish(new Error("cloudflared tunnel
|
|
44
|
-
const read = (chunk) => {
|
|
45
|
+
const timer = setTimeout(() => finish(new Error("cloudflared tunnel connection timeout")), timeoutMs);
|
|
46
|
+
const read = (stream) => (chunk) => {
|
|
45
47
|
const text = chunk.toString();
|
|
46
|
-
buffer = (
|
|
48
|
+
const buffer = (buffers[stream] + text).slice(-8192);
|
|
49
|
+
buffers[stream] = buffer;
|
|
47
50
|
for (const line of text.split(/\r?\n/))
|
|
48
51
|
if (line)
|
|
49
52
|
log(line.replace(/pair=[^\s&]+/g, "pair=[REDACTED]"));
|
|
50
53
|
const match = buffer.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/i);
|
|
51
54
|
if (match)
|
|
52
|
-
|
|
55
|
+
publicUrl = match[0];
|
|
56
|
+
if (/Registered tunnel connection/i.test(buffer))
|
|
57
|
+
connected = true;
|
|
58
|
+
if (publicUrl && connected)
|
|
59
|
+
finish(undefined, publicUrl);
|
|
53
60
|
};
|
|
54
|
-
child.stdout?.on("data", read);
|
|
55
|
-
child.stderr?.on("data", read);
|
|
56
|
-
child.once("exit", (code) => finish(new Error(`cloudflared exited before
|
|
61
|
+
child.stdout?.on("data", read("stdout"));
|
|
62
|
+
child.stderr?.on("data", read("stderr"));
|
|
63
|
+
child.once("exit", (code) => finish(new Error(`cloudflared exited before tunnel became ready (${code})`)));
|
|
57
64
|
child.once("error", (error) => finish(error));
|
|
58
65
|
});
|
|
59
66
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { PassThrough } from "node:stream";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { waitForConnectedUrl } from "./cloudflare-provider.js";
|
|
5
|
+
describe("waitForConnectedUrl", () => {
|
|
6
|
+
it("waits for both the public URL and a registered tunnel connection", async () => {
|
|
7
|
+
const { child, stderr } = fakeChild();
|
|
8
|
+
let resolved = false;
|
|
9
|
+
const ready = waitForConnectedUrl(child, vi.fn(), 100).then((url) => {
|
|
10
|
+
resolved = true;
|
|
11
|
+
return url;
|
|
12
|
+
});
|
|
13
|
+
stderr.write("https://ready.trycloudflare.com\n");
|
|
14
|
+
await new Promise((resolvePromise) => setImmediate(resolvePromise));
|
|
15
|
+
expect(resolved).toBe(false);
|
|
16
|
+
stderr.write("INF Registered tunnel ");
|
|
17
|
+
stderr.write("connection connIndex=0 protocol=quic\n");
|
|
18
|
+
await expect(ready).resolves.toBe("https://ready.trycloudflare.com");
|
|
19
|
+
});
|
|
20
|
+
it("supports registration output arriving before the URL", async () => {
|
|
21
|
+
const { child, stdout, stderr } = fakeChild();
|
|
22
|
+
const ready = waitForConnectedUrl(child, vi.fn(), 100);
|
|
23
|
+
stdout.write("INF Registered tunnel connection connIndex=0 protocol=http2\n");
|
|
24
|
+
stderr.write("Visit https://later.trycloudflare.com when ready\n");
|
|
25
|
+
await expect(ready).resolves.toBe("https://later.trycloudflare.com");
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
function fakeChild() {
|
|
29
|
+
const stdout = new PassThrough();
|
|
30
|
+
const stderr = new PassThrough();
|
|
31
|
+
const child = Object.assign(new EventEmitter(), { stdout, stderr });
|
|
32
|
+
return { child, stdout, stderr };
|
|
33
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
export async function resolveProxyForUrl(target, options = {}) {
|
|
5
|
+
const env = options.env ?? process.env;
|
|
6
|
+
const noProxy = env.no_proxy?.trim() ? env.no_proxy : env.NO_PROXY;
|
|
7
|
+
if (matchesNoProxy(target, noProxy))
|
|
8
|
+
return null;
|
|
9
|
+
const environmentProxy = selectEnvironmentProxy(target.protocol, env);
|
|
10
|
+
if (environmentProxy !== null)
|
|
11
|
+
return normalizeProxyUrl(environmentProxy);
|
|
12
|
+
if ((options.platform ?? process.platform) !== "darwin")
|
|
13
|
+
return null;
|
|
14
|
+
try {
|
|
15
|
+
const output = await (options.readMacSystemProxy ?? readMacSystemProxy)();
|
|
16
|
+
const settings = parseMacSystemProxy(output);
|
|
17
|
+
if (target.protocol === "https:")
|
|
18
|
+
return settings.httpsProxy ?? settings.httpProxy;
|
|
19
|
+
if (target.protocol === "http:")
|
|
20
|
+
return settings.httpProxy;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Proxy discovery is best-effort; direct readiness retains the previous behavior.
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
export function parseMacSystemProxy(output) {
|
|
28
|
+
const values = new Map();
|
|
29
|
+
for (const line of output.split(/\r?\n/)) {
|
|
30
|
+
const match = /^\s*([A-Za-z][A-Za-z0-9]*)\s*:\s*(.*?)\s*$/.exec(line);
|
|
31
|
+
if (match)
|
|
32
|
+
values.set(match[1], match[2]);
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
httpProxy: buildMacProxy(values, "HTTP"),
|
|
36
|
+
httpsProxy: buildMacProxy(values, "HTTPS"),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
async function readMacSystemProxy() {
|
|
40
|
+
const { stdout } = await execFileAsync("/usr/sbin/scutil", ["--proxy"], {
|
|
41
|
+
encoding: "utf8",
|
|
42
|
+
timeout: 2_000,
|
|
43
|
+
});
|
|
44
|
+
return stdout;
|
|
45
|
+
}
|
|
46
|
+
function selectEnvironmentProxy(protocol, env) {
|
|
47
|
+
const candidates = protocol === "https:"
|
|
48
|
+
? [env.https_proxy, env.HTTPS_PROXY, env.http_proxy, env.HTTP_PROXY, env.all_proxy, env.ALL_PROXY]
|
|
49
|
+
: protocol === "http:"
|
|
50
|
+
? [env.http_proxy, env.HTTP_PROXY, env.all_proxy, env.ALL_PROXY]
|
|
51
|
+
: [env.all_proxy, env.ALL_PROXY];
|
|
52
|
+
for (const candidate of candidates) {
|
|
53
|
+
if (candidate !== undefined && candidate.trim() !== "")
|
|
54
|
+
return candidate;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
function normalizeProxyUrl(value) {
|
|
59
|
+
const candidate = value.trim();
|
|
60
|
+
try {
|
|
61
|
+
const proxy = new URL(candidate.includes("://") ? candidate : `http://${candidate}`);
|
|
62
|
+
if (!["http:", "https:"].includes(proxy.protocol) || proxy.hostname === "")
|
|
63
|
+
return null;
|
|
64
|
+
return proxy.toString();
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function buildMacProxy(values, prefix) {
|
|
71
|
+
if (values.get(`${prefix}Enable`) !== "1")
|
|
72
|
+
return null;
|
|
73
|
+
const host = values.get(`${prefix}Proxy`)?.trim();
|
|
74
|
+
const portText = values.get(`${prefix}Port`)?.trim();
|
|
75
|
+
const port = portText === undefined ? Number.NaN : Number(portText);
|
|
76
|
+
if (!host || /[\s\/?#@]/.test(host) || !Number.isInteger(port) || port < 1 || port > 65_535)
|
|
77
|
+
return null;
|
|
78
|
+
const formattedHost = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
|
79
|
+
return normalizeProxyUrl(`http://${formattedHost}:${port}`);
|
|
80
|
+
}
|
|
81
|
+
function matchesNoProxy(target, value) {
|
|
82
|
+
if (!value)
|
|
83
|
+
return false;
|
|
84
|
+
const hostname = normalizeHostname(target.hostname);
|
|
85
|
+
const targetPort = target.port || defaultPort(target.protocol);
|
|
86
|
+
for (const entry of value.split(",")) {
|
|
87
|
+
const rule = parseNoProxyRule(entry);
|
|
88
|
+
if (!rule)
|
|
89
|
+
continue;
|
|
90
|
+
if (rule.host === "*")
|
|
91
|
+
return true;
|
|
92
|
+
if (rule.port !== null && rule.port !== targetPort)
|
|
93
|
+
continue;
|
|
94
|
+
if (hostname === rule.host || hostname.endsWith(`.${rule.host}`))
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
function parseNoProxyRule(entry) {
|
|
100
|
+
let value = entry.trim().toLowerCase();
|
|
101
|
+
if (!value)
|
|
102
|
+
return null;
|
|
103
|
+
if (value === "*")
|
|
104
|
+
return { host: "*", port: null };
|
|
105
|
+
let host = value;
|
|
106
|
+
let port = null;
|
|
107
|
+
if (value.startsWith("[")) {
|
|
108
|
+
const closingBracket = value.indexOf("]");
|
|
109
|
+
if (closingBracket < 0)
|
|
110
|
+
return null;
|
|
111
|
+
host = value.slice(1, closingBracket);
|
|
112
|
+
const remainder = value.slice(closingBracket + 1);
|
|
113
|
+
if (remainder) {
|
|
114
|
+
if (!/^:\d+$/.test(remainder))
|
|
115
|
+
return null;
|
|
116
|
+
port = remainder.slice(1);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
const colon = value.lastIndexOf(":");
|
|
121
|
+
if (colon >= 0 && value.indexOf(":") === colon) {
|
|
122
|
+
const possiblePort = value.slice(colon + 1);
|
|
123
|
+
if (/^\d+$/.test(possiblePort)) {
|
|
124
|
+
host = value.slice(0, colon);
|
|
125
|
+
port = possiblePort;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
host = normalizeHostname(host.replace(/^\*?\./, ""));
|
|
130
|
+
return host ? { host, port } : null;
|
|
131
|
+
}
|
|
132
|
+
function normalizeHostname(hostname) {
|
|
133
|
+
return hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
|
|
134
|
+
}
|
|
135
|
+
function defaultPort(protocol) {
|
|
136
|
+
if (protocol === "https:")
|
|
137
|
+
return "443";
|
|
138
|
+
if (protocol === "http:")
|
|
139
|
+
return "80";
|
|
140
|
+
return "";
|
|
141
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { parseMacSystemProxy, resolveProxyForUrl } from "./proxy-settings.js";
|
|
3
|
+
const target = new URL("https://api.example.test/status");
|
|
4
|
+
describe("resolveProxyForUrl", () => {
|
|
5
|
+
it("prefers the lowercase HTTPS proxy and normalizes an omitted scheme", async () => {
|
|
6
|
+
await expect(resolveProxyForUrl(target, {
|
|
7
|
+
env: {
|
|
8
|
+
https_proxy: "127.0.0.1:7897",
|
|
9
|
+
HTTPS_PROXY: "http://uppercase.test:8080",
|
|
10
|
+
HTTP_PROXY: "http://http.test:8080",
|
|
11
|
+
},
|
|
12
|
+
platform: "linux",
|
|
13
|
+
})).resolves.toBe("http://127.0.0.1:7897/");
|
|
14
|
+
});
|
|
15
|
+
it("uses HTTP_PROXY as the HTTPS fallback and ALL_PROXY for HTTP", async () => {
|
|
16
|
+
await expect(resolveProxyForUrl(target, { env: { HTTP_PROXY: "http://http.test:8080" }, platform: "linux" })).resolves.toBe("http://http.test:8080/");
|
|
17
|
+
await expect(resolveProxyForUrl(new URL("http://example.test"), {
|
|
18
|
+
env: { ALL_PROXY: "https://all.test:8443" },
|
|
19
|
+
platform: "linux",
|
|
20
|
+
})).resolves.toBe("https://all.test:8443/");
|
|
21
|
+
});
|
|
22
|
+
it.each([
|
|
23
|
+
["*", "https://api.example.test/status"],
|
|
24
|
+
["api.example.test", "https://api.example.test/status"],
|
|
25
|
+
[".example.test", "https://api.example.test/status"],
|
|
26
|
+
["example.test", "https://api.example.test/status"],
|
|
27
|
+
["api.example.test:443", "https://api.example.test/status"],
|
|
28
|
+
["api.example.test:8443", "https://api.example.test:8443/status"],
|
|
29
|
+
])("bypasses the proxy for NO_PROXY=%s", async (noProxy, url) => {
|
|
30
|
+
await expect(resolveProxyForUrl(new URL(url), {
|
|
31
|
+
env: { HTTPS_PROXY: "http://proxy.test:8080", NO_PROXY: noProxy },
|
|
32
|
+
platform: "darwin",
|
|
33
|
+
readMacSystemProxy: vi.fn(async () => macProxyOutput()),
|
|
34
|
+
})).resolves.toBeNull();
|
|
35
|
+
});
|
|
36
|
+
it("does not bypass a suffix lookalike or a rule with a different port", async () => {
|
|
37
|
+
await expect(resolveProxyForUrl(target, {
|
|
38
|
+
env: { HTTPS_PROXY: "http://proxy.test:8080", no_proxy: "notexample.test,api.example.test:8443" },
|
|
39
|
+
platform: "linux",
|
|
40
|
+
})).resolves.toBe("http://proxy.test:8080/");
|
|
41
|
+
});
|
|
42
|
+
it("falls back to uppercase NO_PROXY when the lowercase value is empty", async () => {
|
|
43
|
+
await expect(resolveProxyForUrl(target, {
|
|
44
|
+
env: { HTTPS_PROXY: "http://proxy.test:8080", no_proxy: "", NO_PROXY: "example.test" },
|
|
45
|
+
platform: "linux",
|
|
46
|
+
})).resolves.toBeNull();
|
|
47
|
+
});
|
|
48
|
+
it("falls back to the enabled macOS HTTPS proxy", async () => {
|
|
49
|
+
await expect(resolveProxyForUrl(target, {
|
|
50
|
+
env: {},
|
|
51
|
+
platform: "darwin",
|
|
52
|
+
readMacSystemProxy: async () => macProxyOutput(),
|
|
53
|
+
})).resolves.toBe("http://127.0.0.1:7897/");
|
|
54
|
+
});
|
|
55
|
+
it("uses the macOS HTTP proxy when HTTPS proxying is disabled", async () => {
|
|
56
|
+
await expect(resolveProxyForUrl(target, {
|
|
57
|
+
env: {},
|
|
58
|
+
platform: "darwin",
|
|
59
|
+
readMacSystemProxy: async () => macProxyOutput({ httpsEnabled: false }),
|
|
60
|
+
})).resolves.toBe("http://127.0.0.1:7898/");
|
|
61
|
+
});
|
|
62
|
+
it("returns direct mode for command failures and non-macOS platforms", async () => {
|
|
63
|
+
await expect(resolveProxyForUrl(target, {
|
|
64
|
+
env: {},
|
|
65
|
+
platform: "darwin",
|
|
66
|
+
readMacSystemProxy: async () => {
|
|
67
|
+
throw new Error("scutil failed");
|
|
68
|
+
},
|
|
69
|
+
})).resolves.toBeNull();
|
|
70
|
+
const reader = vi.fn(async () => macProxyOutput());
|
|
71
|
+
await expect(resolveProxyForUrl(target, { env: {}, platform: "linux", readMacSystemProxy: reader })).resolves.toBeNull();
|
|
72
|
+
expect(reader).not.toHaveBeenCalled();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
describe("parseMacSystemProxy", () => {
|
|
76
|
+
it("parses enabled HTTP and HTTPS settings", () => {
|
|
77
|
+
expect(parseMacSystemProxy(macProxyOutput())).toEqual({
|
|
78
|
+
httpProxy: "http://127.0.0.1:7898/",
|
|
79
|
+
httpsProxy: "http://127.0.0.1:7897/",
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
it("ignores disabled and malformed settings", () => {
|
|
83
|
+
expect(parseMacSystemProxy(`
|
|
84
|
+
HTTPEnable : 0
|
|
85
|
+
HTTPProxy : 127.0.0.1
|
|
86
|
+
HTTPPort : 7898
|
|
87
|
+
HTTPSEnable : 1
|
|
88
|
+
HTTPSProxy : 127.0.0.1
|
|
89
|
+
HTTPSPort : not-a-port
|
|
90
|
+
`)).toEqual({ httpProxy: null, httpsProxy: null });
|
|
91
|
+
expect(parseMacSystemProxy("HTTPSEnable : 1\nHTTPSProxy : 127.0.0.1\nHTTPSPort : 70000")).toEqual({
|
|
92
|
+
httpProxy: null,
|
|
93
|
+
httpsProxy: null,
|
|
94
|
+
});
|
|
95
|
+
expect(parseMacSystemProxy("HTTPSEnable : 1\nHTTPSProxy : proxy.test/path\nHTTPSPort : 7897")).toEqual({
|
|
96
|
+
httpProxy: null,
|
|
97
|
+
httpsProxy: null,
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
function macProxyOutput({ httpsEnabled = true } = {}) {
|
|
102
|
+
return `
|
|
103
|
+
<dictionary> {
|
|
104
|
+
HTTPEnable : 1
|
|
105
|
+
HTTPPort : 7898
|
|
106
|
+
HTTPProxy : 127.0.0.1
|
|
107
|
+
HTTPSEnable : ${httpsEnabled ? "1" : "0"}
|
|
108
|
+
HTTPSPort : 7897
|
|
109
|
+
HTTPSProxy : 127.0.0.1
|
|
110
|
+
}`;
|
|
111
|
+
}
|
|
@@ -1,52 +1,76 @@
|
|
|
1
|
+
import { fetch as undiciFetch, ProxyAgent } from "undici";
|
|
2
|
+
import { resolveProxyForUrl } from "./proxy-settings.js";
|
|
1
3
|
export async function waitForPublicReadiness(publicUrl, options = {}) {
|
|
2
4
|
const timeoutMs = options.timeoutMs ?? 30_000;
|
|
3
5
|
const intervalMs = options.intervalMs ?? 750;
|
|
4
|
-
const fetchImpl = options.fetchImpl ?? fetch;
|
|
5
6
|
const endpoint = new URL("/api/web-auth/status", `${publicUrl.replace(/\/$/, "")}/`);
|
|
6
|
-
|
|
7
|
-
let
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
7
|
+
let proxyFetch = null;
|
|
8
|
+
let fetchImpl = options.fetchImpl;
|
|
9
|
+
if (!fetchImpl) {
|
|
10
|
+
const proxyResolver = options.proxyResolver ?? resolveProxyForUrl;
|
|
11
|
+
const proxyUrl = await proxyResolver(endpoint).catch(() => null);
|
|
12
|
+
if (proxyUrl)
|
|
13
|
+
proxyFetch = (options.proxyFetchFactory ?? createProxyFetch)(proxyUrl);
|
|
14
|
+
fetchImpl = proxyFetch?.fetch ?? fetch;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const deadline = Date.now() + timeoutMs;
|
|
18
|
+
let lastFailure = "no response";
|
|
19
|
+
while (Date.now() <= deadline) {
|
|
20
|
+
if (options.signal?.aborted)
|
|
21
|
+
throw new Error("public tunnel readiness aborted");
|
|
22
|
+
try {
|
|
23
|
+
const response = await fetchImpl(endpoint, {
|
|
24
|
+
cache: "no-store",
|
|
25
|
+
headers: { "user-agent": "agentroam-readiness/0.2.0-preview.4" },
|
|
26
|
+
signal: options.signal,
|
|
27
|
+
});
|
|
28
|
+
if (response.ok) {
|
|
29
|
+
let body;
|
|
30
|
+
try {
|
|
31
|
+
body = (await response.json());
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
throw new FatalReadinessError("HTTP 200 with invalid auth status JSON");
|
|
35
|
+
}
|
|
36
|
+
if (typeof body.authenticated === "boolean" && typeof body.needsSetup === "boolean")
|
|
37
|
+
return;
|
|
23
38
|
throw new FatalReadinessError("HTTP 200 with invalid auth status JSON");
|
|
24
39
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
40
|
+
else {
|
|
41
|
+
lastFailure = `HTTP ${response.status}`;
|
|
42
|
+
if (![404, 502, 503, 530].includes(response.status))
|
|
43
|
+
throw new FatalReadinessError(lastFailure);
|
|
44
|
+
}
|
|
28
45
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (options.signal?.aborted)
|
|
48
|
+
throw new Error("public tunnel readiness aborted");
|
|
49
|
+
if (error instanceof FatalReadinessError)
|
|
50
|
+
throw new Error(`public tunnel not ready: ${error.message}`);
|
|
51
|
+
lastFailure = error instanceof Error ? error.message : String(error);
|
|
33
52
|
}
|
|
53
|
+
if (Date.now() + intervalMs > deadline)
|
|
54
|
+
break;
|
|
55
|
+
await delay(intervalMs, options.signal);
|
|
34
56
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
throw new Error(`public tunnel not ready: ${error.message}`);
|
|
40
|
-
lastFailure = error instanceof Error ? error.message : String(error);
|
|
41
|
-
}
|
|
42
|
-
if (Date.now() + intervalMs > deadline)
|
|
43
|
-
break;
|
|
44
|
-
await delay(intervalMs, options.signal);
|
|
57
|
+
throw new Error(`public tunnel not ready: ${lastFailure}`);
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
await proxyFetch?.close().catch(() => { });
|
|
45
61
|
}
|
|
46
|
-
throw new Error(`public tunnel not ready: ${lastFailure}`);
|
|
47
62
|
}
|
|
48
63
|
class FatalReadinessError extends Error {
|
|
49
64
|
}
|
|
65
|
+
function createProxyFetch(proxyUrl) {
|
|
66
|
+
const dispatcher = new ProxyAgent(proxyUrl);
|
|
67
|
+
return {
|
|
68
|
+
fetch: async (input, init) => (await undiciFetch(input, { ...init, dispatcher })),
|
|
69
|
+
close: async () => {
|
|
70
|
+
await dispatcher.close();
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
50
74
|
function delay(milliseconds, signal) {
|
|
51
75
|
return new Promise((resolvePromise, reject) => {
|
|
52
76
|
const timer = setTimeout(resolvePromise, milliseconds);
|
|
@@ -21,4 +21,84 @@ describe("waitForPublicReadiness", () => {
|
|
|
21
21
|
const fetchImpl = vi.fn().mockResolvedValue(new Response("bad gateway", { status: 502 }));
|
|
22
22
|
await expect(waitForPublicReadiness("https://example.test", { fetchImpl, intervalMs: 1, timeoutMs: 5 })).rejects.toThrow("HTTP 502");
|
|
23
23
|
});
|
|
24
|
+
it("reuses one proxy handle across retries and closes it after success", async () => {
|
|
25
|
+
const close = vi.fn(async () => { });
|
|
26
|
+
const proxiedFetch = vi
|
|
27
|
+
.fn()
|
|
28
|
+
.mockResolvedValueOnce(new Response("unavailable", { status: 530 }))
|
|
29
|
+
.mockResolvedValueOnce(Response.json({ authenticated: false, needsSetup: true }, { status: 200 }));
|
|
30
|
+
const proxyFetchFactory = vi.fn(() => ({ fetch: proxiedFetch, close }));
|
|
31
|
+
await waitForPublicReadiness("https://example.test", {
|
|
32
|
+
intervalMs: 1,
|
|
33
|
+
timeoutMs: 100,
|
|
34
|
+
proxyResolver: async () => "http://127.0.0.1:7897/",
|
|
35
|
+
proxyFetchFactory,
|
|
36
|
+
});
|
|
37
|
+
expect(proxyFetchFactory).toHaveBeenCalledOnce();
|
|
38
|
+
expect(proxiedFetch).toHaveBeenCalledTimes(2);
|
|
39
|
+
expect(close).toHaveBeenCalledOnce();
|
|
40
|
+
});
|
|
41
|
+
it.each([
|
|
42
|
+
{
|
|
43
|
+
name: "a fatal response",
|
|
44
|
+
fetchImpl: vi.fn().mockResolvedValue(new Response("forbidden", { status: 403 })),
|
|
45
|
+
timeoutMs: 100,
|
|
46
|
+
expected: "HTTP 403",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "a timeout",
|
|
50
|
+
fetchImpl: vi.fn().mockResolvedValue(new Response("unavailable", { status: 530 })),
|
|
51
|
+
timeoutMs: 2,
|
|
52
|
+
expected: "HTTP 530",
|
|
53
|
+
},
|
|
54
|
+
])("closes the proxy handle after $name", async ({ fetchImpl, timeoutMs, expected }) => {
|
|
55
|
+
const close = vi.fn(async () => { });
|
|
56
|
+
await expect(waitForPublicReadiness("https://example.test", {
|
|
57
|
+
intervalMs: 1,
|
|
58
|
+
timeoutMs,
|
|
59
|
+
proxyResolver: async () => "http://127.0.0.1:7897/",
|
|
60
|
+
proxyFetchFactory: () => ({ fetch: fetchImpl, close }),
|
|
61
|
+
})).rejects.toThrow(expected);
|
|
62
|
+
expect(close).toHaveBeenCalledOnce();
|
|
63
|
+
});
|
|
64
|
+
it("closes the proxy handle when readiness is aborted", async () => {
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
const close = vi.fn(async () => { });
|
|
67
|
+
const proxiedFetch = vi.fn().mockImplementation(async () => {
|
|
68
|
+
controller.abort();
|
|
69
|
+
throw new Error("request aborted");
|
|
70
|
+
});
|
|
71
|
+
await expect(waitForPublicReadiness("https://example.test", {
|
|
72
|
+
signal: controller.signal,
|
|
73
|
+
proxyResolver: async () => "http://127.0.0.1:7897/",
|
|
74
|
+
proxyFetchFactory: () => ({ fetch: proxiedFetch, close }),
|
|
75
|
+
})).rejects.toThrow("public tunnel readiness aborted");
|
|
76
|
+
expect(close).toHaveBeenCalledOnce();
|
|
77
|
+
});
|
|
78
|
+
it("does not create a proxy handle in direct mode", async () => {
|
|
79
|
+
const proxyFetchFactory = vi.fn();
|
|
80
|
+
const directFetch = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(Response.json({ authenticated: false, needsSetup: true }));
|
|
81
|
+
try {
|
|
82
|
+
await waitForPublicReadiness("https://example.test", {
|
|
83
|
+
proxyResolver: async () => null,
|
|
84
|
+
proxyFetchFactory,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
directFetch.mockRestore();
|
|
89
|
+
}
|
|
90
|
+
expect(proxyFetchFactory).not.toHaveBeenCalled();
|
|
91
|
+
});
|
|
92
|
+
it("skips proxy discovery when a fetch implementation is injected", async () => {
|
|
93
|
+
const proxyResolver = vi.fn(async () => "http://127.0.0.1:7897/");
|
|
94
|
+
const proxyFetchFactory = vi.fn();
|
|
95
|
+
const fetchImpl = vi.fn().mockResolvedValue(Response.json({ authenticated: false, needsSetup: true }));
|
|
96
|
+
await waitForPublicReadiness("https://example.test", {
|
|
97
|
+
fetchImpl,
|
|
98
|
+
proxyResolver,
|
|
99
|
+
proxyFetchFactory,
|
|
100
|
+
});
|
|
101
|
+
expect(proxyResolver).not.toHaveBeenCalled();
|
|
102
|
+
expect(proxyFetchFactory).not.toHaveBeenCalled();
|
|
103
|
+
});
|
|
24
104
|
});
|
package/package.json
CHANGED
|
@@ -1,23 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentroam",
|
|
3
|
-
"version": "0.2.0-preview.
|
|
3
|
+
"version": "0.2.0-preview.4",
|
|
4
4
|
"description": "Zero-install mobile remote terminal and file console",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
|
|
6
|
+
"bin": {
|
|
7
|
+
"agentroam": "./bin/agentroam.mjs"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=22 <23"
|
|
11
|
+
},
|
|
12
|
+
"os": [
|
|
13
|
+
"darwin"
|
|
14
|
+
],
|
|
15
|
+
"cpu": [
|
|
16
|
+
"arm64"
|
|
17
|
+
],
|
|
18
|
+
"files": [
|
|
19
|
+
"bin",
|
|
20
|
+
"dist",
|
|
21
|
+
"runtime",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
11
24
|
"scripts": {
|
|
12
25
|
"build": "tsc -p tsconfig.json",
|
|
13
26
|
"test": "vitest run --root ../.. packages/cli/src",
|
|
14
27
|
"prepack": "node ../../scripts/stage-cli-runtime.mjs && npm run build"
|
|
15
28
|
},
|
|
16
29
|
"dependencies": {
|
|
17
|
-
"qrcode-terminal": "^0.12.0"
|
|
30
|
+
"qrcode-terminal": "^0.12.0",
|
|
31
|
+
"undici": "^6.25.0"
|
|
18
32
|
},
|
|
19
33
|
"optionalDependencies": {
|
|
20
|
-
"agentroam-cloudflared-darwin-arm64": "0.2.0-preview.
|
|
34
|
+
"agentroam-cloudflared-darwin-arm64": "0.2.0-preview.4"
|
|
21
35
|
},
|
|
22
36
|
"devDependencies": {
|
|
23
37
|
"@types/node": "^22.0.0",
|
|
@@ -26,5 +40,9 @@
|
|
|
26
40
|
"vitest": "^2.1.0"
|
|
27
41
|
},
|
|
28
42
|
"license": "MIT",
|
|
29
|
-
"publishConfig": {
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public",
|
|
45
|
+
"provenance": true,
|
|
46
|
+
"tag": "preview"
|
|
47
|
+
}
|
|
30
48
|
}
|
package/runtime/.next/BUILD_ID
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
dclalTmecO-HMVQvSKG7g
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"pages": {
|
|
3
3
|
"/_not-found/page": [
|
|
4
|
-
"static/chunks/webpack-
|
|
4
|
+
"static/chunks/webpack-0c872a6ee48cbbdf.js",
|
|
5
5
|
"static/chunks/3cc391f0-17a90b8b534c353f.js",
|
|
6
6
|
"static/chunks/183-9c8ca0c781655214.js",
|
|
7
7
|
"static/chunks/main-app-07d67f2d4e96ecb8.js",
|
|
8
8
|
"static/chunks/app/_not-found/page-e98edbffee24e843.js"
|
|
9
9
|
],
|
|
10
10
|
"/layout": [
|
|
11
|
-
"static/chunks/webpack-
|
|
11
|
+
"static/chunks/webpack-0c872a6ee48cbbdf.js",
|
|
12
12
|
"static/chunks/3cc391f0-17a90b8b534c353f.js",
|
|
13
13
|
"static/chunks/183-9c8ca0c781655214.js",
|
|
14
14
|
"static/chunks/main-app-07d67f2d4e96ecb8.js",
|
|
15
15
|
"static/chunks/app/layout-9176f750902030b5.js"
|
|
16
16
|
],
|
|
17
17
|
"/web/page": [
|
|
18
|
-
"static/chunks/webpack-
|
|
18
|
+
"static/chunks/webpack-0c872a6ee48cbbdf.js",
|
|
19
19
|
"static/chunks/3cc391f0-17a90b8b534c353f.js",
|
|
20
20
|
"static/chunks/183-9c8ca0c781655214.js",
|
|
21
21
|
"static/chunks/main-app-07d67f2d4e96ecb8.js",
|
|
22
|
-
"static/chunks/app/web/page-
|
|
22
|
+
"static/chunks/app/web/page-02e0df331bb14489.js"
|
|
23
23
|
]
|
|
24
24
|
}
|
|
25
25
|
}
|