@agent-native/core 0.111.3 → 0.112.0

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 (109) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +18 -0
  3. package/corpus/core/package.json +3 -1
  4. package/corpus/core/src/cli/code-agent-executor.ts +17 -5
  5. package/corpus/core/src/cli/code-agent-runs.ts +43 -2
  6. package/corpus/core/src/cli/migration-codemod.ts +59 -9
  7. package/corpus/core/src/cli/multi-frontier-runs.ts +841 -0
  8. package/corpus/core/src/coding-tools/index.ts +27 -8
  9. package/corpus/core/src/embeddings/index.ts +233 -0
  10. package/corpus/core/src/index.ts +2 -0
  11. package/corpus/core/src/search/index.ts +413 -0
  12. package/corpus/templates/brain/.agents/skills/brain/RUNBOOK.md +48 -5
  13. package/corpus/templates/brain/.agents/skills/brain/SKILL.md +46 -14
  14. package/corpus/templates/brain/README.md +34 -3
  15. package/corpus/templates/brain/actions/approve-proposal.ts +2 -0
  16. package/corpus/templates/brain/actions/claim-distillation.ts +24 -27
  17. package/corpus/templates/brain/actions/create-source.ts +1 -1
  18. package/corpus/templates/brain/actions/get-knowledge.ts +6 -2
  19. package/corpus/templates/brain/actions/get-pilot-report.ts +44 -8
  20. package/corpus/templates/brain/actions/import-capture.ts +21 -9
  21. package/corpus/templates/brain/actions/import-transcript.ts +42 -14
  22. package/corpus/templates/brain/actions/list-captures.ts +10 -1
  23. package/corpus/templates/brain/actions/list-distillation-queue.ts +39 -14
  24. package/corpus/templates/brain/actions/list-projects.ts +51 -0
  25. package/corpus/templates/brain/actions/list-proposals.ts +80 -4
  26. package/corpus/templates/brain/actions/list-sources.ts +17 -3
  27. package/corpus/templates/brain/actions/manage-project.ts +118 -0
  28. package/corpus/templates/brain/actions/mark-capture-distilled.ts +68 -20
  29. package/corpus/templates/brain/actions/navigate.ts +1 -0
  30. package/corpus/templates/brain/actions/reject-proposal.ts +6 -1
  31. package/corpus/templates/brain/actions/resanitize-captures.ts +159 -33
  32. package/corpus/templates/brain/actions/review-proposal.ts +2 -0
  33. package/corpus/templates/brain/actions/search-everything.ts +10 -1
  34. package/corpus/templates/brain/actions/search-knowledge.ts +1 -1
  35. package/corpus/templates/brain/actions/set-settings.ts +36 -1
  36. package/corpus/templates/brain/actions/update-proposal.ts +2 -0
  37. package/corpus/templates/brain/app/hooks/use-distillation-bridge.ts +18 -9
  38. package/corpus/templates/brain/app/hooks/use-navigation-state.ts +3 -0
  39. package/corpus/templates/brain/app/i18n-data.ts +731 -2
  40. package/corpus/templates/brain/app/lib/brain.ts +33 -1
  41. package/corpus/templates/brain/app/routes/ops.tsx +47 -0
  42. package/corpus/templates/brain/app/routes/review.tsx +41 -2
  43. package/corpus/templates/brain/app/routes/search.tsx +33 -2
  44. package/corpus/templates/brain/app/routes/settings.tsx +158 -0
  45. package/corpus/templates/brain/app/routes/sources.tsx +31 -0
  46. package/corpus/templates/brain/changelog/2026-07-19-added-private-source-safe-semantic-search.md +6 -0
  47. package/corpus/templates/brain/jobs/process-ingest-queue.ts +270 -36
  48. package/corpus/templates/brain/package.json +1 -0
  49. package/corpus/templates/brain/server/db/index.ts +18 -0
  50. package/corpus/templates/brain/server/db/schema.ts +197 -1
  51. package/corpus/templates/brain/server/jobs/distillation-queue.ts +12 -5
  52. package/corpus/templates/brain/server/lib/audiences.ts +708 -0
  53. package/corpus/templates/brain/server/lib/brain-health.ts +251 -38
  54. package/corpus/templates/brain/server/lib/brain.ts +877 -50
  55. package/corpus/templates/brain/server/lib/capture-sanitization.ts +217 -53
  56. package/corpus/templates/brain/server/lib/connectors.ts +556 -70
  57. package/corpus/templates/brain/server/lib/demo.ts +17 -4
  58. package/corpus/templates/brain/server/lib/hybrid-search.ts +363 -0
  59. package/corpus/templates/brain/server/lib/ingest-queue.ts +104 -0
  60. package/corpus/templates/brain/server/lib/privacy-readiness.ts +24 -0
  61. package/corpus/templates/brain/server/lib/search-index-contracts.ts +72 -0
  62. package/corpus/templates/brain/server/lib/search-index.ts +714 -0
  63. package/corpus/templates/brain/server/lib/search.ts +120 -4
  64. package/corpus/templates/brain/server/lib/sensitivity-policy.ts +174 -0
  65. package/corpus/templates/brain/server/lib/slack-events.ts +281 -0
  66. package/corpus/templates/brain/server/onboarding.ts +29 -0
  67. package/corpus/templates/brain/server/plugins/db.ts +204 -0
  68. package/corpus/templates/brain/server/register-secrets.ts +46 -0
  69. package/corpus/templates/brain/server/routes/api/_agent-native/brain/ingest.post.ts +36 -16
  70. package/corpus/templates/brain/server/routes/api/_agent-native/brain/slack-events.post.ts +28 -0
  71. package/corpus/templates/brain/shared/types.ts +10 -0
  72. package/dist/cli/code-agent-executor.d.ts +8 -9
  73. package/dist/cli/code-agent-executor.d.ts.map +1 -1
  74. package/dist/cli/code-agent-executor.js +9 -3
  75. package/dist/cli/code-agent-executor.js.map +1 -1
  76. package/dist/cli/code-agent-runs.d.ts +5 -0
  77. package/dist/cli/code-agent-runs.d.ts.map +1 -1
  78. package/dist/cli/code-agent-runs.js +33 -2
  79. package/dist/cli/code-agent-runs.js.map +1 -1
  80. package/dist/cli/migration-codemod.d.ts.map +1 -1
  81. package/dist/cli/migration-codemod.js +48 -10
  82. package/dist/cli/migration-codemod.js.map +1 -1
  83. package/dist/cli/multi-frontier-runs.d.ts +131 -0
  84. package/dist/cli/multi-frontier-runs.d.ts.map +1 -0
  85. package/dist/cli/multi-frontier-runs.js +529 -0
  86. package/dist/cli/multi-frontier-runs.js.map +1 -0
  87. package/dist/coding-tools/index.d.ts +1 -0
  88. package/dist/coding-tools/index.d.ts.map +1 -1
  89. package/dist/coding-tools/index.js +25 -7
  90. package/dist/coding-tools/index.js.map +1 -1
  91. package/dist/collab/awareness.d.ts +2 -2
  92. package/dist/collab/awareness.d.ts.map +1 -1
  93. package/dist/embeddings/index.d.ts +24 -0
  94. package/dist/embeddings/index.d.ts.map +1 -0
  95. package/dist/embeddings/index.js +172 -0
  96. package/dist/embeddings/index.js.map +1 -0
  97. package/dist/index.d.ts +2 -0
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/index.js +2 -0
  100. package/dist/index.js.map +1 -1
  101. package/dist/notifications/routes.d.ts +3 -3
  102. package/dist/progress/routes.d.ts +1 -1
  103. package/dist/resources/handlers.d.ts +1 -1
  104. package/dist/search/index.d.ts +88 -0
  105. package/dist/search/index.d.ts.map +1 -0
  106. package/dist/search/index.js +267 -0
  107. package/dist/search/index.js.map +1 -0
  108. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  109. package/package.json +3 -1
