@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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +18 -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/migration-codemod.ts +59 -9
- 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/migration-codemod.d.ts.map +1 -1
- package/dist/cli/migration-codemod.js +48 -10
- package/dist/cli/migration-codemod.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/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/package.json +3 -1
|
@@ -11,7 +11,9 @@ import { and, desc, eq, inArray, or, sql, type SQL } from "drizzle-orm";
|
|
|
11
11
|
|
|
12
12
|
import type { BrainEvidence } from "../../shared/types.js";
|
|
13
13
|
import { getDb, schema } from "../db/index.js";
|
|
14
|
+
import { listAccessibleAudienceIds } from "./audiences.js";
|
|
14
15
|
import { parseJson, safeCitationUrl } from "./brain.js";
|
|
16
|
+
import { hybridSearchArtifacts } from "./hybrid-search.js";
|
|
15
17
|
|
|
16
18
|
export type UniversalSearchType = "knowledge" | "capture" | "source";
|
|
17
19
|
|
|
@@ -35,10 +37,14 @@ export interface UniversalSearchResult {
|
|
|
35
37
|
captureTitle?: string | null;
|
|
36
38
|
quote?: string | null;
|
|
37
39
|
sourceUrl?: string | null;
|
|
40
|
+
preview?: string | null;
|
|
41
|
+
verbatim?: boolean;
|
|
38
42
|
} | null;
|
|
39
43
|
confidence: number | null;
|
|
40
44
|
updatedAt: string;
|
|
41
45
|
score: number;
|
|
46
|
+
reasons?: string[];
|
|
47
|
+
lane?: "lexical" | "semantic" | "hybrid" | "like";
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
export type FederatedDelegationTarget = "analytics" | "mail" | "dispatch";
|
|
@@ -364,12 +370,27 @@ async function searchKnowledgeResults(
|
|
|
364
370
|
terms: string[],
|
|
365
371
|
limit: number,
|
|
366
372
|
): Promise<UniversalSearchResult[]> {
|
|
373
|
+
const audienceIds = await listAccessibleAudienceIds();
|
|
374
|
+
const accessibleSourceExists = sql`exists (
|
|
375
|
+
select 1 from ${schema.brainSources}
|
|
376
|
+
where ${schema.brainSources.id} = ${schema.brainKnowledge.sourceId}
|
|
377
|
+
and ${accessFilter(schema.brainSources, schema.brainSourceShares)}
|
|
378
|
+
)`;
|
|
367
379
|
const rows = await getDb()
|
|
368
380
|
.select()
|
|
369
381
|
.from(schema.brainKnowledge)
|
|
370
382
|
.where(
|
|
371
383
|
and(
|
|
372
384
|
accessFilter(schema.brainKnowledge, schema.brainKnowledgeShares),
|
|
385
|
+
or(
|
|
386
|
+
sql`${schema.brainKnowledge.captureId} IS NULL`,
|
|
387
|
+
audienceIds.length
|
|
388
|
+
? and(
|
|
389
|
+
accessibleSourceExists,
|
|
390
|
+
inArray(schema.brainKnowledge.audienceId, audienceIds),
|
|
391
|
+
)
|
|
392
|
+
: undefined,
|
|
393
|
+
),
|
|
373
394
|
eq(schema.brainKnowledge.status, "published"),
|
|
374
395
|
anyColumnMatches(
|
|
375
396
|
[
|
|
@@ -437,6 +458,8 @@ async function searchCaptureResults(
|
|
|
437
458
|
terms: string[],
|
|
438
459
|
limit: number,
|
|
439
460
|
): Promise<UniversalSearchResult[]> {
|
|
461
|
+
const audienceIds = await listAccessibleAudienceIds();
|
|
462
|
+
if (!audienceIds.length) return [];
|
|
440
463
|
const accessibleSourceExists = sql`exists (
|
|
441
464
|
select 1 from ${schema.brainSources}
|
|
442
465
|
where ${schema.brainSources.id} = ${schema.brainRawCaptures.sourceId}
|
|
@@ -448,6 +471,12 @@ async function searchCaptureResults(
|
|
|
448
471
|
.where(
|
|
449
472
|
and(
|
|
450
473
|
accessibleSourceExists,
|
|
474
|
+
eq(schema.brainRawCaptures.sensitivityDisposition, "allowed"),
|
|
475
|
+
sql`exists (
|
|
476
|
+
select 1 from ${schema.brainCaptureAudiences}
|
|
477
|
+
where ${schema.brainCaptureAudiences.captureId} = ${schema.brainRawCaptures.id}
|
|
478
|
+
and ${inArray(schema.brainCaptureAudiences.audienceId, audienceIds)}
|
|
479
|
+
)`,
|
|
451
480
|
anyColumnMatches(
|
|
452
481
|
[
|
|
453
482
|
schema.brainRawCaptures.title,
|
|
@@ -481,7 +510,9 @@ async function searchCaptureResults(
|
|
|
481
510
|
citation: {
|
|
482
511
|
captureId: row.id,
|
|
483
512
|
captureTitle: redactSensitiveText(row.title),
|
|
484
|
-
quote:
|
|
513
|
+
quote: null,
|
|
514
|
+
preview: snippet,
|
|
515
|
+
verbatim: false,
|
|
485
516
|
sourceUrl,
|
|
486
517
|
},
|
|
487
518
|
confidence: null,
|
|
@@ -560,23 +591,99 @@ export async function searchEverythingRows(args: {
|
|
|
560
591
|
query: string;
|
|
561
592
|
type?: UniversalSearchType | "all";
|
|
562
593
|
provider?: string;
|
|
594
|
+
kind?: string;
|
|
595
|
+
projectId?: string;
|
|
563
596
|
status?: string;
|
|
564
597
|
limit?: number;
|
|
565
598
|
}): Promise<UniversalSearchResult[]> {
|
|
566
599
|
const terms = normalizeSearchTerms(args.query);
|
|
567
600
|
if (!terms.length) return [];
|
|
601
|
+
const projectSourceIds = args.projectId
|
|
602
|
+
? (
|
|
603
|
+
await getDb()
|
|
604
|
+
.select({ sourceId: schema.brainProjectSources.sourceId })
|
|
605
|
+
.from(schema.brainProjectSources)
|
|
606
|
+
.innerJoin(
|
|
607
|
+
schema.brainProjects,
|
|
608
|
+
eq(schema.brainProjects.id, schema.brainProjectSources.projectId),
|
|
609
|
+
)
|
|
610
|
+
.where(
|
|
611
|
+
and(
|
|
612
|
+
eq(schema.brainProjectSources.projectId, args.projectId),
|
|
613
|
+
accessFilter(schema.brainProjects, schema.brainProjectShares),
|
|
614
|
+
),
|
|
615
|
+
)
|
|
616
|
+
).map((row) => row.sourceId)
|
|
617
|
+
: undefined;
|
|
618
|
+
if (projectSourceIds && !projectSourceIds.length) return [];
|
|
619
|
+
const projectSourceSet = projectSourceIds
|
|
620
|
+
? new Set(projectSourceIds)
|
|
621
|
+
: undefined;
|
|
568
622
|
const limit = args.limit ?? 25;
|
|
569
623
|
const perTypeLimit = Math.max(limit, 10);
|
|
570
624
|
const searches: Array<Promise<UniversalSearchResult[]>> = [];
|
|
571
625
|
if (!args.type || args.type === "all" || args.type === "knowledge") {
|
|
572
626
|
searches.push(searchKnowledgeResults(terms, perTypeLimit));
|
|
573
627
|
}
|
|
574
|
-
if (
|
|
628
|
+
if (
|
|
629
|
+
(!args.type || args.type === "all" || args.type === "capture") &&
|
|
630
|
+
!args.kind
|
|
631
|
+
) {
|
|
575
632
|
searches.push(searchCaptureResults(terms, perTypeLimit));
|
|
576
633
|
}
|
|
577
634
|
if (!args.type || args.type === "all" || args.type === "source") {
|
|
578
635
|
searches.push(searchSourceResults(terms, perTypeLimit));
|
|
579
636
|
}
|
|
637
|
+
if (!args.type || args.type === "all" || args.type === "capture") {
|
|
638
|
+
searches.push(
|
|
639
|
+
hybridSearchArtifacts({
|
|
640
|
+
query: args.query,
|
|
641
|
+
provider: args.provider,
|
|
642
|
+
kind: args.kind,
|
|
643
|
+
projectId: args.projectId,
|
|
644
|
+
limit: perTypeLimit,
|
|
645
|
+
})
|
|
646
|
+
.then(async (artifacts) => {
|
|
647
|
+
const sources = await accessibleSourceMap(
|
|
648
|
+
artifacts.map((artifact) => artifact.sourceId),
|
|
649
|
+
);
|
|
650
|
+
return artifacts.flatMap((artifact) => {
|
|
651
|
+
const source = sources.get(artifact.sourceId);
|
|
652
|
+
if (!source) return [];
|
|
653
|
+
const snippet = redactSensitiveText(
|
|
654
|
+
buildSnippet(artifact.text, terms),
|
|
655
|
+
);
|
|
656
|
+
return [
|
|
657
|
+
{
|
|
658
|
+
type: "capture" as const,
|
|
659
|
+
id: artifact.captureId,
|
|
660
|
+
title: redactSensitiveText(artifact.title),
|
|
661
|
+
snippet,
|
|
662
|
+
summary: snippet,
|
|
663
|
+
status: "indexed",
|
|
664
|
+
provider: source.provider,
|
|
665
|
+
source: serializeSourceInfo(source),
|
|
666
|
+
sourceUrl: null,
|
|
667
|
+
citation: {
|
|
668
|
+
captureId: artifact.captureId,
|
|
669
|
+
captureTitle: redactSensitiveText(artifact.title),
|
|
670
|
+
quote: null,
|
|
671
|
+
preview: snippet,
|
|
672
|
+
verbatim: false,
|
|
673
|
+
sourceUrl: null,
|
|
674
|
+
},
|
|
675
|
+
confidence: null,
|
|
676
|
+
updatedAt: artifact.capturedAt,
|
|
677
|
+
score: artifact.score * 100,
|
|
678
|
+
reasons: artifact.reasons,
|
|
679
|
+
lane: artifact.lane,
|
|
680
|
+
},
|
|
681
|
+
];
|
|
682
|
+
});
|
|
683
|
+
})
|
|
684
|
+
.catch(() => []),
|
|
685
|
+
);
|
|
686
|
+
}
|
|
580
687
|
const provider = args.provider?.toLowerCase();
|
|
581
688
|
const status = args.status?.toLowerCase();
|
|
582
689
|
const results = (await Promise.all(searches))
|
|
@@ -590,7 +697,10 @@ export async function searchEverythingRows(args: {
|
|
|
590
697
|
const resultStatus = result.status.toLowerCase();
|
|
591
698
|
const providerMatches = !provider || resultProvider === provider;
|
|
592
699
|
const statusMatches = !status || resultStatus === status;
|
|
593
|
-
|
|
700
|
+
const projectMatches =
|
|
701
|
+
!projectSourceSet ||
|
|
702
|
+
Boolean(result.source?.id && projectSourceSet.has(result.source.id));
|
|
703
|
+
return providerMatches && statusMatches && projectMatches;
|
|
594
704
|
})
|
|
595
705
|
.sort(
|
|
596
706
|
(a, b) =>
|
|
@@ -598,7 +708,13 @@ export async function searchEverythingRows(args: {
|
|
|
598
708
|
Date.parse(b.updatedAt) - Date.parse(a.updatedAt) ||
|
|
599
709
|
a.title.localeCompare(b.title),
|
|
600
710
|
);
|
|
601
|
-
|
|
711
|
+
const deduped = new Map<string, UniversalSearchResult>();
|
|
712
|
+
for (const result of results) {
|
|
713
|
+
const key = `${result.type}:${result.id}`;
|
|
714
|
+
const current = deduped.get(key);
|
|
715
|
+
if (!current || result.score > current.score) deduped.set(key, result);
|
|
716
|
+
}
|
|
717
|
+
return Array.from(deduped.values()).slice(0, limit);
|
|
602
718
|
}
|
|
603
719
|
|
|
604
720
|
async function readBrainSourceProviderCoverage(): Promise<
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
BRAIN_SENSITIVITY_POLICY_VERSION,
|
|
5
|
+
type BrainSafeSegment,
|
|
6
|
+
type BrainSensitivityCategory,
|
|
7
|
+
type BrainSensitivityDecision,
|
|
8
|
+
} from "./search-index-contracts.js";
|
|
9
|
+
|
|
10
|
+
export const MAX_CLASSIFIER_OUTPUT_CHARS = 80_000;
|
|
11
|
+
export const classifierDecisionSchema = z
|
|
12
|
+
.object({
|
|
13
|
+
disposition: z.enum(["allowed", "suppressed", "quarantined"]),
|
|
14
|
+
categories: z
|
|
15
|
+
.array(
|
|
16
|
+
z.enum([
|
|
17
|
+
"performance",
|
|
18
|
+
"discipline",
|
|
19
|
+
"termination",
|
|
20
|
+
"layoff-reorg",
|
|
21
|
+
"compensation",
|
|
22
|
+
"recruiting",
|
|
23
|
+
"health-accommodation",
|
|
24
|
+
"investigation",
|
|
25
|
+
"privileged-legal",
|
|
26
|
+
"secret-credential",
|
|
27
|
+
"personal",
|
|
28
|
+
]),
|
|
29
|
+
)
|
|
30
|
+
.max(12),
|
|
31
|
+
safeContent: z.string().max(MAX_CLASSIFIER_OUTPUT_CHARS),
|
|
32
|
+
safeSegments: z
|
|
33
|
+
.array(
|
|
34
|
+
z.object({
|
|
35
|
+
text: z.string().min(1).max(8_000),
|
|
36
|
+
sourceUrl: z.string().url().optional(),
|
|
37
|
+
}),
|
|
38
|
+
)
|
|
39
|
+
.max(50),
|
|
40
|
+
})
|
|
41
|
+
.strict();
|
|
42
|
+
|
|
43
|
+
const HARD_CATEGORY_PATTERNS: ReadonlyArray<
|
|
44
|
+
readonly [BrainSensitivityCategory, RegExp]
|
|
45
|
+
> = [
|
|
46
|
+
[
|
|
47
|
+
"performance",
|
|
48
|
+
/\b(performance review|under[- ]?perform(?:er|ing)?|low performer|performance concern)\b/i,
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
"discipline",
|
|
52
|
+
/\b(performance improvement plan|start (?:a )?pip|(?:place(?:d)?|put) (?:them|her|him|the employee) on (?:a )?pip|written warning|disciplinary action|final warning)\b/i,
|
|
53
|
+
],
|
|
54
|
+
[
|
|
55
|
+
"termination",
|
|
56
|
+
/\b(employment termination|terminate the employee|terminat(?:e|ed|ion) (?:their|his|her|the employee'?s?) employment|fired|fire\s+(?:them|her|him)|severance (?:agreement|package|pay))\b/i,
|
|
57
|
+
],
|
|
58
|
+
[
|
|
59
|
+
"layoff-reorg",
|
|
60
|
+
/\b(layoffs?|rif|reorg(?:anization)?|rightsizing|roles? impacted|role eliminat(?:ion|ed)|workforce reduction)\b/i,
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
"compensation",
|
|
64
|
+
/\b(compensation (?:review|discussion|adjustment|change)|salary|(?:annual|signing|performance|retention) bonus|bonus (?:amount|payout|target)|equity grant|stock options?|pay band|payroll|pay raise|salary increase|comp adjustment)\b/i,
|
|
65
|
+
],
|
|
66
|
+
[
|
|
67
|
+
"recruiting",
|
|
68
|
+
/\b(candidate|applicant|interview(?:er|ing)?|recruit(?:er|ing)?|resume|résumé|reference check|job offer|shortlist|hiring panel)\b/i,
|
|
69
|
+
],
|
|
70
|
+
[
|
|
71
|
+
"health-accommodation",
|
|
72
|
+
/\b(health condition|medical (?:condition|diagnosis|leave|record|accommodation)|doctor(?:'s)? note|disability accommodation|workplace accommodation|fmla|mental health (?:condition|leave|accommodation)|pregnan(?:t|cy))\b/i,
|
|
73
|
+
],
|
|
74
|
+
[
|
|
75
|
+
"investigation",
|
|
76
|
+
/\b(investigation|investigat(?:e|ing|ed)|harassment complaint|misconduct|whistleblower|ethics complaint)\b/i,
|
|
77
|
+
],
|
|
78
|
+
[
|
|
79
|
+
"privileged-legal",
|
|
80
|
+
/\b(attorney[- ]client|legal privilege|privileged and confidential|outside counsel|litigation hold)\b/i,
|
|
81
|
+
],
|
|
82
|
+
[
|
|
83
|
+
"secret-credential",
|
|
84
|
+
/\b(?:password|passcode|secret|api[- ]?key|access[- ]?token|private[- ]?key)\s*[:=]|\b(?:sk|pk|rk|ghp|gho|ghu|github_pat)[_-][A-Za-z0-9_=-]{12,}\b/i,
|
|
85
|
+
],
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
const PERSONAL_PATTERN =
|
|
89
|
+
/\b(home address|social security|ssn|birthday|spouse|husband|wife|children?)\b/i;
|
|
90
|
+
const PROMPT_INJECTION_PATTERN =
|
|
91
|
+
/\b(ignore (?:previous|all|privacy)|override (?:the |all )?(?:policy|rules)|reveal (?:the )?(?:secret|private)|persist every)\b/i;
|
|
92
|
+
export interface DeterministicSensitivityScreen {
|
|
93
|
+
categories: BrainSensitivityCategory[];
|
|
94
|
+
sensitiveLines: string[];
|
|
95
|
+
safeLines: string[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function screenSensitivityDeterministically(
|
|
99
|
+
content: string,
|
|
100
|
+
): DeterministicSensitivityScreen {
|
|
101
|
+
const categories = new Set<BrainSensitivityCategory>();
|
|
102
|
+
const sensitiveLines: string[] = [];
|
|
103
|
+
const safeLines: string[] = [];
|
|
104
|
+
|
|
105
|
+
for (const rawLine of content.split(/\r?\n/g)) {
|
|
106
|
+
const line = rawLine.trim();
|
|
107
|
+
if (!line) continue;
|
|
108
|
+
let sensitive = false;
|
|
109
|
+
for (const [category, pattern] of HARD_CATEGORY_PATTERNS) {
|
|
110
|
+
if (!pattern.test(line)) continue;
|
|
111
|
+
categories.add(category);
|
|
112
|
+
sensitive = true;
|
|
113
|
+
}
|
|
114
|
+
if (PERSONAL_PATTERN.test(line)) {
|
|
115
|
+
categories.add("personal");
|
|
116
|
+
sensitive = true;
|
|
117
|
+
}
|
|
118
|
+
if (PROMPT_INJECTION_PATTERN.test(line)) sensitive = true;
|
|
119
|
+
(sensitive ? sensitiveLines : safeLines).push(line);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return { categories: [...categories], sensitiveLines, safeLines };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function fallbackSensitivityDecision(
|
|
126
|
+
content: string,
|
|
127
|
+
capturedAt: string,
|
|
128
|
+
): BrainSensitivityDecision {
|
|
129
|
+
const screen = screenSensitivityDeterministically(content);
|
|
130
|
+
const safeContent = screen.safeLines
|
|
131
|
+
.join("\n")
|
|
132
|
+
.slice(0, MAX_CLASSIFIER_OUTPUT_CHARS);
|
|
133
|
+
const safeSegments: BrainSafeSegment[] = safeContent
|
|
134
|
+
? [{ id: "safe-1", capturedAt, text: safeContent, reactionCount: 0 }]
|
|
135
|
+
: [];
|
|
136
|
+
const disposition =
|
|
137
|
+
screen.categories.length || screen.sensitiveLines.length || !safeContent
|
|
138
|
+
? "quarantined"
|
|
139
|
+
: "allowed";
|
|
140
|
+
return {
|
|
141
|
+
disposition,
|
|
142
|
+
categories: screen.categories,
|
|
143
|
+
confidenceBand:
|
|
144
|
+
disposition === "allowed" || screen.categories.length
|
|
145
|
+
? "deterministic"
|
|
146
|
+
: "uncertain",
|
|
147
|
+
policyVersion: BRAIN_SENSITIVITY_POLICY_VERSION,
|
|
148
|
+
safeSegments,
|
|
149
|
+
safeContent,
|
|
150
|
+
classifier: "deterministic",
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function deterministicQuarantineDecision(
|
|
155
|
+
content: string,
|
|
156
|
+
capturedAt: string,
|
|
157
|
+
): BrainSensitivityDecision | null {
|
|
158
|
+
const screen = screenSensitivityDeterministically(content);
|
|
159
|
+
if (!screen.categories.length) return null;
|
|
160
|
+
const safeContent = screen.safeLines
|
|
161
|
+
.join("\n")
|
|
162
|
+
.slice(0, MAX_CLASSIFIER_OUTPUT_CHARS);
|
|
163
|
+
return {
|
|
164
|
+
disposition: "suppressed",
|
|
165
|
+
categories: screen.categories,
|
|
166
|
+
confidenceBand: "deterministic",
|
|
167
|
+
policyVersion: BRAIN_SENSITIVITY_POLICY_VERSION,
|
|
168
|
+
safeSegments: safeContent
|
|
169
|
+
? [{ id: "safe-1", capturedAt, text: safeContent, reactionCount: 0 }]
|
|
170
|
+
: [],
|
|
171
|
+
safeContent,
|
|
172
|
+
classifier: "deterministic",
|
|
173
|
+
};
|
|
174
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { and, eq } from "drizzle-orm";
|
|
4
|
+
|
|
5
|
+
import { getDb, schema } from "../db/index.js";
|
|
6
|
+
import { parseJson, retireUpstreamDeletedCapture } from "./brain.js";
|
|
7
|
+
import { enqueueBrainOperation } from "./ingest-queue.js";
|
|
8
|
+
import { resolveSourceCredential } from "./source-credentials.js";
|
|
9
|
+
|
|
10
|
+
export interface SlackEventsEnvelope {
|
|
11
|
+
type?: string;
|
|
12
|
+
challenge?: string;
|
|
13
|
+
team_id?: string;
|
|
14
|
+
api_app_id?: string;
|
|
15
|
+
event?: {
|
|
16
|
+
type?: string;
|
|
17
|
+
subtype?: string;
|
|
18
|
+
channel?: string;
|
|
19
|
+
ts?: string;
|
|
20
|
+
thread_ts?: string;
|
|
21
|
+
deleted_ts?: string;
|
|
22
|
+
previous_message?: { ts?: string; thread_ts?: string };
|
|
23
|
+
item?: { type?: string; channel?: string; ts?: string };
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type SlackSource = typeof schema.brainSources.$inferSelect;
|
|
28
|
+
|
|
29
|
+
function objectValue(value: unknown): Record<string, unknown> {
|
|
30
|
+
return value && typeof value === "object"
|
|
31
|
+
? (value as Record<string, unknown>)
|
|
32
|
+
: {};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function stringValue(value: unknown): string | undefined {
|
|
36
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function configuredString(
|
|
40
|
+
config: Record<string, unknown>,
|
|
41
|
+
keys: string[],
|
|
42
|
+
): string | undefined {
|
|
43
|
+
for (const candidate of [config, objectValue(config.slack)]) {
|
|
44
|
+
for (const key of keys) {
|
|
45
|
+
const value = stringValue(candidate[key]);
|
|
46
|
+
if (value) return value;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function configuredChannelIds(config: Record<string, unknown>) {
|
|
53
|
+
const result = new Set<string>();
|
|
54
|
+
for (const candidate of [config, objectValue(config.slack)]) {
|
|
55
|
+
for (const key of [
|
|
56
|
+
"channelIds",
|
|
57
|
+
"channels",
|
|
58
|
+
"allowedChannels",
|
|
59
|
+
"allowlistedChannels",
|
|
60
|
+
"allowList",
|
|
61
|
+
]) {
|
|
62
|
+
const raw = candidate[key];
|
|
63
|
+
const values =
|
|
64
|
+
typeof raw === "string"
|
|
65
|
+
? raw.split(",")
|
|
66
|
+
: Array.isArray(raw)
|
|
67
|
+
? raw.filter((value): value is string => typeof value === "string")
|
|
68
|
+
: [];
|
|
69
|
+
for (const value of values) {
|
|
70
|
+
const id = value.trim().replace(/^#/, "");
|
|
71
|
+
if (/^[CG][A-Z0-9]+$/i.test(id)) result.add(id);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function includesPublicChannels(config: Record<string, unknown>) {
|
|
79
|
+
for (const candidate of [config, objectValue(config.slack)]) {
|
|
80
|
+
const value = candidate.includePublicChannels;
|
|
81
|
+
if (value === true) return true;
|
|
82
|
+
if (
|
|
83
|
+
typeof value === "string" &&
|
|
84
|
+
["true", "1", "yes", "on"].includes(value.toLowerCase())
|
|
85
|
+
) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function slackEventDirective(payload: SlackEventsEnvelope) {
|
|
93
|
+
const event = payload.event;
|
|
94
|
+
if (!event) return null;
|
|
95
|
+
if (event.type === "message") {
|
|
96
|
+
const retiring = event.subtype === "message_deleted";
|
|
97
|
+
const threadTs = retiring
|
|
98
|
+
? (event.previous_message?.thread_ts ??
|
|
99
|
+
event.previous_message?.ts ??
|
|
100
|
+
event.deleted_ts)
|
|
101
|
+
: (event.thread_ts ?? event.ts);
|
|
102
|
+
return event.channel && threadTs
|
|
103
|
+
? {
|
|
104
|
+
action: retiring ? ("retire" as const) : ("refresh" as const),
|
|
105
|
+
channelId: event.channel,
|
|
106
|
+
threadTs,
|
|
107
|
+
}
|
|
108
|
+
: null;
|
|
109
|
+
}
|
|
110
|
+
if (
|
|
111
|
+
(event.type === "reaction_added" || event.type === "reaction_removed") &&
|
|
112
|
+
event.item?.type === "message" &&
|
|
113
|
+
event.item.channel &&
|
|
114
|
+
event.item.ts
|
|
115
|
+
) {
|
|
116
|
+
return {
|
|
117
|
+
action: "refresh" as const,
|
|
118
|
+
channelId: event.item.channel,
|
|
119
|
+
threadTs: event.item.ts,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function sourceMatchesEvent(
|
|
126
|
+
source: SlackSource,
|
|
127
|
+
payload: SlackEventsEnvelope,
|
|
128
|
+
location: { channelId: string; threadTs: string } | null,
|
|
129
|
+
) {
|
|
130
|
+
const config = parseJson<Record<string, unknown>>(source.configJson, {});
|
|
131
|
+
const teamId = configuredString(config, ["slackTeamId", "teamId"]);
|
|
132
|
+
const appId = configuredString(config, ["slackAppId", "appId"]);
|
|
133
|
+
if (!teamId || teamId !== payload.team_id) return false;
|
|
134
|
+
if (appId && appId !== payload.api_app_id) return false;
|
|
135
|
+
if (!location) return payload.type === "url_verification";
|
|
136
|
+
return (
|
|
137
|
+
configuredChannelIds(config).has(location.channelId) ||
|
|
138
|
+
(includesPublicChannels(config) && /^C[A-Z0-9]+$/i.test(location.channelId))
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function parseSlackEventsEnvelope(
|
|
143
|
+
rawBody: string,
|
|
144
|
+
): SlackEventsEnvelope | null {
|
|
145
|
+
try {
|
|
146
|
+
const parsed = JSON.parse(rawBody) as unknown;
|
|
147
|
+
return parsed && typeof parsed === "object"
|
|
148
|
+
? (parsed as SlackEventsEnvelope)
|
|
149
|
+
: null;
|
|
150
|
+
} catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function verifySlackEventSignature(input: {
|
|
156
|
+
rawBody: string;
|
|
157
|
+
timestamp?: string;
|
|
158
|
+
signature?: string;
|
|
159
|
+
signingSecret?: string;
|
|
160
|
+
nowMs?: number;
|
|
161
|
+
}) {
|
|
162
|
+
if (!input.timestamp || !input.signature || !input.signingSecret)
|
|
163
|
+
return false;
|
|
164
|
+
const timestamp = Number.parseInt(input.timestamp, 10);
|
|
165
|
+
if (!Number.isFinite(timestamp)) return false;
|
|
166
|
+
if (Math.abs((input.nowMs ?? Date.now()) / 1_000 - timestamp) > 300)
|
|
167
|
+
return false;
|
|
168
|
+
const expected = `v0=${crypto
|
|
169
|
+
.createHmac("sha256", input.signingSecret)
|
|
170
|
+
.update(`v0:${input.timestamp}:${input.rawBody}`)
|
|
171
|
+
.digest("hex")}`;
|
|
172
|
+
try {
|
|
173
|
+
return crypto.timingSafeEqual(
|
|
174
|
+
Buffer.from(input.signature),
|
|
175
|
+
Buffer.from(expected),
|
|
176
|
+
);
|
|
177
|
+
} catch {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function matchingSlackSource(
|
|
183
|
+
payload: SlackEventsEnvelope,
|
|
184
|
+
location: { channelId: string; threadTs: string } | null,
|
|
185
|
+
) {
|
|
186
|
+
const sources = await getDb()
|
|
187
|
+
.select()
|
|
188
|
+
.from(schema.brainSources)
|
|
189
|
+
// guard:allow-unscoped — Slack's signed Events API has no user session;
|
|
190
|
+
// each candidate must match its configured workspace and channel before use.
|
|
191
|
+
.where(
|
|
192
|
+
and(
|
|
193
|
+
eq(schema.brainSources.provider, "slack"),
|
|
194
|
+
eq(schema.brainSources.status, "active"),
|
|
195
|
+
),
|
|
196
|
+
);
|
|
197
|
+
return (
|
|
198
|
+
sources.find((source) => sourceMatchesEvent(source, payload, location)) ??
|
|
199
|
+
null
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function sourceSigningSecret(source: SlackSource) {
|
|
204
|
+
const config = parseJson<Record<string, unknown>>(source.configJson, {});
|
|
205
|
+
return resolveSourceCredential({
|
|
206
|
+
provider: "slack",
|
|
207
|
+
key: "SLACK_SIGNING_SECRET",
|
|
208
|
+
ctx: { userEmail: source.ownerEmail, orgId: source.orgId },
|
|
209
|
+
workspaceConnectionId: configuredString(config, ["workspaceConnectionId"]),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export async function retireSlackThreadCapture(input: {
|
|
214
|
+
sourceId: string;
|
|
215
|
+
channelId: string;
|
|
216
|
+
threadTs: string;
|
|
217
|
+
}) {
|
|
218
|
+
return retireUpstreamDeletedCapture({
|
|
219
|
+
sourceId: input.sourceId,
|
|
220
|
+
externalId: `slack:${input.channelId}:${input.threadTs}`,
|
|
221
|
+
provider: "slack",
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Verify a Brain Slack event and either enqueue an ID-only refresh or retire a
|
|
227
|
+
* deleted upstream thread. The raw event body is never stored or logged.
|
|
228
|
+
*/
|
|
229
|
+
export async function enqueueSlackThreadRefreshFromEvent(input: {
|
|
230
|
+
rawBody: string;
|
|
231
|
+
timestamp?: string;
|
|
232
|
+
signature?: string;
|
|
233
|
+
}): Promise<
|
|
234
|
+
| { status: "invalid" | "ignored" | "queued"; challenge?: string }
|
|
235
|
+
| { status: "missing-signing-secret" }
|
|
236
|
+
> {
|
|
237
|
+
const payload = parseSlackEventsEnvelope(input.rawBody);
|
|
238
|
+
if (!payload?.team_id) return { status: "invalid" };
|
|
239
|
+
const directive = slackEventDirective(payload);
|
|
240
|
+
const source = await matchingSlackSource(payload, directive);
|
|
241
|
+
if (!source) return { status: "ignored" };
|
|
242
|
+
const signingSecret = await sourceSigningSecret(source);
|
|
243
|
+
if (!signingSecret) return { status: "missing-signing-secret" };
|
|
244
|
+
if (
|
|
245
|
+
!verifySlackEventSignature({
|
|
246
|
+
rawBody: input.rawBody,
|
|
247
|
+
timestamp: input.timestamp,
|
|
248
|
+
signature: input.signature,
|
|
249
|
+
signingSecret,
|
|
250
|
+
})
|
|
251
|
+
) {
|
|
252
|
+
return { status: "invalid" };
|
|
253
|
+
}
|
|
254
|
+
if (
|
|
255
|
+
payload.type === "url_verification" &&
|
|
256
|
+
typeof payload.challenge === "string"
|
|
257
|
+
) {
|
|
258
|
+
return { status: "queued", challenge: payload.challenge };
|
|
259
|
+
}
|
|
260
|
+
if (!directive) return { status: "ignored" };
|
|
261
|
+
if (directive.action === "retire") {
|
|
262
|
+
await retireSlackThreadCapture({
|
|
263
|
+
sourceId: source.id,
|
|
264
|
+
channelId: directive.channelId,
|
|
265
|
+
threadTs: directive.threadTs,
|
|
266
|
+
});
|
|
267
|
+
return { status: "queued" };
|
|
268
|
+
}
|
|
269
|
+
await enqueueBrainOperation({
|
|
270
|
+
operation: "slack-thread-refresh",
|
|
271
|
+
dedupeKey: `slack-thread-refresh:${source.id}:${directive.channelId}:${directive.threadTs}`,
|
|
272
|
+
sourceId: source.id,
|
|
273
|
+
priority: 20,
|
|
274
|
+
payload: {
|
|
275
|
+
teamId: payload.team_id,
|
|
276
|
+
channelId: directive.channelId,
|
|
277
|
+
threadTs: directive.threadTs,
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
return { status: "queued" };
|
|
281
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { registerOnboardingStep } from "@agent-native/core/onboarding";
|
|
2
|
+
|
|
3
|
+
import { readBrainSettings } from "./lib/brain.js";
|
|
4
|
+
import { brainPrivacyReadiness } from "./lib/privacy-readiness.js";
|
|
5
|
+
|
|
6
|
+
registerOnboardingStep({
|
|
7
|
+
id: "brain-privacy-classifier",
|
|
8
|
+
order: 16,
|
|
9
|
+
required: false,
|
|
10
|
+
title: "Configure Brain privacy classification",
|
|
11
|
+
description:
|
|
12
|
+
"Choose the approved model and engine that review captures before storage. Until configured, deterministic-clean content can be stored but uncertain content is quarantined and unavailable to search or agents.",
|
|
13
|
+
methods: [
|
|
14
|
+
{
|
|
15
|
+
id: "settings",
|
|
16
|
+
kind: "link",
|
|
17
|
+
primary: true,
|
|
18
|
+
label: "Open Brain privacy settings",
|
|
19
|
+
payload: { url: "/settings", external: false },
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
isComplete: async () => {
|
|
23
|
+
try {
|
|
24
|
+
return brainPrivacyReadiness(await readBrainSettings()).configured;
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
});
|