@neuralis/brain-core 0.3.1 → 0.3.2
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/README.md +41 -40
- package/app/README.md +3 -3
- package/app/filesystem/FilesystemHost.tsx +7 -0
- package/app/filesystem/README.md +1 -1
- package/app/filesystem/api/index.ts +4 -4
- package/app/filesystem/hooks/README.md +1 -1
- package/app/filesystem/hooks/useFilesystemSSE.ts +41 -1
- package/app/filesystem/host/README.md +2 -2
- package/app/filesystem/host/buildFilesystemHostValue.ts +9 -2
- package/app/filesystem/lib/sourceKindForm.ts +109 -5
- package/app/filesystem/store/filesystemStore.ts +13 -3
- package/app/filesystem/ui/DiscoverDock.tsx +5 -1
- package/app/filesystem/ui/FileTree.tsx +34 -8
- package/app/filesystem/ui/FilesystemWidget.tsx +10 -2
- package/app/filesystem/ui/GitControlPanel.tsx +2 -2
- package/app/filesystem/ui/PendingChangesPanel.tsx +1 -1
- package/app/filesystem/ui/README.md +14 -13
- package/app/filesystem/ui/SourcesPanel.tsx +124 -24
- package/app/filesystem/ui/VectorHealthPanel.tsx +2 -2
- package/dist/app/host.js +41 -37
- package/dist/src/channels/filesystem.d.ts +1 -1
- package/dist/src/channels/filesystem.js +1 -1
- package/dist/src/domain/mime.d.ts +3 -3
- package/dist/src/domain/mime.js +3 -3
- package/dist/src/domain/scopeAccessCtx.d.ts +2 -2
- package/dist/src/domain/scopeAccessCtx.js +2 -2
- package/dist/src/domain/types.d.ts +8 -1
- package/dist/src/domain/types.d.ts.map +1 -1
- package/dist/src/events/pending.d.ts +1 -1
- package/dist/src/events/pending.js +1 -1
- package/dist/src/git/sourceGitControl.d.ts +2 -2
- package/dist/src/git/sourceGitControl.js +3 -3
- package/dist/src/lifecycle.d.ts +1 -1
- package/dist/src/routes/approve.d.ts +1 -1
- package/dist/src/routes/approve.js +1 -1
- package/dist/src/routes/events.d.ts +1 -1
- package/dist/src/routes/events.js +1 -1
- package/dist/src/routes/git.d.ts +7 -7
- package/dist/src/routes/git.js +18 -18
- package/dist/src/routes/health.d.ts +6 -6
- package/dist/src/routes/health.js +12 -12
- package/dist/src/routes/helpers/sourceNotConfigured.d.ts +1 -1
- package/dist/src/routes/helpers/sourceNotConfigured.js +1 -1
- package/dist/src/routes/helpers.d.ts +1 -1
- package/dist/src/routes/helpers.js +1 -1
- package/dist/src/routes/list.d.ts +1 -1
- package/dist/src/routes/list.js +1 -1
- package/dist/src/routes/pending.d.ts +4 -4
- package/dist/src/routes/pending.js +9 -9
- package/dist/src/routes/policies.d.ts +4 -4
- package/dist/src/routes/policies.d.ts.map +1 -1
- package/dist/src/routes/policies.js +11 -11
- package/dist/src/routes/policies.js.map +1 -1
- package/dist/src/routes/raw.d.ts +1 -1
- package/dist/src/routes/raw.js +1 -1
- package/dist/src/routes/read.d.ts +1 -1
- package/dist/src/routes/read.js +1 -1
- package/dist/src/routes/search.d.ts +1 -1
- package/dist/src/routes/search.js +1 -1
- package/dist/src/routes/sources.d.ts +20 -5
- package/dist/src/routes/sources.d.ts.map +1 -1
- package/dist/src/routes/sources.js +118 -118
- package/dist/src/routes/sources.js.map +1 -1
- package/dist/src/routes/upload.d.ts +1 -1
- package/dist/src/routes/upload.js +1 -1
- package/dist/src/routes/write.d.ts +2 -2
- package/dist/src/routes/write.js +2 -2
- package/dist/src/services/BridgeService.d.ts +7 -1
- package/dist/src/services/BridgeService.d.ts.map +1 -1
- package/dist/src/services/BridgeService.js +15 -3
- package/dist/src/services/BridgeService.js.map +1 -1
- package/dist/src/services/FilesystemService.d.ts +1 -1
- package/dist/src/services/FilesystemService.js +2 -2
- package/dist/src/services/declaredSources.d.ts +9 -3
- package/dist/src/services/declaredSources.d.ts.map +1 -1
- package/dist/src/services/declaredSources.js +2 -1
- package/dist/src/services/declaredSources.js.map +1 -1
- package/dist/src/tools/execCtx.d.ts +2 -2
- package/dist/src/tools/execCtx.js +2 -2
- package/dist/src/tools/fs_read.d.ts.map +1 -1
- package/dist/src/tools/fs_read.js +5 -0
- package/dist/src/tools/fs_read.js.map +1 -1
- package/dist/src/usecases/embeddingBacklog.js +1 -1
- package/dist/src/usecases/embeddingBacklog.js.map +1 -1
- package/dist/src/usecases/readFiles.d.ts.map +1 -1
- package/dist/src/usecases/readFiles.js +84 -1
- package/dist/src/usecases/readFiles.js.map +1 -1
- package/dist/src/usecases/syncPointWrites.d.ts +2 -0
- package/dist/src/usecases/syncPointWrites.d.ts.map +1 -1
- package/dist/src/usecases/syncPointWrites.js +5 -1
- package/dist/src/usecases/syncPointWrites.js.map +1 -1
- package/dist/src/usecases/vectorMaintenance.js +1 -1
- package/dist/src/usecases/vectorMaintenance.js.map +1 -1
- package/dist/src/usecases/write/content.js +1 -1
- package/dist/tools/fs_list.json +1 -1
- package/dist/tools/fs_read.json +1 -1
- package/dist/tools/fs_search.json +1 -1
- package/dist/tools/fs_write.json +1 -1
- package/instructions/brain-model.md +1 -1
- package/package.json +70 -63
- package/rules/brain-rules.md +2 -2
- package/skills/brain-start/SKILL.md +1 -1
- package/skills/connect-business-data/SKILL.md +1 -1
- package/skills/create-package/SKILL.md +8 -8
- package/skills/create-package/references/connectors-and-uri-policies.md +7 -2
- package/skills/create-package/references/runtime-and-build.md +1 -1
- package/skills/create-package/references/skills-and-context.md +1 -1
- package/skills/create-package/references/test-and-ship.md +4 -4
- package/skills/create-package/scripts/check-package.sh +1 -1
- package/skills/git/SKILL.md +23 -23
- package/skills/manage-files/SKILL.md +13 -13
- package/skills/manage-files/scripts/upload.sh +1 -1
- package/skills/manage-sources/SKILL.md +55 -54
- package/skills/manage-sources/scripts/reembed.sh +1 -1
- package/skills/manage-uri-policy/SKILL.md +9 -9
- package/skills/organize/SKILL.md +1 -1
- package/skills/sync/SKILL.md +2 -2
- package/tools/fs_list.json +1 -1
- package/tools/fs_read.json +1 -1
- package/tools/fs_search.json +1 -1
- package/tools/fs_write.json +1 -1
- package/workflows/sources-digest.json +1 -1
- package/docs/drive-overview.md +0 -59
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* GET /sources/:slug — get single source config
|
|
7
7
|
* GET /sources/kinds — list registered source kinds (+ trust, package attribution)
|
|
8
8
|
* GET /sources/kinds/:kind/schema — JSON Schema for admin source form
|
|
9
|
-
* GET /sources/discoverable — well-known unattached source roots (W3E;
|
|
9
|
+
* GET /sources/discoverable — well-known unattached source roots (W3E; sources.mount)
|
|
10
10
|
* POST /sources — create source
|
|
11
11
|
* POST /sources/preflight — validate/authorize create without writing
|
|
12
12
|
* PATCH /sources — update source
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
* POST /sources/reset — reset sync state (cancels active job)
|
|
19
19
|
* GET /sources/sync-status — job status (?source= single, else all visible)
|
|
20
20
|
* POST /sources/enabled — light enable/disable toggle (W3K)
|
|
21
|
-
* GET /sources/browse — browse filesystem directories (
|
|
21
|
+
* GET /sources/browse — browse filesystem directories (sources.mount;
|
|
22
22
|
* protected zones + the fs root need
|
|
23
|
-
*
|
|
23
|
+
* sources.privileged)
|
|
24
24
|
* GET /sources/health — get sources health
|
|
25
25
|
*
|
|
26
26
|
* Sync-shaped POSTs return 202 + a SyncQueueJobSnapshot: the run executes on
|
|
@@ -45,17 +45,15 @@ import { listScopeDocumentUris, estimateScopedReembed } from '../usecases/vector
|
|
|
45
45
|
import { embeddingEnabled, resolveActiveEmbeddingShape } from '../config/env.js';
|
|
46
46
|
import { parsePurgeSelector, purgeSourceSelection } from '../usecases/purgeExcludedPoints.js';
|
|
47
47
|
export const pattern = 'sources/*';
|
|
48
|
-
export const feature = '
|
|
48
|
+
export const feature = 'files.read';
|
|
49
49
|
// The module gate is the read tier (listing a source is member-visible); every
|
|
50
50
|
// mutating / enumerating arm escalates in-handler through `hasRouteFeature`.
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
// in a loop — so the drift guard carries it as a named exemption row.
|
|
51
|
+
// A host-plane (paired-device) source is attached by its owner under
|
|
52
|
+
// `device.own`, which agent-core's device gate answers — not a gate here.
|
|
54
53
|
export const escalations = [
|
|
55
|
-
'
|
|
56
|
-
'
|
|
57
|
-
'
|
|
58
|
-
'drive.sync',
|
|
54
|
+
'sources.mount',
|
|
55
|
+
'sources.privileged',
|
|
56
|
+
'sources.sync',
|
|
59
57
|
'platform.vector.maintain',
|
|
60
58
|
];
|
|
61
59
|
export const GET = async (req, raw) => {
|
|
@@ -124,7 +122,7 @@ export const GET = async (req, raw) => {
|
|
|
124
122
|
const origin = matched?.originPackage ?? null;
|
|
125
123
|
const git = await resolveSourceGit(config, infra, ctx);
|
|
126
124
|
const readable = await rootReadable(infra, ctx, config.source, configCache);
|
|
127
|
-
const view = await sourceConfigView(req.session, config, infra?.projectRoot ?? '', protectedRoots, readable, !config.sync && matched && isOwnDataZoneDeclaration(matched.originPackage, matched.decl, state.ctx.selfId) ? dataSyncExclude : undefined);
|
|
125
|
+
const view = await sourceConfigView(req.session, config, infra?.projectRoot ?? '', protectedRoots, readable, deviceAttachVerdict(state, ctx, config.connection), !config.sync && matched && isOwnDataZoneDeclaration(matched.originPackage, matched.decl, state.ctx.selfId) ? dataSyncExclude : undefined);
|
|
128
126
|
return {
|
|
129
127
|
...toPublicSourceConfig(config, view),
|
|
130
128
|
// DERIVED, never persisted — the same chain the `<runtime_stack>`
|
|
@@ -141,7 +139,7 @@ export const GET = async (req, raw) => {
|
|
|
141
139
|
kind_trust: metadata?.packageTrust ?? null,
|
|
142
140
|
origin_package: origin,
|
|
143
141
|
orphan: !metadata, embedding_enabled: embeddingEnabled(),
|
|
144
|
-
canMaintain: hasRouteFeature(req.session, '
|
|
142
|
+
canMaintain: hasRouteFeature(req.session, 'sources.mount') && hasRouteFeature(req.session, 'sources.sync')
|
|
145
143
|
&& canAccessScope(config.scope, access, 'write') && readable,
|
|
146
144
|
orphan_hint: metadata ? null : `Package contributing kind "${config.connection.kind}" is missing — remove the source or reinstall the package.`,
|
|
147
145
|
...(git ? { git } : {}),
|
|
@@ -209,8 +207,8 @@ export const POST = async (req, raw) => {
|
|
|
209
207
|
if (sub === 'purge') {
|
|
210
208
|
const ctx = extractCtx(req);
|
|
211
209
|
await ensureAccess(ctx, state);
|
|
212
|
-
if (!hasRouteFeature(req.session, '
|
|
213
|
-
return { status: 403, body: { error: 'Index cleanup requires
|
|
210
|
+
if (!hasRouteFeature(req.session, 'sources.mount') || !hasRouteFeature(req.session, 'sources.sync')) {
|
|
211
|
+
return { status: 403, body: { error: 'Index cleanup requires sources.mount and sources.sync.' } };
|
|
214
212
|
}
|
|
215
213
|
const body = req.body;
|
|
216
214
|
const source = typeof body?.source === 'string' ? body.source : '';
|
|
@@ -392,8 +390,12 @@ async function handleSourcesCreate(req, state, preflight = false) {
|
|
|
392
390
|
return { status: 400, body: { error: 'recognizesPackages must be a boolean if provided' } };
|
|
393
391
|
}
|
|
394
392
|
await ensureAccess(ctx, state);
|
|
395
|
-
|
|
396
|
-
|
|
393
|
+
// A source on the caller's OWN paired device: its owner with `device.own`
|
|
394
|
+
// satisfies the whole attach stack (`sources.mount` included). On the host
|
|
395
|
+
// plane that is the ONLY way in; every other source meets the stack.
|
|
396
|
+
const ownDevice = deviceAttachVerdict(state, ctx, connection);
|
|
397
|
+
if (!ownDevice.allowed && !hasRouteFeature(req.session, 'sources.mount')) {
|
|
398
|
+
return { status: 403, body: { error: 'Creating sources requires sources.mount.' } };
|
|
397
399
|
}
|
|
398
400
|
if (recognizesPackages !== undefined && !isOwnerOrAdminEffective(req.session)) {
|
|
399
401
|
return { status: 403, body: { error: 'Setting recognizesPackages requires an owner/admin-effective role.' } };
|
|
@@ -401,30 +403,31 @@ async function handleSourcesCreate(req, state, preflight = false) {
|
|
|
401
403
|
if (recognizesPackages === true && connection.kind !== 'local') {
|
|
402
404
|
return { status: 400, body: { error: 'recognizesPackages may only be true for local-kind sources.' } };
|
|
403
405
|
}
|
|
404
|
-
// fenced-courtyard (HEALTH1 B3) — privileged-root ADD gate. `
|
|
406
|
+
// fenced-courtyard (HEALTH1 B3) — privileged-root ADD gate. `sources.mount`
|
|
405
407
|
// attaches ordinary sources, but attaching a source rooted IN, OVER, or AT a
|
|
406
408
|
// protected host zone (App Zone, the Neuralis tree, the whole host) requires
|
|
407
|
-
// the `
|
|
409
|
+
// the `sources.privileged` feature (not granted below the `admin` tier).
|
|
408
410
|
// The check is canonical
|
|
409
411
|
// path CONTAINMENT against the SHARED, filter-free protected-root table — NOT
|
|
410
412
|
// exact-root or a client suggestion id (both spoofable via subpath / ancestor /
|
|
411
413
|
// `..` / trailing-slash / a Docker free-root attach with no suggestion). The
|
|
412
414
|
// requested root is resolved with `resolveLocalRoot` — the SAME canonicaliser
|
|
413
415
|
// the connector uses — so the gate sees exactly what will be mounted.
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
// and no `protectedRoots.length` precondition
|
|
417
|
-
//
|
|
418
|
-
// sub-directory root is gated exactly like `/`.
|
|
416
|
+
// The HOST plane is decided BEFORE the root-sensitive container gate below:
|
|
417
|
+
// the device's owner with `device.own`, nobody else, with no root-string
|
|
418
|
+
// inspection and no `protectedRoots.length` precondition (fail closed even
|
|
419
|
+
// when the runtime stack is unavailable; a sub-directory is gated like `/`).
|
|
419
420
|
// The container-plane arm is kind-AGNOSTIC (see `missingAttachFeature`).
|
|
420
|
-
const missingForAttach = await missingAttachFeature(req.session, connection, buildProtectedRoots(state.ctx.platform.runtimeStack()), async (root) => resolveLocalRoot({ configured: root, projectRoot: (await state.infraFactory.getInfra(projectId)).projectRoot ?? '', source }).resolvedRoot);
|
|
421
|
+
const missingForAttach = await missingAttachFeature(req.session, connection, buildProtectedRoots(state.ctx.platform.runtimeStack()), async (root) => resolveLocalRoot({ configured: root, projectRoot: (await state.infraFactory.getInfra(projectId)).projectRoot ?? '', source }).resolvedRoot, ownDevice);
|
|
421
422
|
if (missingForAttach) {
|
|
422
423
|
return {
|
|
423
424
|
status: 403,
|
|
424
425
|
body: {
|
|
425
|
-
error: missingForAttach.plane === '
|
|
426
|
-
?
|
|
427
|
-
:
|
|
426
|
+
error: missingForAttach.plane === 'device'
|
|
427
|
+
? "Only the device's owner may attach a source on it."
|
|
428
|
+
: missingForAttach.plane === 'host'
|
|
429
|
+
? `Attaching a host-plane source requires the ${missingForAttach.feature} feature.`
|
|
430
|
+
: `Attaching a source rooted in a protected host zone requires the ${missingForAttach.feature} feature.`,
|
|
428
431
|
},
|
|
429
432
|
};
|
|
430
433
|
}
|
|
@@ -519,47 +522,11 @@ function isOwnerOrAdminEffective(session) {
|
|
|
519
522
|
// is containment in BOTH directions (architect MAJOR-1).
|
|
520
523
|
// ---------------------------------------------------------------------------
|
|
521
524
|
/** Feature required to SEE/ADD a high-blast-radius host zone (C8 — no grant below the admin tier). */
|
|
522
|
-
const PRIVILEGED_MOUNT_FEATURE = '
|
|
523
|
-
/**
|
|
524
|
-
* Feature required to attach ANY source on the HOST plane (native-nightjar
|
|
525
|
-
* BFIX / B2 — no grant below the admin tier).
|
|
526
|
-
*
|
|
527
|
-
* `drive.mount` is a MANAGER default. The privileged-root machinery above was
|
|
528
|
-
* once scoped to `kind === 'local'`, so a new connector kind that reaches
|
|
529
|
-
* outside the container would have walked straight past it: an ordinary
|
|
530
|
-
* manager could attach a `host-*` source rooted at `/` and get full host
|
|
531
|
-
* read/write over the `fs_*` tools — with no `exec.host`, no owner,
|
|
532
|
-
* no privileged gate. (The container arm is kind-agnostic now — any
|
|
533
|
-
* non-host-plane kind with a string `root` is gated — but rule 1 below is
|
|
534
|
-
* still what makes the HOST plane safe independent of root shape.)
|
|
535
|
-
*
|
|
536
|
-
* Two rules close it, and BOTH always apply on this plane:
|
|
537
|
-
* 1. every host-plane root is privileged BY CONSTRUCTION (there is no
|
|
538
|
-
* unprivileged host root — see `hostPlaneRequiredFeatures`), and
|
|
539
|
-
* 2. attaching one additionally requires this feature.
|
|
540
|
-
*
|
|
541
|
-
* The operator-owned broker ceiling is the third, out-of-band clamp: even a
|
|
542
|
-
* granted caller only reaches paths the operator declared. The ceiling is the
|
|
543
|
-
* floor; the feature is the audit trail.
|
|
544
|
-
*/
|
|
545
|
-
const HOST_PLANE_MOUNT_FEATURE = 'drive.mount.host';
|
|
546
|
-
// `isHostPlaneKind` lives in `@neuralis/package-system/contracts` — imported,
|
|
547
|
-
// never re-declared. Two copies of a plane predicate is how the attach gate and
|
|
548
|
-
// the terminal gate drift apart while both look correct in isolation.
|
|
549
|
-
/**
|
|
550
|
-
* Features required to attach a host-plane source, independent of its root.
|
|
551
|
-
*
|
|
552
|
-
* There is deliberately NO root-sensitivity here: on the host plane every root
|
|
553
|
-
* is a protected zone, so a `/home/me/notes` attach is gated exactly like `/`.
|
|
554
|
-
* Anything less would re-open B2 through a subdirectory.
|
|
555
|
-
*/
|
|
556
|
-
function hostPlaneRequiredFeatures() {
|
|
557
|
-
return [HOST_PLANE_MOUNT_FEATURE, PRIVILEGED_MOUNT_FEATURE];
|
|
558
|
-
}
|
|
525
|
+
const PRIVILEGED_MOUNT_FEATURE = 'sources.privileged';
|
|
559
526
|
/**
|
|
560
527
|
* Build the filter-free protected-root table from the runtime stack. App Zone,
|
|
561
528
|
* the Neuralis source tree, and the whole host (`/`) all require
|
|
562
|
-
* `
|
|
529
|
+
* `sources.privileged`. `/` is ALWAYS included (both Docker and native) so
|
|
563
530
|
* the ADD gate catches a raw `config.root:'/'` attach that has no discoverable
|
|
564
531
|
* suggestion under Docker.
|
|
565
532
|
*/
|
|
@@ -619,18 +586,21 @@ export function requiredFeaturesForRoot(root, protectedRoots, kind, mode = 'cont
|
|
|
619
586
|
* the listing projection, so "you see the path you could attach" cannot drift
|
|
620
587
|
* from what attaching requires.
|
|
621
588
|
*
|
|
622
|
-
* - Host plane:
|
|
623
|
-
*
|
|
589
|
+
* - Host plane: the DEVICE's owner holding `device.own` (`ownDevice`,
|
|
590
|
+
* agent-core's device gate) and nobody else — a non-owner is refused as
|
|
591
|
+
* `device`, an owner without `device.own` is told the feature it lacks; no
|
|
592
|
+
* root inspection and no runtime-stack precondition (fail closed).
|
|
624
593
|
* - Container plane, kind-AGNOSTIC: any kind whose config carries a string
|
|
625
594
|
* `root` — `local`, and every root-bearing kind a package registers later —
|
|
626
595
|
* resolves through `resolveRoot` (the connector's own canonicaliser) and hits
|
|
627
596
|
* the SAME containment table. Scoping this to one kind is how a newly
|
|
628
597
|
* registered root-bearing kind walks straight past the gate.
|
|
629
598
|
*/
|
|
630
|
-
async function missingAttachFeature(session, connection, protectedRoots, resolveRoot) {
|
|
599
|
+
async function missingAttachFeature(session, connection, protectedRoots, resolveRoot, ownDevice) {
|
|
631
600
|
if (isHostPlaneKind(connection.kind)) {
|
|
632
|
-
|
|
633
|
-
|
|
601
|
+
if (ownDevice.allowed)
|
|
602
|
+
return null;
|
|
603
|
+
return { feature: 'device.own', plane: ownDevice.owner ? 'host' : 'device' };
|
|
634
604
|
}
|
|
635
605
|
if (protectedRoots.length === 0)
|
|
636
606
|
return null;
|
|
@@ -641,6 +611,30 @@ async function missingAttachFeature(session, connection, protectedRoots, resolve
|
|
|
641
611
|
const feature = required.find((f) => !hasRouteFeature(session, f));
|
|
642
612
|
return feature ? { feature, plane: 'zone' } : null;
|
|
643
613
|
}
|
|
614
|
+
/**
|
|
615
|
+
* Ask agent-core's ONE device gate (`devices.mayAttach`) about the device a
|
|
616
|
+
* host-plane connection names. brain-core reads `config.device` as an opaque
|
|
617
|
+
* string and knows no device semantics; no agent-core, no device or a
|
|
618
|
+
* non-host kind ⇒ `{ allowed: false, owner: false }` (fail closed).
|
|
619
|
+
*/
|
|
620
|
+
function deviceAttachVerdict(state, ctx, connection) {
|
|
621
|
+
const denied = { allowed: false, owner: false };
|
|
622
|
+
if (!isHostPlaneKind(connection.kind))
|
|
623
|
+
return denied;
|
|
624
|
+
const device = connection.config?.device;
|
|
625
|
+
if (typeof device !== 'string' || !device)
|
|
626
|
+
return denied;
|
|
627
|
+
const session = {
|
|
628
|
+
userId: ctx.userId,
|
|
629
|
+
projectId: ctx.projectId,
|
|
630
|
+
...(ctx.agentId ? { agentId: ctx.agentId } : {}),
|
|
631
|
+
callerId: '@neuralis/brain-core',
|
|
632
|
+
callerTrust: 'first-party',
|
|
633
|
+
grantedFeatures: ctx.grantedFeatures,
|
|
634
|
+
};
|
|
635
|
+
const agentCore = state.ctx.packages?.getPackageApi('@neuralis/agent-core', session);
|
|
636
|
+
return agentCore?.devices?.mayAttach({ userId: ctx.userId, grantedFeatures: ctx.grantedFeatures }, device) ?? denied;
|
|
637
|
+
}
|
|
644
638
|
/**
|
|
645
639
|
* May this caller READ the source's root — the same per-uri read predicate the
|
|
646
640
|
* listing paths use. Fails closed per row: no config store, or a throwing
|
|
@@ -653,16 +647,16 @@ async function rootReadable(infra, ctx, source, configCache) {
|
|
|
653
647
|
}
|
|
654
648
|
/**
|
|
655
649
|
* The view `toPublicSourceConfig` answers with. The connection config goes to
|
|
656
|
-
* a caller who could attach this source (`
|
|
650
|
+
* a caller who could attach this source (`sources.mount` plus the gates
|
|
657
651
|
* `missingAttachFeature` runs) OR can read its root — the files there already
|
|
658
652
|
* show their host path as `os_uri`, so withholding it from a reader hides
|
|
659
|
-
* nothing; the raw last-sync error goes to a `
|
|
653
|
+
* nothing; the raw last-sync error goes to a `sources.sync` holder, the tier
|
|
660
654
|
* `sources/sync-status` answers to. A feature, never a role name.
|
|
661
655
|
*/
|
|
662
|
-
async function sourceConfigView(session, config, projectRoot, protectedRoots, readable, defaultSyncExclude) {
|
|
663
|
-
const mayAttach = hasRouteFeature(session, '
|
|
664
|
-
&& await missingAttachFeature(session, config.connection, protectedRoots, (root) => resolveLocalRoot({ configured: root, projectRoot, source: config.source }).resolvedRoot) === null;
|
|
665
|
-
return { mayReadPaths: mayAttach || readable, maySeeSyncErrors: hasRouteFeature(session, '
|
|
656
|
+
async function sourceConfigView(session, config, projectRoot, protectedRoots, readable, ownDevice, defaultSyncExclude) {
|
|
657
|
+
const mayAttach = (ownDevice.allowed || hasRouteFeature(session, 'sources.mount'))
|
|
658
|
+
&& await missingAttachFeature(session, config.connection, protectedRoots, (root) => resolveLocalRoot({ configured: root, projectRoot, source: config.source }).resolvedRoot, ownDevice) === null;
|
|
659
|
+
return { mayReadPaths: mayAttach || readable, maySeeSyncErrors: hasRouteFeature(session, 'sources.sync'),
|
|
666
660
|
...(defaultSyncExclude ? { defaultSyncExclude } : {}) };
|
|
667
661
|
}
|
|
668
662
|
/** `sourceConfigView` for a single-row answer (single GET, mutation echoes). */
|
|
@@ -673,7 +667,7 @@ async function publicViewFor(session, ctx, config, state, infra) {
|
|
|
673
667
|
}) : null;
|
|
674
668
|
const defaults = matched && isOwnDataZoneDeclaration(matched.originPackage, matched.decl, state.ctx.selfId)
|
|
675
669
|
? seedExcludedMachineWrittenGlobs(state.ctx.platform.machineWrittenPaths()) : undefined;
|
|
676
|
-
return sourceConfigView(session, config, infra?.projectRoot ?? '', buildProtectedRoots(state.ctx.platform.runtimeStack()), readable, defaults);
|
|
670
|
+
return sourceConfigView(session, config, infra?.projectRoot ?? '', buildProtectedRoots(state.ctx.platform.runtimeStack()), readable, deviceAttachVerdict(state, ctx, config.connection), defaults);
|
|
677
671
|
}
|
|
678
672
|
function filterSourceForList(config, ctx, requestedScope, includeObserved) {
|
|
679
673
|
if (requestedScope && config.scope.kind !== requestedScope)
|
|
@@ -723,12 +717,9 @@ async function handleSourcesUpdate(req, state) {
|
|
|
723
717
|
return { status: 400, body: { error: 'recognizesPackages must be a boolean if provided' } };
|
|
724
718
|
}
|
|
725
719
|
await ensureAccess(ctx, state);
|
|
726
|
-
if (!hasRouteFeature(req.session, 'drive.mount')) {
|
|
727
|
-
return { status: 403, body: { error: 'Updating sources requires drive.mount.' } };
|
|
728
|
-
}
|
|
729
720
|
// R2 (architect MINOR-2) — flagging a source `recognizesPackages` creates a
|
|
730
721
|
// code-LOAD surface, so the WRITE is owner/admin-effective only, even though
|
|
731
|
-
// other source fields are
|
|
722
|
+
// other source fields are sources.mount. Deny-by-default.
|
|
732
723
|
if (recognizesPackages !== undefined && !isOwnerOrAdminEffective(req.session)) {
|
|
733
724
|
return { status: 403, body: { error: 'Setting recognizesPackages requires an owner/admin-effective role.' } };
|
|
734
725
|
}
|
|
@@ -737,6 +728,10 @@ async function handleSourcesUpdate(req, state) {
|
|
|
737
728
|
const existing = await bridge.getSourceConfig(scope, source, toScopeAccessCtx(ctx));
|
|
738
729
|
if (!existing)
|
|
739
730
|
return { status: 404, body: { error: `Source not found: ${source}` } };
|
|
731
|
+
const ownDevice = deviceAttachVerdict(state, ctx, existing.connection);
|
|
732
|
+
if (!ownDevice.allowed && !hasRouteFeature(req.session, 'sources.mount')) {
|
|
733
|
+
return { status: 403, body: { error: 'Updating sources requires sources.mount.' } };
|
|
734
|
+
}
|
|
740
735
|
if (!canAccessScope(existing.scope, toScopeAccessCtx(ctx), 'write')) {
|
|
741
736
|
return { status: 403, body: { error: 'Forbidden for source scope' } };
|
|
742
737
|
}
|
|
@@ -752,22 +747,24 @@ async function handleSourcesUpdate(req, state) {
|
|
|
752
747
|
if (recognizesPackages === true && existing.connection.kind !== 'local') {
|
|
753
748
|
return { status: 400, body: { error: 'recognizesPackages may only be true for local-kind sources.' } };
|
|
754
749
|
}
|
|
755
|
-
//
|
|
756
|
-
//
|
|
757
|
-
// one
|
|
758
|
-
// plain-`
|
|
750
|
+
// PATCH cannot change `connection`, but it CAN widen an existing host-plane
|
|
751
|
+
// source's permissions (read → write). Editing one therefore needs what
|
|
752
|
+
// attaching one does — the device's owner with `device.own` — otherwise a
|
|
753
|
+
// plain-`sources.mount` manager could escalate a read-only host source in place.
|
|
759
754
|
// Same rule for the container plane: a PATCH can widen permissions (read →
|
|
760
755
|
// write) on a source already rooted in a protected zone, so editing one
|
|
761
756
|
// requires the same grants attaching one does — for EVERY root-bearing kind,
|
|
762
757
|
// not just `local`.
|
|
763
|
-
const missingForEdit = await missingAttachFeature(req.session, existing.connection, buildProtectedRoots(state.ctx.platform.runtimeStack()), async (root) => resolveLocalRoot({ configured: root, projectRoot: (await state.infraFactory.getInfra(projectId)).projectRoot ?? '', source }).resolvedRoot);
|
|
758
|
+
const missingForEdit = await missingAttachFeature(req.session, existing.connection, buildProtectedRoots(state.ctx.platform.runtimeStack()), async (root) => resolveLocalRoot({ configured: root, projectRoot: (await state.infraFactory.getInfra(projectId)).projectRoot ?? '', source }).resolvedRoot, ownDevice);
|
|
764
759
|
if (missingForEdit) {
|
|
765
760
|
return {
|
|
766
761
|
status: 403,
|
|
767
762
|
body: {
|
|
768
|
-
error: missingForEdit.plane === '
|
|
769
|
-
?
|
|
770
|
-
:
|
|
763
|
+
error: missingForEdit.plane === 'device'
|
|
764
|
+
? "Only the device's owner may edit a source on it."
|
|
765
|
+
: missingForEdit.plane === 'host'
|
|
766
|
+
? `Editing a host-plane source requires the ${missingForEdit.feature} feature.`
|
|
767
|
+
: `Editing a source rooted in a protected host zone requires the ${missingForEdit.feature} feature.`,
|
|
771
768
|
},
|
|
772
769
|
};
|
|
773
770
|
}
|
|
@@ -809,12 +806,14 @@ async function handleSourcesEnabled(req, state) {
|
|
|
809
806
|
return { status: 400, body: { error: 'enabled must be boolean or "auto"' } };
|
|
810
807
|
}
|
|
811
808
|
await ensureAccess(ctx, state);
|
|
812
|
-
if (!hasRouteFeature(req.session, 'drive.mount')) {
|
|
813
|
-
return { status: 403, body: { error: 'Toggling source enabled state requires drive.mount.' } };
|
|
814
|
-
}
|
|
815
809
|
const bridge = await resolveBridge(state, ctx.projectId);
|
|
816
810
|
const scope = { userId: ctx.userId, projectId: ctx.projectId };
|
|
817
811
|
const existing = await bridge.getSourceConfig(scope, source, toScopeAccessCtx(ctx));
|
|
812
|
+
// The owner of a paired device manages a source on it with `device.own` —
|
|
813
|
+
// online or offline (D24).
|
|
814
|
+
if (!hasRouteFeature(req.session, 'sources.mount') && !(existing && deviceAttachVerdict(state, ctx, existing.connection).allowed)) {
|
|
815
|
+
return { status: 403, body: { error: 'Toggling source enabled state requires sources.mount.' } };
|
|
816
|
+
}
|
|
818
817
|
if (!existing)
|
|
819
818
|
return { status: 404, body: { error: `Source not found: ${source}` } };
|
|
820
819
|
if (!canAccessScope(existing.scope, toScopeAccessCtx(ctx), 'write')) {
|
|
@@ -832,13 +831,14 @@ async function handleSourcesEnabled(req, state) {
|
|
|
832
831
|
async function handleSourcesDelete(slug, req, state) {
|
|
833
832
|
const ctx = extractCtx(req);
|
|
834
833
|
await ensureAccess(ctx, state);
|
|
835
|
-
if (!hasRouteFeature(req.session, 'drive.mount')) {
|
|
836
|
-
return { status: 403, body: { error: 'Deleting sources requires drive.mount.' } };
|
|
837
|
-
}
|
|
838
834
|
const bridge = await resolveBridge(state, ctx.projectId);
|
|
839
835
|
const scope = { userId: ctx.userId, projectId: ctx.projectId };
|
|
840
836
|
const configs = await bridge.listSourceConfigs(scope, toScopeAccessCtx(ctx));
|
|
841
837
|
const match = configs.find((c) => c.source.replace(/[^a-zA-Z0-9-]/g, '-') === slug);
|
|
838
|
+
// The owner of a paired device removes a source on it with `device.own` (D24).
|
|
839
|
+
if (!hasRouteFeature(req.session, 'sources.mount') && !(match && deviceAttachVerdict(state, ctx, match.connection).allowed)) {
|
|
840
|
+
return { status: 403, body: { error: 'Deleting sources requires sources.mount.' } };
|
|
841
|
+
}
|
|
842
842
|
if (!match)
|
|
843
843
|
return { status: 404, body: { error: `Source not found: ${slug}` } };
|
|
844
844
|
if (!canAccessScope(match.scope, toScopeAccessCtx(ctx), 'write')) {
|
|
@@ -876,8 +876,8 @@ async function handleSourcesSync(req, state) {
|
|
|
876
876
|
if (!source)
|
|
877
877
|
return { status: 400, body: { error: 'Missing source' } };
|
|
878
878
|
await ensureAccess(ctx, state);
|
|
879
|
-
if (!hasRouteFeature(req.session, '
|
|
880
|
-
return { status: 403, body: { error: 'Sync requires
|
|
879
|
+
if (!hasRouteFeature(req.session, 'sources.sync')) {
|
|
880
|
+
return { status: 403, body: { error: 'Sync requires sources.sync.' } };
|
|
881
881
|
}
|
|
882
882
|
const bridge = await resolveBridge(state, projectId);
|
|
883
883
|
const scope = { userId: ctx.userId, projectId };
|
|
@@ -906,13 +906,13 @@ async function handleSourcesSync(req, state) {
|
|
|
906
906
|
* (architect F4): cancel is allowed exactly where the caller could trigger a
|
|
907
907
|
* sync — the same `getSourceConfig(…, access)` visibility check — which
|
|
908
908
|
* deliberately covers background/system jobs on that source (the freeze
|
|
909
|
-
* scenario must be killable by any
|
|
909
|
+
* scenario must be killable by any sources.sync holder who can see the source).
|
|
910
910
|
*/
|
|
911
911
|
async function handleSourcesCancel(req, state) {
|
|
912
912
|
const ctx = extractCtx(req);
|
|
913
913
|
await ensureAccess(ctx, state);
|
|
914
|
-
if (!hasRouteFeature(req.session, '
|
|
915
|
-
return { status: 403, body: { error: 'Cancelling sync requires
|
|
914
|
+
if (!hasRouteFeature(req.session, 'sources.sync')) {
|
|
915
|
+
return { status: 403, body: { error: 'Cancelling sync requires sources.sync.' } };
|
|
916
916
|
}
|
|
917
917
|
const bridge = await resolveBridge(state, ctx.projectId);
|
|
918
918
|
const scope = { userId: ctx.userId, projectId: ctx.projectId };
|
|
@@ -935,7 +935,7 @@ async function handleSourcesCancel(req, state) {
|
|
|
935
935
|
return { status: 200, body: { ok: true, cancelled } };
|
|
936
936
|
}
|
|
937
937
|
/**
|
|
938
|
-
* POST /sources/reindex — three operations behind ONE gate (`
|
|
938
|
+
* POST /sources/reindex — three operations behind ONE gate (`sources.sync` +
|
|
939
939
|
* source visibility):
|
|
940
940
|
* - `{source}` — the full re-sync from scratch ("Rescan changes"): markers
|
|
941
941
|
* cleared, a full job enqueued; unchanged indexed files reuse their vectors,
|
|
@@ -951,7 +951,7 @@ async function handleSourcesCancel(req, state) {
|
|
|
951
951
|
* `scope:'platform'` (`platform.vector.maintain`, whole source only) it
|
|
952
952
|
* catches up EVERY writer's points — the way back after a dense clear.
|
|
953
953
|
* The document set is the sync inventory's scope, filtered by the caller's
|
|
954
|
-
* per-uri READ policy before anything is counted — a `
|
|
954
|
+
* per-uri READ policy before anything is counted — a `sources.sync` holder never
|
|
955
955
|
* learns the size of a file it may not read.
|
|
956
956
|
*/
|
|
957
957
|
async function handleSourcesReindex(req, state) {
|
|
@@ -965,8 +965,8 @@ async function handleSourcesReindex(req, state) {
|
|
|
965
965
|
// CLAUDE.md principle 12).
|
|
966
966
|
const ctx = extractCtx(req);
|
|
967
967
|
await ensureAccess(ctx, state);
|
|
968
|
-
if (!hasRouteFeature(req.session, '
|
|
969
|
-
return { status: 403, body: { error: 'Reindex requires
|
|
968
|
+
if (!hasRouteFeature(req.session, 'sources.sync')) {
|
|
969
|
+
return { status: 403, body: { error: 'Reindex requires sources.sync.' } };
|
|
970
970
|
}
|
|
971
971
|
const force = body?.force === true;
|
|
972
972
|
const embeddingOnly = body?.embeddingOnly === true;
|
|
@@ -1089,8 +1089,8 @@ async function handleScopedReembed(ctx, state, bridge, req) {
|
|
|
1089
1089
|
async function handleSyncStatus(req, state) {
|
|
1090
1090
|
const ctx = extractCtx(req);
|
|
1091
1091
|
await ensureAccess(ctx, state);
|
|
1092
|
-
if (!hasRouteFeature(req.session, '
|
|
1093
|
-
return { status: 403, body: { error: 'Sync status requires
|
|
1092
|
+
if (!hasRouteFeature(req.session, 'sources.sync')) {
|
|
1093
|
+
return { status: 403, body: { error: 'Sync status requires sources.sync.' } };
|
|
1094
1094
|
}
|
|
1095
1095
|
const bridge = await resolveBridge(state, ctx.projectId);
|
|
1096
1096
|
const scope = { userId: ctx.userId, projectId: ctx.projectId };
|
|
@@ -1116,10 +1116,10 @@ async function handleSourcesReset(req, state) {
|
|
|
1116
1116
|
const ctx = extractCtx(req);
|
|
1117
1117
|
await ensureAccess(ctx, state);
|
|
1118
1118
|
// Reset mutates sync markers AND cancels the source's active job — it is
|
|
1119
|
-
// a sync-shaped mutation, so it carries the same
|
|
1119
|
+
// a sync-shaped mutation, so it carries the same sources.sync gate + source
|
|
1120
1120
|
// visibility check as sync/cancel/reindex (architect MAJOR-2, F4 rule).
|
|
1121
|
-
if (!hasRouteFeature(req.session, '
|
|
1122
|
-
return { status: 403, body: { error: 'Reset requires
|
|
1121
|
+
if (!hasRouteFeature(req.session, 'sources.sync')) {
|
|
1122
|
+
return { status: 403, body: { error: 'Reset requires sources.sync.' } };
|
|
1123
1123
|
}
|
|
1124
1124
|
const bridge = await resolveBridge(state, ctx.projectId);
|
|
1125
1125
|
const scope = { userId: ctx.userId, projectId: ctx.projectId };
|
|
@@ -1164,8 +1164,8 @@ async function handleSourcesDiscoverable(req, state) {
|
|
|
1164
1164
|
// without this a viewer still read every mount's name and host path. The UI
|
|
1165
1165
|
// is already fail-soft on a 403 here (`FileTree` gates its fetch on the same
|
|
1166
1166
|
// feature; `SourcesPanel` catches into an empty list).
|
|
1167
|
-
if (!hasRouteFeature(req.session, '
|
|
1168
|
-
return { status: 403, body: { error: 'Listing discoverable source roots requires the
|
|
1167
|
+
if (!hasRouteFeature(req.session, 'sources.mount')) {
|
|
1168
|
+
return { status: 403, body: { error: 'Listing discoverable source roots requires the sources.mount feature.' } };
|
|
1169
1169
|
}
|
|
1170
1170
|
const stack = state.ctx.platform.runtimeStack();
|
|
1171
1171
|
const bridge = await resolveBridge(state, ctx.projectId);
|
|
@@ -1368,7 +1368,7 @@ async function handleSourcesDiscoverable(req, state) {
|
|
|
1368
1368
|
// Full host — only in native (non-Docker) mode, where `/` IS the host root
|
|
1369
1369
|
// and is directly readable. In Docker the host is invisible unless bound
|
|
1370
1370
|
// (`pnpm neuralis:mount add / --slug host`), which then surfaces via the
|
|
1371
|
-
// generic mount loop below. Opt-in, gated by `
|
|
1371
|
+
// generic mount loop below. Opt-in, gated by `sources.privileged` (no
|
|
1372
1372
|
// grant below the `admin` tier), attach READ-ONLY by default
|
|
1373
1373
|
// (suggestedAccess) — the whole-host blast radius warrants deny-by-default;
|
|
1374
1374
|
// the attacher narrows the uri-policy (or widens write) before use.
|
|
@@ -1449,15 +1449,15 @@ async function handleSourcesDiscoverable(req, state) {
|
|
|
1449
1449
|
* warded-wren (HOST-OSACC-FB9). This handler used to be the one route in the
|
|
1450
1450
|
* file that checked nothing: no `ensureAccess`, no in-handler feature, no
|
|
1451
1451
|
* protected-root table, and a raw `err.message` in the body. The route-level
|
|
1452
|
-
* `feature = '
|
|
1452
|
+
* `feature = 'files.read'` is a VIEWER default, so on a native install any
|
|
1453
1453
|
* signed-in reader could walk the operator's host one level at a time
|
|
1454
1454
|
* (`?path=/` → `/home` → `/root`), and on win32 enumerate the drive letters.
|
|
1455
1455
|
*
|
|
1456
1456
|
* The rule is one sentence: **you may browse exactly what you would be allowed
|
|
1457
|
-
* to attach.** The base feature is `
|
|
1457
|
+
* to attach.** The base feature is `sources.mount` — the same gate
|
|
1458
1458
|
* `handleSourcesCreate` gives the act this one prepares — and per-path the
|
|
1459
1459
|
* SHARED protected-root table decides, so `/`, the App Zone and the Neuralis
|
|
1460
|
-
* tree cost `
|
|
1460
|
+
* tree cost `sources.privileged` here exactly as they do at attach time.
|
|
1461
1461
|
* `'see'` is the semantically right column; today both columns hold the same
|
|
1462
1462
|
* feature, so nothing in the suite proves the distinction — it is intent, not
|
|
1463
1463
|
* enforcement.
|
|
@@ -1468,8 +1468,8 @@ async function handleSourcesBrowse(req, state) {
|
|
|
1468
1468
|
// Not the security gate — the gates are the feature check below and the
|
|
1469
1469
|
// per-path table. `ensureAccess` buys uniformity with every sibling handler
|
|
1470
1470
|
// and the project-override 403 in `extractCtx`.
|
|
1471
|
-
if (!hasRouteFeature(req.session, '
|
|
1472
|
-
return { status: 403, body: { error: 'Browsing filesystem paths requires the
|
|
1471
|
+
if (!hasRouteFeature(req.session, 'sources.mount')) {
|
|
1472
|
+
return { status: 403, body: { error: 'Browsing filesystem paths requires the sources.mount feature.' } };
|
|
1473
1473
|
}
|
|
1474
1474
|
const { readdir, realpath } = await import('node:fs/promises');
|
|
1475
1475
|
const { resolve, join, dirname } = await import('node:path');
|