@tonyclaw/agent-inspector 3.0.46 → 3.0.48

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 (40) hide show
  1. package/.output/backend/nitro.json +1 -1
  2. package/.output/cli.js +6791 -6006
  3. package/.output/server/_ssr/index.mjs +1 -1
  4. package/.output/server/_ssr/{router-D18yUq36.mjs → router-Cj10mv-Z.mjs} +334 -5
  5. package/.output/server/index.mjs +1 -1
  6. package/.output/ui/assets/{CompareDrawer-DAUuIJ6G.js → CompareDrawer-uRiXFOCp.js} +1 -1
  7. package/.output/ui/assets/{InspectorPet-BRBVjOWI.js → InspectorPet-j-r6xk0V.js} +1 -1
  8. package/.output/ui/assets/{ProxyViewerContainer-D7Sq0ctc.js → ProxyViewerContainer-DnUrkMJF.js} +23 -23
  9. package/.output/ui/assets/{ReplayDialog-nDLsjOhs.js → ReplayDialog-B2URyHMv.js} +1 -1
  10. package/.output/ui/assets/{RequestAnatomy-zd4BDgL0.js → RequestAnatomy-CK0M0CJO.js} +1 -1
  11. package/.output/ui/assets/{ResponseView-AdIXGcSn.js → ResponseView-CYSVA4lT.js} +1 -1
  12. package/.output/ui/assets/{StreamingChunkSequence-CesTz8He.js → StreamingChunkSequence-CHf54ZUV.js} +1 -1
  13. package/.output/ui/assets/{_sessionId-QnBomgPD.js → _sessionId-BXULpTNP.js} +1 -1
  14. package/.output/ui/assets/{_sessionId-BAfzrhSU.js → _sessionId-E4lbj5-I.js} +1 -1
  15. package/.output/ui/assets/{index-D0rtCN9V.js → index-B4ixI3kG.js} +1 -1
  16. package/.output/ui/assets/{index-BcsdxBAd.js → index-BJ-71WLq.js} +1 -1
  17. package/.output/ui/assets/{index-DHpwr08Z.js → index-CW0ab9r3.js} +2 -2
  18. package/.output/ui/assets/{index-dNuk2dsU.js → index-Ce-lO51c.js} +1 -1
  19. package/.output/ui/assets/{json-viewer-BvFHglMb.js → json-viewer-C6E8aLaV.js} +1 -1
  20. package/.output/ui/assets/{jszip.min-B5Z7gNZB.js → jszip.min-DjWdTBbp.js} +1 -1
  21. package/.output/ui/index.html +1 -1
  22. package/README.md +53 -0
  23. package/package.json +17 -3
  24. package/src/backend/routes/api/-instances.ts +7 -109
  25. package/src/cli/instance.ts +21 -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 +458 -0
  31. package/src/cli.ts +171 -14
  32. package/src/components/ProxyViewer.tsx +4 -13
  33. package/src/components/proxy-viewer/proxyViewerLogic.ts +9 -0
  34. package/src/lib/backendImplementation.ts +16 -0
  35. package/src/lib/instanceContract.ts +3 -0
  36. package/src/lib/managedInstance.ts +9 -1
  37. package/src/mcp/instanceHandlers.ts +1 -0
  38. package/src/proxy/identityProxy.ts +148 -0
  39. package/src/proxy/sessionArchive.ts +0 -8
  40. package/src/proxy/sqliteLogIndex.ts +0 -40
@@ -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
- }
@@ -605,46 +605,6 @@ export async function syncSqliteLogIndexEntries(
605
605
  }
606
606
  }
607
607
 
608
- export async function findSqliteLogIndexEntry(id: number): Promise<LogIndexEntry | null> {
609
- const state = await getSqliteState();
610
- if (state.status !== "ready") return null;
611
- const row = getPrepared(state.db, "SELECT * FROM log_index WHERE id = ?", [id]);
612
- return entryFromRow(row);
613
- }
614
-
615
- export async function listSqliteLogIndexEntries({
616
- sessionId,
617
- model,
618
- }: {
619
- sessionId?: string;
620
- model?: string;
621
- } = {}): Promise<LogIndexEntry[] | null> {
622
- const state = await getSqliteState();
623
- if (state.status !== "ready") return null;
624
-
625
- const clauses: string[] = [];
626
- const params: unknown[] = [];
627
- if (sessionId !== undefined) {
628
- clauses.push("session_id = ?");
629
- params.push(sessionId);
630
- }
631
- if (model !== undefined) {
632
- clauses.push("model = ?");
633
- params.push(model);
634
- }
635
-
636
- const where = clauses.length === 0 ? "" : ` WHERE ${clauses.join(" AND ")}`;
637
- const rows = allPrepared(state.db, `SELECT * FROM log_index${where} ORDER BY id ASC`, params);
638
- if (rows === null) return null;
639
-
640
- const entries: LogIndexEntry[] = [];
641
- for (const row of rows) {
642
- const entry = entryFromRow(row);
643
- if (entry !== null) entries.push(entry);
644
- }
645
- return entries;
646
- }
647
-
648
608
  export async function replaceSqliteLogIndexEntries(
649
609
  entries: readonly LogIndexEntry[],
650
610
  ): Promise<boolean> {