@tonyclaw/agent-inspector 3.0.44 → 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.
- package/.output/cli.js +20743 -16945
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-DQHjxFO6.js → CompareDrawer-B3_TLTJh.js} +1 -1
- package/.output/public/assets/{InspectorPet-D1A4505B.js → InspectorPet-Cpn6hv4P.js} +1 -1
- package/.output/public/assets/{ProxyViewerContainer-BhpIgfBi.js → ProxyViewerContainer-BEAhuGOJ.js} +27 -27
- package/.output/public/assets/{ReplayDialog-C5vmbecv.js → ReplayDialog-B3EKVdJk.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-CPGNB8lk.js → RequestAnatomy-COOQjyUC.js} +1 -1
- package/.output/public/assets/{ResponseView-DUfrajCm.js → ResponseView-DBCjkwye.js} +1 -1
- package/.output/public/assets/{StreamingChunkSequence-nbeCQqOz.js → StreamingChunkSequence-CuXhDgMo.js} +1 -1
- package/.output/public/assets/{_sessionId-mw3aA8Bp.js → _sessionId-BJlM3nmt.js} +1 -1
- package/.output/public/assets/{_sessionId-CDhRgzf6.js → _sessionId-zbgYl7ZJ.js} +1 -1
- package/.output/public/assets/{index-B2eoeT-Z.js → index-BIK4bPTv.js} +1 -1
- package/.output/public/assets/{index-C6qZwYlR.js → index-BZo_8koU.js} +15 -15
- package/.output/public/assets/{index-CX7_rf7o.js → index-D2Qw6Ri2.js} +1 -1
- package/.output/public/assets/{index-BWjeqxbk.css → index-ZWschkCh.css} +1 -1
- package/.output/public/assets/{index-BfRhTvOd.js → index-xF_NY8Yb.js} +5 -5
- package/.output/public/assets/{json-viewer-BLmF1sOZ.js → json-viewer-B-Ao6pwW.js} +1 -1
- package/.output/public/assets/{jszip.min-C9rCFoU3.js → jszip.min-B_rfAUCe.js} +1 -1
- package/.output/server/_libs/modelcontextprotocol__core.mjs +25 -25
- package/.output/server/_libs/modelcontextprotocol__server.mjs +2 -2
- package/.output/server/_libs/zod.mjs +6 -6
- package/.output/server/{_sessionId-U54BAqdp.mjs → _sessionId-CJ99gZFV.mjs} +3 -1
- package/.output/server/{_sessionId-E_Dmopc-.mjs → _sessionId-DEjScBpK.mjs} +5 -3
- package/.output/server/_ssr/{CompareDrawer-CksatPed.mjs → CompareDrawer-DtOIMeQB.mjs} +4 -2
- package/.output/server/_ssr/{InspectorPet-DNc4GQ7z.mjs → InspectorPet-CXc4oCBQ.mjs} +3 -1
- package/.output/server/_ssr/{ProxyViewerContainer-B02Ey0p_.mjs → ProxyViewerContainer-DVPlwfkt.mjs} +52 -27
- package/.output/server/_ssr/{ReplayDialog-DdMvHJF1.mjs → ReplayDialog-BQEA95qC.mjs} +6 -4
- package/.output/server/_ssr/{RequestAnatomy--vh2Z8__.mjs → RequestAnatomy-BMyd8B7Q.mjs} +4 -2
- package/.output/server/_ssr/{ResponseView-Dhxc-cKC.mjs → ResponseView-qrTaEIM8.mjs} +4 -2
- package/.output/server/_ssr/{StreamingChunkSequence-BG0EQmaD.mjs → StreamingChunkSequence-BFTL8mIy.mjs} +5 -3
- package/.output/server/_ssr/{index-lcfulIXa.mjs → index-Bcj1Hcy4.mjs} +4 -2
- package/.output/server/_ssr/{index-D0At9nfd.mjs → index-DW-M7XUG.mjs} +3 -1
- package/.output/server/_ssr/index.mjs +4 -3
- package/.output/server/_ssr/{json-viewer-My1J1_5R.mjs → json-viewer-DiI0Yjsn.mjs} +4 -2
- package/.output/server/_ssr/publicBasePathContext-5264L3rX.mjs +148 -0
- package/.output/server/_ssr/{router-DWcUFPR3.mjs → router-BJ_ltLim.mjs} +2432 -475
- package/.output/server/_ssr/start-C6MjPjxp.mjs +57 -0
- package/.output/server/_tanstack-start-manifest_v-C6pXXX9R.mjs +4 -0
- package/.output/server/index.mjs +92 -92
- package/.output/workers/logFinalizer.worker.js +4 -0
- package/.output/workers/sessionWorkerEntry.js +4 -0
- package/README.md +221 -25
- package/docs/instance-control.openapi.yaml +306 -0
- package/package.json +4 -1
- package/src/cli/alias.ts +440 -0
- package/src/cli/instance.ts +1037 -0
- package/src/cli/instanceArgs.ts +266 -0
- package/src/cli/instanceControl.ts +183 -0
- package/src/cli/instanceModel.ts +275 -0
- package/src/cli/instanceRegistry.ts +740 -0
- package/src/cli/onboard.ts +123 -35
- package/src/cli/onboardTarget.ts +179 -0
- package/src/cli/templates/codex-skill-onboard.ts +76 -28
- package/src/cli/templates/skill-onboard.ts +93 -59
- package/src/cli.ts +154 -16
- package/src/components/ProxyViewer.tsx +13 -5
- package/src/components/providers/SettingsDialog.tsx +13 -8
- package/src/lib/basePath.ts +8 -2
- package/src/lib/codingAgentEndpoints.ts +16 -0
- package/src/lib/instanceContract.ts +220 -0
- package/src/lib/managedInstance.ts +104 -0
- package/src/lib/publicBasePathContext.ts +107 -0
- package/src/mcp/instanceHandlers.ts +149 -0
- package/src/mcp/loopback.ts +16 -5
- package/src/mcp/mode.ts +25 -0
- package/src/mcp/server.ts +258 -18
- package/src/mcp/toolHandlers.ts +2 -2
- package/src/proxy/identityProxy.ts +655 -36
- package/src/proxy/runtimeShutdown.ts +73 -1
- package/src/proxy/sessionInfo.ts +5 -4
- package/src/routes/api/-instances.ts +109 -0
- package/src/routes/api/instances.$name.connection.ts +12 -0
- package/src/routes/api/instances.$name.restart.ts +23 -0
- package/src/routes/api/instances.$name.start.ts +18 -0
- package/src/routes/api/instances.$name.stop.ts +23 -0
- package/src/routes/api/instances.$name.ts +12 -0
- package/src/routes/api/instances.ts +16 -0
- package/src/start.ts +10 -0
- package/.output/server/_ssr/start-HYkvq4Ni.mjs +0 -4
- package/.output/server/_tanstack-start-manifest_v-B2chR8ef.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
|
+
}
|
package/src/mcp/loopback.ts
CHANGED
|
@@ -16,8 +16,12 @@
|
|
|
16
16
|
* — should stay identical so callers don't change.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import {
|
|
20
|
-
|
|
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 =
|
|
51
|
-
const
|
|
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;
|