@tonyclaw/agent-inspector 3.0.29 → 3.0.31

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 (156) hide show
  1. package/.output/cli.js +791 -191
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/CompareDrawer-DB2KggLE.js +1 -0
  4. package/.output/public/assets/InspectorPet-CfwDw6kD.js +4108 -0
  5. package/.output/public/assets/ProxyViewerContainer-PcrO96_F.js +126 -0
  6. package/.output/public/assets/{ReplayDialog-CctnITbt.js → ReplayDialog-3Pl7EKjp.js} +1 -1
  7. package/.output/public/assets/{RequestAnatomy-BrN0RBbL.js → RequestAnatomy-BcPjcHLX.js} +1 -1
  8. package/.output/public/assets/ResponseView-DOx51wV3.js +2 -0
  9. package/.output/public/assets/StreamingChunkSequence-LkYJWM1A.js +1 -0
  10. package/.output/public/assets/_sessionId-BSg32vC6.js +1 -0
  11. package/.output/public/assets/{_sessionId-DKM91udC.js → _sessionId-C0CA68QV.js} +1 -1
  12. package/.output/public/assets/{index-Bq-nqolG.js → index-BpBUqdAB.js} +1 -1
  13. package/.output/public/assets/index-D93dCgAQ.js +1 -0
  14. package/.output/public/assets/{index-DPiESBo2.js → index-FV5sxqx3.js} +6 -6
  15. package/.output/public/assets/index-VUsF4nhf.js +76 -0
  16. package/.output/public/assets/index-hyQHZeQI.css +1 -0
  17. package/.output/public/assets/{json-viewer-CfaDJwx8.js → json-viewer-WctIbAh_.js} +1 -1
  18. package/.output/public/assets/jszip.min-DnSIoTix.js +2 -0
  19. package/.output/public/assets/qwen-mMn3f5ul.webp +0 -0
  20. package/.output/server/_libs/jszip.mjs +35 -9
  21. package/.output/server/_libs/lucide-react.mjs +80 -80
  22. package/.output/server/_libs/radix-ui__react-dialog.mjs +3 -3
  23. package/.output/server/_libs/tanstack__react-router.mjs +1 -1
  24. package/.output/server/{_sessionId-CwGXPD4C.mjs → _sessionId-BHBK3VbF.mjs} +15 -15
  25. package/.output/server/_sessionId-CyuJ6Kfl.mjs +81 -0
  26. package/.output/server/_ssr/{CompareDrawer-R_P1-VNr.mjs → CompareDrawer-27amlv_a.mjs} +138 -103
  27. package/.output/server/_ssr/InspectorPet-CQnGXfLs.mjs +925 -0
  28. package/.output/server/_ssr/{ProxyViewerContainer-DUgxRNbj.mjs → ProxyViewerContainer-DlAl0uF-.mjs} +1569 -1646
  29. package/.output/server/_ssr/{ReplayDialog-De9wKKzS.mjs → ReplayDialog-5m2jtlLa.mjs} +18 -18
  30. package/.output/server/_ssr/{RequestAnatomy-Db4MxUYo.mjs → RequestAnatomy--1WtQNY9.mjs} +58 -58
  31. package/.output/server/_ssr/{ResponseView-8rDXOiNB.mjs → ResponseView-CgbqS8sr.mjs} +60 -71
  32. package/.output/server/_ssr/{StreamingChunkSequence-N9teZHkA.mjs → StreamingChunkSequence-BwpnopWx.mjs} +16 -16
  33. package/.output/server/_ssr/{index-D4DvByuW.mjs → index-CNVi1TzT.mjs} +15 -15
  34. package/.output/server/_ssr/index-DMCjH_4V.mjs +81 -0
  35. package/.output/server/_ssr/index.mjs +2 -2
  36. package/.output/server/_ssr/{json-viewer-BjoHRbjS.mjs → json-viewer-DNGN2uoe.mjs} +58 -58
  37. package/.output/server/_ssr/{router-BYlePDj8.mjs → router-CgsqLnk5.mjs} +16007 -12632
  38. package/.output/server/_tanstack-start-manifest_v-BH5f_kWV.mjs +4 -0
  39. package/.output/server/index.mjs +99 -71
  40. package/.output/workers/logFinalizer.worker.js +16908 -0
  41. package/.output/workers/sessionWorkerEntry.js +16904 -0
  42. package/README.md +21 -5
  43. package/package.json +6 -2
  44. package/src/assets/logos/qwen.webp +0 -0
  45. package/src/cli.ts +85 -10
  46. package/src/components/OnboardingBanner.tsx +41 -21
  47. package/src/components/ProxyViewer.tsx +198 -269
  48. package/src/components/ProxyViewerContainer.tsx +128 -17
  49. package/src/components/ecosystem/AgentLabDialog.tsx +166 -19
  50. package/src/components/errors/SafeErrorBoundary.tsx +201 -0
  51. package/src/components/pi-agent/PiAgentPanel.tsx +209 -35
  52. package/src/components/pi-agent/piAgentChatLogic.ts +263 -0
  53. package/src/components/providers/ImportWizardDialog.tsx +18 -42
  54. package/src/components/providers/ProviderLogo.tsx +1 -1
  55. package/src/components/providers/ProvidersPanel.tsx +8 -5
  56. package/src/components/providers/SettingsDialog.tsx +34 -11
  57. package/src/components/proxy-viewer/ApplicationBar.tsx +63 -0
  58. package/src/components/proxy-viewer/CompareDrawer.tsx +92 -49
  59. package/src/components/proxy-viewer/LiveConnectionStatus.tsx +186 -0
  60. package/src/components/proxy-viewer/LogEntry.tsx +283 -275
  61. package/src/components/proxy-viewer/RawExportWarning.tsx +77 -0
  62. package/src/components/proxy-viewer/SessionLoadProgressBar.tsx +61 -0
  63. package/src/components/proxy-viewer/ThreadConnector.tsx +16 -1
  64. package/src/components/proxy-viewer/TurnGroup.tsx +13 -2
  65. package/src/components/proxy-viewer/accessibility.ts +8 -0
  66. package/src/components/proxy-viewer/lazy.ts +4 -0
  67. package/src/components/proxy-viewer/liveConnectionState.ts +158 -0
  68. package/src/components/proxy-viewer/proxyViewerLogic.ts +10 -0
  69. package/src/components/proxy-viewer/viewerState.ts +2 -6
  70. package/src/components/ui/select.tsx +1 -1
  71. package/src/components/ui/tabs.tsx +3 -3
  72. package/src/components/ui/transient-toast.tsx +7 -6
  73. package/src/lib/apiClient.ts +17 -2
  74. package/src/lib/ecosystemContract.ts +34 -0
  75. package/src/lib/export-logs.ts +1 -1
  76. package/src/lib/piAgentContract.ts +80 -0
  77. package/src/lib/providerContract.ts +70 -4
  78. package/src/lib/providerImportContract.ts +27 -0
  79. package/src/lib/providerModelMetadata.ts +16 -7
  80. package/src/lib/resourceLimits.ts +152 -0
  81. package/src/lib/safeDiagnostic.ts +38 -0
  82. package/src/lib/useProviders.ts +4 -4
  83. package/src/mcp/server.ts +39 -6
  84. package/src/mcp/toolHandlers.ts +131 -4
  85. package/src/proxy/chunkStorage.ts +20 -6
  86. package/src/proxy/config.ts +20 -6
  87. package/src/proxy/dataDir.ts +3 -0
  88. package/src/proxy/ecosystemExecutionConfirmation.ts +114 -0
  89. package/src/proxy/ecosystemExecutionRoute.ts +116 -0
  90. package/src/proxy/ecosystemTasks.ts +48 -0
  91. package/src/proxy/evidenceExporter.ts +23 -9
  92. package/src/proxy/groupEvidenceExporter.ts +26 -5
  93. package/src/proxy/groupStore.ts +13 -3
  94. package/src/proxy/handler.ts +103 -20
  95. package/src/proxy/identityProxy.ts +333 -14
  96. package/src/proxy/logFinalizer.ts +73 -6
  97. package/src/proxy/logImportUpload.ts +128 -0
  98. package/src/proxy/logImporter.ts +321 -70
  99. package/src/proxy/logIndex.ts +16 -6
  100. package/src/proxy/logger.ts +294 -41
  101. package/src/proxy/privateDataPath.ts +183 -0
  102. package/src/proxy/providerScanStore.ts +87 -0
  103. package/src/proxy/providerSecretStore.ts +58 -19
  104. package/src/proxy/providers.ts +175 -58
  105. package/src/proxy/rawStreamCapture.ts +66 -5
  106. package/src/proxy/runStore.ts +13 -3
  107. package/src/proxy/runtimeAdmission.ts +52 -0
  108. package/src/proxy/runtimeHealth.ts +206 -0
  109. package/src/proxy/runtimeShutdown.ts +75 -0
  110. package/src/proxy/sessionArchive.ts +15 -2
  111. package/src/proxy/sessionProcess.ts +19 -0
  112. package/src/proxy/sessionRuntime.ts +7 -0
  113. package/src/proxy/shutdownCoordinator.ts +90 -0
  114. package/src/proxy/sqliteLogIndex.ts +18 -2
  115. package/src/proxy/store.ts +18 -2
  116. package/src/routes/__root.tsx +18 -1
  117. package/src/routes/api/alerts.summary.ts +12 -8
  118. package/src/routes/api/alerts.ts +27 -9
  119. package/src/routes/api/ecosystem.packages.$packageId.help.ts +3 -8
  120. package/src/routes/api/ecosystem.packages.$packageId.install.ts +3 -8
  121. package/src/routes/api/ecosystem.packages.$packageId.runner-presets.ts +3 -11
  122. package/src/routes/api/ecosystem.packages.$packageId.upgrade.ts +3 -8
  123. package/src/routes/api/ecosystem.recipes.$recipeId.run.ts +3 -8
  124. package/src/routes/api/health.ts +9 -2
  125. package/src/routes/api/knowledge.project-context.ts +11 -0
  126. package/src/routes/api/knowledge.search.ts +33 -2
  127. package/src/routes/api/logs.$id.body.ts +16 -2
  128. package/src/routes/api/logs.import.ts +7 -18
  129. package/src/routes/api/logs.stream.ts +147 -69
  130. package/src/routes/api/logs.ts +55 -14
  131. package/src/routes/api/providers.$providerId.model-metadata.ts +22 -4
  132. package/src/routes/api/providers.$providerId.ts +37 -6
  133. package/src/routes/api/providers.export.ts +34 -16
  134. package/src/routes/api/providers.import.ts +42 -8
  135. package/src/routes/api/providers.scan.ts +13 -8
  136. package/src/routes/api/providers.ts +36 -5
  137. package/src/routes/api/runs.ts +12 -12
  138. package/src/routes/api/sessions.ts +15 -8
  139. package/src/routes/index.tsx +6 -0
  140. package/src/routes/livez.ts +13 -0
  141. package/src/routes/readyz.ts +18 -0
  142. package/src/routes/session/$sessionId.tsx +6 -0
  143. package/src/services/piAgent.ts +131 -102
  144. package/src/services/piAgentRetrieval.ts +1180 -0
  145. package/styles/globals.css +19 -3
  146. package/.output/public/assets/CompareDrawer-Cb5ccPeX.js +0 -1
  147. package/.output/public/assets/ProxyViewerContainer-COBe5GTT.js +0 -4234
  148. package/.output/public/assets/ResponseView-Cqq87lX_.js +0 -3
  149. package/.output/public/assets/StreamingChunkSequence-CnTa9mCr.js +0 -1
  150. package/.output/public/assets/index-DTLNVcgc.js +0 -76
  151. package/.output/public/assets/index-DliqmwUw.css +0 -1
  152. package/.output/public/assets/qwen-CONDcHqt.png +0 -0
  153. package/.output/server/_tanstack-start-manifest_v-DMCA8KK3.mjs +0 -4
  154. package/src/assets/logos/mcp.png +0 -0
  155. package/src/assets/logos/qwen.png +0 -0
  156. package/src/components/ui/mcp-logo.tsx +0 -20
