@tonyclaw/agent-inspector 3.0.43 → 3.0.45

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 (80) hide show
  1. package/.output/cli.js +20744 -16945
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/{CompareDrawer-BAXxwTwT.js → CompareDrawer-B3_TLTJh.js} +1 -1
  4. package/.output/public/assets/{InspectorPet-xYuGV4Qz.js → InspectorPet-Cpn6hv4P.js} +1 -1
  5. package/.output/public/assets/{ProxyViewerContainer-B6GZnUWi.js → ProxyViewerContainer-BEAhuGOJ.js} +27 -27
  6. package/.output/public/assets/{ReplayDialog-D73pyIg3.js → ReplayDialog-B3EKVdJk.js} +1 -1
  7. package/.output/public/assets/{RequestAnatomy-CSDta_nu.js → RequestAnatomy-COOQjyUC.js} +1 -1
  8. package/.output/public/assets/{ResponseView-C4aIUPby.js → ResponseView-DBCjkwye.js} +1 -1
  9. package/.output/public/assets/{StreamingChunkSequence-D4xxosLx.js → StreamingChunkSequence-CuXhDgMo.js} +1 -1
  10. package/.output/public/assets/{_sessionId-CZHbJyWO.js → _sessionId-BJlM3nmt.js} +1 -1
  11. package/.output/public/assets/{_sessionId-DvuNLzSE.js → _sessionId-zbgYl7ZJ.js} +1 -1
  12. package/.output/public/assets/{index-DZ_wpqQM.js → index-BIK4bPTv.js} +1 -1
  13. package/.output/public/assets/{index-B17u7JMC.js → index-BZo_8koU.js} +15 -15
  14. package/.output/public/assets/{index-C0g79BAs.js → index-D2Qw6Ri2.js} +1 -1
  15. package/.output/public/assets/{index-BWjeqxbk.css → index-ZWschkCh.css} +1 -1
  16. package/.output/public/assets/{index-CmEO8ykj.js → index-xF_NY8Yb.js} +5 -5
  17. package/.output/public/assets/{json-viewer-CuWU-H6K.js → json-viewer-B-Ao6pwW.js} +1 -1
  18. package/.output/public/assets/{jszip.min-DPJI_UKW.js → jszip.min-B_rfAUCe.js} +1 -1
  19. package/.output/server/_libs/modelcontextprotocol__core.mjs +25 -25
  20. package/.output/server/_libs/modelcontextprotocol__server.mjs +2 -2
  21. package/.output/server/_libs/zod.mjs +6 -6
  22. package/.output/server/{_sessionId-Cun21KZF.mjs → _sessionId-CJ99gZFV.mjs} +3 -1
  23. package/.output/server/{_sessionId-CoFyeOti.mjs → _sessionId-DEjScBpK.mjs} +5 -3
  24. package/.output/server/_ssr/{CompareDrawer-Bgc2TZh_.mjs → CompareDrawer-DtOIMeQB.mjs} +4 -2
  25. package/.output/server/_ssr/{InspectorPet-DyK2ojeh.mjs → InspectorPet-CXc4oCBQ.mjs} +3 -1
  26. package/.output/server/_ssr/{ProxyViewerContainer-hvn6oVWD.mjs → ProxyViewerContainer-DVPlwfkt.mjs} +52 -27
  27. package/.output/server/_ssr/{ReplayDialog-KpnrGtZ-.mjs → ReplayDialog-BQEA95qC.mjs} +6 -4
  28. package/.output/server/_ssr/{RequestAnatomy-uCsfucn8.mjs → RequestAnatomy-BMyd8B7Q.mjs} +4 -2
  29. package/.output/server/_ssr/{ResponseView-C8UPqoPO.mjs → ResponseView-qrTaEIM8.mjs} +4 -2
  30. package/.output/server/_ssr/{StreamingChunkSequence-BP28I9sh.mjs → StreamingChunkSequence-BFTL8mIy.mjs} +5 -3
  31. package/.output/server/_ssr/{index-Du3YNDKx.mjs → index-Bcj1Hcy4.mjs} +4 -2
  32. package/.output/server/_ssr/{index-BVwVoQlZ.mjs → index-DW-M7XUG.mjs} +3 -1
  33. package/.output/server/_ssr/index.mjs +4 -3
  34. package/.output/server/_ssr/{json-viewer-DrE0vtlB.mjs → json-viewer-DiI0Yjsn.mjs} +4 -2
  35. package/.output/server/_ssr/publicBasePathContext-5264L3rX.mjs +148 -0
  36. package/.output/server/_ssr/{router-C6nWUZHK.mjs → router-BJ_ltLim.mjs} +2432 -475
  37. package/.output/server/_ssr/start-C6MjPjxp.mjs +57 -0
  38. package/.output/server/_tanstack-start-manifest_v-C6pXXX9R.mjs +4 -0
  39. package/.output/server/index.mjs +99 -99
  40. package/.output/workers/logFinalizer.worker.js +4 -0
  41. package/.output/workers/sessionWorkerEntry.js +4 -0
  42. package/README.md +221 -25
  43. package/docs/instance-control.openapi.yaml +306 -0
  44. package/package.json +4 -1
  45. package/src/cli/alias.ts +440 -0
  46. package/src/cli/instance.ts +1037 -0
  47. package/src/cli/instanceArgs.ts +266 -0
  48. package/src/cli/instanceControl.ts +183 -0
  49. package/src/cli/instanceModel.ts +275 -0
  50. package/src/cli/instanceRegistry.ts +740 -0
  51. package/src/cli/onboard.ts +123 -35
  52. package/src/cli/onboardTarget.ts +179 -0
  53. package/src/cli/templates/codex-skill-onboard.ts +76 -28
  54. package/src/cli/templates/skill-onboard.ts +93 -59
  55. package/src/cli.ts +154 -16
  56. package/src/components/ProxyViewer.tsx +13 -5
  57. package/src/components/providers/SettingsDialog.tsx +13 -8
  58. package/src/lib/basePath.ts +8 -2
  59. package/src/lib/codingAgentEndpoints.ts +16 -0
  60. package/src/lib/instanceContract.ts +220 -0
  61. package/src/lib/managedInstance.ts +104 -0
  62. package/src/lib/publicBasePathContext.ts +107 -0
  63. package/src/mcp/instanceHandlers.ts +149 -0
  64. package/src/mcp/loopback.ts +16 -5
  65. package/src/mcp/mode.ts +25 -0
  66. package/src/mcp/server.ts +258 -18
  67. package/src/mcp/toolHandlers.ts +2 -2
  68. package/src/proxy/identityProxy.ts +656 -36
  69. package/src/proxy/runtimeShutdown.ts +73 -1
  70. package/src/proxy/sessionInfo.ts +5 -4
  71. package/src/routes/api/-instances.ts +109 -0
  72. package/src/routes/api/instances.$name.connection.ts +12 -0
  73. package/src/routes/api/instances.$name.restart.ts +23 -0
  74. package/src/routes/api/instances.$name.start.ts +18 -0
  75. package/src/routes/api/instances.$name.stop.ts +23 -0
  76. package/src/routes/api/instances.$name.ts +12 -0
  77. package/src/routes/api/instances.ts +16 -0
  78. package/src/start.ts +10 -0
  79. package/.output/server/_ssr/start-HYkvq4Ni.mjs +0 -4
  80. package/.output/server/_tanstack-start-manifest_v-rshlRaiN.mjs +0 -4