@@ -1,22 +1,38 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
 
3
+ import { z } from "zod";
4
+
3
5
  import type {
4
6
  BrainCaptureKind,
5
7
  BrainSettings,
6
8
  BrainSourceProvider,
7
9
  } from "../../shared/types.js";
10
+ import {
11
+ BRAIN_SENSITIVITY_POLICY_VERSION,
12
+ type BrainSensitivityDecision,
13
+ } from "./search-index-contracts.js";
14
+ import {
15
+ classifierDecisionSchema,
16
+ deterministicQuarantineDecision,
17
+ fallbackSensitivityDecision,
18
+ MAX_CLASSIFIER_OUTPUT_CHARS,
19
+ screenSensitivityDeterministically,
20
+ } from "./sensitivity-policy.js";
8
21
 
9
22
  const DEFAULT_SANITIZATION_OUTPUT =
10
23
  "No company-relevant content retained from this capture.";
11
24
  const DEFAULT_MAX_MODEL_INPUT_CHARS = 120_000;
12
25
  const AGENTS_CONTEXT_MAX_CHARS = 24_000;
13
26
  const MODEL_TIMEOUT_MS = 45_000;
27
+ const QUARANTINED_CAPTURE_OUTPUT =
28
+ "Capture quarantined by Brain privacy policy; no content retained for storage.";
14
29
 
