@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,9 +1,15 @@
|
|
|
1
1
|
import { readAppState } from "@agent-native/core/application-state";
|
|
2
|
+
import {
|
|
3
|
+
deletePrivateBlob,
|
|
4
|
+
putPrivateBlob,
|
|
5
|
+
type PrivateBlobHandle,
|
|
6
|
+
} from "@agent-native/core/private-blob";
|
|
2
7
|
import {
|
|
3
8
|
resourceDeleteByPath,
|
|
4
9
|
resourcePut,
|
|
5
10
|
SHARED_OWNER,
|
|
6
11
|
} from "@agent-native/core/resources/store";
|
|
12
|
+
import { encryptSecretValue } from "@agent-native/core/secrets";
|
|
7
13
|
import {
|
|
8
14
|
getRequestOrgId,
|
|
9
15
|
getRequestUserEmail,
|
|
@@ -15,7 +21,18 @@ import {
|
|
|
15
21
|
resolveAccess,
|
|
16
22
|
type ResolvedAccess,
|
|
17
23
|
} from "@agent-native/core/sharing";
|
|
18
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
and,
|
|
26
|
+
desc,
|
|
27
|
+
eq,
|
|
28
|
+
inArray,
|
|
29
|
+
isNull,
|
|
30
|
+
like,
|
|
31
|
+
lte,
|
|
32
|
+
notExists,
|
|
33
|
+
or,
|
|
34
|
+
sql,
|
|
35
|
+
} from "drizzle-orm";
|
|
19
36
|
|
|
20
37
|
import {
|
|
21
38
|
DEFAULT_BRAIN_SETTINGS,
|
|
@@ -31,7 +48,20 @@ import {
|
|
|
31
48
|
type BrainSourceStatus,
|
|
32
49
|
} from "../../shared/types.js";
|
|
33
50
|
import { getDb, schema } from "../db/index.js";
|
|
51
|
+
import {
|
|
52
|
+
ensureCaptureAudience,
|
|
53
|
+
ensureEvidenceIntersectionAudience,
|
|
54
|
+
listAccessibleAudienceIds,
|
|
55
|
+
} from "./audiences.js";
|
|
34
56
|
import { sanitizeCaptureForStorage } from "./capture-sanitization.js";
|
|
57
|
+
import {
|
|
58
|
+
enqueueCaptureInvalidation,
|
|
59
|
+
enqueueBrainOperation,
|
|
60
|
+
} from "./ingest-queue.js";
|
|
61
|
+
import type {
|
|
62
|
+
BrainAudienceAssignment,
|
|
63
|
+
BrainSensitivityDecision,
|
|
64
|
+
} from "./search-index-contracts.js";
|
|
35
65
|
|
|
36
66
|
export const BRAIN_SETTINGS_KEY = "brain-settings";
|
|
37
67
|
|
|
@@ -95,7 +125,12 @@ export function safeCitationUrl(value: unknown): string | null {
|
|
|
95
125
|
if (typeof value !== "string") return null;
|
|
96
126
|
const trimmed = value.trim();
|
|
97
127
|
if (!trimmed || isDirectGranolaSummaryUrl(trimmed)) return null;
|
|
98
|
-
|
|
128
|
+
try {
|
|
129
|
+
const url = new URL(trimmed);
|
|
130
|
+
return url.protocol === "https:" ? url.toString() : null;
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
99
134
|
}
|
|
100
135
|
|
|
101
136
|
export function sourceUrlFromMetadataRecord(
|
|
@@ -429,6 +464,8 @@ export function serializeKnowledge(
|
|
|
429
464
|
id: row.id,
|
|
430
465
|
sourceId: row.sourceId,
|
|
431
466
|
captureId: row.captureId,
|
|
467
|
+
audienceId: row.audienceId,
|
|
468
|
+
audienceAclHash: row.audienceAclHash,
|
|
432
469
|
kind: row.kind as BrainKnowledgeKind,
|
|
433
470
|
title: row.title,
|
|
434
471
|
body: row.body,
|
|
@@ -464,6 +501,8 @@ export function serializeProposal(
|
|
|
464
501
|
knowledgeId: row.knowledgeId,
|
|
465
502
|
sourceId: row.sourceId,
|
|
466
503
|
captureId: row.captureId,
|
|
504
|
+
audienceId: row.audienceId,
|
|
505
|
+
audienceAclHash: row.audienceAclHash,
|
|
467
506
|
title: row.title,
|
|
468
507
|
body: row.body,
|
|
469
508
|
rationale: row.rationale,
|
|
@@ -505,15 +544,46 @@ export async function getAccessibleCapture(captureId: string) {
|
|
|
505
544
|
if (!capture) return null;
|
|
506
545
|
const sourceAccess = await resolveAccess("brain-source", capture.sourceId);
|
|
507
546
|
if (!sourceAccess) return null;
|
|
547
|
+
if (capture.sensitivityDisposition !== "allowed") return null;
|
|
548
|
+
const audienceIds = await listAccessibleAudienceIds([capture.sourceId]);
|
|
549
|
+
if (!audienceIds.length) return null;
|
|
550
|
+
const [captureAudience] = await db
|
|
551
|
+
.select({ id: schema.brainCaptureAudiences.id })
|
|
552
|
+
.from(schema.brainCaptureAudiences)
|
|
553
|
+
.where(
|
|
554
|
+
and(
|
|
555
|
+
eq(schema.brainCaptureAudiences.captureId, captureId),
|
|
556
|
+
inArray(schema.brainCaptureAudiences.audienceId, audienceIds),
|
|
557
|
+
),
|
|
558
|
+
)
|
|
559
|
+
.limit(1);
|
|
560
|
+
if (!captureAudience) return null;
|
|
508
561
|
return { capture, source: sourceAccess.resource, role: sourceAccess.role };
|
|
509
562
|
}
|
|
510
563
|
|
|
564
|
+
export async function assertDerivedAudienceAccess(row: {
|
|
565
|
+
captureId?: string | null;
|
|
566
|
+
audienceId?: string | null;
|
|
567
|
+
sourceId?: string | null;
|
|
568
|
+
}) {
|
|
569
|
+
if (!row.captureId) return;
|
|
570
|
+
if (!row.audienceId || !row.sourceId) {
|
|
571
|
+
throw new Error("No access to derived Brain evidence");
|
|
572
|
+
}
|
|
573
|
+
const sourceAccess = await resolveAccess("brain-source", row.sourceId);
|
|
574
|
+
if (!sourceAccess) throw new Error("No access to derived Brain evidence");
|
|
575
|
+
const audienceIds = await listAccessibleAudienceIds([row.sourceId]);
|
|
576
|
+
if (!audienceIds.includes(row.audienceId)) {
|
|
577
|
+
throw new Error("No access to derived Brain evidence");
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
511
581
|
export async function createSource(values: {
|
|
512
582
|
id?: string;
|
|
513
583
|
title: string;
|
|
514
584
|
provider: BrainSourceProvider;
|
|
515
585
|
config?: Record<string, unknown>;
|
|
516
|
-
visibility?: "private" | "org"
|
|
586
|
+
visibility?: "private" | "org";
|
|
517
587
|
}) {
|
|
518
588
|
const db = getDb();
|
|
519
589
|
const now = nowIso();
|
|
@@ -577,6 +647,52 @@ function isUniqueConflict(error: unknown): boolean {
|
|
|
577
647
|
return /unique constraint|duplicate key|unique/i.test(message);
|
|
578
648
|
}
|
|
579
649
|
|
|
650
|
+
const UPSTREAM_DELETED_POLICY_VERSION = "upstream-deleted-v1";
|
|
651
|
+
|
|
652
|
+
async function upstreamDeletionLocatorHmac(
|
|
653
|
+
sourceId: string,
|
|
654
|
+
externalId: string,
|
|
655
|
+
) {
|
|
656
|
+
return sha256Hex(`${sourceId}\0${externalId}`);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
async function findUpstreamDeletionReceipt(
|
|
660
|
+
sourceId: string,
|
|
661
|
+
externalId: string,
|
|
662
|
+
): Promise<BrainSensitivityReceipt | null> {
|
|
663
|
+
const [receipt] = await getDb()
|
|
664
|
+
.select({
|
|
665
|
+
id: schema.brainSensitivityEvents.id,
|
|
666
|
+
sourceId: schema.brainSensitivityEvents.sourceId,
|
|
667
|
+
})
|
|
668
|
+
.from(schema.brainSensitivityEvents)
|
|
669
|
+
.where(
|
|
670
|
+
and(
|
|
671
|
+
eq(
|
|
672
|
+
schema.brainSensitivityEvents.locatorHmac,
|
|
673
|
+
await upstreamDeletionLocatorHmac(sourceId, externalId),
|
|
674
|
+
),
|
|
675
|
+
eq(
|
|
676
|
+
schema.brainSensitivityEvents.policyVersion,
|
|
677
|
+
UPSTREAM_DELETED_POLICY_VERSION,
|
|
678
|
+
),
|
|
679
|
+
eq(schema.brainSensitivityEvents.disposition, "suppressed"),
|
|
680
|
+
),
|
|
681
|
+
)
|
|
682
|
+
.limit(1);
|
|
683
|
+
return receipt
|
|
684
|
+
? {
|
|
685
|
+
id: receipt.id,
|
|
686
|
+
sourceId: receipt.sourceId,
|
|
687
|
+
disposition: "suppressed",
|
|
688
|
+
categories: [],
|
|
689
|
+
confidenceBand: "deterministic",
|
|
690
|
+
policyVersion: UPSTREAM_DELETED_POLICY_VERSION,
|
|
691
|
+
expiresAt: null,
|
|
692
|
+
}
|
|
693
|
+
: null;
|
|
694
|
+
}
|
|
695
|
+
|
|
580
696
|
export async function createCapture(values: {
|
|
581
697
|
id?: string;
|
|
582
698
|
sourceId: string;
|
|
@@ -587,6 +703,11 @@ export async function createCapture(values: {
|
|
|
587
703
|
metadata?: Record<string, unknown>;
|
|
588
704
|
capturedAt?: string;
|
|
589
705
|
status?: "queued" | "distilling" | "distilled" | "ignored";
|
|
706
|
+
audience?: {
|
|
707
|
+
kind: BrainAudienceAssignment["kind"];
|
|
708
|
+
memberEmails?: string[];
|
|
709
|
+
upstreamRefHash?: string | null;
|
|
710
|
+
};
|
|
590
711
|
}) {
|
|
591
712
|
const sourceAccess = await getAccessibleSource(values.sourceId, "editor");
|
|
592
713
|
const source =
|
|
@@ -594,18 +715,23 @@ export async function createCapture(values: {
|
|
|
594
715
|
const db = getDb();
|
|
595
716
|
const now = nowIso();
|
|
596
717
|
const id = values.id ?? nanoid();
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
718
|
+
const [existing] = values.externalId
|
|
719
|
+
? await db
|
|
720
|
+
.select()
|
|
721
|
+
.from(schema.brainRawCaptures)
|
|
722
|
+
.where(
|
|
723
|
+
and(
|
|
724
|
+
eq(schema.brainRawCaptures.sourceId, values.sourceId),
|
|
725
|
+
eq(schema.brainRawCaptures.externalId, values.externalId),
|
|
726
|
+
),
|
|
727
|
+
)
|
|
728
|
+
.limit(1)
|
|
729
|
+
: [];
|
|
730
|
+
const upstreamDeletionReceipt = values.externalId
|
|
731
|
+
? await findUpstreamDeletionReceipt(values.sourceId, values.externalId)
|
|
732
|
+
: null;
|
|
733
|
+
if (upstreamDeletionReceipt) {
|
|
734
|
+
throw new BrainCaptureBlockedError(upstreamDeletionReceipt);
|
|
609
735
|
}
|
|
610
736
|
const settings = await readBrainSettings();
|
|
611
737
|
const metadata = sanitizeGranolaCaptureMetadataLinks(
|
|
@@ -627,24 +753,62 @@ export async function createCapture(values: {
|
|
|
627
753
|
sourceConfig: parseJson<Record<string, unknown>>(source.configJson, {}),
|
|
628
754
|
settings,
|
|
629
755
|
});
|
|
630
|
-
|
|
631
|
-
await
|
|
756
|
+
if (!sanitized.decision || sanitized.decision.disposition !== "allowed") {
|
|
757
|
+
const receipt = await recordBlockedCapture({
|
|
632
758
|
id,
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
759
|
+
existing: existing ?? null,
|
|
760
|
+
source,
|
|
761
|
+
values,
|
|
762
|
+
decision: sanitized.decision ?? {
|
|
763
|
+
disposition: "quarantined",
|
|
764
|
+
categories: [],
|
|
765
|
+
confidenceBand: "uncertain",
|
|
766
|
+
policyVersion: "1",
|
|
767
|
+
safeSegments: [],
|
|
768
|
+
safeContent: "",
|
|
769
|
+
classifier: "deterministic",
|
|
770
|
+
},
|
|
771
|
+
retentionHours: settings.quarantineRetentionHours ?? 72,
|
|
646
772
|
});
|
|
773
|
+
throw new BrainCaptureBlockedError(receipt);
|
|
774
|
+
}
|
|
775
|
+
const nextContentHash = await contentHash(sanitized.content);
|
|
776
|
+
const nextCapturedAt = values.capturedAt ?? existing?.capturedAt ?? now;
|
|
777
|
+
const contentChanged = existing?.contentHash !== nextContentHash;
|
|
778
|
+
const indexedMetadataChanged = Boolean(
|
|
779
|
+
existing &&
|
|
780
|
+
(existing.title !== sanitized.title ||
|
|
781
|
+
existing.capturedAt !== nextCapturedAt),
|
|
782
|
+
);
|
|
783
|
+
const captureId = existing?.id ?? id;
|
|
784
|
+
try {
|
|
785
|
+
if (!existing) {
|
|
786
|
+
await db.insert(schema.brainRawCaptures).values({
|
|
787
|
+
id,
|
|
788
|
+
sourceId: values.sourceId,
|
|
789
|
+
externalId: values.externalId ?? null,
|
|
790
|
+
title: sanitized.title,
|
|
791
|
+
kind: values.kind,
|
|
792
|
+
content: sanitized.content,
|
|
793
|
+
contentHash: nextContentHash,
|
|
794
|
+
metadataJson: stableJson(sanitized.metadata),
|
|
795
|
+
capturedAt: nextCapturedAt,
|
|
796
|
+
importedBy: requireUserEmail(),
|
|
797
|
+
status: values.status ?? "queued",
|
|
798
|
+
distilledAt: values.status === "distilled" ? now : null,
|
|
799
|
+
sensitivityDisposition: "pending",
|
|
800
|
+
sensitivityPolicyVersion: sanitized.decision.policyVersion,
|
|
801
|
+
audienceAclHash: null,
|
|
802
|
+
createdAt: now,
|
|
803
|
+
updatedAt: now,
|
|
804
|
+
});
|
|
805
|
+
}
|
|
647
806
|
} catch (err) {
|
|
807
|
+
if (!existing) {
|
|
808
|
+
await db
|
|
809
|
+
.delete(schema.brainCaptureAudiences)
|
|
810
|
+
.where(eq(schema.brainCaptureAudiences.captureId, captureId));
|
|
811
|
+
}
|
|
648
812
|
if (values.externalId && isUniqueConflict(err)) {
|
|
649
813
|
const [existing] = await db
|
|
650
814
|
.select()
|
|
@@ -660,14 +824,538 @@ export async function createCapture(values: {
|
|
|
660
824
|
}
|
|
661
825
|
throw err;
|
|
662
826
|
}
|
|
827
|
+
const racedDeletionReceipt = values.externalId
|
|
828
|
+
? await findUpstreamDeletionReceipt(values.sourceId, values.externalId)
|
|
829
|
+
: null;
|
|
830
|
+
if (racedDeletionReceipt) {
|
|
831
|
+
await retireUpstreamDeletedCapture({
|
|
832
|
+
sourceId: values.sourceId,
|
|
833
|
+
externalId: values.externalId!,
|
|
834
|
+
provider: source.provider as BrainSourceProvider,
|
|
835
|
+
});
|
|
836
|
+
throw new BrainCaptureBlockedError(racedDeletionReceipt);
|
|
837
|
+
}
|
|
838
|
+
const audience = await ensureCaptureAudience({
|
|
839
|
+
captureId,
|
|
840
|
+
source,
|
|
841
|
+
kind: values.audience?.kind,
|
|
842
|
+
memberEmails:
|
|
843
|
+
values.audience?.memberEmails ??
|
|
844
|
+
(source.visibility === "org" ? undefined : [source.ownerEmail]),
|
|
845
|
+
upstreamRefHash: values.audience?.upstreamRefHash,
|
|
846
|
+
});
|
|
847
|
+
const finalizationClauses = [eq(schema.brainRawCaptures.id, captureId)];
|
|
848
|
+
if (values.externalId) {
|
|
849
|
+
const locatorHmac = await upstreamDeletionLocatorHmac(
|
|
850
|
+
values.sourceId,
|
|
851
|
+
values.externalId,
|
|
852
|
+
);
|
|
853
|
+
finalizationClauses.push(
|
|
854
|
+
notExists(
|
|
855
|
+
db
|
|
856
|
+
.select({ id: schema.brainSensitivityEvents.id })
|
|
857
|
+
.from(schema.brainSensitivityEvents)
|
|
858
|
+
.where(
|
|
859
|
+
and(
|
|
860
|
+
eq(schema.brainSensitivityEvents.locatorHmac, locatorHmac),
|
|
861
|
+
eq(
|
|
862
|
+
schema.brainSensitivityEvents.policyVersion,
|
|
863
|
+
UPSTREAM_DELETED_POLICY_VERSION,
|
|
864
|
+
),
|
|
865
|
+
eq(schema.brainSensitivityEvents.disposition, "suppressed"),
|
|
866
|
+
),
|
|
867
|
+
),
|
|
868
|
+
),
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
const finalized = await db
|
|
872
|
+
.update(schema.brainRawCaptures)
|
|
873
|
+
.set({
|
|
874
|
+
title: sanitized.title,
|
|
875
|
+
kind: values.kind,
|
|
876
|
+
content: sanitized.content,
|
|
877
|
+
contentHash: nextContentHash,
|
|
878
|
+
metadataJson: stableJson(sanitized.metadata),
|
|
879
|
+
capturedAt: nextCapturedAt,
|
|
880
|
+
status: values.status ?? "queued",
|
|
881
|
+
distilledAt: values.status === "distilled" ? now : null,
|
|
882
|
+
sensitivityDisposition: "allowed",
|
|
883
|
+
sensitivityPolicyVersion: sanitized.decision.policyVersion,
|
|
884
|
+
audienceAclHash: audience.aclHash,
|
|
885
|
+
updatedAt: now,
|
|
886
|
+
})
|
|
887
|
+
.where(and(...finalizationClauses));
|
|
888
|
+
if (finalized.rowsAffected === 0 && values.externalId) {
|
|
889
|
+
await db
|
|
890
|
+
.delete(schema.brainCaptureAudiences)
|
|
891
|
+
.where(eq(schema.brainCaptureAudiences.captureId, captureId));
|
|
892
|
+
await retireUpstreamDeletedCapture({
|
|
893
|
+
sourceId: values.sourceId,
|
|
894
|
+
externalId: values.externalId,
|
|
895
|
+
provider: source.provider as BrainSourceProvider,
|
|
896
|
+
});
|
|
897
|
+
const receipt = await findUpstreamDeletionReceipt(
|
|
898
|
+
values.sourceId,
|
|
899
|
+
values.externalId,
|
|
900
|
+
);
|
|
901
|
+
if (receipt) throw new BrainCaptureBlockedError(receipt);
|
|
902
|
+
throw new Error("Capture finalization was blocked");
|
|
903
|
+
}
|
|
904
|
+
if (existing?.contentHash && contentChanged) {
|
|
905
|
+
await invalidateDerivedForCapture(captureId);
|
|
906
|
+
}
|
|
663
907
|
const [capture] = await db
|
|
664
908
|
.select()
|
|
665
909
|
.from(schema.brainRawCaptures)
|
|
666
|
-
.where(eq(schema.brainRawCaptures.id,
|
|
910
|
+
.where(eq(schema.brainRawCaptures.id, captureId))
|
|
667
911
|
.limit(1);
|
|
912
|
+
const invalidationReason =
|
|
913
|
+
!existing || contentChanged || indexedMetadataChanged
|
|
914
|
+
? "content-changed"
|
|
915
|
+
: existing.audienceAclHash !== audience.aclHash
|
|
916
|
+
? "access-changed"
|
|
917
|
+
: existing.sensitivityPolicyVersion !== sanitized.decision.policyVersion
|
|
918
|
+
? "sensitivity-changed"
|
|
919
|
+
: null;
|
|
920
|
+
if (!invalidationReason) return capture;
|
|
921
|
+
try {
|
|
922
|
+
await enqueueCaptureInvalidation({
|
|
923
|
+
captureId,
|
|
924
|
+
sourceId: source.id,
|
|
925
|
+
reason: invalidationReason,
|
|
926
|
+
previous: existing
|
|
927
|
+
? {
|
|
928
|
+
contentHash: existing.contentHash ?? undefined,
|
|
929
|
+
sensitivityPolicyVersion:
|
|
930
|
+
existing.sensitivityPolicyVersion ?? undefined,
|
|
931
|
+
aclHash: existing.audienceAclHash ?? undefined,
|
|
932
|
+
}
|
|
933
|
+
: undefined,
|
|
934
|
+
next: {
|
|
935
|
+
contentHash: nextContentHash,
|
|
936
|
+
sensitivityPolicyVersion: sanitized.decision.policyVersion,
|
|
937
|
+
aclHash: audience.aclHash,
|
|
938
|
+
},
|
|
939
|
+
});
|
|
940
|
+
} catch (error) {
|
|
941
|
+
const { unindexBrainCapture } = await import("./search-index.js");
|
|
942
|
+
await unindexBrainCapture(captureId);
|
|
943
|
+
throw error;
|
|
944
|
+
}
|
|
668
945
|
return capture;
|
|
669
946
|
}
|
|
670
947
|
|
|
948
|
+
export interface BrainSensitivityReceipt {
|
|
949
|
+
id: string;
|
|
950
|
+
sourceId: string;
|
|
951
|
+
disposition: "suppressed" | "quarantined";
|
|
952
|
+
categories: BrainSensitivityDecision["categories"];
|
|
953
|
+
confidenceBand: BrainSensitivityDecision["confidenceBand"];
|
|
954
|
+
policyVersion: string;
|
|
955
|
+
expiresAt: string | null;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
export class BrainCaptureBlockedError extends Error {
|
|
959
|
+
constructor(public readonly receipt: BrainSensitivityReceipt) {
|
|
960
|
+
super(`Capture ${receipt.disposition} by Brain privacy policy`);
|
|
961
|
+
this.name = "BrainCaptureBlockedError";
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
export async function retireUpstreamDeletedCapture(input: {
|
|
966
|
+
sourceId: string;
|
|
967
|
+
externalId: string;
|
|
968
|
+
provider: BrainSourceProvider;
|
|
969
|
+
}) {
|
|
970
|
+
const db = getDb();
|
|
971
|
+
const now = nowIso();
|
|
972
|
+
const locatorHmac = await upstreamDeletionLocatorHmac(
|
|
973
|
+
input.sourceId,
|
|
974
|
+
input.externalId,
|
|
975
|
+
);
|
|
976
|
+
const eventId = `sensitivity_${nanoid(18)}`;
|
|
977
|
+
const emptyContentHash = await contentHash("");
|
|
978
|
+
const [captureBeforeRetirement] = await db
|
|
979
|
+
.select({ id: schema.brainRawCaptures.id })
|
|
980
|
+
.from(schema.brainRawCaptures)
|
|
981
|
+
.where(
|
|
982
|
+
and(
|
|
983
|
+
eq(schema.brainRawCaptures.sourceId, input.sourceId),
|
|
984
|
+
eq(schema.brainRawCaptures.externalId, input.externalId),
|
|
985
|
+
),
|
|
986
|
+
)
|
|
987
|
+
.limit(1);
|
|
988
|
+
if (captureBeforeRetirement) {
|
|
989
|
+
const knowledge = await db
|
|
990
|
+
.select({
|
|
991
|
+
publishedResourcePath: schema.brainKnowledge.publishedResourcePath,
|
|
992
|
+
})
|
|
993
|
+
.from(schema.brainKnowledge)
|
|
994
|
+
.where(
|
|
995
|
+
or(
|
|
996
|
+
eq(schema.brainKnowledge.captureId, captureBeforeRetirement.id),
|
|
997
|
+
like(
|
|
998
|
+
schema.brainKnowledge.evidenceJson,
|
|
999
|
+
`%${captureBeforeRetirement.id}%`,
|
|
1000
|
+
),
|
|
1001
|
+
),
|
|
1002
|
+
);
|
|
1003
|
+
for (const row of knowledge) {
|
|
1004
|
+
if (row.publishedResourcePath) {
|
|
1005
|
+
await resourceDeleteByPath(SHARED_OWNER, row.publishedResourcePath);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
let capture:
|
|
1010
|
+
| {
|
|
1011
|
+
id: string;
|
|
1012
|
+
contentHash: string | null;
|
|
1013
|
+
sensitivityPolicyVersion: string | null;
|
|
1014
|
+
audienceAclHash: string | null;
|
|
1015
|
+
}
|
|
1016
|
+
| undefined;
|
|
1017
|
+
await db.transaction(async (tx) => {
|
|
1018
|
+
await tx
|
|
1019
|
+
.insert(schema.brainSensitivityEvents)
|
|
1020
|
+
.values({
|
|
1021
|
+
id: eventId,
|
|
1022
|
+
sourceId: input.sourceId,
|
|
1023
|
+
captureId: null,
|
|
1024
|
+
locatorHmac,
|
|
1025
|
+
disposition: "suppressed",
|
|
1026
|
+
categoriesJson: "[]",
|
|
1027
|
+
confidenceBand: "deterministic",
|
|
1028
|
+
policyVersion: UPSTREAM_DELETED_POLICY_VERSION,
|
|
1029
|
+
upstreamProvider: input.provider,
|
|
1030
|
+
quarantineBlobHandle: null,
|
|
1031
|
+
expiresAt: null,
|
|
1032
|
+
createdAt: now,
|
|
1033
|
+
updatedAt: now,
|
|
1034
|
+
})
|
|
1035
|
+
.onConflictDoUpdate({
|
|
1036
|
+
target: [
|
|
1037
|
+
schema.brainSensitivityEvents.locatorHmac,
|
|
1038
|
+
schema.brainSensitivityEvents.policyVersion,
|
|
1039
|
+
],
|
|
1040
|
+
set: {
|
|
1041
|
+
disposition: "suppressed",
|
|
1042
|
+
quarantineBlobHandle: null,
|
|
1043
|
+
expiresAt: null,
|
|
1044
|
+
updatedAt: now,
|
|
1045
|
+
},
|
|
1046
|
+
});
|
|
1047
|
+
[capture] = await tx
|
|
1048
|
+
.select({
|
|
1049
|
+
id: schema.brainRawCaptures.id,
|
|
1050
|
+
contentHash: schema.brainRawCaptures.contentHash,
|
|
1051
|
+
sensitivityPolicyVersion:
|
|
1052
|
+
schema.brainRawCaptures.sensitivityPolicyVersion,
|
|
1053
|
+
audienceAclHash: schema.brainRawCaptures.audienceAclHash,
|
|
1054
|
+
})
|
|
1055
|
+
.from(schema.brainRawCaptures)
|
|
1056
|
+
.where(
|
|
1057
|
+
and(
|
|
1058
|
+
eq(schema.brainRawCaptures.sourceId, input.sourceId),
|
|
1059
|
+
eq(schema.brainRawCaptures.externalId, input.externalId),
|
|
1060
|
+
),
|
|
1061
|
+
)
|
|
1062
|
+
.limit(1);
|
|
1063
|
+
if (!capture) return;
|
|
1064
|
+
|
|
1065
|
+
await tx
|
|
1066
|
+
.update(schema.brainSensitivityEvents)
|
|
1067
|
+
.set({ captureId: capture.id, updatedAt: now })
|
|
1068
|
+
.where(
|
|
1069
|
+
and(
|
|
1070
|
+
eq(schema.brainSensitivityEvents.locatorHmac, locatorHmac),
|
|
1071
|
+
eq(
|
|
1072
|
+
schema.brainSensitivityEvents.policyVersion,
|
|
1073
|
+
UPSTREAM_DELETED_POLICY_VERSION,
|
|
1074
|
+
),
|
|
1075
|
+
),
|
|
1076
|
+
);
|
|
1077
|
+
await tx
|
|
1078
|
+
.update(schema.brainRawCaptures)
|
|
1079
|
+
.set({
|
|
1080
|
+
title: "Deleted upstream capture",
|
|
1081
|
+
content: "",
|
|
1082
|
+
contentHash: emptyContentHash,
|
|
1083
|
+
metadataJson: "{}",
|
|
1084
|
+
status: "ignored",
|
|
1085
|
+
sensitivityDisposition: "pending",
|
|
1086
|
+
audienceAclHash: null,
|
|
1087
|
+
updatedAt: now,
|
|
1088
|
+
})
|
|
1089
|
+
.where(eq(schema.brainRawCaptures.id, capture.id));
|
|
1090
|
+
await tx
|
|
1091
|
+
.delete(schema.brainCaptureAudiences)
|
|
1092
|
+
.where(eq(schema.brainCaptureAudiences.captureId, capture.id));
|
|
1093
|
+
await invalidateDerivedForCapture(
|
|
1094
|
+
capture.id,
|
|
1095
|
+
tx as unknown as ReturnType<typeof getDb>,
|
|
1096
|
+
{ deletePublishedResources: false },
|
|
1097
|
+
);
|
|
1098
|
+
});
|
|
1099
|
+
if (!capture) return false;
|
|
1100
|
+
await enqueueCaptureInvalidation({
|
|
1101
|
+
captureId: capture.id,
|
|
1102
|
+
sourceId: input.sourceId,
|
|
1103
|
+
reason: "upstream-deleted",
|
|
1104
|
+
previous: {
|
|
1105
|
+
contentHash: capture.contentHash ?? undefined,
|
|
1106
|
+
sensitivityPolicyVersion: capture.sensitivityPolicyVersion ?? undefined,
|
|
1107
|
+
aclHash: capture.audienceAclHash ?? undefined,
|
|
1108
|
+
},
|
|
1109
|
+
});
|
|
1110
|
+
return true;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function privateQuarantineProvider(provider: BrainSourceProvider) {
|
|
1114
|
+
return provider === "generic" || provider === "clips";
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
async function deleteQuarantineHandle(value: string | null | undefined) {
|
|
1118
|
+
if (!value) return;
|
|
1119
|
+
try {
|
|
1120
|
+
await deletePrivateBlob(JSON.parse(value) as PrivateBlobHandle);
|
|
1121
|
+
} catch {}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
export async function invalidateDerivedForCapture(
|
|
1125
|
+
captureId: string,
|
|
1126
|
+
db = getDb(),
|
|
1127
|
+
options: { deletePublishedResources?: boolean } = {},
|
|
1128
|
+
) {
|
|
1129
|
+
const needle = `%${captureId}%`;
|
|
1130
|
+
const knowledge = await db
|
|
1131
|
+
.select({
|
|
1132
|
+
id: schema.brainKnowledge.id,
|
|
1133
|
+
publishedResourcePath: schema.brainKnowledge.publishedResourcePath,
|
|
1134
|
+
})
|
|
1135
|
+
.from(schema.brainKnowledge)
|
|
1136
|
+
.where(
|
|
1137
|
+
or(
|
|
1138
|
+
eq(schema.brainKnowledge.captureId, captureId),
|
|
1139
|
+
like(schema.brainKnowledge.evidenceJson, needle),
|
|
1140
|
+
),
|
|
1141
|
+
);
|
|
1142
|
+
if (options.deletePublishedResources !== false) {
|
|
1143
|
+
for (const row of knowledge) {
|
|
1144
|
+
if (row.publishedResourcePath) {
|
|
1145
|
+
await resourceDeleteByPath(SHARED_OWNER, row.publishedResourcePath);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
if (knowledge.length) {
|
|
1150
|
+
await db
|
|
1151
|
+
.update(schema.brainKnowledge)
|
|
1152
|
+
.set({
|
|
1153
|
+
title: "Redacted Brain knowledge",
|
|
1154
|
+
summary: "",
|
|
1155
|
+
body: "",
|
|
1156
|
+
evidenceJson: "[]",
|
|
1157
|
+
status: "redacted",
|
|
1158
|
+
publishedResourcePath: null,
|
|
1159
|
+
updatedAt: nowIso(),
|
|
1160
|
+
})
|
|
1161
|
+
.where(
|
|
1162
|
+
inArray(
|
|
1163
|
+
schema.brainKnowledge.id,
|
|
1164
|
+
knowledge.map((row) => row.id),
|
|
1165
|
+
),
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
const proposals = await db
|
|
1169
|
+
.select({ id: schema.brainProposals.id })
|
|
1170
|
+
.from(schema.brainProposals)
|
|
1171
|
+
.where(
|
|
1172
|
+
or(
|
|
1173
|
+
eq(schema.brainProposals.captureId, captureId),
|
|
1174
|
+
like(schema.brainProposals.evidenceJson, needle),
|
|
1175
|
+
),
|
|
1176
|
+
);
|
|
1177
|
+
if (proposals.length) {
|
|
1178
|
+
await db
|
|
1179
|
+
.update(schema.brainProposals)
|
|
1180
|
+
.set({
|
|
1181
|
+
title: "Redacted Brain proposal",
|
|
1182
|
+
body: "",
|
|
1183
|
+
rationale: "",
|
|
1184
|
+
payloadJson: "{}",
|
|
1185
|
+
evidenceJson: "[]",
|
|
1186
|
+
status: "rejected",
|
|
1187
|
+
updatedAt: nowIso(),
|
|
1188
|
+
})
|
|
1189
|
+
.where(
|
|
1190
|
+
inArray(
|
|
1191
|
+
schema.brainProposals.id,
|
|
1192
|
+
proposals.map((row) => row.id),
|
|
1193
|
+
),
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
export async function recordBlockedCapture(input: {
|
|
1199
|
+
id: string;
|
|
1200
|
+
existing: typeof schema.brainRawCaptures.$inferSelect | null;
|
|
1201
|
+
source: typeof schema.brainSources.$inferSelect;
|
|
1202
|
+
values: Parameters<typeof createCapture>[0];
|
|
1203
|
+
decision: BrainSensitivityDecision;
|
|
1204
|
+
retentionHours: number;
|
|
1205
|
+
}): Promise<BrainSensitivityReceipt> {
|
|
1206
|
+
const db = getDb();
|
|
1207
|
+
const now = nowIso();
|
|
1208
|
+
const locatorHmac = await sha256Hex(
|
|
1209
|
+
`${input.source.id}\0${input.values.externalId ?? (await contentHash(input.values.content))}`,
|
|
1210
|
+
);
|
|
1211
|
+
const [prior] = await db
|
|
1212
|
+
.select({
|
|
1213
|
+
id: schema.brainSensitivityEvents.id,
|
|
1214
|
+
quarantineBlobHandle: schema.brainSensitivityEvents.quarantineBlobHandle,
|
|
1215
|
+
})
|
|
1216
|
+
.from(schema.brainSensitivityEvents)
|
|
1217
|
+
.where(
|
|
1218
|
+
and(
|
|
1219
|
+
eq(schema.brainSensitivityEvents.locatorHmac, locatorHmac),
|
|
1220
|
+
eq(
|
|
1221
|
+
schema.brainSensitivityEvents.policyVersion,
|
|
1222
|
+
input.decision.policyVersion,
|
|
1223
|
+
),
|
|
1224
|
+
),
|
|
1225
|
+
)
|
|
1226
|
+
.limit(1);
|
|
1227
|
+
await deleteQuarantineHandle(prior?.quarantineBlobHandle);
|
|
1228
|
+
let disposition: "suppressed" | "quarantined" =
|
|
1229
|
+
input.decision.disposition === "suppressed" ? "suppressed" : "quarantined";
|
|
1230
|
+
let quarantineBlobHandle: string | null = null;
|
|
1231
|
+
let expiresAt: string | null = null;
|
|
1232
|
+
if (
|
|
1233
|
+
disposition === "quarantined" &&
|
|
1234
|
+
privateQuarantineProvider(input.source.provider as BrainSourceProvider)
|
|
1235
|
+
) {
|
|
1236
|
+
const handle = await putPrivateBlob({
|
|
1237
|
+
data: new TextEncoder().encode(encryptSecretValue(input.values.content)),
|
|
1238
|
+
filename: `brain-quarantine-${input.id}.bin`,
|
|
1239
|
+
mimeType: "application/octet-stream",
|
|
1240
|
+
ownerEmail: input.source.ownerEmail,
|
|
1241
|
+
metadata: { app: "brain", kind: "privacy-quarantine" },
|
|
1242
|
+
});
|
|
1243
|
+
if (handle) {
|
|
1244
|
+
quarantineBlobHandle = stableJson(handle);
|
|
1245
|
+
expiresAt = new Date(
|
|
1246
|
+
Date.parse(now) +
|
|
1247
|
+
Math.max(1, Math.min(168, input.retentionHours)) * 60 * 60 * 1000,
|
|
1248
|
+
).toISOString();
|
|
1249
|
+
} else {
|
|
1250
|
+
disposition = "suppressed";
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
const eventId = prior?.id ?? `sensitivity_${nanoid(18)}`;
|
|
1254
|
+
await db
|
|
1255
|
+
.insert(schema.brainSensitivityEvents)
|
|
1256
|
+
.values({
|
|
1257
|
+
id: eventId,
|
|
1258
|
+
sourceId: input.source.id,
|
|
1259
|
+
captureId: input.existing?.id ?? null,
|
|
1260
|
+
locatorHmac,
|
|
1261
|
+
disposition,
|
|
1262
|
+
categoriesJson: stableJson(input.decision.categories),
|
|
1263
|
+
confidenceBand: input.decision.confidenceBand,
|
|
1264
|
+
policyVersion: input.decision.policyVersion,
|
|
1265
|
+
upstreamProvider: input.source.provider,
|
|
1266
|
+
quarantineBlobHandle,
|
|
1267
|
+
expiresAt,
|
|
1268
|
+
createdAt: now,
|
|
1269
|
+
updatedAt: now,
|
|
1270
|
+
})
|
|
1271
|
+
.onConflictDoUpdate({
|
|
1272
|
+
target: [
|
|
1273
|
+
schema.brainSensitivityEvents.locatorHmac,
|
|
1274
|
+
schema.brainSensitivityEvents.policyVersion,
|
|
1275
|
+
],
|
|
1276
|
+
set: {
|
|
1277
|
+
captureId: input.existing?.id ?? null,
|
|
1278
|
+
disposition,
|
|
1279
|
+
categoriesJson: stableJson(input.decision.categories),
|
|
1280
|
+
confidenceBand: input.decision.confidenceBand,
|
|
1281
|
+
quarantineBlobHandle,
|
|
1282
|
+
expiresAt,
|
|
1283
|
+
reviewedBy: null,
|
|
1284
|
+
reviewedAt: null,
|
|
1285
|
+
updatedAt: now,
|
|
1286
|
+
},
|
|
1287
|
+
});
|
|
1288
|
+
if (input.existing) {
|
|
1289
|
+
await invalidateDerivedForCapture(input.existing.id);
|
|
1290
|
+
await db
|
|
1291
|
+
.update(schema.brainRawCaptures)
|
|
1292
|
+
.set({
|
|
1293
|
+
title: "Privacy-blocked capture",
|
|
1294
|
+
content: "",
|
|
1295
|
+
contentHash: await contentHash(""),
|
|
1296
|
+
metadataJson: "{}",
|
|
1297
|
+
status: "ignored",
|
|
1298
|
+
sensitivityDisposition: "pending",
|
|
1299
|
+
sensitivityPolicyVersion: input.decision.policyVersion,
|
|
1300
|
+
audienceAclHash: null,
|
|
1301
|
+
updatedAt: now,
|
|
1302
|
+
})
|
|
1303
|
+
.where(eq(schema.brainRawCaptures.id, input.existing.id));
|
|
1304
|
+
await enqueueBrainOperation({
|
|
1305
|
+
operation: "search-unindex",
|
|
1306
|
+
dedupeKey: `search-unindex:${input.existing.id}:${input.decision.policyVersion}`,
|
|
1307
|
+
sourceId: input.source.id,
|
|
1308
|
+
captureId: input.existing.id,
|
|
1309
|
+
priority: 1,
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
return {
|
|
1313
|
+
id: eventId,
|
|
1314
|
+
sourceId: input.source.id,
|
|
1315
|
+
disposition,
|
|
1316
|
+
categories: input.decision.categories,
|
|
1317
|
+
confidenceBand: input.decision.confidenceBand,
|
|
1318
|
+
policyVersion: input.decision.policyVersion,
|
|
1319
|
+
expiresAt,
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
export async function expireSensitivityQuarantines(at = nowIso()) {
|
|
1324
|
+
const db = getDb();
|
|
1325
|
+
const rows = await db
|
|
1326
|
+
.select({
|
|
1327
|
+
id: schema.brainSensitivityEvents.id,
|
|
1328
|
+
quarantineBlobHandle: schema.brainSensitivityEvents.quarantineBlobHandle,
|
|
1329
|
+
})
|
|
1330
|
+
.from(schema.brainSensitivityEvents)
|
|
1331
|
+
.where(
|
|
1332
|
+
and(
|
|
1333
|
+
eq(schema.brainSensitivityEvents.disposition, "quarantined"),
|
|
1334
|
+
lte(schema.brainSensitivityEvents.expiresAt, at),
|
|
1335
|
+
),
|
|
1336
|
+
);
|
|
1337
|
+
for (const row of rows) {
|
|
1338
|
+
await deleteQuarantineHandle(row.quarantineBlobHandle);
|
|
1339
|
+
}
|
|
1340
|
+
if (rows.length) {
|
|
1341
|
+
await db
|
|
1342
|
+
.update(schema.brainSensitivityEvents)
|
|
1343
|
+
.set({
|
|
1344
|
+
disposition: "expired",
|
|
1345
|
+
quarantineBlobHandle: null,
|
|
1346
|
+
expiresAt: null,
|
|
1347
|
+
updatedAt: at,
|
|
1348
|
+
})
|
|
1349
|
+
.where(
|
|
1350
|
+
inArray(
|
|
1351
|
+
schema.brainSensitivityEvents.id,
|
|
1352
|
+
rows.map((row) => row.id),
|
|
1353
|
+
),
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1356
|
+
return rows.length;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
671
1359
|
export async function validateEvidence(
|
|
672
1360
|
evidence: BrainEvidenceInput[],
|
|
673
1361
|
): Promise<BrainEvidence[]> {
|
|
@@ -891,12 +1579,23 @@ async function publishKnowledgeResource(values: {
|
|
|
891
1579
|
return resource.path;
|
|
892
1580
|
}
|
|
893
1581
|
|
|
1582
|
+
async function canPublishCanonicalAudience(audienceId: string | null) {
|
|
1583
|
+
if (!audienceId) return true;
|
|
1584
|
+
const [audience] = await getDb()
|
|
1585
|
+
.select({ kind: schema.brainAudiences.kind })
|
|
1586
|
+
.from(schema.brainAudiences)
|
|
1587
|
+
.where(eq(schema.brainAudiences.id, audienceId))
|
|
1588
|
+
.limit(1);
|
|
1589
|
+
return audience?.kind === "org";
|
|
1590
|
+
}
|
|
1591
|
+
|
|
894
1592
|
export async function setKnowledgeCanonicalResource(
|
|
895
1593
|
knowledgeId: string,
|
|
896
1594
|
published: boolean,
|
|
897
1595
|
) {
|
|
898
1596
|
const access = await assertAccess("brain-knowledge", knowledgeId, "editor");
|
|
899
1597
|
const row = access.resource;
|
|
1598
|
+
await assertDerivedAudienceAccess(row);
|
|
900
1599
|
const db = getDb();
|
|
901
1600
|
|
|
902
1601
|
if (!published) {
|
|
@@ -908,6 +1607,11 @@ export async function setKnowledgeCanonicalResource(
|
|
|
908
1607
|
.set({ publishedResourcePath: null, updatedAt: nowIso() })
|
|
909
1608
|
.where(eq(schema.brainKnowledge.id, knowledgeId));
|
|
910
1609
|
} else {
|
|
1610
|
+
if (!(await canPublishCanonicalAudience(row.audienceId))) {
|
|
1611
|
+
throw new Error(
|
|
1612
|
+
"Audience-restricted Brain knowledge cannot become workspace-wide canonical context.",
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
911
1615
|
if (row.status !== "published") {
|
|
912
1616
|
throw new Error(
|
|
913
1617
|
"Only published Brain knowledge can become company context.",
|
|
@@ -1051,14 +1755,23 @@ export async function previewKnowledgeCanonicalResource(input: {
|
|
|
1051
1755
|
"viewer",
|
|
1052
1756
|
);
|
|
1053
1757
|
const row = access.resource;
|
|
1758
|
+
await assertDerivedAudienceAccess(row);
|
|
1054
1759
|
const values = canonicalValuesFromKnowledgeRow(row);
|
|
1055
1760
|
const resource = buildCanonicalResource(values);
|
|
1056
1761
|
const warnings: string[] = [];
|
|
1762
|
+
const audienceCanPublish = await canPublishCanonicalAudience(
|
|
1763
|
+
row.audienceId,
|
|
1764
|
+
);
|
|
1057
1765
|
if (row.status !== "published") {
|
|
1058
1766
|
warnings.push(
|
|
1059
1767
|
"Only published Brain knowledge can become company context.",
|
|
1060
1768
|
);
|
|
1061
1769
|
}
|
|
1770
|
+
if (!audienceCanPublish) {
|
|
1771
|
+
warnings.push(
|
|
1772
|
+
"Audience-restricted Brain knowledge cannot become workspace-wide canonical context.",
|
|
1773
|
+
);
|
|
1774
|
+
}
|
|
1062
1775
|
if (input.operation === "unpublish" && !row.publishedResourcePath) {
|
|
1063
1776
|
warnings.push(
|
|
1064
1777
|
"This knowledge is not currently mirrored to workspace context.",
|
|
@@ -1072,7 +1785,7 @@ export async function previewKnowledgeCanonicalResource(input: {
|
|
|
1072
1785
|
pathExact: true,
|
|
1073
1786
|
contentType: resource.contentType,
|
|
1074
1787
|
markdown: resource.markdown,
|
|
1075
|
-
canPublish: row.status === "published",
|
|
1788
|
+
canPublish: row.status === "published" && audienceCanPublish,
|
|
1076
1789
|
alreadyPublishedPath: row.publishedResourcePath,
|
|
1077
1790
|
warnings,
|
|
1078
1791
|
};
|
|
@@ -1084,6 +1797,7 @@ export async function previewKnowledgeCanonicalResource(input: {
|
|
|
1084
1797
|
"viewer",
|
|
1085
1798
|
);
|
|
1086
1799
|
const row = access.resource;
|
|
1800
|
+
await assertDerivedAudienceAccess(row);
|
|
1087
1801
|
const { payload, values } = canonicalValuesFromProposalRow(row, input.draft);
|
|
1088
1802
|
const resource = buildCanonicalResource(values);
|
|
1089
1803
|
const status =
|
|
@@ -1091,6 +1805,7 @@ export async function previewKnowledgeCanonicalResource(input: {
|
|
|
1091
1805
|
? payload.status
|
|
1092
1806
|
: statusForTier(payload.publishTier ?? "company");
|
|
1093
1807
|
const warnings: string[] = [];
|
|
1808
|
+
const audienceCanPublish = await canPublishCanonicalAudience(row.audienceId);
|
|
1094
1809
|
if (status !== "published") {
|
|
1095
1810
|
warnings.push(
|
|
1096
1811
|
"Approving this proposal would not publish canonical context because its resulting knowledge status is not published.",
|
|
@@ -1101,6 +1816,11 @@ export async function previewKnowledgeCanonicalResource(input: {
|
|
|
1101
1816
|
"Approval will assign the final knowledge id, so the Markdown is exact but the path suffix is shown as <new-id>.",
|
|
1102
1817
|
);
|
|
1103
1818
|
}
|
|
1819
|
+
if (!audienceCanPublish) {
|
|
1820
|
+
warnings.push(
|
|
1821
|
+
"Audience-restricted Brain knowledge cannot become workspace-wide canonical context.",
|
|
1822
|
+
);
|
|
1823
|
+
}
|
|
1104
1824
|
if (row.status !== "pending") {
|
|
1105
1825
|
warnings.push(`This proposal is already ${row.status}.`);
|
|
1106
1826
|
}
|
|
@@ -1113,7 +1833,7 @@ export async function previewKnowledgeCanonicalResource(input: {
|
|
|
1113
1833
|
pathExact: resource.pathExact,
|
|
1114
1834
|
contentType: resource.contentType,
|
|
1115
1835
|
markdown: resource.markdown,
|
|
1116
|
-
canPublish: status === "published",
|
|
1836
|
+
canPublish: status === "published" && audienceCanPublish,
|
|
1117
1837
|
warnings,
|
|
1118
1838
|
};
|
|
1119
1839
|
}
|
|
@@ -1125,10 +1845,40 @@ export async function writeKnowledgeRecord(
|
|
|
1125
1845
|
const db = getDb();
|
|
1126
1846
|
const userEmail = requireUserEmail();
|
|
1127
1847
|
const settings = await readBrainSettings();
|
|
1128
|
-
const
|
|
1129
|
-
|
|
1848
|
+
const existingAccess = input.knowledgeId
|
|
1849
|
+
? await assertAccess("brain-knowledge", input.knowledgeId, "editor")
|
|
1850
|
+
: null;
|
|
1851
|
+
const existing = existingAccess?.resource ?? null;
|
|
1852
|
+
if (existing) await assertDerivedAudienceAccess(existing);
|
|
1853
|
+
const tier =
|
|
1854
|
+
input.publishTier ?? existing?.publishTier ?? settings.defaultPublishTier;
|
|
1855
|
+
const evidenceInput =
|
|
1856
|
+
existing?.captureId && !(input.evidence ?? []).length
|
|
1857
|
+
? parseJson<BrainEvidenceInput[]>(existing.evidenceJson, [])
|
|
1858
|
+
: (input.evidence ?? []);
|
|
1859
|
+
if (existing?.captureId && !evidenceInput.length) {
|
|
1860
|
+
throw new Error(
|
|
1861
|
+
"Evidence-derived Brain knowledge cannot clear its evidence",
|
|
1862
|
+
);
|
|
1863
|
+
}
|
|
1864
|
+
const evidence = await validateEvidence(evidenceInput);
|
|
1130
1865
|
const sourceId = evidence[0]?.sourceId ?? null;
|
|
1131
1866
|
const captureId = evidence[0]?.captureId ?? null;
|
|
1867
|
+
const evidenceAudience = evidence.length
|
|
1868
|
+
? await ensureEvidenceIntersectionAudience({
|
|
1869
|
+
captureIds: evidence.map((item) => item.captureId),
|
|
1870
|
+
sourceId: sourceId!,
|
|
1871
|
+
})
|
|
1872
|
+
: null;
|
|
1873
|
+
if (
|
|
1874
|
+
input.publishCanonical &&
|
|
1875
|
+
evidenceAudience &&
|
|
1876
|
+
evidenceAudience.kind !== "org"
|
|
1877
|
+
) {
|
|
1878
|
+
throw new Error(
|
|
1879
|
+
"Audience-restricted Brain evidence cannot become workspace-wide canonical context.",
|
|
1880
|
+
);
|
|
1881
|
+
}
|
|
1132
1882
|
const redacted = applyRedactions({
|
|
1133
1883
|
title: input.title,
|
|
1134
1884
|
body: input.body,
|
|
@@ -1140,13 +1890,9 @@ export async function writeKnowledgeRecord(
|
|
|
1140
1890
|
autoRedactEmails: settings.autoRedactEmails,
|
|
1141
1891
|
});
|
|
1142
1892
|
const now = nowIso();
|
|
1143
|
-
const existingAccess = input.knowledgeId
|
|
1144
|
-
? await assertAccess("brain-knowledge", input.knowledgeId, "editor")
|
|
1145
|
-
: null;
|
|
1146
1893
|
if (input.supersedesId) {
|
|
1147
1894
|
await assertAccess("brain-knowledge", input.supersedesId, "editor");
|
|
1148
1895
|
}
|
|
1149
|
-
const existing = existingAccess?.resource ?? null;
|
|
1150
1896
|
const ownerEmail = existing?.ownerEmail ?? userEmail;
|
|
1151
1897
|
const orgId = existing?.orgId ?? getRequestOrgId() ?? null;
|
|
1152
1898
|
const visibility = visibilityForTier(tier);
|
|
@@ -1188,6 +1934,8 @@ export async function writeKnowledgeRecord(
|
|
|
1188
1934
|
knowledgeId: input.knowledgeId ?? null,
|
|
1189
1935
|
sourceId,
|
|
1190
1936
|
captureId,
|
|
1937
|
+
audienceId: evidenceAudience?.audienceId ?? null,
|
|
1938
|
+
audienceAclHash: evidenceAudience?.aclHash ?? null,
|
|
1191
1939
|
title: redacted.title,
|
|
1192
1940
|
body: redacted.body,
|
|
1193
1941
|
rationale: input.rationale ?? "",
|
|
@@ -1220,6 +1968,8 @@ export async function writeKnowledgeRecord(
|
|
|
1220
1968
|
.set({
|
|
1221
1969
|
sourceId,
|
|
1222
1970
|
captureId,
|
|
1971
|
+
audienceId: evidenceAudience?.audienceId ?? null,
|
|
1972
|
+
audienceAclHash: evidenceAudience?.aclHash ?? null,
|
|
1223
1973
|
kind: input.kind ?? "fact",
|
|
1224
1974
|
title: redacted.title,
|
|
1225
1975
|
body: redacted.body,
|
|
@@ -1243,6 +1993,8 @@ export async function writeKnowledgeRecord(
|
|
|
1243
1993
|
id,
|
|
1244
1994
|
sourceId,
|
|
1245
1995
|
captureId,
|
|
1996
|
+
audienceId: evidenceAudience?.audienceId ?? null,
|
|
1997
|
+
audienceAclHash: evidenceAudience?.aclHash ?? null,
|
|
1246
1998
|
kind: input.kind ?? "fact",
|
|
1247
1999
|
title: redacted.title,
|
|
1248
2000
|
body: redacted.body,
|
|
@@ -1271,7 +2023,25 @@ export async function writeKnowledgeRecord(
|
|
|
1271
2023
|
.where(eq(schema.brainKnowledge.id, id))
|
|
1272
2024
|
.limit(1);
|
|
1273
2025
|
let returned = knowledge;
|
|
1274
|
-
|
|
2026
|
+
const audienceCanPublish = await canPublishCanonicalAudience(
|
|
2027
|
+
evidenceAudience?.audienceId ?? null,
|
|
2028
|
+
);
|
|
2029
|
+
const shouldMaintainCanonical = Boolean(existing?.publishedResourcePath);
|
|
2030
|
+
if (
|
|
2031
|
+
existing?.publishedResourcePath &&
|
|
2032
|
+
(status !== "published" || !audienceCanPublish)
|
|
2033
|
+
) {
|
|
2034
|
+
await resourceDeleteByPath(SHARED_OWNER, existing.publishedResourcePath);
|
|
2035
|
+
await db
|
|
2036
|
+
.update(schema.brainKnowledge)
|
|
2037
|
+
.set({ publishedResourcePath: null, updatedAt: nowIso() })
|
|
2038
|
+
.where(eq(schema.brainKnowledge.id, id));
|
|
2039
|
+
returned = { ...knowledge, publishedResourcePath: null };
|
|
2040
|
+
} else if (
|
|
2041
|
+
status === "published" &&
|
|
2042
|
+
audienceCanPublish &&
|
|
2043
|
+
(input.publishCanonical || shouldMaintainCanonical)
|
|
2044
|
+
) {
|
|
1275
2045
|
const publishedResourcePath = await publishKnowledgeResource({
|
|
1276
2046
|
id,
|
|
1277
2047
|
title: redacted.title,
|
|
@@ -1281,6 +2051,12 @@ export async function writeKnowledgeRecord(
|
|
|
1281
2051
|
tags: redacted.tags,
|
|
1282
2052
|
evidence: redacted.evidence,
|
|
1283
2053
|
});
|
|
2054
|
+
if (
|
|
2055
|
+
existing?.publishedResourcePath &&
|
|
2056
|
+
existing.publishedResourcePath !== publishedResourcePath
|
|
2057
|
+
) {
|
|
2058
|
+
await resourceDeleteByPath(SHARED_OWNER, existing.publishedResourcePath);
|
|
2059
|
+
}
|
|
1284
2060
|
await db
|
|
1285
2061
|
.update(schema.brainKnowledge)
|
|
1286
2062
|
.set({ publishedResourcePath, updatedAt: nowIso() })
|
|
@@ -1313,8 +2089,23 @@ export async function searchKnowledgeRows(args: {
|
|
|
1313
2089
|
limit?: number;
|
|
1314
2090
|
}) {
|
|
1315
2091
|
const db = getDb();
|
|
2092
|
+
const audienceIds = await listAccessibleAudienceIds();
|
|
2093
|
+
const accessibleSourceExists = sql`exists (
|
|
2094
|
+
select 1 from ${schema.brainSources}
|
|
2095
|
+
where ${schema.brainSources.id} = ${schema.brainKnowledge.sourceId}
|
|
2096
|
+
and ${accessFilter(schema.brainSources, schema.brainSourceShares)}
|
|
2097
|
+
)`;
|
|
1316
2098
|
const clauses = [
|
|
1317
2099
|
accessFilter(schema.brainKnowledge, schema.brainKnowledgeShares),
|
|
2100
|
+
or(
|
|
2101
|
+
isNull(schema.brainKnowledge.captureId),
|
|
2102
|
+
audienceIds.length
|
|
2103
|
+
? and(
|
|
2104
|
+
accessibleSourceExists,
|
|
2105
|
+
inArray(schema.brainKnowledge.audienceId, audienceIds),
|
|
2106
|
+
)
|
|
2107
|
+
: undefined,
|
|
2108
|
+
)!,
|
|
1318
2109
|
];
|
|
1319
2110
|
if (args.query) {
|
|
1320
2111
|
const q = `%${args.query}%`;
|
|
@@ -1361,16 +2152,32 @@ export async function readBrainScreen() {
|
|
|
1361
2152
|
const source = await resolveAccess("brain-source", nav.sourceId);
|
|
1362
2153
|
if (source) {
|
|
1363
2154
|
screen.source = serializeSource(source.resource);
|
|
1364
|
-
const
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
2155
|
+
const audienceIds = await listAccessibleAudienceIds([source.resource.id]);
|
|
2156
|
+
const captures = audienceIds.length
|
|
2157
|
+
? await getDb()
|
|
2158
|
+
.select({ capture: schema.brainRawCaptures })
|
|
2159
|
+
.from(schema.brainRawCaptures)
|
|
2160
|
+
.innerJoin(
|
|
2161
|
+
schema.brainCaptureAudiences,
|
|
2162
|
+
eq(
|
|
2163
|
+
schema.brainCaptureAudiences.captureId,
|
|
2164
|
+
schema.brainRawCaptures.id,
|
|
2165
|
+
),
|
|
2166
|
+
)
|
|
2167
|
+
.where(
|
|
2168
|
+
and(
|
|
2169
|
+
eq(schema.brainRawCaptures.sourceId, source.resource.id),
|
|
2170
|
+
eq(schema.brainRawCaptures.sensitivityDisposition, "allowed"),
|
|
2171
|
+
inArray(schema.brainCaptureAudiences.audienceId, audienceIds),
|
|
2172
|
+
),
|
|
2173
|
+
)
|
|
2174
|
+
.orderBy(desc(schema.brainRawCaptures.capturedAt))
|
|
2175
|
+
.limit(10)
|
|
2176
|
+
: [];
|
|
1370
2177
|
const queueByCapture = await latestDistillationQueuesForCaptures(
|
|
1371
|
-
captures.map((capture) => capture.id),
|
|
2178
|
+
captures.map(({ capture }) => capture.id),
|
|
1372
2179
|
);
|
|
1373
|
-
screen.sourceCaptures = captures.map((capture) => ({
|
|
2180
|
+
screen.sourceCaptures = captures.map(({ capture }) => ({
|
|
1374
2181
|
id: capture.id,
|
|
1375
2182
|
sourceId: capture.sourceId,
|
|
1376
2183
|
title: capture.title,
|
|
@@ -1386,18 +2193,25 @@ export async function readBrainScreen() {
|
|
|
1386
2193
|
}
|
|
1387
2194
|
if (nav?.knowledgeId) {
|
|
1388
2195
|
const knowledge = await resolveAccess("brain-knowledge", nav.knowledgeId);
|
|
1389
|
-
if (knowledge)
|
|
2196
|
+
if (knowledge) {
|
|
2197
|
+
await assertDerivedAudienceAccess(knowledge.resource);
|
|
2198
|
+
screen.knowledge = serializeKnowledge(knowledge.resource);
|
|
2199
|
+
}
|
|
1390
2200
|
}
|
|
1391
2201
|
const proposalId = nav?.proposalId ?? nav?.reviewItemId;
|
|
1392
2202
|
if (proposalId) {
|
|
1393
2203
|
const proposal = await resolveAccess("brain-proposal", proposalId);
|
|
1394
|
-
if (proposal)
|
|
2204
|
+
if (proposal) {
|
|
2205
|
+
await assertDerivedAudienceAccess(proposal.resource);
|
|
2206
|
+
screen.proposal = serializeProposal(proposal.resource);
|
|
2207
|
+
}
|
|
1395
2208
|
}
|
|
1396
2209
|
if (nav?.view === "review") {
|
|
1397
2210
|
const params = searchParamsFromPath(nav.path);
|
|
1398
2211
|
const status = proposalStatusFromNavigation(
|
|
1399
2212
|
typeof nav.status === "string" ? nav.status : params.get("status"),
|
|
1400
2213
|
);
|
|
2214
|
+
const audienceIds = await listAccessibleAudienceIds();
|
|
1401
2215
|
const proposals = await getDb()
|
|
1402
2216
|
.select()
|
|
1403
2217
|
.from(schema.brainProposals)
|
|
@@ -1405,6 +2219,19 @@ export async function readBrainScreen() {
|
|
|
1405
2219
|
and(
|
|
1406
2220
|
accessFilter(schema.brainProposals, schema.brainProposalShares),
|
|
1407
2221
|
eq(schema.brainProposals.status, status),
|
|
2222
|
+
or(
|
|
2223
|
+
isNull(schema.brainProposals.captureId),
|
|
2224
|
+
audienceIds.length
|
|
2225
|
+
? and(
|
|
2226
|
+
sql`exists (
|
|
2227
|
+
select 1 from ${schema.brainSources}
|
|
2228
|
+
where ${schema.brainSources.id} = ${schema.brainProposals.sourceId}
|
|
2229
|
+
and ${accessFilter(schema.brainSources, schema.brainSourceShares)}
|
|
2230
|
+
)`,
|
|
2231
|
+
inArray(schema.brainProposals.audienceId, audienceIds),
|
|
2232
|
+
)
|
|
2233
|
+
: undefined,
|
|
2234
|
+
),
|
|
1408
2235
|
),
|
|
1409
2236
|
)
|
|
1410
2237
|
.orderBy(desc(schema.brainProposals.updatedAt))
|