@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.
Files changed (38) hide show
  1. package/.output/backend/nitro.json +1 -1
  2. package/.output/cli.js +6526 -5769
  3. package/.output/server/_ssr/index.mjs +1 -1
  4. package/.output/server/_ssr/{router-D18yUq36.mjs → router-BwZEsYfS.mjs} +32 -5
  5. package/.output/server/index.mjs +1 -1
  6. package/.output/ui/assets/{CompareDrawer-DAUuIJ6G.js → CompareDrawer-V-8lPASC.js} +1 -1
  7. package/.output/ui/assets/{InspectorPet-BRBVjOWI.js → InspectorPet-D4CVnfdf.js} +1 -1
  8. package/.output/ui/assets/{ProxyViewerContainer-D7Sq0ctc.js → ProxyViewerContainer-CO3ZVHLX.js} +4 -4
  9. package/.output/ui/assets/{ReplayDialog-nDLsjOhs.js → ReplayDialog-CpGyTLtt.js} +1 -1
  10. package/.output/ui/assets/{RequestAnatomy-zd4BDgL0.js → RequestAnatomy-BLuJL9dZ.js} +1 -1
  11. package/.output/ui/assets/{ResponseView-AdIXGcSn.js → ResponseView-BuSC-CH7.js} +1 -1
  12. package/.output/ui/assets/{StreamingChunkSequence-CesTz8He.js → StreamingChunkSequence-DeA-JJKa.js} +1 -1
  13. package/.output/ui/assets/{_sessionId-BAfzrhSU.js → _sessionId-8HuHFhjo.js} +1 -1
  14. package/.output/ui/assets/{_sessionId-QnBomgPD.js → _sessionId-Chp67th6.js} +1 -1
  15. package/.output/ui/assets/{index-dNuk2dsU.js → index-DPmKK_5d.js} +1 -1
  16. package/.output/ui/assets/{index-BcsdxBAd.js → index-atoQrlb8.js} +1 -1
  17. package/.output/ui/assets/{index-DHpwr08Z.js → index-nY3E1vxV.js} +2 -2
  18. package/.output/ui/assets/{index-D0rtCN9V.js → index-nl9-dpZn.js} +1 -1
  19. package/.output/ui/assets/{json-viewer-BvFHglMb.js → json-viewer-B9kIzqeX.js} +1 -1
  20. package/.output/ui/assets/{jszip.min-B5Z7gNZB.js → jszip.min-B7tfyE-I.js} +1 -1
  21. package/.output/ui/index.html +1 -1
  22. package/README.md +7 -0
  23. package/package.json +17 -3
  24. package/src/backend/routes/api/-instances.ts +7 -109
  25. package/src/cli/instance.ts +10 -0
  26. package/src/cli/instanceApi.ts +213 -0
  27. package/src/cli/instanceArgs.ts +16 -0
  28. package/src/cli/instanceControl.ts +10 -0
  29. package/src/cli/instanceModel.ts +9 -0
  30. package/src/cli/rustBackendPackage.ts +450 -0
  31. package/src/cli.ts +157 -14
  32. package/src/lib/backendImplementation.ts +16 -0
  33. package/src/lib/instanceContract.ts +3 -0
  34. package/src/lib/managedInstance.ts +9 -1
  35. package/src/mcp/instanceHandlers.ts +1 -0
  36. package/src/proxy/identityProxy.ts +148 -0
  37. package/src/proxy/sessionArchive.ts +0 -8
  38. 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
- function resolveServerCommand(outputDir: string, serverPath: string): ServerCommand {
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 process.platform === "win32" && existsSync(brandedRuntime)
299
- ? { command: brandedRuntime, args: [serverPath] }
300
- : { command: process.execPath, args: [serverPath] };
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 = ["--no-open", "--port", String(options.port), "--mode", options.captureMode];
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
- : background
917
- ? ["ignore", "ignore", "ignore", "ipc"]
918
- : ["ignore", "pipe", "pipe", "ipc"],
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";
@@ -90,6 +90,7 @@ function lifecycleRequest(input: InstanceLifecycleToolInput): InstanceStartReque
90
90
  basePath: input.basePath,
91
91
  host: input.host,
92
92
  captureMode: input.captureMode,
93
+ backend: input.backend,
93
94
  uiEnabled: input.uiEnabled,
94
95
  dataDir: input.dataDir,
95
96
  };
@@ -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
- }