@agent-native/core 0.111.4 → 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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +12 -0
- package/corpus/core/package.json +3 -1
- package/corpus/core/src/cli/code-agent-executor.ts +17 -5
- package/corpus/core/src/cli/code-agent-runs.ts +43 -2
- package/corpus/core/src/cli/multi-frontier-runs.ts +841 -0
- package/corpus/core/src/coding-tools/index.ts +27 -8
- package/corpus/core/src/embeddings/index.ts +233 -0
- package/corpus/core/src/index.ts +2 -0
- package/corpus/core/src/search/index.ts +413 -0
- package/corpus/templates/brain/.agents/skills/brain/RUNBOOK.md +48 -5
- package/corpus/templates/brain/.agents/skills/brain/SKILL.md +46 -14
- package/corpus/templates/brain/README.md +34 -3
- package/corpus/templates/brain/actions/approve-proposal.ts +2 -0
- package/corpus/templates/brain/actions/claim-distillation.ts +24 -27
- package/corpus/templates/brain/actions/create-source.ts +1 -1
- package/corpus/templates/brain/actions/get-knowledge.ts +6 -2
- package/corpus/templates/brain/actions/get-pilot-report.ts +44 -8
- package/corpus/templates/brain/actions/import-capture.ts +21 -9
- package/corpus/templates/brain/actions/import-transcript.ts +42 -14
- package/corpus/templates/brain/actions/list-captures.ts +10 -1
- package/corpus/templates/brain/actions/list-distillation-queue.ts +39 -14
- package/corpus/templates/brain/actions/list-projects.ts +51 -0
- package/corpus/templates/brain/actions/list-proposals.ts +80 -4
- package/corpus/templates/brain/actions/list-sources.ts +17 -3
- package/corpus/templates/brain/actions/manage-project.ts +118 -0
- package/corpus/templates/brain/actions/mark-capture-distilled.ts +68 -20
- package/corpus/templates/brain/actions/navigate.ts +1 -0
- package/corpus/templates/brain/actions/reject-proposal.ts +6 -1
- package/corpus/templates/brain/actions/resanitize-captures.ts +159 -33
- package/corpus/templates/brain/actions/review-proposal.ts +2 -0
- package/corpus/templates/brain/actions/search-everything.ts +10 -1
- package/corpus/templates/brain/actions/search-knowledge.ts +1 -1
- package/corpus/templates/brain/actions/set-settings.ts +36 -1
- package/corpus/templates/brain/actions/update-proposal.ts +2 -0
- package/corpus/templates/brain/app/hooks/use-distillation-bridge.ts +18 -9
- package/corpus/templates/brain/app/hooks/use-navigation-state.ts +3 -0
- package/corpus/templates/brain/app/i18n-data.ts +731 -2
- package/corpus/templates/brain/app/lib/brain.ts +33 -1
- package/corpus/templates/brain/app/routes/ops.tsx +47 -0
- package/corpus/templates/brain/app/routes/review.tsx +41 -2
- package/corpus/templates/brain/app/routes/search.tsx +33 -2
- package/corpus/templates/brain/app/routes/settings.tsx +158 -0
- package/corpus/templates/brain/app/routes/sources.tsx +31 -0
- package/corpus/templates/brain/changelog/2026-07-19-added-private-source-safe-semantic-search.md +6 -0
- package/corpus/templates/brain/jobs/process-ingest-queue.ts +270 -36
- package/corpus/templates/brain/package.json +1 -0
- package/corpus/templates/brain/server/db/index.ts +18 -0
- package/corpus/templates/brain/server/db/schema.ts +197 -1
- package/corpus/templates/brain/server/jobs/distillation-queue.ts +12 -5
- package/corpus/templates/brain/server/lib/audiences.ts +708 -0
- package/corpus/templates/brain/server/lib/brain-health.ts +251 -38
- package/corpus/templates/brain/server/lib/brain.ts +877 -50
- package/corpus/templates/brain/server/lib/capture-sanitization.ts +217 -53
- package/corpus/templates/brain/server/lib/connectors.ts +556 -70
- package/corpus/templates/brain/server/lib/demo.ts +17 -4
- package/corpus/templates/brain/server/lib/hybrid-search.ts +363 -0
- package/corpus/templates/brain/server/lib/ingest-queue.ts +104 -0
- package/corpus/templates/brain/server/lib/privacy-readiness.ts +24 -0
- package/corpus/templates/brain/server/lib/search-index-contracts.ts +72 -0
- package/corpus/templates/brain/server/lib/search-index.ts +714 -0
- package/corpus/templates/brain/server/lib/search.ts +120 -4
- package/corpus/templates/brain/server/lib/sensitivity-policy.ts +174 -0
- package/corpus/templates/brain/server/lib/slack-events.ts +281 -0
- package/corpus/templates/brain/server/onboarding.ts +29 -0
- package/corpus/templates/brain/server/plugins/db.ts +204 -0
- package/corpus/templates/brain/server/register-secrets.ts +46 -0
- package/corpus/templates/brain/server/routes/api/_agent-native/brain/ingest.post.ts +36 -16
- package/corpus/templates/brain/server/routes/api/_agent-native/brain/slack-events.post.ts +28 -0
- package/corpus/templates/brain/shared/types.ts +10 -0
- package/dist/cli/code-agent-executor.d.ts +8 -9
- package/dist/cli/code-agent-executor.d.ts.map +1 -1
- package/dist/cli/code-agent-executor.js +9 -3
- package/dist/cli/code-agent-executor.js.map +1 -1
- package/dist/cli/code-agent-runs.d.ts +5 -0
- package/dist/cli/code-agent-runs.d.ts.map +1 -1
- package/dist/cli/code-agent-runs.js +33 -2
- package/dist/cli/code-agent-runs.js.map +1 -1
- package/dist/cli/multi-frontier-runs.d.ts +131 -0
- package/dist/cli/multi-frontier-runs.d.ts.map +1 -0
- package/dist/cli/multi-frontier-runs.js +529 -0
- package/dist/cli/multi-frontier-runs.js.map +1 -0
- package/dist/coding-tools/index.d.ts +1 -0
- package/dist/coding-tools/index.d.ts.map +1 -1
- package/dist/coding-tools/index.js +25 -7
- package/dist/coding-tools/index.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/embeddings/index.d.ts +24 -0
- package/dist/embeddings/index.d.ts.map +1 -0
- package/dist/embeddings/index.js +172 -0
- package/dist/embeddings/index.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/search/index.d.ts +88 -0
- package/dist/search/index.d.ts.map +1 -0
- package/dist/search/index.js +267 -0
- package/dist/search/index.js.map +1 -0
- 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,
|
|
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
|
-
|
|
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<
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
input
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
const
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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:
|
|
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
|
|
362
|
-
if (!
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
let
|
|
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
|
-
|
|
391
|
-
if (
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
402
|
-
dropTranscriptLines: Boolean(input.metadata?.captureSanitization),
|
|
403
|
-
});
|
|
524
|
+
classifierFailed = classifierConfigured;
|
|
404
525
|
fallbackReason = "model-unavailable";
|
|
405
526
|
}
|
|
406
|
-
|
|
407
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
}
|