@tonyclaw/agent-inspector 3.0.26 → 3.0.28
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 +80 -11
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-SZRUDQc9.js → CompareDrawer-TghZr5t1.js} +1 -1
- package/.output/public/assets/{ProxyViewerContainer-BJvwOrSi.js → ProxyViewerContainer-fShBAZjj.js} +201 -201
- package/.output/public/assets/{ReplayDialog-DgfmqiM3.js → ReplayDialog-CZMApaF4.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-CS6c7Qfn.js → RequestAnatomy-DNWccR7O.js} +1 -1
- package/.output/public/assets/{ResponseView-C0Ri_5gm.js → ResponseView-d25MdFhY.js} +2 -2
- package/.output/public/assets/{StreamingChunkSequence-7dYcHWyZ.js → StreamingChunkSequence-DIFZEnWe.js} +1 -1
- package/.output/public/assets/{_sessionId-D-V84cgw.js → _sessionId-DRfvuE2k.js} +1 -1
- package/.output/public/assets/{index-DgFWCxOs.js → index-B3aAuqWq.js} +1 -1
- package/.output/public/assets/index-BsxAGqMA.css +1 -0
- package/.output/public/assets/{index-DrVJsks2.js → index-D4HkS7Ct.js} +1 -1
- package/.output/public/assets/{index-CSIL7-MK.js → index-DzARL2Fi.js} +3 -3
- package/.output/public/assets/{json-viewer-C9Ji3gYN.js → json-viewer-wuJSKaed.js} +1 -1
- package/.output/server/_libs/lucide-react.mjs +39 -39
- package/.output/server/{_sessionId-BQ6YQOO1.mjs → _sessionId-BJTNiP_O.mjs} +4 -2
- package/.output/server/_ssr/{CompareDrawer-CqNzIeMR.mjs → CompareDrawer-BMRH3N97.mjs} +5 -3
- package/.output/server/_ssr/{ProxyViewerContainer-CMC7Vsbw.mjs → ProxyViewerContainer-AFwcHAIR.mjs} +330 -223
- package/.output/server/_ssr/{ReplayDialog-D9p_LaTr.mjs → ReplayDialog-CbWvRHiN.mjs} +6 -4
- package/.output/server/_ssr/{RequestAnatomy-B4ge3qNZ.mjs → RequestAnatomy-C4kcPyqN.mjs} +4 -2
- package/.output/server/_ssr/{ResponseView-BCqMKApI.mjs → ResponseView-D8VYt-gZ.mjs} +5 -3
- package/.output/server/_ssr/{StreamingChunkSequence-BA4zTLE4.mjs → StreamingChunkSequence-BUUo6vU0.mjs} +4 -2
- package/.output/server/_ssr/{index-CzxhDjlq.mjs → index-DTwMEEXO.mjs} +4 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-B3qV6iEJ.mjs → json-viewer-BqF1pSQD.mjs} +5 -3
- package/.output/server/_ssr/{router-CZe_R31M.mjs → router-C8BOns7y.mjs} +839 -368
- package/.output/server/{_tanstack-start-manifest_v-B50VTLrH.mjs → _tanstack-start-manifest_v-BBf2nRhb.mjs} +1 -1
- package/.output/server/index.mjs +70 -70
- package/README.md +8 -0
- package/package.json +4 -5
- package/src/cli.ts +43 -9
- package/src/components/ProxyViewer.tsx +57 -1
- package/src/components/clients/ClientLogo.tsx +43 -4
- package/src/components/inspector-pet/InspectorPet.tsx +0 -7
- package/src/components/providers/ProviderCard.tsx +3 -36
- package/src/components/providers/ProviderForm.tsx +13 -12
- package/src/components/providers/ProvidersPanel.tsx +2 -2
- package/src/components/providers/SettingsDialog.tsx +3 -3
- package/src/components/proxy-viewer/ConversationGroup.tsx +1 -1
- package/src/components/proxy-viewer/ConversationGroupList.tsx +233 -152
- package/src/components/proxy-viewer/ConversationHeader.tsx +29 -13
- package/src/components/proxy-viewer/ProviderLogoStack.tsx +0 -3
- package/src/contracts/anthropic.ts +0 -5
- package/src/contracts/index.ts +0 -4
- package/src/contracts/log.ts +0 -1
- package/src/contracts/openai.ts +0 -3
- package/src/knowledge/openclawClient.ts +22 -9
- package/src/knowledge/openclawGatewayClient.ts +20 -13
- package/src/lib/alertContract.ts +0 -3
- package/src/lib/ecosystemContract.ts +0 -3
- package/src/lib/export-logs.ts +1 -4
- package/src/lib/jsonFile.ts +46 -0
- package/src/lib/safeFetch.ts +100 -0
- package/src/lib/ssrfGuard.ts +133 -0
- package/src/lib/stopReason.ts +1 -4
- package/src/lib/unknown.ts +27 -0
- package/src/lib/useProviders.ts +1 -1
- package/src/mcp/mode.ts +3 -9
- package/src/mcp/server.ts +5 -14
- package/src/mcp/toolHandlers.ts +45 -54
- package/src/proxy/constants.ts +17 -0
- package/src/proxy/evidenceAnalysis.ts +116 -117
- package/src/proxy/formats/anthropic/handler.ts +5 -1
- package/src/proxy/formats/anthropic/schemas.ts +1 -7
- package/src/proxy/formats/index.ts +1 -1
- package/src/proxy/formats/openai/schemas.ts +0 -3
- package/src/proxy/formats/registry.ts +0 -3
- package/src/proxy/groupEvidenceExporter.ts +4 -8
- package/src/proxy/groupStore.ts +3 -10
- package/src/proxy/handler.ts +91 -12
- package/src/proxy/identityProxy.ts +74 -1
- package/src/proxy/logImporter.ts +82 -12
- package/src/proxy/logIndex.ts +1 -4
- package/src/proxy/providerSecretStore.ts +177 -0
- package/src/proxy/providers.ts +84 -11
- package/src/proxy/runFailures.ts +4 -10
- package/src/proxy/runStore.ts +4 -11
- package/src/proxy/schemas.ts +1 -8
- package/src/proxy/sqliteLogIndex.ts +0 -16
- package/src/proxy/store.ts +1 -5
- package/src/proxy/toolSchemaWarnings.ts +1 -9
- package/src/routes/api/logs.$id.replay.ts +17 -5
- package/src/routes/api/providers.$providerId.model-metadata.ts +11 -1
- package/src/routes/api/providers.$providerId.ts +18 -5
- package/src/routes/api/providers.ts +18 -7
- package/.output/public/assets/index-CmeF5XXS.css +0 -1
package/src/mcp/server.ts
CHANGED
|
@@ -1262,15 +1262,10 @@ function registerTools(server: McpServer): void {
|
|
|
1262
1262
|
{
|
|
1263
1263
|
title: "List configured LLM providers",
|
|
1264
1264
|
description:
|
|
1265
|
-
"Returns the configured ProviderConfig array with
|
|
1266
|
-
inputSchema: z.object({
|
|
1267
|
-
includeSecrets: z
|
|
1268
|
-
.boolean()
|
|
1269
|
-
.optional()
|
|
1270
|
-
.describe("Return plaintext provider API keys. Exposes local secrets when true."),
|
|
1271
|
-
}),
|
|
1265
|
+
"Returns the configured ProviderConfig array with write-only API keys represented as configured.",
|
|
1266
|
+
inputSchema: z.object({}),
|
|
1272
1267
|
},
|
|
1273
|
-
(
|
|
1268
|
+
() => safeCall(() => listProvidersImpl(callApi)),
|
|
1274
1269
|
);
|
|
1275
1270
|
|
|
1276
1271
|
// ----- Action tools -----
|
|
@@ -1280,16 +1275,12 @@ function registerTools(server: McpServer): void {
|
|
|
1280
1275
|
{
|
|
1281
1276
|
title: "Get a single provider by id",
|
|
1282
1277
|
description:
|
|
1283
|
-
"Returns one ProviderConfig
|
|
1278
|
+
"Returns one ProviderConfig. API keys are write-only and represented as configured.",
|
|
1284
1279
|
inputSchema: z.object({
|
|
1285
1280
|
id: z.string().describe("The provider id."),
|
|
1286
|
-
includeSecrets: z
|
|
1287
|
-
.boolean()
|
|
1288
|
-
.optional()
|
|
1289
|
-
.describe("Return the plaintext provider API key. Exposes a local secret when true."),
|
|
1290
1281
|
}),
|
|
1291
1282
|
},
|
|
1292
|
-
({ id
|
|
1283
|
+
({ id }) => safeCall(() => getProviderImpl(callApi, id)),
|
|
1293
1284
|
);
|
|
1294
1285
|
|
|
1295
1286
|
server.registerTool(
|
package/src/mcp/toolHandlers.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
} from "../lib/groupContract";
|
|
37
37
|
import { type CreateInspectorRunInput, type UpdateInspectorRunInput } from "../lib/runContract";
|
|
38
38
|
import { SessionInfoSchema } from "../lib/sessionInfoContract";
|
|
39
|
+
import { isRecord, textByteLength } from "../lib/unknown";
|
|
39
40
|
import { CapturedLogSchema, type CapturedLog } from "../contracts";
|
|
40
41
|
import {
|
|
41
42
|
addInspectorGroupSession,
|
|
@@ -112,11 +113,6 @@ function inspectorBaseUrl(): string {
|
|
|
112
113
|
return `http://127.0.0.1:${String(getCurrentPort())}`;
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
function textByteLength(value: string | null): number | null {
|
|
116
|
-
if (value === null) return null;
|
|
117
|
-
return Buffer.byteLength(value, "utf8");
|
|
118
|
-
}
|
|
119
|
-
|
|
120
116
|
function hasBodyText(value: string | null, byteLength: number | null | undefined): boolean {
|
|
121
117
|
if (value !== null && value.length > 0) return true;
|
|
122
118
|
return byteLength !== undefined && byteLength !== null && byteLength > 0;
|
|
@@ -272,15 +268,44 @@ async function readCompactLogsPage(
|
|
|
272
268
|
params: URLSearchParams,
|
|
273
269
|
): Promise<{ logs: CapturedLog[]; total: number } | ToolResult> {
|
|
274
270
|
const path = `/api/logs?${params.toString()}`;
|
|
275
|
-
const
|
|
276
|
-
if (!
|
|
277
|
-
const rawBody: unknown = await res.json();
|
|
278
|
-
const parsed = LogsListResponseSchema.safeParse(rawBody);
|
|
279
|
-
if (!parsed.success) return toolError("GET /api/logs returned an unparseable logs shape");
|
|
271
|
+
const parsed = await fetchApiJson(callApi, path, LogsListResponseSchema);
|
|
272
|
+
if (!parsed.ok) return parsed.error;
|
|
280
273
|
const logs = parsed.data.logs ?? [];
|
|
281
274
|
return { logs, total: parsed.data.total ?? logs.length };
|
|
282
275
|
}
|
|
283
276
|
|
|
277
|
+
/**
|
|
278
|
+
* Fetches a JSON endpoint and validates it against a Zod schema in one call.
|
|
279
|
+
* On HTTP error or schema mismatch returns a `ToolResult` error so the caller
|
|
280
|
+
* can propagate it directly. On success returns the typed parsed value.
|
|
281
|
+
*/
|
|
282
|
+
type FetchApiResult<T> = { ok: true; data: T } | { ok: false; error: ToolResult };
|
|
283
|
+
|
|
284
|
+
async function fetchApiJson<T>(
|
|
285
|
+
callApi: CallApiFn,
|
|
286
|
+
path: string,
|
|
287
|
+
schema: z.ZodType<T>,
|
|
288
|
+
init?: RequestInit,
|
|
289
|
+
): Promise<FetchApiResult<T>> {
|
|
290
|
+
const res = await callApi(path, init);
|
|
291
|
+
if (!res.ok) {
|
|
292
|
+
return { ok: false, error: toolError(`${methodFor(init)} ${path} returned ${res.status}`) };
|
|
293
|
+
}
|
|
294
|
+
const rawBody: unknown = await res.json();
|
|
295
|
+
const parsed = schema.safeParse(rawBody);
|
|
296
|
+
if (!parsed.success) {
|
|
297
|
+
return {
|
|
298
|
+
ok: false,
|
|
299
|
+
error: toolError(`${methodFor(init)} ${path} returned an unparseable shape`),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
return { ok: true, data: parsed.data };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function methodFor(init: RequestInit | undefined): string {
|
|
306
|
+
return (init?.method ?? "GET").toUpperCase();
|
|
307
|
+
}
|
|
308
|
+
|
|
284
309
|
function isToolResult(value: unknown): value is ToolResult {
|
|
285
310
|
return (
|
|
286
311
|
typeof value === "object" &&
|
|
@@ -373,13 +398,8 @@ export async function searchLogsImpl(
|
|
|
373
398
|
}
|
|
374
399
|
|
|
375
400
|
export async function getLogImpl(callApi: CallApiFn, id: number): Promise<ToolResult> {
|
|
376
|
-
const
|
|
377
|
-
if (!
|
|
378
|
-
const rawBody: unknown = await res.json();
|
|
379
|
-
const parsed = CapturedLogSchema.safeParse(rawBody);
|
|
380
|
-
if (!parsed.success) {
|
|
381
|
-
return toolError(`GET /api/logs/${id} returned an unparseable CapturedLog`);
|
|
382
|
-
}
|
|
401
|
+
const parsed = await fetchApiJson(callApi, `/api/logs/${id}`, CapturedLogSchema);
|
|
402
|
+
if (!parsed.ok) return parsed.error;
|
|
383
403
|
return textJson(buildBoundedLogDetail(parsed.data));
|
|
384
404
|
}
|
|
385
405
|
|
|
@@ -413,11 +433,8 @@ export async function getSessionImpl(
|
|
|
413
433
|
params.set("includeHistory", "1");
|
|
414
434
|
}
|
|
415
435
|
const path = `/api/sessions?${params.toString()}`;
|
|
416
|
-
const
|
|
417
|
-
if (!
|
|
418
|
-
const rawBody: unknown = await res.json();
|
|
419
|
-
const parsed = SessionInfoSchema.safeParse(rawBody);
|
|
420
|
-
if (!parsed.success) return toolError("GET /api/sessions returned an unparseable session info");
|
|
436
|
+
const parsed = await fetchApiJson(callApi, path, SessionInfoSchema);
|
|
437
|
+
if (!parsed.ok) return parsed.error;
|
|
421
438
|
return textJson(parsed.data);
|
|
422
439
|
}
|
|
423
440
|
|
|
@@ -724,13 +741,8 @@ export async function getProcessTimelineImpl(
|
|
|
724
741
|
}
|
|
725
742
|
|
|
726
743
|
export async function getLogHeadersImpl(callApi: CallApiFn, id: number): Promise<ToolResult> {
|
|
727
|
-
const
|
|
728
|
-
if (!
|
|
729
|
-
const rawBody: unknown = await res.json();
|
|
730
|
-
const parsed = CapturedLogSchema.safeParse(rawBody);
|
|
731
|
-
if (!parsed.success) {
|
|
732
|
-
return toolError(`GET /api/logs/${id} returned an unparseable CapturedLog`);
|
|
733
|
-
}
|
|
744
|
+
const parsed = await fetchApiJson(callApi, `/api/logs/${id}`, CapturedLogSchema);
|
|
745
|
+
if (!parsed.ok) return parsed.error;
|
|
734
746
|
const log = parsed.data;
|
|
735
747
|
return textJson({
|
|
736
748
|
id: log.id,
|
|
@@ -1045,31 +1057,14 @@ export async function listModelsImpl(callApi: CallApiFn): Promise<ToolResult> {
|
|
|
1045
1057
|
return textJson(await res.json());
|
|
1046
1058
|
}
|
|
1047
1059
|
|
|
1048
|
-
export
|
|
1049
|
-
|
|
1050
|
-
};
|
|
1051
|
-
|
|
1052
|
-
function includeSecretsQuery(includeSecrets: boolean | undefined): string {
|
|
1053
|
-
return includeSecrets === true ? "?includeSecrets=1" : "";
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
export async function listProvidersImpl(
|
|
1057
|
-
callApi: CallApiFn,
|
|
1058
|
-
args: ProviderReadArgs = {},
|
|
1059
|
-
): Promise<ToolResult> {
|
|
1060
|
-
const res = await callApi(`/api/providers${includeSecretsQuery(args.includeSecrets)}`);
|
|
1060
|
+
export async function listProvidersImpl(callApi: CallApiFn): Promise<ToolResult> {
|
|
1061
|
+
const res = await callApi("/api/providers");
|
|
1061
1062
|
if (!res.ok) return toolError(`GET /api/providers returned ${res.status}`);
|
|
1062
1063
|
return textJson(await res.json());
|
|
1063
1064
|
}
|
|
1064
1065
|
|
|
1065
|
-
export async function getProviderImpl(
|
|
1066
|
-
callApi
|
|
1067
|
-
id: string,
|
|
1068
|
-
args: ProviderReadArgs = {},
|
|
1069
|
-
): Promise<ToolResult> {
|
|
1070
|
-
const res = await callApi(
|
|
1071
|
-
`/api/providers/${encodeURIComponent(id)}${includeSecretsQuery(args.includeSecrets)}`,
|
|
1072
|
-
);
|
|
1066
|
+
export async function getProviderImpl(callApi: CallApiFn, id: string): Promise<ToolResult> {
|
|
1067
|
+
const res = await callApi(`/api/providers/${encodeURIComponent(id)}`);
|
|
1073
1068
|
if (!res.ok) return toolError(`GET /api/providers/${id} returned ${res.status}`);
|
|
1074
1069
|
return textJson(await res.json());
|
|
1075
1070
|
}
|
|
@@ -1182,10 +1177,6 @@ type ProviderDiagnosticIssue = {
|
|
|
1182
1177
|
endpoint: string | null;
|
|
1183
1178
|
};
|
|
1184
1179
|
|
|
1185
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
1186
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
1180
|
function stringField(record: Record<string, unknown>, key: string): string | null {
|
|
1190
1181
|
const value = record[key];
|
|
1191
1182
|
return typeof value === "string" && value.length > 0 ? value : null;
|
package/src/proxy/constants.ts
CHANGED
|
@@ -26,12 +26,29 @@ export const HEADER_HOST = "host";
|
|
|
26
26
|
// Auth header types
|
|
27
27
|
export const AUTH_HEADER_X_API_KEY = "x-api-key";
|
|
28
28
|
|
|
29
|
+
// Headers whose values contain secrets (API keys, bearer tokens).
|
|
30
|
+
// These are redacted before being written to logs when captureMode is "full".
|
|
31
|
+
export const SENSITIVE_HEADERS = new Set([
|
|
32
|
+
HEADER_AUTHORIZATION,
|
|
33
|
+
HEADER_X_API_KEY,
|
|
34
|
+
"cookie",
|
|
35
|
+
"set-cookie",
|
|
36
|
+
"proxy-authorization",
|
|
37
|
+
]);
|
|
38
|
+
|
|
29
39
|
// Content types
|
|
30
40
|
export const CONTENT_TYPE_EVENT_STREAM = "text/event-stream";
|
|
31
41
|
|
|
32
42
|
// Response status
|
|
33
43
|
export const STATUS_FORBIDDEN = 403;
|
|
34
44
|
export const STATUS_BAD_GATEWAY = 502;
|
|
45
|
+
export const STATUS_PAYLOAD_TOO_LARGE = 413;
|
|
46
|
+
|
|
47
|
+
// Maximum request body size accepted by the proxy (50 MB).
|
|
48
|
+
// Legitimate LLM API requests (system prompts, conversation history, tool
|
|
49
|
+
// definitions) are well under this limit; larger payloads are almost certainly
|
|
50
|
+
// malicious or malformed.
|
|
51
|
+
export const MAX_REQUEST_BODY_BYTES = 50 * 1024 * 1024;
|
|
35
52
|
|
|
36
53
|
// Standard/safe headers to preserve when forwarding to upstream.
|
|
37
54
|
// NOTE: Authorization is intentionally excluded to prevent client auth tokens from leaking.
|
|
@@ -96,86 +96,113 @@ function hasStreamingMismatch(session: SessionInfo): boolean {
|
|
|
96
96
|
return false;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return "Provider rejected the request because of rate limit or quota pressure.";
|
|
113
|
-
case "network-or-container":
|
|
114
|
-
return "Network connectivity looks suspicious; check container boundaries and localhost use.";
|
|
115
|
-
case "streaming-mismatch":
|
|
116
|
-
return "Streaming and non-streaming behavior differ for the same provider/model.";
|
|
117
|
-
case "context-pressure":
|
|
118
|
-
return "The failure looks related to context window or token pressure.";
|
|
119
|
-
case "runtime-task-error":
|
|
120
|
-
return "Inspector session runtime reported a task error.";
|
|
121
|
-
case "model-output-error":
|
|
122
|
-
return "The model output or upstream payload shape appears invalid.";
|
|
123
|
-
case "unknown-failure":
|
|
124
|
-
return "Inspector captured errors, but the root cause is not yet classified.";
|
|
125
|
-
case "no-session-data":
|
|
126
|
-
return "No Inspector session data was available for this run.";
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function categoryHints(category: FailureCategory): string[] {
|
|
131
|
-
switch (category) {
|
|
132
|
-
case "none":
|
|
133
|
-
return ["Keep the evidence pack as the success proof for CI or release notes."];
|
|
134
|
-
case "active":
|
|
135
|
-
return ["Wait for the session to settle, then export evidence again."];
|
|
136
|
-
case "provider-timeout":
|
|
137
|
-
return [
|
|
138
|
-
"Increase the provider test timeout for slow models.",
|
|
139
|
-
"Compare streaming and non-streaming probes for the same model.",
|
|
140
|
-
];
|
|
141
|
-
case "provider-http-error":
|
|
142
|
-
return [
|
|
143
|
-
"Open the cited log ids and inspect upstream status, response body, and provider URL.",
|
|
144
|
-
];
|
|
145
|
-
case "provider-auth-error":
|
|
146
|
-
return ["Verify the provider API key, auth header mode, and configured base URL."];
|
|
147
|
-
case "provider-rate-limit":
|
|
148
|
-
return [
|
|
149
|
-
"Retry after provider quota recovers or switch to a less constrained provider/model.",
|
|
150
|
-
];
|
|
151
|
-
case "network-or-container":
|
|
152
|
-
return [
|
|
153
|
-
"If the coding agent and Inspector are in different containers, replace localhost with a reachable host.",
|
|
154
|
-
"Check DNS, proxy, firewall, and container network routes.",
|
|
155
|
-
];
|
|
156
|
-
case "streaming-mismatch":
|
|
157
|
-
return [
|
|
158
|
-
"Compare the streaming and non-streaming captured logs side by side.",
|
|
159
|
-
"Check whether the provider's OpenAI-compatible endpoint handles stream=false differently.",
|
|
160
|
-
];
|
|
161
|
-
case "context-pressure":
|
|
162
|
-
return [
|
|
163
|
-
"Reduce prompt/tool schema/history size or configure the model context window metadata.",
|
|
164
|
-
];
|
|
165
|
-
case "runtime-task-error":
|
|
166
|
-
return ["Inspect the session runtime error and retry after clearing stuck session tasks."];
|
|
167
|
-
case "model-output-error":
|
|
168
|
-
return ["Inspect the raw response and schema parsing details for the cited log ids."];
|
|
169
|
-
case "unknown-failure":
|
|
170
|
-
return [
|
|
171
|
-
"Open the cited logs and inspect error/status/latency fields to refine the diagnosis.",
|
|
172
|
-
];
|
|
173
|
-
case "no-session-data":
|
|
174
|
-
return [
|
|
175
|
-
"Confirm the run's sessionId matches captured traffic and that history scanning is enabled when needed.",
|
|
176
|
-
];
|
|
99
|
+
/**
|
|
100
|
+
* Per-category summary, hints, severity, and outcome. Keyed by every
|
|
101
|
+
* `FailureCategory` so TypeScript's `Record` enforces exhaustiveness —
|
|
102
|
+
* adding a new category is a single-place compile error instead of four
|
|
103
|
+
* parallel switch statements missing a case.
|
|
104
|
+
*/
|
|
105
|
+
const FAILURE_CATEGORY_INFO: Record<
|
|
106
|
+
FailureCategory,
|
|
107
|
+
{
|
|
108
|
+
summary: string;
|
|
109
|
+
hints: string[];
|
|
110
|
+
severity: FailureClassification["severity"];
|
|
111
|
+
outcome: FailureClassification["outcome"];
|
|
177
112
|
}
|
|
178
|
-
|
|
113
|
+
> = {
|
|
114
|
+
none: {
|
|
115
|
+
summary: "Session completed without captured errors.",
|
|
116
|
+
hints: ["Keep the evidence pack as the success proof for CI or release notes."],
|
|
117
|
+
severity: "none",
|
|
118
|
+
outcome: "success",
|
|
119
|
+
},
|
|
120
|
+
active: {
|
|
121
|
+
summary: "Session still has active requests or running tasks.",
|
|
122
|
+
hints: ["Wait for the session to settle, then export evidence again."],
|
|
123
|
+
severity: "low",
|
|
124
|
+
outcome: "active",
|
|
125
|
+
},
|
|
126
|
+
"provider-timeout": {
|
|
127
|
+
summary: "Provider call appears to have timed out.",
|
|
128
|
+
hints: [
|
|
129
|
+
"Increase the provider test timeout for slow models.",
|
|
130
|
+
"Compare streaming and non-streaming probes for the same model.",
|
|
131
|
+
],
|
|
132
|
+
severity: "high",
|
|
133
|
+
outcome: "failure",
|
|
134
|
+
},
|
|
135
|
+
"provider-http-error": {
|
|
136
|
+
summary: "Provider returned an HTTP error.",
|
|
137
|
+
hints: ["Open the cited log ids and inspect upstream status, response body, and provider URL."],
|
|
138
|
+
severity: "high",
|
|
139
|
+
outcome: "failure",
|
|
140
|
+
},
|
|
141
|
+
"provider-auth-error": {
|
|
142
|
+
summary: "Provider authentication or API key configuration appears invalid.",
|
|
143
|
+
hints: ["Verify the provider API key, auth header mode, and configured base URL."],
|
|
144
|
+
severity: "high",
|
|
145
|
+
outcome: "failure",
|
|
146
|
+
},
|
|
147
|
+
"provider-rate-limit": {
|
|
148
|
+
summary: "Provider rejected the request because of rate limit or quota pressure.",
|
|
149
|
+
hints: ["Retry after provider quota recovers or switch to a less constrained provider/model."],
|
|
150
|
+
severity: "high",
|
|
151
|
+
outcome: "failure",
|
|
152
|
+
},
|
|
153
|
+
"network-or-container": {
|
|
154
|
+
summary: "Network connectivity looks suspicious; check container boundaries and localhost use.",
|
|
155
|
+
hints: [
|
|
156
|
+
"If the coding agent and Inspector are in different containers, replace localhost with a reachable host.",
|
|
157
|
+
"Check DNS, proxy, firewall, and container network routes.",
|
|
158
|
+
],
|
|
159
|
+
severity: "high",
|
|
160
|
+
outcome: "failure",
|
|
161
|
+
},
|
|
162
|
+
"streaming-mismatch": {
|
|
163
|
+
summary: "Streaming and non-streaming behavior differ for the same provider/model.",
|
|
164
|
+
hints: [
|
|
165
|
+
"Compare the streaming and non-streaming captured logs side by side.",
|
|
166
|
+
"Check whether the provider's OpenAI-compatible endpoint handles stream=false differently.",
|
|
167
|
+
],
|
|
168
|
+
severity: "high",
|
|
169
|
+
outcome: "failure",
|
|
170
|
+
},
|
|
171
|
+
"context-pressure": {
|
|
172
|
+
summary: "The failure looks related to context window or token pressure.",
|
|
173
|
+
hints: [
|
|
174
|
+
"Reduce prompt/tool schema/history size or configure the model context window metadata.",
|
|
175
|
+
],
|
|
176
|
+
severity: "medium",
|
|
177
|
+
outcome: "failure",
|
|
178
|
+
},
|
|
179
|
+
"runtime-task-error": {
|
|
180
|
+
summary: "Inspector session runtime reported a task error.",
|
|
181
|
+
hints: ["Inspect the session runtime error and retry after clearing stuck session tasks."],
|
|
182
|
+
severity: "high",
|
|
183
|
+
outcome: "failure",
|
|
184
|
+
},
|
|
185
|
+
"model-output-error": {
|
|
186
|
+
summary: "The model output or upstream payload shape appears invalid.",
|
|
187
|
+
hints: ["Inspect the raw response and schema parsing details for the cited log ids."],
|
|
188
|
+
severity: "medium",
|
|
189
|
+
outcome: "failure",
|
|
190
|
+
},
|
|
191
|
+
"unknown-failure": {
|
|
192
|
+
summary: "Inspector captured errors, but the root cause is not yet classified.",
|
|
193
|
+
hints: ["Open the cited logs and inspect error/status/latency fields to refine the diagnosis."],
|
|
194
|
+
severity: "medium",
|
|
195
|
+
outcome: "failure",
|
|
196
|
+
},
|
|
197
|
+
"no-session-data": {
|
|
198
|
+
summary: "No Inspector session data was available for this run.",
|
|
199
|
+
hints: [
|
|
200
|
+
"Confirm the run's sessionId matches captured traffic and that history scanning is enabled when needed.",
|
|
201
|
+
],
|
|
202
|
+
severity: "medium",
|
|
203
|
+
outcome: "unknown",
|
|
204
|
+
},
|
|
205
|
+
};
|
|
179
206
|
|
|
180
207
|
function classifyCategory(
|
|
181
208
|
session: SessionInfo,
|
|
@@ -221,47 +248,19 @@ function classifyCategory(
|
|
|
221
248
|
}
|
|
222
249
|
|
|
223
250
|
function severityFor(category: FailureCategory): FailureClassification["severity"] {
|
|
224
|
-
|
|
225
|
-
case "none":
|
|
226
|
-
return "none";
|
|
227
|
-
case "active":
|
|
228
|
-
return "low";
|
|
229
|
-
case "provider-timeout":
|
|
230
|
-
case "provider-http-error":
|
|
231
|
-
case "provider-auth-error":
|
|
232
|
-
case "provider-rate-limit":
|
|
233
|
-
case "network-or-container":
|
|
234
|
-
case "streaming-mismatch":
|
|
235
|
-
case "runtime-task-error":
|
|
236
|
-
return "high";
|
|
237
|
-
case "context-pressure":
|
|
238
|
-
case "model-output-error":
|
|
239
|
-
case "unknown-failure":
|
|
240
|
-
case "no-session-data":
|
|
241
|
-
return "medium";
|
|
242
|
-
}
|
|
251
|
+
return FAILURE_CATEGORY_INFO[category].severity;
|
|
243
252
|
}
|
|
244
253
|
|
|
245
254
|
function outcomeFor(category: FailureCategory): FailureClassification["outcome"] {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
case "provider-auth-error":
|
|
256
|
-
case "provider-rate-limit":
|
|
257
|
-
case "network-or-container":
|
|
258
|
-
case "streaming-mismatch":
|
|
259
|
-
case "context-pressure":
|
|
260
|
-
case "runtime-task-error":
|
|
261
|
-
case "model-output-error":
|
|
262
|
-
case "unknown-failure":
|
|
263
|
-
return "failure";
|
|
264
|
-
}
|
|
255
|
+
return FAILURE_CATEGORY_INFO[category].outcome;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function categorySummary(category: FailureCategory): string {
|
|
259
|
+
return FAILURE_CATEGORY_INFO[category].summary;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function categoryHints(category: FailureCategory): string[] {
|
|
263
|
+
return FAILURE_CATEGORY_INFO[category].hints;
|
|
265
264
|
}
|
|
266
265
|
|
|
267
266
|
export function classifyRunFailure(
|
|
@@ -13,7 +13,11 @@ export const AnthropicFormatHandler: FormatHandler = {
|
|
|
13
13
|
if (result.success) {
|
|
14
14
|
return {
|
|
15
15
|
model: result.data.model,
|
|
16
|
-
sessionId:
|
|
16
|
+
sessionId:
|
|
17
|
+
result.data.metadata?.user_id ??
|
|
18
|
+
headers?.get("x-agent-inspector-session-id") ??
|
|
19
|
+
headers?.get("x-session-affinity") ??
|
|
20
|
+
null,
|
|
17
21
|
};
|
|
18
22
|
}
|
|
19
23
|
return null;
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
export {
|
|
2
2
|
AnthropicRequestSchema,
|
|
3
3
|
AnthropicResponseSchema,
|
|
4
|
-
InspectorRequestSchema,
|
|
5
|
-
InspectorResponseSchema,
|
|
6
4
|
SseEventSchema,
|
|
7
5
|
} from "../../../contracts/anthropic";
|
|
8
|
-
export type {
|
|
9
|
-
AnthropicResponse,
|
|
10
|
-
InspectorResponse,
|
|
11
|
-
ResponseContentBlockType,
|
|
12
|
-
} from "../../../contracts/anthropic";
|
|
6
|
+
export type { AnthropicResponse, ResponseContentBlockType } from "../../../contracts/anthropic";
|
|
@@ -9,5 +9,5 @@ import "./openai/zhipuProvider";
|
|
|
9
9
|
import "./openai/alibabaProvider";
|
|
10
10
|
|
|
11
11
|
export type { FormatHandler, ParsedRequest } from "./handler";
|
|
12
|
-
export { apiFormatForPath, formatRegistry, formatForPath
|
|
12
|
+
export { apiFormatForPath, formatRegistry, formatForPath } from "./registry";
|
|
13
13
|
export { registry } from "./providers";
|
|
@@ -72,9 +72,6 @@ export function apiFormatForPath(path: string): ApiFormat {
|
|
|
72
72
|
return formatRegistry.getApiFormatByPath(path) ?? "unknown";
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
/** Backward-compatible alias for older internal imports. */
|
|
76
|
-
export const requestFormatForPath = apiFormatForPath;
|
|
77
|
-
|
|
78
75
|
// Register known paths
|
|
79
76
|
formatRegistry.registerPath(PATH_V1_MESSAGES, "anthropic");
|
|
80
77
|
formatRegistry.registerPath(PATH_V1_CHAT_COMPLETIONS, "openai");
|
|
@@ -15,6 +15,7 @@ import { getDataDir } from "./dataDir";
|
|
|
15
15
|
import { getRun } from "./runStore";
|
|
16
16
|
import { getSessionInfo } from "./store";
|
|
17
17
|
import { updateGroupEvidence } from "./groupStore";
|
|
18
|
+
import { readJsonFile } from "../lib/jsonFile";
|
|
18
19
|
|
|
19
20
|
type GroupEvidenceDocument = GroupEvidenceExportResult;
|
|
20
21
|
|
|
@@ -343,12 +344,7 @@ export function readGroupEvidenceMarkdown(group: InspectorGroup): string | null
|
|
|
343
344
|
|
|
344
345
|
export function readGroupEvidenceDocument(group: InspectorGroup): GroupEvidenceExportResult | null {
|
|
345
346
|
const path = group.evidence?.jsonPath;
|
|
346
|
-
if (path === undefined
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const parsed = GroupEvidenceExportResultSchema.safeParse(raw);
|
|
350
|
-
return parsed.success ? parsed.data : null;
|
|
351
|
-
} catch {
|
|
352
|
-
return null;
|
|
353
|
-
}
|
|
347
|
+
if (path === undefined) return null;
|
|
348
|
+
const result = readJsonFile(path, GroupEvidenceExportResultSchema);
|
|
349
|
+
return result.ok ? result.data : null;
|
|
354
350
|
}
|
package/src/proxy/groupStore.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
type UpdateInspectorGroupInput,
|
|
20
20
|
} from "../lib/groupContract";
|
|
21
21
|
import { getDataDir } from "./dataDir";
|
|
22
|
+
import { readJsonFile } from "../lib/jsonFile";
|
|
22
23
|
|
|
23
24
|
const GroupsFileSchema = z.object({
|
|
24
25
|
groups: z.array(InspectorGroupSchema),
|
|
@@ -70,16 +71,8 @@ function nullableText(value: string | null | undefined): string | null {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
function readGroups(): InspectorGroup[] {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
const parsed: unknown = JSON.parse(readFileSync(filePath, "utf8"));
|
|
78
|
-
const result = GroupsFileSchema.safeParse(parsed);
|
|
79
|
-
return result.success ? result.data.groups : [];
|
|
80
|
-
} catch {
|
|
81
|
-
return [];
|
|
82
|
-
}
|
|
74
|
+
const result = readJsonFile(groupsFilePath(), GroupsFileSchema);
|
|
75
|
+
return result.ok ? result.data.groups : [];
|
|
83
76
|
}
|
|
84
77
|
|
|
85
78
|
function writeGroups(groups: readonly InspectorGroup[]): void {
|