15
30
  const RAW_METADATA_KEYS = new Set([
16
31
  "raw",
17
32
  "segments",
18
33
  "transcript",
19
34
  "transcriptSegments",
35
+ "safeSegments",
20
36
  "messages",
21
37
  "utterances",
22
38
  "words",
@@ -39,7 +55,7 @@ const COMPANY_SIGNAL =
39
55
  /\b(action|annual|answer|answers|api|app|architecture|beta|billing|blocked|blocker|brain|builder|bug|citation|citations|cited|clip|clips|company|contract|customer|data|decision|demo|design|docs|enterprise|evidence|experiment|feature|feedback|freemium|fusion|github|go[- ]?to[- ]?market|gtm|implementation|incident|integration|issue|knowledge|launch|metric|migration|model|plan|plans|pricing|process|procurement|product|project|proposal|raw capture|retrieval|roadmap|risk|sales|security|ship|slack|source policy|superseded|support|tauri|template|timeline|workflow|workspace)\b/i;
40
56
 
41
57
  const PERSONAL_SIGNAL =
42
- /\b(birthday|child|children|commute|current role|doctor|exit timeline|family|global experience|grew revenue|home address|husband|kid|kids|key traits|medical|partner|previous:|rebuilt sales teams|salary|sales transition|software experience since|spouse|ssn|vacation|wife)\b/i;
58
+ /\b(birthday|child|children|commute|current role|doctor|exit timeline|family|global experience|grew revenue|home address|husband|kid|kids|key traits|medical (?:condition|diagnosis|leave|record|accommodation)|partner|previous:|rebuilt sales teams|salary|sales transition|software experience since|spouse|ssn|vacation|wife)\b/i;
43
59
 
44
60
  const RECRUITING_SIGNAL =
45
61
  /\b(applicant|big company experience|candidate|candidate pipeline|candidate screen|commercial sales background|comp plan|cro search|current president|cv|headcount|hire|hiring|interview|interviewing|job search|offer|outbound candidate|pedigree|personnel change|president role|product background wants to focus|recruit|recruited|recruiter|recruiting|reference check|resume|résumé|sales leader|search firm|set up slack channel|shortlist|slack channel preferred over email|slack connection details|slate|sourcing|talent|vp of sales)\b/i;
@@ -64,6 +80,7 @@ export interface CaptureSanitizationResult {
64
80
  title: string;
65
81
  content: string;
66
82
  metadata: Record<string, unknown>;
83
+ decision?: BrainSensitivityDecision;
67
84
  }
68
85
 
69
86
  function booleanSetting(value: unknown): boolean | undefined {
@@ -94,6 +111,7 @@ function numberSetting(value: unknown): number | undefined {
94
111
  export function shouldSanitizeCaptureBeforeStorage(
95
112
  input: CaptureSanitizationInput,
96
113
  ): boolean {
114
+ if (input.source.provider === "slack") return true;
97
115
  if (input.settings.captureSanitizationEnabled === false) return false;
98
116
 
99
117
  const metadataOverride = booleanSetting(
@@ -115,7 +133,9 @@ function sanitizeSensitiveText(value: string): string {
115
133
  .replace(/<@[UW][A-Z0-9]+(?:\|[^>]+)?>/g, "[redacted]")
116
134
  .replace(/\bU[A-Z0-9]{8,}\b/g, "[redacted]")
117
135
  .replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, "[redacted]")
118
- .replace(/(?:\+?\d|\(\d{2,4}\))[\d\s().-]{6,}\d/g, "[redacted]")
136
+ .replace(/(?:\+?\d|\(\d{2,4}\))[\d\s().-]{6,}\d/g, (candidate) =>
137
+ /^\d{4}-\d{2}-\d{2}$/.test(candidate) ? candidate : "[redacted]",
138
+ )
119
139
  .replace(
120
140
  /\b(?:sk|pk|rk|ghp|gho|ghu|github_pat)_[A-Za-z0-9_=-]{16,}\b/g,
121
141
  "[redacted]",
@@ -224,12 +244,59 @@ function sanitizeMetadata(metadata: Record<string, unknown>): {
224
244
  Object.assign(next, summarizeDroppedMetadata(key, value));
225
245
  continue;
226
246
  }
247
+ if (
248
+ typeof value === "string" &&
249
+ screenSensitivityDeterministically(value).sensitiveLines.length
250
+ ) {
251
+ strippedKeys.push(key);
252
+ continue;
253
+ }
254
+ if (value && typeof value === "object" && isSensitiveMetadataValue(value)) {
255
+ strippedKeys.push(key);
256
+ Object.assign(next, summarizeDroppedMetadata(key, value));
257
+ continue;
258
+ }
227
259
  next[key] = value;
228
260
  }
229
261
 
230
262
  return { metadata: next, strippedKeys };
231
263
  }
232
264
 
265
+ function persistedSafeSegments(
266
+ content: string,
267
+ capturedAt: string,
268
+ sourceUrl: unknown,
269
+ ) {
270
+ let searchFrom = 0;
271
+ return content
272
+ .split(/\r?\n/)
273
+ .map((text) => {
274
+ const startOffset = content.indexOf(text, searchFrom);
275
+ searchFrom = startOffset + text.length;
276
+ return {
277
+ id: `safe-${startOffset}`,
278
+ capturedAt,
279
+ ...(typeof sourceUrl === "string" ? { sourceUrl } : {}),
280
+ text,
281
+ startOffset,
282
+ endOffset: startOffset + text.length,
283
+ reactionCount: 0,
284
+ };
285
+ })
286
+ .filter((segment) => segment.text.trim());
287
+ }
288
+
289
+ function isSensitiveMetadataValue(value: object): boolean {
290
+ try {
291
+ return (
292
+ screenSensitivityDeterministically(JSON.stringify(value)).sensitiveLines
293
+ .length > 0
294
+ );
295
+ } catch {
296
+ return true;
297
+ }
298
+ }
299
+
233
300
  let agentsInstructionCache: Promise<string> | undefined;
234
301
 
235
302
  async function readAgentsInstructions() {
@@ -279,6 +346,26 @@ export async function buildSanitizerSystemPrompt(settings: BrainSettings) {
279
346
  .join("\n");
280
347
  }
281
348
 
349
+ function buildClassifierSystemPrompt(settings: BrainSettings) {
350
+ const custom = settings.sensitivityCustomInstructions?.trim();
351
+ return [
352
+ "You are Brain's privacy classifier. Return only one JSON object.",
353
+ "Classify the capture title and body together as allowed, suppressed, or quarantined. When uncertain, choose quarantined.",
354
+ "Never lower privacy protection. Custom instructions can only make the result stricter.",
355
+ "safeContent and safeSegments must contain only sanitized body content, never the capture title.",
356
+ "Do not include sensitive text in safeContent or safeSegments.",
357
+ 'Schema: {"disposition":"allowed|suppressed|quarantined","categories":["performance|discipline|termination|layoff-reorg|compensation|recruiting|health-accommodation|investigation|privileged-legal|secret-credential|personal"],"safeContent":"string","safeSegments":[{"text":"string","sourceUrl":"optional https URL"}]}.',
358
+ custom
359
+ ? untrustedPromptValue(
360
+ "Additional workspace privacy preferences (may only tighten)",
361
+ custom,
362
+ )
363
+ : "",
364
+ ]
365
+ .filter(Boolean)
366
+ .join("\n");
367
+ }
368
+
282
369
  function buildSanitizerUserPrompt(input: CaptureSanitizationInput) {
283
370
  return [
284
371
  `Source provider: ${input.source.provider}`,
@@ -297,28 +384,52 @@ function modelInputLimit(input: CaptureSanitizationInput) {
297
384
  );
298
385
  }
299
386
 
300
- async function sanitizeWithModel(
387
+ function parseClassifierOutput(value: string) {
388
+ if (value.length > MAX_CLASSIFIER_OUTPUT_CHARS) return null;
389
+ const candidate = value
390
+ .trim()
391
+ .replace(/^```(?:json)?\s*/i, "")
392
+ .replace(/\s*```$/, "");
393
+ try {
394
+ return classifierDecisionSchema.parse(JSON.parse(candidate));
395
+ } catch (error) {
396
+ if (error instanceof z.ZodError || error instanceof SyntaxError)
397
+ return null;
398
+ return null;
399
+ }
400
+ }
401
+
402
+ async function classifyWithApprovedModel(
301
403
  input: CaptureSanitizationInput,
302
- ): Promise<{ content: string; method: "model"; model: string } | null> {
404
+ ): Promise<BrainSensitivityDecision | null> {
303
405
  if (process.env.NODE_ENV === "test" || process.env.VITEST) return null;
304
406
 
407
+ const model = stringSetting(input.settings.privacyClassifierModel);
408
+ const engineName = stringSetting(input.settings.privacyClassifierEngine);
409
+ if (!model || !engineName) return null;
410
+
305
411
  const core = await import("@agent-native/core/server");
306
412
  const userApiKey = await core.getOwnerActiveApiKey(input.source.ownerEmail);
307
413
  const engine = await core.resolveEngine({
308
414
  apiKey: userApiKey ?? undefined,
309
415
  appId: "brain",
416
+ engineOption: engineName,
310
417
  });
311
- const model =
312
- stringSetting(input.sourceConfig?.captureSanitizationModel) ??
313
- input.settings.captureSanitizationModel?.trim() ??
314
- (await core.getStoredModelForEngine(engine, { appId: "brain" })) ??
315
- engine.defaultModel;
316
-
317
- const maxChars = modelInputLimit(input);
318
- const promptInput =
319
- input.content.length > maxChars
320
- ? { ...input, content: input.content.slice(0, maxChars) }
321
- : input;
418
+
419
+ const maxChars = Math.min(
420
+ modelInputLimit(input),
421
+ DEFAULT_MAX_MODEL_INPUT_CHARS,
422
+ );
423
+ const screenedTitle = screenSensitivityDeterministically(input.title);
424
+ const screenedContent = screenSensitivityDeterministically(input.content);
425
+ const safeInput = [
426
+ `Capture title: ${sanitizeSensitiveText(screenedTitle.safeLines.join(" "))}`,
427
+ "Capture body:",
428
+ screenedContent.safeLines.join("\n"),
429
+ ]
430
+ .join("\n")
431
+ .slice(0, maxChars);
432
+ if (!safeInput) return null;
322
433
  const controller = new AbortController();
323
434
  const timeout = setTimeout(() => controller.abort(), MODEL_TIMEOUT_MS);
324
435
  let streamed = "";
@@ -327,13 +438,11 @@ async function sanitizeWithModel(
327
438
  try {
328
439
  for await (const event of engine.stream({
329
440
  model,
330
- systemPrompt: await buildSanitizerSystemPrompt(input.settings),
441
+ systemPrompt: buildClassifierSystemPrompt(input.settings),
331
442
  messages: [
332
443
  {
333
444
  role: "user",
334
- content: [
335
- { type: "text", text: buildSanitizerUserPrompt(promptInput) },
336
- ],
445
+ content: [{ type: "text", text: safeInput }],
337
446
  },
338
447
  ],
339
448
  tools: [],
@@ -358,12 +467,30 @@ async function sanitizeWithModel(
358
467
  }
359
468
 
360
469
  if (terminalError) throw new Error(terminalError);
361
- const content = (finalText || streamed).trim();
362
- if (!content) return null;
470
+ const parsed = parseClassifierOutput(finalText || streamed);
471
+ if (!parsed) return null;
472
+ const rescreened = deterministicQuarantineDecision(
473
+ [
474
+ parsed.safeContent,
475
+ ...parsed.safeSegments.map((segment) => segment.text),
476
+ ].join("\n"),
477
+ input.capturedAt ?? new Date(0).toISOString(),
478
+ );
479
+ if (rescreened) return rescreened;
363
480
  return {
364
- content: sanitizeSensitiveText(content),
365
- method: "model",
366
- model,
481
+ disposition: parsed.categories.length ? "quarantined" : parsed.disposition,
482
+ categories: parsed.categories,
483
+ confidenceBand: "high",
484
+ policyVersion: BRAIN_SENSITIVITY_POLICY_VERSION,
485
+ safeContent: sanitizeSensitiveText(parsed.safeContent),
486
+ safeSegments: parsed.safeSegments.map((segment, index) => ({
487
+ id: `model-safe-${index + 1}`,
488
+ capturedAt: input.capturedAt ?? new Date(0).toISOString(),
489
+ sourceUrl: segment.sourceUrl,
490
+ text: sanitizeSensitiveText(segment.text),
491
+ reactionCount: 0,
492
+ })),
493
+ classifier: "approved-model",
367
494
  };
368
495
  }
369
496
 
@@ -371,56 +498,93 @@ export async function sanitizeCaptureForStorage(
371
498
  input: CaptureSanitizationInput,
372
499
  ): Promise<CaptureSanitizationResult> {
373
500
  const metadata = input.metadata ?? {};
374
- if (!shouldSanitizeCaptureBeforeStorage(input)) {
375
- return {
376
- title: input.title,
377
- content: input.content,
378
- metadata,
379
- };
380
- }
381
-
382
501
  const { metadata: sanitizedMetadata, strippedKeys } =
383
502
  sanitizeMetadata(metadata);
384
- let content: string;
385
- let method: "model" | "deterministic" = "deterministic";
386
- let model: string | undefined;
503
+ const capturedAt = input.capturedAt ?? new Date(0).toISOString();
504
+ const titleDecision = fallbackSensitivityDecision(input.title, capturedAt);
505
+ let decision =
506
+ deterministicQuarantineDecision(input.title, capturedAt) ??
507
+ (titleDecision.disposition === "allowed" ? null : titleDecision) ??
508
+ deterministicQuarantineDecision(input.content, capturedAt);
387
509
  let fallbackReason: string | undefined;
388
-
510
+ const classifierConfigured = Boolean(
511
+ stringSetting(input.settings.privacyClassifierModel) &&
512
+ stringSetting(input.settings.privacyClassifierEngine),
513
+ );
514
+ let classifierFailed = false;
389
515
  try {
390
- const modelResult = await sanitizeWithModel(input);
391
- if (modelResult) {
392
- content = modelResult.content;
393
- method = modelResult.method;
394
- model = modelResult.model;
395
- } else {
396
- content = deterministicSanitizeContent(input.content, {
397
- dropTranscriptLines: Boolean(input.metadata?.captureSanitization),
398
- });
516
+ decision ??= await classifyWithApprovedModel(input);
517
+ if (!decision) {
518
+ classifierFailed = classifierConfigured;
519
+ fallbackReason = classifierConfigured
520
+ ? "classifier-malformed"
521
+ : "classifier-not-approved-or-malformed";
399
522
  }
400
523
  } catch {
401
- content = deterministicSanitizeContent(input.content, {
402
- dropTranscriptLines: Boolean(input.metadata?.captureSanitization),
403
- });
524
+ classifierFailed = classifierConfigured;
404
525
  fallbackReason = "model-unavailable";
405
526
  }
406
-
407
- const sanitizedContent = content.trim() || DEFAULT_SANITIZATION_OUTPUT;
527
+ decision ??= fallbackSensitivityDecision(input.content, capturedAt);
528
+ if (classifierFailed) {
529
+ decision = {
530
+ ...decision,
531
+ disposition: "quarantined",
532
+ confidenceBand: "uncertain",
533
+ };
534
+ }
535
+ const sanitizationRequested = shouldSanitizeCaptureBeforeStorage(input);
536
+
537
+ const sanitizedContent =
538
+ decision.disposition === "allowed"
539
+ ? sanitizationRequested
540
+ ? deterministicSanitizeContent(
541
+ sanitizeSensitiveText(decision.safeContent),
542
+ {
543
+ dropTranscriptLines: Boolean(input.metadata?.captureSanitization),
544
+ },
545
+ )
546
+ : sanitizeSensitiveText(decision.safeContent)
547
+ : QUARANTINED_CAPTURE_OUTPUT;
548
+ const safeSegmentMetadata =
549
+ decision.disposition === "allowed" && input.source.provider === "slack"
550
+ ? {
551
+ safeSegments: persistedSafeSegments(
552
+ sanitizedContent,
553
+ capturedAt,
554
+ sanitizedMetadata.sourceUrl,
555
+ ),
556
+ }
557
+ : {};
408
558
  return {
409
- title: safeTranscriptTitle(input),
559
+ title:
560
+ decision.disposition === "allowed"
561
+ ? sanitizeSensitiveText(
562
+ sanitizationRequested ? safeTranscriptTitle(input) : input.title,
563
+ )
564
+ : safeTranscriptTitle(input),
410
565
  content: sanitizedContent,
411
566
  metadata: {
412
567
  ...sanitizedMetadata,
568
+ ...safeSegmentMetadata,
413
569
  captureSanitization: {
414
570
  sanitizedBeforeStorage: true,
415
571
  rawContentRetained: false,
416
- method,
417
- model,
572
+ method: decision.classifier,
573
+ classifierModel:
574
+ decision.classifier === "approved-model"
575
+ ? input.settings.privacyClassifierModel
576
+ : undefined,
418
577
  fallbackReason,
578
+ disposition: decision.disposition,
579
+ categories: decision.categories,
580
+ confidenceBand: decision.confidenceBand,
581
+ policyVersion: decision.policyVersion,
419
582
  strippedMetadataKeys: strippedKeys,
420
583
  originalContentLength: input.content.length,
421
584
  sanitizedContentLength: sanitizedContent.length,
422
585
  sanitizedAt: new Date().toISOString(),
423
586
  },
424
587
  },
588
+ decision,
425
589
  };
426
590
  }