@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.
Files changed (35) hide show
  1. package/openapi.yaml +507 -0
  2. package/package.json +1 -1
  3. package/src/__tests__/llm-catalog-parity.test.ts +16 -0
  4. package/src/__tests__/log-export-workspace.test.ts +468 -3
  5. package/src/__tests__/secret-fixtures.ts +20 -0
  6. package/src/__tests__/tool-approval-handler.test.ts +85 -0
  7. package/src/__tests__/tool-audit-listener.test.ts +86 -0
  8. package/src/__tests__/workspace-migration-100-upgrade-quality-profile-to-fable-5.test.ts +174 -0
  9. package/src/__tests__/workspace-migration-101-upgrade-balanced-economy-to-minimax-m3.test.ts +162 -0
  10. package/src/acp/__tests__/agent-process.test.ts +315 -2
  11. package/src/acp/__tests__/prepare-agent-env.test.ts +79 -5
  12. package/src/acp/agent-process.ts +163 -34
  13. package/src/acp/prepare-agent-env.ts +55 -15
  14. package/src/bundler/app-compiler.ts +8 -0
  15. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +10 -4
  16. package/src/cli/lib/upgrade-plugin.ts +13 -7
  17. package/src/config/seed-inference-profiles.ts +4 -8
  18. package/src/events/tool-audit-listener.ts +40 -9
  19. package/src/providers/__tests__/unparseable-tool-args.test.ts +53 -0
  20. package/src/providers/model-catalog.ts +28 -0
  21. package/src/providers/model-intents.ts +1 -1
  22. package/src/providers/openai/chat-completions-provider.ts +2 -1
  23. package/src/providers/openai/responses-provider.ts +2 -1
  24. package/src/providers/unparseable-tool-args.ts +56 -0
  25. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +132 -0
  26. package/src/runtime/routes/__tests__/plugins-routes.test.ts +347 -0
  27. package/src/runtime/routes/conversation-query-routes.ts +79 -4
  28. package/src/runtime/routes/log-export-routes.ts +143 -96
  29. package/src/runtime/routes/plugins-routes.ts +359 -0
  30. package/src/runtime/routes/redact-staged-export.ts +259 -0
  31. package/src/security/redact-json.ts +61 -0
  32. package/src/tools/tool-approval-handler.ts +31 -0
  33. package/src/workspace/migrations/100-upgrade-quality-profile-to-fable-5.ts +86 -0
  34. package/src/workspace/migrations/101-upgrade-balanced-economy-to-minimax-m3.ts +70 -0
  35. 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
- function normalizeLlmContextLog(log: LogRow): LlmContextRouteResult & {
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({ pathParams = {} }: RouteHandlerArgs) {
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 = db
111
- .select()
112
- .from(messages)
113
- .where(
114
- and(
115
- ...conversationFilter,
116
- startTime ? gte(messages.createdAt, startTime) : undefined,
117
- endTime ? lte(messages.createdAt, endTime) : undefined,
118
- ),
119
- )
120
- .orderBy(messages.createdAt)
121
- .all();
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 = db
133
- .select()
134
- .from(llmRequestLogs)
135
- .where(
136
- and(
137
- ...llmConversationFilter,
138
- startTime ? gte(llmRequestLogs.createdAt, startTime) : undefined,
139
- endTime ? lte(llmRequestLogs.createdAt, endTime) : undefined,
140
- ),
141
- )
142
- .orderBy(llmRequestLogs.createdAt)
143
- .all();
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 = db
155
- .select()
156
- .from(llmUsageEvents)
157
- .where(
158
- and(
159
- ...usageConversationFilter,
160
- startTime ? gte(llmUsageEvents.createdAt, startTime) : undefined,
161
- endTime ? lte(llmUsageEvents.createdAt, endTime) : undefined,
162
- ),
163
- )
164
- .orderBy(llmUsageEvents.createdAt)
165
- .all();
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
- if (config.ingress && typeof config.ingress === "object") {
400
- const ingress = config.ingress as Record<string, unknown>;
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
- if (config.skills && typeof config.skills === "object") {
410
- const skills = config.skills as Record<string, unknown>;
411
- if (skills.entries && typeof skills.entries === "object") {
412
- const entries = skills.entries as Record<string, unknown>;
413
- for (const name of Object.keys(entries)) {
414
- const entry = entries[name];
415
- if (entry && typeof entry === "object") {
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
- if (config.twilio && typeof config.twilio === "object") {
430
- const twilio = config.twilio as Record<string, unknown>;
431
- twilio.accountSid = redactStringValue(twilio.accountSid);
432
- config.twilio = twilio;
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
- if (config.mcp && typeof config.mcp === "object") {
436
- const mcp = config.mcp as Record<string, unknown>;
437
- if (mcp.servers && typeof mcp.servers === "object") {
438
- const servers = mcp.servers as Record<string, unknown>;
439
- for (const name of Object.keys(servers)) {
440
- const server = servers[name];
441
- if (server && typeof server === "object") {
442
- const s = server as Record<string, unknown>;
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;