@neuralis/brain-core 0.3.0 → 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.
Files changed (123) hide show
  1. package/README.md +41 -40
  2. package/app/README.md +3 -3
  3. package/app/filesystem/FilesystemHost.tsx +7 -0
  4. package/app/filesystem/README.md +1 -1
  5. package/app/filesystem/api/index.ts +4 -4
  6. package/app/filesystem/hooks/README.md +1 -1
  7. package/app/filesystem/hooks/useFilesystemSSE.ts +41 -1
  8. package/app/filesystem/host/README.md +2 -2
  9. package/app/filesystem/host/buildFilesystemHostValue.ts +9 -2
  10. package/app/filesystem/lib/sourceKindForm.ts +109 -5
  11. package/app/filesystem/store/filesystemStore.ts +13 -3
  12. package/app/filesystem/ui/DiscoverDock.tsx +5 -1
  13. package/app/filesystem/ui/FileTree.tsx +34 -8
  14. package/app/filesystem/ui/FilesystemWidget.tsx +10 -2
  15. package/app/filesystem/ui/GitControlPanel.tsx +2 -2
  16. package/app/filesystem/ui/PendingChangesPanel.tsx +1 -1
  17. package/app/filesystem/ui/README.md +14 -13
  18. package/app/filesystem/ui/SourcesPanel.tsx +124 -24
  19. package/app/filesystem/ui/VectorHealthPanel.tsx +2 -2
  20. package/dist/app/host.js +41 -37
  21. package/dist/src/channels/filesystem.d.ts +1 -1
  22. package/dist/src/channels/filesystem.js +1 -1
  23. package/dist/src/domain/mime.d.ts +3 -3
  24. package/dist/src/domain/mime.js +3 -3
  25. package/dist/src/domain/scopeAccessCtx.d.ts +2 -2
  26. package/dist/src/domain/scopeAccessCtx.js +2 -2
  27. package/dist/src/domain/types.d.ts +8 -1
  28. package/dist/src/domain/types.d.ts.map +1 -1
  29. package/dist/src/events/pending.d.ts +1 -1
  30. package/dist/src/events/pending.js +1 -1
  31. package/dist/src/git/sourceGitControl.d.ts +2 -2
  32. package/dist/src/git/sourceGitControl.js +3 -3
  33. package/dist/src/lifecycle.d.ts +1 -1
  34. package/dist/src/routes/approve.d.ts +1 -1
  35. package/dist/src/routes/approve.js +1 -1
  36. package/dist/src/routes/events.d.ts +1 -1
  37. package/dist/src/routes/events.js +1 -1
  38. package/dist/src/routes/git.d.ts +7 -7
  39. package/dist/src/routes/git.js +18 -18
  40. package/dist/src/routes/health.d.ts +6 -6
  41. package/dist/src/routes/health.js +12 -12
  42. package/dist/src/routes/helpers/sourceNotConfigured.d.ts +1 -1
  43. package/dist/src/routes/helpers/sourceNotConfigured.js +1 -1
  44. package/dist/src/routes/helpers.d.ts +1 -1
  45. package/dist/src/routes/helpers.js +1 -1
  46. package/dist/src/routes/list.d.ts +1 -1
  47. package/dist/src/routes/list.js +1 -1
  48. package/dist/src/routes/pending.d.ts +4 -4
  49. package/dist/src/routes/pending.js +9 -9
  50. package/dist/src/routes/policies.d.ts +4 -4
  51. package/dist/src/routes/policies.d.ts.map +1 -1
  52. package/dist/src/routes/policies.js +11 -11
  53. package/dist/src/routes/policies.js.map +1 -1
  54. package/dist/src/routes/raw.d.ts +1 -1
  55. package/dist/src/routes/raw.js +1 -1
  56. package/dist/src/routes/read.d.ts +1 -1
  57. package/dist/src/routes/read.js +1 -1
  58. package/dist/src/routes/search.d.ts +1 -1
  59. package/dist/src/routes/search.js +1 -1
  60. package/dist/src/routes/sources.d.ts +20 -5
  61. package/dist/src/routes/sources.d.ts.map +1 -1
  62. package/dist/src/routes/sources.js +118 -118
  63. package/dist/src/routes/sources.js.map +1 -1
  64. package/dist/src/routes/upload.d.ts +1 -1
  65. package/dist/src/routes/upload.js +1 -1
  66. package/dist/src/routes/write.d.ts +2 -2
  67. package/dist/src/routes/write.js +2 -2
  68. package/dist/src/services/BridgeService.d.ts +7 -1
  69. package/dist/src/services/BridgeService.d.ts.map +1 -1
  70. package/dist/src/services/BridgeService.js +15 -3
  71. package/dist/src/services/BridgeService.js.map +1 -1
  72. package/dist/src/services/FilesystemService.d.ts +1 -1
  73. package/dist/src/services/FilesystemService.js +2 -2
  74. package/dist/src/services/declaredSources.d.ts +9 -3
  75. package/dist/src/services/declaredSources.d.ts.map +1 -1
  76. package/dist/src/services/declaredSources.js +2 -1
  77. package/dist/src/services/declaredSources.js.map +1 -1
  78. package/dist/src/tools/execCtx.d.ts +2 -2
  79. package/dist/src/tools/execCtx.js +2 -2
  80. package/dist/src/tools/fs_read.d.ts.map +1 -1
  81. package/dist/src/tools/fs_read.js +5 -0
  82. package/dist/src/tools/fs_read.js.map +1 -1
  83. package/dist/src/usecases/embeddingBacklog.js +1 -1
  84. package/dist/src/usecases/embeddingBacklog.js.map +1 -1
  85. package/dist/src/usecases/readFiles.d.ts.map +1 -1
  86. package/dist/src/usecases/readFiles.js +84 -1
  87. package/dist/src/usecases/readFiles.js.map +1 -1
  88. package/dist/src/usecases/syncPointWrites.d.ts +2 -0
  89. package/dist/src/usecases/syncPointWrites.d.ts.map +1 -1
  90. package/dist/src/usecases/syncPointWrites.js +5 -1
  91. package/dist/src/usecases/syncPointWrites.js.map +1 -1
  92. package/dist/src/usecases/vectorMaintenance.js +1 -1
  93. package/dist/src/usecases/vectorMaintenance.js.map +1 -1
  94. package/dist/src/usecases/write/content.js +1 -1
  95. package/dist/tools/fs_list.json +1 -1
  96. package/dist/tools/fs_read.json +1 -1
  97. package/dist/tools/fs_search.json +1 -1
  98. package/dist/tools/fs_write.json +1 -1
  99. package/instructions/brain-model.md +1 -1
  100. package/package.json +70 -63
  101. package/rules/brain-rules.md +2 -2
  102. package/skills/brain-start/SKILL.md +1 -1
  103. package/skills/connect-business-data/SKILL.md +1 -1
  104. package/skills/create-package/SKILL.md +8 -8
  105. package/skills/create-package/references/connectors-and-uri-policies.md +7 -2
  106. package/skills/create-package/references/runtime-and-build.md +1 -1
  107. package/skills/create-package/references/skills-and-context.md +1 -1
  108. package/skills/create-package/references/test-and-ship.md +4 -4
  109. package/skills/create-package/scripts/check-package.sh +1 -1
  110. package/skills/git/SKILL.md +23 -23
  111. package/skills/manage-files/SKILL.md +13 -13
  112. package/skills/manage-files/scripts/upload.sh +1 -1
  113. package/skills/manage-sources/SKILL.md +55 -54
  114. package/skills/manage-sources/scripts/reembed.sh +1 -1
  115. package/skills/manage-uri-policy/SKILL.md +9 -9
  116. package/skills/organize/SKILL.md +1 -1
  117. package/skills/sync/SKILL.md +2 -2
  118. package/tools/fs_list.json +1 -1
  119. package/tools/fs_read.json +1 -1
  120. package/tools/fs_search.json +1 -1
  121. package/tools/fs_write.json +1 -1
  122. package/workflows/sources-digest.json +1 -1
  123. 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 the
