@tonyclaw/agent-inspector 2.0.41 → 2.0.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-DCg6S2cl.js → CompareDrawer-CxM1gCfL.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-TtRG-0E7.js +117 -0
  4. package/.output/public/assets/{ReplayDialog-ClVgjSDE.js → ReplayDialog-UseUkucS.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-BhqvLQp9.js → RequestAnatomy-aPxgEJ2L.js} +1 -1
  6. package/.output/public/assets/{ResponseView-BZAJoK6B.js → ResponseView-DA-F4F97.js} +1 -1
  7. package/.output/public/assets/{StreamingChunkSequence-CaORmoKX.js → StreamingChunkSequence-JSQEPeNS.js} +1 -1
  8. package/.output/public/assets/_sessionId-BEXuCWq5.js +1 -0
  9. package/.output/public/assets/index-Bhsa_2xX.js +1 -0
  10. package/.output/public/assets/index-DOWlRJ0W.css +1 -0
  11. package/.output/public/assets/{main-CI3HFEcV.js → main-BpGVJcpB.js} +2 -2
  12. package/.output/server/{_sessionId-DQ0ljHQ8.mjs → _sessionId-DGn-TENM.mjs} +2 -2
  13. package/.output/server/_ssr/{CompareDrawer-nkVa9epn.mjs → CompareDrawer-CFElCSYY.mjs} +2 -2
  14. package/.output/server/_ssr/{ProxyViewerContainer-CXA7iH3H.mjs → ProxyViewerContainer-B7SR9mrD.mjs} +383 -76
  15. package/.output/server/_ssr/{ReplayDialog-CK71-ucq.mjs → ReplayDialog-DfKapj0k.mjs} +3 -3
  16. package/.output/server/_ssr/{RequestAnatomy-DDCUJJ3X.mjs → RequestAnatomy-CUxTCg31.mjs} +2 -2
  17. package/.output/server/_ssr/{ResponseView-C6ZA7V3B.mjs → ResponseView-BXY0w197.mjs} +2 -2
  18. package/.output/server/_ssr/{StreamingChunkSequence-DW5v_o6G.mjs → StreamingChunkSequence-CzMnES9H.mjs} +2 -2
  19. package/.output/server/_ssr/{index-Ckex98yq.mjs → index-CVlT-REW.mjs} +2 -2
  20. package/.output/server/_ssr/index.mjs +2 -2
  21. package/.output/server/_ssr/{router-ChJIVv7-.mjs → router-B2d1LUx6.mjs} +1313 -468
  22. package/.output/server/{_tanstack-start-manifest_v-CJ4__weU.mjs → _tanstack-start-manifest_v-Drpi28BM.mjs} +1 -1
  23. package/.output/server/index.mjs +60 -60
  24. package/README.md +36 -12
  25. package/package.json +1 -1
  26. package/src/components/ProxyViewer.tsx +156 -17
  27. package/src/components/ProxyViewerContainer.tsx +66 -4
  28. package/src/components/groups/GroupsDialog.tsx +11 -10
  29. package/src/components/providers/SettingsDialog.tsx +137 -0
  30. package/src/components/proxy-viewer/LogEntry.tsx +107 -56
  31. package/src/components/ui/dialog.tsx +1 -1
  32. package/src/lib/runContract.ts +3 -0
  33. package/src/mcp/currentContext.ts +65 -0
  34. package/src/mcp/mode.ts +56 -0
  35. package/src/mcp/server.ts +466 -15
  36. package/src/mcp/toolHandlers.ts +372 -0
  37. package/src/proxy/evidenceExporter.ts +1 -0
  38. package/src/proxy/runStore.ts +74 -22
  39. package/.output/public/assets/ProxyViewerContainer-DsU6QETm.js +0 -115
  40. package/.output/public/assets/_sessionId-BHqywvM3.js +0 -1
  41. package/.output/public/assets/index-B_LPYuM0.js +0 -1
  42. package/.output/public/assets/index-BfGJEb-2.css +0 -1
@@ -1,7 +1,8 @@
1
1
  import { AlertTriangle, GitCompareArrows } from "lucide-react";
2
2
  import { Suspense, type JSX } from "react";
3
3
  import { useCallback, useEffect, useMemo, useRef, useState, memo } from "react";
