@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
package/README.md
CHANGED
|
@@ -33,10 +33,11 @@ content-free existence probe a caller uses instead of reading a whole file to
|
|
|
33
33
|
learn it is still there), `brain`
|
|
34
34
|
(Qdrant-backed internal memory; reads go through `FilesystemService`, not the
|
|
35
35
|
placeholder connector). Other packages contribute more kinds — `webtop` from
|
|
36
|
-
`machine-core`, and `host` from `agent-core` (a read-WRITE view of
|
|
37
|
-
|
|
38
|
-
`write`/`delete`/`move`/`mkdir`/`rmdir
|
|
39
|
-
|
|
36
|
+
`machine-core`, and `host` from `agent-core` (a read-WRITE view of a paired
|
|
37
|
+
device — the server or a person's own machine — through its broker over the device
|
|
38
|
+
link; 13 declared capabilities including `write`/`delete`/`move`/`mkdir`/`rmdir`
|
|
39
|
+
and `rawRead`; containment comes from the device owner's ceiling, never from a
|
|
40
|
+
missing capability. See the
|
|
40
41
|
host-plane rules below).
|
|
41
42
|
|
|
42
43
|
A connector's **manifest id is not its kind**, and only the KIND addresses the
|
|
@@ -82,7 +83,7 @@ URI schemes you will see in production:
|
|
|
82
83
|
| `data://` | `local` | builtin | Project data zone (`{neuralisHome}/projects/{pid}/data/`) |
|
|
83
84
|
| `data.X://` | `local` | user | Additional `local` mount with a named slug |
|
|
84
85
|
| `packages://` | `local` | builtin | Project's `_packages/` directory |
|
|
85
|
-
| `app://` | `local` | builtin (privileged mount — `
|
|
86
|
+
| `app://` | `local` | builtin (privileged mount — `sources.privileged`) | Platform app zone (`~/.neuralis/app/`); writes from agents are denied |
|
|
86
87
|
| `neuralis://` | `local` | user (`pnpm neuralis:mount add … --neuralis`) | The deployment's OWN `neuralis/` folder, live from the host. Surfaces as a `local`-kind source — there is **no** dedicated `neuralis` connector kind. The switch is `--neuralis`, not `--slug neuralis` (that is the ordinary data mount at `/mounts/neuralis`, without the two anonymous volumes that keep `_runtime/` and `node_modules/` from being shadowed). |
|
|
87
88
|
| `neuralis-skills://` | `local` | user (same overlay, rooted at `<root>/skills`) | The narrower default: only the shipped operator skills. Preferred over `neuralis://` for giving an agent the operator-command vocabulary — the overlay root holds `.env`, and a source root is a READ boundary in a way a sync `include` is not. |
|
|
88
89
|
| `machine://` | `webtop` | machine-core | Webtop sandbox; declared by `machine-core`, not brain-core. `machine` is the slug / `urlScheme` — the registry KIND is `webtop`, and only the kind addresses `GET /sources/kinds/:kind/schema`. |
|
|
@@ -483,18 +484,18 @@ in-source over-refresh is fine, never a leak), and cache invalidation is already
|
|
|
483
484
|
|
|
484
485
|
**`git.canWrite` — the write-UI visibility gate.** `canWrite` is computed SERVER-side
|
|
485
486
|
**per-caller** as the SAME two-plane gate the write routes enforce: the caller HAS the
|
|
486
|
-
`
|
|
487
|
+
`files.write` FEATURE **and** the caller's RESOLVED uri-policy `write` on the source root
|
|
487
488
|
`<slug>://` is allowed (`resolveGitCanWrite`, deny-by-default). The panel shows the
|
|
488
489
|
Stage/Unstage/Discard/Commit/branch-write affordances IFF `git.canWrite === true` — or, with
|
|
489
490
|
a focused NESTED repo, IFF that repo's `/git/repos` row carries `canWrite: true` (the
|
|
490
491
|
per-repo granularity above; the root row's flag is not consulted). This must NOT
|
|
491
492
|
be derived from `SourceInfo.permissions.write` (the source's DEFAULT permission block) — that
|
|
492
493
|
hid the owner's write UI on a `default write:false` source where the owner gains write via
|
|
493
|
-
`byRole`/`byUser`, and would also show buttons to a `
|
|
494
|
+
`byRole`/`byUser`, and would also show buttons to a `files.read`-only role. The server route is
|
|
494
495
|
the deny-by-default floor regardless of this UI gate.
|
|
495
496
|
|
|
496
|
-
**The `git/*` routes** (`src/routes/git.ts`, file-level `feature='
|
|
497
|
-
- **GET (read, `
|
|
497
|
+
**The `git/*` routes** (`src/routes/git.ts`, file-level `feature='files.read'`):
|
|
498
|
+
- **GET (read, `files.read`):** `status?source=<slug>` (whole-repo change-set + current
|
|
498
499
|
branch, plus `upstream`/`ahead`/`behind` when the branch tracks an upstream — all
|
|
499
500
|
three absent otherwise), `branch?source=<slug>` (local branch list, for the switcher),
|
|
500
501
|
`diff?source=<slug>&path=<repoRelPath>&(staged=<0|1> | commit=<sha> | base=<sha>&head=<sha>)`
|
|
@@ -526,7 +527,7 @@ the deny-by-default floor regardless of this UI gate.
|
|
|
526
527
|
`repos?source=<slug>` (the in-source repo-picker listing `{rel, name, branch?,
|
|
527
528
|
canWrite}`; names-only, source-root read gate + per-nested-name filter —
|
|
528
529
|
see the multi-repo model above).
|
|
529
|
-
- **POST (write, additionally `
|
|
530
|
+
- **POST (write, additionally `files.write`-gated):** `stage` (`{paths[]}` whole-file `add`
|
|
530
531
|
OR `{hunk}` `git apply --cached`), `unstage` (`{paths[]}` `reset -- <p>` OR `{hunk}`
|
|
531
532
|
reverse-apply), `discard` (`{paths[]}` restore WT from index; typed `rescue?` returned),
|
|
532
533
|
`commit` (`{message, amend?}`), `branch` (`{action:'create'|'delete', name, startPoint?,
|
|
@@ -582,7 +583,7 @@ would corrupt expanded diff context.
|
|
|
582
583
|
|
|
583
584
|
**Write model — human consent + SERVER unconditional auto-snapshot.** The write POST
|
|
584
585
|
routes are the HUMAN face of the git engine: the caller's authenticated session IS the consent
|
|
585
|
-
(`
|
|
586
|
+
(`files.write` feature + uri-policy `write`), NOT the `PendingInteraction` agent-approval
|
|
586
587
|
guard (that is the additive AGENT face, a later increment; both faces drive the same `GitRunner`
|
|
587
588
|
mutating methods). Before ANY **destructive** op (`discard`, `branch` delete) the SERVER
|
|
588
589
|
**UNCONDITIONALLY** `snapshot()`s a reflog-recoverable rescue point (a `refs/nrs-rescue/<label>`
|
|
@@ -592,7 +593,7 @@ directly). The op returns a TYPED, CONDITIONAL `rescue` carrier — `{kind:'stas
|
|
|
592
593
|
dirty-tree discard, `{kind:'branch', ref, name}` on a resolved-tip branch delete (absent when
|
|
593
594
|
there is nothing to recover) — which drives the **one-click UNDO**: `POST /git/restore
|
|
594
595
|
{rescue}` re-snapshots first (a restore is itself a mutation) then `git stash apply` /
|
|
595
|
-
`git branch <name> <ref>` (non-force → 409 on an existing name), same `
|
|
596
|
+
`git branch <name> <ref>` (non-force → 409 on an existing name), same `files.write` +
|
|
596
597
|
uri-policy `write` floor, no `PendingInteraction`. The panel's rescue banner grows an **Undo**
|
|
597
598
|
button beside Dismiss. Every successful write publishes the IN-APP `changeType:'badge'`
|
|
598
599
|
event with the actor's **REAL** `(userId, projectId, agentId)` via `publishInAppGitBadge` (the
|
|
@@ -695,7 +696,7 @@ Lifecycle exports (`src/lifecycle.ts`):
|
|
|
695
696
|
|
|
696
697
|
- `init`, `start`, `onBootstrapComplete`, `drain`, `stop`, `api`, `onCredentialChanged`, `health`, `onPrincipalRevoked`, `provisionProject` (`usecases/provisionProject.ts` — a NEW project's declared `seed:'auto'` sources, written complete through the same create path as the boot repair; see Declared sources), `deprovisionProject` (`usecases/deprovisionProject.ts` — on a PERMANENT project delete, ALL of the project's vector points leave the shared index in ONE bounded `deleteByFilter({ must: [{ field: 'projectId', equals: id }] })` — chunks + folder-ancestor points covered by the same keyword-index filter. MUST-HAVE: a failed delete throws, and so does a degraded boot where Qdrant is configured but the in-memory index is wired (a delete there would leave every real point in Qdrant); the host then stops the purge before any file is removed. It runs BEFORE the fs tree is removed, so resolving the brain infra cannot re-materialize the project dir).
|
|
697
698
|
- `init` state exposes `services["package-source-roots"]` for kernel registration; its `listPackageSourceRoots` delegate is the same function as the direct API method, and its `subscribePackageSourceRoots(listener)` calls `listener(projectId)` after every committed source-config write of the app root (`SourceConfigMutation.onChanged` — fired outside the write lock, a no-op write never fires, a throwing listener is logged and never reaches the writer), so the host re-derives that project's flagged package roots at once.
|
|
698
|
-
- `BrainCoreApi` — `getProjectServices`, `getProjectInfra`, `getFilesystem`, `upsertSourceConfig`, `listShellSourceRoots(projectId)` (every shell-capable source with its persisted permissions; feeds agent-core's shell uri-policy gate and the terminal's host-tab resolver; NOT access-filtered — consumers evaluate the policy against the caller's own `SessionContext`. Rows carry an explicit **`plane`**: `container` sources contribute their `containerRoot`, resolved by the canonical `resolveLocalRoot`; **host-plane** sources contribute a raw `hostRoot` and an intentionally EMPTY `containerRoot` — no container path exists for them, and the empty value is load-bearing, because the consumer's container-plane filter drops rootless entries so a host source can never leak into a container-plane lookup even if `plane` were ignored. The plane is derived from the connector KIND, never from the slug string — an ordinary local source may legitimately be named `host-notes`. Kinds that host no shell at all (brain vectors, webtop) contribute nothing), and `listSourceRuntimeSummaries(projectId, { userId, agentId, role?, grantedFeatures? })` (the READ sibling of `listShellSourceRoots`, feeding agent-core's `<runtime_stack>` source rendering — **all** connector kinds, **access-filtered** (host-plane rows carry `containerDir: null` — their root is a path in the OPERATOR's filesystem with no container equivalent, and the local-shaped derivation would otherwise print a container path that does not exist, baked into every agent's cached system prompt) via `canAccessScope` → `
|
|
699
|
+
- `BrainCoreApi` — `getProjectServices`, `getProjectInfra`, `getFilesystem`, `upsertSourceConfig`, `listShellSourceRoots(projectId)` (every shell-capable source with its persisted permissions; feeds agent-core's shell uri-policy gate and the terminal's host-tab resolver; NOT access-filtered — consumers evaluate the policy against the caller's own `SessionContext`. Rows carry an explicit **`plane`**: `container` sources contribute their `containerRoot`, resolved by the canonical `resolveLocalRoot`; **host-plane** sources contribute a raw `hostRoot` and an intentionally EMPTY `containerRoot` — no container path exists for them, and the empty value is load-bearing, because the consumer's container-plane filter drops rootless entries so a host source can never leak into a container-plane lookup even if `plane` were ignored. The plane is derived from the connector KIND, never from the slug string — an ordinary local source may legitimately be named `host-notes`. Kinds that host no shell at all (brain vectors, webtop) contribute nothing), and `listSourceRuntimeSummaries(projectId, { userId, agentId, role?, grantedFeatures? })` (the READ sibling of `listShellSourceRoots`, feeding agent-core's `<runtime_stack>` source rendering — **all** connector kinds, **access-filtered** (host-plane rows carry `containerDir: null` — their root is a path in the OPERATOR's filesystem with no container equivalent, and the local-shaped derivation would otherwise print a container path that does not exist, baked into every agent's cached system prompt) via `canAccessScope` → `files.readOthers`/deny-by-default, each row carrying connector, `os_uri`/`container_dir`, the caller-effective uri-policy, `description`, and the cached index snapshot; reads persisted source-config JSON ONLY — no live walk / no vector count — so it is safe to call once per stream. Agent-core renders the stable structure in the cached `<runtime_stack>` prefix and the volatile counts + on/off in `<workspace_live>`. Disabled sources are INCLUDED, marked `enabledEffective:false`). `listPackageSourceRoots(projectId)` (enumerate every project-associated source as a runtime package-source candidate `{ slug, uriRoot, containerRoot, scope, recognizesPackages, enabled, kind }`; identity-free like `listShellSourceRoots`, ALL candidates returned so the host applies the floor — project-scope + flagged + enabled + local; the host's `projectPackages` consumes it to scan + hot-reload flagged source dirs alongside `_packages/`). Filesystem events reach the `/brain-core/events` SSE route and the discovered `filesystem` channel of the host `/api/events` hub through ONE authorization adapter: it takes the real session, authorizes EACH requested agent deny-by-default (a foreign agent yields zero events, never an existence-confirming error), and filters events by user/project/types and the authorized agent or source.
|
|
699
700
|
|
|
700
701
|
**Cross-package writes go through `FilesystemService` itself, and its permission check is
|
|
701
702
|
the ONLY gate on that path.** `getProjectServices(projectId).filesystem` hands back the live
|
|
@@ -708,7 +709,7 @@ filesystem. Two consequences for anyone mirroring the request types into another
|
|
|
708
709
|
would be a cycle, so that mirror is pinned instead by a compile-time guard against brain-core's
|
|
709
710
|
shipped `.d.ts`): the mirrored type must expose neither
|
|
710
711
|
`review` nor `allowEmpty` — both are INTERNAL-only governance escapes deliberately absent from
|
|
711
|
-
every tool schema and route body — and the caller must gate on its own feature (`
|
|
712
|
+
every tool schema and route body — and the caller must gate on its own feature (`files.write`)
|
|
712
713
|
before it gets here, because `#checkPermission` answers "may this URI be written", not "may
|
|
713
714
|
this caller write at all".
|
|
714
715
|
|
|
@@ -881,8 +882,8 @@ survive in two places on purpose — stored transcripts still hold 1 800+ of the
|
|
|
881
882
|
rows, so the `filesystem-change` card's manifest `match.tools` and agent-core's
|
|
882
883
|
`REVERTIBLE_TOOLS` keep naming them so that history renders and rewinds.
|
|
883
884
|
|
|
884
|
-
Each tool declares its feature gate on itself (`x-neuralis.requires.features`): `fs_read`/`fs_list` → `
|
|
885
|
-
`fs_search` → `
|
|
885
|
+
Each tool declares its feature gate on itself (`x-neuralis.requires.features`): `fs_read`/`fs_list` → `files.read`, `fs_write` → `files.write`,
|
|
886
|
+
`fs_search` → `files.search`. This is the ONE source for the tool gate — the
|
|
886
887
|
former manifest `policies[] kind:'access'` block was removed; only the `fs_*`
|
|
887
888
|
timeout `safety` policy remains. The HTTP routes below carry the SAME features
|
|
888
889
|
independently (route-layer `requireFeature`), so both the agent-tool path and
|
|
@@ -890,14 +891,14 @@ the direct-HTTP path are gated.
|
|
|
890
891
|
|
|
891
892
|
Routes (`src/routes/*.ts`, exported `pattern` + `feature`):
|
|
892
893
|
|
|
893
|
-
- `read` (POST, `
|
|
894
|
-
- `write` (POST, `
|
|
895
|
-
- `raw` (GET, `
|
|
894
|
+
- `read` (POST, `files.read`), `list` (POST, `files.read`), `search` (POST, `files.search` — body key `search_mode`, the same vocabulary as the `fs_search` tool; an unknown value or the legacy `mode` key is a hard 400, never a silent semantic fallthrough).
|
|
895
|
+
- `write` (POST, `files.write` — the ONE write route, every `fs_write` verb key; a copy-only call is guarded as `read` on the source), `approve` (POST, `files.write`), `upload` (POST, `files.write`).
|
|
896
|
+
- `raw` (GET, `files.read`), `events` (GET SSE, `files.read` — a single-agent caller of the `subscribeFilesystemEventsImpl` authorization adapter, which re-authorizes the agentId deny-by-default), `health/*` (GET/POST, `files.read` — but the `gc` sub-path is **POST-only** and escalates in-handler to **`files.write`**, and the GET `hot-uris` sub-path (the write monitor) takes the same `files.write` tier and then filters per uri, both declared via `export const escalations`; see the Vector health section).
|
|
896
897
|
|
|
897
|
-
**The `raw` byte lane never asks a browser to RENDER what a member wrote.** Response headers come from one owner, `buildRawResponseHeaders` (`domain/mime.ts`), and the route builds none of its own. `
|
|
898
|
-
- `git/*` (`src/routes/git.ts` — the Git Control panel surface): **GET** (`
|
|
899
|
-
- `policies/uri` (GET read = `
|
|
900
|
-
- `sources/*` (CRUD + `sync` (202 + queue job snapshot) + `cancel` (per-source `{source}` or all-visible;
|
|
898
|
+
**The `raw` byte lane never asks a browser to RENDER what a member wrote.** Response headers come from one owner, `buildRawResponseHeaders` (`domain/mime.ts`), and the route builds none of its own. `files.write` is a member default on the seeded `data` source, so anything the lane renders on the app origin runs with the READER's session — an SVG carrying `<script>` was a stored-XSS primitive while the route echoed the file's own mime with `content-disposition: inline`. Three classes, and the split preserves every capability: safe previews (image except SVG, video, audio, PDF, text, JSON) stay `inline`; the ACTIVE-DOCUMENT class (SVG, HTML, XHTML, XML and every `+xml` subtype) is served with its TRUE type and `content-disposition: attachment`, so the file downloads normally and simply never becomes a live document; everything else downloads too, **keeping its real type** — the protection comes from the disposition, so a `.docx` or a `.wasm` is not flattened to `application/octet-stream` (only an absent or unparseable type is). A non-inline response always carries the header even without a filename, and filenames ride as a sanitized ASCII form plus the RFC 5987 `filename*` form so non-ASCII names survive. The store side is closed with it: `upload` no longer persists the caller-supplied part `Content-Type` verbatim (`normalizeStoredMimeType` — extension first, declared value only for unknown extensions, magic-byte check on the raster formats). Defense in depth, not a substitute for either: the host adds `Content-Security-Policy: sandbox` to every `/api/packages/*` response, which forces any navigated document on the lane into an opaque origin.
|
|
899
|
+
- `git/*` (`src/routes/git.ts` — the Git Control panel surface): **GET** (`files.read`) `status` (whole-repo change-set + branch + `upstream`/`ahead`/`behind` when tracking exists) + `branch` (local branch list) + `diff` (per-file unified diff — `?path=&staged=` WT↔Index vs Index↔HEAD, `?commit=<sha>` commit↔first-parent, or `?base=&head=` explicit sha pair; modes mutually exclusive, shas whitelisted, untracked rendered fully-added via `--no-index`, per-path uri-policy gate in every mode) + `log` (`?maxCount=&skip=&ref=` — ONE page of the commit DAG for the graph; page ≤ 1999, `skip` ≤ 100 000 (past the window ⇒ `hasMore:false`), exact `hasMore` probe replaced `truncated` here; each entry carries `subject` + the full `body` (both server-capped) for the hover card; plus `outgoing?: string[]` not-pushed shas, absent without an upstream) + `commit` (`?sha=` — the commit's file list, first-parent two-tree form, root commit = all-adds vs the empty tree; names-only ⇒ root-gated) + `outgoing` (server-resolved merge-base(`@{u}`,`HEAD`)↔`HEAD` aggregate file list + sha pair; NO caller refs; local refs only, fail-soft without an upstream) + `show` (`?path=&rev=<sha|index>` — blob content at a rev for the diff expanders; per-path uri-policy gate, blob-only + 2 MB/binary guards, typed `ok:false` refusals instead of truncation) + `log-stats` (per-commit `+/-` totals for one log page, totals only, never a path) + `repos` (the in-source repo picker — and the only route that tells a caller whether a git plane exists at all: `rel:""` is the source root, any other `rel` is a nested checkout a root-gated read will not see). **The four root-gated summary reads — `status`/`branch`/`log`/`log-stats` — fail SOFT to an empty `ok:true` on a non-repo, a containment refusal or any git error, which is byte-identical to a clean repository; `commit`/`outgoing`/`show` answer the typed `{ok:false,'unavailable'}` dialect instead.** **POST** (additionally `files.write` + uri-policy `write`) `stage`/`unstage` (whole-file OR `{hunk}`) + `discard` + `commit` + `branch` (create/delete) + `checkout` — HUMAN consent (the session, NOT the agent-approval guard), SERVER unconditional auto-snapshot before a destructive op (returns `rescue`), and an in-app badge with the actor's REAL identity on success. All per-source (`?source=` GET / body `source` POST), deny-by-default (non-accessible → 404), repo-relative paths only + host-path-scrubbed errors, size-capped (diff 2 MB — the human-panel memory bound; the 20 KB `boundToolResult` floor stays the model-facing invariant for the planned hosted git tools).
|
|
900
|
+
- `policies/uri` (GET read = `files.read`; POST patch escalates in-handler to **`sources.policy`** — a feature replacing the former owner/admin role-name check; not granted below the admin tier — owner via `'*'`, admin via its enumerated default grants — and grantable per-role; declared via `export const escalations`). The brain-local `hasRouteFeature` is now a thin wrapper over the shared `hasFeature` (one-predicate invariant). In-handler escalation features (`sources.mount`, `sources.privileged`, `sources.sync`, `files.readOthers`/`files.writeOthers`, `sources.policy`) gate path/scope-dependent operations on top of the route's declared baseline — the same two-tier pattern machine-core uses; they are LIVE, not dead. **`sources.privileged`** (not granted below the admin tier — owner via `'*'`, admin via its enumerated default grants — and grantable per-role) gates SEEing in `discoverable`, ADDing (create), EDITING (PATCH — permission widening on an already-attached protected-rooted source), and — since warded-wren — BROWSING (`sources/browse`) a source rooted in a high-blast-radius zone (the app zone, the Neuralis tree, or the whole host `/`); the add/edit arm is kind-agnostic on the container plane (any non-host-plane kind whose config carries a string `root`); enforced by canonical path containment in `sources.ts` (browse matches IS-or-UNDER only and omits protected children from the listing, and it requires the feature for the filesystem ROOT and for ANY path while the runtime stack is unavailable — an empty protected-root table must never read as *unrestricted*) — see `src/routes/README.md`. A source on the **host plane** — any connector kind that resolves OUTSIDE the container (`host`, `host-*`), i.e. a paired device — is attached or edited only by **that device's owner holding `device.own`**, answered by agent-core's device gate; no role feature opens it for anyone else, at any root, and the check runs BEFORE the container-plane protected-root logic with no runtime-stack precondition (fail closed). **Editing** one needs the same: `PATCH` cannot change `connection`, but it CAN widen `read` → `write`, so a plain-`sources.mount` manager must not be able to escalate an existing host source in place. Beyond that, the device's own allowlist (`neuralis device grant`) bounds what the owner's source can actually reach (see the agent-core README's host-access section). **`packages.author`** gates the package-authoring *affordances* — the Packages-panel scaffold launcher and the install/build actions — granted to `admin` and `manager` via `defaultRoleGrants` (owner passes on the wildcard). It does NOT gate the authoring SKILL: `skills/create-package` is ungated on purpose, so a caller who cannot yet write or install can still read the contract and learn exactly which capability they are missing instead of failing blind. The downstream gates are unchanged — scaffolding into `packages://` still needs `files.write`, and install/build still need `packages.author`.
|
|
901
|
+
- `sources/*` (CRUD + `sync` (202 + queue job snapshot) + `cancel` (per-source `{source}` or all-visible; sources.sync) + `reindex` (202) + `reset` + `sync-status` (GET job snapshots, sources.sync) + `browse` (**`sources.mount`**; the filesystem root and any protected host zone additionally need `sources.privileged`) + `preflight` (POST-only create validation/authorization, no write) + `kinds` + `kinds/:kind/schema` + `enabled` (light toggle, `'auto'` machine-only) + `discoverable` (**`sources.mount`** — well-known unattached source roots from the runtime stack, e.g. the "Neuralis Tree" `neuralis://` mount when `pnpm neuralis:mount add … --neuralis` is configured), mixed `drive.*`). A suggestion may pre-fill **`suggestedSync`** (`include` + `trigger`, both riding the contract — the add form no longer hard-codes `'manual'`) and **`suggestedPolicies`** (uri-policy path rules, `$source`-tokenised so they survive a slug rename). The two are not interchangeable: `include` narrows what is **indexed**, `suggestedPolicies` narrows what can be **read**. The `--neuralis` overlay therefore offers TWO proposals, narrowest first — `neuralis-skills://` rooted at `<root>/skills` (root = boundary, `existsSync`-guarded so a `skills`-less overlay advertises nothing) and `neuralis://` over the whole folder, which pre-fills `include: ['skills/**']` plus a `.env*` read-deny because that root holds `.env`. `POST sources/sync` classifies its two pre-flight refusals rather than redacting them: **404** (plain shape) when the slug does not resolve for the caller, **400** when it resolves but carries no `sync` block. **PATCH replaces `permissions` wholesale — it never merges.** A body carrying only `permissions.default` answers 200 and drops every `paths[]` rule the source held; only `floor:true` rules are protected (409), so a 200 is not evidence the rest survived. Every caller — skill, UI, test — must round-trip the source's current `permissions` object. **A connector-declared floor self-heals on ANY scheme; the manifest-baseline union self-heals on `data`/`packages` only; nothing else self-heals at all.** The two halves run on different gates. `reconcileImmutablePolicyFloors` runs on EVERY boot, past the version stamp, for every source whose connector declares `floor:true` rows — so a machine/webtop source restores its floors too, and a floor row added by a later package version reaches EXISTING configs without a version bump. The version-gated half is the baseline UNION, and a PATCH no longer re-arms it: `updateSourceConfig` rebuilds the record from an explicit FIELD ALLOWLIST (every sibling writer spreads `existing`), and `baselineRepairVersion` was missing from that list, so any PATCH — a no-op included — silently un-stamped the config and the next boot's `repairSourceBaselines` re-unioned the manifest baselines. Since `mergeBaselines`'s `paths` semantics are UNION, that RESURRECTED a non-floor rule a `removeRule` had deliberately taken out. The allowlist now preserves the stamp. On every other scheme there is no baseline union to re-run. Hand-added rules never come back on any scheme.
|
|
901
902
|
|
|
902
903
|
**A non-project-scoped source never keeps the `default` you send — and the two non-project kinds seed DIFFERENTLY.** `applyScopeSeedPermissions` forces `default: {read:false,write:false,exec:false}` and `byRole: {owner:{read:true}, admin:{read:true}}` for BOTH `user` and `agent` scope, but the principal grant branches on the kind:
|
|
903
904
|
|
|
@@ -964,9 +965,9 @@ embed call, because no embed call uses an LLM key.
|
|
|
964
965
|
|
|
965
966
|
The per-category contribution folders are the agent-facing knowledge surface for brain-core:
|
|
966
967
|
|
|
967
|
-
- `rules/brain-rules.md` and `
|
|
968
|
+
- `rules/brain-rules.md` (always on) and `instructions/brain-model.md` (on demand) explain URI identity, connector-first read/list behavior, search freshness, the **fs write policy** (modify-don't-recreate, read-before-modify, `revert` as 1-level undo — moved here from agent-core's `tool-use` so the filesystem owner owns writes), source scope and the independent gates; each `fs_*` tool description carries its own mechanics. brain-core ships no `docs/` and no MCP `resources[]`.
|
|
968
969
|
- brain-core ships **nine** skill bundles under `skills/`. `skills/brain-start`, `sync`, and `organize` are operational workflows for using the filesystem safely; `manage-files` (bulk/route-level file ops when the `fs_*` tools are insufficient or an approval flow is involved), `manage-sources` (source survey + index-health diagnostics alongside its CRUD/sync lifecycle), `manage-uri-policy` (inspect/patch per-source read/write/exec path policy) and `git` (drive a git-backed source through the governed `/git/*` routes) each wrap a slice of the HTTP surface — see the LLM-callable skills table below.
|
|
969
|
-
- `skills/connect-business-data` is the governance-first orchestration layer for organisation-wide and single-user external data. It establishes ownership, approved location, audience/scope, sensitivity/retention, credential scope, copy permission and provenance before routing to a configured source, connector or reusable package. Corpus content remains source-backed; identity and memory never become a shadow dataset, while skills/tools/workflows operate on governed source URIs. Gated `
|
|
970
|
+
- `skills/connect-business-data` is the governance-first orchestration layer for organisation-wide and single-user external data. It establishes ownership, approved location, audience/scope, sensitivity/retention, credential scope, copy permission and provenance before routing to a configured source, connector or reusable package. Corpus content remains source-backed; identity and memory never become a shadow dataset, while skills/tools/workflows operate on governed source URIs. Gated `files.read` (authoring still needs its own write/install capability).
|
|
970
971
|
- `skills/create-package` is the SINGLE package-authoring skill — a router `SKILL.md` over `references/` (context files, manifest + discovery, tool schemas, runtime + build, app surfaces, connectors + uriPolicies, config + credentials, team + workflows, test + ship) plus two `scripts/` (a static pre-flight and a runtime load check). ONE dispatch surface, so an agent that finds the skill at all reaches the right chapter. It is **ungated** — every caller can read the contract and learn which capability they are missing; the write / install / build capabilities still gate the actual work.
|
|
971
972
|
- brain-core ships **no** `agents/`. Its four package-authoring subagents (`planner-agent`, `reviewer-agent`, `scaffolder-agent`, `brain-search-agent`) were retired: they duplicated the platform operating floor they were already injected with, and their `allowed-tools` allowlists hid every non-`fs_*` tool from them — no `execute`, no web, no MCP, no package tool. Three of the four were additionally gated on `packages.author`, which member and viewer do not hold. The universal roles now live in agent-core (`agents/*.md`: explore, research, planner, execute, validate, operate, general) and the package contract these encoded is in `skills/create-package`, which any of those roles can activate.
|
|
972
973
|
|
|
@@ -988,9 +989,9 @@ There is intentionally no README inside the contribution folders; this section i
|
|
|
988
989
|
## Security & scope
|
|
989
990
|
|
|
990
991
|
- Every route helper (`src/routes/helpers.ts`) extracts `(userId, projectId, agentId, role, grantedFeatures, agentAccess, agentOwnership)` from the canonical `req.session` (`SessionContext`) and rejects sentinel identities. `projectId`/`userId` come straight from the session; `agentId` resolves `body ?? query ?? req.session.agentId`, so a session-ticket caller (skill-script curl) inherits the stream's agent without passing `AGENT_ID` — `ensureAccess` only 400s when no agent is present anywhere. A body/query `projectId` that disagrees with the session is rejected (403). `getServices(state, projectId)` resolves project-scoped infra; sentinel `__system__` ids never reach a real Qdrant collection.
|
|
991
|
-
- Source scope evaluation uses `canAccessScope` from `@neuralis/package-system/contracts`. Project-scoped sources are visible to every project member; user-scoped sources require ownership or the `
|
|
992
|
+
- Source scope evaluation uses `canAccessScope` from `@neuralis/package-system/contracts`. Project-scoped sources are visible to every project member; user-scoped sources require ownership or the `files.readOthers` / `files.writeOthers` feature. The grant alone decides — `canAccessScope` carries no role-name conjunct, so a custom role explicitly granted the feature observes exactly what a wildcard holder does. Default grants: `files.readOthers` AND `files.writeOthers` are both enumerated for `admin`; `owner` reaches them through the `'*'` wildcard; `manager`/`member`/`viewer` hold neither and reach a foreign scope only through an explicit grant. Since `canAccessScope` lost its role-name conjunct, these grants are what carry the decision — they are not inert. `allowedScopes` declared on a connector kind rejects mismatched scope at create time. Every route-level source-config read (`BridgeService.getSourceConfig` / `listSourceConfigs`) MUST pass the caller's access context (`toScopeAccessCtx(ctx)` from `routes/helpers.ts`) — omitting it downgrades the caller to a feature-less `member` and hides user/agent-scoped sources from their own owner (the "Source not found on edit" regression). The `FilesystemService` accessible-sources cache is keyed on the full access context (user, project, agent, role, grantedFeatures), never on `userId:projectId` alone — a narrower key lets one agent's set poison another agent's view for the TTL window.
|
|
992
993
|
- A delete asks through the GUARD, not through a hand-written gate: `fs_write` declares `x-neuralis.askOnKeys: ["delete"]`, so the presence of that root key makes `balanced` open the approval while an edit stays quiet, and `auto` runs neither. Approval replays the original `toolUseId` and runs the mutation; denial leaves connector and index state untouched. The old tool-side consent (`maybeRequestDeleteApproval`) is gone — a second approval gate outside `evaluateGuard` is exactly what `auto` could not short-circuit.
|
|
993
|
-
- **Pending-resolution security floor (2026-07-17).** Every pending-change verb (list / approve / revert / discard) resolves its vector target with an exact tenant-scoped `(projectId, id)` lookup — never a collection-global `retrieve(id)` — so a same-id point in another project is invisible: the previously tenant-blind lookups are hardened and the bounded uri-echo disclosure (a foreign tenant's point donating its uri to a discard/revert response) is closed. Chunk deletes/relocations are project-scoped the same way. The canonical check order on every mutating verb: session identity → `
|
|
994
|
+
- **Pending-resolution security floor (2026-07-17).** Every pending-change verb (list / approve / revert / discard) resolves its vector target with an exact tenant-scoped `(projectId, id)` lookup — never a collection-global `retrieve(id)` — so a same-id point in another project is invisible: the previously tenant-blind lookups are hardened and the bounded uri-echo disclosure (a foreign tenant's point donating its uri to a discard/revert response) is closed. Chunk deletes/relocations are project-scoped the same way. The canonical check order on every mutating verb: session identity → `files.write` (in-handler, before any lookup) → projectId tenant boundary → per-record owner (`canAccessScope` + `recordOwnerScope`) → source scope + uri-policy on the RESOLVED uri(s) (including a move destination and a move-revert's baseline uri; id-only modifies are gated too) → only then side effects. Cross-user resolution widens ONLY for a `files.writeOthers` HOLDER — the grant decides, not the role name (`canAccessScope` carries no role-name conjunct) — and preserves the record author's provenance; approving a delete tombstone deletes the point + chunks instead of resurrecting an active ghost. The owner floor also precedes the idempotent already-terminal early-return on discard/dismiss — a foreign caller probing a terminal record id gets `FORBIDDEN` (403) with no uri echo; terminal state is no oracle. The route maps every single-record resolution verb (revert / discard / dismiss) through ONE status mapping: `FORBIDDEN` → 403, `NOT_FOUND` → 404, retryable → 503, else 409.
|
|
994
995
|
- `LocalDiskConnector` blocks symlink traversal, `..` escape, and explicit access to the platform app zone (`~/.neuralis/app`) or credential files.
|
|
995
996
|
- The brain source is per-agent inside the same project — `searchFiles` isolates brain rows by `agentId` unless `include_siblings` is set: an explicit `brain://` scope gets the vector pre-filter, an UNSCOPED (no `folder_uri`) search drops foreign brain rows per-node via `isVisibleSearchNode` while non-brain rows stay project-shared (the old global pre-filter starved cross-source search); cross-agent observation requires the corresponding feature.
|
|
996
997
|
|
|
@@ -1026,11 +1027,11 @@ fixes a wrong guess. Before this it fell into the catch-all 500, where the
|
|
|
1026
1027
|
dispatcher redacts the message away.
|
|
1027
1028
|
|
|
1028
1029
|
A source the caller cannot SEE — another user's or agent's, without
|
|
1029
|
-
`
|
|
1030
|
+
`files.readOthers` — answers exactly like an absent one, on every plane:
|
|
1030
1031
|
the same 404 body (its slug never in `availableSources`; a disabled hidden source
|
|
1031
1032
|
never answers `source_disabled`, which named its owner), the same 403 at the uri
|
|
1032
1033
|
guard. (Creating a source under a taken slug is still refused — the per-project
|
|
1033
|
-
slug namespace needs that, and only a `
|
|
1034
|
+
slug namespace needs that, and only a `sources.mount` holder reaches it.) A source the caller CAN see keeps its real
|
|
1034
1035
|
answers: `403 uri_policy_denied` for a denied path, `409 source_disabled` when it
|
|
1035
1036
|
is off — never the 404 and its suggestion list.
|
|
1036
1037
|
|
|
@@ -1122,7 +1123,7 @@ Review uses a fresh `POST /pending` `{action:"preview",id}` receipt, separating
|
|
|
1122
1123
|
|
|
1123
1124
|
### Source delete + vector purge
|
|
1124
1125
|
|
|
1125
|
-
`POST /sources/purge` cleans cached index data without deleting the source or connector files. Body: `{source,selector:{kind:"source"}|{kind:"file"|"folder",uri},mode:"estimate"|"apply",obsoleteOnly?:boolean}`. Canonical selectors stay within the source; both
|
|
1126
|
+
`POST /sources/purge` cleans cached index data without deleting the source or connector files. Body: `{source,selector:{kind:"source"}|{kind:"file"|"folder",uri},mode:"estimate"|"apply",obsoleteOnly?:boolean}`. Canonical selectors stay within the source; both sources.mount and sources.sync plus source-management and per-path READ gates apply. `obsoleteOnly:true` removes only current pattern/media exclusions; false/omitted explicitly clears selected eligible cached data too, while retaining recovery payloads. Estimate returns counts without provider work; confirm before apply. Unknown records remain unresolved. The projected inventory reuses caller READ configuration until its revision changes; selected points recheck fresh READ and source-management authority before hydration or mutation.
|
|
1126
1127
|
|
|
1127
1128
|
`DELETE /sources/:slug` always removes the source-config JSON and the
|
|
1128
1129
|
in-memory connector binding. **The host folder is never touched** — the
|
|
@@ -1172,16 +1173,16 @@ collection walk:
|
|
|
1172
1173
|
that walks every point: orphan-chunk + duplicate-document detection.
|
|
1173
1174
|
|
|
1174
1175
|
The GC actions — **`POST /health/gc` only, and gated one tier above the rest of
|
|
1175
|
-
the route.** The module feature is `
|
|
1176
|
+
the route.** The module feature is `files.read`, which every role holds down to
|
|
1176
1177
|
`viewer`, and that is right for the two read arms above; `gc` and
|
|
1177
1178
|
`repair-orphans` are mutations whose `dryRun` defaults to **false**, so the
|
|
1178
|
-
sub-path escalates in-handler to **`
|
|
1179
|
-
it `403 Missing feature:
|
|
1179
|
+
sub-path escalates in-handler to **`files.write`** and answers a caller without
|
|
1180
|
+
it `403 Missing feature: files.write (vector health repair)`. `deduplicate` is
|
|
1180
1181
|
read-only but rides the same gate — its rows enumerate other agents' `data://`
|
|
1181
1182
|
paths. `GET /health/gc` answers **405**, so a link or a prefetch can never start
|
|
1182
1183
|
a deletion; it declares `Allow: POST` (the host catch-all forwards route headers
|
|
1183
1184
|
only on content-type-bearing responses, so it is not on the wire yet). The arm is
|
|
1184
|
-
fenced, not removed: a `
|
|
1185
|
+
fenced, not removed: a `files.write` holder (manager tier and up) runs every
|
|
1185
1186
|
action exactly as before, and the Vector Health panel already posts.
|
|
1186
1187
|
`action:"repair-pending"` (dry-run supported in the panel) works on the CALLER's own
|
|
1187
1188
|
visible pending records, never the project scope: it counts orphaned and
|
|
@@ -1247,7 +1248,7 @@ and `points` (everything written, chunks included). It is bounded to
|
|
|
1247
1248
|
`vectorWriteMonitorMaxUris` uris per project (least recently written dropped
|
|
1248
1249
|
first). A uri reaching `vectorWriteHotUriThreshold` writes in the hour logs ONE
|
|
1249
1250
|
server-side `vector.write.hot-uri` warn per hour. `GET /health/hot-uris` lists the
|
|
1250
|
-
top `vectorWriteHotUriTopN` for the caller: it takes the same `
|
|
1251
|
+
top `vectorWriteHotUriTopN` for the caller: it takes the same `files.write` tier as
|
|
1251
1252
|
`gc` (the list names files across the project's users and agents) and is then
|
|
1252
1253
|
filtered per uri exactly like a file listing — the caller's source scope, the
|
|
1253
1254
|
uri's READ policy, and agent isolation for `brain://` — before it is ranked and
|
|
@@ -1511,7 +1512,7 @@ The package declares the optional singleton `package-source-roots` service, dele
|
|
|
1511
1512
|
- `commitWrite` (`usecases/write/commit.ts`) orders the slow phases so a write pays the least it can, and **every step of that order is an invariant** — the full rule, with its degrade matrix, lives in `src/usecases/README.md` and is stated there alone.
|
|
1512
1513
|
- `OllamaEmbedder` now sends `keep_alive: '10m'` on every `/api/embed` request (configurable via `OllamaEmbedderConfig.keepAlive`; pass `0` to defer to the upstream default). Mirrors the LLM-stream keep-alive contract — keeps the embedding model resident on the GPU between fs_* calls and avoids the 1–2 s cold-start when the model gets paged out.
|
|
1513
1514
|
- Host integration: the runtime UI module `app/host.tsx` (`neuralis.app.module`, built by `pnpm build:ui`) exports `installFilesystemHostComponents`, which registers the Files widget, the `filesystem.change` card and the `workspace.provider:@neuralis/brain-core` fill (`<FilesystemPackageHost port>` around the workspace tree). That install entry also mounts `<WorkspaceHostPortProvider port={port}>` once around the widget so the additive `SkillLauncher` launchers throughout the filesystem UI resolve the port from context — without it they render `null` silently, which the host's `hostPortProviderMount.test.ts` now fails on.
|
|
1514
|
-
- Filesystem-UI `SkillLauncher` launchers (additive composer-prefill affordances; visibility DERIVED from the host's per-caller skill catalog, never a hand-typed feature list): `Toolbar` → `sync` + `manage-files`; `SourcesPanel` header → `manage-sources`, per-source Permissions header → `manage-uri-policy` (URI / path-permission edit; `
|
|
1515
|
+
- Filesystem-UI `SkillLauncher` launchers (additive composer-prefill affordances; visibility DERIVED from the host's per-caller skill catalog, never a hand-typed feature list): `Toolbar` → `sync` + `manage-files`; `SourcesPanel` header → `manage-sources`, per-source Permissions header → `manage-uri-policy` (URI / path-permission edit; `sources.policy`); `FileTree` empty-state → `manage-files`; `GitControlPanel` header → `git`; `PendingChangesPanel` → `manage-files`; `VectorHealthPanel` → `manage-sources`; `PackagesPanel` header → `create-package` (the one launcher that narrows further with `alsoRequires={["packages.author"]}` even though the skill is ungated — the button scaffolds into `packages://`, which the default `member` role cannot write). Full per-placement coverage note lives in `app/filesystem/ui/README.md`.
|
|
1515
1516
|
|
|
1516
1517
|
## LLM-callable skills
|
|
1517
1518
|
|
|
@@ -1522,9 +1523,9 @@ the `execute action="skill"` + `execute action="shell"` pipeline:
|
|
|
1522
1523
|
|
|
1523
1524
|
| Bundle | Surface | Scripts |
|
|
1524
1525
|
|---|---|---|
|
|
1525
|
-
| `manage-sources` | `/sources/*` CRUD + sync / sync-status / cancel / reset / reindex / enabled lifecycle, scoped index purge, kind catalog, browse + discoverable roots (both `
|
|
1526
|
-
| `manage-files` | `/list`, `/search`, `/read`, `/raw`, `/create`, `/modify`, `/mkdir`, `/copy`, `/upload`, `/approve`, `/pending` (bulk; `fs_*` tools remain primary for one-offs). `/create` and `/upload` both use the write engine and `
|
|
1527
|
-
| `manage-uri-policy` | `/policies/uri/*` — read manifests baselines, read effective merged policy, patch path rules. Reads are the route's `
|
|
1526
|
+
| `manage-sources` | `/sources/*` CRUD + sync / sync-status / cancel / reset / reindex / enabled lifecycle, scoped index purge, kind catalog, browse + discoverable roots (both `sources.mount`, not the skill's `files.read` floor) | 17 |
|
|
1527
|
+
| `manage-files` | `/list`, `/search`, `/read`, `/raw`, `/create`, `/modify`, `/mkdir`, `/copy`, `/upload`, `/approve`, `/pending` (bulk; `fs_*` tools remain primary for one-offs). `/create` and `/upload` both use the write engine and `files.write`; agent-origin uploads are journaled as pending review, while interactive human uploads land committed | 11 |
|
|
1528
|
+
| `manage-uri-policy` | `/policies/uri/*` — read manifests baselines, read effective merged policy, patch path rules. Reads are the route's `files.read` module feature (every built-in role holds it); the patch escalates in-handler to `sources.policy` — a feature check, not a role-name check. Immutable `floor:true` rules (trusted-manifest-only) can't be created or removed here — the route answers `409 immutable_policy_floor`. | 3 |
|
|
1528
1529
|
| `git` | `/git/*` — the same routes the Git Control panel drives. Reads: status / branch / diff / log / log-stats / commit / outgoing / show / repos. Writes: stage / unstage / discard, commit, branch / checkout, `restore`, and fetch / pull / push (uri-policy `exec` + connected remote). its routes taught as `neuralis request <METHOD> @neuralis/brain-core/git/<route>` lines; also documents the raw-shell and external-GitHub-MCP git planes (a routes-based skill, NOT a `git_*` MCP bundle). **The helper takes its HTTP VERB as a positional argument, so the mutating half of the skill is invisible to anything that classifies an example by the SCRIPT FILE** — 10 of the 19 routes are POST writes, and the skill's write examples are marked `NOT DISPOSABLE` (git routes only act on an already-attached source; there is no throwaway repo) with point-of-use consequences for commit / checkout / pull / push | 1 |
|
|
1529
1530
|
|
|
1530
1531
|
`manage-uri-policy` migrated here from `@neuralis/agent-core` on
|
|
@@ -1538,7 +1539,7 @@ env vars and examples.
|
|
|
1538
1539
|
`workflows/sources-digest.json` — a working-day digest of what changed across
|
|
1539
1540
|
the synced sources (fs_search/fs_list since the last run, hygiene flags for
|
|
1540
1541
|
stale/contradicting docs, per-source summary into the workflow's state
|
|
1541
|
-
`reports/`). Feature-gated by `
|
|
1542
|
+
`reports/`). Feature-gated by `files.search` (hidden from default viewers — a
|
|
1542
1543
|
deliberate gate-visibility decision: the runs need search). Cron default
|
|
1543
1544
|
`0 8 * * 1-5` hardcoded UTC (template crons never depend on install-time
|
|
1544
1545
|
locale); draft-first install like every template. `workflows` is in the
|
package/app/README.md
CHANGED
|
@@ -23,9 +23,9 @@ React surface contributed by `@neuralis/brain-core`: the Files widget, the `file
|
|
|
23
23
|
|
|
24
24
|
## Security & scope
|
|
25
25
|
|
|
26
|
-
- Every API call flows through `PackageApiProvider`, which the host scopes by `(projectId, agentId, userId)` from the workspace session. The brain-core route layer enforces feature grants — `
|
|
27
|
-
- File marks and SSE event subscriptions are per-agent. Cross-agent visibility in the tree requires the same feature grants the route layer enforces (`
|
|
28
|
-
- The `filesystem.change` card reads tool input from agent-core's chat store; it never re-issues writes. Approval flows go through the `approve` route, gated by `
|
|
26
|
+
- Every API call flows through `PackageApiProvider`, which the host scopes by `(projectId, agentId, userId)` from the workspace session. The brain-core route layer enforces feature grants — `files.read`, `files.write`, `files.search`, `sources.sync`, `sources.mount` (plus the in-handler escalation `sources.privileged` for privileged roots — a source on a paired device is attached only by its owner under `device.own` — and `sources.policy` for the URI-policy patch), together with `files.readOthers` / `files.writeOthers` for cross-scope access — and source-scope access. The UI is not authoritative.
|
|
27
|
+
- File marks and SSE event subscriptions are per-agent. Cross-agent visibility in the tree requires the same feature grants the route layer enforces (`files.readOthers`).
|
|
28
|
+
- The `filesystem.change` card reads tool input from agent-core's chat store; it never re-issues writes. Approval flows go through the `approve` route, gated by `files.write`.
|
|
29
29
|
|
|
30
30
|
## Gotchas
|
|
31
31
|
|
|
@@ -31,6 +31,13 @@ export interface FilesystemHostValue {
|
|
|
31
31
|
onEvent: (event: unknown) => void,
|
|
32
32
|
onConnChange?: () => void,
|
|
33
33
|
): () => void;
|
|
34
|
+
/**
|
|
35
|
+
* Subscribe to paired-device state TRANSITIONS (`devices` channel of the same
|
|
36
|
+
* hub): the frame is `{deviceId, status}` only — the subscriber re-reads its
|
|
37
|
+
* own lists (source summaries, a device `select`). The hub decides who hears
|
|
38
|
+
* which device. Optional: a host without it gives no live device updates.
|
|
39
|
+
*/
|
|
40
|
+
subscribeDeviceEvents?(projectId: string, onEvent: (event: unknown) => void): () => void;
|
|
34
41
|
isDocker?: boolean;
|
|
35
42
|
/**
|
|
36
43
|
* Optional: the signed-in user's identity, used by the Sources "Add Source"
|
package/app/filesystem/README.md
CHANGED
|
@@ -34,7 +34,7 @@ Files widget package — the React surface mounted by the host as the brain-core
|
|
|
34
34
|
|
|
35
35
|
## Pending Changes tab
|
|
36
36
|
|
|
37
|
-
`ui/PendingChangesPanel.tsx` is the **Pending Changes** view in the Files widget — a journal of revertible filesystem changes that an agent has made (the durable backend is `src/store/PendingChangeStore.ts`; the route is `pending`, see `src/routes/README.md`). It offers two genuinely distinct views: a **compact list** (filename + full folder path, no content fetch) and **diff cards** (the default — opened inline, each card loads its hunks lazily from ONE fresh `preview` (baseline + current state, the record's own fingerprint) through `hooks/useVisibleDiffCards.ts`; on a source-revision change or a virtualizer remount the card keeps its last preview on screen while the fresh one loads, and the index badge shows only for the current key's answer; `+N/−M` counts are computed from the loaded hunks when the record carries none). Records are ordered **oldest-first** so the review walks first→last. A focused-card cursor (◄/► header buttons, plus `↑`/`↓`/`←`/`→`, `a`=approve, `r`=revert, `x`=dismiss when the panel is focused) an **Approve N** (two-step confirm; only records that need no acknowledgement) button and an **M need confirmation** button make a full sweep fast — the latter opens `PendingReviewDialog`, grouped by reason, 100 records per batch; a finished batch disables its group buttons and offers **Next batch (K left)**; approving advances to the next change. Each live (non-orphaned) row also carries a neutral-tone **Dismiss** (x) button — "accept-and-stop-reviewing": the change is kept exactly as it is (a present file stays on disk + indexed, an agent's delete stays deleted) and only the review entry resolves, with no connector write (the `pending` route's `dismiss` action; see `src/routes/README.md`). Clicking a filename opens the file in the tree (never replacing the panel). A filter bar narrows by source / agent / conversation, and the conversation chip is **clearable** (the `×` resets `pendingPanelFilter`). The panel is registered as `WorkspacePanelId += 'pending'` in the store, a Toolbar button, and a `FileEditor` tab + render branch — `FileEditor`'s `showFilePane` **excludes** the pending tab so the file header / amber pending banner / file-content layer never overlay the panel (they share one flex container). The tab is gated by `
|
|
37
|
+
`ui/PendingChangesPanel.tsx` is the **Pending Changes** view in the Files widget — a journal of revertible filesystem changes that an agent has made (the durable backend is `src/store/PendingChangeStore.ts`; the route is `pending`, see `src/routes/README.md`). It offers two genuinely distinct views: a **compact list** (filename + full folder path, no content fetch) and **diff cards** (the default — opened inline, each card loads its hunks lazily from ONE fresh `preview` (baseline + current state, the record's own fingerprint) through `hooks/useVisibleDiffCards.ts`; on a source-revision change or a virtualizer remount the card keeps its last preview on screen while the fresh one loads, and the index badge shows only for the current key's answer; `+N/−M` counts are computed from the loaded hunks when the record carries none). Records are ordered **oldest-first** so the review walks first→last. A focused-card cursor (◄/► header buttons, plus `↑`/`↓`/`←`/`→`, `a`=approve, `r`=revert, `x`=dismiss when the panel is focused) an **Approve N** (two-step confirm; only records that need no acknowledgement) button and an **M need confirmation** button make a full sweep fast — the latter opens `PendingReviewDialog`, grouped by reason, 100 records per batch; a finished batch disables its group buttons and offers **Next batch (K left)**; approving advances to the next change. Each live (non-orphaned) row also carries a neutral-tone **Dismiss** (x) button — "accept-and-stop-reviewing": the change is kept exactly as it is (a present file stays on disk + indexed, an agent's delete stays deleted) and only the review entry resolves, with no connector write (the `pending` route's `dismiss` action; see `src/routes/README.md`). Clicking a filename opens the file in the tree (never replacing the panel). A filter bar narrows by source / agent / conversation, and the conversation chip is **clearable** (the `×` resets `pendingPanelFilter`). The panel is registered as `WorkspacePanelId += 'pending'` in the store, a Toolbar button, and a `FileEditor` tab + render branch — `FileEditor`'s `showFilePane` **excludes** the pending tab so the file header / amber pending banner / file-content layer never overlay the panel (they share one flex container). The tab is gated by `files.read` (visible to every drive user), while the *content* is filtered per record server-side (owner isolation + uri-policy on the `pending` route). The chat composer's "Open in Files" button (agent-core Changes&Actions) opens this tab scoped to a conversation; this is navigation only and is distinct from the chat approve/deny `PendingInteraction` card. A notification row opens the widget through the same one-shot `applyNav`: `open-pending` (the pending summary), `open-sources` (a source's sync/embedding outcome) and `open-vector-health` (an operator's collection action) — navigation only, every gate re-applied by the panel's own reads.
|
|
38
38
|
|
|
39
39
|
## Open-to-file tree hydration
|
|
40
40
|
|
|
@@ -552,7 +552,7 @@ export async function getGitShow(params: {
|
|
|
552
552
|
|
|
553
553
|
// ---------------------------------------------------------------------------
|
|
554
554
|
// Git Control WRITE fetchers (G2.3 / spellbound-heron) — the HUMAN face of the
|
|
555
|
-
// git engine. All POST; the server additionally gates `
|
|
555
|
+
// git engine. All POST; the server additionally gates `files.write` + a
|
|
556
556
|
// uri-policy `write` capability (uniform 403 on missing feature, 404 on scope,
|
|
557
557
|
// 403 `uri_policy_denied` on path deny), unconditionally auto-snapshots before a
|
|
558
558
|
// destructive op (Q6), and publishes the in-app badge with the actor's REAL
|
|
@@ -732,7 +732,7 @@ export async function deleteGitBranch(params: {
|
|
|
732
732
|
* One-click UNDO of a destructive op (G2.5) — POST the typed `rescue` a prior
|
|
733
733
|
* `discardPaths` / `deleteGitBranch` returned. The server re-snapshots first
|
|
734
734
|
* (a restore is itself a mutation), then `git stash apply` (discard) or
|
|
735
|
-
* `git branch <name> <ref>` (branch). `
|
|
735
|
+
* `git branch <name> <ref>` (branch). `files.write` + uri-policy `write`.
|
|
736
736
|
*/
|
|
737
737
|
export async function restoreGit(params: {
|
|
738
738
|
agentId: string;
|
|
@@ -771,7 +771,7 @@ export async function checkoutGitBranch(params: {
|
|
|
771
771
|
}
|
|
772
772
|
|
|
773
773
|
/**
|
|
774
|
-
* Remote git ops (G4 keyring-kestrel Slice 2) — `
|
|
774
|
+
* Remote git ops (G4 keyring-kestrel Slice 2) — `files.write` + uri-policy
|
|
775
775
|
* `exec`. The server derives the remote host, reads the caller's OWN connected
|
|
776
776
|
* binding, resolves their OWN push token, and runs the op with a host-matched
|
|
777
777
|
* credential helper (HTTPS-only, no force-push). `fetch`/`pull` echo the fresh
|
|
@@ -1259,7 +1259,7 @@ export async function getSourcesHealth(params: {
|
|
|
1259
1259
|
* `agentId` is REQUIRED (warded-wren): the route runs `ensureAccess` like every
|
|
1260
1260
|
* sibling handler, and that refuses a call with no agent in context. Every other
|
|
1261
1261
|
* source call on this client already sends it; browse was the one that did not.
|
|
1262
|
-
* The route gate itself is `
|
|
1262
|
+
* The route gate itself is `sources.mount` — `sources.privileged` for the
|
|
1263
1263
|
* filesystem root and the protected host zones — so a caller who cannot attach
|
|
1264
1264
|
* a source gets a 403 here too.
|
|
1265
1265
|
*/
|
|
@@ -24,7 +24,7 @@ React hooks used by the Files widget: viewport diff loading and the filesystem S
|
|
|
24
24
|
|
|
25
25
|
## How it plugs into the package-system kernel
|
|
26
26
|
|
|
27
|
-
- `useFilesystemSSE` subscribes through `WorkspaceHostPort.subscribeRealtime({ channel: 'filesystem', agentId, projectId, onEvent })` (via `getFilesystemHostImperative().subscribeFilesystemEvents`) — the shared, multiplexed `/api/events` hub owned by the host, NOT a per-widget `EventSource` (the SSE-consolidation invariant: never open a per-widget connection — see `docs/architect/realtime-transport.md`). The hub owns reconnect/backoff; the host route is gated by `
|
|
27
|
+
- `useFilesystemSSE` subscribes through `WorkspaceHostPort.subscribeRealtime({ channel: 'filesystem', agentId, projectId, onEvent })` (via `getFilesystemHostImperative().subscribeFilesystemEvents`) — the shared, multiplexed `/api/events` hub owned by the host, NOT a per-widget `EventSource` (the SSE-consolidation invariant: never open a per-widget connection — see `docs/architect/realtime-transport.md`). The hub owns reconnect/backoff; the host route is gated by `files.read`.
|
|
28
28
|
- Handlers dispatch into the actions of the window's OWN store (`useFilesystemStore` / `useFilesystemStoreApi`) per event; a frame the hub delivers after the scope changed (agent, project or store) is dropped, never applied to the next scope: `upsertFileNodeFromEvent` (per-file), `reconcileSourceFolders` (`sync:completed` and the `external:true` branch — both EARLY short-circuits BEFORE the `uri` guard, because the payload `uri` is the bare `<source>://` and would otherwise fabricate a phantom root node), `reconcileFolders` (a `changeType:'structure'` create/update/delete/revert — the affected PARENTS, never a global refresh), `ensureFolderLoaded({force})` + `refresh` (badge → `FileNode.git` subtree re-list + `SourceInfo.git` summaries), and `setSourceDirty` for a source that is not hydrated. An external change and a changing `sync:completed` share ONE per-source reconcile schedule: a trailing debounce (`RECONCILE_QUIET_MS` = 2 s — every event restarts it) bounded by a max-wait (`RECONCILE_MAX_WAIT_MS` = 6 s from the window's first event), so one agent turn's burst costs one or two passes of the visible folders instead of one per event, and a continuous burst still converges. The quiet window is sized on a measured turn, whose events arrive up to ~1.8 s apart. Pending passes are dropped on unmount and on an agent/project change. The badge branch keeps its own 250 ms timers (it does different work; a shared key would let one cancel the other). Badge/status and reconnect callbacks publish source revisions for Changes; timers are cancelled on context changes, and an epoch check prevents an old reconnect response from publishing into the new context.
|
|
29
29
|
- **The Changes badge re-count.** `pending:changed` (content-free, project-wide, derived server-side) never reaches the file handlers: it — plus a window focus / tab return and a successful reconnect probe — feeds ONE trailing schedule (`PENDING_COUNT_QUIET_MS` 1 000 ms, `PENDING_COUNT_MAX_WAIT_MS` 5 000 ms) that calls `refreshPendingCount`; a hidden tab skips the call, the mount counts once, a context change drops the timer. No poll, no timer while idle.
|
|
30
30
|
- **Reconnect recovery.** The filesystem channel is strictly live fan-out — the package route has no `Last-Event-ID`, no backfill and no cursor, and the host hub replays a snapshot only for `presence`. Everything emitted while the connection was down is lost. The hook therefore subscribes with `onConnChange` and, on a settled liveness flip, PROBES with the cheap summaries call and then re-converges every hydrated source. The hub's flip carries no direction, which is why the probe exists instead of a guess. This is what makes the toolbar Refresh a rare, deliberate action rather than routine repair.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useCallback, useRef } from 'react';
|
|
2
|
+
import { createDebouncedRefetch } from '@neuralis/package-system/client';
|
|
2
3
|
import { useFilesystemStore, useFilesystemStoreApi } from '../store/filesystemStore';
|
|
3
|
-
import { getFilesystemHostImperative } from '../FilesystemHost';
|
|
4
|
+
import { getFilesystemHostImperative, type FilesystemHostValue } from '../FilesystemHost';
|
|
4
5
|
import { uriAncestors, sourceFromUri, folderUri } from '../store/helpers';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -515,6 +516,8 @@ export function useFilesystemSSE(agentId: string | undefined, projectId: string
|
|
|
515
516
|
if (epoch !== contextEpoch.current) return;
|
|
516
517
|
// A gap may have hidden pending:changed frames — re-count once.
|
|
517
518
|
scheduleCountRef.current();
|
|
519
|
+
// …and `devices` frames: a device select re-reads too.
|
|
520
|
+
storeApi.setState((s) => ({ deviceRevision: s.deviceRevision + 1 }));
|
|
518
521
|
const state = storeApi.getState();
|
|
519
522
|
storeApi.setState(s => ({ sourceChangeRevision: Object.fromEntries(s.sources.map(source => [sourceFromUri(source.uri_root), (s.sourceChangeRevision[sourceFromUri(source.uri_root)] ?? 0) + 1])) }));
|
|
520
523
|
const sources = new Set<string>();
|
|
@@ -575,4 +578,41 @@ export function useFilesystemSSE(agentId: string | undefined, projectId: string
|
|
|
575
578
|
}, () => { if (live) handleConnChangeRef.current(); });
|
|
576
579
|
return () => { live = false; unsubscribe(); };
|
|
577
580
|
}, [agentId, projectId, storeApi]);
|
|
581
|
+
|
|
582
|
+
// Paired-device state is LIVE here too: a host source's liveness and the
|
|
583
|
+
// source form's device list follow a `devices` frame. ONE subscription per
|
|
584
|
+
// Files window; a burst is ONE summaries re-read + ONE revision bump. A hub
|
|
585
|
+
// reconnect is already covered above (the summaries re-read + the bump).
|
|
586
|
+
useEffect(() => {
|
|
587
|
+
if (!agentId || !projectId) return;
|
|
588
|
+
const host = getFilesystemHostImperative();
|
|
589
|
+
if (!host) return;
|
|
590
|
+
return subscribeDeviceRefresh(host, projectId, () => {
|
|
591
|
+
void loadSourceSummaries({ agentId, projectId }).catch(() => undefined);
|
|
592
|
+
storeApi.setState((s) => ({ deviceRevision: s.deviceRevision + 1 }));
|
|
593
|
+
});
|
|
594
|
+
}, [agentId, projectId, storeApi, loadSourceSummaries]);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* `onBurst` once per burst of `devices` frames (trailing `debounceMs`). The
|
|
599
|
+
* frame is `{deviceId, status}`; anything without a device id is ignored. A
|
|
600
|
+
* host without the method gives no live device updates.
|
|
601
|
+
*/
|
|
602
|
+
export function subscribeDeviceRefresh(
|
|
603
|
+
host: Pick<FilesystemHostValue, 'subscribeDeviceEvents'>,
|
|
604
|
+
projectId: string,
|
|
605
|
+
onBurst: () => void,
|
|
606
|
+
debounceMs = 400,
|
|
607
|
+
): () => void {
|
|
608
|
+
if (!host.subscribeDeviceEvents) return () => {};
|
|
609
|
+
const debounced = createDebouncedRefetch(onBurst, debounceMs);
|
|
610
|
+
const unsubscribe = host.subscribeDeviceEvents(projectId, (payload) => {
|
|
611
|
+
if (typeof (payload as { deviceId?: unknown } | null)?.deviceId !== 'string') return;
|
|
612
|
+
debounced.trigger();
|
|
613
|
+
});
|
|
614
|
+
return () => {
|
|
615
|
+
debounced.cancel();
|
|
616
|
+
unsubscribe();
|
|
617
|
+
};
|
|
578
618
|
}
|
|
@@ -13,7 +13,7 @@ The host integration entry point — `installFilesystemHostComponents`, re-expor
|
|
|
13
13
|
- `FilesystemPackageHost`, `FilesystemPackageHostProps` — `{ port: WorkspaceHostPort, children: ReactNode }`.
|
|
14
14
|
- `useBuildFilesystemHostValue({ port })` — `FilesystemHostValue`. Includes the optional `usePackageIconBySlug()` hook (added 2026-05-07): fetches `/api/packages/agent-core/packages/overview` once per project and exposes `slug → lucide component` so the filetree can render package dock-icons on top-level `packages://<slug>/` and `data://<slug>/` folders. Empty map on miss → falls back to the plain Folder icon.
|
|
15
15
|
- `FilesystemHostValue.currentRole` — `{ role?, priority? }` for the caller in the active project, derived from `WorkspaceHostPort.useCurrentUser()` (which now returns role + ordinal priority alongside name/email). Owner/admin-effective is `priority <= 2`. Used to HIDE owner-only Sources/Packages controls (the `recognizesPackages` toggle, the package access-feature override); UX-only — the server route stays the enforcement floor, so an absent signal hides the control (deny-by-default).
|
|
16
|
-
- `FilesystemHostValue.grantedFeatures` — `readonly string[]`, the caller's granted feature ids in the active project, read from `WorkspaceHostPort.getGrantedFeatures(projectId, userId)`. **Evaluate it ONLY through `hasFeature` / `meetsRequires` from `@neuralis/package-system/access`** — never a hand-rolled `includes` (it would miss the `'*'` wildcard), and never `currentRole`: the role NAME is not an authority source, not even for a hide-only gate. It is `readonly` so no UI file can mutate the caller's grants; the kernel predicates take a mutable array, so copy at the call site rather than widening the contract. UX-only exactly like `currentRole` — it hides an affordance the caller could not use (the S2 "hidden means absent" discipline: hidden entirely, never a greyed-out or locked row), and the route re-checks every call regardless. Absent/empty ⇒ hide, deny-by-default. First consumer: the Explorer tree's **Discover dock**, gated on `
|
|
16
|
+
- `FilesystemHostValue.grantedFeatures` — `readonly string[]`, the caller's granted feature ids in the active project, read from `WorkspaceHostPort.getGrantedFeatures(projectId, userId)`. **Evaluate it ONLY through `hasFeature` / `meetsRequires` from `@neuralis/package-system/access`** — never a hand-rolled `includes` (it would miss the `'*'` wildcard), and never `currentRole`: the role NAME is not an authority source, not even for a hide-only gate. It is `readonly` so no UI file can mutate the caller's grants; the kernel predicates take a mutable array, so copy at the call site rather than widening the contract. UX-only exactly like `currentRole` — it hides an affordance the caller could not use (the S2 "hidden means absent" discipline: hidden entirely, never a greyed-out or locked row), and the route re-checks every call regardless. Absent/empty ⇒ hide, deny-by-default. First consumer: the Explorer tree's **Discover dock**, gated on `sources.mount`.
|
|
17
17
|
- The port returns a FRESH array on every call, so `buildFilesystemHostValue` memoises on a joined key rather than array identity — a raw pass-through would invalidate the whole host-value memo on every render.
|
|
18
18
|
- `installFilesystemHostComponents({ registry: PackageHostRegistry, port: WorkspaceHostPort })` — side-effecting; registers widget + card + the provider fill (one component per install, closed over the port; it ignores the slot's `snapshot`/`assetScope` props), and subscribes ONCE to the host's optional `port.onClientStateReset` with `resetFilesClientState`, so the dock broom (instance / widget / agent / project level) reaches every mounted Files window it scopes and the one persisted record. A host without the method simply never resets.
|
|
19
19
|
- `InstallFilesystemHostComponentsOptions`.
|
|
@@ -29,7 +29,7 @@ The host integration entry point — `installFilesystemHostComponents`, re-expor
|
|
|
29
29
|
|
|
30
30
|
- One window store per mounted Files window, hidden and minimized windows included (they stay mounted); a window id addresses a reset and keys a persisted view, never an authority.
|
|
31
31
|
- The widget never sees raw host internals. `WorkspaceHostPort` exposes only the active session ids, the agent map, file marks, the SSE URL builder, granted features (read-only), the docker flag, and the client-state reset subscription (a scope, never a package name — the host stays generic).
|
|
32
|
-
- File marks and event subscriptions are agent-scoped; cross-agent observation requires the corresponding `
|
|
32
|
+
- File marks and event subscriptions are agent-scoped; cross-agent observation requires the corresponding `files.readOthers` / `files.writeOthers` features which the route layer enforces.
|
|
33
33
|
|
|
34
34
|
## Gotchas
|
|
35
35
|
|
|
@@ -33,7 +33,7 @@ export function useBuildFilesystemHostValue({ port }: BuildFilesystemHostValueOp
|
|
|
33
33
|
[userRole, userPriority],
|
|
34
34
|
);
|
|
35
35
|
// counted-cormorant — the caller's GRANTS, for hide-only affordance gates (the
|
|
36
|
-
// Discover dock's `
|
|
36
|
+
// Discover dock's `sources.mount` check). Deliberately separate from
|
|
37
37
|
// `currentRole`: the role NAME decides nothing, the grant does. Joined into a
|
|
38
38
|
// stable string so the memo below doesn't churn on a fresh array each render.
|
|
39
39
|
// Named `active*`: `usePackageIconBySlug` below declares its OWN `projectId`
|
|
@@ -42,7 +42,7 @@ export function useBuildFilesystemHostValue({ port }: BuildFilesystemHostValueOp
|
|
|
42
42
|
const activeUserId = port.useUserId();
|
|
43
43
|
// `getGrantedFeatures` returns a FRESH array on every call, so memoise on a
|
|
44
44
|
// joined key rather than on array identity. Feature ids are dotted lowercase
|
|
45
|
-
// (`
|
|
45
|
+
// (`sources.mount`) and can never contain a comma, so the round-trip is exact.
|
|
46
46
|
const grantedKey = port.getGrantedFeatures(activeProjectId, activeUserId).join(',');
|
|
47
47
|
const grantedFeatures = useMemo<readonly string[]>(
|
|
48
48
|
() => grantedKey.split(',').filter(Boolean),
|
|
@@ -92,6 +92,13 @@ export function useBuildFilesystemHostValue({ port }: BuildFilesystemHostValueOp
|
|
|
92
92
|
...(onConnChange ? { onConnChange } : {}),
|
|
93
93
|
});
|
|
94
94
|
},
|
|
95
|
+
subscribeDeviceEvents(projectId, onEvent) {
|
|
96
|
+
return port.subscribeRealtime({
|
|
97
|
+
channel: 'devices',
|
|
98
|
+
projectId,
|
|
99
|
+
onEvent: (_name, payload) => onEvent(payload),
|
|
100
|
+
});
|
|
101
|
+
},
|
|
95
102
|
isDocker: port.isDocker,
|
|
96
103
|
/**
|
|
97
104
|
* Fetches `/api/packages/agent-core/packages/overview` once per project
|