@tonyclaw/agent-inspector 3.0.46 → 3.0.47
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/backend/nitro.json +1 -1
- package/.output/cli.js +6526 -5769
- package/.output/server/_ssr/index.mjs +1 -1
- package/.output/server/_ssr/{router-D18yUq36.mjs → router-BwZEsYfS.mjs} +32 -5
- package/.output/server/index.mjs +1 -1
- package/.output/ui/assets/{CompareDrawer-DAUuIJ6G.js → CompareDrawer-V-8lPASC.js} +1 -1
- package/.output/ui/assets/{InspectorPet-BRBVjOWI.js → InspectorPet-D4CVnfdf.js} +1 -1
- package/.output/ui/assets/{ProxyViewerContainer-D7Sq0ctc.js → ProxyViewerContainer-CO3ZVHLX.js} +4 -4
- package/.output/ui/assets/{ReplayDialog-nDLsjOhs.js → ReplayDialog-CpGyTLtt.js} +1 -1
- package/.output/ui/assets/{RequestAnatomy-zd4BDgL0.js → RequestAnatomy-BLuJL9dZ.js} +1 -1
- package/.output/ui/assets/{ResponseView-AdIXGcSn.js → ResponseView-BuSC-CH7.js} +1 -1
- package/.output/ui/assets/{StreamingChunkSequence-CesTz8He.js → StreamingChunkSequence-DeA-JJKa.js} +1 -1
- package/.output/ui/assets/{_sessionId-BAfzrhSU.js → _sessionId-8HuHFhjo.js} +1 -1
- package/.output/ui/assets/{_sessionId-QnBomgPD.js → _sessionId-Chp67th6.js} +1 -1
- package/.output/ui/assets/{index-dNuk2dsU.js → index-DPmKK_5d.js} +1 -1
- package/.output/ui/assets/{index-BcsdxBAd.js → index-atoQrlb8.js} +1 -1
- package/.output/ui/assets/{index-DHpwr08Z.js → index-nY3E1vxV.js} +2 -2
- package/.output/ui/assets/{index-D0rtCN9V.js → index-nl9-dpZn.js} +1 -1
- package/.output/ui/assets/{json-viewer-BvFHglMb.js → json-viewer-B9kIzqeX.js} +1 -1
- package/.output/ui/assets/{jszip.min-B5Z7gNZB.js → jszip.min-B7tfyE-I.js} +1 -1
- package/.output/ui/index.html +1 -1
- package/README.md +7 -0
- package/package.json +17 -3
- package/src/backend/routes/api/-instances.ts +7 -109
- package/src/cli/instance.ts +10 -0
- package/src/cli/instanceApi.ts +213 -0
- package/src/cli/instanceArgs.ts +16 -0
- package/src/cli/instanceControl.ts +10 -0
- package/src/cli/instanceModel.ts +9 -0
- package/src/cli/rustBackendPackage.ts +450 -0
- package/src/cli.ts +157 -14
- package/src/lib/backendImplementation.ts +16 -0
- package/src/lib/instanceContract.ts +3 -0
- package/src/lib/managedInstance.ts +9 -1
- package/src/mcp/instanceHandlers.ts +1 -0
- package/src/proxy/identityProxy.ts +148 -0
- package/src/proxy/sessionArchive.ts +0 -8
- package/src/proxy/sqliteLogIndex.ts +0 -40
package/src/cli.ts
CHANGED
|
@@ -7,6 +7,13 @@ import { dirname, join, resolve as resolvePath } from "node:path";
|
|
|
7
7
|
import { existsSync } from "node:fs";
|
|
8
8
|
import type { Readable, Writable } from "node:stream";
|
|
9
9
|
import packageJson from "../package.json";
|
|
10
|
+
import {
|
|
11
|
+
AGENT_INSPECTOR_BACKEND_ENV,
|
|
12
|
+
AGENT_INSPECTOR_BACKEND_HEADER,
|
|
13
|
+
BackendImplementationSchema,
|
|
14
|
+
parseBackendImplementation,
|
|
15
|
+
type BackendImplementation,
|
|
16
|
+
} from "./lib/backendImplementation.js";
|
|
10
17
|
import {
|
|
11
18
|
AGENT_INSPECTOR_BASE_PATH_ENV,
|
|
12
19
|
AGENT_INSPECTOR_INTERNAL_STRIPPED_BASE_PATH_ENV,
|
|
@@ -33,6 +40,8 @@ import {
|
|
|
33
40
|
shouldSuppressServerOutputLine,
|
|
34
41
|
type ServerOutputFilterContext,
|
|
35
42
|
} from "./cli/startupOutput.js";
|
|
43
|
+
import { resolveRustBackendPackage } from "./cli/rustBackendPackage.js";
|
|
44
|
+
import { handleNamedInstanceApi } from "./cli/instanceApi.js";
|
|
36
45
|
import {
|
|
37
46
|
closeIdentityProxyGracefully,
|
|
38
47
|
forceCloseIdentityProxy,
|
|
@@ -87,6 +96,7 @@ Global options:
|
|
|
87
96
|
-v, --version Print the package version
|
|
88
97
|
|
|
89
98
|
Start options:
|
|
99
|
+
--backend <typescript|rust> Select the backend implementation (default: typescript)
|
|
90
100
|
--backend-only, --headless Start the API, proxy, and MCP runtime without serving the UI
|
|
91
101
|
--with-ui Serve the independently built UI through the protected ingress
|
|
92
102
|
--port, -p <port> Public ingress port (default: 9527)
|
|
@@ -194,6 +204,33 @@ async function getRunningCaptureMode(
|
|
|
194
204
|
}
|
|
195
205
|
}
|
|
196
206
|
|
|
207
|
+
async function getRunningBackend(
|
|
208
|
+
port: number,
|
|
209
|
+
host?: string,
|
|
210
|
+
basePath = "",
|
|
211
|
+
): Promise<BackendImplementation | null> {
|
|
212
|
+
const controller = new AbortController();
|
|
213
|
+
const timeout = setTimeout(() => controller.abort(), LOCAL_PROBE_TIMEOUT_MS);
|
|
214
|
+
try {
|
|
215
|
+
const response = await fetch(
|
|
216
|
+
appendBasePathToOrigin(urlForHost(port, probeHostForBindHost(host)), "/api/health", basePath),
|
|
217
|
+
{
|
|
218
|
+
cache: "no-store",
|
|
219
|
+
signal: controller.signal,
|
|
220
|
+
},
|
|
221
|
+
);
|
|
222
|
+
if (!response.ok) return null;
|
|
223
|
+
const header = response.headers.get(AGENT_INSPECTOR_BACKEND_HEADER);
|
|
224
|
+
if (header === null) return "typescript";
|
|
225
|
+
const parsed = BackendImplementationSchema.safeParse(header.trim().toLowerCase());
|
|
226
|
+
return parsed.success ? parsed.data : null;
|
|
227
|
+
} catch {
|
|
228
|
+
return null;
|
|
229
|
+
} finally {
|
|
230
|
+
clearTimeout(timeout);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
197
234
|
function isPortAcceptingConnections(port: number, host?: string): Promise<boolean> {
|
|
198
235
|
return new Promise((resolve) => {
|
|
199
236
|
const socket = createConnection({ host: probeHostForBindHost(host), port });
|
|
@@ -293,11 +330,35 @@ type ServerCommand = {
|
|
|
293
330
|
args: string[];
|
|
294
331
|
};
|
|
295
332
|
|
|
296
|
-
|
|
333
|
+
type ServerCommandResolution =
|
|
334
|
+
| { kind: "success"; command: ServerCommand }
|
|
335
|
+
| { kind: "error"; message: string };
|
|
336
|
+
|
|
337
|
+
function resolveServerCommand(
|
|
338
|
+
outputDir: string,
|
|
339
|
+
serverPath: string,
|
|
340
|
+
backend: BackendImplementation,
|
|
341
|
+
): ServerCommandResolution {
|
|
342
|
+
if (backend === "rust") {
|
|
343
|
+
const resolved = resolveRustBackendPackage({ rootVersion: packageJson.version });
|
|
344
|
+
return resolved.kind === "error"
|
|
345
|
+
? {
|
|
346
|
+
kind: "error",
|
|
347
|
+
message: `Cannot start the Rust backend (${resolved.error.code}): ${resolved.error.message}`,
|
|
348
|
+
}
|
|
349
|
+
: {
|
|
350
|
+
kind: "success",
|
|
351
|
+
command: { command: resolved.value.command, args: [...resolved.value.args] },
|
|
352
|
+
};
|
|
353
|
+
}
|
|
297
354
|
const brandedRuntime = join(outputDir, BRANDED_WINDOWS_RUNTIME_EXE);
|
|
298
|
-
return
|
|
299
|
-
|
|
300
|
-
|
|
355
|
+
return {
|
|
356
|
+
kind: "success",
|
|
357
|
+
command:
|
|
358
|
+
process.platform === "win32" && existsSync(brandedRuntime)
|
|
359
|
+
? { command: brandedRuntime, args: [serverPath] }
|
|
360
|
+
: { command: process.execPath, args: [serverPath] },
|
|
361
|
+
};
|
|
301
362
|
}
|
|
302
363
|
|
|
303
364
|
function parseCaptureMode(value: string | undefined): CaptureMode | null {
|
|
@@ -385,6 +446,7 @@ function normalizeExplicitPublicOrigin(value: string | undefined): string | null
|
|
|
385
446
|
}
|
|
386
447
|
|
|
387
448
|
type BackgroundSupervisorOptions = {
|
|
449
|
+
backend: BackendImplementation;
|
|
388
450
|
port: number;
|
|
389
451
|
host: string | undefined;
|
|
390
452
|
configDir: string | undefined;
|
|
@@ -396,7 +458,15 @@ type BackgroundSupervisorOptions = {
|
|
|
396
458
|
};
|
|
397
459
|
|
|
398
460
|
function buildBackgroundSupervisorArgs(options: BackgroundSupervisorOptions): string[] {
|
|
399
|
-
const args = [
|
|
461
|
+
const args = [
|
|
462
|
+
"--no-open",
|
|
463
|
+
"--backend",
|
|
464
|
+
options.backend,
|
|
465
|
+
"--port",
|
|
466
|
+
String(options.port),
|
|
467
|
+
"--mode",
|
|
468
|
+
options.captureMode,
|
|
469
|
+
];
|
|
400
470
|
if (options.host !== undefined) {
|
|
401
471
|
args.push("--host", options.host);
|
|
402
472
|
}
|
|
@@ -449,6 +519,7 @@ async function tryStartIdentityProxy(
|
|
|
449
519
|
allowedUiOrigins: readonly string[],
|
|
450
520
|
uiDirectory: string | undefined,
|
|
451
521
|
publicOrigin: string | undefined,
|
|
522
|
+
backend: BackendImplementation,
|
|
452
523
|
): Promise<import("node:http").Server | null> {
|
|
453
524
|
try {
|
|
454
525
|
const server = await startIdentityProxy({
|
|
@@ -456,6 +527,7 @@ async function tryStartIdentityProxy(
|
|
|
456
527
|
listenHost,
|
|
457
528
|
upstreamHost,
|
|
458
529
|
upstreamPort,
|
|
530
|
+
backendImplementation: backend,
|
|
459
531
|
restrictRemoteControl: !isLoopbackHost(listenHost),
|
|
460
532
|
allowRemoteControl: process.env["AGENT_INSPECTOR_ALLOW_REMOTE_CONTROL"] === "1",
|
|
461
533
|
controlToken: process.env["AGENT_INSPECTOR_CONTROL_TOKEN"],
|
|
@@ -472,6 +544,7 @@ async function tryStartIdentityProxy(
|
|
|
472
544
|
maxRequestBytes: readPositiveIntegerEnv("AGENT_INSPECTOR_MAX_REQUEST_BYTES"),
|
|
473
545
|
requestTimeoutMs: readPositiveIntegerEnv("AGENT_INSPECTOR_REQUEST_TIMEOUT_MS"),
|
|
474
546
|
managedInstance,
|
|
547
|
+
instanceApiHandler: backend === "rust" ? handleNamedInstanceApi : undefined,
|
|
475
548
|
});
|
|
476
549
|
return server;
|
|
477
550
|
} catch (err) {
|
|
@@ -495,6 +568,14 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
495
568
|
const envHost = process.env["NITRO_HOST"] ?? process.env["HOST"];
|
|
496
569
|
const envMode =
|
|
497
570
|
process.env["AGENT_INSPECTOR_CAPTURE_MODE"] ?? process.env["AGENT_INSPECTOR_MODE"];
|
|
571
|
+
const envBackend = parseBackendImplementation(process.env[AGENT_INSPECTOR_BACKEND_ENV]);
|
|
572
|
+
if (envBackend === null) {
|
|
573
|
+
console.error(
|
|
574
|
+
`Invalid ${AGENT_INSPECTOR_BACKEND_ENV}. Use typescript or rust; no backend was started.`,
|
|
575
|
+
);
|
|
576
|
+
process.exitCode = 1;
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
498
579
|
|
|
499
580
|
let port = portDefault;
|
|
500
581
|
let host =
|
|
@@ -510,6 +591,7 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
510
591
|
let enableIdentityProxy = true;
|
|
511
592
|
let legacyAliasEnabled = false;
|
|
512
593
|
let serveUi = process.env["AGENT_INSPECTOR_BACKEND_ONLY"] !== "1";
|
|
594
|
+
let backend: BackendImplementation = envBackend;
|
|
513
595
|
|
|
514
596
|
if (envMode !== undefined && envMode !== "") {
|
|
515
597
|
const parsedMode = parseCaptureMode(envMode);
|
|
@@ -556,6 +638,21 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
556
638
|
captureModeWasSpecified = true;
|
|
557
639
|
continue;
|
|
558
640
|
}
|
|
641
|
+
if (arg.startsWith("--backend=")) {
|
|
642
|
+
const parsedBackend = BackendImplementationSchema.safeParse(
|
|
643
|
+
arg
|
|
644
|
+
.slice(arg.indexOf("=") + 1)
|
|
645
|
+
.trim()
|
|
646
|
+
.toLowerCase(),
|
|
647
|
+
);
|
|
648
|
+
if (!parsedBackend.success) {
|
|
649
|
+
console.error("Invalid backend implementation. Use typescript or rust.");
|
|
650
|
+
process.exitCode = 1;
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
backend = parsedBackend.data;
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
559
656
|
switch (arg) {
|
|
560
657
|
case "--port":
|
|
561
658
|
case "-p":
|
|
@@ -574,6 +671,19 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
574
671
|
i++;
|
|
575
672
|
break;
|
|
576
673
|
}
|
|
674
|
+
case "--backend": {
|
|
675
|
+
const parsedBackend = BackendImplementationSchema.safeParse(
|
|
676
|
+
args[i + 1]?.trim().toLowerCase(),
|
|
677
|
+
);
|
|
678
|
+
if (!parsedBackend.success) {
|
|
679
|
+
console.error("Invalid backend implementation. Use typescript or rust.");
|
|
680
|
+
process.exitCode = 1;
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
backend = parsedBackend.data;
|
|
684
|
+
i++;
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
577
687
|
case "--no-open":
|
|
578
688
|
open = false;
|
|
579
689
|
openWasSpecified = true;
|
|
@@ -662,6 +772,14 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
662
772
|
return;
|
|
663
773
|
}
|
|
664
774
|
|
|
775
|
+
if (managedLaunch !== null && managedLaunch.backend !== backend) {
|
|
776
|
+
console.error(
|
|
777
|
+
`Managed instance backend mismatch: launch identity requires ${managedLaunch.backend}, but ${backend} was selected.`,
|
|
778
|
+
);
|
|
779
|
+
process.exitCode = 1;
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
|
|
665
783
|
if (!enableIdentityProxy && process.env["AGENT_INSPECTOR_ALLOW_UNPROTECTED_INGRESS"] !== "1") {
|
|
666
784
|
console.error(
|
|
667
785
|
"Refusing --no-identity-proxy because it also disables Host, CSRF, credential-scope, and first-hop budget enforcement. Keep the protected ingress enabled or set AGENT_INSPECTOR_ALLOW_UNPROTECTED_INGRESS=1 explicitly for temporary diagnostics.",
|
|
@@ -704,6 +822,16 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
704
822
|
return;
|
|
705
823
|
}
|
|
706
824
|
|
|
825
|
+
const outputDir = __dirname;
|
|
826
|
+
const serverPath = join(outputDir, "../.output/server/index.mjs");
|
|
827
|
+
const serverCommandResolution = resolveServerCommand(outputDir, serverPath, backend);
|
|
828
|
+
if (serverCommandResolution.kind === "error") {
|
|
829
|
+
console.error(serverCommandResolution.message);
|
|
830
|
+
process.exitCode = 1;
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
const serverCommand = serverCommandResolution.command;
|
|
834
|
+
|
|
707
835
|
/**
|
|
708
836
|
* Check if a port is in use and kill the process using it
|
|
709
837
|
*/
|
|
@@ -770,6 +898,16 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
770
898
|
: appendBasePathToOrigin(localUrlForPort(legacyAliasPort), "/api/mcp", basePath);
|
|
771
899
|
|
|
772
900
|
if (!forceRestart && (await isInspectorHealthy(port, host, basePath))) {
|
|
901
|
+
const runningBackend = await getRunningBackend(port, host, basePath);
|
|
902
|
+
if (runningBackend !== backend) {
|
|
903
|
+
console.error(
|
|
904
|
+
runningBackend === null
|
|
905
|
+
? `A healthy Agent Inspector is already running at ${surfaceBaseUrl}, but its backend implementation cannot be verified. Use --force-restart to replace it safely.`
|
|
906
|
+
: `Agent Inspector at ${surfaceBaseUrl} uses ${runningBackend}, but ${backend} was requested. Use --force-restart to switch implementations.`,
|
|
907
|
+
);
|
|
908
|
+
process.exitCode = 1;
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
773
911
|
console.log(
|
|
774
912
|
`agent-inspector ${serveUi ? "is already running" : "backend is already running"} at ${surfaceBaseUrl}`,
|
|
775
913
|
);
|
|
@@ -816,6 +954,7 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
816
954
|
}
|
|
817
955
|
|
|
818
956
|
const supervisorArgs = buildBackgroundSupervisorArgs({
|
|
957
|
+
backend,
|
|
819
958
|
port,
|
|
820
959
|
host,
|
|
821
960
|
configDir,
|
|
@@ -861,11 +1000,6 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
861
1000
|
return;
|
|
862
1001
|
}
|
|
863
1002
|
|
|
864
|
-
// Compute server path
|
|
865
|
-
const outputDir = __dirname;
|
|
866
|
-
const serverPath = join(outputDir, "../.output/server/index.mjs");
|
|
867
|
-
const serverCommand = resolveServerCommand(outputDir, serverPath);
|
|
868
|
-
|
|
869
1003
|
// Start the server with the branded Windows runtime when postinstall created it.
|
|
870
1004
|
const serverEnv = { ...process.env };
|
|
871
1005
|
const currentCliEntry = process.env[AGENT_INSPECTOR_CLI_ENTRY_ENV] ?? process.argv[1];
|
|
@@ -902,6 +1036,7 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
902
1036
|
serverEnv["AGENT_INSPECTOR_PUBLIC_PORT"] = String(port);
|
|
903
1037
|
serverEnv["PROXY_PORT"] = String(port);
|
|
904
1038
|
serverEnv["AGENT_INSPECTOR_CAPTURE_MODE"] = captureMode;
|
|
1039
|
+
serverEnv[AGENT_INSPECTOR_BACKEND_ENV] = backend;
|
|
905
1040
|
serverEnv["AGENT_INSPECTOR_BACKEND_ONLY"] = serveUi ? "0" : "1";
|
|
906
1041
|
const workerDir = join(__dirname, "workers");
|
|
907
1042
|
if (existsSync(workerDir)) {
|
|
@@ -913,9 +1048,13 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
913
1048
|
? background
|
|
914
1049
|
? ["ignore", "ignore", "ignore"]
|
|
915
1050
|
: ["ignore", "pipe", "pipe"]
|
|
916
|
-
:
|
|
917
|
-
?
|
|
918
|
-
|
|
1051
|
+
: backend === "typescript"
|
|
1052
|
+
? background
|
|
1053
|
+
? ["ignore", "ignore", "ignore", "ipc"]
|
|
1054
|
+
: ["ignore", "pipe", "pipe", "ipc"]
|
|
1055
|
+
: background
|
|
1056
|
+
? ["ignore", "ignore", "ignore"]
|
|
1057
|
+
: ["ignore", "pipe", "pipe"],
|
|
919
1058
|
detached: background,
|
|
920
1059
|
env: serverEnv,
|
|
921
1060
|
windowsHide: background,
|
|
@@ -947,7 +1086,7 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
947
1086
|
const requestSupervisorShutdown = (signal: "SIGINT" | "SIGTERM"): void => {
|
|
948
1087
|
if (supervisorShutdownPromise !== null) return;
|
|
949
1088
|
for (const proxy of identityProxies) stopIdentityProxyAdmission(proxy.server);
|
|
950
|
-
if (managedLaunch !== null && serverProcess.connected) {
|
|
1089
|
+
if (managedLaunch !== null && backend === "typescript" && serverProcess.connected) {
|
|
951
1090
|
serverProcess.send(managedInstanceShutdownMessage(managedLaunch.controlToken), (error) => {
|
|
952
1091
|
if (error !== null && serverProcess.exitCode === null) serverProcess.kill(signal);
|
|
953
1092
|
});
|
|
@@ -994,6 +1133,7 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
994
1133
|
controlToken: managedLaunch.controlToken,
|
|
995
1134
|
supervisorPid: process.pid,
|
|
996
1135
|
startedAt: managedLaunch.startedAt,
|
|
1136
|
+
backend,
|
|
997
1137
|
publicPort: port,
|
|
998
1138
|
upstreamPort,
|
|
999
1139
|
requestShutdown: () => requestSupervisorShutdown("SIGTERM"),
|
|
@@ -1008,6 +1148,7 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
1008
1148
|
uiOriginAllowlist.origins,
|
|
1009
1149
|
uiDirectory,
|
|
1010
1150
|
publicOrigin,
|
|
1151
|
+
backend,
|
|
1011
1152
|
);
|
|
1012
1153
|
if (primaryProxy === null) {
|
|
1013
1154
|
console.error(
|
|
@@ -1032,6 +1173,7 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
1032
1173
|
uiOriginAllowlist.origins,
|
|
1033
1174
|
uiDirectory,
|
|
1034
1175
|
publicOrigin,
|
|
1176
|
+
backend,
|
|
1035
1177
|
);
|
|
1036
1178
|
if (legacyProxy !== null) {
|
|
1037
1179
|
identityProxies.push({ label: "legacy", port: legacyAliasPort, server: legacyProxy });
|
|
@@ -1104,6 +1246,7 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
1104
1246
|
`${serveUi ? "UI and backend" : "Backend control and agent runtime"} running at ${surfaceBaseUrl}`,
|
|
1105
1247
|
);
|
|
1106
1248
|
console.log(` Capture mode: ${captureMode}`);
|
|
1249
|
+
console.log(` Backend: ${backend}`);
|
|
1107
1250
|
console.log(` Proxy: ${proxyUrl}`);
|
|
1108
1251
|
printAccessHints(port, host, basePath, serveUi, publicOrigin);
|
|
1109
1252
|
if (legacyUrl !== null && legacyProxyUrl !== null && legacyMcpUrl !== null) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const AGENT_INSPECTOR_BACKEND_ENV = "AGENT_INSPECTOR_BACKEND";
|
|
4
|
+
export const AGENT_INSPECTOR_BACKEND_HEADER = "x-agent-inspector-backend";
|
|
5
|
+
|
|
6
|
+
export const BackendImplementationSchema = z.enum(["typescript", "rust"]);
|
|
7
|
+
|
|
8
|
+
export type BackendImplementation = z.infer<typeof BackendImplementationSchema>;
|
|
9
|
+
|
|
10
|
+
export function parseBackendImplementation(
|
|
11
|
+
value: string | undefined,
|
|
12
|
+
): BackendImplementation | null {
|
|
13
|
+
if (value === undefined) return "typescript";
|
|
14
|
+
const parsed = BackendImplementationSchema.safeParse(value.trim().toLowerCase());
|
|
15
|
+
return parsed.success ? parsed.data : null;
|
|
16
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { BackendImplementationSchema } from "./backendImplementation";
|
|
2
3
|
import { normalizePublicBasePath } from "./basePath";
|
|
3
4
|
|
|
4
5
|
export const INSTANCE_API_SCHEMA_VERSION = 1;
|
|
@@ -43,6 +44,7 @@ export const InstanceStatusOutputSchema = z
|
|
|
43
44
|
upstreamPort: z.number().int().min(1).max(65_535),
|
|
44
45
|
basePath: z.string().refine(isCanonicalPublicBasePath, "Invalid public Base Path"),
|
|
45
46
|
captureMode: z.enum(["simple", "full"]),
|
|
47
|
+
backend: BackendImplementationSchema.default("typescript"),
|
|
46
48
|
uiEnabled: z.boolean(),
|
|
47
49
|
dataDir: z.string().min(1),
|
|
48
50
|
supervisorPid: z.number().int().positive().nullable(),
|
|
@@ -124,6 +126,7 @@ export const InstanceStartRequestSchema = z
|
|
|
124
126
|
)
|
|
125
127
|
.optional(),
|
|
126
128
|
captureMode: z.enum(["simple", "full"]).optional(),
|
|
129
|
+
backend: BackendImplementationSchema.optional(),
|
|
127
130
|
uiEnabled: z.boolean().optional(),
|
|
128
131
|
dataDir: z.string().trim().min(1).max(32_767).optional(),
|
|
129
132
|
})
|
|
@@ -5,6 +5,7 @@ export const MANAGED_INSTANCE_SHUTDOWN_MESSAGE_TYPE = "agent-inspector:managed-s
|
|
|
5
5
|
export const AGENT_INSPECTOR_CLI_ENTRY_ENV = "AGENT_INSPECTOR_CLI_ENTRY";
|
|
6
6
|
|
|
7
7
|
export const MANAGED_INSTANCE_ENV = {
|
|
8
|
+
backend: "AGENT_INSPECTOR_INSTANCE_BACKEND",
|
|
8
9
|
name: "AGENT_INSPECTOR_INSTANCE_NAME",
|
|
9
10
|
instanceId: "AGENT_INSPECTOR_INSTANCE_ID",
|
|
10
11
|
launchId: "AGENT_INSPECTOR_INSTANCE_LAUNCH_ID",
|
|
@@ -16,6 +17,7 @@ export const MANAGED_INSTANCE_ENV = {
|
|
|
16
17
|
type ManagedInstanceEnv = Readonly<Record<string, string | undefined>>;
|
|
17
18
|
|
|
18
19
|
export type ManagedInstanceLaunch = {
|
|
20
|
+
backend: BackendImplementation;
|
|
19
21
|
name: string;
|
|
20
22
|
instanceId: string;
|
|
21
23
|
launchId: string;
|
|
@@ -47,6 +49,7 @@ export function readManagedInstanceLaunch(
|
|
|
47
49
|
env: ManagedInstanceEnv = process.env,
|
|
48
50
|
): ManagedInstanceLaunchResult {
|
|
49
51
|
const values = [
|
|
52
|
+
env[MANAGED_INSTANCE_ENV.backend],
|
|
50
53
|
env[MANAGED_INSTANCE_ENV.name],
|
|
51
54
|
env[MANAGED_INSTANCE_ENV.instanceId],
|
|
52
55
|
env[MANAGED_INSTANCE_ENV.launchId],
|
|
@@ -59,6 +62,9 @@ export function readManagedInstanceLaunch(
|
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
const name = nonEmpty(env[MANAGED_INSTANCE_ENV.name]);
|
|
65
|
+
const backend = parseBackendImplementation(
|
|
66
|
+
nonEmpty(env[MANAGED_INSTANCE_ENV.backend]) ?? undefined,
|
|
67
|
+
);
|
|
62
68
|
const instanceId = nonEmpty(env[MANAGED_INSTANCE_ENV.instanceId]);
|
|
63
69
|
const launchId = nonEmpty(env[MANAGED_INSTANCE_ENV.launchId]);
|
|
64
70
|
const controlToken = nonEmpty(env[MANAGED_INSTANCE_ENV.controlToken]);
|
|
@@ -66,6 +72,7 @@ export function readManagedInstanceLaunch(
|
|
|
66
72
|
const upstreamPort = Number(env[MANAGED_INSTANCE_ENV.upstreamPort]);
|
|
67
73
|
if (
|
|
68
74
|
name === null ||
|
|
75
|
+
backend === null ||
|
|
69
76
|
instanceId === null ||
|
|
70
77
|
launchId === null ||
|
|
71
78
|
controlToken === null ||
|
|
@@ -83,7 +90,7 @@ export function readManagedInstanceLaunch(
|
|
|
83
90
|
}
|
|
84
91
|
return {
|
|
85
92
|
kind: "managed",
|
|
86
|
-
launch: { name, instanceId, launchId, controlToken, startedAt, upstreamPort },
|
|
93
|
+
launch: { backend, name, instanceId, launchId, controlToken, startedAt, upstreamPort },
|
|
87
94
|
};
|
|
88
95
|
}
|
|
89
96
|
|
|
@@ -102,3 +109,4 @@ export function parseManagedInstanceShutdownMessage(
|
|
|
102
109
|
? { type: MANAGED_INSTANCE_SHUTDOWN_MESSAGE_TYPE, controlToken: value["controlToken"] }
|
|
103
110
|
: null;
|
|
104
111
|
}
|
|
112
|
+
import { parseBackendImplementation, type BackendImplementation } from "./backendImplementation";
|
|
@@ -7,6 +7,10 @@ import {
|
|
|
7
7
|
BROWSER_RUNTIME_SCHEMA_VERSION,
|
|
8
8
|
type BrowserRuntimeDeployment,
|
|
9
9
|
} from "../lib/browserRuntimeContract";
|
|
10
|
+
import {
|
|
11
|
+
AGENT_INSPECTOR_BACKEND_HEADER,
|
|
12
|
+
type BackendImplementation,
|
|
13
|
+
} from "../lib/backendImplementation";
|
|
10
14
|
import {
|
|
11
15
|
AGENT_INSPECTOR_BASE_PATH_META_NAME,
|
|
12
16
|
AGENT_INSPECTOR_PUBLIC_BASE_PATH_HEADER,
|
|
@@ -39,6 +43,8 @@ const CSRF_HEADER = "x-agent-inspector-csrf";
|
|
|
39
43
|
const DEFAULT_MAX_REQUEST_BYTES = 64 * 1024 * 1024;
|
|
40
44
|
const DEFAULT_REQUEST_TIMEOUT_MS = 120_000;
|
|
41
45
|
const ALIAS_CONTROL_MAX_REQUEST_BYTES = 8 * 1024;
|
|
46
|
+
const INSTANCE_API_MAX_REQUEST_BYTES = 64 * 1024;
|
|
47
|
+
const LOCAL_CONTROL_MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
42
48
|
|
|
43
49
|
export const IDENTITY_PROXY_CONTROL_NAMESPACE = "/.agent-inspector";
|
|
44
50
|
export const IDENTITY_PROXY_ALIAS_CONTROL_PATH = `${IDENTITY_PROXY_CONTROL_NAMESPACE}/base-path-aliases`;
|
|
@@ -480,6 +486,8 @@ async function tryServeUiAsset(
|
|
|
480
486
|
}
|
|
481
487
|
|
|
482
488
|
export type IdentityProxyOptions = {
|
|
489
|
+
/** Backend child implementation supervised behind this protected ingress. */
|
|
490
|
+
backendImplementation?: BackendImplementation;
|
|
483
491
|
/** Public port to listen on. Clients point at this. */
|
|
484
492
|
listenPort: number;
|
|
485
493
|
/** Interface exposed to clients. Defaults are chosen by the CLI. */
|
|
@@ -514,9 +522,12 @@ export type IdentityProxyOptions = {
|
|
|
514
522
|
aliasRegistry?: BasePathAliasRegistry;
|
|
515
523
|
/** Private lifecycle identity exposed only to the owning local instance manager. */
|
|
516
524
|
managedInstance?: ManagedInstanceControlOptions;
|
|
525
|
+
/** Optional Node-supervisor instance API retained when the selected backend is Rust. */
|
|
526
|
+
instanceApiHandler?: (request: Request) => Promise<Response>;
|
|
517
527
|
};
|
|
518
528
|
|
|
519
529
|
export type ManagedInstanceControlOptions = {
|
|
530
|
+
backend?: BackendImplementation;
|
|
520
531
|
name: string;
|
|
521
532
|
instanceId: string;
|
|
522
533
|
launchId: string;
|
|
@@ -900,6 +911,128 @@ function readAliasControlBody(req: http.IncomingMessage): Promise<AliasControlBo
|
|
|
900
911
|
});
|
|
901
912
|
}
|
|
902
913
|
|
|
914
|
+
type LocalControlBodyResult =
|
|
915
|
+
| { ok: true; body: Buffer }
|
|
916
|
+
| { ok: false; status: number; error: string };
|
|
917
|
+
|
|
918
|
+
function readLocalControlBody(
|
|
919
|
+
req: http.IncomingMessage,
|
|
920
|
+
maxBytes: number,
|
|
921
|
+
timeoutMs: number,
|
|
922
|
+
): Promise<LocalControlBodyResult> {
|
|
923
|
+
return new Promise((resolve) => {
|
|
924
|
+
const chunks: Buffer[] = [];
|
|
925
|
+
let receivedBytes = 0;
|
|
926
|
+
let settled = false;
|
|
927
|
+
const timer = setTimeout(() => {
|
|
928
|
+
finish({ ok: false, status: 408, error: "Instance request body timed out" });
|
|
929
|
+
req.resume();
|
|
930
|
+
}, timeoutMs);
|
|
931
|
+
const finish = (result: LocalControlBodyResult): void => {
|
|
932
|
+
if (settled) return;
|
|
933
|
+
settled = true;
|
|
934
|
+
clearTimeout(timer);
|
|
935
|
+
resolve(result);
|
|
936
|
+
};
|
|
937
|
+
req.on("data", (chunk: unknown) => {
|
|
938
|
+
if (settled) return;
|
|
939
|
+
const buffer = toBuffer(chunk);
|
|
940
|
+
receivedBytes += buffer.byteLength;
|
|
941
|
+
if (receivedBytes > maxBytes) {
|
|
942
|
+
finish({
|
|
943
|
+
ok: false,
|
|
944
|
+
status: 413,
|
|
945
|
+
error: `Instance request body exceeds ${String(maxBytes)} bytes`,
|
|
946
|
+
});
|
|
947
|
+
req.resume();
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
chunks.push(buffer);
|
|
951
|
+
});
|
|
952
|
+
req.on("end", () => finish({ ok: true, body: Buffer.concat(chunks) }));
|
|
953
|
+
req.on("aborted", () =>
|
|
954
|
+
finish({ ok: false, status: 400, error: "Client aborted instance request body" }),
|
|
955
|
+
);
|
|
956
|
+
req.on("error", () =>
|
|
957
|
+
finish({ ok: false, status: 400, error: "Instance request body failed" }),
|
|
958
|
+
);
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function writeLocalControlError(
|
|
963
|
+
res: http.ServerResponse,
|
|
964
|
+
status: number,
|
|
965
|
+
error: string,
|
|
966
|
+
allowedUiOrigin: string | null,
|
|
967
|
+
): void {
|
|
968
|
+
res.writeHead(status, {
|
|
969
|
+
"content-type": "application/json; charset=utf-8",
|
|
970
|
+
"cache-control": "no-store",
|
|
971
|
+
...uiCorsHeaders(allowedUiOrigin),
|
|
972
|
+
});
|
|
973
|
+
res.end(JSON.stringify({ error }));
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
async function handleLocalInstanceApi(
|
|
977
|
+
req: http.IncomingMessage,
|
|
978
|
+
res: http.ServerResponse,
|
|
979
|
+
handler: (request: Request) => Promise<Response>,
|
|
980
|
+
upstreamPath: string,
|
|
981
|
+
requestPublicOrigin: string,
|
|
982
|
+
allowedUiOrigin: string | null,
|
|
983
|
+
maxRequestBytes: number,
|
|
984
|
+
requestTimeoutMs: number,
|
|
985
|
+
): Promise<void> {
|
|
986
|
+
const body = await readLocalControlBody(
|
|
987
|
+
req,
|
|
988
|
+
Math.min(maxRequestBytes, INSTANCE_API_MAX_REQUEST_BYTES),
|
|
989
|
+
requestTimeoutMs,
|
|
990
|
+
);
|
|
991
|
+
if (!body.ok) {
|
|
992
|
+
writeLocalControlError(res, body.status, body.error, allowedUiOrigin);
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
996
|
+
const headers = new Headers();
|
|
997
|
+
const contentType = firstHeader(req.headers["content-type"]);
|
|
998
|
+
if (contentType !== undefined) headers.set("content-type", contentType);
|
|
999
|
+
const request = new Request(new URL(upstreamPath, requestPublicOrigin), {
|
|
1000
|
+
method,
|
|
1001
|
+
headers,
|
|
1002
|
+
body: method === "GET" || method === "HEAD" ? undefined : body.body.toString("utf8"),
|
|
1003
|
+
});
|
|
1004
|
+
let response: Response;
|
|
1005
|
+
try {
|
|
1006
|
+
response = await handler(request);
|
|
1007
|
+
} catch {
|
|
1008
|
+
writeLocalControlError(res, 503, "Instance supervisor request failed", allowedUiOrigin);
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
const responseBody = Buffer.from(await response.arrayBuffer());
|
|
1012
|
+
if (responseBody.byteLength > LOCAL_CONTROL_MAX_RESPONSE_BYTES) {
|
|
1013
|
+
writeLocalControlError(
|
|
1014
|
+
res,
|
|
1015
|
+
502,
|
|
1016
|
+
"Instance supervisor response exceeded its resource limit",
|
|
1017
|
+
allowedUiOrigin,
|
|
1018
|
+
);
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
const responseHeaders: Record<string, string> = {
|
|
1022
|
+
...uiCorsHeaders(allowedUiOrigin),
|
|
1023
|
+
"content-length": String(responseBody.byteLength),
|
|
1024
|
+
};
|
|
1025
|
+
response.headers.forEach((value, name) => {
|
|
1026
|
+
const lowerName = name.toLowerCase();
|
|
1027
|
+
if (!REQUEST_HOP_BY_HOP.has(lowerName) && lowerName !== "content-length") {
|
|
1028
|
+
responseHeaders[lowerName] = value;
|
|
1029
|
+
}
|
|
1030
|
+
});
|
|
1031
|
+
responseHeaders["cache-control"] = "no-store";
|
|
1032
|
+
res.writeHead(response.status, responseHeaders);
|
|
1033
|
+
res.end(responseBody);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
903
1036
|
function isUnknownRecord(value: unknown): value is Record<string, unknown> {
|
|
904
1037
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
905
1038
|
}
|
|
@@ -1070,6 +1203,7 @@ function handleManagedInstanceControlRequest(
|
|
|
1070
1203
|
}
|
|
1071
1204
|
writeManagedInstanceControlResponse(res, 200, {
|
|
1072
1205
|
schemaVersion: 1,
|
|
1206
|
+
backend: managedInstance.backend ?? "typescript",
|
|
1073
1207
|
name: managedInstance.name,
|
|
1074
1208
|
instanceId: managedInstance.instanceId,
|
|
1075
1209
|
launchId: managedInstance.launchId,
|
|
@@ -1483,6 +1617,19 @@ async function handleRequest(
|
|
|
1483
1617
|
options.requestTimeoutMs,
|
|
1484
1618
|
DEFAULT_REQUEST_TIMEOUT_MS,
|
|
1485
1619
|
);
|
|
1620
|
+
if (options.instanceApiHandler !== undefined && isInstanceManagementPath(upstreamPathname)) {
|
|
1621
|
+
await handleLocalInstanceApi(
|
|
1622
|
+
req,
|
|
1623
|
+
res,
|
|
1624
|
+
options.instanceApiHandler,
|
|
1625
|
+
upstreamPath,
|
|
1626
|
+
requestPublicOrigin,
|
|
1627
|
+
allowedUiOrigin,
|
|
1628
|
+
maxRequestBytes,
|
|
1629
|
+
requestTimeoutMs,
|
|
1630
|
+
);
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1486
1633
|
const proxyReq = http.request(
|
|
1487
1634
|
{
|
|
1488
1635
|
hostname: options.upstreamHost,
|
|
@@ -1554,6 +1701,7 @@ async function handleRequest(
|
|
|
1554
1701
|
for (const [name, value] of Object.entries(BASE_SECURITY_HEADERS)) {
|
|
1555
1702
|
outHeaders[name] = value;
|
|
1556
1703
|
}
|
|
1704
|
+
outHeaders[AGENT_INSPECTOR_BACKEND_HEADER] = options.backendImplementation ?? "typescript";
|
|
1557
1705
|
if (allowedUiOrigin !== null && isUiControlPath(upstreamPathname)) {
|
|
1558
1706
|
outHeaders["access-control-allow-origin"] = allowedUiOrigin;
|
|
1559
1707
|
outHeaders["access-control-expose-headers"] = "Content-Disposition";
|
|
@@ -278,10 +278,6 @@ export async function archiveSessionLogs(logs: readonly CapturedLog[]): Promise<
|
|
|
278
278
|
return archivedCount;
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
-
export async function archiveSessionLog(log: CapturedLog): Promise<boolean> {
|
|
282
|
-
return (await archiveSessionLogs([log])) === 1;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
281
|
function readString(row: unknown, name: string): string | null {
|
|
286
282
|
const value = readProperty(row, name);
|
|
287
283
|
return typeof value === "string" ? value : null;
|
|
@@ -481,7 +477,3 @@ export async function clearSessionArchives(): Promise<number> {
|
|
|
481
477
|
return 0;
|
|
482
478
|
}
|
|
483
479
|
}
|
|
484
|
-
|
|
485
|
-
export function _resetSessionArchiveCacheForTests(): void {
|
|
486
|
-
cachedArchiveMax = null;
|
|
487
|
-
}
|