37
- operator's machine through the host broker — 12 declared capabilities including
38
- `write`/`delete`/`move`/`mkdir`/`rmdir`; containment comes from the
39
- operator-owned broker ceiling, never from a missing capability. See the
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 — `drive.mount.privileged`) | Platform app zone (`~/.neuralis/app/`); writes from agents are denied |
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
- `drive.write` FEATURE **and** the caller's RESOLVED uri-policy `write` on the source root
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 `drive.read`-only role. The server route is
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='drive.read'`):
497
- - **GET (read, `drive.read`):** `status?source=<slug>` (whole-repo change-set + current
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 `drive.write`-gated):** `stage` (`{paths[]}` whole-file `add`
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
- (`drive.write` feature + uri-policy `write`), NOT the `PendingInteraction` agent-approval
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 `drive.write` +
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` → `filesystem.observeScoped`/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
+ - `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 (`drive.write`)
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` → `drive.read`, `fs_write` → `drive.write`,
885
- `fs_search` → `drive.search`. This is the ONE source for the tool gate — the
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, `drive.read`), `list` (POST, `drive.read`), `search` (POST, `drive.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).
894
- - `write` (POST, `drive.write` — the ONE write route, every `fs_write` verb key; a copy-only call is guarded as `read` on the source), `approve` (POST, `drive.write`), `upload` (POST, `drive.write`).
895
- - `raw` (GET, `drive.read`), `events` (GET SSE, `drive.read` — a single-agent caller of the `subscribeFilesystemEventsImpl` authorization adapter, which re-authorizes the agentId deny-by-default), `health/*` (GET/POST, `drive.read` — but the `gc` sub-path is **POST-only** and escalates in-handler to **`drive.write`**, and the GET `hot-uris` sub-path (the write monitor) takes the same `drive.write` tier and then filters per uri, both declared via `export const escalations`; see the Vector health section).
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. `drive.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.
898
- - `git/*` (`src/routes/git.ts` — the Git Control panel surface): **GET** (`drive.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 `drive.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).
899
- - `policies/uri` (GET read = `drive.read`; POST patch escalates in-handler to **`drive.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 (`drive.mount`, `drive.mount.privileged`, `drive.sync`, `filesystem.observeScoped`/`modifyScoped`, `drive.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. **`drive.mount.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`. **`drive.mount.host`** (not granted below the admin tier — owner via `'*'`, admin via its enumerated default grants — and grantable per-role) gates the **host plane** — any source whose connector kind resolves OUTSIDE the container (`host`, `host-*`). It is required **together with** `drive.mount.privileged`, and both are checked independently: on the host plane **every root is privileged by construction**, so `/home/me/notes` is gated exactly like `/` — there is deliberately no root-sensitivity, because a sub-directory carve-out would re-open the escalation through a subdirectory. The check runs BEFORE the container-plane protected-root logic and needs no runtime-stack information, so it holds even when the stack is unavailable (fail closed). **Editing** a host-plane source requires the same pair as attaching one: `PATCH` cannot change `connection`, but it CAN widen `read` → `write`, so a plain-`drive.mount` manager must not be able to escalate an existing host source in place. Beyond these features, the operator-owned host-broker ceiling clamps what a granted caller can actually reach (see the agent-core README's host-access section) — the ceiling is the floor; the features are the audit trail. **`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 `drive.write`, and install/build still need `packages.author`.
900
- - `sources/*` (CRUD + `sync` (202 + queue job snapshot) + `cancel` (per-source `{source}` or all-visible; drive.sync) + `reindex` (202) + `reset` + `sync-status` (GET job snapshots, drive.sync) + `browse` (**`drive.mount`**; the filesystem root and any protected host zone additionally need `drive.mount.privileged`) + `preflight` (POST-only create validation/authorization, no write) + `kinds` + `kinds/:kind/schema` + `enabled` (light toggle, `'auto'` machine-only) + `discoverable` (**`drive.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.
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 `docs/drive-overview.md` 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, feature gates, recovery, and vector health.
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 `drive.read` (authoring still needs its own write/install capability).
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 `filesystem.observeScoped` / `filesystem.modifyScoped` 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: `filesystem.observeScoped` AND `filesystem.modifyScoped` 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
+ - 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 → `drive.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 `filesystem.modifyScoped` 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
+ - **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
- `filesystem.observeScoped` — answers exactly like an absent one, on every plane:
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 `drive.mount` holder reaches it.) A source the caller CAN see keeps its real
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 drive.mount and drive.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
+ `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 `drive.read`, which every role holds down to
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 **`drive.write`** and answers a caller without
1179
- it `403 Missing feature: drive.write (vector health repair)`. `deduplicate` is
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 `drive.write` holder (manager tier and up) runs every
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 `drive.write` tier as
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; `drive.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
+ - 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 `drive.mount`, not the skill's `drive.read` floor) | 17 |
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 `drive.write`; agent-origin uploads are journaled as pending review, while interactive human uploads land committed | 11 |
1527
- | `manage-uri-policy` | `/policies/uri/*` — read manifests baselines, read effective merged policy, patch path rules. Reads are the route's `drive.read` module feature (every built-in role holds it); the patch escalates in-handler to `drive.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 |
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 `drive.search` (hidden from default viewers — a
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 — `drive.read`, `drive.write`, `drive.search`, `drive.sync`, `drive.mount` (plus the in-handler escalations `drive.mount.privileged` / `drive.mount.host` for privileged and host-plane roots, and `drive.policy` for the URI-policy patch), together with `filesystem.observeScoped` / `filesystem.modifyScoped` 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 (`filesystem.observeScoped`).
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 `drive.write`.
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"
@@ -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 `drive.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.
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 `drive.write` + a
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). `drive.write` + uri-policy `write`.
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) — `drive.write` + uri-policy
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 `drive.mount` — `drive.mount.privileged` for the
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 `drive.read`.
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 `drive.mount`.
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 `filesystem.observeScoped` / `filesystem.modifyScoped` features which the route layer enforces.
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 `drive.mount` check). Deliberately separate from
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
- // (`drive.mount`) and can never contain a comma, so the round-trip is exact.
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