@tonyclaw/agent-inspector 3.0.38 → 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.
Files changed (58) hide show
  1. package/.output/cli.js +249 -84
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/{CompareDrawer-BXQWabQJ.js → CompareDrawer-BjQ2HH8j.js} +1 -1
  4. package/.output/public/assets/{InspectorPet-CpQEXKcr.js → InspectorPet-DDScgJ7_.js} +1 -1
  5. package/.output/public/assets/ProxyViewerContainer-C3iHhtkc.js +126 -0
  6. package/.output/public/assets/{ReplayDialog-CnYt4UKE.js → ReplayDialog-BujXVTMJ.js} +1 -1
  7. package/.output/public/assets/{RequestAnatomy-BI25al3m.js → RequestAnatomy-iK2nOuCP.js} +1 -1
  8. package/.output/public/assets/{ResponseView-D3y-I9nb.js → ResponseView-iAuOJxem.js} +1 -1
  9. package/.output/public/assets/{StreamingChunkSequence-n90dYzPp.js → StreamingChunkSequence-DiZ21icS.js} +1 -1
  10. package/.output/public/assets/{_sessionId-DLbOYm3h.js → _sessionId-CLTlZx8L.js} +1 -1
  11. package/.output/public/assets/{_sessionId-CogrNhCS.js → _sessionId-D3WwDIit.js} +1 -1
  12. package/.output/public/assets/{index-I5zN_KrA.js → index--ND7WRCU.js} +1 -1
  13. package/.output/public/assets/{index-DXGp27k9.js → index-DGuDxV-I.js} +1 -1
  14. package/.output/public/assets/index-fXi7tXyH.js +76 -0
  15. package/.output/public/assets/{index-BWWA1Rf6.js → index-nN3UPNM0.js} +6 -6
  16. package/.output/public/assets/{json-viewer-BoB2BfHg.js → json-viewer-CzcwNVJa.js} +1 -1
  17. package/.output/public/assets/{jszip.min-CAXkxQd6.js → jszip.min-DAbOCDPI.js} +1 -1
  18. package/.output/server/{_sessionId-_hORwhWH.mjs → _sessionId-DSetUDiv.mjs} +1 -1
  19. package/.output/server/{_sessionId-DScqlEFC.mjs → _sessionId-DtRKr8Jt.mjs} +2 -2
  20. package/.output/server/_ssr/{CompareDrawer-BtGm5i_L.mjs → CompareDrawer-Cns26sqx.mjs} +2 -2
  21. package/.output/server/_ssr/{InspectorPet-BnN6lVqM.mjs → InspectorPet-CyWBvBuh.mjs} +1 -1
  22. package/.output/server/_ssr/{ProxyViewerContainer-aduEJ_Xy.mjs → ProxyViewerContainer-CimBv5WL.mjs} +58 -23
  23. package/.output/server/_ssr/{ReplayDialog-BSGZ-TFs.mjs → ReplayDialog-DrM897xL.mjs} +3 -3
  24. package/.output/server/_ssr/{RequestAnatomy-DN_eSnDN.mjs → RequestAnatomy-BRgezegq.mjs} +2 -2
  25. package/.output/server/_ssr/{ResponseView-Dk9Td_HM.mjs → ResponseView-D7u7r-lA.mjs} +2 -2
  26. package/.output/server/_ssr/{StreamingChunkSequence-DthH8Lx4.mjs → StreamingChunkSequence-DM26ZaVC.mjs} +2 -2
  27. package/.output/server/_ssr/{index-CqeZgFFF.mjs → index-BeWedHzY.mjs} +2 -2
  28. package/.output/server/_ssr/{index-DvVHL942.mjs → index-C284XGkZ.mjs} +1 -1
  29. package/.output/server/_ssr/index.mjs +2 -2
  30. package/.output/server/_ssr/{json-viewer-DLKA2jUq.mjs → json-viewer-CD_GbR7V.mjs} +2 -2
  31. package/.output/server/_ssr/{router-CRKEDn6n.mjs → router-DxPlMfLZ.mjs} +125 -58
  32. package/.output/server/{_tanstack-start-manifest_v-pQO9kTmV.mjs → _tanstack-start-manifest_v-82w9tFI-.mjs} +1 -1
  33. package/.output/server/index.mjs +84 -84
  34. package/.output/workers/logFinalizer.worker.js +6 -1
  35. package/.output/workers/sessionWorkerEntry.js +6 -1
  36. package/README.md +42 -10
  37. package/package.json +1 -1
  38. package/src/cli/doctor.ts +17 -10
  39. package/src/cli/networkHints.ts +15 -8
  40. package/src/cli/onboard.ts +10 -3
  41. package/src/cli.ts +90 -56
  42. package/src/components/ProxyViewer.tsx +11 -2
  43. package/src/components/ProxyViewerContainer.tsx +2 -1
  44. package/src/components/pi-agent/PiAgentPanel.tsx +2 -1
  45. package/src/components/providers/SettingsDialog.tsx +24 -7
  46. package/src/lib/apiClient.ts +9 -2
  47. package/src/lib/basePath.ts +83 -0
  48. package/src/lib/sessionUrl.ts +4 -2
  49. package/src/mcp/loopback.ts +2 -1
  50. package/src/mcp/server.ts +6 -2
  51. package/src/proxy/identityProxy.ts +98 -6
  52. package/src/proxy/privateDataPath.ts +15 -1
  53. package/src/proxy/sessionInfo.ts +10 -3
  54. package/src/proxy/upstream.ts +3 -1
  55. package/src/router.tsx +7 -1
  56. package/src/routes/__root.tsx +9 -3
  57. package/.output/public/assets/ProxyViewerContainer-8pNJsCJ5.js +0 -126
  58. package/.output/public/assets/index-Ca0grOxW.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
- { id: "opencode-proxy", label: "OpenCode proxy", value: `LLM_BASE_URL=${origin}/proxy` },
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
- { id: "mimo-proxy", label: "MiMo Code proxy", value: `OPENAI_BASE_URL=${origin}/proxy` },
375
- { id: "mcp", label: "MCP URL", value: `${origin}/api/mcp` },
376
- { id: "proxy", label: "Proxy URL", value: `${origin}/proxy` },
377
- { id: "anthropic", label: "Anthropic base", value: `ANTHROPIC_BASE_URL=${origin}/proxy` },
378
- { id: "openai", label: "OpenAI base", value: `OPENAI_BASE_URL=${origin}/proxy` },
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 },
@@ -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
+ }
@@ -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
  }
@@ -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())}/api/mcp`;
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, "&amp;")
81
+ .replace(/"/gu, "&quot;")
82
+ .replace(/</gu, "&lt;")
83
+ .replace(/>/gu, "&gt;");
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
- return pathname === "/livez" || pathname === "/readyz" || pathname === "/api/health";
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 = new URL(input.path, "http://localhost").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 = new URL(input.path, "http://localhost").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: req.url ?? "/",
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
- if (RESPONSE_HOP_BY_HOP.has(name.toLowerCase())) continue;
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 result = spawnSync(executable, [...args], {
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,
@@ -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("/api/sessions", new URLSearchParams({ sessionId }));
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("/api/logs", new URLSearchParams({ compact: "1", sessionId }));
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) => ({
@@ -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
- return url.pathname.replace(/^\/proxy/, "") + url.search;
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
- export function getRouter() {
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);
@@ -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