@vellumai/assistant 0.8.12-staging.1 → 0.8.12
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/openapi.yaml +507 -0
- package/package.json +1 -1
- package/src/__tests__/llm-catalog-parity.test.ts +16 -0
- package/src/__tests__/log-export-workspace.test.ts +468 -3
- package/src/__tests__/secret-fixtures.ts +20 -0
- package/src/__tests__/tool-approval-handler.test.ts +85 -0
- package/src/__tests__/tool-audit-listener.test.ts +86 -0
- package/src/__tests__/workspace-migration-100-upgrade-quality-profile-to-fable-5.test.ts +174 -0
- package/src/__tests__/workspace-migration-101-upgrade-balanced-economy-to-minimax-m3.test.ts +162 -0
- package/src/acp/__tests__/agent-process.test.ts +315 -2
- package/src/acp/__tests__/prepare-agent-env.test.ts +79 -5
- package/src/acp/agent-process.ts +163 -34
- package/src/acp/prepare-agent-env.ts +55 -15
- package/src/bundler/app-compiler.ts +8 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +10 -4
- package/src/cli/lib/upgrade-plugin.ts +13 -7
- package/src/config/seed-inference-profiles.ts +4 -8
- package/src/events/tool-audit-listener.ts +40 -9
- package/src/providers/__tests__/unparseable-tool-args.test.ts +53 -0
- package/src/providers/model-catalog.ts +28 -0
- package/src/providers/model-intents.ts +1 -1
- package/src/providers/openai/chat-completions-provider.ts +2 -1
- package/src/providers/openai/responses-provider.ts +2 -1
- package/src/providers/unparseable-tool-args.ts +56 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +132 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +347 -0
- package/src/runtime/routes/conversation-query-routes.ts +79 -4
- package/src/runtime/routes/log-export-routes.ts +143 -96
- package/src/runtime/routes/plugins-routes.ts +359 -0
- package/src/runtime/routes/redact-staged-export.ts +259 -0
- package/src/security/redact-json.ts +61 -0
- package/src/tools/tool-approval-handler.ts +31 -0
- package/src/workspace/migrations/100-upgrade-quality-profile-to-fable-5.ts +86 -0
- package/src/workspace/migrations/101-upgrade-balanced-economy-to-minimax-m3.ts +70 -0
- package/src/workspace/migrations/registry.ts +4 -0
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* GET /v1/messages/:id/content — full message content
|
|
15
15
|
* GET /v1/messages/:id/llm-context — LLM request logs for a message
|
|
16
16
|
* GET /v1/llm-request-logs/:id/payload — raw payload for a single log
|
|
17
|
+
* GET /v1/llm-request-logs/:id/context — normalized context for a single log
|
|
17
18
|
* DELETE /v1/messages/queued/:id — delete queued message
|
|
18
19
|
* POST /v1/messages/queued/:id/steer — steer to a queued message
|
|
19
20
|
*/
|
|
@@ -21,6 +22,7 @@
|
|
|
21
22
|
import { z } from "zod";
|
|
22
23
|
|
|
23
24
|
import { LlmContextResponseSchema } from "../../api/responses/llm-context-response.js";
|
|
25
|
+
import { LLMRequestLogEntrySchema } from "../../api/responses/llm-request-log-entry.js";
|
|
24
26
|
import {
|
|
25
27
|
deepMergeOverwrite,
|
|
26
28
|
fillContextDefaultsForMissingKeys,
|
|
@@ -236,7 +238,26 @@ function applyStoredProviderToLlmContextResult(
|
|
|
236
238
|
return { ...normalized, summary };
|
|
237
239
|
}
|
|
238
240
|
|
|
239
|
-
|
|
241
|
+
/**
|
|
242
|
+
* `full` returns the complete normalized entry including request/response
|
|
243
|
+
* sections; `summary` omits the sections so list responses stay small —
|
|
244
|
+
* sections for a single call are fetched lazily through
|
|
245
|
+
* `/v1/llm-request-logs/{logId}/context`.
|
|
246
|
+
*/
|
|
247
|
+
type LlmContextView = "full" | "summary";
|
|
248
|
+
|
|
249
|
+
function resolveLlmContextView(view: string | undefined): LlmContextView {
|
|
250
|
+
if (view === undefined || view === "full") return "full";
|
|
251
|
+
if (view === "summary") return "summary";
|
|
252
|
+
throw new BadRequestError(
|
|
253
|
+
`Invalid view parameter: ${view}. Expected "full" or "summary".`,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function normalizeLlmContextLog(
|
|
258
|
+
log: LogRow,
|
|
259
|
+
view: LlmContextView = "full",
|
|
260
|
+
): LlmContextRouteResult & {
|
|
240
261
|
id: string;
|
|
241
262
|
requestPayload: null;
|
|
242
263
|
responsePayload: null;
|
|
@@ -283,6 +304,9 @@ function normalizeLlmContextLog(log: LogRow): LlmContextRouteResult & {
|
|
|
283
304
|
// existing `agent_loop_exit_reason` column tells it WHICH error fired.
|
|
284
305
|
callSite: log.callSite ?? null,
|
|
285
306
|
...result,
|
|
307
|
+
...(view === "summary"
|
|
308
|
+
? { requestSections: undefined, responseSections: undefined }
|
|
309
|
+
: {}),
|
|
286
310
|
};
|
|
287
311
|
}
|
|
288
312
|
|
|
@@ -898,11 +922,15 @@ function resolveConversationKind(
|
|
|
898
922
|
return "user";
|
|
899
923
|
}
|
|
900
924
|
|
|
901
|
-
async function handleGetLlmContext({
|
|
925
|
+
async function handleGetLlmContext({
|
|
926
|
+
pathParams = {},
|
|
927
|
+
queryParams = {},
|
|
928
|
+
}: RouteHandlerArgs) {
|
|
902
929
|
const messageId = pathParams.id;
|
|
903
930
|
if (!messageId) {
|
|
904
931
|
throw new BadRequestError("message id is required");
|
|
905
932
|
}
|
|
933
|
+
const view = resolveLlmContextView(queryParams.view);
|
|
906
934
|
const source = await getLlmRequestLogSource();
|
|
907
935
|
const logs = await source.getRequestLogsByMessageId(messageId);
|
|
908
936
|
const turnMessageIds = getAssistantMessageIdsInTurn(messageId);
|
|
@@ -932,7 +960,7 @@ async function handleGetLlmContext({ pathParams = {} }: RouteHandlerArgs) {
|
|
|
932
960
|
messageId,
|
|
933
961
|
conversationKind,
|
|
934
962
|
conversationTotalEstimatedCostUsd,
|
|
935
|
-
logs: logs.map(normalizeLlmContextLog),
|
|
963
|
+
logs: logs.map((log) => normalizeLlmContextLog(log, view)),
|
|
936
964
|
memoryRecall: memoryRecallLog ?? null,
|
|
937
965
|
memoryV2Activation: memoryV2Activation ?? null,
|
|
938
966
|
memoryV3Selection,
|
|
@@ -944,6 +972,7 @@ async function handleGetConversationLlmContext({
|
|
|
944
972
|
}: RouteHandlerArgs) {
|
|
945
973
|
const conversationKey = queryParams.conversationKey;
|
|
946
974
|
const requestedConversationId = queryParams.conversationId;
|
|
975
|
+
const view = resolveLlmContextView(queryParams.view);
|
|
947
976
|
|
|
948
977
|
let conversationId: string | undefined = requestedConversationId;
|
|
949
978
|
if (!conversationId && conversationKey) {
|
|
@@ -1000,7 +1029,7 @@ async function handleGetConversationLlmContext({
|
|
|
1000
1029
|
conversationId: conversation.id,
|
|
1001
1030
|
conversationKind,
|
|
1002
1031
|
conversationTotalEstimatedCostUsd,
|
|
1003
|
-
logs: logs.map(normalizeLlmContextLog),
|
|
1032
|
+
logs: logs.map((log) => normalizeLlmContextLog(log, view)),
|
|
1004
1033
|
memoryRecall: null,
|
|
1005
1034
|
memoryV2Activation: null,
|
|
1006
1035
|
memoryV3Selection: null,
|
|
@@ -1034,6 +1063,21 @@ async function handleGetLlmRequestLogPayload({
|
|
|
1034
1063
|
return { id: log.id, requestPayload, responsePayload };
|
|
1035
1064
|
}
|
|
1036
1065
|
|
|
1066
|
+
async function handleGetLlmRequestLogContext({
|
|
1067
|
+
pathParams = {},
|
|
1068
|
+
}: RouteHandlerArgs) {
|
|
1069
|
+
const logId = pathParams.id;
|
|
1070
|
+
if (!logId) {
|
|
1071
|
+
throw new BadRequestError("log id is required");
|
|
1072
|
+
}
|
|
1073
|
+
const source = await getLlmRequestLogSource();
|
|
1074
|
+
const log = await source.getRequestLogById(logId);
|
|
1075
|
+
if (!log) {
|
|
1076
|
+
throw new NotFoundError("log not found");
|
|
1077
|
+
}
|
|
1078
|
+
return normalizeLlmContextLog(log);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1037
1081
|
function handleDeleteQueuedMessage({
|
|
1038
1082
|
queryParams = {},
|
|
1039
1083
|
pathParams = {},
|
|
@@ -1322,6 +1366,13 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
1322
1366
|
schema: { type: "string" },
|
|
1323
1367
|
description: "Internal conversation identifier.",
|
|
1324
1368
|
},
|
|
1369
|
+
{
|
|
1370
|
+
name: "view",
|
|
1371
|
+
required: false,
|
|
1372
|
+
schema: { type: "string", enum: ["full", "summary"] },
|
|
1373
|
+
description:
|
|
1374
|
+
"Response shape. 'summary' omits per-log request/response sections; defaults to 'full'.",
|
|
1375
|
+
},
|
|
1325
1376
|
],
|
|
1326
1377
|
responseBody: LlmContextResponseSchema,
|
|
1327
1378
|
handler: handleGetConversationLlmContext,
|
|
@@ -1338,6 +1389,15 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
1338
1389
|
description:
|
|
1339
1390
|
"Return request/response logs and memory recall data for a specific message.",
|
|
1340
1391
|
tags: ["messages"],
|
|
1392
|
+
queryParams: [
|
|
1393
|
+
{
|
|
1394
|
+
name: "view",
|
|
1395
|
+
required: false,
|
|
1396
|
+
schema: { type: "string", enum: ["full", "summary"] },
|
|
1397
|
+
description:
|
|
1398
|
+
"Response shape. 'summary' omits per-log request/response sections; defaults to 'full'.",
|
|
1399
|
+
},
|
|
1400
|
+
],
|
|
1341
1401
|
responseBody: LlmContextResponseSchema,
|
|
1342
1402
|
handler: handleGetLlmContext,
|
|
1343
1403
|
},
|
|
@@ -1360,6 +1420,21 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
1360
1420
|
}),
|
|
1361
1421
|
handler: handleGetLlmRequestLogPayload,
|
|
1362
1422
|
},
|
|
1423
|
+
{
|
|
1424
|
+
operationId: "llm_request_logs_context_get",
|
|
1425
|
+
endpoint: "llm-request-logs/:id/context",
|
|
1426
|
+
method: "GET",
|
|
1427
|
+
policy: {
|
|
1428
|
+
requiredScopes: ["chat.read"],
|
|
1429
|
+
allowedPrincipalTypes: ACTOR_PRINCIPALS,
|
|
1430
|
+
},
|
|
1431
|
+
summary: "Get normalized context for a single LLM request log",
|
|
1432
|
+
description:
|
|
1433
|
+
"Return the normalized summary and request/response sections for a specific log entry.",
|
|
1434
|
+
tags: ["messages"],
|
|
1435
|
+
responseBody: LLMRequestLogEntrySchema,
|
|
1436
|
+
handler: handleGetLlmRequestLogContext,
|
|
1437
|
+
},
|
|
1363
1438
|
{
|
|
1364
1439
|
operationId: "messages_queued_delete",
|
|
1365
1440
|
endpoint: "messages/queued/:id",
|
|
@@ -41,6 +41,7 @@ import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
|
|
|
41
41
|
import { createTarGz } from "./archive-utils.js";
|
|
42
42
|
import { InternalError } from "./errors.js";
|
|
43
43
|
import { collectWorkspaceData } from "./log-export/workspace-allowlist.js";
|
|
44
|
+
import { redactStagedExportFiles } from "./redact-staged-export.js";
|
|
44
45
|
import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
|
|
45
46
|
|
|
46
47
|
const log = getLogger("log-export-routes");
|
|
@@ -48,6 +49,22 @@ const log = getLogger("log-export-routes");
|
|
|
48
49
|
/** Maximum total payload size for log file contents (10 MB). */
|
|
49
50
|
const MAX_LOG_PAYLOAD_BYTES = 10 * 1024 * 1024;
|
|
50
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Row caps for the conversation-data table dumps (`messages.json`,
|
|
54
|
+
* `llm-request-logs.json`, `llm-usage-events.json`). Without them, a
|
|
55
|
+
* `full: true` export dumps entire tables — for long-tenured users the only
|
|
56
|
+
* realistic way to blow past the sweep's `MAX_SWEEP_FILE_BYTES` cap, which
|
|
57
|
+
* fails closed by replacing the whole file with an omission note. Bounding
|
|
58
|
+
* at the source keeps the most recent (and most useful) rows instead,
|
|
59
|
+
* mirroring the `auditLimit` pattern: order by createdAt desc, newest rows
|
|
60
|
+
* first. Truncated sections are logged and surfaced in both the export log
|
|
61
|
+
* line and `export-manifest.json` (`truncatedSections`), so a capped bundle
|
|
62
|
+
* is distinguishable from a complete one.
|
|
63
|
+
*/
|
|
64
|
+
const MAX_EXPORT_MESSAGE_ROWS = 10_000;
|
|
65
|
+
export const MAX_EXPORT_LLM_REQUEST_LOG_ROWS = 2_000;
|
|
66
|
+
const MAX_EXPORT_LLM_USAGE_EVENT_ROWS = 10_000;
|
|
67
|
+
|
|
51
68
|
interface ExportRequestBody {
|
|
52
69
|
auditLimit?: number;
|
|
53
70
|
conversationId?: string;
|
|
@@ -102,23 +119,44 @@ async function handleExport({
|
|
|
102
119
|
);
|
|
103
120
|
|
|
104
121
|
// --- Conversation data tables ---
|
|
122
|
+
// Sections truncated by a row cap, surfaced in the export log line and
|
|
123
|
+
// the export manifest.
|
|
124
|
+
const truncatedSections: string[] = [];
|
|
125
|
+
/**
|
|
126
|
+
* Keep the newest `limit` rows of a section dump. Callers fetch
|
|
127
|
+
* `limit + 1` rows so truncation is detectable without a COUNT query.
|
|
128
|
+
*/
|
|
129
|
+
const capRows = <T>(rows: T[], limit: number, section: string): T[] => {
|
|
130
|
+
if (rows.length <= limit) return rows;
|
|
131
|
+
truncatedSections.push(section);
|
|
132
|
+
log.warn(
|
|
133
|
+
{ section, limit },
|
|
134
|
+
"Export section exceeded its row cap; keeping only the most recent rows",
|
|
135
|
+
);
|
|
136
|
+
return rows.slice(0, limit);
|
|
137
|
+
};
|
|
105
138
|
if (conversationId || full) {
|
|
106
139
|
const conversationFilter = conversationId
|
|
107
140
|
? [eq(messages.conversationId, conversationId)]
|
|
108
141
|
: [];
|
|
109
142
|
|
|
110
|
-
const messageRows =
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
143
|
+
const messageRows = capRows(
|
|
144
|
+
db
|
|
145
|
+
.select()
|
|
146
|
+
.from(messages)
|
|
147
|
+
.where(
|
|
148
|
+
and(
|
|
149
|
+
...conversationFilter,
|
|
150
|
+
startTime ? gte(messages.createdAt, startTime) : undefined,
|
|
151
|
+
endTime ? lte(messages.createdAt, endTime) : undefined,
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
.orderBy(desc(messages.createdAt))
|
|
155
|
+
.limit(MAX_EXPORT_MESSAGE_ROWS + 1)
|
|
156
|
+
.all(),
|
|
157
|
+
MAX_EXPORT_MESSAGE_ROWS,
|
|
158
|
+
"messages",
|
|
159
|
+
);
|
|
122
160
|
writeFileSync(
|
|
123
161
|
join(staging, "messages.json"),
|
|
124
162
|
JSON.stringify(messageRows, null, 2),
|
|
@@ -129,18 +167,23 @@ async function handleExport({
|
|
|
129
167
|
? [eq(llmRequestLogs.conversationId, conversationId)]
|
|
130
168
|
: [];
|
|
131
169
|
|
|
132
|
-
const llmLogRows =
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
170
|
+
const llmLogRows = capRows(
|
|
171
|
+
db
|
|
172
|
+
.select()
|
|
173
|
+
.from(llmRequestLogs)
|
|
174
|
+
.where(
|
|
175
|
+
and(
|
|
176
|
+
...llmConversationFilter,
|
|
177
|
+
startTime ? gte(llmRequestLogs.createdAt, startTime) : undefined,
|
|
178
|
+
endTime ? lte(llmRequestLogs.createdAt, endTime) : undefined,
|
|
179
|
+
),
|
|
180
|
+
)
|
|
181
|
+
.orderBy(desc(llmRequestLogs.createdAt))
|
|
182
|
+
.limit(MAX_EXPORT_LLM_REQUEST_LOG_ROWS + 1)
|
|
183
|
+
.all(),
|
|
184
|
+
MAX_EXPORT_LLM_REQUEST_LOG_ROWS,
|
|
185
|
+
"llm-request-logs",
|
|
186
|
+
);
|
|
144
187
|
writeFileSync(
|
|
145
188
|
join(staging, "llm-request-logs.json"),
|
|
146
189
|
JSON.stringify(llmLogRows, null, 2),
|
|
@@ -151,18 +194,23 @@ async function handleExport({
|
|
|
151
194
|
? [eq(llmUsageEvents.conversationId, conversationId)]
|
|
152
195
|
: [];
|
|
153
196
|
|
|
154
|
-
const usageRows =
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
197
|
+
const usageRows = capRows(
|
|
198
|
+
db
|
|
199
|
+
.select()
|
|
200
|
+
.from(llmUsageEvents)
|
|
201
|
+
.where(
|
|
202
|
+
and(
|
|
203
|
+
...usageConversationFilter,
|
|
204
|
+
startTime ? gte(llmUsageEvents.createdAt, startTime) : undefined,
|
|
205
|
+
endTime ? lte(llmUsageEvents.createdAt, endTime) : undefined,
|
|
206
|
+
),
|
|
207
|
+
)
|
|
208
|
+
.orderBy(desc(llmUsageEvents.createdAt))
|
|
209
|
+
.limit(MAX_EXPORT_LLM_USAGE_EVENT_ROWS + 1)
|
|
210
|
+
.all(),
|
|
211
|
+
MAX_EXPORT_LLM_USAGE_EVENT_ROWS,
|
|
212
|
+
"llm-usage-events",
|
|
213
|
+
);
|
|
166
214
|
writeFileSync(
|
|
167
215
|
join(staging, "llm-usage-events.json"),
|
|
168
216
|
JSON.stringify(usageRows, null, 2),
|
|
@@ -335,6 +383,7 @@ async function handleExport({
|
|
|
335
383
|
commitSha: COMMIT_SHA,
|
|
336
384
|
...(startTime !== undefined ? { startTime } : {}),
|
|
337
385
|
...(endTime !== undefined ? { endTime } : {}),
|
|
386
|
+
...(truncatedSections.length > 0 ? { truncatedSections } : {}),
|
|
338
387
|
exportedAt: new Date().toISOString(),
|
|
339
388
|
};
|
|
340
389
|
writeFileSync(
|
|
@@ -343,6 +392,13 @@ async function handleExport({
|
|
|
343
392
|
"utf-8",
|
|
344
393
|
);
|
|
345
394
|
|
|
395
|
+
// --- Secret-redaction sweep over every staged text file ---
|
|
396
|
+
// Belt-and-suspenders over the structural sanitizers above: catches
|
|
397
|
+
// legacy audit rows persisted with plaintext inputs (written before
|
|
398
|
+
// write-time redaction existed) and secrets sitting in copied workspace
|
|
399
|
+
// conversation files.
|
|
400
|
+
const redactionResult = redactStagedExportFiles(staging);
|
|
401
|
+
|
|
346
402
|
log.info(
|
|
347
403
|
{
|
|
348
404
|
auditCount: auditRows.length,
|
|
@@ -353,6 +409,10 @@ async function handleExport({
|
|
|
353
409
|
full: full ?? false,
|
|
354
410
|
workspaceEntries: workspaceResult.entries.length,
|
|
355
411
|
workspaceBytes: workspaceResult.totalBytes,
|
|
412
|
+
truncatedSections,
|
|
413
|
+
redactionScanned: redactionResult.filesScanned,
|
|
414
|
+
redactionRedacted: redactionResult.filesRedacted,
|
|
415
|
+
redactionOmitted: redactionResult.filesOmitted,
|
|
356
416
|
},
|
|
357
417
|
"Export collected, creating tar.gz archive",
|
|
358
418
|
);
|
|
@@ -386,6 +446,25 @@ function redactStringValue(val: unknown): string {
|
|
|
386
446
|
return val ? "(set)" : "(empty)";
|
|
387
447
|
}
|
|
388
448
|
|
|
449
|
+
/**
|
|
450
|
+
* Narrow an unknown value to a mutable record, or undefined if not an object.
|
|
451
|
+
* Deliberately looser than `isPlainObject` (`util/object.ts`): arrays are
|
|
452
|
+
* admitted on purpose so sanitization stays fail-closed — a malformed config
|
|
453
|
+
* shape must redact more, never less.
|
|
454
|
+
*/
|
|
455
|
+
function asRecord(v: unknown): Record<string, unknown> | undefined {
|
|
456
|
+
return v && typeof v === "object"
|
|
457
|
+
? (v as Record<string, unknown>)
|
|
458
|
+
: undefined;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** Replace every value in a map with its (set)/(empty) presence flag. */
|
|
462
|
+
function redactValueMap(obj: Record<string, unknown>): Record<string, string> {
|
|
463
|
+
return Object.fromEntries(
|
|
464
|
+
Object.keys(obj).map((k) => [k, redactStringValue(obj[k])]),
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
389
468
|
function readSanitizedConfig(): Record<string, unknown> | undefined {
|
|
390
469
|
const configPath = getWorkspaceConfigPath();
|
|
391
470
|
if (!existsSync(configPath)) return undefined;
|
|
@@ -396,71 +475,39 @@ function readSanitizedConfig(): Record<string, unknown> | undefined {
|
|
|
396
475
|
|
|
397
476
|
delete config.apiKeys;
|
|
398
477
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
if (ingress.webhook && typeof ingress.webhook === "object") {
|
|
402
|
-
const webhook = ingress.webhook as Record<string, unknown>;
|
|
403
|
-
webhook.secret = redactStringValue(webhook.secret);
|
|
404
|
-
ingress.webhook = webhook;
|
|
405
|
-
}
|
|
406
|
-
config.ingress = ingress;
|
|
407
|
-
}
|
|
478
|
+
const webhook = asRecord(asRecord(config.ingress)?.webhook);
|
|
479
|
+
if (webhook) webhook.secret = redactStringValue(webhook.secret);
|
|
408
480
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
const e = entry as Record<string, unknown>;
|
|
417
|
-
if ("apiKey" in e) e.apiKey = redactStringValue(e.apiKey);
|
|
418
|
-
if (e.env && typeof e.env === "object") {
|
|
419
|
-
const env = e.env as Record<string, unknown>;
|
|
420
|
-
e.env = Object.fromEntries(
|
|
421
|
-
Object.keys(env).map((k) => [k, redactStringValue(env[k])]),
|
|
422
|
-
);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
}
|
|
481
|
+
const skillEntries = asRecord(asRecord(config.skills)?.entries);
|
|
482
|
+
for (const entry of Object.values(skillEntries ?? {})) {
|
|
483
|
+
const e = asRecord(entry);
|
|
484
|
+
if (!e) continue;
|
|
485
|
+
if ("apiKey" in e) e.apiKey = redactStringValue(e.apiKey);
|
|
486
|
+
const env = asRecord(e.env);
|
|
487
|
+
if (env) e.env = redactValueMap(env);
|
|
427
488
|
}
|
|
428
489
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
490
|
+
const twilio = asRecord(config.twilio);
|
|
491
|
+
if (twilio) twilio.accountSid = redactStringValue(twilio.accountSid);
|
|
492
|
+
|
|
493
|
+
const acpAgents = asRecord(asRecord(config.acp)?.agents);
|
|
494
|
+
for (const agent of Object.values(acpAgents ?? {})) {
|
|
495
|
+
const a = asRecord(agent);
|
|
496
|
+
if (!a) continue;
|
|
497
|
+
// Agent env is an arbitrary user-supplied map (often API keys);
|
|
498
|
+
// redact every value and keep only the key names.
|
|
499
|
+
const env = asRecord(a.env);
|
|
500
|
+
if (env) a.env = redactValueMap(env);
|
|
433
501
|
}
|
|
434
502
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
if (s.transport && typeof s.transport === "object") {
|
|
444
|
-
const transport = s.transport as Record<string, unknown>;
|
|
445
|
-
if (transport.headers && typeof transport.headers === "object") {
|
|
446
|
-
const headers = transport.headers as Record<string, unknown>;
|
|
447
|
-
transport.headers = Object.fromEntries(
|
|
448
|
-
Object.keys(headers).map((k) => [
|
|
449
|
-
k,
|
|
450
|
-
redactStringValue(headers[k]),
|
|
451
|
-
]),
|
|
452
|
-
);
|
|
453
|
-
}
|
|
454
|
-
if (transport.env && typeof transport.env === "object") {
|
|
455
|
-
const env = transport.env as Record<string, unknown>;
|
|
456
|
-
transport.env = Object.fromEntries(
|
|
457
|
-
Object.keys(env).map((k) => [k, redactStringValue(env[k])]),
|
|
458
|
-
);
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
}
|
|
503
|
+
const mcpServers = asRecord(asRecord(config.mcp)?.servers);
|
|
504
|
+
for (const server of Object.values(mcpServers ?? {})) {
|
|
505
|
+
const transport = asRecord(asRecord(server)?.transport);
|
|
506
|
+
if (!transport) continue;
|
|
507
|
+
const headers = asRecord(transport.headers);
|
|
508
|
+
if (headers) transport.headers = redactValueMap(headers);
|
|
509
|
+
const env = asRecord(transport.env);
|
|
510
|
+
if (env) transport.env = redactValueMap(env);
|
|
464
511
|
}
|
|
465
512
|
|
|
466
513
|
return config;
|