4
- import type { CapturedLog } from "../../contracts";
4
+ import { CapturedLogSchema, type CapturedLog } from "../../contracts";
5
+ import { fetchJson } from "../../lib/apiClient";
5
6
  import type { TimeDisplayFormat } from "../../lib/runtimeConfig";
6
7
  import { stripClaudeCodeBillingHeader } from "../../proxy/claudeCodeStrip";
7
8
  import { Button } from "../ui/button";
@@ -50,6 +51,12 @@ function resolveFocusedTab(tab: LogFocusTab, anatomySegments: AnatomySegment[] |
50
51
  }
51
52
  }
52
53
 
54
+ type BodyHydrationState = "idle" | "loading" | "failed";
55
+
56
+ function shouldHydrateLogBody(log: CapturedLog): boolean {
57
+ return log.bodyContentMode === "compact" || log.bodyContentMode === "truncated";
58
+ }
59
+
53
60
  export type LogEntryProps = {
54
61
  log: CapturedLog;
55
62
  viewMode?: "simple" | "full";
@@ -144,53 +151,56 @@ export const LogEntry = memo(function ({
144
151
  const [requestDiff, setRequestDiff] = useState<boolean>(false);
145
152
  const [activeTab, setActiveTab] = useState("request");
146
153
  const [expandToPath, setExpandToPath] = useState<string | null>(null);
154
+ const [hydratedLog, setHydratedLog] = useState<CapturedLog | null>(null);
155
+ const [bodyHydrationState, setBodyHydrationState] = useState<BodyHydrationState>("idle");
147
156
  const requestJsonRef = useRef<HTMLDivElement | null>(null);
148
- const resolvedFormat = resolveLogFormat(log);
157
+ const displayLog = hydratedLog ?? log;
158
+ const resolvedFormat = resolveLogFormat(displayLog);
149
159
  const adapter = getLogFormatAdapter(resolvedFormat);
150
160
  const requestAnalysis = useMemo(
151
- () => adapter.analyzeRequest(log.rawRequestBody),
152
- [adapter, log.rawRequestBody],
161
+ () => adapter.analyzeRequest(displayLog.rawRequestBody),
162
+ [adapter, displayLog.rawRequestBody],
153
163
  );
154
164
  const responseAnalysis = useMemo(
155
- () => adapter.analyzeResponse(log.responseText),
156
- [adapter, log.responseText],
165
+ () => adapter.analyzeResponse(displayLog.responseText),
166
+ [adapter, displayLog.responseText],
157
167
  );
158
168
  const strippedRequestBody = useMemo(() => {
159
- if (!strip || resolvedFormat !== "anthropic" || log.rawRequestBody === null) {
169
+ if (!strip || resolvedFormat !== "anthropic" || displayLog.rawRequestBody === null) {
160
170
  return null;
161
171
  }
162
- return stripClaudeCodeBillingHeader(log.rawRequestBody).body;
163
- }, [log.rawRequestBody, resolvedFormat, strip]);
164
- const displayedRequestBody = strippedRequestBody ?? log.rawRequestBody;
172
+ return stripClaudeCodeBillingHeader(displayLog.rawRequestBody).body;
173
+ }, [displayLog.rawRequestBody, resolvedFormat, strip]);
174
+ const displayedRequestBody = strippedRequestBody ?? displayLog.rawRequestBody;
165
175
  const requestExpansion = useJsonBulkExpansion(displayedRequestBody);
166
- const rawRequestExpansion = useJsonBulkExpansion(log.rawRequestBody);
167
- const responseExpansion = useJsonBulkExpansion(log.responseText);
176
+ const rawRequestExpansion = useJsonBulkExpansion(displayLog.rawRequestBody);
177
+ const responseExpansion = useJsonBulkExpansion(displayLog.responseText);
168
178
 
169
179
  // Headers are rendered as a flat list, so we copy them as pretty-printed JSON.
170
180
  // Only build the string when there's at least one entry — empty headers would
171
181
  // otherwise copy "{}", which is misleading.
172
182
  const headersText = useMemo(
173
183
  () =>
174
- log.headers && Object.keys(log.headers).length > 0
175
- ? JSON.stringify(log.headers, null, 2)
184
+ displayLog.headers && Object.keys(displayLog.headers).length > 0
185
+ ? JSON.stringify(displayLog.headers, null, 2)
176
186
  : null,
177
- [log.headers],
187
+ [displayLog.headers],
178
188
  );
179
189
  const rawHeadersText = useMemo(
180
190
  () =>
181
- log.rawHeaders && Object.keys(log.rawHeaders).length > 0
182
- ? JSON.stringify(log.rawHeaders, null, 2)
191
+ displayLog.rawHeaders && Object.keys(displayLog.rawHeaders).length > 0
192
+ ? JSON.stringify(displayLog.rawHeaders, null, 2)
183
193
  : null,
184
- [log.rawHeaders],
194
+ [displayLog.rawHeaders],
185
195
  );
186
196
 
187
197
  // One copy-feedback hook per JSON-bearing tab, so each tab can surface its
188
198
  // own Copy button in the header with independent "Copied!" feedback.
189
199
  const requestCopy = useCopyFeedback(displayedRequestBody);
190
- const rawRequestCopy = useCopyFeedback(log.rawRequestBody);
200
+ const rawRequestCopy = useCopyFeedback(displayLog.rawRequestBody);
191
201
  const headersCopy = useCopyFeedback(headersText);
192
202
  const rawHeadersCopy = useCopyFeedback(rawHeadersText);
193
- const responseCopy = useCopyFeedback(log.responseText);
203
+ const responseCopy = useCopyFeedback(displayLog.responseText);
194
204
 
195
205
  // Per-tab action bundles consumed by the header. The header renders the
196
206
  // entry whose key matches `activeTab`. Tabs without an entry (Context,
@@ -214,7 +224,7 @@ export const LogEntry = memo(function ({
214
224
  resolvedFormat,
215
225
  viewMode,
216
226
  strip,
217
- log.rawRequestBody !== null,
227
+ displayLog.rawRequestBody !== null,
218
228
  )
219
229
  ? { active: requestDiff, onToggle: () => setRequestDiff(!requestDiff) }
220
230
  : undefined,
@@ -222,12 +232,12 @@ export const LogEntry = memo(function ({
222
232
  onCompareWithPrevious === undefined
223
233
  ? undefined
224
234
  : () => {
225
- onCompareWithPrevious(log);
235
+ onCompareWithPrevious(displayLog);
226
236
  },
227
237
  },
228
238
  "raw-request": {
229
239
  copyLabel: "Copy raw request",
230
- copyText: log.rawRequestBody,
240
+ copyText: displayLog.rawRequestBody,
231
241
  copyCopied: rawRequestCopy.copied,
232
242
  onCopy: rawRequestCopy.copy,
233
243
  expansion: {
@@ -253,7 +263,7 @@ export const LogEntry = memo(function ({
253
263
  },
254
264
  raw: {
255
265
  copyLabel: "Copy response",
256
- copyText: log.responseText,
266
+ copyText: displayLog.responseText,
257
267
  copyCopied: responseCopy.copied,
258
268
  onCopy: responseCopy.copy,
259
269
  expansion: {
@@ -268,14 +278,15 @@ export const LogEntry = memo(function ({
268
278
  requestCopy,
269
279
  requestExpansion,
270
280
  requestDiff,
271
- log.rawRequestBody,
281
+ displayLog,
282
+ displayLog.rawRequestBody,
272
283
  rawRequestCopy,
273
284
  rawRequestExpansion,
274
285
  headersText,
275
286
  headersCopy,
276
287
  rawHeadersText,
277
288
  rawHeadersCopy,
278
- log.responseText,
289
+ displayLog.responseText,
279
290
  responseCopy,
280
291
  responseExpansion,
281
292
  resolvedFormat,
@@ -295,7 +306,7 @@ export const LogEntry = memo(function ({
295
306
  () => parseRequestTools(requestExpansion.parsedData),
296
307
  [requestExpansion.parsedData],
297
308
  );
298
- const warnings = log.warnings ?? [];
309
+ const warnings = displayLog.warnings ?? [];
299
310
  const anatomyPaths = useMemo(() => {
300
311
  if (anatomySegments === null) return undefined;
301
312
  return new Set(anatomySegments.map((s) => s.path));
@@ -316,6 +327,34 @@ export const LogEntry = memo(function ({
316
327
  setExpanded(nextExpanded);
317
328
  }, [anatomySegments, expanded]);
318
329
 
330
+ useEffect(() => {
331
+ setHydratedLog(null);
332
+ setBodyHydrationState("idle");
333
+ }, [log.id]);
334
+
335
+ useEffect(() => {
336
+ if (!expanded) return;
337
+ if (!shouldHydrateLogBody(log)) return;
338
+ if (hydratedLog !== null || bodyHydrationState !== "idle") return;
339
+
340
+ let cancelled = false;
341
+ setBodyHydrationState("loading");
342
+ void fetchJson(`/api/logs/${String(log.id)}`, CapturedLogSchema)
343
+ .then((fullLog) => {
344
+ if (cancelled) return;
345
+ setHydratedLog(fullLog);
346
+ setBodyHydrationState("idle");
347
+ })
348
+ .catch(() => {
349
+ if (cancelled) return;
350
+ setBodyHydrationState("failed");
351
+ });
352
+
353
+ return () => {
354
+ cancelled = true;
355
+ };
356
+ }, [bodyHydrationState, expanded, hydratedLog, log.bodyContentMode, log.id]);
357
+
319
358
  useEffect(() => {
320
359
  const handleLogFocusRequest = (event: Event): void => {
321
360
  const request = readLogFocusRequest(event);
@@ -335,7 +374,7 @@ export const LogEntry = memo(function ({
335
374
  <TooltipProvider>
336
375
  <div className="border border-border rounded-lg mb-1 overflow-hidden">
337
376
  <LogEntryHeader
338
- log={log}
377
+ log={displayLog}
339
378
  messageCount={requestAnalysis.messageCount}
340
379
  toolCount={requestAnalysis.toolCount}
341
380
  expanded={expanded}
@@ -373,7 +412,7 @@ export const LogEntry = memo(function ({
373
412
  <TabsContent value="raw-request">
374
413
  {activeTab === "raw-request" && (
375
414
  <div className="px-4 pt-1 pb-3">
376
- {log.rawRequestBody === null ? (
415
+ {displayLog.rawRequestBody === null ? (
377
416
  <p className="text-xs text-muted-foreground italic">No request body</p>
378
417
  ) : rawRequestExpansion.parsedData !== null ? (
379
418
  <Suspense fallback={<TabFallback />}>
@@ -385,7 +424,7 @@ export const LogEntry = memo(function ({
385
424
  </Suspense>
386
425
  ) : (
387
426
  <pre className="font-mono text-xs whitespace-pre-wrap break-words">
388
- {log.rawRequestBody}
427
+ {displayLog.rawRequestBody}
389
428
  </pre>
390
429
  )}
391
430
  </div>
@@ -396,6 +435,17 @@ export const LogEntry = memo(function ({
396
435
  <TabsContent value="request">
397
436
  {activeTab === "request" && (
398
437
  <div className="px-4 pt-1 pb-3">
438
+ {bodyHydrationState === "loading" && (
439
+ <div className="mb-3 rounded border border-border bg-muted/30 p-3 text-xs text-muted-foreground">
440
+ Loading full request and response bodies...
441
+ </div>
442
+ )}
443
+ {bodyHydrationState === "failed" && (
444
+ <div className="mb-3 rounded border border-amber-500/30 bg-amber-500/10 p-3 text-xs text-amber-200">
445
+ Full request and response bodies could not be loaded. Compact metadata is
446
+ still available.
447
+ </div>
448
+ )}
399
449
  {warnings.length > 0 && (
400
450
  <div className="mb-3 rounded border border-amber-500/30 bg-amber-500/10 p-3 text-xs">
401
451
  <div className="flex items-center gap-2 font-semibold text-amber-300">
@@ -418,7 +468,7 @@ export const LogEntry = memo(function ({
418
468
  the diff or JSON view, depending on toggle state. */}
419
469
  {requestDiff ? (
420
470
  <RequestDiffContent
421
- rawBody={log.rawRequestBody}
471
+ rawBody={displayLog.rawRequestBody}
422
472
  displayedBody={displayedRequestBody}
423
473
  emptyLabel="No transformation applied; raw and sent request bodies are identical."
424
474
  />
@@ -453,8 +503,8 @@ export const LogEntry = memo(function ({
453
503
  <Suspense fallback={<TabFallback />}>
454
504
  <LazyRequestAnatomy
455
505
  parsed={requestExpansion.parsedData}
456
- inputTokens={log.inputTokens ?? null}
457
- model={log.model}
506
+ inputTokens={displayLog.inputTokens ?? null}
507
+ model={displayLog.model}
458
508
  segments={anatomySegments}
459
509
  onSegmentActivate={jumpToAnatomySegment}
460
510
  />
@@ -471,7 +521,8 @@ export const LogEntry = memo(function ({
471
521
  <div className="flex justify-end gap-2 mb-2">
472
522
  {shouldShowHeadersDiffButton(
473
523
  viewMode,
474
- log.rawHeaders !== undefined && Object.keys(log.rawHeaders).length > 0,
524
+ displayLog.rawHeaders !== undefined &&
525
+ Object.keys(displayLog.rawHeaders).length > 0,
475
526
  ) && (
476
527
  <DiffToggleButton
477
528
  active={headersDiff}
@@ -484,13 +535,13 @@ export const LogEntry = memo(function ({
484
535
  </div>
485
536
  {headersDiff ? (
486
537
  <HeadersDiffContent
487
- rawHeaders={log.rawHeaders}
488
- headers={log.headers}
538
+ rawHeaders={displayLog.rawHeaders}
539
+ headers={displayLog.headers}
489
540
  emptyLabel="No transformation applied; raw and processed headers are identical."
490
541
  />
491
- ) : log.headers && Object.keys(log.headers).length > 0 ? (
542
+ ) : displayLog.headers && Object.keys(displayLog.headers).length > 0 ? (
492
543
  <div className="space-y-1 font-mono text-xs">
493
- {Object.entries(log.headers)
544
+ {Object.entries(displayLog.headers)
494
545
  .sort(([a], [b]) => a.localeCompare(b))
495
546
  .map(([key, value]) => (
496
547
  <div key={key} className="flex gap-2">
@@ -516,9 +567,9 @@ export const LogEntry = memo(function ({
516
567
  {activeTab === "raw-headers" && (
517
568
  <div className="px-4 pt-1 pb-3">
518
569
  {/* Copy lives in the log header. */}
519
- {log.rawHeaders && Object.keys(log.rawHeaders).length > 0 ? (
570
+ {displayLog.rawHeaders && Object.keys(displayLog.rawHeaders).length > 0 ? (
520
571
  <div className="space-y-1 font-mono text-xs">
521
- {Object.entries(log.rawHeaders)
572
+ {Object.entries(displayLog.rawHeaders)
522
573
  .sort(([a], [b]) => a.localeCompare(b))
523
574
  .map(([key, value]) => (
524
575
  <div key={key} className="flex gap-2">
@@ -544,16 +595,16 @@ export const LogEntry = memo(function ({
544
595
  <TabsContent value="raw">
545
596
  {activeTab === "raw" && (
546
597
  <div className="px-4 pt-1 pb-3 space-y-3">
547
- {log.error !== undefined && log.error !== null && (
598
+ {displayLog.error !== undefined && displayLog.error !== null && (
548
599
  <div className="rounded border border-destructive/50 bg-destructive/10 p-3 text-xs">
549
600
  <div className="font-semibold text-destructive mb-1">SSE Error</div>
550
- <div className="text-muted-foreground font-mono">{log.error}</div>
601
+ <div className="text-muted-foreground font-mono">{displayLog.error}</div>
551
602
  </div>
552
603
  )}
553
- {log.responseText !== null ? (
604
+ {displayLog.responseText !== null ? (
554
605
  <Suspense fallback={<TabFallback />}>
555
606
  <LazyJsonViewerFromString
556
- text={log.responseText}
607
+ text={displayLog.responseText}
557
608
  defaultExpandDepth={0}
558
609
  bulkDepth={responseExpansion.bulkDepth}
559
610
  bulkRevision={responseExpansion.bulkRevision}
@@ -562,11 +613,11 @@ export const LogEntry = memo(function ({
562
613
  ) : (
563
614
  <p className="text-xs text-muted-foreground italic">No response</p>
564
615
  )}
565
- {log.streaming === true && (
616
+ {displayLog.streaming === true && (
566
617
  <Suspense fallback={<TabFallback />}>
567
618
  <LazyStreamingChunkSequence
568
- logId={log.id}
569
- truncated={log.streamingChunksPath !== null}
619
+ logId={displayLog.id}
620
+ truncated={displayLog.streamingChunksPath !== null}
570
621
  />
571
622
  </Suspense>
572
623
  )}
@@ -579,15 +630,15 @@ export const LogEntry = memo(function ({
579
630
  <div className="px-4 pt-1 pb-3">
580
631
  <Suspense fallback={<TabFallback />}>
581
632
  <LazyResponseView
582
- responseText={log.responseText}
583
- responseStatus={log.responseStatus}
584
- streaming={log.streaming}
585
- inputTokens={log.inputTokens}
586
- outputTokens={log.outputTokens}
587
- cacheCreationInputTokens={log.cacheCreationInputTokens}
588
- cacheReadInputTokens={log.cacheReadInputTokens}
633
+ responseText={displayLog.responseText}
634
+ responseStatus={displayLog.responseStatus}
635
+ streaming={displayLog.streaming}
636
+ inputTokens={displayLog.inputTokens}
637
+ outputTokens={displayLog.outputTokens}
638
+ cacheCreationInputTokens={displayLog.cacheCreationInputTokens}
639
+ cacheReadInputTokens={displayLog.cacheReadInputTokens}
589
640
  apiFormat={resolvedFormat}
590
- error={log.error}
641
+ error={displayLog.error}
591
642
  />
592
643
  </Suspense>
593
644
  </div>
@@ -598,7 +649,7 @@ export const LogEntry = memo(function ({
598
649
  )}
599
650
  </div>
600
651
  <Suspense fallback={null}>
601
- <LazyReplayDialog log={log} open={replayOpen} onOpenChange={setReplayOpen} />
652
+ <LazyReplayDialog log={displayLog} open={replayOpen} onOpenChange={setReplayOpen} />
602
653
  </Suspense>
603
654
  </TooltipProvider>
604
655
  );
@@ -38,7 +38,7 @@ function DialogOverlay({
38
38
  <DialogPrimitive.Overlay
39
39
  data-slot="dialog-overlay"
40
40
  className={cn(
41
- "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
41
+ "fixed inset-0 z-40 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
42
42
  className,
43
43
  )}
44
44
  {...props}
@@ -76,6 +76,7 @@ export const EvidenceReportSchema = JenkinsReportSchema;
76
76
  export const InspectorRunSchema = z.object({
77
77
  id: z.string(),
78
78
  sessionId: z.string(),
79
+ groupId: z.string().nullable().default(null),
79
80
  title: z.string(),
80
81
  task: z.string().nullable(),
81
82
  project: z.string().nullable(),
@@ -92,6 +93,7 @@ export const CreateInspectorRunInputSchema = z
92
93
  .object({
93
94
  runId: z.string().min(1).optional(),
94
95
  sessionId: z.string().min(1).optional(),
96
+ groupId: z.string().min(1).nullable().optional(),
95
97
  title: z.string().min(1).optional(),
96
98
  task: z.string().nullable().optional(),
97
99
  project: z.string().nullable().optional(),
@@ -104,6 +106,7 @@ export const CreateInspectorRunInputSchema = z
104
106
 
105
107
  export const UpdateInspectorRunInputSchema = z.object({
106
108
  sessionId: z.string().min(1).optional(),
109
+ groupId: z.string().min(1).nullable().optional(),
107
110
  title: z.string().min(1).optional(),
108
111
  task: z.string().nullable().optional(),
109
112
  project: z.string().nullable().optional(),
@@ -0,0 +1,65 @@
1
+ import type { JsonValue } from "../contracts";
2
+
3
+ export type InspectorCurrentContext = {
4
+ groupId: string | null;
5
+ runId: string | null;
6
+ sessionId: string | null;
7
+ model: string | null;
8
+ provider: string | null;
9
+ agent: string | null;
10
+ project: string | null;
11
+ task: string | null;
12
+ metadata: Record<string, JsonValue>;
13
+ updatedAt: string;
14
+ };
15
+
16
+ export type InspectorCurrentContextPatch = Partial<
17
+ Omit<InspectorCurrentContext, "metadata" | "updatedAt">
18
+ > & {
19
+ metadata?: Record<string, JsonValue>;
20
+ };
21
+
22
+ function envString(name: string): string | null {
23
+ const value = process.env[name];
24
+ if (value === undefined) return null;
25
+ const trimmed = value.trim();
26
+ return trimmed.length === 0 ? null : trimmed;
27
+ }
28
+
29
+ function initialContext(): InspectorCurrentContext {
30
+ return {
31
+ groupId: envString("INSPECTOR_GROUP_ID"),
32
+ runId: envString("INSPECTOR_RUN_ID"),
33
+ sessionId: envString("INSPECTOR_SESSION_ID"),
34
+ model: envString("INSPECTOR_MODEL"),
35
+ provider: envString("INSPECTOR_PROVIDER"),
36
+ agent: envString("INSPECTOR_AGENT"),
37
+ project: envString("INSPECTOR_PROJECT"),
38
+ task: envString("INSPECTOR_TASK"),
39
+ metadata: {},
40
+ updatedAt: new Date().toISOString(),
41
+ };
42
+ }
43
+
44
+ let currentContext = initialContext();
45
+
46
+ export function getCurrentContext(): InspectorCurrentContext {
47
+ return currentContext;
48
+ }
49
+
50
+ export function setCurrentContext(patch: InspectorCurrentContextPatch): InspectorCurrentContext {
51
+ currentContext = {
52
+ ...currentContext,
53
+ ...patch,
54
+ metadata:
55
+ patch.metadata === undefined
56
+ ? currentContext.metadata
57
+ : { ...currentContext.metadata, ...patch.metadata },
58
+ updatedAt: new Date().toISOString(),
59
+ };
60
+ return currentContext;
61
+ }
62
+
63
+ export function _resetCurrentContextForTests(): void {
64
+ currentContext = initialContext();
65
+ }
@@ -0,0 +1,56 @@
1
+ export type McpWriteMode = "enabled" | "readonly";
2
+
3
+ function parseFlag(value: string | undefined): boolean | null {
4
+ if (value === undefined) return null;
5
+ switch (value.trim().toLowerCase()) {
6
+ case "1":
7
+ case "true":
8
+ case "yes":
9
+ case "on":
10
+ return true;
11
+ case "0":
12
+ case "false":
13
+ case "no":
14
+ case "off":
15
+ return false;
16
+ default:
17
+ return null;
18
+ }
19
+ }
20
+
21
+ export function isMcpWriteEnabled(): boolean {
22
+ const readonlyFlag = parseFlag(process.env["AGENT_INSPECTOR_MCP_READONLY"]);
23
+ if (readonlyFlag === true) return false;
24
+
25
+ const writesFlag = parseFlag(process.env["AGENT_INSPECTOR_MCP_WRITES"]);
26
+ if (writesFlag === false) return false;
27
+
28
+ return true;
29
+ }
30
+
31
+ export function getMcpWriteMode(): McpWriteMode {
32
+ return isMcpWriteEnabled() ? "enabled" : "readonly";
33
+ }
34
+
35
+ export function getMcpModeInfo(): {
36
+ writeMode: McpWriteMode;
37
+ writesEnabled: boolean;
38
+ env: {
39
+ AGENT_INSPECTOR_MCP_READONLY: string | null;
40
+ AGENT_INSPECTOR_MCP_WRITES: string | null;
41
+ };
42
+ note: string;
43
+ } {
44
+ const writesEnabled = isMcpWriteEnabled();
45
+ return {
46
+ writeMode: writesEnabled ? "enabled" : "readonly",
47
+ writesEnabled,
48
+ env: {
49
+ AGENT_INSPECTOR_MCP_READONLY: process.env["AGENT_INSPECTOR_MCP_READONLY"] ?? null,
50
+ AGENT_INSPECTOR_MCP_WRITES: process.env["AGENT_INSPECTOR_MCP_WRITES"] ?? null,
51
+ },
52
+ note: writesEnabled
53
+ ? "MCP write/action tools are enabled. Set AGENT_INSPECTOR_MCP_READONLY=1 to expose the same catalog while blocking mutations."
54
+ : "MCP is in readonly mode. Read tools/resources still work, while write/action tools return an isError result.",
55
+ };
56
+ }