@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
|
@@ -24,6 +24,22 @@ import { resolveLogFormat } from "./log-formats";
|
|
|
24
24
|
const HEADER_ACTION_BUTTON_CLASS =
|
|
25
25
|
"bg-white/[0.045] hover:bg-white/[0.075] inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:text-foreground";
|
|
26
26
|
|
|
27
|
+
export function isSyntheticSessionId(conversationId: string): boolean {
|
|
28
|
+
return (
|
|
29
|
+
conversationId.startsWith("PID:") ||
|
|
30
|
+
conversationId.startsWith("process:") ||
|
|
31
|
+
conversationId.startsWith("connection:") ||
|
|
32
|
+
conversationId.includes("|")
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function compactSessionLabel(conversationId: string): string {
|
|
37
|
+
if (isSyntheticSessionId(conversationId)) return "Sessionless";
|
|
38
|
+
return conversationId.length > 24
|
|
39
|
+
? `${conversationId.slice(0, 12)}...${conversationId.slice(-12)}`
|
|
40
|
+
: conversationId;
|
|
41
|
+
}
|
|
42
|
+
|
|
27
43
|
function HeaderMetric({
|
|
28
44
|
icon,
|
|
29
45
|
children,
|
|
@@ -52,7 +68,7 @@ function HeaderMetric({
|
|
|
52
68
|
function PendingTurnBadge(): JSX.Element {
|
|
53
69
|
return (
|
|
54
70
|
<span
|
|
55
|
-
className="inline-flex h-6 shrink-0 items-center gap-1.5 rounded-md bg-amber-300/[0.10] px-1.5 font-mono text-[10px] uppercase
|
|
71
|
+
className="inline-flex h-6 shrink-0 items-center gap-1.5 rounded-md bg-amber-300/[0.10] px-1.5 font-mono text-[10px] uppercase text-amber-100/90 ring-1 ring-amber-200/10"
|
|
56
72
|
title="This session has a turn that has not finished yet"
|
|
57
73
|
>
|
|
58
74
|
<Loader2 className="size-3 animate-spin" />
|
|
@@ -145,11 +161,8 @@ export function ConversationHeader({
|
|
|
145
161
|
const clientDisplayLabel = detectedClientLabel ?? userAgent ?? null;
|
|
146
162
|
const clientTooltip =
|
|
147
163
|
detectedClientLabel !== null && hasUserAgent ? `${detectedClientLabel} - ${userAgent}` : null;
|
|
148
|
-
const isFallbackConversation =
|
|
149
|
-
const
|
|
150
|
-
conversationId.length > 24
|
|
151
|
-
? conversationId.slice(0, 12) + "..." + conversationId.slice(-12)
|
|
152
|
-
: conversationId;
|
|
164
|
+
const isFallbackConversation = isSyntheticSessionId(conversationId);
|
|
165
|
+
const sessionLabel = compactSessionLabel(conversationId);
|
|
153
166
|
|
|
154
167
|
const timeLabel = formatTimestampRange(startTime, endTime, timeDisplayFormat);
|
|
155
168
|
const callsLabel = `${totalCalls} call${totalCalls !== 1 ? "s" : ""}`;
|
|
@@ -161,8 +174,8 @@ export function ConversationHeader({
|
|
|
161
174
|
data-nav-id={`conv-${conversationId}`}
|
|
162
175
|
data-nav-action={expanded ? "collapse" : "expand"}
|
|
163
176
|
className={cn(
|
|
164
|
-
"bg-
|
|
165
|
-
"hover:bg-
|
|
177
|
+
"bg-black/[0.10] relative flex cursor-pointer items-start gap-2.5 overflow-hidden rounded-[8px] px-3 py-2.5 transition-colors",
|
|
178
|
+
"hover:bg-black/[0.16]",
|
|
166
179
|
"select-none",
|
|
167
180
|
"sticky top-0 z-10 mb-2",
|
|
168
181
|
"focus-visible:ring-1 focus-visible:ring-cyan-300/35 focus-visible:outline-none",
|
|
@@ -185,17 +198,20 @@ export function ConversationHeader({
|
|
|
185
198
|
|
|
186
199
|
<span className="min-w-0 flex-1">
|
|
187
200
|
<span className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
|
|
201
|
+
<span className="shrink-0 rounded bg-white/[0.04] px-1.5 py-0.5 font-mono text-[9px] uppercase text-muted-foreground/70">
|
|
202
|
+
Session
|
|
203
|
+
</span>
|
|
188
204
|
{isFallbackConversation && showProcessMetadata ? (
|
|
189
205
|
clientPid !== null && clientPid !== undefined ? (
|
|
190
206
|
<span
|
|
191
|
-
className="min-w-0 max-w-full truncate font-mono text-xs font-semibold text-
|
|
207
|
+
className="min-w-0 max-w-full truncate font-mono text-xs font-semibold text-slate-100"
|
|
192
208
|
title={`PID ${clientPid}`}
|
|
193
209
|
>
|
|
194
210
|
PID {clientPid}
|
|
195
211
|
</span>
|
|
196
212
|
) : (
|
|
197
213
|
<span
|
|
198
|
-
className="min-w-0 max-w-full truncate font-mono text-xs font-semibold text-
|
|
214
|
+
className="min-w-0 max-w-full truncate font-mono text-xs font-semibold text-slate-100"
|
|
199
215
|
title={conversationId}
|
|
200
216
|
>
|
|
201
217
|
{conversationId}
|
|
@@ -203,10 +219,10 @@ export function ConversationHeader({
|
|
|
203
219
|
)
|
|
204
220
|
) : (
|
|
205
221
|
<span
|
|
206
|
-
className="min-w-0 max-w-full truncate font-mono text-xs font-semibold text-
|
|
222
|
+
className="min-w-0 max-w-full truncate font-mono text-xs font-semibold text-slate-100"
|
|
207
223
|
title={conversationId}
|
|
208
224
|
>
|
|
209
|
-
{
|
|
225
|
+
{sessionLabel}
|
|
210
226
|
</span>
|
|
211
227
|
)}
|
|
212
228
|
|
|
@@ -240,7 +256,7 @@ export function ConversationHeader({
|
|
|
240
256
|
className="bg-white/[0.045] inline-flex min-h-6 shrink-0 flex-wrap items-center gap-1 rounded-md px-1.5 py-0.5 text-xs text-muted-foreground"
|
|
241
257
|
title={clientTooltip ?? clientDisplayLabel}
|
|
242
258
|
>
|
|
243
|
-
<ClientLogo client={clientApp} className="size-4 shrink-0" />
|
|
259
|
+
<ClientLogo client={clientApp} className="size-4 shrink-0" decorative={true} />
|
|
244
260
|
<span className="font-mono leading-snug tabular-nums">{clientDisplayLabel}</span>
|
|
245
261
|
</span>
|
|
246
262
|
)}
|
|
@@ -5,9 +5,6 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/
|
|
|
5
5
|
|
|
6
6
|
const MAX_VISIBLE_LOGOS = 3;
|
|
7
7
|
|
|
8
|
-
export const ROW_CHEVRON_SLOT_CLASS =
|
|
9
|
-
"flex h-8 w-5 shrink-0 items-center justify-center text-muted-foreground";
|
|
10
|
-
|
|
11
8
|
export type ProviderLogoStackItem = {
|
|
12
9
|
provider: Provider;
|
|
13
10
|
tooltip: string;
|
|
@@ -228,8 +228,3 @@ export const SseEventSchema = z.discriminatedUnion("type", [
|
|
|
228
228
|
|
|
229
229
|
export type AnthropicResponse = z.infer<typeof AnthropicResponseSchema>;
|
|
230
230
|
export type ResponseContentBlockType = z.infer<typeof ResponseContentBlock>;
|
|
231
|
-
|
|
232
|
-
// Backward-compatible aliases (originally named Inspector*)
|
|
233
|
-
export const InspectorRequestSchema = AnthropicRequestSchema;
|
|
234
|
-
export const InspectorResponseSchema = AnthropicResponseSchema;
|
|
235
|
-
export type InspectorResponse = AnthropicResponse;
|
package/src/contracts/index.ts
CHANGED
|
@@ -12,7 +12,6 @@ export type {
|
|
|
12
12
|
CapturedLog,
|
|
13
13
|
LogBodyChunk,
|
|
14
14
|
LogBodyPart,
|
|
15
|
-
RequestFormat,
|
|
16
15
|
StreamingChunk,
|
|
17
16
|
TokenUsage,
|
|
18
17
|
} from "./log";
|
|
@@ -46,9 +45,6 @@ export {
|
|
|
46
45
|
} from "./openai";
|
|
47
46
|
export type {
|
|
48
47
|
OpenAIResponse,
|
|
49
|
-
OpenAIResponsesRequest,
|
|
50
48
|
OpenAIResponsesResponse,
|
|
51
|
-
OpenAIResponsesSSEvent,
|
|
52
|
-
OpenAIResponsesOutputItem,
|
|
53
49
|
OpenAIToolCall,
|
|
54
50
|
} from "./openai";
|
package/src/contracts/log.ts
CHANGED
package/src/contracts/openai.ts
CHANGED
|
@@ -242,10 +242,7 @@ export const OpenAIResponsesSSEventSchema = z
|
|
|
242
242
|
.passthrough();
|
|
243
243
|
|
|
244
244
|
export type OpenAIResponse = z.infer<typeof OpenAIResponseSchema>;
|
|
245
|
-
export type OpenAIResponsesRequest = z.infer<typeof OpenAIResponsesRequestSchema>;
|
|
246
245
|
export type OpenAIResponsesResponse = z.infer<typeof OpenAIResponsesResponseSchema>;
|
|
247
|
-
export type OpenAIResponsesSSEvent = z.infer<typeof OpenAIResponsesSSEventSchema>;
|
|
248
|
-
export type OpenAIResponsesOutputItem = z.infer<typeof OpenAIResponsesOutputItemSchema>;
|
|
249
246
|
|
|
250
247
|
export function parseOpenAIResponse(rawBody: string): OpenAIResponse | null {
|
|
251
248
|
try {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { safeFetch } from "../lib/safeFetch";
|
|
2
3
|
import type { KnowledgeCandidate, KnowledgeSearchResponse, OpenClawMemoryPayload } from "./types";
|
|
3
4
|
import { KnowledgeSearchResponseSchema, KnowledgeSearchResultSchema } from "./types";
|
|
4
5
|
import { isOpenClawGatewayConfigured, searchOpenClawGateway } from "./openclawGatewayClient";
|
|
@@ -81,11 +82,17 @@ export async function promoteToOpenClaw(candidate: KnowledgeCandidate): Promise<
|
|
|
81
82
|
};
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
const fetchResult = await safeFetch(
|
|
86
|
+
buildMemoryUrl(endpoint),
|
|
87
|
+
{
|
|
88
|
+
method: "POST",
|
|
89
|
+
headers: authHeaders(),
|
|
90
|
+
body: JSON.stringify(buildOpenClawPayload(candidate)),
|
|
91
|
+
},
|
|
92
|
+
{ bypass: true },
|
|
93
|
+
);
|
|
94
|
+
if (!fetchResult.ok) return { success: false, error: fetchResult.reason };
|
|
95
|
+
const response = fetchResult.response;
|
|
89
96
|
|
|
90
97
|
if (!response.ok) {
|
|
91
98
|
return { success: false, error: `OpenClaw memory write failed with ${response.status}` };
|
|
@@ -120,10 +127,16 @@ export async function searchOpenClaw(
|
|
|
120
127
|
};
|
|
121
128
|
}
|
|
122
129
|
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
130
|
+
const fetchResult = await safeFetch(
|
|
131
|
+
buildSearchUrl(endpoint, query, project),
|
|
132
|
+
{
|
|
133
|
+
method: "GET",
|
|
134
|
+
headers: authHeaders(),
|
|
135
|
+
},
|
|
136
|
+
{ bypass: true },
|
|
137
|
+
);
|
|
138
|
+
if (!fetchResult.ok) return { results: [], warning: fetchResult.reason };
|
|
139
|
+
const response = fetchResult.response;
|
|
127
140
|
if (!response.ok) {
|
|
128
141
|
return { results: [], warning: `OpenClaw search failed with ${response.status}` };
|
|
129
142
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { safeFetch } from "../lib/safeFetch";
|
|
2
3
|
import type { KnowledgeSearchResponse, KnowledgeSearchResult } from "./types";
|
|
3
4
|
|
|
4
5
|
const OpenClawGatewayErrorSchema = z
|
|
@@ -192,19 +193,25 @@ export async function searchOpenClawGateway(
|
|
|
192
193
|
};
|
|
193
194
|
}
|
|
194
195
|
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
196
|
+
const fetchResult = await safeFetch(
|
|
197
|
+
buildToolInvokeUrl(gatewayUrl),
|
|
198
|
+
{
|
|
199
|
+
method: "POST",
|
|
200
|
+
headers: gatewayHeaders(),
|
|
201
|
+
body: JSON.stringify({
|
|
202
|
+
tool: "memory_search",
|
|
203
|
+
args: {
|
|
204
|
+
query: buildSearchQuery(query, project),
|
|
205
|
+
maxResults: 10,
|
|
206
|
+
corpus: "all",
|
|
207
|
+
},
|
|
208
|
+
sessionKey: getOpenClawSessionKey(),
|
|
209
|
+
}),
|
|
210
|
+
},
|
|
211
|
+
{ bypass: true },
|
|
212
|
+
);
|
|
213
|
+
if (!fetchResult.ok) return { results: [], warning: fetchResult.reason };
|
|
214
|
+
const response = fetchResult.response;
|
|
208
215
|
|
|
209
216
|
const raw: unknown = await response.json().catch(() => null);
|
|
210
217
|
if (!response.ok) {
|
package/src/lib/alertContract.ts
CHANGED
|
@@ -68,12 +68,9 @@ export const AlertListResponseSchema = z.object({
|
|
|
68
68
|
});
|
|
69
69
|
|
|
70
70
|
export type AlertSeverity = z.infer<typeof AlertSeveritySchema>;
|
|
71
|
-
export type AlertStatus = z.infer<typeof AlertStatusSchema>;
|
|
72
71
|
export type AlertSource = z.infer<typeof AlertSourceSchema>;
|
|
73
72
|
export type AlertCategory = z.infer<typeof AlertCategorySchema>;
|
|
74
|
-
export type AlertEvidenceKind = z.infer<typeof AlertEvidenceKindSchema>;
|
|
75
73
|
export type AlertEvidenceLink = z.infer<typeof AlertEvidenceLinkSchema>;
|
|
76
74
|
export type Alert = z.infer<typeof AlertSchema>;
|
|
77
|
-
export type AlertCategoryCount = z.infer<typeof AlertCategoryCountSchema>;
|
|
78
75
|
export type AlertSummary = z.infer<typeof AlertSummarySchema>;
|
|
79
76
|
export type AlertListResponse = z.infer<typeof AlertListResponseSchema>;
|
|
@@ -91,9 +91,6 @@ export type EcosystemPackageState = z.infer<typeof EcosystemPackageStateSchema>;
|
|
|
91
91
|
export type EcosystemPackage = z.infer<typeof EcosystemPackageSchema>;
|
|
92
92
|
export type EcosystemRunnerPreset = z.infer<typeof EcosystemRunnerPresetSchema>;
|
|
93
93
|
export type EcosystemRecipe = z.infer<typeof EcosystemRecipeSchema>;
|
|
94
|
-
export type EcosystemPackagesResponse = z.infer<typeof EcosystemPackagesResponseSchema>;
|
|
95
94
|
export type EcosystemTaskAction = z.infer<typeof EcosystemTaskActionSchema>;
|
|
96
95
|
export type EcosystemTaskStatus = z.infer<typeof EcosystemTaskStatusSchema>;
|
|
97
96
|
export type EcosystemTask = z.infer<typeof EcosystemTaskSchema>;
|
|
98
|
-
export type EcosystemTaskResponse = z.infer<typeof EcosystemTaskResponseSchema>;
|
|
99
|
-
export type EcosystemTasksResponse = z.infer<typeof EcosystemTasksResponseSchema>;
|
package/src/lib/export-logs.ts
CHANGED
|
@@ -2,6 +2,7 @@ import JSZip from "jszip";
|
|
|
2
2
|
import type { CapturedLog } from "../contracts";
|
|
3
3
|
import { fetchWithTimeout } from "./apiClient";
|
|
4
4
|
import packageJson from "../../package.json";
|
|
5
|
+
import { isRecord } from "./unknown";
|
|
5
6
|
|
|
6
7
|
export type ExportMode = "redacted" | "raw";
|
|
7
8
|
export type ExportLogsResult =
|
|
@@ -97,10 +98,6 @@ async function fetchStreamingChunks(
|
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
101
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
101
|
function redactValue(value: unknown): unknown {
|
|
105
102
|
if (Array.isArray(value)) return value.map((item) => redactValue(item));
|
|
106
103
|
if (!isRecord(value)) return value;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { readProperty } from "./unknown";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reads a UTF-8 JSON file from disk and validates it against a Zod schema in
|
|
7
|
+
* one call. Used by config/secret/evidence files that store small records
|
|
8
|
+
* atomically rewritten on update — *not* for streaming log files (those go
|
|
9
|
+
* through line-by-line parsing in store.ts).
|
|
10
|
+
*
|
|
11
|
+
* On any failure returns `{ ok: false, reason }` rather than throwing. The
|
|
12
|
+
* caller decides between returning a fallback, logging, or surfacing an
|
|
13
|
+
* error response. To log on failure, wrap the call site with the project's
|
|
14
|
+
* `logger.warn` (the helper stays a pure parser to avoid a `lib → proxy`
|
|
15
|
+
* dependency direction).
|
|
16
|
+
*/
|
|
17
|
+
export type ReadJsonFileFailureReason = "missing" | "io-error" | "parse-error" | "schema-mismatch";
|
|
18
|
+
|
|
19
|
+
export type ReadJsonFileResult<T> =
|
|
20
|
+
| { ok: true; data: T }
|
|
21
|
+
| { ok: false; reason: ReadJsonFileFailureReason };
|
|
22
|
+
|
|
23
|
+
export function readJsonFile<T>(path: string, schema: z.ZodType<T>): ReadJsonFileResult<T> {
|
|
24
|
+
let raw: string;
|
|
25
|
+
try {
|
|
26
|
+
raw = readFileSync(path, "utf8");
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (isEnoent(error)) return { ok: false, reason: "missing" };
|
|
29
|
+
return { ok: false, reason: "io-error" };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let parsed: unknown;
|
|
33
|
+
try {
|
|
34
|
+
parsed = JSON.parse(raw);
|
|
35
|
+
} catch {
|
|
36
|
+
return { ok: false, reason: "parse-error" };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const result = schema.safeParse(parsed);
|
|
40
|
+
if (!result.success) return { ok: false, reason: "schema-mismatch" };
|
|
41
|
+
return { ok: true, data: result.data };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isEnoent(error: unknown): boolean {
|
|
45
|
+
return readProperty(error, "code") === "ENOENT";
|
|
46
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { lookup } from "node:dns/promises";
|
|
2
|
+
import { isIP } from "node:net";
|
|
3
|
+
import { evaluateUpstreamUrl, isInternalHost, type SsrfGuardOptions } from "./ssrfGuard";
|
|
4
|
+
|
|
5
|
+
export type ResolvedAddress = {
|
|
6
|
+
address: string;
|
|
7
|
+
family: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type HostResolver = (hostname: string) => Promise<readonly ResolvedAddress[]>;
|
|
11
|
+
|
|
12
|
+
export type SafeFetchOptions = SsrfGuardOptions & {
|
|
13
|
+
resolver?: HostResolver;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type SafeFetchResult =
|
|
17
|
+
| { ok: true; response: Response }
|
|
18
|
+
| { ok: false; kind: "network-policy" | "redirect"; reason: string };
|
|
19
|
+
|
|
20
|
+
async function defaultResolver(hostname: string): Promise<readonly ResolvedAddress[]> {
|
|
21
|
+
return lookup(hostname, { all: true, verbatim: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isLoopbackAddress(hostname: string): boolean {
|
|
25
|
+
const lower = hostname.toLowerCase();
|
|
26
|
+
return (
|
|
27
|
+
lower === "::1" ||
|
|
28
|
+
lower.startsWith("::ffff:127.") ||
|
|
29
|
+
lower.startsWith("::ffff:7f00:") ||
|
|
30
|
+
/^127\./.test(lower)
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function evaluateResolvedUpstreamUrl(
|
|
35
|
+
rawUrl: string,
|
|
36
|
+
options: SafeFetchOptions = {},
|
|
37
|
+
): Promise<{ allow: true } | { allow: false; reason: string }> {
|
|
38
|
+
const initial = evaluateUpstreamUrl(rawUrl, options);
|
|
39
|
+
if (!initial.allow || options.bypass === true) return initial;
|
|
40
|
+
|
|
41
|
+
const parsed = new URL(rawUrl);
|
|
42
|
+
const hostname = parsed.hostname.replace(/^\[|]$/g, "");
|
|
43
|
+
if (isIP(hostname) !== 0 || hostname.toLowerCase() === "localhost") return initial;
|
|
44
|
+
|
|
45
|
+
let addresses: readonly ResolvedAddress[];
|
|
46
|
+
try {
|
|
47
|
+
addresses = await (options.resolver ?? defaultResolver)(hostname);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
return {
|
|
50
|
+
allow: false,
|
|
51
|
+
reason: `Could not safely resolve upstream host "${hostname}": ${String(error)}`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (addresses.length === 0) {
|
|
56
|
+
return { allow: false, reason: `Upstream host "${hostname}" resolved to no addresses` };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const resolved of addresses) {
|
|
60
|
+
if (!isInternalHost(resolved.address)) continue;
|
|
61
|
+
if (options.allowLoopback === true && isLoopbackAddress(resolved.address)) continue;
|
|
62
|
+
return {
|
|
63
|
+
allow: false,
|
|
64
|
+
reason: `Refusing upstream host "${hostname}" because it resolves to internal address "${resolved.address}"`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { allow: true };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isRedirectStatus(status: number): boolean {
|
|
72
|
+
return status >= 300 && status < 400;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function safeFetch(
|
|
76
|
+
rawUrl: string,
|
|
77
|
+
init: RequestInit = {},
|
|
78
|
+
options: SafeFetchOptions = {},
|
|
79
|
+
): Promise<SafeFetchResult> {
|
|
80
|
+
const decision = await evaluateResolvedUpstreamUrl(rawUrl, options);
|
|
81
|
+
if (!decision.allow) {
|
|
82
|
+
return { ok: false, kind: "network-policy", reason: decision.reason };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const response = await fetch(rawUrl, { ...init, redirect: "manual" });
|
|
86
|
+
if (!isRedirectStatus(response.status)) return { ok: true, response };
|
|
87
|
+
|
|
88
|
+
if (response.body !== null) {
|
|
89
|
+
await response.body.cancel().catch(() => undefined);
|
|
90
|
+
}
|
|
91
|
+
const location = response.headers.get("location");
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
kind: "redirect",
|
|
95
|
+
reason:
|
|
96
|
+
location === null
|
|
97
|
+
? `Upstream returned blocked redirect HTTP ${String(response.status)}`
|
|
98
|
+
: `Upstream redirect to "${location}" is blocked; configure the final API URL directly`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSRF guard for upstream provider URLs.
|
|
3
|
+
*
|
|
4
|
+
* The proxy fetches arbitrary provider-configured base URLs (Anthropic, OpenAI,
|
|
5
|
+
* third-party compatible endpoints). Without protection, a malicious provider
|
|
6
|
+
* config could point the proxy at cloud metadata endpoints
|
|
7
|
+
* (169.254.169.254 / fd00:ec2::254), loopback services, or private RFC1918
|
|
8
|
+
* ranges — and the response body is captured to a log file the attacker can
|
|
9
|
+
* read back. We block those targets by default; an env var opt-in allows
|
|
10
|
+
* intentional localhost development (e.g. Ollama on 127.0.0.1).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type SsrfDecision = { allow: true } | { allow: false; reason: string };
|
|
14
|
+
|
|
15
|
+
const LOOPBACK_HOSTNAMES = new Set([
|
|
16
|
+
"localhost",
|
|
17
|
+
"ip6-localhost",
|
|
18
|
+
"ip6-loopback",
|
|
19
|
+
"[::1]",
|
|
20
|
+
"[::ffff:127.0.0.1]",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Returns true when the host is a loopback address, link-local address, or
|
|
25
|
+
* private RFC1918 / RFC4193 / unique-local range. Both IPv4 dotted-quad and
|
|
26
|
+
* IPv6 forms are recognized; bracketed forms (`[::1]`) are normalized.
|
|
27
|
+
*/
|
|
28
|
+
function isLoopbackHost(hostname: string): boolean {
|
|
29
|
+
const lower = hostname.toLowerCase().replace(/^\[|]$/g, "");
|
|
30
|
+
if (LOOPBACK_HOSTNAMES.has(lower)) return true;
|
|
31
|
+
if (lower === "::1") return true;
|
|
32
|
+
if (lower.startsWith("::ffff:127.")) return true;
|
|
33
|
+
if (lower.startsWith("::ffff:7f00:")) return true;
|
|
34
|
+
return /^127\./.test(lower);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function mappedIpv4Address(hostname: string): string | null {
|
|
38
|
+
const lower = hostname.toLowerCase();
|
|
39
|
+
if (!lower.startsWith("::ffff:")) return null;
|
|
40
|
+
const suffix = lower.slice("::ffff:".length);
|
|
41
|
+
if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(suffix)) return suffix;
|
|
42
|
+
const match = /^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(suffix);
|
|
43
|
+
if (match === null) return null;
|
|
44
|
+
const highText = match[1];
|
|
45
|
+
const lowText = match[2];
|
|
46
|
+
if (highText === undefined || lowText === undefined) return null;
|
|
47
|
+
const high = Number.parseInt(highText, 16);
|
|
48
|
+
const low = Number.parseInt(lowText, 16);
|
|
49
|
+
return `${String(high >> 8)}.${String(high & 0xff)}.${String(low >> 8)}.${String(low & 0xff)}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function isInternalHost(hostname: string): boolean {
|
|
53
|
+
if (LOOPBACK_HOSTNAMES.has(hostname.toLowerCase())) return true;
|
|
54
|
+
|
|
55
|
+
const mappedIpv4 = mappedIpv4Address(hostname);
|
|
56
|
+
if (mappedIpv4 !== null) return isInternalHost(mappedIpv4);
|
|
57
|
+
|
|
58
|
+
const ipv4Match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
59
|
+
if (ipv4Match !== null) {
|
|
60
|
+
const [, a, b] = ipv4Match;
|
|
61
|
+
if (a === undefined || b === undefined) return false;
|
|
62
|
+
const first = Number.parseInt(a, 10);
|
|
63
|
+
const second = Number.parseInt(b, 10);
|
|
64
|
+
if (first === 127) return true; // 127.0.0.0/8 loopback
|
|
65
|
+
if (first === 10) return true; // 10.0.0.0/8 private
|
|
66
|
+
if (first === 172 && second >= 16 && second <= 31) return true; // 172.16.0.0/12
|
|
67
|
+
if (first === 192 && second === 168) return true; // 192.168.0.0/16
|
|
68
|
+
if (first === 169 && second === 254) return true; // 169.254.0.0/16 link-local
|
|
69
|
+
if (first === 0) return true; // 0.0.0.0/8
|
|
70
|
+
if (first === 100 && second >= 64 && second <= 127) return true; // 100.64.0.0/10 CGNAT
|
|
71
|
+
if (first >= 224) return true; // multicast and reserved
|
|
72
|
+
if (first === 198 && (second === 18 || second === 19)) return true; // benchmark range
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// IPv6 — bracketless form already stripped by URL parser; check for loopback
|
|
77
|
+
// and link-local prefixes.
|
|
78
|
+
const lower = hostname.toLowerCase();
|
|
79
|
+
if (lower === "::") return true;
|
|
80
|
+
if (lower === "::1") return true;
|
|
81
|
+
if (/^fe[89ab]/.test(lower)) return true; // fe80::/10 link-local
|
|
82
|
+
if (lower.startsWith("fc") || lower.startsWith("fd")) return true; // ULA (fd00::/8)
|
|
83
|
+
if (lower.startsWith("ff")) return true; // multicast
|
|
84
|
+
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type SsrfGuardOptions = {
|
|
89
|
+
/** Bypass the guard entirely (used by trusted test code). */
|
|
90
|
+
bypass?: boolean;
|
|
91
|
+
/** When true, allow loopback hosts (127.0.0.1, ::1, localhost). Default false. */
|
|
92
|
+
allowLoopback?: boolean;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Inspects a provider base URL and decides whether the proxy is allowed to
|
|
97
|
+
* forward requests to it. Returns `{ allow: true }` for public hosts and
|
|
98
|
+
* `{ allow: false, reason }` for internal/private addresses.
|
|
99
|
+
*/
|
|
100
|
+
export function evaluateUpstreamUrl(rawUrl: string, options: SsrfGuardOptions = {}): SsrfDecision {
|
|
101
|
+
if (options.bypass === true) return { allow: true };
|
|
102
|
+
|
|
103
|
+
let parsed: URL;
|
|
104
|
+
try {
|
|
105
|
+
parsed = new URL(rawUrl);
|
|
106
|
+
} catch {
|
|
107
|
+
return { allow: false, reason: "Not a valid URL" };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
111
|
+
return { allow: false, reason: `Unsupported scheme "${parsed.protocol}"` };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (options.allowLoopback === true && parsed.hostname === "localhost") {
|
|
115
|
+
return { allow: true };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Bun's URL parser keeps brackets on IPv6 hostnames (`[::1]`); strip them so
|
|
119
|
+
// prefix checks below work against the canonical form.
|
|
120
|
+
const hostname = parsed.hostname.replace(/^\[|]$/g, "");
|
|
121
|
+
|
|
122
|
+
if (isInternalHost(hostname)) {
|
|
123
|
+
if (options.allowLoopback === true) {
|
|
124
|
+
if (isLoopbackHost(hostname)) return { allow: true };
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
allow: false,
|
|
128
|
+
reason: `Refusing to proxy to internal address "${hostname}"`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { allow: true };
|
|
133
|
+
}
|
package/src/lib/stopReason.ts
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import type { CapturedLog } from "../contracts";
|
|
2
|
+
import { isRecord } from "./unknown";
|
|
2
3
|
|
|
3
4
|
export type StopReason = "end_turn" | "tool_use" | "stop" | "length" | null;
|
|
4
5
|
|
|
5
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
6
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
6
|
function normalizeAnthropicStopReason(value: string): StopReason {
|
|
10
7
|
switch (value) {
|
|
11
8
|
case "end_turn":
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for traversing values of type `unknown` without resorting to
|
|
3
|
+
* type assertions. The project's ESLint rules forbid `as` casts and non-null
|
|
4
|
+
* assertions, so every safe property read goes through these helpers.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** True if `value` is a non-array, non-null object. */
|
|
8
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
9
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Safely read an own property of `value`, returning `undefined` for any
|
|
14
|
+
* non-object input (including arrays, null, primitives). Uses
|
|
15
|
+
* `getOwnPropertyDescriptor` so that `__proto__` keys cannot be reached.
|
|
16
|
+
*/
|
|
17
|
+
export function readProperty(value: unknown, name: string): unknown {
|
|
18
|
+
if (!isRecord(value)) return undefined;
|
|
19
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, name);
|
|
20
|
+
return descriptor === undefined ? undefined : descriptor.value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Byte length of a UTF-8 string, or `null` if the string is null. */
|
|
24
|
+
export function textByteLength(value: string | null): number | null {
|
|
25
|
+
if (value === null) return null;
|
|
26
|
+
return new TextEncoder().encode(value).byteLength;
|
|
27
|
+
}
|
package/src/lib/useProviders.ts
CHANGED
|
@@ -13,7 +13,7 @@ async function fetcher(url: string): Promise<ProviderConfig[]> {
|
|
|
13
13
|
|
|
14
14
|
export function useProviders() {
|
|
15
15
|
const response: SWRResponse<ProviderConfig[], Error> = useSWR<ProviderConfig[], Error>(
|
|
16
|
-
"/api/providers
|
|
16
|
+
"/api/providers",
|
|
17
17
|
fetcher,
|
|
18
18
|
{
|
|
19
19
|
revalidateOnFocus: false,
|
package/src/mcp/mode.ts
CHANGED
|
@@ -23,13 +23,7 @@ export function isMcpWriteEnabled(): boolean {
|
|
|
23
23
|
if (readonlyFlag === true) return false;
|
|
24
24
|
|
|
25
25
|
const writesFlag = parseFlag(process.env["AGENT_INSPECTOR_MCP_WRITES"]);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return true;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function getMcpWriteMode(): McpWriteMode {
|
|
32
|
-
return isMcpWriteEnabled() ? "enabled" : "readonly";
|
|
26
|
+
return writesFlag === true;
|
|
33
27
|
}
|
|
34
28
|
|
|
35
29
|
export function getMcpModeInfo(): {
|
|
@@ -50,7 +44,7 @@ export function getMcpModeInfo(): {
|
|
|
50
44
|
AGENT_INSPECTOR_MCP_WRITES: process.env["AGENT_INSPECTOR_MCP_WRITES"] ?? null,
|
|
51
45
|
},
|
|
52
46
|
note: writesEnabled
|
|
53
|
-
? "MCP write/action tools are enabled
|
|
54
|
-
: "MCP is
|
|
47
|
+
? "MCP write/action tools are enabled explicitly by AGENT_INSPECTOR_MCP_WRITES=1."
|
|
48
|
+
: "MCP is readonly by default. Read tools/resources still work; set AGENT_INSPECTOR_MCP_WRITES=1 to enable mutations.",
|
|
55
49
|
};
|
|
56
50
|
}
|