@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
|
@@ -15,6 +15,42 @@ in the public `template-brain.md` overview and previously sat inline in
|
|
|
15
15
|
intentionally not in the public docs. Read it when you are operating, rolling
|
|
16
16
|
out, or debugging Brain internals rather than answering an ordinary question.
|
|
17
17
|
|
|
18
|
+
## Privacy, Evidence Audiences, And Search
|
|
19
|
+
|
|
20
|
+
Brain applies the deterministic hard-category screen before any privacy model.
|
|
21
|
+
It blocks performance/discipline, layoffs or termination, compensation,
|
|
22
|
+
recruiting, health/accommodation, investigation, privileged legal, credentials,
|
|
23
|
+
and personal data. An approved privacy classifier may make handling stricter;
|
|
24
|
+
it cannot override a deterministic hit. When no classifier is configured,
|
|
25
|
+
Brain is in degraded deterministic-only mode: clearly clean company material
|
|
26
|
+
may be allowed, while uncertainty is quarantined and `get-brain-health` reports
|
|
27
|
+
the missing configuration.
|
|
28
|
+
|
|
29
|
+
Quarantined re-fetchable material from Slack, GitHub, or Granola keeps only a
|
|
30
|
+
metadata receipt because the content can be re-fetched during an authorized
|
|
31
|
+
review. Push-only generic and Clips payloads instead use an encrypted,
|
|
32
|
+
short-TTL private side store that no search, agent, source editor, log,
|
|
33
|
+
citation, or distillation surface can read; expiry converts it to a suppression
|
|
34
|
+
receipt. Review records policy metadata, never a shortcut to raw sensitive
|
|
35
|
+
content. HR-blocked evidence is never declassifiable: write a new,
|
|
36
|
+
non-identifying reviewed statement if it has durable organizational value.
|
|
37
|
+
|
|
38
|
+
Every allowed capture and derived row has an audience id and ACL hash. Public
|
|
39
|
+
and organization material use the trivial organization audience. Private Slack
|
|
40
|
+
material uses its channel membership audience, and meeting material uses its
|
|
41
|
+
attendee audience. Retrieval prefilters by audience before FTS/vector ranking;
|
|
42
|
+
answers spanning multiple sources may only cite the intersection of their
|
|
43
|
+
evidence audiences. Private Slack audiences fail closed when membership has not
|
|
44
|
+
been refreshed for 15 minutes, so removal upstream cannot leave indefinite
|
|
45
|
+
Brain access.
|
|
46
|
+
|
|
47
|
+
Semantic indexing starts when a capture is `allowed`, rather than waiting for
|
|
48
|
+
distillation. A derived row is current only when its staleness key matches
|
|
49
|
+
`contentHash + BRAIN_SEARCH_INDEX_VERSION + sensitivityPolicyVersion + aclHash`.
|
|
50
|
+
Changing content, policy, or membership invalidates indexed artifacts,
|
|
51
|
+
knowledge/proposal evidence, and mirrored canonical resources before they can
|
|
52
|
+
be served.
|
|
53
|
+
|
|
18
54
|
## Search model layers
|
|
19
55
|
|
|
20
56
|
Brain search has three layers:
|
|
@@ -60,8 +96,11 @@ pnpm --filter brain action create-source \
|
|
|
60
96
|
```
|
|
61
97
|
|
|
62
98
|
The connector verifies each configured conversation before reading history and
|
|
63
|
-
rejects DMs and MPIMs.
|
|
64
|
-
|
|
99
|
+
rejects DMs and MPIMs. Public discovery is opt-in and exclusion patterns are
|
|
100
|
+
applied before capture. Private channels are never silently joined: an operator
|
|
101
|
+
must manually invite the Slack app before validation and membership sync. Cursor
|
|
102
|
+
state is stored on the source so each sync can pick up where the last one
|
|
103
|
+
stopped, including after Slack rate limiting.
|
|
65
104
|
|
|
66
105
|
Use `test-slack-connection` before a production backfill. It validates the Slack
|
|
67
106
|
bot token with `auth.test` and, when channel refs are provided, checks channel
|
|
@@ -121,7 +160,8 @@ recommended rollout steps without returning raw capture bodies.
|
|
|
121
160
|
|
|
122
161
|
Recommended production rollout:
|
|
123
162
|
|
|
124
|
-
1. Start with one or two high-signal channels and
|
|
163
|
+
1. Start with one or two high-signal public channels and explicitly invited
|
|
164
|
+
private channels; never use discovery as implicit private-channel consent.
|
|
125
165
|
2. Keep `autoSync: false` until review quality is proven.
|
|
126
166
|
3. Run `test-slack-connection`, then `run-slack-pilot` without history.
|
|
127
167
|
4. Run one explicit `run-slack-pilot --readHistory true` sample when the report
|
|
@@ -246,8 +286,11 @@ with `mode: "retrieval"`.
|
|
|
246
286
|
|
|
247
287
|
The repository-level `pnpm test` command includes `pnpm test:brain-evals`, which
|
|
248
288
|
runs Brain's product-demo and retrieval action evals against a disposable local
|
|
249
|
-
SQLite database.
|
|
250
|
-
|
|
289
|
+
SQLite database. `pnpm test:brain-privacy-evals` is a separate CI lane with
|
|
290
|
+
must-block sensitive fixtures and must-allow benign false-positive fixtures
|
|
291
|
+
(such as API-limit raises, bonus features, and medical-device customers). Both
|
|
292
|
+
paths are fully seeded and offline; neither requires production Slack, Granola,
|
|
293
|
+
Clips, or external workspace data.
|
|
251
294
|
|
|
252
295
|
## Generic ingest payload validation
|
|
253
296
|
|
|
@@ -17,18 +17,48 @@ Use Brain actions rather than raw SQL.
|
|
|
17
17
|
for a capture that's already queued/processing refreshes the handoff
|
|
18
18
|
instructions instead of creating a duplicate queue row.
|
|
19
19
|
4. Before writing knowledge, call `get-capture` and copy short exact quotes.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
Quotes and offsets always reference the persisted safe capture, never an
|
|
21
|
+
upstream raw payload. `get-capture` redacts `title`/`content`/`metadata` by
|
|
22
|
+
default; `includeRawContent: true` only reveals allowed, source-accessible
|
|
23
|
+
capture content and never bypasses a sensitivity disposition.
|
|
23
24
|
5. Call `write-knowledge` with `evidence` entries whose `quote` fields are exact capture substrings — `validateEvidence` throws otherwise.
|
|
24
25
|
6. If `write-knowledge` returns `mode: "proposal"`, leave it in review unless the user asks to approve. See AGENTS.md for the exact tier/confidence conditions that trigger a proposal.
|
|
25
26
|
|
|
27
|
+
## Privacy, Quarantine, And Safe Captures
|
|
28
|
+
|
|
29
|
+
Every ingest first receives a deterministic sensitivity screen. Performance,
|
|
30
|
+
discipline, termination/layoff, compensation, recruiting, health or
|
|
31
|
+
accommodation, investigation, privileged legal, credential, and personal-data
|
|
32
|
+
signals can only tighten handling; no model or workspace instruction can lower
|
|
33
|
+
that boundary.
|
|
34
|
+
|
|
35
|
+
- `allowed` captures persist safe content and may be indexed immediately. This
|
|
36
|
+
is independent of distillation: semantic coverage must not wait for an agent
|
|
37
|
+
to author a memory.
|
|
38
|
+
- `quarantined` or `suppressed` content is unavailable to search, citation,
|
|
39
|
+
distillation, source editors, agent tools, and logs. Review shows only the
|
|
40
|
+
minimal policy metadata needed to operate the queue.
|
|
41
|
+
- Re-fetchable providers (Slack, GitHub, Granola) retain metadata-only
|
|
42
|
+
quarantine records. Push-only `generic` and `clips` material is retained only
|
|
43
|
+
in the encrypted, short-TTL private quarantine store; expiry becomes a
|
|
44
|
+
suppression receipt.
|
|
45
|
+
- If no approved privacy classifier is configured, deterministic-only mode
|
|
46
|
+
allows clearly clean, company-relevant material and quarantines uncertainty.
|
|
47
|
+
Treat the health/setup warning as a requirement to configure the classifier
|
|
48
|
+
before broad ingestion.
|
|
49
|
+
|
|
50
|
+
Administrators may review a disposition but may not declassify HR-blocked
|
|
51
|
+
evidence. A broader statement must be a newly reviewed, non-identifying memory
|
|
52
|
+
with no private quotes, links, or identities.
|
|
53
|
+
|
|
26
54
|
## Capture Sanitization (Transcripts)
|
|
27
55
|
|
|
28
56
|
Transcript-kind captures are sanitized **before storage** by default
|
|
29
57
|
(`shouldSanitizeCaptureBeforeStorage` — true whenever `kind === "transcript"`,
|
|
30
58
|
unless `captureSanitizationEnabled: false` in settings or a per-capture
|
|
31
59
|
`metadata.sanitizeBeforeStorage` / source-config override says otherwise).
|
|
60
|
+
That override may skip relevance-oriented transcript cleanup, but deterministic
|
|
61
|
+
privacy and PII scrubbing always run and raw input is never retained.
|
|
32
62
|
Sanitization always strips, regardless of settings:
|
|
33
63
|
|
|
34
64
|
- Recruiting/hiring/candidate-evaluation content (`RECRUITING_SIGNAL`).
|
|
@@ -46,19 +76,20 @@ company-relevant survives, the stored content becomes the literal string "No
|
|
|
46
76
|
company-relevant content retained from this capture." — treat that string as
|
|
47
77
|
"this capture had nothing worth distilling," not as an error.
|
|
48
78
|
|
|
49
|
-
## Search:
|
|
79
|
+
## Search: Scoped Hybrid Retrieval
|
|
50
80
|
|
|
51
|
-
- `search-knowledge` —
|
|
52
|
-
|
|
53
|
-
X."
|
|
81
|
+
- `search-knowledge` — scoped retrieval over **distilled knowledge only**. Use
|
|
82
|
+
for "what does Brain officially know about X."
|
|
54
83
|
- `search-everything` — broader pass across knowledge, raw captures, and
|
|
55
84
|
sources in one call, plus `federatedCoverage` (delegation hints for other
|
|
56
|
-
apps).
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
85
|
+
apps). It uses full-text and available semantic signals after applying source,
|
|
86
|
+
project, kind, and audience filters. Use it as the default first search for
|
|
87
|
+
an open-ended question; narrow with `type: "knowledge" | "capture" |
|
|
88
|
+
"source"` when you already know which record type you need.
|
|
89
|
+
- Audience filtering happens before ranking. Public and organization sources
|
|
90
|
+
use the cheap organization audience; private channels and meetings use their
|
|
91
|
+
restricted audience. A multi-source answer must use the intersection of the
|
|
92
|
+
cited evidence audiences.
|
|
62
93
|
|
|
63
94
|
Follow `sourcePolicy` for how much of `search-everything`'s output an answer
|
|
64
95
|
may lean on: `strict` means reviewed knowledge only, `balanced` means raw
|
|
@@ -76,5 +107,6 @@ directly.
|
|
|
76
107
|
- `ingestion-and-connectors` — source creation, health states, sync scheduling,
|
|
77
108
|
and credential resolution order.
|
|
78
109
|
- `brain-runbook` — internal architecture and ops detail (Slack rollout,
|
|
79
|
-
distillation worker, scheduled sync cron,
|
|
110
|
+
privacy quarantine, semantic index, distillation worker, scheduled sync cron,
|
|
111
|
+
demo/eval seeding).
|
|
80
112
|
- `ask-across-everything`, `security`, `sharing`.
|
|
@@ -8,16 +8,47 @@ from approved company knowledge, with links back to the source.
|
|
|
8
8
|
|
|
9
9
|
Brain ingests approved Slack channels, meetings, transcripts, GitHub issues/PRs,
|
|
10
10
|
and webhook captures, distills them into reviewable knowledge, and answers with
|
|
11
|
-
exact evidence quotes and source links instead of guesses.
|
|
11
|
+
exact evidence quotes and source links instead of guesses. It can search both
|
|
12
|
+
organization-wide material and explicitly invited private channels or
|
|
13
|
+
attendee-scoped meetings without widening their audience.
|
|
12
14
|
|
|
13
15
|
## Features
|
|
14
16
|
|
|
15
17
|
- Company chat that answers from cited, reviewed knowledge — not hallucinations.
|
|
16
18
|
- Source connectors for Slack, Granola, GitHub, Clips, and generic webhooks.
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
+
- Deterministic privacy screening before storage, with an optional approved
|
|
20
|
+
privacy classifier for richer review decisions.
|
|
21
|
+
- Hybrid full-text and semantic search over allowed captures and knowledge,
|
|
22
|
+
filtered by the requesting user's evidence audience before ranking.
|
|
23
|
+
- Private-channel and meeting evidence stays audience-scoped; multi-source
|
|
24
|
+
answers use only the intersection of their evidence audiences.
|
|
25
|
+
- Sensitive sources are suppressed or quarantined before search, citations,
|
|
26
|
+
distillation, and source editors can see their content.
|
|
19
27
|
- Read-only, citation-backed retrieval exposed to other apps over A2A.
|
|
20
28
|
|
|
29
|
+
## Privacy posture
|
|
30
|
+
|
|
31
|
+
Brain never treats a connected provider as permission to index everything.
|
|
32
|
+
Slack public-channel discovery is configurable, while private channels require a
|
|
33
|
+
manual app invite and membership sync. Meetings use their attendee audience.
|
|
34
|
+
|
|
35
|
+
Hard privacy categories such as performance discussions, layoffs, compensation,
|
|
36
|
+
recruiting, health accommodations, investigations, privileged legal material,
|
|
37
|
+
and credentials are blocked before they become searchable content. Re-fetchable
|
|
38
|
+
providers keep only a metadata receipt in quarantine. Push-only generic and
|
|
39
|
+
Clips payloads use an encrypted, short-lived private quarantine store and turn
|
|
40
|
+
into a suppression receipt at expiry.
|
|
41
|
+
|
|
42
|
+
Without an approved privacy classifier, Brain runs in deterministic-only
|
|
43
|
+
degraded mode: clearly clean, company-relevant material may proceed; ambiguous
|
|
44
|
+
or sensitive-looking material is quarantined and health surfaces the setup
|
|
45
|
+
warning. Configure the classifier before relying on higher-throughput ingest.
|
|
46
|
+
|
|
47
|
+
`sanitizeBeforeStorage: false` only disables relevance-oriented transcript
|
|
48
|
+
cleanup. Brain still runs deterministic privacy and PII scrubbing before
|
|
49
|
+
persisting titles, content, metadata, safe segments, or search artifacts; the
|
|
50
|
+
setting never retains raw input.
|
|
51
|
+
|
|
21
52
|
## Develop locally
|
|
22
53
|
|
|
23
54
|
Scaffold your own copy and run it:
|
|
@@ -6,6 +6,7 @@ import { z } from "zod";
|
|
|
6
6
|
|
|
7
7
|
import { getDb, schema } from "../server/db/index.js";
|
|
8
8
|
import {
|
|
9
|
+
assertDerivedAudienceAccess,
|
|
9
10
|
nowIso,
|
|
10
11
|
parseJson,
|
|
11
12
|
serializeProposal,
|
|
@@ -30,6 +31,7 @@ export default defineAction({
|
|
|
30
31
|
run: async ({ proposalId, reviewerNotes, publishCanonical }) => {
|
|
31
32
|
const access = await assertAccess("brain-proposal", proposalId, "editor");
|
|
32
33
|
const proposal = access.resource;
|
|
34
|
+
await assertDerivedAudienceAccess(proposal);
|
|
33
35
|
if (proposal.status !== "pending") {
|
|
34
36
|
throw new Error(`Proposal ${proposalId} is already ${proposal.status}`);
|
|
35
37
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import { getDbExec } from "@agent-native/core/db";
|
|
2
3
|
import { and, desc, eq } from "drizzle-orm";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
|
|
@@ -12,6 +13,8 @@ import {
|
|
|
12
13
|
stableJson,
|
|
13
14
|
} from "../server/lib/brain.js";
|
|
14
15
|
|
|
16
|
+
const LEASE_MS = 15 * 60 * 1000;
|
|
17
|
+
|
|
15
18
|
export default defineAction({
|
|
16
19
|
description:
|
|
17
20
|
"Claim a queued Brain capture distillation item before handing it to an agent worker.",
|
|
@@ -41,45 +44,39 @@ export default defineAction({
|
|
|
41
44
|
|
|
42
45
|
const now = nowIso();
|
|
43
46
|
const claimToken = nanoid(16);
|
|
47
|
+
const leaseExpiresAt = new Date(Date.parse(now) + LEASE_MS).toISOString();
|
|
44
48
|
const payload = parseJson<Record<string, unknown>>(queue.payloadJson, {});
|
|
45
|
-
await
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
eq(schema.brainIngestQueue.status, "queued"),
|
|
64
|
-
),
|
|
65
|
-
);
|
|
49
|
+
const result = await getDbExec().execute({
|
|
50
|
+
sql: `UPDATE brain_ingest_queue
|
|
51
|
+
SET status = ?, attempts = ?, payload_json = ?, error = NULL,
|
|
52
|
+
run_after = NULL, lease_token = ?, lease_expires_at = ?, updated_at = ?
|
|
53
|
+
WHERE id = ? AND status = ? AND updated_at = ?`,
|
|
54
|
+
args: [
|
|
55
|
+
"processing",
|
|
56
|
+
queue.attempts + 1,
|
|
57
|
+
stableJson({ ...payload, claimedAt: now, claimedBy: "brain-agent" }),
|
|
58
|
+
claimToken,
|
|
59
|
+
leaseExpiresAt,
|
|
60
|
+
now,
|
|
61
|
+
queue.id,
|
|
62
|
+
"queued",
|
|
63
|
+
queue.updatedAt,
|
|
64
|
+
],
|
|
65
|
+
});
|
|
66
|
+
if (result.rowsAffected === 0) return { claimed: false, queueItem: null };
|
|
66
67
|
|
|
67
68
|
const [updated] = await db
|
|
68
69
|
.select()
|
|
69
70
|
.from(schema.brainIngestQueue)
|
|
70
71
|
.where(eq(schema.brainIngestQueue.id, queue.id))
|
|
71
72
|
.limit(1);
|
|
72
|
-
const updatedPayload = parseJson<Record<string, unknown>>(
|
|
73
|
-
updated?.payloadJson,
|
|
74
|
-
{},
|
|
75
|
-
);
|
|
76
73
|
const claimed =
|
|
77
|
-
updated?.status === "processing" &&
|
|
78
|
-
updatedPayload.claimToken === claimToken;
|
|
74
|
+
updated?.status === "processing" && updated?.leaseToken === claimToken;
|
|
79
75
|
|
|
80
76
|
return {
|
|
81
77
|
claimed,
|
|
82
78
|
queueItem: updated ? serializeDistillationQueue(updated) : null,
|
|
79
|
+
claimToken: claimed ? claimToken : null,
|
|
83
80
|
};
|
|
84
81
|
},
|
|
85
82
|
});
|
|
@@ -26,7 +26,7 @@ export default defineAction({
|
|
|
26
26
|
.string()
|
|
27
27
|
.optional()
|
|
28
28
|
.describe("Optional signed-ingest bearer token; stored only as a hash"),
|
|
29
|
-
visibility: z.enum(["private", "org"
|
|
29
|
+
visibility: z.enum(["private", "org"]).default("org"),
|
|
30
30
|
}),
|
|
31
31
|
run: async (args) => {
|
|
32
32
|
const config = { ...args.config };
|
|
@@ -2,7 +2,10 @@ import { defineAction } from "@agent-native/core";
|
|
|
2
2
|
import { resolveAccess } from "@agent-native/core/sharing";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
assertDerivedAudienceAccess,
|
|
7
|
+
serializeKnowledge,
|
|
8
|
+
} from "../server/lib/brain.js";
|
|
6
9
|
|
|
7
10
|
export default defineAction({
|
|
8
11
|
description: "Get one Brain knowledge item by ID.",
|
|
@@ -14,12 +17,13 @@ export default defineAction({
|
|
|
14
17
|
publicAgent: {
|
|
15
18
|
expose: true,
|
|
16
19
|
readOnly: true,
|
|
17
|
-
requiresAuth:
|
|
20
|
+
requiresAuth: true,
|
|
18
21
|
isConsequential: false,
|
|
19
22
|
},
|
|
20
23
|
run: async ({ id }) => {
|
|
21
24
|
const access = await resolveAccess("brain-knowledge", id);
|
|
22
25
|
if (!access) return { knowledge: null };
|
|
26
|
+
await assertDerivedAudienceAccess(access.resource);
|
|
23
27
|
return {
|
|
24
28
|
knowledge: serializeKnowledge(access.resource),
|
|
25
29
|
accessRole: access.role,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
2
|
import { accessFilter } from "@agent-native/core/sharing";
|
|
3
|
-
import { and, desc, eq, inArray, or } from "drizzle-orm";
|
|
3
|
+
import { and, desc, eq, inArray, isNull, or, sql } from "drizzle-orm";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
|
|
6
6
|
import { getDb, schema } from "../server/db/index.js";
|
|
7
|
+
import { listAccessibleAudienceIds } from "../server/lib/audiences.js";
|
|
7
8
|
import {
|
|
8
9
|
getAccessibleSource,
|
|
9
10
|
parseJson,
|
|
@@ -451,6 +452,22 @@ export default defineAction({
|
|
|
451
452
|
run: async ({ sourceId, targetChannel }) => {
|
|
452
453
|
const access = await getAccessibleSource(sourceId);
|
|
453
454
|
const db = getDb();
|
|
455
|
+
const accessibleAudienceIds = await listAccessibleAudienceIds([sourceId]);
|
|
456
|
+
const accessibleCaptureAudienceFilter = accessibleAudienceIds.length
|
|
457
|
+
? inArray(schema.brainCaptureAudiences.audienceId, accessibleAudienceIds)
|
|
458
|
+
: sql`1=0`;
|
|
459
|
+
const accessibleDerivedAudienceFilter = or(
|
|
460
|
+
isNull(schema.brainKnowledge.captureId),
|
|
461
|
+
accessibleAudienceIds.length
|
|
462
|
+
? inArray(schema.brainKnowledge.audienceId, accessibleAudienceIds)
|
|
463
|
+
: sql`1=0`,
|
|
464
|
+
)!;
|
|
465
|
+
const accessibleProposalAudienceFilter = or(
|
|
466
|
+
isNull(schema.brainProposals.captureId),
|
|
467
|
+
accessibleAudienceIds.length
|
|
468
|
+
? inArray(schema.brainProposals.audienceId, accessibleAudienceIds)
|
|
469
|
+
: sql`1=0`,
|
|
470
|
+
)!;
|
|
454
471
|
|
|
455
472
|
const [latestSyncRun] = await db
|
|
456
473
|
.select()
|
|
@@ -465,7 +482,17 @@ export default defineAction({
|
|
|
465
482
|
status: schema.brainRawCaptures.status,
|
|
466
483
|
})
|
|
467
484
|
.from(schema.brainRawCaptures)
|
|
468
|
-
.
|
|
485
|
+
.innerJoin(
|
|
486
|
+
schema.brainCaptureAudiences,
|
|
487
|
+
eq(schema.brainCaptureAudiences.captureId, schema.brainRawCaptures.id),
|
|
488
|
+
)
|
|
489
|
+
.where(
|
|
490
|
+
and(
|
|
491
|
+
eq(schema.brainRawCaptures.sourceId, sourceId),
|
|
492
|
+
eq(schema.brainRawCaptures.sensitivityDisposition, "allowed"),
|
|
493
|
+
accessibleCaptureAudienceFilter,
|
|
494
|
+
),
|
|
495
|
+
);
|
|
469
496
|
|
|
470
497
|
const recentCaptureRows = await db
|
|
471
498
|
.select({
|
|
@@ -480,17 +507,24 @@ export default defineAction({
|
|
|
480
507
|
updatedAt: schema.brainRawCaptures.updatedAt,
|
|
481
508
|
})
|
|
482
509
|
.from(schema.brainRawCaptures)
|
|
483
|
-
.
|
|
510
|
+
.innerJoin(
|
|
511
|
+
schema.brainCaptureAudiences,
|
|
512
|
+
eq(schema.brainCaptureAudiences.captureId, schema.brainRawCaptures.id),
|
|
513
|
+
)
|
|
514
|
+
.where(
|
|
515
|
+
and(
|
|
516
|
+
eq(schema.brainRawCaptures.sourceId, sourceId),
|
|
517
|
+
eq(schema.brainRawCaptures.sensitivityDisposition, "allowed"),
|
|
518
|
+
accessibleCaptureAudienceFilter,
|
|
519
|
+
),
|
|
520
|
+
)
|
|
484
521
|
.orderBy(desc(schema.brainRawCaptures.capturedAt))
|
|
485
522
|
.limit(RECENT_LIMIT);
|
|
486
523
|
|
|
487
524
|
const captureIds = captureRows.map((capture) => capture.id);
|
|
488
525
|
const queueSourceFilter = captureIds.length
|
|
489
|
-
?
|
|
490
|
-
|
|
491
|
-
inArray(schema.brainIngestQueue.captureId, captureIds),
|
|
492
|
-
)!
|
|
493
|
-
: eq(schema.brainIngestQueue.sourceId, sourceId);
|
|
526
|
+
? inArray(schema.brainIngestQueue.captureId, captureIds)
|
|
527
|
+
: sql`1=0`;
|
|
494
528
|
|
|
495
529
|
const queueRows = await db
|
|
496
530
|
.select({
|
|
@@ -532,6 +566,7 @@ export default defineAction({
|
|
|
532
566
|
and(
|
|
533
567
|
accessFilter(schema.brainKnowledge, schema.brainKnowledgeShares),
|
|
534
568
|
eq(schema.brainKnowledge.sourceId, sourceId),
|
|
569
|
+
accessibleDerivedAudienceFilter,
|
|
535
570
|
),
|
|
536
571
|
)
|
|
537
572
|
.orderBy(desc(schema.brainKnowledge.updatedAt));
|
|
@@ -556,6 +591,7 @@ export default defineAction({
|
|
|
556
591
|
and(
|
|
557
592
|
accessFilter(schema.brainProposals, schema.brainProposalShares),
|
|
558
593
|
eq(schema.brainProposals.sourceId, sourceId),
|
|
594
|
+
accessibleProposalAudienceFilter,
|
|
559
595
|
),
|
|
560
596
|
)
|
|
561
597
|
.orderBy(desc(schema.brainProposals.updatedAt));
|
|
@@ -2,6 +2,7 @@ import { defineAction } from "@agent-native/core";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
+
BrainCaptureBlockedError,
|
|
5
6
|
createCapture,
|
|
6
7
|
ensureManualSource,
|
|
7
8
|
serializeCapture,
|
|
@@ -31,15 +32,26 @@ export default defineAction({
|
|
|
31
32
|
? null
|
|
32
33
|
: await ensureManualSource(args.sourceTitle ?? "Manual imports");
|
|
33
34
|
const sourceId = args.sourceId ?? source!.id;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
let capture;
|
|
36
|
+
try {
|
|
37
|
+
capture = await createCapture({
|
|
38
|
+
sourceId,
|
|
39
|
+
externalId: args.externalId,
|
|
40
|
+
title: args.title,
|
|
41
|
+
kind: args.kind,
|
|
42
|
+
content: args.content,
|
|
43
|
+
capturedAt: args.capturedAt,
|
|
44
|
+
metadata: args.metadata,
|
|
45
|
+
});
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (!(error instanceof BrainCaptureBlockedError)) throw error;
|
|
48
|
+
return {
|
|
49
|
+
source: source ? serializeSource(source) : undefined,
|
|
50
|
+
capture: undefined,
|
|
51
|
+
sensitivityReceipt: error.receipt,
|
|
52
|
+
nextAction: undefined,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
43
55
|
return {
|
|
44
56
|
source: source ? serializeSource(source) : undefined,
|
|
45
57
|
capture: serializeCapture(capture),
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import { getRequestUserEmail } from "@agent-native/core/server/request-context";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
|
|
4
5
|
import {
|
|
6
|
+
BrainCaptureBlockedError,
|
|
5
7
|
createCapture,
|
|
6
8
|
ensureManualSource,
|
|
7
9
|
serializeCapture,
|
|
@@ -26,23 +28,49 @@ export default defineAction({
|
|
|
26
28
|
enqueueDistillation: z.coerce.boolean().default(true),
|
|
27
29
|
}),
|
|
28
30
|
run: async (args) => {
|
|
31
|
+
const importerEmail = getRequestUserEmail()?.trim().toLowerCase();
|
|
32
|
+
let participants = args.participants;
|
|
33
|
+
if (!participants.length) {
|
|
34
|
+
if (!importerEmail) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
"Importing a transcript without participants requires an authenticated importer.",
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
participants = [importerEmail];
|
|
40
|
+
}
|
|
29
41
|
const source = args.sourceId
|
|
30
42
|
? null
|
|
31
43
|
: await ensureManualSource(args.sourceTitle);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
let capture;
|
|
45
|
+
try {
|
|
46
|
+
capture = await createCapture({
|
|
47
|
+
sourceId: args.sourceId ?? source!.id,
|
|
48
|
+
externalId: args.externalId,
|
|
49
|
+
title: args.title,
|
|
50
|
+
kind: "transcript",
|
|
51
|
+
content: args.transcript,
|
|
52
|
+
capturedAt: args.capturedAt,
|
|
53
|
+
metadata: {
|
|
54
|
+
...(args.metadata ?? {}),
|
|
55
|
+
participants,
|
|
56
|
+
sourceUrl: args.sourceUrl,
|
|
57
|
+
tags: args.tags,
|
|
58
|
+
},
|
|
59
|
+
audience: {
|
|
60
|
+
kind: "meeting",
|
|
61
|
+
memberEmails: participants,
|
|
62
|
+
upstreamRefHash: args.externalId,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (!(error instanceof BrainCaptureBlockedError)) throw error;
|
|
67
|
+
return {
|
|
68
|
+
source: source ? serializeSource(source) : undefined,
|
|
69
|
+
capture: undefined,
|
|
70
|
+
sensitivityReceipt: error.receipt,
|
|
71
|
+
nextAction: undefined,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
46
74
|
return {
|
|
47
75
|
source: source ? serializeSource(source) : undefined,
|
|
48
76
|
capture: serializeCapture(capture),
|
|
@@ -4,6 +4,7 @@ import { and, desc, eq, inArray, sql } from "drizzle-orm";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
|
|
6
6
|
import { getDb, schema } from "../server/db/index.js";
|
|
7
|
+
import { listAccessibleAudienceIds } from "../server/lib/audiences.js";
|
|
7
8
|
import {
|
|
8
9
|
latestDistillationQueuesForCaptures,
|
|
9
10
|
parseJson,
|
|
@@ -98,9 +99,13 @@ export default defineAction({
|
|
|
98
99
|
|
|
99
100
|
const sourceIds = sourceRows.map((source) => source.id);
|
|
100
101
|
if (!sourceIds.length) return { count: 0, captures: [] };
|
|
102
|
+
const audienceIds = await listAccessibleAudienceIds(sourceIds);
|
|
103
|
+
if (!audienceIds.length) return { count: 0, captures: [] };
|
|
101
104
|
const sourceMap = new Map(sourceRows.map((source) => [source.id, source]));
|
|
102
105
|
const captureClauses = [
|
|
103
106
|
inArray(schema.brainRawCaptures.sourceId, sourceIds),
|
|
107
|
+
eq(schema.brainRawCaptures.sensitivityDisposition, "allowed"),
|
|
108
|
+
inArray(schema.brainCaptureAudiences.audienceId, audienceIds),
|
|
104
109
|
];
|
|
105
110
|
if (args.status) {
|
|
106
111
|
captureClauses.push(eq(schema.brainRawCaptures.status, args.status));
|
|
@@ -118,7 +123,7 @@ export default defineAction({
|
|
|
118
123
|
? Math.min(args.previewLength * 4, 4000)
|
|
119
124
|
: 0;
|
|
120
125
|
const rows = await db
|
|
121
|
-
.
|
|
126
|
+
.selectDistinct({
|
|
122
127
|
id: schema.brainRawCaptures.id,
|
|
123
128
|
sourceId: schema.brainRawCaptures.sourceId,
|
|
124
129
|
externalId: schema.brainRawCaptures.externalId,
|
|
@@ -136,6 +141,10 @@ export default defineAction({
|
|
|
136
141
|
: {}),
|
|
137
142
|
})
|
|
138
143
|
.from(schema.brainRawCaptures)
|
|
144
|
+
.innerJoin(
|
|
145
|
+
schema.brainCaptureAudiences,
|
|
146
|
+
eq(schema.brainCaptureAudiences.captureId, schema.brainRawCaptures.id),
|
|
147
|
+
)
|
|
139
148
|
.where(and(...captureClauses))
|
|
140
149
|
.orderBy(desc(schema.brainRawCaptures.capturedAt))
|
|
141
150
|
.limit(args.limit);
|