@@ -0,0 +1,104 @@
1
+ export const MANAGED_INSTANCE_CONTROL_PATH = "/.agent-inspector/instance";
2
+ export const MANAGED_INSTANCE_SHUTDOWN_PATH = `${MANAGED_INSTANCE_CONTROL_PATH}/shutdown`;
3
+ export const MANAGED_INSTANCE_TOKEN_HEADER = "x-agent-inspector-instance-token";
4
+ export const MANAGED_INSTANCE_SHUTDOWN_MESSAGE_TYPE = "agent-inspector:managed-shutdown";
5
+ export const AGENT_INSPECTOR_CLI_ENTRY_ENV = "AGENT_INSPECTOR_CLI_ENTRY";
6
+
7
+ export const MANAGED_INSTANCE_ENV = {
8
+ name: "AGENT_INSPECTOR_INSTANCE_NAME",
9
+ instanceId: "AGENT_INSPECTOR_INSTANCE_ID",
10
+ launchId: "AGENT_INSPECTOR_INSTANCE_LAUNCH_ID",
11
+ controlToken: "AGENT_INSPECTOR_INSTANCE_CONTROL_TOKEN",
12
+ startedAt: "AGENT_INSPECTOR_INSTANCE_STARTED_AT",
13
+ upstreamPort: "AGENT_INSPECTOR_UPSTREAM_PORT",
14
+ } as const;
15
+
16
+ type ManagedInstanceEnv = Readonly<Record<string, string | undefined>>;
17
+
18
+ export type ManagedInstanceLaunch = {
19
+ name: string;
20
+ instanceId: string;
21
+ launchId: string;
22
+ controlToken: string;
23
+ startedAt: string;
24
+ upstreamPort: number;
25
+ };
26
+
27
+ export type ManagedInstanceLaunchResult =
28
+ | { kind: "unmanaged" }
29
+ | { kind: "invalid"; message: string }
30
+ | { kind: "managed"; launch: ManagedInstanceLaunch };
31
+
32
+ export type ManagedInstanceShutdownMessage = {
33
+ type: typeof MANAGED_INSTANCE_SHUTDOWN_MESSAGE_TYPE;
34
+ controlToken: string;
35
+ };
36
+
37
+ function nonEmpty(value: string | undefined): string | null {
38
+ const trimmed = value?.trim();
39
+ return trimmed === undefined || trimmed.length === 0 ? null : trimmed;
40
+ }
41
+
42
+ function isRecord(value: unknown): value is Record<string, unknown> {
43
+ return typeof value === "object" && value !== null && !Array.isArray(value);
44
+ }
45
+
46
+ export function readManagedInstanceLaunch(
47
+ env: ManagedInstanceEnv = process.env,
48
+ ): ManagedInstanceLaunchResult {
49
+ const values = [
50
+ env[MANAGED_INSTANCE_ENV.name],
51
+ env[MANAGED_INSTANCE_ENV.instanceId],
52
+ env[MANAGED_INSTANCE_ENV.launchId],
53
+ env[MANAGED_INSTANCE_ENV.controlToken],
54
+ env[MANAGED_INSTANCE_ENV.startedAt],
55
+ env[MANAGED_INSTANCE_ENV.upstreamPort],
56
+ ];
57
+ if (values.every((value) => value === undefined || value.trim().length === 0)) {
58
+ return { kind: "unmanaged" };
59
+ }
60
+
61
+ const name = nonEmpty(env[MANAGED_INSTANCE_ENV.name]);
62
+ const instanceId = nonEmpty(env[MANAGED_INSTANCE_ENV.instanceId]);
63
+ const launchId = nonEmpty(env[MANAGED_INSTANCE_ENV.launchId]);
64
+ const controlToken = nonEmpty(env[MANAGED_INSTANCE_ENV.controlToken]);
65
+ const startedAt = nonEmpty(env[MANAGED_INSTANCE_ENV.startedAt]);
66
+ const upstreamPort = Number(env[MANAGED_INSTANCE_ENV.upstreamPort]);
67
+ if (
68
+ name === null ||
69
+ instanceId === null ||
70
+ launchId === null ||
71
+ controlToken === null ||
72
+ controlToken.length < 32 ||
73
+ startedAt === null ||
74
+ !Number.isFinite(Date.parse(startedAt)) ||
75
+ !Number.isInteger(upstreamPort) ||
76
+ upstreamPort < 1 ||
77
+ upstreamPort > 65_535
78
+ ) {
79
+ return {
80
+ kind: "invalid",
81
+ message: "Managed instance launch environment is incomplete or invalid.",
82
+ };
83
+ }
84
+ return {
85
+ kind: "managed",
86
+ launch: { name, instanceId, launchId, controlToken, startedAt, upstreamPort },
87
+ };
88
+ }
89
+
90
+ export function managedInstanceShutdownMessage(
91
+ controlToken: string,
92
+ ): ManagedInstanceShutdownMessage {
93
+ return { type: MANAGED_INSTANCE_SHUTDOWN_MESSAGE_TYPE, controlToken };
94
+ }
95
+
96
+ export function parseManagedInstanceShutdownMessage(
97
+ value: unknown,
98
+ ): ManagedInstanceShutdownMessage | null {
99
+ if (!isRecord(value)) return null;
100
+ return value["type"] === MANAGED_INSTANCE_SHUTDOWN_MESSAGE_TYPE &&
101
+ typeof value["controlToken"] === "string"
102
+ ? { type: MANAGED_INSTANCE_SHUTDOWN_MESSAGE_TYPE, controlToken: value["controlToken"] }
103
+ : null;
104
+ }
@@ -0,0 +1,107 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import {
3
+ AGENT_INSPECTOR_PUBLIC_BASE_PATH_HEADER,
4
+ AGENT_INSPECTOR_PUBLIC_PORT_HEADER,
5
+ getConfiguredBasePath,
6
+ normalizeBasePath,
7
+ } from "./basePath";
8
+ import { getCurrentPort } from "./serverPort";
9
+
10
+ type BasePathEnv = Readonly<Record<string, string | undefined>>;
11
+
12
+ type PublicRequestContext = {
13
+ basePath: string;
14
+ publicPort: number | null;
15
+ };
16
+
17
+ const publicRequestStorage = new AsyncLocalStorage<PublicRequestContext>();
18
+
19
+ function isSafePublicBasePath(value: string): boolean {
20
+ if (value.length === 0) return true;
21
+ if (!/^\/[A-Za-z0-9._~-]+(?:\/[A-Za-z0-9._~-]+)*$/u.test(value)) return false;
22
+ return !value.split("/").some((segment) => segment === "." || segment === "..");
23
+ }
24
+
25
+ /**
26
+ * Resolve the public Base Path attached by the trusted ingress. Invalid or
27
+ * missing values fall back to the process's canonical Base Path.
28
+ */
29
+ export function resolveRequestPublicBasePath(request: Request, env?: BasePathEnv): string {
30
+ const rawValue = request.headers.get(AGENT_INSPECTOR_PUBLIC_BASE_PATH_HEADER);
31
+ if (rawValue !== null) {
32
+ const normalized = normalizeBasePath(rawValue);
33
+ if (isSafePublicBasePath(normalized)) return normalized;
34
+ }
35
+ return getConfiguredBasePath(env ?? process.env);
36
+ }
37
+
38
+ /** Resolve the trusted ingress listener port without consulting request URL or Host. */
39
+ export function resolveRequestPublicPort(request: Request): number | null {
40
+ const rawValue = request.headers.get(AGENT_INSPECTOR_PUBLIC_PORT_HEADER);
41
+ if (rawValue !== null && /^\d{1,5}$/u.test(rawValue)) {
42
+ const headerPort = Number(rawValue);
43
+ if (Number.isSafeInteger(headerPort) && headerPort > 0 && headerPort <= 65_535) {
44
+ return headerPort;
45
+ }
46
+ }
47
+ try {
48
+ return getCurrentPort();
49
+ } catch {
50
+ // Vite dev does not always publish its listener port through process env.
51
+ }
52
+ try {
53
+ const urlPort = new URL(request.url).port;
54
+ if (!/^\d{1,5}$/u.test(urlPort)) return null;
55
+ const port = Number(urlPort);
56
+ return Number.isSafeInteger(port) && port > 0 && port <= 65_535 ? port : null;
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ /** Return the originating request's public Base Path, or the canonical path outside a request. */
63
+ export function getPublicBasePath(env?: BasePathEnv): string {
64
+ return publicRequestStorage.getStore()?.basePath ?? getConfiguredBasePath(env ?? process.env);
65
+ }
66
+
67
+ /** Return the trusted ingress listener port, or the canonical process port outside a request. */
68
+ export function getPublicPort(): number {
69
+ return publicRequestStorage.getStore()?.publicPort ?? getCurrentPort();
70
+ }
71
+
72
+ /** Run work with an explicitly selected public Base Path. Primarily useful for internal calls. */
73
+ export function runWithPublicBasePath<T>(basePath: string, callback: () => T): T {
74
+ const current = publicRequestStorage.getStore();
75
+ return publicRequestStorage.run(
76
+ {
77
+ basePath: normalizeBasePath(basePath),
78
+ publicPort: current?.publicPort ?? null,
79
+ },
80
+ callback,
81
+ );
82
+ }
83
+
84
+ /** Run work with an explicit trusted public listener port. */
85
+ export function runWithPublicPort<T>(publicPort: number, callback: () => T): T {
86
+ const current = publicRequestStorage.getStore();
87
+ const normalizedPort =
88
+ Number.isSafeInteger(publicPort) && publicPort > 0 && publicPort <= 65_535 ? publicPort : null;
89
+ return publicRequestStorage.run(
90
+ {
91
+ basePath: current?.basePath ?? getConfiguredBasePath(process.env),
92
+ publicPort: normalizedPort,
93
+ },
94
+ callback,
95
+ );
96
+ }
97
+
98
+ /** Run the complete request chain with the ingress-selected public Base Path. */
99
+ export function runWithRequestPublicBasePath<T>(request: Request, callback: () => T): T {
100
+ return publicRequestStorage.run(
101
+ {
102
+ basePath: resolveRequestPublicBasePath(request),
103
+ publicPort: resolveRequestPublicPort(request),
104
+ },
105
+ callback,
106
+ );
107
+ }
@@ -0,0 +1,149 @@
1
+ import { z } from "zod";
2
+ import {
3
+ InstanceConnectionOutputSchema,
4
+ InstanceEmptyRequestSchema,
5
+ InstanceErrorOutputSchema,
6
+ InstanceListOutputSchema,
7
+ InstanceOperationOutputSchema,
8
+ InstanceStartRequestSchema,
9
+ InstanceStatusOutputSchema,
10
+ type InstanceStartRequest,
11
+ } from "../lib/instanceContract";
12
+ import type { CallApiOptions } from "./loopback";
13
+ import type { ToolResult } from "./toolHandlers";
14
+
15
+ export const INSTANCE_LIFECYCLE_TIMEOUT_MS = 60_000;
16
+
17
+ export const InstanceToolNameSchema = z.string().trim().min(1).max(64);
18
+
19
+ export const InstanceLifecycleToolInputSchema = InstanceStartRequestSchema.extend({
20
+ name: InstanceToolNameSchema,
21
+ });
22
+
23
+ export const InstanceNameToolInputSchema = z
24
+ .object({
25
+ name: InstanceToolNameSchema,
26
+ })
27
+ .strict();
28
+
29
+ export type InstanceCallApiFn = (path: string, options?: CallApiOptions) => Promise<Response>;
30
+
31
+ export type InstanceLifecycleToolInput = InstanceStartRequest & { name: string };
32
+
33
+ function textJson(data: unknown): ToolResult {
34
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
35
+ }
36
+
37
+ function toolError(message: string): ToolResult {
38
+ return { content: [{ type: "text", text: message }], isError: true };
39
+ }
40
+
41
+ async function responseJson(response: Response): Promise<unknown | null> {
42
+ try {
43
+ const value: unknown = await response.json();
44
+ return value;
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ async function validatedInstanceResponse(
51
+ response: Response,
52
+ schema: z.ZodType,
53
+ operation: string,
54
+ ): Promise<ToolResult> {
55
+ const value = await responseJson(response);
56
+ if (!response.ok) {
57
+ const error = InstanceErrorOutputSchema.safeParse(value);
58
+ return error.success
59
+ ? { ...textJson(error.data), isError: true }
60
+ : toolError(
61
+ `${operation} returned HTTP ${String(response.status)} with an invalid error body`,
62
+ );
63
+ }
64
+
65
+ const parsed = schema.safeParse(value);
66
+ return parsed.success
67
+ ? textJson(parsed.data)
68
+ : toolError(`${operation} returned an invalid public instance document`);
69
+ }
70
+
71
+ function instancePath(name: string, suffix = ""): string {
72
+ return `/api/instances/${encodeURIComponent(name)}${suffix}`;
73
+ }
74
+
75
+ function jsonPost(body: unknown): CallApiOptions {
76
+ return {
77
+ method: "POST",
78
+ headers: {
79
+ accept: "application/json",
80
+ "content-type": "application/json",
81
+ },
82
+ body: JSON.stringify(body),
83
+ timeoutMs: INSTANCE_LIFECYCLE_TIMEOUT_MS,
84
+ };
85
+ }
86
+
87
+ function lifecycleRequest(input: InstanceLifecycleToolInput): InstanceStartRequest {
88
+ return {
89
+ port: input.port,
90
+ basePath: input.basePath,
91
+ host: input.host,
92
+ captureMode: input.captureMode,
93
+ dataDir: input.dataDir,
94
+ };
95
+ }
96
+
97
+ export async function listInstancesImpl(callApi: InstanceCallApiFn): Promise<ToolResult> {
98
+ const response = await callApi("/api/instances", {
99
+ headers: { accept: "application/json" },
100
+ });
101
+ return validatedInstanceResponse(response, InstanceListOutputSchema, "GET /api/instances");
102
+ }
103
+
104
+ export async function getInstanceImpl(
105
+ callApi: InstanceCallApiFn,
106
+ name: string,
107
+ ): Promise<ToolResult> {
108
+ const path = instancePath(name);
109
+ const response = await callApi(path, { headers: { accept: "application/json" } });
110
+ return validatedInstanceResponse(response, InstanceStatusOutputSchema, `GET ${path}`);
111
+ }
112
+
113
+ export async function getInstanceConnectionImpl(
114
+ callApi: InstanceCallApiFn,
115
+ name: string,
116
+ ): Promise<ToolResult> {
117
+ const path = instancePath(name, "/connection");
118
+ const response = await callApi(path, { headers: { accept: "application/json" } });
119
+ return validatedInstanceResponse(response, InstanceConnectionOutputSchema, `GET ${path}`);
120
+ }
121
+
122
+ export async function startInstanceImpl(
123
+ callApi: InstanceCallApiFn,
124
+ input: InstanceLifecycleToolInput,
125
+ ): Promise<ToolResult> {
126
+ const path = instancePath(input.name, "/start");
127
+ const request = lifecycleRequest(input);
128
+ const response = await callApi(path, jsonPost(InstanceStartRequestSchema.parse(request)));
129
+ return validatedInstanceResponse(response, InstanceOperationOutputSchema, `POST ${path}`);
130
+ }
131
+
132
+ export async function stopInstanceImpl(
133
+ callApi: InstanceCallApiFn,
134
+ name: string,
135
+ ): Promise<ToolResult> {
136
+ const path = instancePath(name, "/stop");
137
+ const response = await callApi(path, jsonPost(InstanceEmptyRequestSchema.parse({})));
138
+ return validatedInstanceResponse(response, InstanceOperationOutputSchema, `POST ${path}`);
139
+ }
140
+
141
+ export async function restartInstanceImpl(
142
+ callApi: InstanceCallApiFn,
143
+ input: InstanceLifecycleToolInput,
144
+ ): Promise<ToolResult> {
145
+ const path = instancePath(input.name, "/restart");
146
+ const request = lifecycleRequest(input);
147
+ const response = await callApi(path, jsonPost(InstanceStartRequestSchema.parse(request)));
148
+ return validatedInstanceResponse(response, InstanceOperationOutputSchema, `POST ${path}`);
149
+ }
@@ -16,8 +16,12 @@
16
16
  * — should stay identical so callers don't change.
17
17
  */
18
18
 
19
- import { getConfiguredBasePath, withBasePath } from "../lib/basePath";
20
- import { getCurrentPort } from "../lib/serverPort";
19
+ import {
20
+ AGENT_INSPECTOR_PUBLIC_BASE_PATH_HEADER,
21
+ AGENT_INSPECTOR_PUBLIC_PORT_HEADER,
22
+ withBasePath,
23
+ } from "../lib/basePath";
24
+ import { getPublicBasePath, getPublicPort } from "../lib/publicBasePathContext";
21
25
 
22
26
  const DEFAULT_TIMEOUT_MS = 30_000;
23
27
 
@@ -47,10 +51,17 @@ export async function callApi(path: string, options: CallApiOptions = {}): Promi
47
51
  if (!path.startsWith("/")) {
48
52
  throw new Error(`callApi: path must start with '/', got: ${path}`);
49
53
  }
50
- const port = getCurrentPort();
51
- const url = `http://127.0.0.1:${port}${withBasePath(path, getConfiguredBasePath(process.env))}`;
54
+ const port = getPublicPort();
55
+ const publicBasePath = getPublicBasePath();
56
+ const url = `http://127.0.0.1:${port}${withBasePath(path, publicBasePath)}`;
52
57
 
53
58
  const { timeoutMs = DEFAULT_TIMEOUT_MS, signal: userSignal, ...rest } = options;
59
+ const headers = new Headers(rest.headers);
60
+ headers.set(
61
+ AGENT_INSPECTOR_PUBLIC_BASE_PATH_HEADER,
62
+ publicBasePath.length === 0 ? "/" : publicBasePath,
63
+ );
64
+ headers.set(AGENT_INSPECTOR_PUBLIC_PORT_HEADER, String(port));
54
65
 
55
66
  const controller = new AbortController();
56
67
  const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
@@ -65,7 +76,7 @@ export async function callApi(path: string, options: CallApiOptions = {}): Promi
65
76
  }
66
77
 
67
78
  try {
68
- return await fetch(url, { ...rest, signal: controller.signal });
79
+ return await fetch(url, { ...rest, headers, signal: controller.signal });
69
80
  } catch (err) {
70
81
  if (err instanceof Error && err.name === "AbortError") {
71
82
  throw new LoopbackTimeoutError(path, timeoutMs);
package/src/mcp/mode.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export type McpWriteMode = "enabled" | "readonly";
2
2
 
3
+ export const MCP_INSTANCE_WRITES_ENV = "AGENT_INSPECTOR_MCP_INSTANCE_WRITES";
4
+
3
5
  function parseFlag(value: string | undefined): boolean | null {
4
6
  if (value === undefined) return null;
5
7
  switch (value.trim().toLowerCase()) {
@@ -26,6 +28,29 @@ export function isMcpWriteEnabled(): boolean {
26
28
  return writesFlag === true;
27
29
  }
28
30
 
31
+ export function isMcpInstanceWriteEnabled(): boolean {
32
+ return process.env[MCP_INSTANCE_WRITES_ENV] === "1";
33
+ }
34
+
35
+ export function getMcpInstanceModeInfo(): {
36
+ writeMode: McpWriteMode;
37
+ writesEnabled: boolean;
38
+ env: { AGENT_INSPECTOR_MCP_INSTANCE_WRITES: string | null };
39
+ note: string;
40
+ } {
41
+ const writesEnabled = isMcpInstanceWriteEnabled();
42
+ return {
43
+ writeMode: writesEnabled ? "enabled" : "readonly",
44
+ writesEnabled,
45
+ env: {
46
+ AGENT_INSPECTOR_MCP_INSTANCE_WRITES: process.env[MCP_INSTANCE_WRITES_ENV] ?? null,
47
+ },
48
+ note: writesEnabled
49
+ ? "Named-instance lifecycle tools passed the instance-specific opt-in. General MCP writes must also be enabled."
50
+ : "Named-instance lifecycle tools require AGENT_INSPECTOR_MCP_INSTANCE_WRITES=1 in addition to general MCP writes.",
51
+ };
52
+ }
53
+
29
54
  export function getMcpModeInfo(): {
30
55
  writeMode: McpWriteMode;
31
56
  writesEnabled: boolean;