@tonyclaw/agent-inspector 3.0.37 → 3.0.39
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/.output/cli.js +249 -84
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-BvF04FAX.js → CompareDrawer-BjQ2HH8j.js} +1 -1
- package/.output/public/assets/{InspectorPet-CSCn4jWU.js → InspectorPet-DDScgJ7_.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-C3iHhtkc.js +126 -0
- package/.output/public/assets/{ReplayDialog-DWJkKTDB.js → ReplayDialog-BujXVTMJ.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-CDFI55pJ.js → RequestAnatomy-iK2nOuCP.js} +1 -1
- package/.output/public/assets/{ResponseView-tlIj9vRr.js → ResponseView-iAuOJxem.js} +1 -1
- package/.output/public/assets/{StreamingChunkSequence-C7Ntivbf.js → StreamingChunkSequence-DiZ21icS.js} +1 -1
- package/.output/public/assets/{_sessionId-BbrBvkvn.js → _sessionId-CLTlZx8L.js} +1 -1
- package/.output/public/assets/{_sessionId-DUL-l56X.js → _sessionId-D3WwDIit.js} +1 -1
- package/.output/public/assets/{index-e-fWO2Du.js → index--ND7WRCU.js} +1 -1
- package/.output/public/assets/{index-By0CyDmk.js → index-DGuDxV-I.js} +1 -1
- package/.output/public/assets/{index-DCj1X8Qt.css → index-DKwrwHsI.css} +1 -1
- package/.output/public/assets/index-fXi7tXyH.js +76 -0
- package/.output/public/assets/{index-DF6bouqm.js → index-nN3UPNM0.js} +6 -6
- package/.output/public/assets/{json-viewer-0fOrL_Cz.js → json-viewer-CzcwNVJa.js} +1 -1
- package/.output/public/assets/{jszip.min-2Bsoez0Q.js → jszip.min-DAbOCDPI.js} +1 -1
- package/.output/server/_libs/lucide-react.mjs +13 -13
- package/.output/server/_libs/radix-ui__react-select.mjs +7 -7
- package/.output/server/{_sessionId-DzY1bB_L.mjs → _sessionId-DSetUDiv.mjs} +1 -1
- package/.output/server/{_sessionId-D-GcecKN.mjs → _sessionId-DtRKr8Jt.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-BXm8BitS.mjs → CompareDrawer-Cns26sqx.mjs} +3 -3
- package/.output/server/_ssr/{InspectorPet-CZUOr9sO.mjs → InspectorPet-CyWBvBuh.mjs} +1 -1
- package/.output/server/_ssr/{ProxyViewerContainer-BAQY5x_x.mjs → ProxyViewerContainer-CimBv5WL.mjs} +84 -48
- package/.output/server/_ssr/{ReplayDialog-BOODYnLb.mjs → ReplayDialog-DrM897xL.mjs} +4 -4
- package/.output/server/_ssr/{RequestAnatomy-Bd2OBiJP.mjs → RequestAnatomy-BRgezegq.mjs} +2 -2
- package/.output/server/_ssr/{ResponseView-ZSjpdHLG.mjs → ResponseView-D7u7r-lA.mjs} +3 -3
- package/.output/server/_ssr/{StreamingChunkSequence-BT-yQGLL.mjs → StreamingChunkSequence-DM26ZaVC.mjs} +2 -2
- package/.output/server/_ssr/{index-CXouA5fR.mjs → index-BeWedHzY.mjs} +2 -2
- package/.output/server/_ssr/{index-CSJlUfwz.mjs → index-C284XGkZ.mjs} +1 -1
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-DF2oLr4x.mjs → json-viewer-CD_GbR7V.mjs} +3 -3
- package/.output/server/_ssr/{router-75eycvXZ.mjs → router-DxPlMfLZ.mjs} +126 -59
- package/.output/server/{_tanstack-start-manifest_v-DjCobDeN.mjs → _tanstack-start-manifest_v-82w9tFI-.mjs} +1 -1
- package/.output/server/index.mjs +93 -93
- package/.output/workers/logFinalizer.worker.js +6 -1
- package/.output/workers/sessionWorkerEntry.js +6 -1
- package/README.md +42 -10
- package/package.json +1 -1
- package/src/cli/doctor.ts +17 -10
- package/src/cli/networkHints.ts +15 -8
- package/src/cli/onboard.ts +10 -3
- package/src/cli.ts +90 -56
- package/src/components/ProxyViewer.tsx +34 -21
- package/src/components/ProxyViewerContainer.tsx +2 -1
- package/src/components/pi-agent/PiAgentPanel.tsx +2 -1
- package/src/components/providers/SettingsDialog.tsx +24 -7
- package/src/lib/apiClient.ts +9 -2
- package/src/lib/basePath.ts +83 -0
- package/src/lib/sessionUrl.ts +4 -2
- package/src/mcp/loopback.ts +2 -1
- package/src/mcp/server.ts +6 -2
- package/src/proxy/identityProxy.ts +98 -6
- package/src/proxy/privateDataPath.ts +15 -1
- package/src/proxy/sessionInfo.ts +10 -3
- package/src/proxy/upstream.ts +3 -1
- package/src/router.tsx +7 -1
- package/src/routes/__root.tsx +9 -3
- package/.output/public/assets/ProxyViewerContainer-9mjL1YlZ.js +0 -126
- package/.output/public/assets/index-CEdulUJ3.js +0 -76
|
@@ -10,6 +10,7 @@ import { useProviders } from "../../lib/useProviders";
|
|
|
10
10
|
import { useStripConfig } from "../../lib/useStripConfig";
|
|
11
11
|
import { copyTextToClipboard } from "../../lib/clipboard";
|
|
12
12
|
import { fetchJsonWithTimeout } from "../../lib/apiClient";
|
|
13
|
+
import { withBasePath } from "../../lib/basePath";
|
|
13
14
|
import {
|
|
14
15
|
MAX_PROVIDER_TEST_TIMEOUT_SECONDS,
|
|
15
16
|
MAX_SLOW_RESPONSE_THRESHOLD_SECONDS,
|
|
@@ -359,7 +360,11 @@ function OnboardingSettingsTab(): JSX.Element {
|
|
|
359
360
|
value: "agent-inspector onboard --status --opencode-only",
|
|
360
361
|
},
|
|
361
362
|
{ id: "opencode-verify", label: "OpenCode verify", value: "opencode mcp list" },
|
|
362
|
-
{
|
|
363
|
+
{
|
|
364
|
+
id: "opencode-proxy",
|
|
365
|
+
label: "OpenCode proxy",
|
|
366
|
+
value: `LLM_BASE_URL=${origin}${withBasePath("/proxy")}`,
|
|
367
|
+
},
|
|
363
368
|
{
|
|
364
369
|
id: "mimo-config",
|
|
365
370
|
label: "MiMo Code config",
|
|
@@ -371,11 +376,23 @@ function OnboardingSettingsTab(): JSX.Element {
|
|
|
371
376
|
value: "agent-inspector onboard --status --mimo-only",
|
|
372
377
|
},
|
|
373
378
|
{ id: "mimo-verify", label: "MiMo Code verify", value: "mimo mcp list" },
|
|
374
|
-
{
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
+
{
|
|
380
|
+
id: "mimo-proxy",
|
|
381
|
+
label: "MiMo Code proxy",
|
|
382
|
+
value: `OPENAI_BASE_URL=${origin}${withBasePath("/proxy")}`,
|
|
383
|
+
},
|
|
384
|
+
{ id: "mcp", label: "MCP URL", value: `${origin}${withBasePath("/api/mcp")}` },
|
|
385
|
+
{ id: "proxy", label: "Proxy URL", value: `${origin}${withBasePath("/proxy")}` },
|
|
386
|
+
{
|
|
387
|
+
id: "anthropic",
|
|
388
|
+
label: "Anthropic base",
|
|
389
|
+
value: `ANTHROPIC_BASE_URL=${origin}${withBasePath("/proxy")}`,
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
id: "openai",
|
|
393
|
+
label: "OpenAI base",
|
|
394
|
+
value: `OPENAI_BASE_URL=${origin}${withBasePath("/proxy")}`,
|
|
395
|
+
},
|
|
379
396
|
],
|
|
380
397
|
[origin],
|
|
381
398
|
);
|
|
@@ -479,7 +496,7 @@ function McpSettingsTab(): JSX.Element {
|
|
|
479
496
|
if (typeof window === "undefined") return "http://localhost:9527";
|
|
480
497
|
return window.location.origin;
|
|
481
498
|
}, []);
|
|
482
|
-
const endpoint = `${origin}/api/mcp`;
|
|
499
|
+
const endpoint = `${origin}${withBasePath("/api/mcp")}`;
|
|
483
500
|
const values = useMemo(
|
|
484
501
|
() => [
|
|
485
502
|
{ id: "mcp-url", label: "Streamable HTTP MCP URL", value: endpoint },
|
package/src/lib/apiClient.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* eslint-disable functional/no-throw-statements */
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { withBasePath } from "./basePath";
|
|
3
4
|
|
|
4
5
|
const ApiErrorSchema = z.object({
|
|
5
6
|
error: z.string(),
|
|
@@ -7,6 +8,12 @@ const ApiErrorSchema = z.object({
|
|
|
7
8
|
|
|
8
9
|
const MUTATION_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
9
10
|
|
|
11
|
+
function withInspectorBasePath(input: RequestInfo | URL): RequestInfo | URL {
|
|
12
|
+
if (typeof input !== "string") return input;
|
|
13
|
+
if (!input.startsWith("/") || input.startsWith("//")) return input;
|
|
14
|
+
return withBasePath(input);
|
|
15
|
+
}
|
|
16
|
+
|
|
10
17
|
export function withBrowserMutationProof(init?: RequestInit): RequestInit | undefined {
|
|
11
18
|
const method = init?.method?.toUpperCase();
|
|
12
19
|
if (method === undefined || !MUTATION_METHODS.has(method)) return init;
|
|
@@ -52,7 +59,7 @@ export async function fetchJson<T>(
|
|
|
52
59
|
init?: RequestInit,
|
|
53
60
|
errorFallback?: (response: Response) => string,
|
|
54
61
|
): Promise<T> {
|
|
55
|
-
const response = await fetch(input, withBrowserMutationProof(init));
|
|
62
|
+
const response = await fetch(withInspectorBasePath(input), withBrowserMutationProof(init));
|
|
56
63
|
if (!response.ok) {
|
|
57
64
|
const fallback = errorFallback?.(response) ?? `Request failed with status ${response.status}`;
|
|
58
65
|
throw new Error(await readApiError(response, fallback));
|
|
@@ -100,7 +107,7 @@ export async function fetchWithTimeout(
|
|
|
100
107
|
}
|
|
101
108
|
|
|
102
109
|
try {
|
|
103
|
-
return await fetch(input, {
|
|
110
|
+
return await fetch(withInspectorBasePath(input), {
|
|
104
111
|
...withBrowserMutationProof(init),
|
|
105
112
|
signal: controller.signal,
|
|
106
113
|
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
export const AGENT_INSPECTOR_BASE_PATH_ENV = "AGENT_INSPECTOR_BASE_PATH";
|
|
4
|
+
export const AGENT_INSPECTOR_INTERNAL_STRIPPED_BASE_PATH_ENV =
|
|
5
|
+
"AGENT_INSPECTOR_INTERNAL_STRIPPED_BASE_PATH";
|
|
6
|
+
export const AGENT_INSPECTOR_BASE_PATH_META_NAME = "agent-inspector-base-path";
|
|
7
|
+
export const DEFAULT_AGENT_INSPECTOR_BASE_PATH = "/inspector";
|
|
8
|
+
|
|
9
|
+
type BasePathEnv = Readonly<Record<string, string | undefined>>;
|
|
10
|
+
|
|
11
|
+
function trimSlashes(value: string): string {
|
|
12
|
+
return value.replace(/^\/+/u, "").replace(/\/+$/u, "");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function normalizeBasePath(value: string | undefined): string {
|
|
16
|
+
const trimmed = value?.trim();
|
|
17
|
+
if (trimmed === undefined || trimmed.length === 0 || trimmed === "/") return "";
|
|
18
|
+
const path = trimSlashes(trimmed);
|
|
19
|
+
if (path.length === 0) return "";
|
|
20
|
+
return `/${path}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readDocumentBasePath(): string | undefined {
|
|
24
|
+
if (typeof document === "undefined") return undefined;
|
|
25
|
+
return (
|
|
26
|
+
document
|
|
27
|
+
.querySelector(`meta[name="${AGENT_INSPECTOR_BASE_PATH_META_NAME}"]`)
|
|
28
|
+
?.getAttribute("content") ?? undefined
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readProcessBasePath(): string | undefined {
|
|
33
|
+
if (typeof process === "undefined") return undefined;
|
|
34
|
+
return process.env[AGENT_INSPECTOR_BASE_PATH_ENV];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getConfiguredBasePath(env?: BasePathEnv): string {
|
|
38
|
+
return normalizeBasePath(
|
|
39
|
+
env?.[AGENT_INSPECTOR_BASE_PATH_ENV] ??
|
|
40
|
+
readProcessBasePath() ??
|
|
41
|
+
readDocumentBasePath() ??
|
|
42
|
+
import.meta.env?.BASE_URL ??
|
|
43
|
+
DEFAULT_AGENT_INSPECTOR_BASE_PATH,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getRouterBasePath(env?: BasePathEnv): string {
|
|
48
|
+
if (env?.[AGENT_INSPECTOR_INTERNAL_STRIPPED_BASE_PATH_ENV] === "1") return "";
|
|
49
|
+
return getConfiguredBasePath(env);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function basePathForVite(basePath: string): string {
|
|
53
|
+
const normalized = normalizeBasePath(basePath);
|
|
54
|
+
return normalized.length === 0 ? "/" : `${normalized}/`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function withBasePath(path: string, basePath = getConfiguredBasePath()): string {
|
|
58
|
+
const normalized = normalizeBasePath(basePath);
|
|
59
|
+
if (/^[a-z][a-z0-9+.-]*:/iu.test(path)) return path;
|
|
60
|
+
if (normalized.length === 0) return path;
|
|
61
|
+
if (path.length === 0 || path === "/") return normalized;
|
|
62
|
+
if (path.startsWith(`${normalized}/`) || path === normalized) return path;
|
|
63
|
+
return path.startsWith("/") ? `${normalized}${path}` : `${normalized}/${path}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function stripBasePath(pathname: string, basePath = getConfiguredBasePath()): string {
|
|
67
|
+
const normalized = normalizeBasePath(basePath);
|
|
68
|
+
if (normalized.length === 0) return pathname;
|
|
69
|
+
if (pathname === normalized) return "/";
|
|
70
|
+
if (pathname.startsWith(`${normalized}/`)) return pathname.slice(normalized.length);
|
|
71
|
+
return pathname;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function appendBasePathToOrigin(origin: string, path: string, basePath: string): string {
|
|
75
|
+
const normalized = normalizeBasePath(basePath);
|
|
76
|
+
if (normalized.length === 0 && path === "/") return origin;
|
|
77
|
+
return `${origin}${withBasePath(path, normalized)}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function stripBasePathFromRequestPath(rawPath: string, basePath: string): string {
|
|
81
|
+
const url = new URL(rawPath, "http://localhost");
|
|
82
|
+
return `${stripBasePath(url.pathname, basePath)}${url.search}`;
|
|
83
|
+
}
|
package/src/lib/sessionUrl.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { withBasePath } from "./basePath";
|
|
2
|
+
|
|
1
3
|
const B64URL_RE = /^[A-Za-z0-9_-]+$/;
|
|
2
4
|
|
|
3
5
|
function bytesToBinary(bytes: Uint8Array): string {
|
|
@@ -39,6 +41,6 @@ export function decodeSessionIdFromPath(encoded: string): string {
|
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
export function getSessionPath(sessionId: string): string {
|
|
43
|
-
return `/session/${encodeSessionIdForPath(sessionId)}
|
|
44
|
+
export function getSessionPath(sessionId: string, basePath?: string): string {
|
|
45
|
+
return withBasePath(`/session/${encodeSessionIdForPath(sessionId)}`, basePath);
|
|
44
46
|
}
|
package/src/mcp/loopback.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* — should stay identical so callers don't change.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
import { getConfiguredBasePath, withBasePath } from "../lib/basePath";
|
|
19
20
|
import { getCurrentPort } from "../lib/serverPort";
|
|
20
21
|
|
|
21
22
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
@@ -47,7 +48,7 @@ export async function callApi(path: string, options: CallApiOptions = {}): Promi
|
|
|
47
48
|
throw new Error(`callApi: path must start with '/', got: ${path}`);
|
|
48
49
|
}
|
|
49
50
|
const port = getCurrentPort();
|
|
50
|
-
const url = `http://127.0.0.1:${port}${path}`;
|
|
51
|
+
const url = `http://127.0.0.1:${port}${withBasePath(path, getConfiguredBasePath(process.env))}`;
|
|
51
52
|
|
|
52
53
|
const { timeoutMs = DEFAULT_TIMEOUT_MS, signal: userSignal, ...rest } = options;
|
|
53
54
|
|
package/src/mcp/server.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
} from "../lib/groupContract";
|
|
35
35
|
import { InspectorRunStatusSchema, UpdateInspectorRunInputSchema } from "../lib/runContract";
|
|
36
36
|
import { getCurrentPort, setCurrentPort } from "../lib/serverPort";
|
|
37
|
+
import { getConfiguredBasePath, withBasePath } from "../lib/basePath";
|
|
37
38
|
import { JsonValueSchema } from "../contracts";
|
|
38
39
|
import {
|
|
39
40
|
MAX_PROVIDER_MODEL_NAME_CHARS,
|
|
@@ -326,7 +327,10 @@ function jsonResource(uri: URL, data: unknown): ReadResourceResult {
|
|
|
326
327
|
}
|
|
327
328
|
|
|
328
329
|
function mcpEndpointUrl(): string {
|
|
329
|
-
return `http://localhost:${String(getCurrentPort())}
|
|
330
|
+
return `http://localhost:${String(getCurrentPort())}${withBasePath(
|
|
331
|
+
"/api/mcp",
|
|
332
|
+
getConfiguredBasePath(process.env),
|
|
333
|
+
)}`;
|
|
330
334
|
}
|
|
331
335
|
|
|
332
336
|
function mcpHealth(): Record<string, unknown> {
|
|
@@ -338,7 +342,7 @@ function mcpHealth(): Record<string, unknown> {
|
|
|
338
342
|
},
|
|
339
343
|
transport: {
|
|
340
344
|
primary: "streamable-http",
|
|
341
|
-
endpoint: "/api/mcp",
|
|
345
|
+
endpoint: withBasePath("/api/mcp", getConfiguredBasePath(process.env)),
|
|
342
346
|
stateless: true,
|
|
343
347
|
},
|
|
344
348
|
mode: getMcpModeInfo(),
|
|
@@ -2,6 +2,13 @@ import http from "node:http";
|
|
|
2
2
|
import { Buffer } from "node:buffer";
|
|
3
3
|
import { timingSafeEqual } from "node:crypto";
|
|
4
4
|
import { isIP } from "node:net";
|
|
5
|
+
import {
|
|
6
|
+
AGENT_INSPECTOR_BASE_PATH_META_NAME,
|
|
7
|
+
getConfiguredBasePath,
|
|
8
|
+
normalizeBasePath,
|
|
9
|
+
stripBasePath,
|
|
10
|
+
stripBasePathFromRequestPath,
|
|
11
|
+
} from "../lib/basePath";
|
|
5
12
|
import { lookupClientByPort } from "./socketTracker";
|
|
6
13
|
|
|
7
14
|
const IDENTITY_HEADERS = {
|
|
@@ -68,6 +75,43 @@ const DOCUMENT_CONTENT_SECURITY_POLICY = [
|
|
|
68
75
|
"worker-src 'self' blob:",
|
|
69
76
|
].join("; ");
|
|
70
77
|
|
|
78
|
+
function htmlAttributeEscape(value: string): string {
|
|
79
|
+
return value
|
|
80
|
+
.replace(/&/gu, "&")
|
|
81
|
+
.replace(/"/gu, """)
|
|
82
|
+
.replace(/</gu, "<")
|
|
83
|
+
.replace(/>/gu, ">");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function rewriteHtmlForBasePath(html: string, basePath: string): string {
|
|
87
|
+
const normalized = normalizeBasePath(basePath);
|
|
88
|
+
if (normalized.length === 0) return html;
|
|
89
|
+
const escaped = htmlAttributeEscape(normalized);
|
|
90
|
+
const meta = `<meta name="${AGENT_INSPECTOR_BASE_PATH_META_NAME}" content="${escaped}">`;
|
|
91
|
+
const withMeta = html.includes(`name="${AGENT_INSPECTOR_BASE_PATH_META_NAME}"`)
|
|
92
|
+
? html
|
|
93
|
+
: html.replace(/<head(\s[^>]*)?>/iu, (match) => `${match}${meta}`);
|
|
94
|
+
const escapedBase = normalized.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
95
|
+
return withMeta.replace(
|
|
96
|
+
new RegExp(`\\b(href|src|action)=("|')/(?!/|${escapedBase.slice(1)}(?:/|$))`, "giu"),
|
|
97
|
+
(_match: string, attr: string, quote: string) => `${attr}=${quote}${normalized}/`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function javaScriptStringEscape(value: string): string {
|
|
102
|
+
return value.replace(/\\/gu, "\\\\").replace(/"/gu, '\\"');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function rewriteJavaScriptForBasePath(source: string, basePath: string): string {
|
|
106
|
+
const normalized = normalizeBasePath(basePath);
|
|
107
|
+
if (normalized.length === 0) return source;
|
|
108
|
+
const escaped = javaScriptStringEscape(normalized);
|
|
109
|
+
return source
|
|
110
|
+
.replace(/\bbasepath:\s*""/gu, `basepath:"${escaped}"`)
|
|
111
|
+
.replace(/return"\/"\+([A-Za-z_$][\w$]*)/gu, `return"${escaped}/"+$1`)
|
|
112
|
+
.replace(/(["'])\/assets\//gu, `$1${normalized}/assets/`);
|
|
113
|
+
}
|
|
114
|
+
|
|
71
115
|
export type IdentityProxyOptions = {
|
|
72
116
|
/** Public port to listen on. Clients point at this. */
|
|
73
117
|
listenPort: number;
|
|
@@ -159,7 +203,8 @@ function isSafeMethod(method: string | undefined): boolean {
|
|
|
159
203
|
}
|
|
160
204
|
|
|
161
205
|
function isPublicHealthPath(pathname: string): boolean {
|
|
162
|
-
|
|
206
|
+
const path = stripBasePath(pathname, getConfiguredBasePath(process.env));
|
|
207
|
+
return path === "/livez" || path === "/readyz" || path === "/api/health";
|
|
163
208
|
}
|
|
164
209
|
|
|
165
210
|
function configuredPositiveInteger(value: number | undefined, fallback: number): number {
|
|
@@ -182,7 +227,10 @@ export function isBrowserMutationAllowed(input: {
|
|
|
182
227
|
csrfToken?: string;
|
|
183
228
|
}): boolean {
|
|
184
229
|
if (isSafeMethod(input.method)) return true;
|
|
185
|
-
const pathname =
|
|
230
|
+
const pathname = stripBasePath(
|
|
231
|
+
new URL(input.path, "http://localhost").pathname,
|
|
232
|
+
getConfiguredBasePath(process.env),
|
|
233
|
+
);
|
|
186
234
|
if (pathname === "/proxy" || pathname.startsWith("/proxy/")) return true;
|
|
187
235
|
|
|
188
236
|
const fetchSite = input.secFetchSite?.toLowerCase();
|
|
@@ -219,7 +267,10 @@ export function isIdentityProxyRequestAllowed(input: {
|
|
|
219
267
|
if (input.options.allowRemoteControl === true) return true;
|
|
220
268
|
if (isLoopbackPeer(input.remoteAddress)) return true;
|
|
221
269
|
|
|
222
|
-
const pathname =
|
|
270
|
+
const pathname = stripBasePath(
|
|
271
|
+
new URL(input.path, "http://localhost").pathname,
|
|
272
|
+
getConfiguredBasePath(process.env),
|
|
273
|
+
);
|
|
223
274
|
if (pathname === "/proxy" || pathname.startsWith("/proxy/")) {
|
|
224
275
|
if (input.options.allowUnauthenticatedRemoteProxy === true) return true;
|
|
225
276
|
const proxyCredential = bearerToken(input.proxyAuthorization) ?? input.proxyTokenHeader ?? null;
|
|
@@ -349,6 +400,9 @@ async function handleRequest(
|
|
|
349
400
|
res: http.ServerResponse,
|
|
350
401
|
options: IdentityProxyOptions,
|
|
351
402
|
): Promise<void> {
|
|
403
|
+
const basePath = getConfiguredBasePath(process.env);
|
|
404
|
+
const upstreamPath = stripBasePathFromRequestPath(req.url ?? "/", basePath);
|
|
405
|
+
const requestUrl = new URL(req.url ?? "/", "http://localhost");
|
|
352
406
|
if (!isTrustedIdentityProxyHost(req.headers.host, options)) {
|
|
353
407
|
res.writeHead(400, {
|
|
354
408
|
"content-type": "application/json; charset=utf-8",
|
|
@@ -359,6 +413,16 @@ async function handleRequest(
|
|
|
359
413
|
return;
|
|
360
414
|
}
|
|
361
415
|
|
|
416
|
+
if (basePath.length > 0 && requestUrl.pathname.startsWith("/assets/")) {
|
|
417
|
+
res.writeHead(307, {
|
|
418
|
+
location: `${basePath}${requestUrl.pathname}${requestUrl.search}`,
|
|
419
|
+
"cache-control": "no-store",
|
|
420
|
+
});
|
|
421
|
+
res.end();
|
|
422
|
+
req.resume();
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
362
426
|
if (
|
|
363
427
|
!isBrowserMutationAllowed({
|
|
364
428
|
path: req.url ?? "/",
|
|
@@ -443,6 +507,7 @@ async function handleRequest(
|
|
|
443
507
|
for (const [name, value] of Object.entries(req.headers)) {
|
|
444
508
|
const lowerName = name.toLowerCase();
|
|
445
509
|
if (REQUEST_HOP_BY_HOP.has(lowerName)) continue;
|
|
510
|
+
if (basePath.length > 0 && lowerName === "accept-encoding") continue;
|
|
446
511
|
if (lowerName.startsWith(IDENTITY_HEADER_PREFIX)) continue;
|
|
447
512
|
if (value === undefined) continue;
|
|
448
513
|
upstreamHeaders[name] = value;
|
|
@@ -469,7 +534,7 @@ async function handleRequest(
|
|
|
469
534
|
{
|
|
470
535
|
hostname: options.upstreamHost,
|
|
471
536
|
port: options.upstreamPort,
|
|
472
|
-
path:
|
|
537
|
+
path: upstreamPath,
|
|
473
538
|
method: req.method,
|
|
474
539
|
headers: upstreamHeaders,
|
|
475
540
|
},
|
|
@@ -479,19 +544,46 @@ async function handleRequest(
|
|
|
479
544
|
return;
|
|
480
545
|
}
|
|
481
546
|
const outHeaders: Record<string, string | string[]> = {};
|
|
547
|
+
const contentType = firstHeader(proxyRes.headers["content-type"]);
|
|
548
|
+
const shouldRewriteHtml =
|
|
549
|
+
basePath.length > 0 && contentType?.toLowerCase().includes("text/html") === true;
|
|
550
|
+
const shouldRewriteJavaScript =
|
|
551
|
+
basePath.length > 0 &&
|
|
552
|
+
(contentType?.toLowerCase().includes("javascript") === true ||
|
|
553
|
+
upstreamPath.split("?", 1)[0]?.endsWith(".js") === true);
|
|
554
|
+
const shouldRewriteBody = shouldRewriteHtml || shouldRewriteJavaScript;
|
|
482
555
|
for (const [name, value] of Object.entries(proxyRes.headers)) {
|
|
483
|
-
|
|
556
|
+
const lowerName = name.toLowerCase();
|
|
557
|
+
if (RESPONSE_HOP_BY_HOP.has(lowerName)) continue;
|
|
558
|
+
if (shouldRewriteBody && lowerName === "content-length") continue;
|
|
484
559
|
if (value === undefined) continue;
|
|
485
560
|
outHeaders[name] = value;
|
|
486
561
|
}
|
|
487
562
|
for (const [name, value] of Object.entries(BASE_SECURITY_HEADERS)) {
|
|
488
563
|
outHeaders[name] = value;
|
|
489
564
|
}
|
|
490
|
-
const contentType = firstHeader(proxyRes.headers["content-type"]);
|
|
491
565
|
if (contentType?.toLowerCase().includes("text/html") === true) {
|
|
492
566
|
outHeaders["content-security-policy"] = DOCUMENT_CONTENT_SECURITY_POLICY;
|
|
493
567
|
}
|
|
494
568
|
res.writeHead(proxyRes.statusCode ?? 502, outHeaders);
|
|
569
|
+
if (shouldRewriteBody) {
|
|
570
|
+
const chunks: Buffer[] = [];
|
|
571
|
+
proxyRes.on("data", (chunk: unknown) => {
|
|
572
|
+
chunks.push(toBuffer(chunk));
|
|
573
|
+
});
|
|
574
|
+
proxyRes.on("end", () => {
|
|
575
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
576
|
+
res.end(
|
|
577
|
+
shouldRewriteHtml
|
|
578
|
+
? rewriteHtmlForBasePath(body, basePath)
|
|
579
|
+
: rewriteJavaScriptForBasePath(body, basePath),
|
|
580
|
+
);
|
|
581
|
+
});
|
|
582
|
+
proxyRes.on("error", () => {
|
|
583
|
+
if (!res.writableEnded) res.destroy();
|
|
584
|
+
});
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
495
587
|
proxyRes.pipe(res);
|
|
496
588
|
},
|
|
497
589
|
);
|
|
@@ -21,8 +21,22 @@ const securedPaths = new Set<string>();
|
|
|
21
21
|
const securedFileIdentities = new Map<string, string>();
|
|
22
22
|
let cachedWindowsSid: string | null = null;
|
|
23
23
|
|
|
24
|
+
// Always invoke the system copies of `whoami.exe` / `icacls.exe` regardless
|
|
25
|
+
// of PATH ordering. Git Bash and similar shells ship stub binaries (e.g.
|
|
26
|
+
// `/usr/bin/whoami.exe`) that take precedence on the inherited PATH and do
|
|
27
|
+
// not understand Windows ACL arguments, which makes `ensurePrivate*` fail
|
|
28
|
+
// in environments where `C:\Windows\System32` is not first.
|
|
29
|
+
function windowsSystemPath(executable: string): string {
|
|
30
|
+
const systemRoot = process.env["SystemRoot"] ?? "C:\\Windows";
|
|
31
|
+
return `${systemRoot}\\System32\\${executable}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
24
34
|
function runWindowsCommand(executable: string, args: readonly string[]): WindowsCommandResult {
|
|
25
|
-
const
|
|
35
|
+
const resolved =
|
|
36
|
+
process.platform === "win32" && /^(whoami|icacls)\.exe$/iu.test(executable)
|
|
37
|
+
? windowsSystemPath(executable)
|
|
38
|
+
: executable;
|
|
39
|
+
const result = spawnSync(resolved, [...args], {
|
|
26
40
|
encoding: "utf8",
|
|
27
41
|
shell: false,
|
|
28
42
|
windowsHide: true,
|
package/src/proxy/sessionInfo.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SessionInfo, SessionLogSummary, SessionTokenUsage } from "../lib/sessionInfoContract";
|
|
2
|
+
import { getConfiguredBasePath, withBasePath } from "../lib/basePath";
|
|
2
3
|
import { getSessionPath } from "../lib/sessionUrl";
|
|
3
4
|
import type { CapturedLog } from "./schemas";
|
|
4
5
|
import type { SessionSnapshot } from "./sessionSupervisor";
|
|
@@ -104,11 +105,17 @@ function buildAbsoluteUrl(baseUrl: string, path: string): string {
|
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
function buildApiPath(sessionId: string): string {
|
|
107
|
-
return buildRelativePath(
|
|
108
|
+
return buildRelativePath(
|
|
109
|
+
withBasePath("/api/sessions", getConfiguredBasePath(process.env)),
|
|
110
|
+
new URLSearchParams({ sessionId }),
|
|
111
|
+
);
|
|
108
112
|
}
|
|
109
113
|
|
|
110
114
|
function buildCompactLogsPath(sessionId: string): string {
|
|
111
|
-
return buildRelativePath(
|
|
115
|
+
return buildRelativePath(
|
|
116
|
+
withBasePath("/api/logs", getConfiguredBasePath(process.env)),
|
|
117
|
+
new URLSearchParams({ compact: "1", sessionId }),
|
|
118
|
+
);
|
|
112
119
|
}
|
|
113
120
|
|
|
114
121
|
function collectSortedValues(values: readonly (string | null)[]): string[] {
|
|
@@ -204,7 +211,7 @@ export function buildSessionInfo(input: BuildSessionInfoInput): SessionInfo {
|
|
|
204
211
|
const queuedTasks = input.snapshot?.queuedTasks ?? 0;
|
|
205
212
|
const runningTasks = input.snapshot?.runningTasks ?? 0;
|
|
206
213
|
const lastTaskError = normalizeString(input.snapshot?.lastTaskError);
|
|
207
|
-
const inspectorPath = getSessionPath(input.sessionId);
|
|
214
|
+
const inspectorPath = getSessionPath(input.sessionId, getConfiguredBasePath(process.env));
|
|
208
215
|
const apiPath = buildApiPath(input.sessionId);
|
|
209
216
|
const compactLogsPath = buildCompactLogsPath(input.sessionId);
|
|
210
217
|
const latestLogs = summaries.slice(0, latestLogLimit).map((summary, index) => ({
|
package/src/proxy/upstream.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ProviderConfig } from "./providers";
|
|
2
|
+
import { getConfiguredBasePath, stripBasePath } from "../lib/basePath";
|
|
2
3
|
import { logger } from "./logger";
|
|
3
4
|
import {
|
|
4
5
|
AUTH_HEADER_X_API_KEY,
|
|
@@ -41,7 +42,8 @@ export function describeApiRoute(apiPath: string): ApiRoute {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
export function getProxyApiPath(url: URL): string {
|
|
44
|
-
|
|
45
|
+
const pathname = stripBasePath(url.pathname, getConfiguredBasePath(process.env));
|
|
46
|
+
return pathname.replace(/^\/proxy/, "") + url.search;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
export function selectUpstreamBase(route: ApiRoute, provider: ProviderConfig | null): string {
|
package/src/router.tsx
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { createRouter } from "@tanstack/react-router";
|
|
2
|
+
import { getRouterBasePath } from "./lib/basePath";
|
|
2
3
|
import { routeTree } from "./routeTree.gen";
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
function defaultRouterBasePath(): string {
|
|
6
|
+
return getRouterBasePath(typeof process === "undefined" ? undefined : process.env);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getRouter(basepath = defaultRouterBasePath()) {
|
|
5
10
|
const router = createRouter({
|
|
6
11
|
routeTree,
|
|
12
|
+
basepath,
|
|
7
13
|
scrollRestoration: false,
|
|
8
14
|
});
|
|
9
15
|
attachServerSsrStoreCompatibility(router);
|
package/src/routes/__root.tsx
CHANGED
|
@@ -5,6 +5,11 @@ import { SWRConfig } from "swr";
|
|
|
5
5
|
import faviconSvg from "../assets/favicon.svg?url";
|
|
6
6
|
import { SafeErrorBoundary, SafeRouteError } from "../components/errors/SafeErrorBoundary";
|
|
7
7
|
import appCss from "../index.css?url";
|
|
8
|
+
import {
|
|
9
|
+
AGENT_INSPECTOR_BASE_PATH_META_NAME,
|
|
10
|
+
getConfiguredBasePath,
|
|
11
|
+
withBasePath,
|
|
12
|
+
} from "../lib/basePath";
|
|
8
13
|
import { installChunkLoadRecovery } from "../lib/chunkLoadRecovery";
|
|
9
14
|
import { THEME_MODE_STORAGE_KEY } from "../lib/themeMode";
|
|
10
15
|
import {
|
|
@@ -87,11 +92,12 @@ export const Route = createRootRoute({
|
|
|
87
92
|
meta: [
|
|
88
93
|
{ charSet: "utf-8" },
|
|
89
94
|
{ name: "viewport", content: "width=device-width, initial-scale=1" },
|
|
95
|
+
{ name: AGENT_INSPECTOR_BASE_PATH_META_NAME, content: getConfiguredBasePath() },
|
|
90
96
|
{ title: "Agent Inspector" },
|
|
91
97
|
],
|
|
92
98
|
links: [
|
|
93
|
-
{ rel: "stylesheet", href: appCss },
|
|
94
|
-
{ rel: "icon", type: "image/svg+xml", href: faviconSvg },
|
|
99
|
+
{ rel: "stylesheet", href: withBasePath(appCss) },
|
|
100
|
+
{ rel: "icon", type: "image/svg+xml", href: withBasePath(faviconSvg) },
|
|
95
101
|
],
|
|
96
102
|
}),
|
|
97
103
|
component: RootComponent,
|
|
@@ -138,7 +144,7 @@ function RootNotFoundComponent() {
|
|
|
138
144
|
This route is not part of the Agent Inspector UI or API surface.
|
|
139
145
|
</p>
|
|
140
146
|
<a
|
|
141
|
-
href="/"
|
|
147
|
+
href={withBasePath("/")}
|
|
142
148
|
className="bg-primary text-primary-foreground hover:bg-primary/90 mt-6 inline-flex h-9 items-center justify-center rounded-md px-4 text-sm font-medium transition-colors"
|
|
143
149
|
>
|
|
144
150
|
Open Agent Inspector
|