@@ -1,8 +1,9 @@
1
- import { mkdir } from "node:fs/promises";
1
+ import { existsSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { logger, resolveLogDir } from "./logger";
4
4
  import type { ApiFormat } from "./schemas";
5
5
  import type { LogIndexEntry, LogIndexSummary } from "./logIndex";
6
+ import { ensurePrivateDirectorySync, securePrivateFileSync } from "./privateDataPath";
6
7
 
7
8
  type SqliteState =
8
9
  | { status: "ready"; db: unknown; path: string }
@@ -299,10 +300,19 @@ async function initializeSqliteState(): Promise<SqliteState> {
299
300
  }
300
301
 
301
302
  try {
302
- await mkdir(dirname(path), { recursive: true });
303
+ if (!ensurePrivateDirectorySync(dirname(path))) {
304
+ return { status: "unavailable", path, reason: "SQLite index directory is not private" };
305
+ }
303
306
  const db = sqliteConstructor.constructor(path);
304
307
  execSql(db, "PRAGMA journal_mode = WAL");
305
308
  execSql(db, "PRAGMA synchronous = NORMAL");
309
+ if (!securePrivateFileSync(path, true)) {
310
+ callMethod(db, "close", []);
311
+ return { status: "unavailable", path, reason: "SQLite index file is not private" };
312
+ }
313
+ for (const sidecarPath of [`${path}-wal`, `${path}-shm`]) {
314
+ if (existsSync(sidecarPath)) void securePrivateFileSync(sidecarPath);
315
+ }
306
316
  if (!execSql(db, CREATE_SCHEMA_SQL)) {
307
317
  return { status: "unavailable", path, reason: "SQLite schema initialization failed" };
308
318
  }
@@ -326,6 +336,12 @@ async function getSqliteState(): Promise<SqliteState> {
326
336
  return await sqliteStatePromise;
327
337
  }
328
338
 
339
+ /** Report only index availability; internal paths and failure reasons remain private. */
340
+ export async function isSqliteLogIndexReady(): Promise<boolean> {
341
+ const state = await getSqliteState();
342
+ return state.status === "ready";
343
+ }
344
+
329
345
  export function closeSqliteLogIndex(): void {
330
346
  if (activeDb !== null) {
331
347
  callMethod(activeDb, "close", []);
@@ -10,7 +10,7 @@ import { open, unlink, rename } from "node:fs/promises";
10
10
  import { readdir as readdirCallback } from "node:fs/promises";
11
11
  import { createInterface } from "node:readline";
12
12
  import { Buffer } from "node:buffer";
13
- import { join } from "node:path";
13
+ import { dirname, join } from "node:path";
14
14
  import {
15
15
  appendLogEntry,
16
16
  resolveLogDir,
@@ -50,6 +50,11 @@ import {
50
50
  listArchivedSessionLogs,
51
51
  listArchivedSessionIds,
52
52
  } from "./sessionArchive";
53
+ import {
54
+ ensurePrivateDirectorySync,
55
+ PRIVATE_FILE_MODE,
56
+ securePrivateFileSync,
57
+ } from "./privateDataPath";
53
58
  import {
54
59
  clearSessionRegistry,
55
60
  getLogSessionId,
@@ -1383,7 +1388,13 @@ async function rewriteLogFileWithoutIds(
1383
1388
  ids: ReadonlySet<number>,
1384
1389
  ): Promise<RewriteLogFileResult> {
1385
1390
  const tempPath = tempRewritePath(filePath);
1386
- const writer = createWriteStream(tempPath, { encoding: "utf-8" });
1391
+ if (!ensurePrivateDirectorySync(dirname(filePath))) {
1392
+ return { rewritten: false, removedIds: new Set() };
1393
+ }
1394
+ const writer = createWriteStream(tempPath, {
1395
+ encoding: "utf-8",
1396
+ mode: PRIVATE_FILE_MODE,
1397
+ });
1387
1398
  const removedIds = new Set<number>();
1388
1399
  let keptLineCount = 0;
1389
1400
  let writeOk = true;
@@ -1415,6 +1426,11 @@ async function rewriteLogFileWithoutIds(
1415
1426
  return { rewritten: false, removedIds: new Set() };
1416
1427
  }
1417
1428
 
1429
+ if (!securePrivateFileSync(tempPath)) {
1430
+ await removeTempFile(tempPath);
1431
+ return { rewritten: false, removedIds: new Set() };
1432
+ }
1433
+
1418
1434
  if (removedIds.size === 0) {
1419
1435
  await removeTempFile(tempPath);
1420
1436
  return { rewritten: false, removedIds };
@@ -3,6 +3,7 @@ import { Outlet, createRootRoute, HeadContent, Scripts } from "@tanstack/react-r
3
3
  import { useEffect, type ReactNode } from "react";
4
4
  import { SWRConfig } from "swr";
5
5
  import faviconSvg from "../assets/favicon.svg?url";
6
+ import { SafeErrorBoundary, SafeRouteError } from "../components/errors/SafeErrorBoundary";
6
7
  import appCss from "../index.css?url";
7
8
  import { installChunkLoadRecovery } from "../lib/chunkLoadRecovery";
8
9
  import { THEME_MODE_STORAGE_KEY } from "../lib/themeMode";
@@ -94,6 +95,7 @@ export const Route = createRootRoute({
94
95
  ],
95
96
  }),
96
97
  component: RootComponent,
98
+ errorComponent: RootRouteErrorComponent,
97
99
  notFoundComponent: RootNotFoundComponent,
98
100
  });
99
101
 
@@ -102,7 +104,22 @@ function RootComponent() {
102
104
 
103
105
  return (
104
106
  <RootDocument>
105
- <Outlet />
107
+ <SafeErrorBoundary
108
+ scope="root-render"
109
+ title="Agent Inspector could not continue"
110
+ description="The application stopped this render safely. Retry or reload Inspector."
111
+ variant="page"
112
+ >
113
+ <Outlet />
114
+ </SafeErrorBoundary>
115
+ </RootDocument>
116
+ );
117
+ }
118
+
119
+ function RootRouteErrorComponent({ error, reset }: { error: Error; reset: () => void }) {
120
+ return (
121
+ <RootDocument>
122
+ <SafeRouteError error={error} reset={reset} scope="root-route" />
106
123
  </RootDocument>
107
124
  );
108
125
  }
@@ -1,21 +1,25 @@
1
1
  import { createFileRoute } from "@tanstack/react-router";
2
2
  import { getInspectorAlertSummary } from "../../services/alerts";
3
-
4
- function parsePositiveInt(value: string | null): number | undefined {
5
- if (value === null) return undefined;
6
- const parsed = Number(value);
7
- if (!Number.isInteger(parsed) || parsed < 1) return undefined;
8
- return parsed;
9
- }
3
+ import { DEFAULT_ALERT_SCAN_LIMIT, MAX_ALERT_SCAN_LIMIT } from "../../proxy/alerts";
4
+ import { parseBoundedIntegerParameter } from "../../lib/resourceLimits";
10
5
 
11
6
  export const Route = createFileRoute("/api/alerts/summary")({
12
7
  server: {
13
8
  handlers: {
14
9
  GET: async ({ request }: { request: Request }) => {
15
10
  const url = new URL(request.url);
11
+ const scanLimitResult = parseBoundedIntegerParameter({
12
+ value: url.searchParams.get("scanLimit"),
13
+ parameter: "scanLimit",
14
+ fallback: DEFAULT_ALERT_SCAN_LIMIT,
15
+ minimum: 1,
16
+ maximum: MAX_ALERT_SCAN_LIMIT,
17
+ resource: "alerts-scan-size",
18
+ });
19
+ if (!scanLimitResult.ok) return scanLimitResult.response;
16
20
  return Response.json(
17
21
  await getInspectorAlertSummary({
18
- scanLimit: parsePositiveInt(url.searchParams.get("scanLimit")),
22
+ scanLimit: scanLimitResult.value,
19
23
  }),
20
24
  );
21
25
  },
@@ -8,6 +8,13 @@ import {
8
8
  type AlertSource,
9
9
  } from "../../lib/alertContract";
10
10
  import { listInspectorAlerts } from "../../services/alerts";
11
+ import {
12
+ DEFAULT_ALERT_LIMIT,
13
+ DEFAULT_ALERT_SCAN_LIMIT,
14
+ MAX_ALERT_LIMIT,
15
+ MAX_ALERT_SCAN_LIMIT,
16
+ } from "../../proxy/alerts";
17
+ import { parseBoundedIntegerParameter } from "../../lib/resourceLimits";
11
18
 
12
19
  type ParsedFilters = {
13
20
  severity?: AlertSeverity;
@@ -15,13 +22,6 @@ type ParsedFilters = {
15
22
  source?: AlertSource;
16
23
  };
17
24
 
18
- function parsePositiveInt(value: string | null): number | undefined {
19
- if (value === null) return undefined;
20
- const parsed = Number(value);
21
- if (!Number.isInteger(parsed) || parsed < 1) return undefined;
22
- return parsed;
23
- }
24
-
25
25
  function parseFilters(url: URL): ParsedFilters {
26
26
  const severityResult = AlertSeveritySchema.safeParse(url.searchParams.get("severity"));
27
27
  const categoryResult = AlertCategorySchema.safeParse(url.searchParams.get("category"));
@@ -39,11 +39,29 @@ export const Route = createFileRoute("/api/alerts")({
39
39
  handlers: {
40
40
  GET: async ({ request }: { request: Request }) => {
41
41
  const url = new URL(request.url);
42
+ const limitResult = parseBoundedIntegerParameter({
43
+ value: url.searchParams.get("limit"),
44
+ parameter: "limit",
45
+ fallback: DEFAULT_ALERT_LIMIT,
46
+ minimum: 1,
47
+ maximum: MAX_ALERT_LIMIT,
48
+ resource: "alerts-page-size",
49
+ });
50
+ if (!limitResult.ok) return limitResult.response;
51
+ const scanLimitResult = parseBoundedIntegerParameter({
52
+ value: url.searchParams.get("scanLimit"),
53
+ parameter: "scanLimit",
54
+ fallback: DEFAULT_ALERT_SCAN_LIMIT,
55
+ minimum: 1,
56
+ maximum: MAX_ALERT_SCAN_LIMIT,
57
+ resource: "alerts-scan-size",
58
+ });
59
+ if (!scanLimitResult.ok) return scanLimitResult.response;
42
60
  return Response.json(
43
61
  await listInspectorAlerts({
44
62
  ...parseFilters(url),
45
- limit: parsePositiveInt(url.searchParams.get("limit")),
46
- scanLimit: parsePositiveInt(url.searchParams.get("scanLimit")),
63
+ limit: limitResult.value,
64
+ scanLimit: scanLimitResult.value,
47
65
  }),
48
66
  );
49
67
  },
@@ -1,16 +1,11 @@
1
1
  import { createFileRoute } from "@tanstack/react-router";
2
- import { startEcosystemTask } from "../../proxy/ecosystemTasks";
2
+ import { handleEcosystemPackageExecution } from "../../proxy/ecosystemExecutionRoute";
3
3
 
4
4
  export const Route = createFileRoute("/api/ecosystem/packages/$packageId/help")({
5
5
  server: {
6
6
  handlers: {
7
- POST: ({ params }: { params: { packageId: string } }) => {
8
- const task = startEcosystemTask(params.packageId, "help");
9
- if (task === null) {
10
- return Response.json({ error: "Unknown ecosystem package" }, { status: 404 });
11
- }
12
- return Response.json({ task });
13
- },
7
+ POST: ({ params, request }: { params: { packageId: string }; request: Request }) =>
8
+ handleEcosystemPackageExecution(request, params.packageId, "help"),
14
9
  },
15
10
  },
16
11
  });
@@ -1,16 +1,11 @@
1
1
  import { createFileRoute } from "@tanstack/react-router";
2
- import { startEcosystemTask } from "../../proxy/ecosystemTasks";
2
+ import { handleEcosystemPackageExecution } from "../../proxy/ecosystemExecutionRoute";
3
3
 
4
4
  export const Route = createFileRoute("/api/ecosystem/packages/$packageId/install")({
5
5
  server: {
6
6
  handlers: {
7
- POST: ({ params }: { params: { packageId: string } }) => {
8
- const task = startEcosystemTask(params.packageId, "install");
9
- if (task === null) {
10
- return Response.json({ error: "Unknown ecosystem package" }, { status: 404 });
11
- }
12
- return Response.json({ task });
13
- },
7
+ POST: ({ params, request }: { params: { packageId: string }; request: Request }) =>
8
+ handleEcosystemPackageExecution(request, params.packageId, "install"),
14
9
  },
15
10
  },
16
11
  });
@@ -1,19 +1,11 @@
1
1
  import { createFileRoute } from "@tanstack/react-router";
2
- import { startEcosystemTask } from "../../proxy/ecosystemTasks";
2
+ import { handleEcosystemPackageExecution } from "../../proxy/ecosystemExecutionRoute";
3
3
 
4
4
  export const Route = createFileRoute("/api/ecosystem/packages/$packageId/runner-presets")({
5
5
  server: {
6
6
  handlers: {
7
- POST: ({ params }: { params: { packageId: string } }) => {
8
- const task = startEcosystemTask(params.packageId, "runner-presets");
9
- if (task === null) {
10
- return Response.json(
11
- { error: "Runner presets are only available for Eval Harness" },
12
- { status: 404 },
13
- );
14
- }
15
- return Response.json({ task });
16
- },
7
+ POST: ({ params, request }: { params: { packageId: string }; request: Request }) =>
8
+ handleEcosystemPackageExecution(request, params.packageId, "runner-presets"),
17
9
  },
18
10
  },
19
11
  });
@@ -1,16 +1,11 @@
1
1
  import { createFileRoute } from "@tanstack/react-router";
2
- import { startEcosystemTask } from "../../proxy/ecosystemTasks";
2
+ import { handleEcosystemPackageExecution } from "../../proxy/ecosystemExecutionRoute";
3
3
 
4
4
  export const Route = createFileRoute("/api/ecosystem/packages/$packageId/upgrade")({
5
5
  server: {
6
6
  handlers: {
7
- POST: ({ params }: { params: { packageId: string } }) => {
8
- const task = startEcosystemTask(params.packageId, "upgrade");
9
- if (task === null) {
10
- return Response.json({ error: "Unknown ecosystem package" }, { status: 404 });
11
- }
12
- return Response.json({ task });
13
- },
7
+ POST: ({ params, request }: { params: { packageId: string }; request: Request }) =>
8
+ handleEcosystemPackageExecution(request, params.packageId, "upgrade"),
14
9
  },
15
10
  },
16
11
  });
@@ -1,16 +1,11 @@
1
1
  import { createFileRoute } from "@tanstack/react-router";
2
- import { startEcosystemRecipeTask } from "../../proxy/ecosystemTasks";
2
+ import { handleEcosystemRecipeExecution } from "../../proxy/ecosystemExecutionRoute";
3
3
 
4
4
  export const Route = createFileRoute("/api/ecosystem/recipes/$recipeId/run")({
5
5
  server: {
6
6
  handlers: {
7
- POST: ({ params }: { params: { recipeId: string } }) => {
8
- const task = startEcosystemRecipeTask(params.recipeId);
9
- if (task === null) {
10
- return Response.json({ error: "Unknown ecosystem recipe" }, { status: 404 });
11
- }
12
- return Response.json({ task });
13
- },
7
+ POST: ({ params, request }: { params: { recipeId: string }; request: Request }) =>
8
+ handleEcosystemRecipeExecution(request, params.recipeId),
14
9
  },
15
10
  },
16
11
  });
@@ -1,14 +1,21 @@
1
1
  import { createFileRoute } from "@tanstack/react-router";
2
2
  import { logger } from "../../proxy/logger";
3
+ import { getRuntimeReadiness } from "../../proxy/runtimeHealth";
4
+ import { installRuntimeShutdownSignalHandlers } from "../../proxy/runtimeShutdown";
3
5
 
4
6
  // Trigger logger auto-initialization when this module loads
7
+ installRuntimeShutdownSignalHandlers();
5
8
  logger.debug("Health endpoint loaded");
6
9
 
7
10
  export const Route = createFileRoute("/api/health")({
8
11
  server: {
9
12
  handlers: {
10
- GET: () => {
11
- return Response.json({ status: "ok" });
13
+ GET: async () => {
14
+ const readiness = await getRuntimeReadiness();
15
+ return Response.json(
16
+ { status: "ok", readiness: readiness.status },
17
+ { headers: { "cache-control": "no-store" } },
18
+ );
12
19
  },
13
20
  },
14
21
  },
@@ -1,5 +1,6 @@
1
1
  import { createFileRoute } from "@tanstack/react-router";
2
2
  import { searchOpenClaw } from "../../knowledge/openclawClient";
3
+ import { MAX_SEARCH_PROJECT_CHARS, resourceLimitResponse } from "../../lib/resourceLimits";
3
4
 
4
5
  export const Route = createFileRoute("/api/knowledge/project-context")({
5
6
  server: {
@@ -10,6 +11,16 @@ export const Route = createFileRoute("/api/knowledge/project-context")({
10
11
  if (project === "") {
11
12
  return Response.json({ error: "Missing project search parameter" }, { status: 400 });
12
13
  }
14
+ if (project.length > MAX_SEARCH_PROJECT_CHARS) {
15
+ return resourceLimitResponse({
16
+ error: `Search project filters may contain at most ${String(MAX_SEARCH_PROJECT_CHARS)} characters.`,
17
+ code: "search-limit-exceeded",
18
+ status: 400,
19
+ resource: "knowledge-search-project",
20
+ limit: MAX_SEARCH_PROJECT_CHARS,
21
+ retryable: false,
22
+ });
23
+ }
13
24
  const response = await searchOpenClaw(`project context ${project}`, project);
14
25
  return Response.json({ project, ...response });
15
26
  },
@@ -1,12 +1,32 @@
1
1
  import { createFileRoute } from "@tanstack/react-router";
2
2
  import { z } from "zod";
3
3
  import { searchOpenClaw } from "../../knowledge/openclawClient";
4
+ import { MAX_SEARCH_QUERY_CHARS, resourceLimitResponse } from "../../lib/resourceLimits";
4
5
 
5
6
  const SearchBodySchema = z.object({
6
- query: z.string().min(1),
7
+ query: z.string().min(1).max(MAX_SEARCH_QUERY_CHARS),
7
8
  project: z.string().optional(),
8
9
  });
9
10
 
11
+ function searchLimitResponse(): Response {
12
+ return resourceLimitResponse({
13
+ error: `Search queries may contain at most ${String(MAX_SEARCH_QUERY_CHARS)} characters.`,
14
+ code: "search-limit-exceeded",
15
+ status: 400,
16
+ resource: "knowledge-search-query",
17
+ limit: MAX_SEARCH_QUERY_CHARS,
18
+ retryable: false,
19
+ });
20
+ }
21
+
22
+ function bodySearchQuery(value: unknown): string | null {
23
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
24
+ const descriptor = Object.getOwnPropertyDescriptor(value, "query");
25
+ if (descriptor === undefined) return null;
26
+ const query: unknown = descriptor.value;
27
+ return typeof query === "string" ? query : null;
28
+ }
29
+
10
30
  export const Route = createFileRoute("/api/knowledge/search")({
11
31
  server: {
12
32
  handlers: {
@@ -17,10 +37,21 @@ export const Route = createFileRoute("/api/knowledge/search")({
17
37
  if (query === "") {
18
38
  return Response.json({ error: "Missing q search parameter" }, { status: 400 });
19
39
  }
40
+ if (query.length > MAX_SEARCH_QUERY_CHARS) return searchLimitResponse();
20
41
  return Response.json(await searchOpenClaw(query, project));
21
42
  },
22
43
  POST: async ({ request }: { request: Request }) => {
23
- const parsed = SearchBodySchema.safeParse(await request.json());
44
+ let body: unknown;
45
+ try {
46
+ body = await request.json();
47
+ } catch {
48
+ return Response.json({ error: "Invalid JSON body" }, { status: 400 });
49
+ }
50
+ const query = bodySearchQuery(body);
51
+ if (query !== null && query.length > MAX_SEARCH_QUERY_CHARS) {
52
+ return searchLimitResponse();
53
+ }
54
+ const parsed = SearchBodySchema.safeParse(body);
24
55
  if (!parsed.success) {
25
56
  return Response.json({ error: parsed.error.message }, { status: 400 });
26
57
  }
@@ -1,7 +1,12 @@
1
1
  import { createFileRoute } from "@tanstack/react-router";
2
2
  import { LogBodyPartSchema } from "../../contracts";
3
- import { createLogBodyChunk } from "../../proxy/logBodyChunks";
3
+ import {
4
+ createLogBodyChunk,
5
+ DEFAULT_LOG_BODY_CHUNK_BYTES,
6
+ MAX_LOG_BODY_CHUNK_BYTES,
7
+ } from "../../proxy/logBodyChunks";
4
8
  import { getLogById } from "../../proxy/store";
9
+ import { parseBoundedIntegerParameter } from "../../lib/resourceLimits";
5
10
 
6
11
  function parseIntegerParam(value: string | null, fallback: number): number {
7
12
  if (value === null) return fallback;
@@ -24,6 +29,15 @@ export const Route = createFileRoute("/api/logs/$id/body")({
24
29
  if (!partResult.success) {
25
30
  return Response.json({ error: "Invalid body part" }, { status: 400 });
26
31
  }
32
+ const limitResult = parseBoundedIntegerParameter({
33
+ value: url.searchParams.get("limit"),
34
+ parameter: "limit",
35
+ fallback: DEFAULT_LOG_BODY_CHUNK_BYTES,
36
+ minimum: 1,
37
+ maximum: MAX_LOG_BODY_CHUNK_BYTES,
38
+ resource: "log-body-page-bytes",
39
+ });
40
+ if (!limitResult.ok) return limitResult.response;
27
41
 
28
42
  const log = await getLogById(id);
29
43
  if (log === null) {
@@ -35,7 +49,7 @@ export const Route = createFileRoute("/api/logs/$id/body")({
35
49
  log,
36
50
  part: partResult.data,
37
51
  offset: parseIntegerParam(url.searchParams.get("offset"), 0),
38
- limit: parseIntegerParam(url.searchParams.get("limit"), 0),
52
+ limit: limitResult.value,
39
53
  }),
40
54
  );
41
55
  },
@@ -1,32 +1,21 @@
1
- import { Buffer } from "node:buffer";
2
1
  import { createFileRoute } from "@tanstack/react-router";
3
2
  import { parseImportedLogFile, sessionNameFromImportFileName } from "../../proxy/logImporter";
3
+ import { readBoundedLogImportUpload } from "../../proxy/logImportUpload";
4
4
  import { compactLogForList, importCapturedLogs } from "../../proxy/store";
5
5
 
6
- function isUploadedFile(value: FormDataEntryValue | null): value is File {
7
- return value instanceof File;
8
- }
9
-
10
6
  export const Route = createFileRoute("/api/logs/import")({
11
7
  server: {
12
8
  handlers: {
13
9
  POST: async ({ request }: { request: Request }) => {
14
- let formData: FormData;
15
- try {
16
- formData = await request.formData();
17
- } catch {
18
- return Response.json({ error: "Expected multipart form data" }, { status: 400 });
19
- }
20
-
21
- const upload = formData.get("file");
22
- if (!isUploadedFile(upload)) {
23
- return Response.json({ error: "Missing import file" }, { status: 400 });
10
+ const upload = await readBoundedLogImportUpload(request);
11
+ if (!upload.ok) {
12
+ return Response.json({ error: upload.message }, { status: upload.status });
24
13
  }
25
14
 
26
- const sessionId = sessionNameFromImportFileName(upload.name);
15
+ const sessionId = sessionNameFromImportFileName(upload.fileName);
27
16
  const parsed = await parseImportedLogFile({
28
- fileName: upload.name,
29
- data: Buffer.from(await upload.arrayBuffer()),
17
+ fileName: upload.fileName,
18
+ data: upload.data,
30
19
  });
31
20
 
32
21
  if (!parsed.ok) {