@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
package/src/mcp/toolHandlers.ts
CHANGED
|
@@ -36,8 +36,20 @@ 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 {
|
|
40
|
+
ProviderConfigSchema,
|
|
41
|
+
toRedactedProviderDto,
|
|
42
|
+
toRedactedProviderDtos,
|
|
43
|
+
} from "../lib/providerContract";
|
|
39
44
|
import { isRecord, textByteLength } from "../lib/unknown";
|
|
40
45
|
import { CapturedLogSchema, type CapturedLog } from "../contracts";
|
|
46
|
+
import {
|
|
47
|
+
createResourceLimitError,
|
|
48
|
+
MAX_PROVIDER_MODELS,
|
|
49
|
+
MAX_SEARCH_PROJECT_CHARS,
|
|
50
|
+
MAX_SEARCH_QUERY_CHARS,
|
|
51
|
+
type ResourceLimitErrorInput,
|
|
52
|
+
} from "../lib/resourceLimits";
|
|
41
53
|
import {
|
|
42
54
|
addInspectorGroupSession,
|
|
43
55
|
createInspectorGroup,
|
|
@@ -109,6 +121,22 @@ function toolError(message: string): ToolResult {
|
|
|
109
121
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
110
122
|
}
|
|
111
123
|
|
|
124
|
+
function resourceLimitToolError(input: ResourceLimitErrorInput): ToolResult {
|
|
125
|
+
return toolError(JSON.stringify(createResourceLimitError(input), null, 2));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function redactedProviderResult(value: unknown): ToolResult {
|
|
129
|
+
const parsed = ProviderConfigSchema.safeParse(value);
|
|
130
|
+
if (!parsed.success) return toolError("Provider API returned an invalid provider response");
|
|
131
|
+
return textJson(toRedactedProviderDto(parsed.data));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function redactedProvidersResult(value: unknown): ToolResult {
|
|
135
|
+
const parsed = ProviderConfigSchema.array().safeParse(value);
|
|
136
|
+
if (!parsed.success) return toolError("Provider API returned an invalid providers response");
|
|
137
|
+
return textJson(toRedactedProviderDtos(parsed.data));
|
|
138
|
+
}
|
|
139
|
+
|
|
112
140
|
function inspectorBaseUrl(): string {
|
|
113
141
|
return `http://127.0.0.1:${String(getCurrentPort())}`;
|
|
114
142
|
}
|
|
@@ -327,6 +355,19 @@ export type ListLogsArgs = {
|
|
|
327
355
|
};
|
|
328
356
|
|
|
329
357
|
export async function listLogsImpl(callApi: CallApiFn, args: ListLogsArgs): Promise<ToolResult> {
|
|
358
|
+
if (
|
|
359
|
+
args.limit !== undefined &&
|
|
360
|
+
(!Number.isSafeInteger(args.limit) || args.limit < 1 || args.limit > LIMIT_HARD_CAP)
|
|
361
|
+
) {
|
|
362
|
+
return resourceLimitToolError({
|
|
363
|
+
error: `MCP log pages may contain at most ${String(LIMIT_HARD_CAP)} entries.`,
|
|
364
|
+
code: "invalid-page-request",
|
|
365
|
+
status: 400,
|
|
366
|
+
resource: "mcp-log-page-size",
|
|
367
|
+
limit: LIMIT_HARD_CAP,
|
|
368
|
+
retryable: false,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
330
371
|
const offset = args.offset ?? PAGINATION_DEFAULTS.offset;
|
|
331
372
|
const limit = clampLimit(args.limit);
|
|
332
373
|
const params = new URLSearchParams({
|
|
@@ -366,6 +407,42 @@ export async function searchLogsImpl(
|
|
|
366
407
|
callApi: CallApiFn,
|
|
367
408
|
args: SearchLogsArgs,
|
|
368
409
|
): Promise<ToolResult> {
|
|
410
|
+
if ((args.query ?? "").length > MAX_SEARCH_QUERY_CHARS) {
|
|
411
|
+
return resourceLimitToolError({
|
|
412
|
+
error: `MCP search queries may contain at most ${String(MAX_SEARCH_QUERY_CHARS)} characters.`,
|
|
413
|
+
code: "search-limit-exceeded",
|
|
414
|
+
status: 400,
|
|
415
|
+
resource: "mcp-log-search-query",
|
|
416
|
+
limit: MAX_SEARCH_QUERY_CHARS,
|
|
417
|
+
retryable: false,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
if (
|
|
421
|
+
args.limit !== undefined &&
|
|
422
|
+
(!Number.isSafeInteger(args.limit) || args.limit < 1 || args.limit > LIMIT_HARD_CAP)
|
|
423
|
+
) {
|
|
424
|
+
return resourceLimitToolError({
|
|
425
|
+
error: `MCP search pages may contain at most ${String(LIMIT_HARD_CAP)} entries.`,
|
|
426
|
+
code: "invalid-page-request",
|
|
427
|
+
status: 400,
|
|
428
|
+
resource: "mcp-log-search-page-size",
|
|
429
|
+
limit: LIMIT_HARD_CAP,
|
|
430
|
+
retryable: false,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
if (
|
|
434
|
+
args.scanLimit !== undefined &&
|
|
435
|
+
(!Number.isSafeInteger(args.scanLimit) || args.scanLimit < 1 || args.scanLimit > 1_000)
|
|
436
|
+
) {
|
|
437
|
+
return resourceLimitToolError({
|
|
438
|
+
error: "MCP log searches may scan at most 1000 entries.",
|
|
439
|
+
code: "search-limit-exceeded",
|
|
440
|
+
status: 400,
|
|
441
|
+
resource: "mcp-log-search-scan-size",
|
|
442
|
+
limit: 1_000,
|
|
443
|
+
retryable: false,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
369
446
|
const offset = args.offset ?? PAGINATION_DEFAULTS.offset;
|
|
370
447
|
const limit = clampLimit(args.limit);
|
|
371
448
|
const params = new URLSearchParams({
|
|
@@ -1060,13 +1137,13 @@ export async function listModelsImpl(callApi: CallApiFn): Promise<ToolResult> {
|
|
|
1060
1137
|
export async function listProvidersImpl(callApi: CallApiFn): Promise<ToolResult> {
|
|
1061
1138
|
const res = await callApi("/api/providers");
|
|
1062
1139
|
if (!res.ok) return toolError(`GET /api/providers returned ${res.status}`);
|
|
1063
|
-
return
|
|
1140
|
+
return redactedProvidersResult(await res.json());
|
|
1064
1141
|
}
|
|
1065
1142
|
|
|
1066
1143
|
export async function getProviderImpl(callApi: CallApiFn, id: string): Promise<ToolResult> {
|
|
1067
1144
|
const res = await callApi(`/api/providers/${encodeURIComponent(id)}`);
|
|
1068
1145
|
if (!res.ok) return toolError(`GET /api/providers/${id} returned ${res.status}`);
|
|
1069
|
-
return
|
|
1146
|
+
return redactedProviderResult(await res.json());
|
|
1070
1147
|
}
|
|
1071
1148
|
|
|
1072
1149
|
export type ReplayLogArgs = { id: number };
|
|
@@ -1118,13 +1195,23 @@ export async function addProviderImpl(
|
|
|
1118
1195
|
provider: AddProviderInput,
|
|
1119
1196
|
): Promise<ToolResult> {
|
|
1120
1197
|
const models = normalizedProviderModels(provider);
|
|
1198
|
+
if (models.length > MAX_PROVIDER_MODELS) {
|
|
1199
|
+
return resourceLimitToolError({
|
|
1200
|
+
error: `Provider model lists may contain at most ${String(MAX_PROVIDER_MODELS)} entries.`,
|
|
1201
|
+
code: "provider-model-limit-exceeded",
|
|
1202
|
+
status: 413,
|
|
1203
|
+
resource: "provider-models",
|
|
1204
|
+
limit: MAX_PROVIDER_MODELS,
|
|
1205
|
+
retryable: false,
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1121
1208
|
const res = await callApi("/api/providers", {
|
|
1122
1209
|
method: "POST",
|
|
1123
1210
|
headers: { "content-type": "application/json" },
|
|
1124
1211
|
body: JSON.stringify({ ...provider, models }),
|
|
1125
1212
|
});
|
|
1126
1213
|
if (!res.ok) return toolError(`POST /api/providers returned ${res.status}`);
|
|
1127
|
-
return
|
|
1214
|
+
return redactedProviderResult(await res.json());
|
|
1128
1215
|
}
|
|
1129
1216
|
|
|
1130
1217
|
export type UpdateProviderInput = {
|
|
@@ -1151,6 +1238,16 @@ export async function updateProviderImpl(
|
|
|
1151
1238
|
patch.models !== undefined || patch.model !== undefined
|
|
1152
1239
|
? normalizedProviderModels(patch)
|
|
1153
1240
|
: undefined;
|
|
1241
|
+
if (models !== undefined && models.length > MAX_PROVIDER_MODELS) {
|
|
1242
|
+
return resourceLimitToolError({
|
|
1243
|
+
error: `Provider model lists may contain at most ${String(MAX_PROVIDER_MODELS)} entries.`,
|
|
1244
|
+
code: "provider-model-limit-exceeded",
|
|
1245
|
+
status: 413,
|
|
1246
|
+
resource: "provider-models",
|
|
1247
|
+
limit: MAX_PROVIDER_MODELS,
|
|
1248
|
+
retryable: false,
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1154
1251
|
const body = models === undefined ? patch : { ...patch, models };
|
|
1155
1252
|
const res = await callApi(`/api/providers/${encodeURIComponent(id)}`, {
|
|
1156
1253
|
method: "PUT",
|
|
@@ -1158,7 +1255,7 @@ export async function updateProviderImpl(
|
|
|
1158
1255
|
body: JSON.stringify(body),
|
|
1159
1256
|
});
|
|
1160
1257
|
if (!res.ok) return toolError(`PUT /api/providers/${id} returned ${res.status}`);
|
|
1161
|
-
return
|
|
1258
|
+
return redactedProviderResult(await res.json());
|
|
1162
1259
|
}
|
|
1163
1260
|
|
|
1164
1261
|
export async function testProviderImpl(callApi: CallApiFn, id: string): Promise<ToolResult> {
|
|
@@ -1365,6 +1462,26 @@ export async function searchKnowledgeImpl(
|
|
|
1365
1462
|
callApi: CallApiFn,
|
|
1366
1463
|
args: SearchKnowledgeArgs,
|
|
1367
1464
|
): Promise<ToolResult> {
|
|
1465
|
+
if (args.query.length > MAX_SEARCH_QUERY_CHARS) {
|
|
1466
|
+
return resourceLimitToolError({
|
|
1467
|
+
error: `MCP search queries may contain at most ${String(MAX_SEARCH_QUERY_CHARS)} characters.`,
|
|
1468
|
+
code: "search-limit-exceeded",
|
|
1469
|
+
status: 400,
|
|
1470
|
+
resource: "mcp-knowledge-search-query",
|
|
1471
|
+
limit: MAX_SEARCH_QUERY_CHARS,
|
|
1472
|
+
retryable: false,
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
if (args.project !== undefined && args.project.length > MAX_SEARCH_PROJECT_CHARS) {
|
|
1476
|
+
return resourceLimitToolError({
|
|
1477
|
+
error: `MCP search project filters may contain at most ${String(MAX_SEARCH_PROJECT_CHARS)} characters.`,
|
|
1478
|
+
code: "search-limit-exceeded",
|
|
1479
|
+
status: 400,
|
|
1480
|
+
resource: "mcp-knowledge-search-project",
|
|
1481
|
+
limit: MAX_SEARCH_PROJECT_CHARS,
|
|
1482
|
+
retryable: false,
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1368
1485
|
const params = new URLSearchParams({ q: args.query });
|
|
1369
1486
|
if (args.project !== undefined && args.project.length > 0) {
|
|
1370
1487
|
params.set("project", args.project);
|
|
@@ -1382,6 +1499,16 @@ export async function getProjectContextImpl(
|
|
|
1382
1499
|
callApi: CallApiFn,
|
|
1383
1500
|
args: GetProjectContextArgs,
|
|
1384
1501
|
): Promise<ToolResult> {
|
|
1502
|
+
if (args.project.length > MAX_SEARCH_PROJECT_CHARS) {
|
|
1503
|
+
return resourceLimitToolError({
|
|
1504
|
+
error: `MCP search project filters may contain at most ${String(MAX_SEARCH_PROJECT_CHARS)} characters.`,
|
|
1505
|
+
code: "search-limit-exceeded",
|
|
1506
|
+
status: 400,
|
|
1507
|
+
resource: "mcp-knowledge-search-project",
|
|
1508
|
+
limit: MAX_SEARCH_PROJECT_CHARS,
|
|
1509
|
+
retryable: false,
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1385
1512
|
const params = new URLSearchParams({ project: args.project });
|
|
1386
1513
|
const res = await callApi(`/api/knowledge/project-context?${params.toString()}`);
|
|
1387
1514
|
if (!res.ok) return toolError(`GET /api/knowledge/project-context returned ${res.status}`);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
writeFileSync,
|
|
3
|
-
mkdirSync,
|
|
4
3
|
readFileSync,
|
|
5
4
|
unlinkSync,
|
|
6
5
|
existsSync,
|
|
@@ -12,6 +11,11 @@ import { logger } from "./logger";
|
|
|
12
11
|
import { z } from "zod";
|
|
13
12
|
import { JsonValueSchema, type StreamingChunk } from "./schemas";
|
|
14
13
|
import { getDataDir } from "./dataDir";
|
|
14
|
+
import {
|
|
15
|
+
ensurePrivateDirectorySync,
|
|
16
|
+
PRIVATE_FILE_MODE,
|
|
17
|
+
securePrivateFileSync,
|
|
18
|
+
} from "./privateDataPath";
|
|
15
19
|
|
|
16
20
|
export type StreamingChunksData = {
|
|
17
21
|
chunks: StreamingChunk[];
|
|
@@ -56,17 +60,24 @@ export function writeChunks(logId: number, chunks: StreamingChunk[], truncated?:
|
|
|
56
60
|
const tempPath = getTempFilePath(logId);
|
|
57
61
|
|
|
58
62
|
// Ensure directory exists
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
logger.error("[chunkStorage] Failed to create chunks directory:", String(err));
|
|
63
|
+
if (!ensurePrivateDirectorySync(dir)) {
|
|
64
|
+
logger.error("[chunkStorage] Failed to secure chunks directory:", dir);
|
|
65
|
+
return targetPath;
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
const data: StreamingChunksData = { chunks, truncated };
|
|
66
69
|
|
|
67
70
|
// Write to temp file first
|
|
68
71
|
try {
|
|
69
|
-
writeFileSync(tempPath, JSON.stringify(data),
|
|
72
|
+
writeFileSync(tempPath, JSON.stringify(data), {
|
|
73
|
+
encoding: "utf-8",
|
|
74
|
+
mode: PRIVATE_FILE_MODE,
|
|
75
|
+
});
|
|
76
|
+
if (!securePrivateFileSync(tempPath)) {
|
|
77
|
+
unlinkSync(tempPath);
|
|
78
|
+
logger.error("[chunkStorage] Failed to secure chunks temp file:", tempPath);
|
|
79
|
+
return targetPath;
|
|
80
|
+
}
|
|
70
81
|
} catch (err) {
|
|
71
82
|
logger.error("[chunkStorage] Failed to write chunks temp file:", String(err));
|
|
72
83
|
return targetPath;
|
|
@@ -80,6 +91,9 @@ export function writeChunks(logId: number, chunks: StreamingChunk[], truncated?:
|
|
|
80
91
|
// If rename fails, try copy + delete
|
|
81
92
|
try {
|
|
82
93
|
copyFileSync(tempPath, targetPath);
|
|
94
|
+
if (!securePrivateFileSync(targetPath)) {
|
|
95
|
+
logger.error("[chunkStorage] Failed to secure chunks file:", targetPath);
|
|
96
|
+
}
|
|
83
97
|
unlinkSync(tempPath);
|
|
84
98
|
} catch (copyErr) {
|
|
85
99
|
logger.error("[chunkStorage] Failed to copy chunks file:", String(copyErr));
|
package/src/proxy/config.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
writeFileSync,
|
|
3
|
-
mkdirSync,
|
|
4
3
|
readFileSync,
|
|
5
4
|
unlinkSync,
|
|
6
5
|
existsSync,
|
|
@@ -10,6 +9,11 @@ import {
|
|
|
10
9
|
import { join } from "node:path";
|
|
11
10
|
import { logger } from "./logger";
|
|
12
11
|
import { getDataDir } from "./dataDir";
|
|
12
|
+
import {
|
|
13
|
+
ensurePrivateDirectorySync,
|
|
14
|
+
PRIVATE_FILE_MODE,
|
|
15
|
+
securePrivateFileSync,
|
|
16
|
+
} from "./privateDataPath";
|
|
13
17
|
import {
|
|
14
18
|
CaptureModeSchema,
|
|
15
19
|
DEFAULT_CAPTURE_MODE,
|
|
@@ -168,15 +172,21 @@ function persistConfig(value: RuntimeConfig): boolean {
|
|
|
168
172
|
const targetPath = getConfigFilePath();
|
|
169
173
|
const tempPath = getConfigTempPath();
|
|
170
174
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
} catch (err) {
|
|
174
|
-
logger.error(`[config] Failed to create data directory ${dir}: ${String(err)}`);
|
|
175
|
+
if (!ensurePrivateDirectorySync(dir)) {
|
|
176
|
+
logger.error(`[config] Failed to secure data directory ${dir}`);
|
|
175
177
|
return false;
|
|
176
178
|
}
|
|
177
179
|
|
|
178
180
|
try {
|
|
179
|
-
writeFileSync(tempPath, JSON.stringify(toPersistedConfig(value)),
|
|
181
|
+
writeFileSync(tempPath, JSON.stringify(toPersistedConfig(value)), {
|
|
182
|
+
encoding: "utf-8",
|
|
183
|
+
mode: PRIVATE_FILE_MODE,
|
|
184
|
+
});
|
|
185
|
+
if (!securePrivateFileSync(tempPath)) {
|
|
186
|
+
unlinkSync(tempPath);
|
|
187
|
+
logger.error(`[config] Failed to secure config temp file ${tempPath}`);
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
180
190
|
} catch (err) {
|
|
181
191
|
logger.error(`[config] Failed to write config temp file: ${String(err)}`);
|
|
182
192
|
return false;
|
|
@@ -189,6 +199,10 @@ function persistConfig(value: RuntimeConfig): boolean {
|
|
|
189
199
|
logger.warn(`[config] Rename failed, falling back to copy+delete: ${String(err)}`);
|
|
190
200
|
try {
|
|
191
201
|
copyFileSync(tempPath, targetPath);
|
|
202
|
+
if (!securePrivateFileSync(targetPath)) {
|
|
203
|
+
logger.error(`[config] Failed to secure config file ${targetPath}`);
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
192
206
|
unlinkSync(tempPath);
|
|
193
207
|
return true;
|
|
194
208
|
} catch (copyErr) {
|
package/src/proxy/dataDir.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cpSync, existsSync, rmSync } from "node:fs";
|
|
2
2
|
import { isAbsolute, join } from "node:path";
|
|
3
|
+
import { ensurePrivateDirectorySync, securePrivateTreeSync } from "./privateDataPath";
|
|
3
4
|
|
|
4
5
|
const CURRENT_DATA_DIR_NAME = ".agent-inspector";
|
|
5
6
|
const LEGACY_DATA_DIR_NAMES = [".llm-inspector", ".llm-agent"];
|
|
@@ -98,7 +99,9 @@ export function migrateLegacyDataDirs(): void {
|
|
|
98
99
|
if (!existsSync(legacyDir)) continue;
|
|
99
100
|
|
|
100
101
|
try {
|
|
102
|
+
if (!ensurePrivateDirectorySync(currentDir)) return;
|
|
101
103
|
cpSync(legacyDir, currentDir, { recursive: true, force: true });
|
|
104
|
+
if (!securePrivateTreeSync(currentDir)) return;
|
|
102
105
|
rmSync(legacyDir, { recursive: true, force: true });
|
|
103
106
|
} catch {
|
|
104
107
|
// Best-effort migration: startup must continue even when the old
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
ECOSYSTEM_EXECUTION_CONFIRMATION_PHRASE,
|
|
4
|
+
type EcosystemExecutionConfirmation,
|
|
5
|
+
type EcosystemExecutionPlan,
|
|
6
|
+
type EcosystemTaskAction,
|
|
7
|
+
} from "../lib/ecosystemContract";
|
|
8
|
+
import { logger } from "./logger";
|
|
9
|
+
|
|
10
|
+
const CONFIRMATION_TTL_MS = 2 * 60 * 1000;
|
|
11
|
+
const MAX_PENDING_CONFIRMATIONS = 100;
|
|
12
|
+
|
|
13
|
+
type ConfirmationRecord = {
|
|
14
|
+
confirmation: EcosystemExecutionConfirmation;
|
|
15
|
+
expiresAtMs: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type ConfirmationFailureReason =
|
|
19
|
+
| "missing_or_invalid_confirmation"
|
|
20
|
+
| "expired_confirmation"
|
|
21
|
+
| "mismatched_confirmation";
|
|
22
|
+
|
|
23
|
+
type ConsumeEcosystemConfirmationResult =
|
|
24
|
+
| { ok: true; plan: EcosystemExecutionPlan }
|
|
25
|
+
| { ok: false; reason: ConfirmationFailureReason };
|
|
26
|
+
|
|
27
|
+
type EcosystemAuditEvent = "confirmation_issued" | "confirmation_rejected" | "execution_started";
|
|
28
|
+
type EcosystemAuditReason =
|
|
29
|
+
| "none"
|
|
30
|
+
| "invalid_request"
|
|
31
|
+
| "unknown_target"
|
|
32
|
+
| "missing_or_invalid_confirmation"
|
|
33
|
+
| "expired_confirmation"
|
|
34
|
+
| "mismatched_confirmation"
|
|
35
|
+
| "task_unavailable";
|
|
36
|
+
|
|
37
|
+
const confirmations = new Map<string, ConfirmationRecord>();
|
|
38
|
+
|
|
39
|
+
function pruneConfirmations(nowMs: number): void {
|
|
40
|
+
for (const [nonce, record] of confirmations) {
|
|
41
|
+
if (record.expiresAtMs <= nowMs) confirmations.delete(nonce);
|
|
42
|
+
}
|
|
43
|
+
while (confirmations.size >= MAX_PENDING_CONFIRMATIONS) {
|
|
44
|
+
const oldestNonce = confirmations.keys().next().value;
|
|
45
|
+
if (typeof oldestNonce !== "string") return;
|
|
46
|
+
confirmations.delete(oldestNonce);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function safeAuditTarget(targetId: string | null): string {
|
|
51
|
+
if (targetId === null || !/^[a-z0-9-]{1,64}$/.test(targetId)) return "unrecognized";
|
|
52
|
+
return targetId;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function auditEcosystemExecution(
|
|
56
|
+
event: EcosystemAuditEvent,
|
|
57
|
+
action: EcosystemTaskAction,
|
|
58
|
+
targetId: string | null,
|
|
59
|
+
reason: EcosystemAuditReason,
|
|
60
|
+
): void {
|
|
61
|
+
logger.info(
|
|
62
|
+
`[security-audit] event=${event} action=${action} target=${safeAuditTarget(targetId)} reason=${reason}`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function createEcosystemExecutionConfirmation(
|
|
67
|
+
plan: EcosystemExecutionPlan,
|
|
68
|
+
nowMs = Date.now(),
|
|
69
|
+
): EcosystemExecutionConfirmation {
|
|
70
|
+
pruneConfirmations(nowMs);
|
|
71
|
+
const expiresAtMs = nowMs + CONFIRMATION_TTL_MS;
|
|
72
|
+
const confirmation: EcosystemExecutionConfirmation = {
|
|
73
|
+
nonce: randomUUID(),
|
|
74
|
+
phrase: ECOSYSTEM_EXECUTION_CONFIRMATION_PHRASE,
|
|
75
|
+
expiresAt: new Date(expiresAtMs).toISOString(),
|
|
76
|
+
plan,
|
|
77
|
+
};
|
|
78
|
+
confirmations.set(confirmation.nonce, { confirmation, expiresAtMs });
|
|
79
|
+
return confirmation;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function consumeEcosystemExecutionConfirmation(
|
|
83
|
+
nonce: string,
|
|
84
|
+
phrase: string,
|
|
85
|
+
expectedPlan: EcosystemExecutionPlan,
|
|
86
|
+
nowMs = Date.now(),
|
|
87
|
+
): ConsumeEcosystemConfirmationResult {
|
|
88
|
+
const record = confirmations.get(nonce);
|
|
89
|
+
if (record === undefined) {
|
|
90
|
+
return { ok: false, reason: "missing_or_invalid_confirmation" };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
confirmations.delete(nonce);
|
|
94
|
+
if (record.expiresAtMs <= nowMs) return { ok: false, reason: "expired_confirmation" };
|
|
95
|
+
const reviewedPlan = record.confirmation.plan;
|
|
96
|
+
if (
|
|
97
|
+
phrase !== ECOSYSTEM_EXECUTION_CONFIRMATION_PHRASE ||
|
|
98
|
+
reviewedPlan.action !== expectedPlan.action ||
|
|
99
|
+
reviewedPlan.targetId !== expectedPlan.targetId ||
|
|
100
|
+
reviewedPlan.targetName !== expectedPlan.targetName ||
|
|
101
|
+
reviewedPlan.command !== expectedPlan.command ||
|
|
102
|
+
reviewedPlan.source !== expectedPlan.source ||
|
|
103
|
+
reviewedPlan.timeoutMs !== expectedPlan.timeoutMs ||
|
|
104
|
+
reviewedPlan.scope !== expectedPlan.scope
|
|
105
|
+
) {
|
|
106
|
+
return { ok: false, reason: "mismatched_confirmation" };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { ok: true, plan: reviewedPlan };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function resetEcosystemExecutionConfirmationsForTests(): void {
|
|
113
|
+
confirmations.clear();
|
|
114
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EcosystemExecutionRequestSchema,
|
|
3
|
+
type EcosystemTask,
|
|
4
|
+
type EcosystemTaskAction,
|
|
5
|
+
} from "../lib/ecosystemContract";
|
|
6
|
+
import {
|
|
7
|
+
auditEcosystemExecution,
|
|
8
|
+
consumeEcosystemExecutionConfirmation,
|
|
9
|
+
createEcosystemExecutionConfirmation,
|
|
10
|
+
} from "./ecosystemExecutionConfirmation";
|
|
11
|
+
import {
|
|
12
|
+
getEcosystemPackageExecutionPlan,
|
|
13
|
+
getEcosystemRecipeExecutionPlan,
|
|
14
|
+
startEcosystemRecipeTask,
|
|
15
|
+
startEcosystemTask,
|
|
16
|
+
} from "./ecosystemTasks";
|
|
17
|
+
|
|
18
|
+
const NO_STORE_HEADERS = { "Cache-Control": "no-store" };
|
|
19
|
+
|
|
20
|
+
type StartPackageTask = (packageId: string, action: EcosystemTaskAction) => EcosystemTask | null;
|
|
21
|
+
type StartRecipeTask = (recipeId: string) => EcosystemTask | null;
|
|
22
|
+
|
|
23
|
+
function noStoreJson(body: unknown, status = 200): Response {
|
|
24
|
+
return Response.json(body, { status, headers: NO_STORE_HEADERS });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function readExecutionRequest(request: Request) {
|
|
28
|
+
const body: unknown = await request.json().catch(() => null);
|
|
29
|
+
return EcosystemExecutionRequestSchema.safeParse(body);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function handleEcosystemPackageExecution(
|
|
33
|
+
request: Request,
|
|
34
|
+
packageId: string,
|
|
35
|
+
action: Exclude<EcosystemTaskAction, "recipe">,
|
|
36
|
+
startTask: StartPackageTask = startEcosystemTask,
|
|
37
|
+
): Promise<Response> {
|
|
38
|
+
const parsed = await readExecutionRequest(request);
|
|
39
|
+
if (!parsed.success) {
|
|
40
|
+
auditEcosystemExecution("confirmation_rejected", action, null, "invalid_request");
|
|
41
|
+
return noStoreJson({ error: "Explicit local command confirmation is required" }, 400);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const plan = getEcosystemPackageExecutionPlan(packageId, action);
|
|
45
|
+
if (plan === null) {
|
|
46
|
+
auditEcosystemExecution("confirmation_rejected", action, null, "unknown_target");
|
|
47
|
+
return noStoreJson({ error: "Unknown or unavailable ecosystem action" }, 404);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (parsed.data.intent === "prepare") {
|
|
51
|
+
const confirmation = createEcosystemExecutionConfirmation(plan);
|
|
52
|
+
auditEcosystemExecution("confirmation_issued", action, plan.targetId, "none");
|
|
53
|
+
return noStoreJson({ confirmation });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const consumed = consumeEcosystemExecutionConfirmation(
|
|
57
|
+
parsed.data.nonce,
|
|
58
|
+
parsed.data.confirmation,
|
|
59
|
+
plan,
|
|
60
|
+
);
|
|
61
|
+
if (!consumed.ok) {
|
|
62
|
+
auditEcosystemExecution("confirmation_rejected", action, plan.targetId, consumed.reason);
|
|
63
|
+
return noStoreJson({ error: "Confirmation is invalid, expired, or already used" }, 409);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const task = startTask(packageId, action);
|
|
67
|
+
if (task === null) {
|
|
68
|
+
auditEcosystemExecution("confirmation_rejected", action, plan.targetId, "task_unavailable");
|
|
69
|
+
return noStoreJson({ error: "Ecosystem action is no longer available" }, 409);
|
|
70
|
+
}
|
|
71
|
+
auditEcosystemExecution("execution_started", action, plan.targetId, "none");
|
|
72
|
+
return noStoreJson({ task });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function handleEcosystemRecipeExecution(
|
|
76
|
+
request: Request,
|
|
77
|
+
recipeId: string,
|
|
78
|
+
startTask: StartRecipeTask = startEcosystemRecipeTask,
|
|
79
|
+
): Promise<Response> {
|
|
80
|
+
const action = "recipe";
|
|
81
|
+
const parsed = await readExecutionRequest(request);
|
|
82
|
+
if (!parsed.success) {
|
|
83
|
+
auditEcosystemExecution("confirmation_rejected", action, null, "invalid_request");
|
|
84
|
+
return noStoreJson({ error: "Explicit local command confirmation is required" }, 400);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const plan = getEcosystemRecipeExecutionPlan(recipeId);
|
|
88
|
+
if (plan === null) {
|
|
89
|
+
auditEcosystemExecution("confirmation_rejected", action, null, "unknown_target");
|
|
90
|
+
return noStoreJson({ error: "Unknown or unavailable ecosystem recipe" }, 404);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (parsed.data.intent === "prepare") {
|
|
94
|
+
const confirmation = createEcosystemExecutionConfirmation(plan);
|
|
95
|
+
auditEcosystemExecution("confirmation_issued", action, plan.targetId, "none");
|
|
96
|
+
return noStoreJson({ confirmation });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const consumed = consumeEcosystemExecutionConfirmation(
|
|
100
|
+
parsed.data.nonce,
|
|
101
|
+
parsed.data.confirmation,
|
|
102
|
+
plan,
|
|
103
|
+
);
|
|
104
|
+
if (!consumed.ok) {
|
|
105
|
+
auditEcosystemExecution("confirmation_rejected", action, plan.targetId, consumed.reason);
|
|
106
|
+
return noStoreJson({ error: "Confirmation is invalid, expired, or already used" }, 409);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const task = startTask(recipeId);
|
|
110
|
+
if (task === null) {
|
|
111
|
+
auditEcosystemExecution("confirmation_rejected", action, plan.targetId, "task_unavailable");
|
|
112
|
+
return noStoreJson({ error: "Ecosystem recipe is no longer available" }, 409);
|
|
113
|
+
}
|
|
114
|
+
auditEcosystemExecution("execution_started", action, plan.targetId, "none");
|
|
115
|
+
return noStoreJson({ task });
|
|
116
|
+
}
|
|
@@ -2,6 +2,7 @@ import { Buffer } from "node:buffer";
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import type {
|
|
5
|
+
EcosystemExecutionPlan,
|
|
5
6
|
EcosystemTask,
|
|
6
7
|
EcosystemTaskAction,
|
|
7
8
|
EcosystemTaskStatus,
|
|
@@ -93,6 +94,52 @@ function taskCommand(args: readonly string[]): string {
|
|
|
93
94
|
return `${npmCommand()} ${args.join(" ")}`;
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
export function getEcosystemPackageExecutionPlan(
|
|
98
|
+
packageId: string,
|
|
99
|
+
action: EcosystemTaskAction,
|
|
100
|
+
): EcosystemExecutionPlan | null {
|
|
101
|
+
if (action === "recipe") return null;
|
|
102
|
+
const definition =
|
|
103
|
+
action === "runner-presets"
|
|
104
|
+
? findEcosystemPackage("eval-harness")
|
|
105
|
+
: findEcosystemPackage(packageId);
|
|
106
|
+
if (definition === null) return null;
|
|
107
|
+
if (action === "runner-presets" && packageId !== "eval-harness") return null;
|
|
108
|
+
|
|
109
|
+
const args = packageActionArgs(action, definition.npmName);
|
|
110
|
+
const mutatesGlobalPackages = action === "install" || action === "upgrade";
|
|
111
|
+
return {
|
|
112
|
+
action,
|
|
113
|
+
targetId: definition.id,
|
|
114
|
+
targetName: definition.name,
|
|
115
|
+
command: taskCommand(args),
|
|
116
|
+
source: `npm registry: ${definition.npmName}`,
|
|
117
|
+
timeoutMs: packageActionTimeout(action),
|
|
118
|
+
scope: mutatesGlobalPackages
|
|
119
|
+
? "Global npm package installation"
|
|
120
|
+
: "Ephemeral npm exec process in the Inspector service account",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function getEcosystemRecipeExecutionPlan(recipeId: string): EcosystemExecutionPlan | null {
|
|
125
|
+
const recipe = findEcosystemRecipe(recipeId);
|
|
126
|
+
if (recipe === null || !recipe.runnable) return null;
|
|
127
|
+
const definition = findEcosystemPackage(recipe.packageId);
|
|
128
|
+
if (definition === null) return null;
|
|
129
|
+
const args = recipeActionArgs(recipe.id);
|
|
130
|
+
if (args === null) return null;
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
action: "recipe",
|
|
134
|
+
targetId: recipe.id,
|
|
135
|
+
targetName: recipe.title,
|
|
136
|
+
command: taskCommand(args),
|
|
137
|
+
source: `Built-in allowlisted recipe using ${definition.npmName}`,
|
|
138
|
+
timeoutMs: RECIPE_TIMEOUT_MS,
|
|
139
|
+
scope: "Ephemeral npm exec process in the Inspector service account",
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
96
143
|
function completeTask(
|
|
97
144
|
task: EcosystemTask,
|
|
98
145
|
status: Exclude<EcosystemTaskStatus, "queued" | "running">,
|
|
@@ -117,6 +164,7 @@ export function startEcosystemTask(
|
|
|
117
164
|
packageId: string,
|
|
118
165
|
action: EcosystemTaskAction,
|
|
119
166
|
): EcosystemTask | null {
|
|
167
|
+
if (action === "recipe") return null;
|
|
120
168
|
const definition =
|
|
121
169
|
action === "runner-presets"
|
|
122
170
|
? findEcosystemPackage("eval-harness")
|