@tonyclaw/agent-inspector 3.0.28 → 3.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.output/cli.js +791 -191
- package/.output/nitro.json +1 -1
- package/.output/public/assets/CompareDrawer-B6cXJohL.js +1 -0
- package/.output/public/assets/InspectorPet-mAzeGmEE.js +4108 -0
- package/.output/public/assets/ProxyViewerContainer-CkFWv_Nm.js +126 -0
- package/.output/public/assets/{ReplayDialog-CZMApaF4.js → ReplayDialog-DodiTL6E.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-DNWccR7O.js → RequestAnatomy-B3IauToI.js} +1 -1
- package/.output/public/assets/ResponseView-Hn-5ordK.js +3 -0
- package/.output/public/assets/StreamingChunkSequence-vLDOVQM_.js +1 -0
- package/.output/public/assets/_sessionId-BFH4TOaI.js +1 -0
- package/.output/public/assets/{_sessionId-DRfvuE2k.js → _sessionId-Cj_HIFZx.js} +1 -1
- package/.output/public/assets/{index-D4HkS7Ct.js → index-Bmj2lcWE.js} +1 -1
- package/.output/public/assets/index-CgJj-HBC.css +1 -0
- package/.output/public/assets/index-Ch7uqnCT.js +1 -0
- package/.output/public/assets/index-DkDdKvLl.js +76 -0
- package/.output/public/assets/{index-B3aAuqWq.js → index-ZlhqCrSg.js} +6 -6
- package/.output/public/assets/{json-viewer-wuJSKaed.js → json-viewer-BdrnVQu-.js} +1 -1
- package/.output/public/assets/jszip.min-CS_nt_o1.js +2 -0
- package/.output/public/assets/qwen-mMn3f5ul.webp +0 -0
- package/.output/server/_libs/jszip.mjs +35 -9
- package/.output/server/_libs/lucide-react.mjs +86 -86
- package/.output/server/_libs/radix-ui__react-dialog.mjs +3 -3
- package/.output/server/_libs/tanstack__react-router.mjs +1 -1
- package/.output/server/{_sessionId-BJTNiP_O.mjs → _sessionId-C5S3pGZd.mjs} +15 -15
- package/.output/server/_sessionId-g6D69lKq.mjs +81 -0
- package/.output/server/_ssr/{CompareDrawer-BMRH3N97.mjs → CompareDrawer-CqUmGHal.mjs} +138 -103
- package/.output/server/_ssr/InspectorPet-DI0UJd01.mjs +925 -0
- package/.output/server/_ssr/{ProxyViewerContainer-AFwcHAIR.mjs → ProxyViewerContainer-CvsUmbJ3.mjs} +1216 -1659
- package/.output/server/_ssr/{ReplayDialog-CbWvRHiN.mjs → ReplayDialog-DS48dpFG.mjs} +18 -18
- package/.output/server/_ssr/{RequestAnatomy-C4kcPyqN.mjs → RequestAnatomy-DxYVQU1C.mjs} +58 -58
- package/.output/server/_ssr/{ResponseView-D8VYt-gZ.mjs → ResponseView-BLXW73eq.mjs} +60 -60
- package/.output/server/_ssr/{StreamingChunkSequence-BUUo6vU0.mjs → StreamingChunkSequence-DD0iFuy0.mjs} +16 -16
- package/.output/server/_ssr/{index-DTwMEEXO.mjs → index-B10LIaK0.mjs} +15 -15
- package/.output/server/_ssr/index-RHVJnU2p.mjs +81 -0
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-BqF1pSQD.mjs → json-viewer-LDu2XBaK.mjs} +58 -58
- package/.output/server/_ssr/{router-C8BOns7y.mjs → router-GKvrwifJ.mjs} +9005 -6488
- package/.output/server/_tanstack-start-manifest_v-Df2n4mbT.mjs +4 -0
- package/.output/server/index.mjs +105 -77
- package/.output/workers/logFinalizer.worker.js +16908 -0
- package/.output/workers/sessionWorkerEntry.js +16904 -0
- package/README.md +21 -5
- package/package.json +6 -2
- package/src/assets/logos/qwen.webp +0 -0
- package/src/cli.ts +85 -10
- package/src/components/OnboardingBanner.tsx +41 -21
- package/src/components/ProxyViewer.tsx +198 -269
- package/src/components/ProxyViewerContainer.tsx +128 -17
- package/src/components/clients/ClientLogo.tsx +9 -6
- package/src/components/ecosystem/AgentLabDialog.tsx +166 -19
- package/src/components/errors/SafeErrorBoundary.tsx +201 -0
- package/src/components/pi-agent/PiAgentPanel.tsx +4 -1
- package/src/components/providers/ImportWizardDialog.tsx +18 -42
- package/src/components/providers/ProviderLogo.tsx +1 -1
- package/src/components/providers/ProvidersPanel.tsx +8 -5
- package/src/components/providers/SettingsDialog.tsx +34 -11
- package/src/components/proxy-viewer/ApplicationBar.tsx +63 -0
- package/src/components/proxy-viewer/CompareDrawer.tsx +92 -49
- package/src/components/proxy-viewer/ConversationGroupList.tsx +49 -34
- package/src/components/proxy-viewer/ConversationHeader.tsx +22 -18
- package/src/components/proxy-viewer/LiveConnectionStatus.tsx +186 -0
- package/src/components/proxy-viewer/LogEntry.tsx +283 -275
- package/src/components/proxy-viewer/RawExportWarning.tsx +77 -0
- package/src/components/proxy-viewer/SessionLoadProgressBar.tsx +61 -0
- package/src/components/proxy-viewer/ThreadConnector.tsx +16 -1
- package/src/components/proxy-viewer/TurnGroup.tsx +13 -2
- package/src/components/proxy-viewer/accessibility.ts +8 -0
- package/src/components/proxy-viewer/lazy.ts +4 -0
- package/src/components/proxy-viewer/liveConnectionState.ts +158 -0
- package/src/components/proxy-viewer/proxyViewerLogic.ts +10 -0
- package/src/components/proxy-viewer/viewerState.ts +2 -6
- package/src/components/ui/select.tsx +1 -1
- package/src/components/ui/tabs.tsx +3 -3
- package/src/components/ui/transient-toast.tsx +1 -1
- package/src/lib/apiClient.ts +17 -2
- package/src/lib/ecosystemContract.ts +34 -0
- package/src/lib/export-logs.ts +1 -1
- package/src/lib/providerContract.ts +70 -4
- package/src/lib/providerImportContract.ts +27 -0
- package/src/lib/providerModelMetadata.ts +16 -7
- package/src/lib/resourceLimits.ts +152 -0
- package/src/lib/safeDiagnostic.ts +38 -0
- package/src/lib/useProviders.ts +4 -4
- package/src/lib/utils.ts +5 -1
- package/src/mcp/server.ts +39 -6
- package/src/mcp/toolHandlers.ts +131 -4
- package/src/proxy/chunkStorage.ts +20 -6
- package/src/proxy/config.ts +20 -6
- package/src/proxy/dataDir.ts +3 -0
- package/src/proxy/ecosystemExecutionConfirmation.ts +114 -0
- package/src/proxy/ecosystemExecutionRoute.ts +116 -0
- package/src/proxy/ecosystemTasks.ts +48 -0
- package/src/proxy/evidenceExporter.ts +23 -9
- package/src/proxy/groupEvidenceExporter.ts +26 -5
- package/src/proxy/groupStore.ts +13 -3
- package/src/proxy/handler.ts +103 -20
- package/src/proxy/identityProxy.ts +333 -14
- package/src/proxy/logFinalizer.ts +73 -6
- package/src/proxy/logImportUpload.ts +128 -0
- package/src/proxy/logImporter.ts +321 -70
- package/src/proxy/logIndex.ts +16 -6
- package/src/proxy/logger.ts +294 -41
- package/src/proxy/privateDataPath.ts +183 -0
- package/src/proxy/providerScanStore.ts +87 -0
- package/src/proxy/providerSecretStore.ts +58 -19
- package/src/proxy/providers.ts +175 -58
- package/src/proxy/rawStreamCapture.ts +66 -5
- package/src/proxy/runStore.ts +13 -3
- package/src/proxy/runtimeAdmission.ts +52 -0
- package/src/proxy/runtimeHealth.ts +206 -0
- package/src/proxy/runtimeShutdown.ts +75 -0
- package/src/proxy/sessionArchive.ts +15 -2
- package/src/proxy/sessionProcess.ts +19 -0
- package/src/proxy/sessionRuntime.ts +7 -0
- package/src/proxy/shutdownCoordinator.ts +90 -0
- package/src/proxy/sqliteLogIndex.ts +18 -2
- package/src/proxy/store.ts +18 -2
- package/src/routes/__root.tsx +18 -1
- package/src/routes/api/alerts.summary.ts +12 -8
- package/src/routes/api/alerts.ts +27 -9
- package/src/routes/api/ecosystem.packages.$packageId.help.ts +3 -8
- package/src/routes/api/ecosystem.packages.$packageId.install.ts +3 -8
- package/src/routes/api/ecosystem.packages.$packageId.runner-presets.ts +3 -11
- package/src/routes/api/ecosystem.packages.$packageId.upgrade.ts +3 -8
- package/src/routes/api/ecosystem.recipes.$recipeId.run.ts +3 -8
- package/src/routes/api/health.ts +9 -2
- package/src/routes/api/knowledge.project-context.ts +11 -0
- package/src/routes/api/knowledge.search.ts +33 -2
- package/src/routes/api/logs.$id.body.ts +16 -2
- package/src/routes/api/logs.import.ts +7 -18
- package/src/routes/api/logs.stream.ts +147 -69
- package/src/routes/api/logs.ts +55 -14
- package/src/routes/api/providers.$providerId.model-metadata.ts +22 -4
- package/src/routes/api/providers.$providerId.ts +37 -6
- package/src/routes/api/providers.export.ts +34 -16
- package/src/routes/api/providers.import.ts +42 -8
- package/src/routes/api/providers.scan.ts +13 -8
- package/src/routes/api/providers.ts +36 -5
- package/src/routes/api/runs.ts +12 -12
- package/src/routes/api/sessions.ts +15 -8
- package/src/routes/index.tsx +6 -0
- package/src/routes/livez.ts +13 -0
- package/src/routes/readyz.ts +18 -0
- package/src/routes/session/$sessionId.tsx +6 -0
- package/styles/globals.css +19 -3
- package/.output/public/assets/CompareDrawer-TghZr5t1.js +0 -1
- package/.output/public/assets/ProxyViewerContainer-fShBAZjj.js +0 -4234
- package/.output/public/assets/ResponseView-d25MdFhY.js +0 -3
- package/.output/public/assets/StreamingChunkSequence-DIFZEnWe.js +0 -1
- package/.output/public/assets/index-BsxAGqMA.css +0 -1
- package/.output/public/assets/index-DzARL2Fi.js +0 -76
- package/.output/public/assets/qwen-CONDcHqt.png +0 -0
- package/.output/server/_tanstack-start-manifest_v-BBf2nRhb.mjs +0 -4
- package/src/assets/logos/mcp.png +0 -0
- package/src/assets/logos/qwen.png +0 -0
- package/src/components/ui/mcp-logo.tsx +0 -20
|
@@ -1,85 +1,163 @@
|
|
|
1
1
|
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
compactLogForList,
|
|
4
|
+
onLogUpdate,
|
|
5
|
+
getLogSessionId,
|
|
6
|
+
listLogsCursorPage,
|
|
7
|
+
} from "../../proxy/store";
|
|
3
8
|
import { type CapturedLog } from "../../contracts";
|
|
9
|
+
import {
|
|
10
|
+
createCapacityLimiter,
|
|
11
|
+
MAX_SSE_CLIENTS,
|
|
12
|
+
resourceLimitResponse,
|
|
13
|
+
type CapacityLimiter,
|
|
14
|
+
} from "../../lib/resourceLimits";
|
|
4
15
|
|
|
5
16
|
const INITIAL_STREAM_LOG_LIMIT = 100;
|
|
17
|
+
const UPDATE_COALESCE_MS = 50;
|
|
18
|
+
const MAX_BUFFERED_UPDATE_EVENTS = 16;
|
|
19
|
+
const sseClientLimiter = createCapacityLimiter(MAX_SSE_CLIENTS);
|
|
6
20
|
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
GET: ({ request }: { request: Request }) => {
|
|
11
|
-
const url = new URL(request.url);
|
|
12
|
-
const sessionId = url.searchParams.get("sessionId") ?? undefined;
|
|
13
|
-
const model = url.searchParams.get("model") ?? undefined;
|
|
14
|
-
const includeBodies = url.searchParams.get("compact") !== "1";
|
|
21
|
+
export function streamIncludesBodies(url: URL): boolean {
|
|
22
|
+
return url.searchParams.get("compact") === "0";
|
|
23
|
+
}
|
|
15
24
|
|
|
16
|
-
|
|
17
|
-
|
|
25
|
+
export function parseLastEventLogId(value: string | null): number | undefined {
|
|
26
|
+
if (value === null || !/^\d+$/.test(value)) return undefined;
|
|
27
|
+
const parsed = Number(value);
|
|
28
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
29
|
+
}
|
|
18
30
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (model !== undefined && log.model !== model) return;
|
|
24
|
-
try {
|
|
25
|
-
const payloadLog = includeBodies ? log : compactLogForList(log);
|
|
26
|
-
const data = `data: ${JSON.stringify({ type: "update", log: payloadLog })}\n\n`;
|
|
27
|
-
controllerRef.enqueue(new TextEncoder().encode(data));
|
|
28
|
-
} catch {
|
|
29
|
-
cleanup();
|
|
30
|
-
}
|
|
31
|
-
});
|
|
31
|
+
function encodeSse(value: unknown, id?: number): Uint8Array {
|
|
32
|
+
const idLine = id === undefined ? "" : `id: ${String(id)}\n`;
|
|
33
|
+
return new TextEncoder().encode(`${idLine}data: ${JSON.stringify(value)}\n\n`);
|
|
34
|
+
}
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
export function createLogsStreamResponse(
|
|
37
|
+
request: Request,
|
|
38
|
+
clientLimiter: CapacityLimiter = sseClientLimiter,
|
|
39
|
+
): Response {
|
|
40
|
+
const lease = clientLimiter.tryAcquire();
|
|
41
|
+
if (lease === null) {
|
|
42
|
+
return resourceLimitResponse({
|
|
43
|
+
error: `SSE client capacity reached. At most ${String(clientLimiter.capacity)} live clients are allowed; retry after a connection closes.`,
|
|
44
|
+
code: "sse-capacity-exceeded",
|
|
45
|
+
status: 503,
|
|
46
|
+
resource: "sse-clients",
|
|
47
|
+
limit: clientLimiter.capacity,
|
|
48
|
+
retryable: true,
|
|
49
|
+
retryAfterSeconds: 1,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const url = new URL(request.url);
|
|
53
|
+
const sessionId = url.searchParams.get("sessionId") ?? undefined;
|
|
54
|
+
const model = url.searchParams.get("model") ?? undefined;
|
|
55
|
+
const includeBodies = streamIncludesBodies(url);
|
|
56
|
+
const lastEventLogId = parseLastEventLogId(request.headers.get("last-event-id"));
|
|
41
57
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
controllerRef.enqueue(new TextEncoder().encode(": heartbeat\n\n"));
|
|
47
|
-
} catch {
|
|
48
|
-
cleanup();
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}, 30000);
|
|
58
|
+
let controllerRef: ReadableStreamDefaultController<Uint8Array> | null = null;
|
|
59
|
+
let cleanedUp = false;
|
|
60
|
+
let updateTimer: ReturnType<typeof setTimeout> | null = null;
|
|
61
|
+
const pendingUpdates = new Map<number, CapturedLog>();
|
|
52
62
|
|
|
53
|
-
|
|
63
|
+
const flushPendingUpdates = (): void => {
|
|
64
|
+
updateTimer = null;
|
|
65
|
+
const controller = controllerRef;
|
|
66
|
+
if (controller === null) return;
|
|
67
|
+
if (controller.desiredSize !== null && controller.desiredSize <= -MAX_BUFFERED_UPDATE_EVENTS) {
|
|
68
|
+
controller.close();
|
|
69
|
+
cleanup();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
for (const log of pendingUpdates.values()) {
|
|
73
|
+
if (
|
|
74
|
+
controller.desiredSize !== null &&
|
|
75
|
+
controller.desiredSize <= -MAX_BUFFERED_UPDATE_EVENTS
|
|
76
|
+
) {
|
|
77
|
+
controller.close();
|
|
78
|
+
cleanup();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const payloadLog = includeBodies ? log : compactLogForList(log);
|
|
82
|
+
controller.enqueue(encodeSse({ type: "update", log: payloadLog }, log.id));
|
|
83
|
+
}
|
|
84
|
+
pendingUpdates.clear();
|
|
85
|
+
};
|
|
54
86
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
87
|
+
const unsubscribe = onLogUpdate((log: CapturedLog) => {
|
|
88
|
+
if (controllerRef === null) return;
|
|
89
|
+
// Filter by session/model if specified
|
|
90
|
+
if (sessionId !== undefined && getLogSessionId(log) !== sessionId) return;
|
|
91
|
+
if (model !== undefined && log.model !== model) return;
|
|
92
|
+
pendingUpdates.set(log.id, log);
|
|
93
|
+
if (updateTimer === null) updateTimer = setTimeout(flushPendingUpdates, UPDATE_COALESCE_MS);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const cleanup = (): void => {
|
|
97
|
+
if (cleanedUp) return;
|
|
98
|
+
cleanedUp = true;
|
|
99
|
+
clearInterval(heartbeat);
|
|
100
|
+
if (updateTimer !== null) clearTimeout(updateTimer);
|
|
101
|
+
updateTimer = null;
|
|
102
|
+
pendingUpdates.clear();
|
|
103
|
+
unsubscribe();
|
|
104
|
+
lease.release();
|
|
105
|
+
controllerRef = null;
|
|
106
|
+
request.signal.removeEventListener("abort", cleanup);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Send heartbeat comment every 30s to keep connection alive
|
|
110
|
+
const heartbeat = setInterval(() => {
|
|
111
|
+
if (controllerRef !== null) {
|
|
112
|
+
try {
|
|
113
|
+
controllerRef.enqueue(new TextEncoder().encode(": heartbeat\n\n"));
|
|
114
|
+
} catch {
|
|
115
|
+
cleanup();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}, 30000);
|
|
119
|
+
|
|
120
|
+
request.signal.addEventListener("abort", cleanup, { once: true });
|
|
74
121
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
122
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
123
|
+
async start(controller) {
|
|
124
|
+
controllerRef = controller;
|
|
125
|
+
try {
|
|
126
|
+
const result = await listLogsCursorPage({
|
|
127
|
+
sessionId,
|
|
128
|
+
model,
|
|
129
|
+
limit: INITIAL_STREAM_LOG_LIMIT,
|
|
130
|
+
includeBodies,
|
|
131
|
+
afterLogId: lastEventLogId,
|
|
132
|
+
anchor: lastEventLogId === undefined ? "newest" : undefined,
|
|
81
133
|
});
|
|
82
|
-
|
|
134
|
+
if (cleanedUp) return;
|
|
135
|
+
const logs = result.logs;
|
|
136
|
+
const newestLogId = logs.at(-1)?.id;
|
|
137
|
+
controller.enqueue(encodeSse({ type: "init", logs }, newestLogId));
|
|
138
|
+
} catch (error) {
|
|
139
|
+
cleanup();
|
|
140
|
+
controller.error(error);
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
cancel() {
|
|
144
|
+
cleanup();
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return new Response(stream, {
|
|
149
|
+
headers: {
|
|
150
|
+
"Content-Type": "text/event-stream",
|
|
151
|
+
"Cache-Control": "no-cache",
|
|
152
|
+
Connection: "keep-alive",
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export const Route = createFileRoute("/api/logs/stream")({
|
|
158
|
+
server: {
|
|
159
|
+
handlers: {
|
|
160
|
+
GET: ({ request }: { request: Request }) => createLogsStreamResponse(request),
|
|
83
161
|
},
|
|
84
162
|
},
|
|
85
163
|
});
|
package/src/routes/api/logs.ts
CHANGED
|
@@ -11,8 +11,16 @@ import {
|
|
|
11
11
|
import {
|
|
12
12
|
LOG_SEARCH_DEFAULT_LIMIT,
|
|
13
13
|
LOG_SEARCH_DEFAULT_SCAN_LIMIT,
|
|
14
|
+
LOG_SEARCH_MAX_LIMIT,
|
|
15
|
+
LOG_SEARCH_MAX_SCAN_LIMIT,
|
|
14
16
|
searchLogsPage,
|
|
15
17
|
} from "../../proxy/logSearch";
|
|
18
|
+
import {
|
|
19
|
+
MAX_REST_LOG_PAGE_SIZE,
|
|
20
|
+
MAX_SEARCH_QUERY_CHARS,
|
|
21
|
+
parseBoundedIntegerParameter,
|
|
22
|
+
resourceLimitResponse,
|
|
23
|
+
} from "../../lib/resourceLimits";
|
|
16
24
|
|
|
17
25
|
export const DeleteLogsBodySchema = z
|
|
18
26
|
.object({
|
|
@@ -27,13 +35,6 @@ function parseNonNegativeInt(value: string | null, fallback: number): number {
|
|
|
27
35
|
return parsed;
|
|
28
36
|
}
|
|
29
37
|
|
|
30
|
-
function parsePositiveInt(value: string | null, fallback: number): number {
|
|
31
|
-
if (value === null) return fallback;
|
|
32
|
-
const parsed = Number(value);
|
|
33
|
-
if (!Number.isInteger(parsed) || parsed < 1) return fallback;
|
|
34
|
-
return parsed;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
38
|
type OptionalPositiveIntResult = { ok: true; value: number | null } | { ok: false };
|
|
38
39
|
|
|
39
40
|
function parseOptionalPositiveInt(value: string | null): OptionalPositiveIntResult {
|
|
@@ -70,16 +71,56 @@ export const Route = createFileRoute("/api/logs")({
|
|
|
70
71
|
const query = url.searchParams.get("q") ?? url.searchParams.get("search");
|
|
71
72
|
const offset = parseNonNegativeInt(url.searchParams.get("offset"), 0);
|
|
72
73
|
if (query !== null) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
if (query.length > MAX_SEARCH_QUERY_CHARS) {
|
|
75
|
+
return resourceLimitResponse({
|
|
76
|
+
error: `Search queries may contain at most ${String(MAX_SEARCH_QUERY_CHARS)} characters.`,
|
|
77
|
+
code: "search-limit-exceeded",
|
|
78
|
+
status: 400,
|
|
79
|
+
resource: "log-search-query",
|
|
80
|
+
limit: MAX_SEARCH_QUERY_CHARS,
|
|
81
|
+
retryable: false,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const limitResult = parseBoundedIntegerParameter({
|
|
85
|
+
value: url.searchParams.get("limit"),
|
|
86
|
+
parameter: "limit",
|
|
87
|
+
fallback: LOG_SEARCH_DEFAULT_LIMIT,
|
|
88
|
+
minimum: 1,
|
|
89
|
+
maximum: LOG_SEARCH_MAX_LIMIT,
|
|
90
|
+
resource: "log-search-page-size",
|
|
91
|
+
});
|
|
92
|
+
if (!limitResult.ok) return limitResult.response;
|
|
93
|
+
const scanLimitResult = parseBoundedIntegerParameter({
|
|
94
|
+
value: url.searchParams.get("scanLimit"),
|
|
95
|
+
parameter: "scanLimit",
|
|
96
|
+
fallback: LOG_SEARCH_DEFAULT_SCAN_LIMIT,
|
|
97
|
+
minimum: 1,
|
|
98
|
+
maximum: LOG_SEARCH_MAX_SCAN_LIMIT,
|
|
99
|
+
resource: "log-search-scan-size",
|
|
100
|
+
});
|
|
101
|
+
if (!scanLimitResult.ok) return scanLimitResult.response;
|
|
78
102
|
return Response.json(
|
|
79
|
-
await searchLogsPage({
|
|
103
|
+
await searchLogsPage({
|
|
104
|
+
query,
|
|
105
|
+
sessionId,
|
|
106
|
+
model,
|
|
107
|
+
clientPid,
|
|
108
|
+
offset,
|
|
109
|
+
limit: limitResult.value,
|
|
110
|
+
scanLimit: scanLimitResult.value,
|
|
111
|
+
}),
|
|
80
112
|
);
|
|
81
113
|
}
|
|
82
|
-
const
|
|
114
|
+
const limitResult = parseBoundedIntegerParameter({
|
|
115
|
+
value: url.searchParams.get("limit"),
|
|
116
|
+
parameter: "limit",
|
|
117
|
+
fallback: 50,
|
|
118
|
+
minimum: 1,
|
|
119
|
+
maximum: MAX_REST_LOG_PAGE_SIZE,
|
|
120
|
+
resource: "logs-page-size",
|
|
121
|
+
});
|
|
122
|
+
if (!limitResult.ok) return limitResult.response;
|
|
123
|
+
const limit = limitResult.value;
|
|
83
124
|
const includeBodies = url.searchParams.get("compact") !== "1";
|
|
84
125
|
|
|
85
126
|
if (url.searchParams.get("cursor") === "1" || url.searchParams.get("cursor") === "true") {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createFileRoute } from "@tanstack/react-router";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { getProvider, updateProvider } from "../../proxy/providers";
|
|
4
|
+
import { toRedactedProviderDto } from "../../lib/providerContract";
|
|
4
5
|
import {
|
|
5
6
|
applyBuiltinProviderModelMetadata,
|
|
6
7
|
applyProviderModelRegistry,
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
ProviderModelRegistrySchema,
|
|
9
10
|
} from "../../lib/providerModelMetadata";
|
|
10
11
|
import { safeFetch } from "../../lib/safeFetch";
|
|
12
|
+
import { MAX_PROVIDER_MODELS, resourceLimitResponse } from "../../lib/resourceLimits";
|
|
11
13
|
|
|
12
14
|
const RefreshModelMetadataInputSchema = z.object({
|
|
13
15
|
url: z.string().optional(),
|
|
@@ -61,10 +63,13 @@ export const Route = createFileRoute("/api/providers/$providerId/model-metadata"
|
|
|
61
63
|
modelMetadata: result.modelMetadata,
|
|
62
64
|
});
|
|
63
65
|
if (updated === null) {
|
|
64
|
-
return Response.json(
|
|
66
|
+
return Response.json(
|
|
67
|
+
{ error: "Secure provider storage is unavailable; metadata was not updated" },
|
|
68
|
+
{ status: 503 },
|
|
69
|
+
);
|
|
65
70
|
}
|
|
66
71
|
return Response.json({
|
|
67
|
-
provider: updated,
|
|
72
|
+
provider: toRedactedProviderDto(updated),
|
|
68
73
|
imported: result.imported,
|
|
69
74
|
matchedModels: result.matchedModels,
|
|
70
75
|
missingModels: result.missingModels,
|
|
@@ -114,6 +119,16 @@ export const Route = createFileRoute("/api/providers/$providerId/model-metadata"
|
|
|
114
119
|
|
|
115
120
|
const registry = ProviderModelRegistrySchema.safeParse(registryJson);
|
|
116
121
|
if (!registry.success) {
|
|
122
|
+
if (registry.error.issues.some((issue) => issue.code === "too_big")) {
|
|
123
|
+
return resourceLimitResponse({
|
|
124
|
+
error: `Provider metadata model collections may contain at most ${String(MAX_PROVIDER_MODELS)} entries.`,
|
|
125
|
+
code: "provider-model-limit-exceeded",
|
|
126
|
+
status: 413,
|
|
127
|
+
resource: "provider-model-metadata",
|
|
128
|
+
limit: MAX_PROVIDER_MODELS,
|
|
129
|
+
retryable: false,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
117
132
|
return Response.json({ error: registry.error.message }, { status: 400 });
|
|
118
133
|
}
|
|
119
134
|
|
|
@@ -128,11 +143,14 @@ export const Route = createFileRoute("/api/providers/$providerId/model-metadata"
|
|
|
128
143
|
modelMetadataUrl: sourceUrl,
|
|
129
144
|
});
|
|
130
145
|
if (updated === null) {
|
|
131
|
-
return Response.json(
|
|
146
|
+
return Response.json(
|
|
147
|
+
{ error: "Secure provider storage is unavailable; metadata was not updated" },
|
|
148
|
+
{ status: 503 },
|
|
149
|
+
);
|
|
132
150
|
}
|
|
133
151
|
|
|
134
152
|
return Response.json({
|
|
135
|
-
provider: updated,
|
|
153
|
+
provider: toRedactedProviderDto(updated),
|
|
136
154
|
imported: result.imported,
|
|
137
155
|
matchedModels: result.matchedModels,
|
|
138
156
|
missingModels: result.missingModels,
|
|
@@ -6,8 +6,14 @@ import {
|
|
|
6
6
|
deleteProvider,
|
|
7
7
|
redactProvider,
|
|
8
8
|
} from "../../proxy/providers";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
getProviderModelLimitViolation,
|
|
11
|
+
ProviderModelMetadataListSchema,
|
|
12
|
+
ProviderModelNameSchema,
|
|
13
|
+
ProviderModelsSchema,
|
|
14
|
+
} from "../../lib/providerContract";
|
|
10
15
|
import { evaluateUpstreamUrl } from "../../lib/ssrfGuard";
|
|
16
|
+
import { resourceLimitResponse } from "../../lib/resourceLimits";
|
|
11
17
|
|
|
12
18
|
const ALLOW_LOOPBACK = process.env["AGENT_INSPECTOR_ALLOW_LOOPBACK"] === "1";
|
|
13
19
|
|
|
@@ -16,15 +22,15 @@ const ProviderUpdateSchema = z.object({
|
|
|
16
22
|
apiKey: z.string().min(1, "API key is required").optional(),
|
|
17
23
|
format: z.enum(["anthropic", "openai"]).optional(),
|
|
18
24
|
baseUrl: z.string().min(1, "Base URL is required").optional(),
|
|
19
|
-
model:
|
|
20
|
-
models:
|
|
25
|
+
model: ProviderModelNameSchema.optional(),
|
|
26
|
+
models: ProviderModelsSchema.optional(),
|
|
21
27
|
authHeader: z.enum(["bearer", "x-api-key"]).optional(),
|
|
22
28
|
anthropicBaseUrl: z.string().optional(),
|
|
23
29
|
openaiBaseUrl: z.string().optional(),
|
|
24
30
|
openaiResponsesBaseUrl: z.string().optional(),
|
|
25
31
|
apiDocsUrl: z.string().optional(),
|
|
26
32
|
modelMetadataUrl: z.string().optional(),
|
|
27
|
-
modelMetadata:
|
|
33
|
+
modelMetadata: ProviderModelMetadataListSchema.optional(),
|
|
28
34
|
source: z.enum(["company", "personal"]).optional(),
|
|
29
35
|
});
|
|
30
36
|
|
|
@@ -40,7 +46,26 @@ export const Route = createFileRoute("/api/providers/$providerId")({
|
|
|
40
46
|
return Response.json(redactProvider(provider));
|
|
41
47
|
},
|
|
42
48
|
PUT: async ({ params, request }: { params: { providerId: string }; request: Request }) => {
|
|
43
|
-
|
|
49
|
+
let body: unknown;
|
|
50
|
+
try {
|
|
51
|
+
body = await request.json();
|
|
52
|
+
} catch {
|
|
53
|
+
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const violation = getProviderModelLimitViolation(body);
|
|
57
|
+
if (violation !== null) {
|
|
58
|
+
return resourceLimitResponse({
|
|
59
|
+
error: `Provider model collections may contain at most ${String(violation.limit)} entries.`,
|
|
60
|
+
code: "provider-model-limit-exceeded",
|
|
61
|
+
status: 413,
|
|
62
|
+
resource: violation.resource,
|
|
63
|
+
limit: violation.limit,
|
|
64
|
+
retryable: false,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const parsed = ProviderUpdateSchema.safeParse(body);
|
|
44
69
|
if (!parsed.success) {
|
|
45
70
|
return Response.json({ error: parsed.error.message }, { status: 400 });
|
|
46
71
|
}
|
|
@@ -56,9 +81,15 @@ export const Route = createFileRoute("/api/providers/$providerId")({
|
|
|
56
81
|
return Response.json({ error: decision.reason }, { status: 400 });
|
|
57
82
|
}
|
|
58
83
|
}
|
|
84
|
+
const providerExists = getProviders().some((provider) => provider.id === params.providerId);
|
|
59
85
|
const updated = updateProvider(params.providerId, parsed.data);
|
|
60
86
|
if (!updated) {
|
|
61
|
-
return
|
|
87
|
+
return providerExists
|
|
88
|
+
? Response.json(
|
|
89
|
+
{ error: "Secure provider storage is unavailable; provider was not updated" },
|
|
90
|
+
{ status: 503 },
|
|
91
|
+
)
|
|
92
|
+
: Response.json({ error: "Provider not found" }, { status: 404 });
|
|
62
93
|
}
|
|
63
94
|
return Response.json(redactProvider(updated));
|
|
64
95
|
},
|
|
@@ -1,25 +1,43 @@
|
|
|
1
1
|
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { z } from "zod";
|
|
2
3
|
import { exportProviders, exportProvidersWithKeys } from "../../proxy/providers";
|
|
3
4
|
|
|
5
|
+
const SecretExportRequestSchema = z.object({
|
|
6
|
+
confirmation: z.literal("EXPORT_PROVIDER_KEYS"),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
function exportResponse(json: string, filename: string): Response {
|
|
10
|
+
return new Response(json, {
|
|
11
|
+
headers: {
|
|
12
|
+
"Content-Type": "application/json",
|
|
13
|
+
"Content-Disposition": `attachment; filename="${filename}"`,
|
|
14
|
+
"Cache-Control": "no-store",
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
4
19
|
export const Route = createFileRoute("/api/providers/export")({
|
|
5
20
|
server: {
|
|
6
21
|
handlers: {
|
|
7
|
-
GET: (
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
GET: () => {
|
|
23
|
+
return exportResponse(
|
|
24
|
+
exportProviders(),
|
|
25
|
+
`agent-inspector-providers-safe-${Date.now()}.json`,
|
|
26
|
+
);
|
|
27
|
+
},
|
|
28
|
+
POST: async ({ request }: { request: Request }) => {
|
|
29
|
+
const body: unknown = await request.json().catch(() => null);
|
|
30
|
+
const parsed = SecretExportRequestSchema.safeParse(body);
|
|
31
|
+
if (!parsed.success) {
|
|
32
|
+
return Response.json(
|
|
33
|
+
{ error: "Plaintext key export requires explicit confirmation" },
|
|
34
|
+
{ status: 400, headers: { "Cache-Control": "no-store" } },
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return exportResponse(
|
|
38
|
+
exportProvidersWithKeys(),
|
|
39
|
+
`agent-inspector-providers-sensitive-${Date.now()}.json`,
|
|
40
|
+
);
|
|
23
41
|
},
|
|
24
42
|
},
|
|
25
43
|
},
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { createFileRoute } from "@tanstack/react-router";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { ProviderScanImportRequestSchema } from "../../lib/providerImportContract";
|
|
5
|
+
import { consumeProviderScan } from "../../proxy/providerScanStore";
|
|
3
6
|
import { importProviders } from "../../proxy/providers";
|
|
4
7
|
|
|
5
|
-
const
|
|
8
|
+
const ProviderFileImportSchema = z.union([z.string(), z.object({ providers: z.unknown() })]);
|
|
6
9
|
|
|
7
10
|
export const Route = createFileRoute("/api/providers/import")({
|
|
8
11
|
server: {
|
|
@@ -13,12 +16,46 @@ export const Route = createFileRoute("/api/providers/import")({
|
|
|
13
16
|
let jsonContent: string;
|
|
14
17
|
try {
|
|
15
18
|
const parsedBody: unknown = JSON.parse(rawBody);
|
|
16
|
-
|
|
19
|
+
const scanRequest = ProviderScanImportRequestSchema.safeParse(parsedBody);
|
|
20
|
+
if (scanRequest.success) {
|
|
21
|
+
const selected = consumeProviderScan(
|
|
22
|
+
scanRequest.data.scanId,
|
|
23
|
+
scanRequest.data.selectionIds,
|
|
24
|
+
);
|
|
25
|
+
if (selected === null) {
|
|
26
|
+
return Response.json(
|
|
27
|
+
{ error: "Provider scan expired or selection is invalid" },
|
|
28
|
+
{ status: 410, headers: { "cache-control": "no-store" } },
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
const now = new Date().toISOString();
|
|
32
|
+
jsonContent = JSON.stringify({
|
|
33
|
+
providers: selected.map((provider) => ({
|
|
34
|
+
id: randomUUID(),
|
|
35
|
+
name: provider.name,
|
|
36
|
+
apiKey: provider.apiKey,
|
|
37
|
+
format: provider.format,
|
|
38
|
+
anthropicBaseUrl: provider.anthropicBaseUrl,
|
|
39
|
+
openaiBaseUrl: provider.openaiBaseUrl,
|
|
40
|
+
openaiResponsesBaseUrl: provider.openaiResponsesBaseUrl,
|
|
41
|
+
models: provider.models,
|
|
42
|
+
createdAt: now,
|
|
43
|
+
updatedAt: now,
|
|
44
|
+
})),
|
|
45
|
+
});
|
|
46
|
+
} else {
|
|
47
|
+
const fileImport = ProviderFileImportSchema.safeParse(parsedBody);
|
|
48
|
+
jsonContent = fileImport.success
|
|
49
|
+
? typeof fileImport.data === "string"
|
|
50
|
+
? fileImport.data
|
|
51
|
+
: JSON.stringify(fileImport.data)
|
|
52
|
+
: rawBody;
|
|
53
|
+
}
|
|
17
54
|
} catch {
|
|
18
55
|
jsonContent = rawBody;
|
|
19
56
|
}
|
|
20
57
|
|
|
21
|
-
if (
|
|
58
|
+
if (jsonContent.trim() === "") {
|
|
22
59
|
return Response.json({ error: "No JSON content provided" }, { status: 400 });
|
|
23
60
|
}
|
|
24
61
|
|
|
@@ -35,11 +72,8 @@ export const Route = createFileRoute("/api/providers/import")({
|
|
|
35
72
|
? `Import completed with ${result.errors.length} error(s)`
|
|
36
73
|
: "No providers imported",
|
|
37
74
|
});
|
|
38
|
-
} catch
|
|
39
|
-
return Response.json(
|
|
40
|
-
{ error: `Failed to import: ${err instanceof Error ? err.message : String(err)}` },
|
|
41
|
-
{ status: 400 },
|
|
42
|
-
);
|
|
75
|
+
} catch {
|
|
76
|
+
return Response.json({ error: "Provider import failed" }, { status: 400 });
|
|
43
77
|
}
|
|
44
78
|
},
|
|
45
79
|
},
|
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
import { createFileRoute } from "@tanstack/react-router";
|
|
2
2
|
import { scanExternalProviders } from "../../proxy/providerImporters";
|
|
3
|
+
import { createProviderScan } from "../../proxy/providerScanStore";
|
|
3
4
|
|
|
4
5
|
export const Route = createFileRoute("/api/providers/scan")({
|
|
5
6
|
server: {
|
|
6
7
|
handlers: {
|
|
7
|
-
|
|
8
|
+
POST: () => {
|
|
8
9
|
try {
|
|
9
10
|
const result = scanExternalProviders();
|
|
10
|
-
|
|
11
|
-
providers: result.providers,
|
|
12
|
-
warnings: result.warnings,
|
|
13
|
-
});
|
|
14
|
-
} catch (err) {
|
|
11
|
+
const scan = createProviderScan(result.providers);
|
|
15
12
|
return Response.json(
|
|
16
|
-
{
|
|
17
|
-
|
|
13
|
+
{
|
|
14
|
+
scanId: scan.scanId,
|
|
15
|
+
providers: scan.providers,
|
|
16
|
+
warnings: result.warnings,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
headers: { "cache-control": "no-store" },
|
|
20
|
+
},
|
|
18
21
|
);
|
|
22
|
+
} catch {
|
|
23
|
+
return Response.json({ error: "Provider scan failed" }, { status: 500 });
|
|
19
24
|
}
|
|
20
25
|
},
|
|
21
26
|
},
|