@neuralis/brain-core 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. package/README.md +79 -1409
  2. package/app/README.md +1 -1
  3. package/app/cards/FilesystemCardShell.tsx +42 -4
  4. package/app/cards/FilesystemChangeCard.tsx +34 -11
  5. package/app/cards/README.md +6 -5
  6. package/app/cards/diffUtils.ts +99 -1
  7. package/app/filesystem/FilesystemHost.tsx +17 -2
  8. package/app/filesystem/api/README.md +3 -3
  9. package/app/filesystem/api/index.ts +35 -11
  10. package/app/filesystem/api/projectPackages.ts +13 -102
  11. package/app/filesystem/domain/types.ts +30 -10
  12. package/app/filesystem/hooks/README.md +3 -3
  13. package/app/filesystem/hooks/useDiscoverableSources.ts +19 -30
  14. package/app/filesystem/hooks/useFilesystemSSE.ts +1 -1
  15. package/app/filesystem/host/README.md +5 -3
  16. package/app/filesystem/host/buildFilesystemHostValue.ts +19 -0
  17. package/app/filesystem/host/installFilesystemHostComponents.tsx +1 -1
  18. package/app/filesystem/lib/sourceKindForm.ts +118 -39
  19. package/app/filesystem/lib/treeVisualState.tsx +98 -4
  20. package/app/filesystem/store/README.md +1 -0
  21. package/app/filesystem/store/filesystemStore.ts +86 -5
  22. package/app/filesystem/store/gitSlice.ts +5 -0
  23. package/app/filesystem/store/helpers.ts +17 -0
  24. package/app/filesystem/store/listSlice.ts +8 -0
  25. package/app/filesystem/ui/CodeEditor.tsx +18 -3
  26. package/app/filesystem/ui/FileEditor.tsx +15 -10
  27. package/app/filesystem/ui/FileTree.tsx +41 -16
  28. package/app/filesystem/ui/FolderPicker.tsx +251 -0
  29. package/app/filesystem/ui/GitControlPanel.tsx +5 -0
  30. package/app/filesystem/ui/PackagesPanel.tsx +52 -374
  31. package/app/filesystem/ui/PolicyEditor.tsx +109 -24
  32. package/app/filesystem/ui/README.md +17 -11
  33. package/app/filesystem/ui/RawMediaPreview.tsx +58 -0
  34. package/app/filesystem/ui/SourceBranchHeader.tsx +4 -4
  35. package/app/filesystem/ui/SourcesPanel.tsx +420 -360
  36. package/app/filesystem/ui/Toolbar.tsx +1 -2
  37. package/app/shared/filesystem.png +0 -0
  38. package/dist/app/host.js +57 -49
  39. package/dist/scripts/sourceRootPairV1.d.ts +47 -0
  40. package/dist/scripts/sourceRootPairV1.d.ts.map +1 -0
  41. package/dist/scripts/sourceRootPairV1.js +147 -0
  42. package/dist/scripts/sourceRootPairV1.js.map +1 -0
  43. package/dist/scripts/sourceToFolderV1.d.ts +56 -0
  44. package/dist/scripts/sourceToFolderV1.d.ts.map +1 -0
  45. package/dist/scripts/sourceToFolderV1.js +152 -0
  46. package/dist/scripts/sourceToFolderV1.js.map +1 -0
  47. package/dist/scripts/sourceToHostV1.d.ts +56 -0
  48. package/dist/scripts/sourceToHostV1.d.ts.map +1 -0
  49. package/dist/scripts/sourceToHostV1.js +132 -0
  50. package/dist/scripts/sourceToHostV1.js.map +1 -0
  51. package/dist/src/connectors/AppSourceConfigStore.d.ts +17 -3
  52. package/dist/src/connectors/AppSourceConfigStore.d.ts.map +1 -1
  53. package/dist/src/connectors/AppSourceConfigStore.js +46 -6
  54. package/dist/src/connectors/AppSourceConfigStore.js.map +1 -1
  55. package/dist/src/connectors/HostConnector.d.ts +201 -0
  56. package/dist/src/connectors/HostConnector.d.ts.map +1 -0
  57. package/dist/src/connectors/HostConnector.js +903 -0
  58. package/dist/src/connectors/HostConnector.js.map +1 -0
  59. package/dist/src/connectors/inProcessTransport.d.ts +131 -0
  60. package/dist/src/connectors/inProcessTransport.d.ts.map +1 -0
  61. package/dist/src/connectors/inProcessTransport.js +447 -0
  62. package/dist/src/connectors/inProcessTransport.js.map +1 -0
  63. package/dist/src/domain/access/callerPermissions.d.ts +29 -0
  64. package/dist/src/domain/access/callerPermissions.d.ts.map +1 -1
  65. package/dist/src/domain/access/callerPermissions.js +85 -5
  66. package/dist/src/domain/access/callerPermissions.js.map +1 -1
  67. package/dist/src/domain/access/nativeLocalFence.d.ts +65 -0
  68. package/dist/src/domain/access/nativeLocalFence.d.ts.map +1 -0
  69. package/dist/src/domain/access/nativeLocalFence.js +171 -0
  70. package/dist/src/domain/access/nativeLocalFence.js.map +1 -0
  71. package/dist/src/domain/access/uriPermission.js +3 -3
  72. package/dist/src/domain/access/uriPermission.js.map +1 -1
  73. package/dist/src/domain/byteRange.d.ts +15 -0
  74. package/dist/src/domain/byteRange.d.ts.map +1 -0
  75. package/dist/src/domain/byteRange.js +30 -0
  76. package/dist/src/domain/byteRange.js.map +1 -0
  77. package/dist/src/domain/defaultPermissions.d.ts.map +1 -1
  78. package/dist/src/domain/defaultPermissions.js +29 -0
  79. package/dist/src/domain/defaultPermissions.js.map +1 -1
  80. package/dist/src/domain/mime.d.ts +8 -16
  81. package/dist/src/domain/mime.d.ts.map +1 -1
  82. package/dist/src/domain/mime.js +10 -99
  83. package/dist/src/domain/mime.js.map +1 -1
  84. package/dist/src/domain/paths.d.ts.map +1 -1
  85. package/dist/src/domain/paths.js +5 -3
  86. package/dist/src/domain/paths.js.map +1 -1
  87. package/dist/src/domain/sourceConfig.d.ts +52 -19
  88. package/dist/src/domain/sourceConfig.d.ts.map +1 -1
  89. package/dist/src/domain/sourceConfig.js +103 -19
  90. package/dist/src/domain/sourceConfig.js.map +1 -1
  91. package/dist/src/domain/sourcePackages.d.ts +8 -8
  92. package/dist/src/domain/sourcePackages.d.ts.map +1 -1
  93. package/dist/src/domain/sourceRootPair.d.ts +88 -0
  94. package/dist/src/domain/sourceRootPair.d.ts.map +1 -0
  95. package/dist/src/domain/sourceRootPair.js +108 -0
  96. package/dist/src/domain/sourceRootPair.js.map +1 -0
  97. package/dist/src/domain/syncTypes.d.ts +8 -1
  98. package/dist/src/domain/syncTypes.d.ts.map +1 -1
  99. package/dist/src/domain/syncTypes.js +1 -0
  100. package/dist/src/domain/syncTypes.js.map +1 -1
  101. package/dist/src/domain/types.d.ts +24 -1
  102. package/dist/src/domain/types.d.ts.map +1 -1
  103. package/dist/src/domain/uri.d.ts.map +1 -1
  104. package/dist/src/domain/uri.js +3 -2
  105. package/dist/src/domain/uri.js.map +1 -1
  106. package/dist/src/embeddings/clients/GeminiEmbedder.d.ts.map +1 -1
  107. package/dist/src/embeddings/clients/GeminiEmbedder.js +2 -5
  108. package/dist/src/embeddings/clients/GeminiEmbedder.js.map +1 -1
  109. package/dist/src/embeddings/clients/OpenAIEmbedder.d.ts.map +1 -1
  110. package/dist/src/embeddings/clients/OpenAIEmbedder.js +2 -3
  111. package/dist/src/embeddings/clients/OpenAIEmbedder.js.map +1 -1
  112. package/dist/src/embeddings/clients/QwenEmbedder.d.ts +2 -0
  113. package/dist/src/embeddings/clients/QwenEmbedder.d.ts.map +1 -1
  114. package/dist/src/embeddings/clients/QwenEmbedder.js +10 -2
  115. package/dist/src/embeddings/clients/QwenEmbedder.js.map +1 -1
  116. package/dist/src/embeddings/clients/VoyageEmbedder.d.ts +2 -0
  117. package/dist/src/embeddings/clients/VoyageEmbedder.d.ts.map +1 -1
  118. package/dist/src/embeddings/clients/VoyageEmbedder.js +8 -1
  119. package/dist/src/embeddings/clients/VoyageEmbedder.js.map +1 -1
  120. package/dist/src/embeddings/factory.d.ts +5 -3
  121. package/dist/src/embeddings/factory.d.ts.map +1 -1
  122. package/dist/src/embeddings/factory.js +7 -3
  123. package/dist/src/embeddings/factory.js.map +1 -1
  124. package/dist/src/embeddings/models.d.ts +1 -1
  125. package/dist/src/embeddings/models.d.ts.map +1 -1
  126. package/dist/src/embeddings/retry.js +1 -1
  127. package/dist/src/embeddings/retry.js.map +1 -1
  128. package/dist/src/embeddings/types.d.ts +6 -0
  129. package/dist/src/embeddings/types.d.ts.map +1 -1
  130. package/dist/src/embeddings/types.js +11 -0
  131. package/dist/src/embeddings/types.js.map +1 -1
  132. package/dist/src/git/GitRunner.d.ts +110 -30
  133. package/dist/src/git/GitRunner.d.ts.map +1 -1
  134. package/dist/src/git/GitRunner.js +511 -199
  135. package/dist/src/git/GitRunner.js.map +1 -1
  136. package/dist/src/git/GitWatchRegistry.d.ts +41 -35
  137. package/dist/src/git/GitWatchRegistry.d.ts.map +1 -1
  138. package/dist/src/git/GitWatchRegistry.js +61 -79
  139. package/dist/src/git/GitWatchRegistry.js.map +1 -1
  140. package/dist/src/git/gitPlace.d.ts +61 -0
  141. package/dist/src/git/gitPlace.d.ts.map +1 -0
  142. package/dist/src/git/gitPlace.js +74 -0
  143. package/dist/src/git/gitPlace.js.map +1 -0
  144. package/dist/src/git/sourceGitControl.d.ts +31 -19
  145. package/dist/src/git/sourceGitControl.d.ts.map +1 -1
  146. package/dist/src/git/sourceGitControl.js +208 -92
  147. package/dist/src/git/sourceGitControl.js.map +1 -1
  148. package/dist/src/git/sourceGitStatus.js +1 -1
  149. package/dist/src/git/sourceGitStatus.js.map +1 -1
  150. package/dist/src/git/untrackedStats.js +1 -1
  151. package/dist/src/git/untrackedStats.js.map +1 -1
  152. package/dist/src/index.d.ts +2 -1
  153. package/dist/src/index.d.ts.map +1 -1
  154. package/dist/src/index.js +2 -1
  155. package/dist/src/index.js.map +1 -1
  156. package/dist/src/lifecycle.d.ts +8 -11
  157. package/dist/src/lifecycle.d.ts.map +1 -1
  158. package/dist/src/lifecycle.js +2 -1
  159. package/dist/src/lifecycle.js.map +1 -1
  160. package/dist/src/routes/git.d.ts.map +1 -1
  161. package/dist/src/routes/git.js +183 -54
  162. package/dist/src/routes/git.js.map +1 -1
  163. package/dist/src/routes/helpers/uriGuard.d.ts +1 -1
  164. package/dist/src/routes/helpers/uriGuard.d.ts.map +1 -1
  165. package/dist/src/routes/helpers/uriGuard.js +3 -2
  166. package/dist/src/routes/helpers/uriGuard.js.map +1 -1
  167. package/dist/src/routes/helpers.d.ts +10 -3
  168. package/dist/src/routes/helpers.d.ts.map +1 -1
  169. package/dist/src/routes/helpers.js +12 -5
  170. package/dist/src/routes/helpers.js.map +1 -1
  171. package/dist/src/routes/list.d.ts.map +1 -1
  172. package/dist/src/routes/list.js +2 -3
  173. package/dist/src/routes/list.js.map +1 -1
  174. package/dist/src/routes/policies.d.ts +6 -4
  175. package/dist/src/routes/policies.d.ts.map +1 -1
  176. package/dist/src/routes/policies.js +22 -16
  177. package/dist/src/routes/policies.js.map +1 -1
  178. package/dist/src/routes/raw.d.ts.map +1 -1
  179. package/dist/src/routes/raw.js +65 -11
  180. package/dist/src/routes/raw.js.map +1 -1
  181. package/dist/src/routes/sources.d.ts +60 -18
  182. package/dist/src/routes/sources.d.ts.map +1 -1
  183. package/dist/src/routes/sources.js +411 -299
  184. package/dist/src/routes/sources.js.map +1 -1
  185. package/dist/src/services/BridgeService.d.ts +15 -35
  186. package/dist/src/services/BridgeService.d.ts.map +1 -1
  187. package/dist/src/services/BridgeService.js +30 -88
  188. package/dist/src/services/BridgeService.js.map +1 -1
  189. package/dist/src/services/FilesystemService.d.ts +3 -2
  190. package/dist/src/services/FilesystemService.d.ts.map +1 -1
  191. package/dist/src/services/FilesystemService.js +110 -32
  192. package/dist/src/services/FilesystemService.js.map +1 -1
  193. package/dist/src/services/FilesystemUiService.d.ts +4 -1
  194. package/dist/src/services/FilesystemUiService.d.ts.map +1 -1
  195. package/dist/src/services/FilesystemUiService.js +66 -25
  196. package/dist/src/services/FilesystemUiService.js.map +1 -1
  197. package/dist/src/services/SyncScheduler.d.ts +12 -4
  198. package/dist/src/services/SyncScheduler.d.ts.map +1 -1
  199. package/dist/src/services/SyncScheduler.js +37 -8
  200. package/dist/src/services/SyncScheduler.js.map +1 -1
  201. package/dist/src/services/baselineRepair.d.ts +13 -3
  202. package/dist/src/services/baselineRepair.d.ts.map +1 -1
  203. package/dist/src/services/baselineRepair.js +13 -3
  204. package/dist/src/services/baselineRepair.js.map +1 -1
  205. package/dist/src/services/declaredSources.d.ts +125 -42
  206. package/dist/src/services/declaredSources.d.ts.map +1 -1
  207. package/dist/src/services/declaredSources.js +117 -45
  208. package/dist/src/services/declaredSources.js.map +1 -1
  209. package/dist/src/store/dataFormats.d.ts +17 -2
  210. package/dist/src/store/dataFormats.d.ts.map +1 -1
  211. package/dist/src/store/dataFormats.js +18 -3
  212. package/dist/src/store/dataFormats.js.map +1 -1
  213. package/dist/src/sync/DeltaSyncRunner.d.ts.map +1 -1
  214. package/dist/src/sync/DeltaSyncRunner.js +144 -4
  215. package/dist/src/sync/DeltaSyncRunner.js.map +1 -1
  216. package/dist/src/sync/SyncJobQueue.d.ts +7 -2
  217. package/dist/src/sync/SyncJobQueue.d.ts.map +1 -1
  218. package/dist/src/sync/SyncJobQueue.js +16 -10
  219. package/dist/src/sync/SyncJobQueue.js.map +1 -1
  220. package/dist/src/sync/SyncWatchService.d.ts +21 -4
  221. package/dist/src/sync/SyncWatchService.d.ts.map +1 -1
  222. package/dist/src/sync/SyncWatchService.js +115 -10
  223. package/dist/src/sync/SyncWatchService.js.map +1 -1
  224. package/dist/src/sync/categoryInference.d.ts +1 -1
  225. package/dist/src/sync/categoryInference.js +1 -1
  226. package/dist/src/usecases/admitSourceRoot.d.ts +65 -0
  227. package/dist/src/usecases/admitSourceRoot.d.ts.map +1 -0
  228. package/dist/src/usecases/admitSourceRoot.js +85 -0
  229. package/dist/src/usecases/admitSourceRoot.js.map +1 -0
  230. package/dist/src/usecases/browseSourceFolders.d.ts +85 -0
  231. package/dist/src/usecases/browseSourceFolders.d.ts.map +1 -0
  232. package/dist/src/usecases/browseSourceFolders.js +210 -0
  233. package/dist/src/usecases/browseSourceFolders.js.map +1 -0
  234. package/dist/src/usecases/createDeclaredSources.d.ts +13 -2
  235. package/dist/src/usecases/createDeclaredSources.d.ts.map +1 -1
  236. package/dist/src/usecases/createDeclaredSources.js +39 -0
  237. package/dist/src/usecases/createDeclaredSources.js.map +1 -1
  238. package/dist/src/usecases/list.d.ts.map +1 -1
  239. package/dist/src/usecases/list.js +83 -26
  240. package/dist/src/usecases/list.js.map +1 -1
  241. package/dist/src/usecases/listAttachableDevices.d.ts +13 -0
  242. package/dist/src/usecases/listAttachableDevices.d.ts.map +1 -0
  243. package/dist/src/usecases/listAttachableDevices.js +10 -0
  244. package/dist/src/usecases/listAttachableDevices.js.map +1 -0
  245. package/dist/src/usecases/listSourcePackages.d.ts.map +1 -1
  246. package/dist/src/usecases/listSourcePackages.js +2 -5
  247. package/dist/src/usecases/listSourcePackages.js.map +1 -1
  248. package/dist/src/usecases/purgeExcludedPoints.d.ts.map +1 -1
  249. package/dist/src/usecases/purgeExcludedPoints.js +3 -2
  250. package/dist/src/usecases/purgeExcludedPoints.js.map +1 -1
  251. package/dist/src/usecases/readFiles.d.ts.map +1 -1
  252. package/dist/src/usecases/readFiles.js +15 -0
  253. package/dist/src/usecases/readFiles.js.map +1 -1
  254. package/dist/src/usecases/reconcileSourcePackageRoots.d.ts +1 -1
  255. package/dist/src/usecases/reconcileSourcePackageRoots.d.ts.map +1 -1
  256. package/dist/src/usecases/reconcileSourcePackageRoots.js +2 -1
  257. package/dist/src/usecases/reconcileSourcePackageRoots.js.map +1 -1
  258. package/dist/src/usecases/revokePrincipal.d.ts +7 -5
  259. package/dist/src/usecases/revokePrincipal.d.ts.map +1 -1
  260. package/dist/src/usecases/revokePrincipal.js +29 -7
  261. package/dist/src/usecases/revokePrincipal.js.map +1 -1
  262. package/dist/src/usecases/scheduleSourceSync.d.ts +7 -0
  263. package/dist/src/usecases/scheduleSourceSync.d.ts.map +1 -1
  264. package/dist/src/usecases/scheduleSourceSync.js +6 -5
  265. package/dist/src/usecases/scheduleSourceSync.js.map +1 -1
  266. package/dist/src/usecases/triggerSourceSync.d.ts +46 -0
  267. package/dist/src/usecases/triggerSourceSync.d.ts.map +1 -0
  268. package/dist/src/usecases/triggerSourceSync.js +75 -0
  269. package/dist/src/usecases/triggerSourceSync.js.map +1 -0
  270. package/dist/src/usecases/uploadBinary.d.ts +2 -1
  271. package/dist/src/usecases/uploadBinary.d.ts.map +1 -1
  272. package/dist/src/usecases/uploadBinary.js +2 -2
  273. package/dist/src/usecases/uploadBinary.js.map +1 -1
  274. package/dist/src/usecases/vector/endpointGuard.d.ts +1 -1
  275. package/dist/src/usecases/vector/endpointGuard.js +1 -1
  276. package/dist/src/vector/qdrantClient.d.ts +1 -1
  277. package/dist/src/vector/qdrantClient.js +1 -1
  278. package/dist/src/wiring.d.ts +3 -0
  279. package/dist/src/wiring.d.ts.map +1 -1
  280. package/dist/src/wiring.js +72 -6
  281. package/dist/src/wiring.js.map +1 -1
  282. package/package.json +145 -54
  283. package/skills/connect-business-data/SKILL.md +2 -2
  284. package/skills/create-package/SKILL.md +18 -18
  285. package/skills/create-package/references/config-and-credentials.md +16 -13
  286. package/skills/create-package/references/connectors-and-uri-policies.md +36 -27
  287. package/skills/create-package/references/manifest-and-discovery.md +12 -12
  288. package/skills/create-package/references/runtime-and-build.md +18 -18
  289. package/skills/create-package/references/skills-and-context.md +35 -19
  290. package/skills/create-package/references/surfaces-and-design.md +16 -16
  291. package/skills/create-package/references/team-and-workflows.md +2 -2
  292. package/skills/create-package/references/test-and-ship.md +17 -17
  293. package/skills/create-package/references/tools-and-schemas.md +5 -5
  294. package/skills/create-package/scripts/{check-package.sh → check-package.mjs} +78 -64
  295. package/skills/create-package/scripts/{verify-loaded.sh → verify-loaded.mjs} +156 -162
  296. package/skills/git/SKILL.md +31 -29
  297. package/skills/manage-files/SKILL.md +11 -14
  298. package/skills/manage-sources/SKILL.md +85 -45
  299. package/skills/manage-uri-policy/SKILL.md +9 -6
  300. package/skills/sync/SKILL.md +1 -1
  301. package/dist/src/connectors/LocalDiskConnector.d.ts +0 -171
  302. package/dist/src/connectors/LocalDiskConnector.d.ts.map +0 -1
  303. package/dist/src/connectors/LocalDiskConnector.js +0 -1233
  304. package/dist/src/connectors/LocalDiskConnector.js.map +0 -1
  305. package/skills/manage-files/scripts/copy.sh +0 -7
  306. package/skills/manage-files/scripts/create.sh +0 -7
  307. package/skills/manage-files/scripts/mkdir.sh +0 -7
  308. package/skills/manage-files/scripts/modify.sh +0 -7
  309. package/skills/manage-files/scripts/upload.sh +0 -16
  310. package/skills/manage-files/scripts/write.sh +0 -29
  311. package/skills/manage-sources/scripts/reembed.sh +0 -23
@@ -18,9 +18,13 @@
18
18
  * POST /sources/reset — reset sync state (cancels active job)
19
19
  * GET /sources/sync-status — job status (?source= single, else all visible)
20
20
  * POST /sources/enabled — light enable/disable toggle (W3K)
21
- * GET /sources/browse — browse filesystem directories (sources.mount;
22
- * protected zones + the fs root need
23
- * sources.privileged)
21
+ * GET /sources/browse — one level of folders: on this server
22
+ * (sources.mount; protected zones + the
23
+ * fs root need sources.privileged; in
24
+ * Docker under /mounts), or with
25
+ * ?device= on the caller's OWN device
26
+ * (device.own; empty path = the
27
+ * folders it allows this host)
24
28
  * GET /sources/health — get sources health
25
29
  *
26
30
  * Sync-shaped POSTs return 202 + a SyncQueueJobSnapshot: the run executes on
@@ -28,19 +32,27 @@
28
32
  * froze the host on repo-sized sources).
29
33
  */
30
34
  import { existsSync } from 'node:fs';
31
- import { isAbsolute, resolve as resolvePath } from 'node:path';
32
- import { canAccessScope, isHostPlaneKind, isSourceScope } from '@neuralis/package-system/contracts';
33
- import { hasFeature, BUILTIN_ROLE_PRIORITY } from '@neuralis/package-system/access';
35
+ import { dirname as dirnameOf, isAbsolute, resolve as resolvePath } from 'node:path';
36
+ import { HOST_SOURCE_KIND, canAccessScope, isConnectorPlaneExecKind, isSourceScope, sourcePlane } from '@neuralis/package-system/contracts';
37
+ // `isOwnerOrAdminEffective` gates the code-LOAD surface (`packageFolders`): the
38
+ // ordinal, never a role name — the Sources panel hides the field with the SAME call.
39
+ import { hasFeature, isOwnerOrAdminEffective } from '@neuralis/package-system/access';
34
40
  import { assertImmutablePolicyFloorsPreserved, CallerPolicyFloorError, containsPolicyFloorMarker, ImmutablePolicyFloorError, } from '@neuralis/package-system/policies';
35
- import { protectedRootPaths, seedExcludedMachineWrittenGlobs } from '@neuralis/package-system/paths';
36
- import { resolveLocalRoot, LocalDiskConnector } from '../connectors/LocalDiskConnector.js';
41
+ import { hostPathFlavour, isFilesystemRoot, isPathInside, protectedRootPaths, seedExcludedMachineWrittenGlobs } from '@neuralis/package-system/paths';
42
+ import { NEURALIS_TREE_ANCHOR, resolveLocalRoot, resolvedLocalRootOrNull } from '../connectors/inProcessTransport.js';
43
+ import { SOURCE_ANCHOR_UNRESOLVABLE, admitSourceRoot, unlessAnchorUnresolvable } from '../usecases/admitSourceRoot.js';
44
+ import { HostConnector } from '../connectors/HostConnector.js';
45
+ import { listAttachableDevices } from '../usecases/listAttachableDevices.js';
46
+ import { browseDeviceFolders, browseServerFolders } from '../usecases/browseSourceFolders.js';
37
47
  import { filterVisibleRepoNames, getSourceGitSummary, resolveGitCanWrite, resolveGitCanExec } from '../git/sourceGitControl.js';
48
+ import { probePlaceOf } from '../git/gitPlace.js';
38
49
  import { matchDeclaredSource, isOwnDataZoneDeclaration } from '../services/declaredSources.js';
39
50
  import { platformRegistries } from '../wiring.js';
40
- import { resolveSourceDescription } from '../domain/sourceConfig.js';
51
+ import { resolveSourceDescription, validatePackageFolders, sourceScopeOrigin } from '../domain/sourceConfig.js';
41
52
  import { extractCtx, ensureAccess, getServices, toPublicSourceConfig, toScopeAccessCtx, isPermissions, normalizePermissions, isSyncConfig } from './helpers.js';
42
53
  import { EmbeddingScopeLimitError } from '../sync/SyncJobQueue.js';
43
54
  import { canReadUri } from '../domain/access/uriReadable.js';
55
+ import { NATIVE_LOCAL_EDIT_REFUSAL, NATIVE_LOCAL_REFUSAL, attachableNatively, fencedEditWidens, nativeLocalFenceApplies, nativeLocalPlacement, nativeLocalRootRefused } from '../domain/access/nativeLocalFence.js';
44
56
  import { listScopeDocumentUris, estimateScopedReembed } from '../usecases/vector/estimate.js';
45
57
  import { embeddingEnabled, resolveActiveEmbeddingShape } from '../config/env.js';
46
58
  import { parsePurgeSelector, purgeSourceSelection } from '../usecases/purgeExcludedPoints.js';
@@ -48,7 +60,7 @@ export const pattern = 'sources/*';
48
60
  export const feature = 'files.read';
49
61
  // The module gate is the read tier (listing a source is member-visible); every
50
62
  // mutating / enumerating arm escalates in-handler through `hasRouteFeature`.
51
- // A host-plane (paired-device) source is attached by its owner under
63
+ // A device-plane (paired-device) source is attached by its owner under
52
64
  // `device.own`, which agent-core's device gate answers — not a gate here.
53
65
  export const escalations = [
54
66
  'sources.mount',
@@ -73,6 +85,11 @@ export const GET = async (req, raw) => {
73
85
  if (sub === 'sync-status') {
74
86
  return handleSyncStatus(req, state);
75
87
  }
88
+ if (sub === 'devices') {
89
+ const ctx = extractCtx(req);
90
+ await ensureAccess(ctx, state);
91
+ return { status: 200, body: await listAttachableDevices(state.ctx.packages, ctx.userId) };
92
+ }
76
93
  if (sub === 'health') {
77
94
  const ctx = extractCtx(req);
78
95
  await ensureAccess(ctx, state);
@@ -100,17 +117,18 @@ export const GET = async (req, raw) => {
100
117
  // against the declared sources (unspoofable slug+kind+resolved-root) so the
101
118
  // Sources panel can group rows by their owning package; user mounts → null.
102
119
  const infra = await state.infraFactory.getInfra(ctx.projectId);
103
- const matchCtx = { projectRoot: infra?.projectRoot ?? '', appRoot: state.ctx.platform.runtimeStack().appRoot };
120
+ const matchCtx = { projectRoot: infra?.projectRoot ?? '', appRoot: state.ctx.platform.runtimeStack().appRoot, isDocker: state.ctx.platform.runtimeStack().isDocker };
104
121
  const declsByPkg = state.ctx.platform.packageSourceDeclarations();
105
122
  const dataSyncExclude = seedExcludedMachineWrittenGlobs(state.ctx.platform.machineWrittenPaths());
106
123
  // The projection reads every row's config from the listing already in hand,
107
124
  // so the per-row root-read check costs no config fetch.
108
- const protectedRoots = buildProtectedRoots(state.ctx.platform.runtimeStack());
125
+ const stack = state.ctx.platform.runtimeStack();
126
+ const protectedRoots = buildProtectedRoots(stack);
109
127
  const configCache = new Map(allConfigs.map((c) => [c.source, c]));
110
128
  // Source-level git indicator (G2.1 / spellbound-heron) — the CONFIG-panel half
111
129
  // of the two summary builders (the TREE half is `FilesystemService.list`;
112
130
  // MAJOR-1 — BOTH must be enriched). CHEAP + bounded O(1)/source: for each
113
- // git-backed LOCAL source, resolve its connector root and stamp
131
+ // git-backed `host` folder (this server's or a device's — read without a spawn), resolve its root and stamp
114
132
  // `git = { isRepo, repoName, branch }` from `getSourceGitSummary` (a `.git` stat + an
115
133
  // O(1) `.git/HEAD` branch read, TTL-cached — never the whole-repo status).
116
134
  // Runs on `allConfigs` AFTER `listSourceConfigs`'s scope filter (leak-safe).
@@ -122,7 +140,7 @@ export const GET = async (req, raw) => {
122
140
  const origin = matched?.originPackage ?? null;
123
141
  const git = await resolveSourceGit(config, infra, ctx);
124
142
  const readable = await rootReadable(infra, ctx, config.source, configCache);
125
- const view = await sourceConfigView(req.session, config, infra?.projectRoot ?? '', protectedRoots, readable, deviceAttachVerdict(state, ctx, config.connection), !config.sync && matched && isOwnDataZoneDeclaration(matched.originPackage, matched.decl, state.ctx.selfId) ? dataSyncExclude : undefined);
143
+ const view = await sourceConfigView(req.session, config, infra?.projectRoot ?? '', stack.isDocker, protectedRoots, readable, deviceAttachVerdict(state, ctx, config.connection), !config.sync && matched && isOwnDataZoneDeclaration(matched.originPackage, matched.decl, state.ctx.selfId) ? dataSyncExclude : undefined);
126
144
  return {
127
145
  ...toPublicSourceConfig(config, view),
128
146
  // DERIVED, never persisted — the same chain the `<runtime_stack>`
@@ -140,7 +158,7 @@ export const GET = async (req, raw) => {
140
158
  origin_package: origin,
141
159
  orphan: !metadata, embedding_enabled: embeddingEnabled(),
142
160
  canMaintain: hasRouteFeature(req.session, 'sources.mount') && hasRouteFeature(req.session, 'sources.sync')
143
- && canAccessScope(config.scope, access, 'write') && readable,
161
+ && canAccessScope(config.scope, access, 'write', sourceScopeOrigin(config.connection)) && readable,
144
162
  orphan_hint: metadata ? null : `Package contributing kind "${config.connection.kind}" is missing — remove the source or reinstall the package.`,
145
163
  ...(git ? { git } : {}),
146
164
  };
@@ -149,8 +167,8 @@ export const GET = async (req, raw) => {
149
167
  };
150
168
  /**
151
169
  * Resolve the source-row git indicator for `GET /sources` (G2.1). Returns
152
- * `{ isRepo: true, repoName?, branch? }` for a git-backed LOCAL source — or,
153
- * E1 (MAJOR-C), `{ isRepo: false, containsRepos: true }` for a LOCAL source
170
+ * `{ isRepo: true, repoName?, branch? }` for a git-backed `host` folder — or,
171
+ * E1 (MAJOR-C), `{ isRepo: false, containsRepos: true }` for a folder
154
172
  * whose NON-repo root contains nested depth-1 checkouts (the drop condition is
155
173
  * `isRepo || containsRepos`; without the widening the field never reaches the
156
174
  * client and the multi-repo picker is dead-on-arrival). `undefined` for
@@ -162,12 +180,12 @@ export const GET = async (req, raw) => {
162
180
  * never a host path. Fail-soft: any git problem yields `undefined`.
163
181
  */
164
182
  async function resolveSourceGit(config, infra, ctx) {
165
- if (config.connection?.kind !== 'local')
183
+ if (!config.connection || sourcePlane(config.connection.kind ?? '', config.connection.config) === 'connector')
166
184
  return undefined;
167
185
  const connector = infra?.connectors?.resolveBySource?.(config.source) ?? null;
168
- if (!(connector instanceof LocalDiskConnector))
186
+ if (!(connector instanceof HostConnector))
169
187
  return undefined;
170
- const summary = await getSourceGitSummary(connector.root);
188
+ const summary = await getSourceGitSummary(probePlaceOf(connector));
171
189
  if (!summary)
172
190
  return undefined;
173
191
  if (!summary.isRepo) {
@@ -219,7 +237,7 @@ export const POST = async (req, raw) => {
219
237
  }
220
238
  const { bridge, infra } = await getServices(state, ctx.projectId);
221
239
  const config = await bridge.getSourceConfig(ctx, source, toScopeAccessCtx(ctx));
222
- if (!config || !canAccessScope(config.scope, toScopeAccessCtx(ctx), 'write')) {
240
+ if (!config || !canAccessScope(config.scope, toScopeAccessCtx(ctx), 'write', sourceScopeOrigin(config.connection))) {
223
241
  return { status: 404, body: { error: 'Source not found.' } };
224
242
  }
225
243
  try {
@@ -265,6 +283,14 @@ async function handleKindsList(state) {
265
283
  packageId: m.packageId,
266
284
  trust: m.packageTrust,
267
285
  capabilities: [...m.capabilities].sort(),
286
+ // Can a shell run in this kind's sources at all — the `host` kind's own
287
+ // planes (the S1 uri-policy shell, the device's broker) or a connector that
288
+ // declares `exec`? The kernel's two plane predicates answer; the form offers
289
+ // the Exec policy bit exactly then. `execRequires` (the entry features) is
290
+ // information for its tooltip, never a reason to hide the bit — an owner may
291
+ // set `exec` for members who hold them.
292
+ shell: sourcePlane(m.kind) !== 'connector' || isConnectorPlaneExecKind(m.kind, m.capabilities),
293
+ ...(m.execRequires?.length ? { execRequires: [...m.execRequires] } : {}),
268
294
  runtimeGated: m.runtimeGated,
269
295
  urlScheme: m.uri?.suggestedScheme ?? m.kind,
270
296
  configSchema: m.configSchema,
@@ -334,6 +360,7 @@ async function withEffectiveDescription(config, state, ctx, session) {
334
360
  const matched = matchDeclaredSource(config, state.ctx.platform.packageSourceDeclarations(), {
335
361
  projectRoot: infra?.projectRoot ?? '',
336
362
  appRoot: state.ctx.platform.runtimeStack().appRoot,
363
+ isDocker: state.ctx.platform.runtimeStack().isDocker,
337
364
  });
338
365
  return {
339
366
  ...toPublicSourceConfig(config, await publicViewFor(session, ctx, config, state, infra)),
@@ -357,7 +384,7 @@ async function handleSourcesCreate(req, state, preflight = false) {
357
384
  const description = body?.description;
358
385
  // `null` = absent. A read-then-resend caller (the manage-sources skill's jq
359
386
  // projection) legitimately round-trips a field the listing never carried.
360
- const recognizesPackages = body?.recognizesPackages ?? undefined;
387
+ const packageFolders = body?.packageFolders ?? undefined;
361
388
  if (!source)
362
389
  return { status: 400, body: { error: 'Missing source' } };
363
390
  if (description !== undefined && typeof description !== 'string') {
@@ -382,13 +409,13 @@ async function handleSourcesCreate(req, state, preflight = false) {
382
409
  },
383
410
  };
384
411
  }
385
- // R2b — `recognizesPackages` is a code-LOAD surface (it turns a source into a
386
- // WASM-package source). Mirror the PATCH gate: boolean, owner/admin-effective,
387
- // local-kind only. Validated here BEFORE the access check so a malformed value
388
- // is a 400 regardless of role.
389
- if (recognizesPackages !== undefined && typeof recognizesPackages !== 'boolean') {
390
- return { status: 400, body: { error: 'recognizesPackages must be a boolean if provided' } };
391
- }
412
+ // R2b — `packageFolders` is a code-LOAD surface (it turns folders of a source
413
+ // into package sources). Mirror the PATCH gate: source-relative folders,
414
+ // owner/admin-effective, folder kind only. Validated here BEFORE the access
415
+ // check so a malformed value is a 400 regardless of role.
416
+ const packageFoldersError = packageFolders === undefined ? undefined : validatePackageFolders(packageFolders, connection)[0];
417
+ if (packageFoldersError)
418
+ return { status: 400, body: { error: packageFoldersError } };
392
419
  await ensureAccess(ctx, state);
393
420
  // A source on the caller's OWN paired device: its owner with `device.own`
394
421
  // satisfies the whole attach stack (`sources.mount` included). On the host
@@ -397,12 +424,20 @@ async function handleSourcesCreate(req, state, preflight = false) {
397
424
  if (!ownDevice.allowed && !hasRouteFeature(req.session, 'sources.mount')) {
398
425
  return { status: 403, body: { error: 'Creating sources requires sources.mount.' } };
399
426
  }
400
- if (recognizesPackages !== undefined && !isOwnerOrAdminEffective(req.session)) {
401
- return { status: 403, body: { error: 'Setting recognizesPackages requires an owner/admin-effective role.' } };
402
- }
403
- if (recognizesPackages === true && connection.kind !== 'local') {
404
- return { status: 400, body: { error: 'recognizesPackages may only be true for local-kind sources.' } };
427
+ if (packageFolders !== undefined && !isOwnerOrAdminEffective(req.session)) {
428
+ return { status: 403, body: { error: 'Setting packageFolders requires an owner/admin-effective role.' } };
405
429
  }
430
+ // The root PAIR is derived HERE, before any gate, so every gate below judges
431
+ // the pair that will be stored and mounted on either plane.
432
+ const paired = admitSourceRoot({
433
+ connection: connection,
434
+ stack: state.ctx.platform.runtimeStack(),
435
+ mayHandSet: hasRouteFeature(req.session, PRIVILEGED_MOUNT_FEATURE),
436
+ audit: state.ctx.hostPorts?.audit,
437
+ });
438
+ if ('status' in paired)
439
+ return paired;
440
+ const pairedConnection = paired.connection;
406
441
  // fenced-courtyard (HEALTH1 B3) — privileged-root ADD gate. `sources.mount`
407
442
  // attaches ordinary sources, but attaching a source rooted IN, OVER, or AT a
408
443
  // protected host zone (App Zone, the Neuralis tree, the whole host) requires
@@ -413,46 +448,62 @@ async function handleSourcesCreate(req, state, preflight = false) {
413
448
  // `..` / trailing-slash / a Docker free-root attach with no suggestion). The
414
449
  // requested root is resolved with `resolveLocalRoot` — the SAME canonicaliser
415
450
  // the connector uses — so the gate sees exactly what will be mounted.
416
- // The HOST plane is decided BEFORE the root-sensitive container gate below:
451
+ // The DEVICE plane is decided BEFORE the root-sensitive container gate below:
417
452
  // the device's owner with `device.own`, nobody else, with no root-string
418
453
  // inspection and no `protectedRoots.length` precondition (fail closed even
419
454
  // when the runtime stack is unavailable; a sub-directory is gated like `/`).
420
- // The container-plane arm is kind-AGNOSTIC (see `missingAttachFeature`).
421
- const missingForAttach = await missingAttachFeature(req.session, connection, buildProtectedRoots(state.ctx.platform.runtimeStack()), async (root) => resolveLocalRoot({ configured: root, projectRoot: (await state.infraFactory.getInfra(projectId)).projectRoot ?? '', source }).resolvedRoot, ownDevice);
455
+ // The local-plane arm is kind-AGNOSTIC (see `missingAttachFeature`).
456
+ const missingForAttach = await missingAttachFeature(req.session, pairedConnection, buildProtectedRoots(state.ctx.platform.runtimeStack()), async (root, hostRoot) => resolveLocalRoot({
457
+ configured: root, hostRoot, projectRoot: (await state.infraFactory.getInfra(projectId)).projectRoot ?? '',
458
+ isDocker: state.ctx.platform.runtimeStack().isDocker, source,
459
+ }).resolvedRoot, ownDevice);
422
460
  if (missingForAttach) {
423
461
  return {
424
462
  status: 403,
425
463
  body: {
426
464
  error: missingForAttach.plane === 'device'
427
465
  ? "Only the device's owner may attach a source on it."
428
- : missingForAttach.plane === 'host'
429
- ? `Attaching a host-plane source requires the ${missingForAttach.feature} feature.`
466
+ : missingForAttach.plane === 'owner'
467
+ ? `Attaching a source on your device requires the ${missingForAttach.feature} feature.`
430
468
  : `Attaching a source rooted in a protected host zone requires the ${missingForAttach.feature} feature.`,
431
469
  },
432
470
  };
433
471
  }
472
+ const fencedAttach = await nativeFenceRefusal(state, projectId, source, pairedConnection);
473
+ if (fencedAttach)
474
+ return fencedAttach;
434
475
  const bridge = await resolveBridge(state, projectId);
435
476
  const scope = { userId: ctx.userId, projectId };
436
477
  const existing = await bridge.getSourceConfig(scope, source, toScopeAccessCtx(ctx));
437
478
  if (existing)
438
479
  return { status: 409, body: { error: `Source already exists: ${source}` } };
439
- const normalizedPerms = normalizePermissions(permissions);
480
+ const normalizedPerms = normalizePermissions(permissions, { rolePriorities: ctx.rolePriorities });
440
481
  const normalizedConnection = {
441
- kind: connection.kind,
442
- config: (connection.config ?? {}),
482
+ kind: pairedConnection.kind,
483
+ config: (pairedConnection.config ?? {}),
443
484
  };
444
485
  const kindMetadata = platformRegistries(state.ctx.platform).sourceKinds.get(normalizedConnection.kind);
445
486
  if (!kindMetadata)
446
487
  return { status: 400, body: { error: `Unknown source kind "${normalizedConnection.kind}"` } };
447
- const sourceScope = resolveRequestedSourceScope(requestedScope, kindMetadata.defaultScope, ctx);
448
- const allowedScopes = kindMetadata.allowedScopes ?? (kindMetadata.scopeSelectable ? ['project', 'user', 'agent'] : [kindMetadata.defaultScope]);
488
+ // A device folder defaults to the attaching owner's USER scope and is never
489
+ // agent-scoped: `files.read` is a member default, so a project-scoped folder on
490
+ // somebody's machine is readable project-wide the moment it exists — the owner
491
+ // widens it on purpose, never by omission.
492
+ const onDevice = sourcePlane(normalizedConnection.kind, normalizedConnection.config) === 'device';
493
+ const sourceScope = resolveRequestedSourceScope(requestedScope, onDevice ? 'user' : kindMetadata.defaultScope, ctx);
494
+ if (!sourceScope) {
495
+ return { status: 400, body: { code: 'invalid_scope', error: 'scope is "project", "user" or "agent", or an object with one of those kinds' } };
496
+ }
497
+ const allowedScopes = onDevice
498
+ ? DEVICE_FOLDER_SCOPES
499
+ : kindMetadata.allowedScopes ?? (kindMetadata.scopeSelectable ? ['project', 'user', 'agent'] : [kindMetadata.defaultScope]);
449
500
  if (!kindMetadata.scopeSelectable && sourceScope.kind !== kindMetadata.defaultScope) {
450
501
  return { status: 400, body: { error: `Kind "${kindMetadata.kind}" is project-scope only.` } };
451
502
  }
452
503
  if (!allowedScopes.includes(sourceScope.kind)) {
453
504
  return { status: 400, body: { error: `Scope "${sourceScope.kind}" is not allowed for kind "${kindMetadata.kind}".` } };
454
505
  }
455
- if (!canAccessScope(sourceScope, toScopeAccessCtx(ctx), 'write')) {
506
+ if (!canAccessScope(sourceScope, toScopeAccessCtx(ctx), 'write', sourceScopeOrigin(normalizedConnection))) {
456
507
  return { status: 403, body: { error: 'Forbidden for requested source scope' } };
457
508
  }
458
509
  // Multi-operation callers need every conditional feature refusal before an
@@ -470,8 +521,9 @@ async function handleSourcesCreate(req, state, preflight = false) {
470
521
  caller: toSyncCaller(ctx),
471
522
  ...(description !== undefined ? { description: description } : {}),
472
523
  ...(sync && isSyncConfig(sync) ? { sync: sync } : {}),
473
- ...(recognizesPackages !== undefined ? { recognizesPackages: recognizesPackages } : {}),
524
+ ...(packageFolders !== undefined ? { packageFolders: packageFolders } : {}),
474
525
  });
526
+ paired.auditHandSet?.(ctx.userId, projectId, config.source);
475
527
  const infra = await state.infraFactory.getInfra(projectId);
476
528
  return { status: 201, body: toPublicSourceConfig(config, await publicViewFor(req.session, ctx, config, state, infra)) };
477
529
  }
@@ -488,23 +540,6 @@ async function handleSourcesCreate(req, state, preflight = false) {
488
540
  function hasRouteFeature(session, featureId) {
489
541
  return hasFeature(session, featureId);
490
542
  }
491
- /**
492
- * Owner/admin-STRENGTH check (R2), keyed on the ordinal, never on a role NAME
493
- * (D-A). The built-in anchors are `owner 1 · admin 2 · manager 10 · member 20 ·
494
- * viewer 30` (lower = stronger, `SessionContext.priority`) and a custom role
495
- * declares its own number in 1..99 — so the admin ANCHOR is the threshold, and
496
- * a custom role at priority 1 or 2 passes exactly like the built-ins. A
497
- * wildcard feature grant (`'*'`) is owner-effective too, resolved through the
498
- * ONE kernel `hasFeature`. Used to gate code-LOAD surfaces like
499
- * `recognizesPackages`.
500
- */
501
- function isOwnerOrAdminEffective(session) {
502
- const s = session;
503
- const priority = typeof s?.priority === 'number' ? s.priority : undefined;
504
- if (priority !== undefined && priority <= BUILTIN_ROLE_PRIORITY.admin)
505
- return true;
506
- return hasRouteFeature(session, '*');
507
- }
508
543
  // ---------------------------------------------------------------------------
509
544
  // fenced-courtyard (HEALTH1 B3) — the per-discoverable-source access table.
510
545
  //
@@ -523,6 +558,24 @@ function isOwnerOrAdminEffective(session) {
523
558
  // ---------------------------------------------------------------------------
524
559
  /** Feature required to SEE/ADD a high-blast-radius host zone (C8 — no grant below the admin tier). */
525
560
  const PRIVILEGED_MOUNT_FEATURE = 'sources.privileged';
561
+ /**
562
+ * The DEVICE plane (a folder whose stored config names a `device` — a paired
563
+ * device, outside the container) has ONE attach rule: the device's owner, holding `device.own`,
564
+ * attaches or edits a source on it, and nobody else does. agent-core's device
565
+ * gate (`devices.mayAttach`) answers both halves; brain-core keeps no device-plane
566
+ * feature of its own. `sources.mount` is a MANAGER default and never reaches a
567
+ * device: a non-owner is refused whatever the role grants, so a manager cannot
568
+ * attach someone's machine at `/` and get read/write over it through `fs_*`.
569
+ * That device's own allowlist (`neuralis device grant`) still bounds what the
570
+ * owner's source can reach.
571
+ *
572
+ * The plane is the kernel's `sourcePlane` (`@neuralis/package-system/contracts`)
573
+ * over the STORED connection — imported, never re-declared. Two copies of a plane
574
+ * predicate is how the attach gate and the terminal gate drift apart while both
575
+ * look correct in isolation.
576
+ */
577
+ /** The scopes a device folder may be attached at — never `agent` (see `handleSourcesCreate`). */
578
+ const DEVICE_FOLDER_SCOPES = ['user', 'project'];
526
579
  /**
527
580
  * Build the filter-free protected-root table from the runtime stack. App Zone,
528
581
  * the Neuralis source tree, and the whole host (`/`) all require
@@ -539,16 +592,22 @@ export function buildProtectedRoots(stack) {
539
592
  }
540
593
  /**
541
594
  * Containment in BOTH directions, canonical paths. Fires when the requested
542
- * root IS, is UNDER, or is an ANCESTOR of a protected root — except `/`, which
543
- * matches only an exact whole-host request (it is everyone's ancestor).
595
+ * root IS, is UNDER, or is an ANCESTOR of a protected root — except a
596
+ * filesystem root (`/`, a Windows drive root), which matches only a whole-host
597
+ * request (it is everyone's ancestor): `/`, or any drive root (`D:\`).
598
+ *
599
+ * The comparison runs in the path flavour of the zone itself: a protected root
600
+ * is a canonical host path, so a drive-letter or UNC form IS a Windows host —
601
+ * backslashes, drive roots and case-insensitive names.
544
602
  */
545
603
  function rootMatches(requested, protectedRoot, mode = 'containment') {
546
- if (protectedRoot === '/')
547
- return requested === '/';
548
- if (requested === protectedRoot)
549
- return true;
550
- if (requested.startsWith(`${protectedRoot}/`))
551
- return true; // requested is under the zone (subpath attach)
604
+ const p = hostPathFlavour(protectedRoot);
605
+ if (isFilesystemRoot(protectedRoot))
606
+ return isFilesystemRoot(requested);
607
+ if (!p.isAbsolute(requested))
608
+ return false;
609
+ if (isPathInside(protectedRoot, requested, p))
610
+ return true; // IS the zone, or under it (subpath attach)
552
611
  // The ANCESTOR arm belongs to ATTACH, not to enumeration. Attaching `/home`
553
612
  // hands over everything below it, so exposing a zone from above is the same
554
613
  // act; LISTING `/home` one level deep does not, and the browse handler drops
@@ -558,7 +617,7 @@ function rootMatches(requested, protectedRoot, mode = 'containment') {
558
617
  // opens at `/`, so the first click would deny with no route downward. The
559
618
  // zone itself stays protected: `is-or-under` still fires on it, and the
560
619
  // handler filters protected CHILDREN out of the listing it returns.
561
- if (mode === 'containment' && protectedRoot.startsWith(`${requested}/`))
620
+ if (mode === 'containment' && isPathInside(requested, protectedRoot, p))
562
621
  return true;
563
622
  return false;
564
623
  }
@@ -586,40 +645,41 @@ export function requiredFeaturesForRoot(root, protectedRoots, kind, mode = 'cont
586
645
  * the listing projection, so "you see the path you could attach" cannot drift
587
646
  * from what attaching requires.
588
647
  *
589
- * - Host plane: the DEVICE's owner holding `device.own` (`ownDevice`,
648
+ * - Device plane: the DEVICE's owner holding `device.own` (`ownDevice`,
590
649
  * agent-core's device gate) and nobody else — a non-owner is refused as
591
650
  * `device`, an owner without `device.own` is told the feature it lacks; no
592
651
  * root inspection and no runtime-stack precondition (fail closed).
593
- * - Container plane, kind-AGNOSTIC: any kind whose config carries a string
594
- * `root` — `local`, and every root-bearing kind a package registers later —
652
+ * - Local plane, kind-AGNOSTIC: any kind whose config carries a string
653
+ * `root` — the in-process folder, and every root-bearing kind a package registers later —
595
654
  * resolves through `resolveRoot` (the connector's own canonicaliser) and hits
596
655
  * the SAME containment table. Scoping this to one kind is how a newly
597
656
  * registered root-bearing kind walks straight past the gate.
598
657
  */
599
658
  async function missingAttachFeature(session, connection, protectedRoots, resolveRoot, ownDevice) {
600
- if (isHostPlaneKind(connection.kind)) {
659
+ if (sourcePlane(connection.kind, connection.config) === 'device') {
601
660
  if (ownDevice.allowed)
602
661
  return null;
603
- return { feature: 'device.own', plane: ownDevice.owner ? 'host' : 'device' };
662
+ return { feature: 'device.own', plane: ownDevice.owner ? 'owner' : 'device' };
604
663
  }
605
664
  if (protectedRoots.length === 0)
606
665
  return null;
607
- const root = connection.config?.root;
666
+ const { root, hostRoot } = (connection.config ?? {});
608
667
  if (typeof root !== 'string' || root.length === 0)
609
668
  return null;
610
- const required = requiredFeaturesForRoot(await resolveRoot(root), protectedRoots, 'add');
669
+ // Only the in-process folder reads the pair; another root-bearing kind is judged on `root`.
670
+ const required = requiredFeaturesForRoot(await resolveRoot(root, connection.kind === HOST_SOURCE_KIND ? hostRoot : undefined), protectedRoots, 'add');
611
671
  const feature = required.find((f) => !hasRouteFeature(session, f));
612
672
  return feature ? { feature, plane: 'zone' } : null;
613
673
  }
614
674
  /**
615
675
  * Ask agent-core's ONE device gate (`devices.mayAttach`) about the device a
616
- * host-plane connection names. brain-core reads `config.device` as an opaque
676
+ * device-plane connection names. brain-core reads `config.device` as an opaque
617
677
  * string and knows no device semantics; no agent-core, no device or a
618
678
  * non-host kind ⇒ `{ allowed: false, owner: false }` (fail closed).
619
679
  */
620
680
  function deviceAttachVerdict(state, ctx, connection) {
621
681
  const denied = { allowed: false, owner: false };
622
- if (!isHostPlaneKind(connection.kind))
682
+ if (sourcePlane(connection.kind, connection.config) !== 'device')
623
683
  return denied;
624
684
  const device = connection.config?.device;
625
685
  if (typeof device !== 'string' || !device)
@@ -629,11 +689,110 @@ function deviceAttachVerdict(state, ctx, connection) {
629
689
  projectId: ctx.projectId,
630
690
  ...(ctx.agentId ? { agentId: ctx.agentId } : {}),
631
691
  callerId: '@neuralis/brain-core',
632
- callerTrust: 'first-party',
633
- grantedFeatures: ctx.grantedFeatures,
692
+ callerTrust: 'native',
693
+ grantedFeatures: [...ctx.grantedFeatures],
634
694
  };
635
695
  const agentCore = state.ctx.packages?.getPackageApi('@neuralis/agent-core', session);
636
- return agentCore?.devices?.mayAttach({ userId: ctx.userId, grantedFeatures: ctx.grantedFeatures }, device) ?? denied;
696
+ return agentCore?.devices?.mayAttach({ userId: ctx.userId, grantedFeatures: [...ctx.grantedFeatures] }, device) ?? denied;
697
+ }
698
+ /** The 403 text of a refused edit — one wording for the route PATCH and the cross-package writer. */
699
+ function editRefusalMessage(missing) {
700
+ return missing.plane === 'device'
701
+ ? "Only the device's owner may edit a source on it."
702
+ : missing.plane === 'owner'
703
+ ? `Editing a source on your device requires the ${missing.feature} feature.`
704
+ : `Editing a source rooted in a protected host zone requires the ${missing.feature} feature.`;
705
+ }
706
+ /**
707
+ * The edit gates of a source PATCH, in order — ONE ladder for every
708
+ * source-config writer (the route PATCH, `POST policies/uri`, a widening
709
+ * `POST sources/enabled`, and the cross-package writer `sourceEditFence`), so
710
+ * none can skip a rung:
711
+ * the source's scope (another member's user/agent source needs
712
+ * `files.writeOthers`), the immutable `floor:true` rows (409 when dropped or
713
+ * altered), and what attaching it needs (`missingAttachFeature`: a device folder
714
+ * is its owner's alone, a protected-zone root needs `sources.privileged`).
715
+ * The refusal the caller answers, or null.
716
+ */
717
+ export async function sourceEditRefusal(state, editor, existing, nextPermissions, ownDevice = deviceAttachVerdict(state, editor, existing.connection)) {
718
+ const grants = { grantedFeatures: [...editor.grantedFeatures] };
719
+ if (!canAccessScope(existing.scope, { userId: editor.userId, ...(editor.agentId ? { agentId: editor.agentId } : {}), grantedFeatures: grants.grantedFeatures }, 'write', sourceScopeOrigin(existing.connection))) {
720
+ return { status: 403, body: { error: 'Forbidden for source scope' } };
721
+ }
722
+ try {
723
+ assertImmutablePolicyFloorsPreserved(existing.permissions, nextPermissions);
724
+ }
725
+ catch (err) {
726
+ if (err instanceof ImmutablePolicyFloorError)
727
+ return { status: 409, body: { code: err.code, error: err.message } };
728
+ throw err;
729
+ }
730
+ // PATCH cannot change `connection` (its `device` included — a transport flip is
731
+ // a delete and a re-attach, never an edit), but it CAN widen an existing device-plane
732
+ // source's permissions (read → write). Editing one therefore needs what
733
+ // attaching one does — the device's owner with `device.own` — otherwise a
734
+ // plain-`sources.mount` manager could escalate a read-only host source in place.
735
+ // Same rule for the local plane: a PATCH can widen permissions (read →
736
+ // write) on a source already rooted in a protected zone, so editing one
737
+ // requires the same grants attaching one does — for EVERY root-bearing kind,
738
+ // not just the folder kind.
739
+ const stack = state.ctx.platform.runtimeStack();
740
+ const editJudged = await unlessAnchorUnresolvable(missingAttachFeature(grants, existing.connection, buildProtectedRoots(stack), async (root, hostRoot) => resolveLocalRoot({
741
+ configured: root, hostRoot, projectRoot: (await state.infraFactory.getInfra(editor.projectId)).projectRoot ?? '',
742
+ isDocker: stack.isDocker, source: existing.source,
743
+ }).resolvedRoot, ownDevice));
744
+ // An anchored folder this plane cannot open is never edited (fail closed, nothing written).
745
+ if (!editJudged)
746
+ return { status: 400, body: SOURCE_ANCHOR_UNRESOLVABLE };
747
+ return editJudged.value ? { status: 403, body: { error: editRefusalMessage(editJudged.value) } } : null;
748
+ }
749
+ /**
750
+ * The native folder fence for an attach or an edit (`domain/access/nativeLocalFence.ts`):
751
+ * the 403 a fenced root answers, or null. Judged on the root as the connector
752
+ * resolves it; a device folder or another kind ⇒ never this gate's.
753
+ */
754
+ export async function nativeFenceRefusal(state, projectId, source, connection) {
755
+ if (sourcePlane(connection.kind, connection.config) !== 'local')
756
+ return null;
757
+ const stack = state.ctx.platform.runtimeStack();
758
+ if (!nativeLocalFenceApplies(stack))
759
+ return null;
760
+ const projectRoot = (await state.infraFactory.getInfra(projectId)).projectRoot ?? '';
761
+ const { root, hostRoot } = (connection.config ?? {});
762
+ const refused = await nativeLocalRootRefused({
763
+ stack,
764
+ connection,
765
+ resolvedRoot: resolveLocalRoot({ configured: typeof root === 'string' ? root : '', hostRoot, projectRoot, isDocker: stack.isDocker, source }).resolvedRoot,
766
+ projectRoot,
767
+ privilegedRoots: buildProtectedRoots(stack).map((p) => p.root),
768
+ });
769
+ return refused ? { status: 403, body: NATIVE_LOCAL_REFUSAL } : null;
770
+ }
771
+ /**
772
+ * The route PATCH's edit gates on the cross-package write (`upsertSourceConfig`):
773
+ * the SAME `sourceEditRefusal` ladder (scope, immutable floors, device owner,
774
+ * protected zone) with the route's codes and texts, then the native fence, which
775
+ * refuses only an edit that widens a fenced source; an unjudgeable shape is refused.
776
+ */
777
+ export function sourceEditFence(state) {
778
+ return async (caller, existing, next) => {
779
+ const refused = await sourceEditRefusal(state, caller, existing, next.permissions);
780
+ if (refused)
781
+ return { status: refused.status, error: refused.body.error, ...(refused.body.code ? { code: refused.body.code } : {}) };
782
+ if (!(await nativeFenceRefusal(state, caller.projectId, existing.source, existing.connection)))
783
+ return null;
784
+ let widens = true;
785
+ try {
786
+ widens = fencedEditWidens(existing, {
787
+ permissions: next.permissions,
788
+ ...(next.sync ? { sync: next.sync } : {}),
789
+ });
790
+ }
791
+ catch {
792
+ widens = true;
793
+ }
794
+ return widens ? { status: 403, error: NATIVE_LOCAL_EDIT_REFUSAL.error } : null;
795
+ };
637
796
  }
638
797
  /**
639
798
  * May this caller READ the source's root — the same per-uri read predicate the
@@ -653,21 +812,31 @@ async function rootReadable(infra, ctx, source, configCache) {
653
812
  * nothing; the raw last-sync error goes to a `sources.sync` holder, the tier
654
813
  * `sources/sync-status` answers to. A feature, never a role name.
655
814
  */
656
- async function sourceConfigView(session, config, projectRoot, protectedRoots, readable, ownDevice, defaultSyncExclude) {
815
+ async function sourceConfigView(session, config, projectRoot, isDocker, protectedRoots, readable, ownDevice, defaultSyncExclude) {
816
+ const resolve = (root, hostRoot) => resolveLocalRoot({ configured: root, hostRoot, projectRoot, isDocker, source: config.source }).resolvedRoot;
817
+ // A stored anchor this plane cannot open is a folder nobody may attach here
818
+ // (and it opens nowhere) — never an error that empties the whole listing.
657
819
  const mayAttach = (ownDevice.allowed || hasRouteFeature(session, 'sources.mount'))
658
- && await missingAttachFeature(session, config.connection, protectedRoots, (root) => resolveLocalRoot({ configured: root, projectRoot, source: config.source }).resolvedRoot, ownDevice) === null;
820
+ && (await unlessAnchorUnresolvable(missingAttachFeature(session, config.connection, protectedRoots, resolve, ownDevice)))?.value === null;
821
+ // The folder THIS plane opens — natively the pair's machine half, never the container spelling.
822
+ const { root, hostRoot } = config.connection.config ?? {};
823
+ const inProcess = sourcePlane(config.connection.kind, config.connection.config) === 'local';
824
+ const rootEffective = inProcess && typeof root === 'string' && root
825
+ ? resolvedLocalRootOrNull({ configured: root, hostRoot, projectRoot, isDocker, source: config.source }) ?? undefined
826
+ : undefined;
659
827
  return { mayReadPaths: mayAttach || readable, maySeeSyncErrors: hasRouteFeature(session, 'sources.sync'),
660
- ...(defaultSyncExclude ? { defaultSyncExclude } : {}) };
828
+ ...(defaultSyncExclude ? { defaultSyncExclude } : {}), ...(rootEffective ? { rootEffective } : {}) };
661
829
  }
662
830
  /** `sourceConfigView` for a single-row answer (single GET, mutation echoes). */
663
831
  async function publicViewFor(session, ctx, config, state, infra) {
664
832
  const readable = await rootReadable(infra, ctx, config.source, new Map([[config.source, config]]));
833
+ const stack = state.ctx.platform.runtimeStack();
665
834
  const matched = !config.sync ? matchDeclaredSource(config, state.ctx.platform.packageSourceDeclarations(), {
666
- projectRoot: infra?.projectRoot ?? '', appRoot: state.ctx.platform.runtimeStack().appRoot,
835
+ projectRoot: infra?.projectRoot ?? '', appRoot: stack.appRoot, isDocker: stack.isDocker,
667
836
  }) : null;
668
837
  const defaults = matched && isOwnDataZoneDeclaration(matched.originPackage, matched.decl, state.ctx.selfId)
669
838
  ? seedExcludedMachineWrittenGlobs(state.ctx.platform.machineWrittenPaths()) : undefined;
670
- return sourceConfigView(session, config, infra?.projectRoot ?? '', buildProtectedRoots(state.ctx.platform.runtimeStack()), readable, deviceAttachVerdict(state, ctx, config.connection), defaults);
839
+ return sourceConfigView(session, config, infra?.projectRoot ?? '', stack.isDocker, buildProtectedRoots(stack), readable, deviceAttachVerdict(state, ctx, config.connection), defaults);
671
840
  }
672
841
  function filterSourceForList(config, ctx, requestedScope, includeObserved) {
673
842
  if (requestedScope && config.scope.kind !== requestedScope)
@@ -677,10 +846,21 @@ function filterSourceForList(config, ctx, requestedScope, includeObserved) {
677
846
  return false;
678
847
  return true;
679
848
  }
849
+ /**
850
+ * The create body's `scope`: a kind name, or an object. A complete object is taken as
851
+ * written (a foreign `userId` then meets the write `canAccessScope`); an object naming
852
+ * only a valid `kind` gets the caller's ids, like the name; any other object or name is
853
+ * `null` (400 `invalid_scope`) — never silently the kind default. Absent = the default.
854
+ */
680
855
  function resolveRequestedSourceScope(raw, defaultScope, ctx) {
681
856
  if (isSourceScope(raw))
682
857
  return raw;
683
- const requested = raw === 'project' || raw === 'user' || raw === 'agent' ? raw : defaultScope;
858
+ const named = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw.kind : raw;
859
+ if (raw && typeof raw === 'object' && named !== 'project' && named !== 'user' && named !== 'agent')
860
+ return null;
861
+ if (typeof raw === 'string' && raw !== 'project' && raw !== 'user' && raw !== 'agent')
862
+ return null;
863
+ const requested = named === 'project' || named === 'user' || named === 'agent' ? named : defaultScope;
684
864
  if (requested === 'user')
685
865
  return { kind: 'user', userId: ctx.userId };
686
866
  if (requested === 'agent')
@@ -700,7 +880,7 @@ async function handleSourcesUpdate(req, state) {
700
880
  const description = body?.description;
701
881
  // `null` = absent. A read-then-resend caller (the manage-sources skill's jq
702
882
  // projection) legitimately round-trips a field the listing never carried.
703
- const recognizesPackages = body?.recognizesPackages ?? undefined;
883
+ const packageFolders = body?.packageFolders ?? undefined;
704
884
  if (!source)
705
885
  return { status: 400, body: { error: 'Missing source' } };
706
886
  if (description !== undefined && typeof description !== 'string') {
@@ -713,15 +893,15 @@ async function handleSourcesUpdate(req, state) {
713
893
  if (enabledRaw !== undefined && typeof enabledRaw !== 'boolean' && enabledRaw !== 'auto') {
714
894
  return { status: 400, body: { error: 'enabled must be boolean or "auto" if provided' } };
715
895
  }
716
- if (recognizesPackages !== undefined && typeof recognizesPackages !== 'boolean') {
717
- return { status: 400, body: { error: 'recognizesPackages must be a boolean if provided' } };
896
+ if (packageFolders !== undefined && !Array.isArray(packageFolders)) {
897
+ return { status: 400, body: { error: 'packageFolders must be an array of folder paths if provided' } };
718
898
  }
719
899
  await ensureAccess(ctx, state);
720
- // R2 (architect MINOR-2) — flagging a source `recognizesPackages` creates a
721
- // code-LOAD surface, so the WRITE is owner/admin-effective only, even though
722
- // other source fields are sources.mount. Deny-by-default.
723
- if (recognizesPackages !== undefined && !isOwnerOrAdminEffective(req.session)) {
724
- return { status: 403, body: { error: 'Setting recognizesPackages requires an owner/admin-effective role.' } };
900
+ // R2 (architect MINOR-2) — a `packageFolders` entry creates a code-LOAD
901
+ // surface, so the WRITE is owner/admin-effective only, even though other
902
+ // source fields are sources.mount. Deny-by-default.
903
+ if (packageFolders !== undefined && !isOwnerOrAdminEffective(req.session)) {
904
+ return { status: 403, body: { error: 'Setting packageFolders requires an owner/admin-effective role.' } };
725
905
  }
726
906
  const bridge = await resolveBridge(state, projectId);
727
907
  const scope = { userId: ctx.userId, projectId };
@@ -732,43 +912,23 @@ async function handleSourcesUpdate(req, state) {
732
912
  if (!ownDevice.allowed && !hasRouteFeature(req.session, 'sources.mount')) {
733
913
  return { status: 403, body: { error: 'Updating sources requires sources.mount.' } };
734
914
  }
735
- if (!canAccessScope(existing.scope, toScopeAccessCtx(ctx), 'write')) {
736
- return { status: 403, body: { error: 'Forbidden for source scope' } };
915
+ const refused = await sourceEditRefusal(state, ctx, existing, permissions, ownDevice);
916
+ if (refused)
917
+ return refused;
918
+ const packageFoldersError = packageFolders === undefined ? undefined : validatePackageFolders(packageFolders, existing.connection)[0];
919
+ if (packageFoldersError)
920
+ return { status: 400, body: { error: packageFoldersError } };
921
+ const normalizedPerms = normalizePermissions(permissions, { rolePriorities: ctx.rolePriorities, existing: existing.permissions });
922
+ // A source the native fence refuses (it predates the fence) may still be
923
+ // narrowed — disabled, a right dropped, described — never widened.
924
+ if (await nativeFenceRefusal(state, projectId, source, existing.connection) && fencedEditWidens(existing, {
925
+ permissions: normalizedPerms, clearSync,
926
+ ...(sync ? { sync } : {}),
927
+ ...(enabledRaw !== undefined ? { enabled: enabledRaw } : {}),
928
+ ...(packageFolders !== undefined ? { packageFolders: packageFolders } : {}),
929
+ })) {
930
+ return { status: 403, body: NATIVE_LOCAL_EDIT_REFUSAL };
737
931
  }
738
- try {
739
- assertImmutablePolicyFloorsPreserved(existing.permissions, permissions);
740
- }
741
- catch (err) {
742
- if (err instanceof ImmutablePolicyFloorError) {
743
- return { status: 409, body: { code: err.code, error: err.message } };
744
- }
745
- throw err;
746
- }
747
- if (recognizesPackages === true && existing.connection.kind !== 'local') {
748
- return { status: 400, body: { error: 'recognizesPackages may only be true for local-kind sources.' } };
749
- }
750
- // PATCH cannot change `connection`, but it CAN widen an existing host-plane
751
- // source's permissions (read → write). Editing one therefore needs what
752
- // attaching one does — the device's owner with `device.own` — otherwise a
753
- // plain-`sources.mount` manager could escalate a read-only host source in place.
754
- // Same rule for the container plane: a PATCH can widen permissions (read →
755
- // write) on a source already rooted in a protected zone, so editing one
756
- // requires the same grants attaching one does — for EVERY root-bearing kind,
757
- // not just `local`.
758
- const missingForEdit = await missingAttachFeature(req.session, existing.connection, buildProtectedRoots(state.ctx.platform.runtimeStack()), async (root) => resolveLocalRoot({ configured: root, projectRoot: (await state.infraFactory.getInfra(projectId)).projectRoot ?? '', source }).resolvedRoot, ownDevice);
759
- if (missingForEdit) {
760
- return {
761
- status: 403,
762
- body: {
763
- error: missingForEdit.plane === 'device'
764
- ? "Only the device's owner may edit a source on it."
765
- : missingForEdit.plane === 'host'
766
- ? `Editing a host-plane source requires the ${missingForEdit.feature} feature.`
767
- : `Editing a source rooted in a protected host zone requires the ${missingForEdit.feature} feature.`,
768
- },
769
- };
770
- }
771
- const normalizedPerms = normalizePermissions(permissions);
772
932
  try {
773
933
  const updated = await bridge.updateSourceConfig(scope, {
774
934
  agentId,
@@ -778,7 +938,7 @@ async function handleSourcesUpdate(req, state) {
778
938
  ...(sync ? { sync } : {}),
779
939
  ...(clearSync ? { clearSync: true } : {}),
780
940
  ...(enabledRaw !== undefined ? { enabled: enabledRaw } : {}),
781
- ...(recognizesPackages !== undefined ? { recognizesPackages: recognizesPackages } : {}),
941
+ ...(packageFolders !== undefined ? { packageFolders: packageFolders } : {}),
782
942
  });
783
943
  const infra = await state.infraFactory.getInfra(projectId);
784
944
  return { status: 200, body: toPublicSourceConfig(updated, await publicViewFor(req.session, ctx, updated, state, infra)) };
@@ -816,9 +976,20 @@ async function handleSourcesEnabled(req, state) {
816
976
  }
817
977
  if (!existing)
818
978
  return { status: 404, body: { error: `Source not found: ${source}` } };
819
- if (!canAccessScope(existing.scope, toScopeAccessCtx(ctx), 'write')) {
979
+ if (!canAccessScope(existing.scope, toScopeAccessCtx(ctx), 'write', sourceScopeOrigin(existing.connection))) {
820
980
  return { status: 403, body: { error: 'Forbidden for source scope' } };
821
981
  }
982
+ // Turning a source ON widens its reach, so it runs the edit ladder (a device
983
+ // folder is its owner's alone, a protected zone needs its feature). Turning it
984
+ // off narrows and stays the `sources.mount` holder's.
985
+ if (fencedEditWidens(existing, { enabled })) {
986
+ const refused = await sourceEditRefusal(state, ctx, existing, existing.permissions);
987
+ if (refused)
988
+ return refused;
989
+ if (await nativeFenceRefusal(state, ctx.projectId, source, existing.connection)) {
990
+ return { status: 403, body: NATIVE_LOCAL_EDIT_REFUSAL };
991
+ }
992
+ }
822
993
  try {
823
994
  const updated = await bridge.setSourceEnabled(scope, { source, enabled });
824
995
  const infra = await state.infraFactory.getInfra(ctx.projectId);
@@ -841,7 +1012,7 @@ async function handleSourcesDelete(slug, req, state) {
841
1012
  }
842
1013
  if (!match)
843
1014
  return { status: 404, body: { error: `Source not found: ${slug}` } };
844
- if (!canAccessScope(match.scope, toScopeAccessCtx(ctx), 'write')) {
1015
+ if (!canAccessScope(match.scope, toScopeAccessCtx(ctx), 'write', sourceScopeOrigin(match.connection))) {
845
1016
  return { status: 403, body: { error: 'Forbidden for source scope' } };
846
1017
  }
847
1018
  const purgeRaw = req.query?.purge;
@@ -897,7 +1068,9 @@ async function handleSourcesSync(req, state) {
897
1068
  if (!config.sync) {
898
1069
  return { status: 400, body: { error: `Source "${config.source}" has no sync configuration. Add one before triggering a sync.` } };
899
1070
  }
900
- const job = await bridge.triggerSourceSync(scope, { agentId, source, caller: toSyncCaller(ctx) });
1071
+ // `confirmEmpty` lets ONE full pass tombstone a source whose root now holds nothing.
1072
+ const confirmEmpty = body?.confirmEmpty === true;
1073
+ const job = await bridge.triggerSourceSync(scope, { agentId, source, caller: toSyncCaller(ctx), ...(confirmEmpty ? { confirmEmpty } : {}) });
901
1074
  return { status: 202, body: job };
902
1075
  }
903
1076
  /**
@@ -1139,20 +1312,16 @@ async function handleSourcesReset(req, state) {
1139
1312
  * suggested kind + config.
1140
1313
  *
1141
1314
  * Suggested kinds today:
1142
- * - `app` — `{neuralisHome}/app/` (admin-only zone; per W3I the
1143
- * admin package will register a dedicated SourceKind in
1144
- * a follow-up commit; until then we suggest the path
1145
- * with `kind: 'local'` so the existing add-source flow
1146
- * works).
1315
+ * - `app` — `{neuralisHome}/app/` (admin-only zone), suggested as a
1316
+ * `host` source.
1147
1317
  * - `neuralis` — the downloaded Neuralis tree
1148
1318
  * (`/home/<user>/projects/Neuralis` on host installs,
1149
- * `os://repo` mount inside Docker). Same caveat: full
1150
- * registration happens later; today we surface the path
1151
- * with `kind: 'local'`.
1319
+ * `os://repo` mount inside Docker), suggested as a `host`
1320
+ * source.
1152
1321
  * - `projects` — the per-project zone next door (`{neuralisHome}/projects`).
1153
1322
  *
1154
1323
  * Already-attached paths are filtered out by comparing each suggestion's
1155
- * resolved root against the persisted source configs' `connection.config.root`.
1324
+ * root against every persisted source's root as THIS plane resolves it.
1156
1325
  */
1157
1326
  async function handleSourcesDiscoverable(req, state) {
1158
1327
  const ctx = extractCtx(req);
@@ -1172,11 +1341,17 @@ async function handleSourcesDiscoverable(req, state) {
1172
1341
  const scope = { userId: ctx.userId, projectId: ctx.projectId };
1173
1342
  const access = toScopeAccessCtx(ctx);
1174
1343
  const existing = await bridge.listSourceConfigs(scope, access);
1344
+ // Compared on the root THIS plane opens, so a natively attached folder (stored
1345
+ // under its container spelling) is not offered again.
1346
+ const attachedProjectRoot = (await state.infraFactory.getInfra(ctx.projectId)).projectRoot ?? '';
1175
1347
  const attachedRoots = new Set();
1176
1348
  for (const config of existing) {
1177
1349
  const root = config.connection?.config?.root?.trim();
1178
- if (root)
1179
- attachedRoots.add(root);
1350
+ const attached = root
1351
+ ? resolvedLocalRootOrNull({ configured: root, hostRoot: config.connection.config.hostRoot, projectRoot: attachedProjectRoot, isDocker: stack.isDocker, source: config.source })
1352
+ : null;
1353
+ if (attached !== null)
1354
+ attachedRoots.add(attached);
1180
1355
  }
1181
1356
  const suggestions = [];
1182
1357
  // Declarative `seed:'discoverable'` sources (modular-brewing-sutherland) —
@@ -1210,7 +1385,7 @@ async function handleSourcesDiscoverable(req, state) {
1210
1385
  ...(def ? { suggestedAccess: { read: !!def.read, write: !!def.write, exec: !!def.exec } } : {}),
1211
1386
  ...(decl.visibility?.seeRequires?.length ? { seeRequires: decl.visibility.seeRequires } : {}),
1212
1387
  };
1213
- if (decl.kind === 'local') {
1388
+ if (decl.kind === HOST_SOURCE_KIND) {
1214
1389
  let resolvedRoot = null;
1215
1390
  let hostRoot;
1216
1391
  if (decl.root === '${appRoot}') {
@@ -1263,10 +1438,16 @@ async function handleSourcesDiscoverable(req, state) {
1263
1438
  // shape `neuralis-operations` prescribes), and the whole-tree proposal
1264
1439
  // stays available for source work but says what it exposes and pre-fills a
1265
1440
  // `.env*` read-deny.
1441
+ //
1442
+ // A native host has no overlay and no container: its packaged operator
1443
+ // skills sit in its OWN tree (`stack.neuralisTree`, the Neuralis tree the
1444
+ // privileged-root table names there), so the skills proposal is rooted
1445
+ // there — the whole-tree proposal stays the overlay's.
1266
1446
  const neuralisMount = stack.osMounts.find((m) => m.key === 'neuralis');
1267
1447
  const neuralisRoot = neuralisMount?.containerPath ?? null;
1268
- if (neuralisRoot) {
1269
- const skillsRoot = resolvePath(neuralisRoot, 'skills');
1448
+ const skillsTree = neuralisRoot ?? (stack.isDocker ? null : stack.neuralisTree);
1449
+ if (skillsTree) {
1450
+ const skillsRoot = resolvePath(skillsTree, 'skills');
1270
1451
  // Guarded: an overlay rooted somewhere without a `skills/` folder must
1271
1452
  // not advertise a directory that is not there — silent-zero-index is the
1272
1453
  // exact trap this suggestion exists to remove.
@@ -1275,12 +1456,13 @@ async function handleSourcesDiscoverable(req, state) {
1275
1456
  id: 'discover:neuralis-skills',
1276
1457
  label: 'Neuralis Skills',
1277
1458
  description: "The deployment's own operator skills (`neuralis/skills/`). Each SKILL.md folder is a discovered source package, so the operator-command vocabulary reaches an agent's context by itself.",
1278
- suggestedKind: 'local',
1459
+ suggestedKind: HOST_SOURCE_KIND,
1279
1460
  suggestedScheme: 'neuralis-skills',
1280
- suggestedConfig: {
1281
- root: skillsRoot,
1282
- ...(neuralisMount?.hostPath ? { hostRoot: resolvePath(neuralisMount.hostPath, 'skills') } : {}),
1283
- },
1461
+ // The LOGICAL root — the tree's `skills/` on whichever plane opens the
1462
+ // source (the overlay in Docker, the host's own tree natively) — so the
1463
+ // attach survives a move between the two; the machine half derives
1464
+ // from the overlay's mount at bind time.
1465
+ suggestedConfig: { root: `${NEURALIS_TREE_ANCHOR}skills` },
1284
1466
  root: skillsRoot,
1285
1467
  icon: 'BookOpen',
1286
1468
  suggestedScope: 'user',
@@ -1299,20 +1481,17 @@ async function handleSourcesDiscoverable(req, state) {
1299
1481
  // trigger, which rides the contract rather than the form's old
1300
1482
  // hard-coded literal.
1301
1483
  suggestedSync: { trigger: 'manual' },
1302
- note: 'Owner-only. Mounted via `pnpm neuralis:mount add … --neuralis`.',
1484
+ note: neuralisRoot ? 'Owner-only. Mounted via `pnpm neuralis:mount add … --neuralis`.' : "Owner-only. This install's own packaged skills (read-only).",
1303
1485
  });
1304
1486
  }
1305
- if (!attachedRoots.has(neuralisRoot)) {
1487
+ if (neuralisRoot && !attachedRoots.has(neuralisRoot)) {
1306
1488
  suggestions.push({
1307
1489
  id: 'discover:neuralis',
1308
1490
  label: 'Neuralis Tree',
1309
1491
  description: "The deployment's own `neuralis/` folder, live from the host (scripts/, skills/, src/, config). Holds `.env` — attach it only for source work; prefer Neuralis Skills to give an agent the operator commands.",
1310
- suggestedKind: 'local',
1492
+ suggestedKind: HOST_SOURCE_KIND,
1311
1493
  suggestedScheme: 'neuralis',
1312
- suggestedConfig: {
1313
- root: neuralisRoot,
1314
- ...(neuralisMount?.hostPath ? { hostRoot: neuralisMount.hostPath } : {}),
1315
- },
1494
+ suggestedConfig: { root: NEURALIS_TREE_ANCHOR },
1316
1495
  root: neuralisRoot,
1317
1496
  icon: 'GitBranch',
1318
1497
  // Privileged host zone — default to USER scope so it is not shared
@@ -1355,7 +1534,7 @@ async function handleSourcesDiscoverable(req, state) {
1355
1534
  id: 'discover:projects',
1356
1535
  label: 'Projects Zone',
1357
1536
  description: '`{neuralisHome}/projects/` — sibling of the active project workspace.',
1358
- suggestedKind: 'local',
1537
+ suggestedKind: HOST_SOURCE_KIND,
1359
1538
  suggestedScheme: 'local',
1360
1539
  suggestedConfig: {
1361
1540
  root: stack.projectsRoot,
@@ -1365,22 +1544,30 @@ async function handleSourcesDiscoverable(req, state) {
1365
1544
  icon: 'FolderTree',
1366
1545
  });
1367
1546
  }
1368
- // Full host — only in native (non-Docker) mode, where `/` IS the host root
1369
- // and is directly readable. In Docker the host is invisible unless bound
1370
- // (`pnpm neuralis:mount add / --slug host`), which then surfaces via the
1371
- // generic mount loop below. Opt-in, gated by `sources.privileged` (no
1372
- // grant below the `admin` tier), attach READ-ONLY by default
1373
- // (suggestedAccess) — the whole-host blast radius warrants deny-by-default;
1374
- // the attacher narrows the uri-policy (or widens write) before use.
1375
- if (!stack.isDocker && !attachedRoots.has('/')) {
1547
+ // Full host — only in native (non-Docker) mode, where the filesystem root IS
1548
+ // the host root and is directly readable: `/`, on Windows each drive root a
1549
+ // zone lives on (`C:\` — `/` there means "the current drive"). In Docker
1550
+ // the host is invisible unless bound (`pnpm neuralis:mount add / --slug
1551
+ // host`), which then surfaces via the generic mount loop below. Opt-in,
1552
+ // gated by `sources.privileged` (no grant below the `admin` tier), attach
1553
+ // READ-ONLY by default (suggestedAccess) — the whole-host blast radius
1554
+ // warrants deny-by-default; the attacher narrows the uri-policy (or widens
1555
+ // write) before use. The roots are the privileged table's own whole-host rows.
1556
+ const hostRoots = stack.isDocker ? [] : protectedRootPaths(stack).filter(isFilesystemRoot);
1557
+ for (const root of hostRoots) {
1558
+ if (attachedRoots.has(root))
1559
+ continue;
1560
+ const drive = /^([A-Za-z]):/.exec(root)?.[1]?.toLowerCase();
1561
+ if (root !== '/' && !drive)
1562
+ continue; // a UNC share root is not "the host"
1376
1563
  suggestions.push({
1377
- id: 'discover:host-root',
1378
- label: 'Full Host',
1379
- description: 'The entire host filesystem (`/`). Attach read-only and scope it before use.',
1380
- suggestedKind: 'local',
1381
- suggestedScheme: 'host',
1382
- suggestedConfig: { root: '/' },
1383
- root: '/',
1564
+ id: drive ? `discover:host-root:${drive}` : 'discover:host-root',
1565
+ label: drive ? `Full Host (${root})` : 'Full Host',
1566
+ description: `The entire host filesystem (\`${root}\`). Attach read-only and scope it before use.`,
1567
+ suggestedKind: HOST_SOURCE_KIND,
1568
+ suggestedScheme: drive ? `host-${drive}` : 'host',
1569
+ suggestedConfig: { root },
1570
+ root,
1384
1571
  icon: 'HardDrive',
1385
1572
  // Privileged host zone — default to USER scope (not shared project-wide).
1386
1573
  suggestedScope: 'user',
@@ -1405,7 +1592,7 @@ async function handleSourcesDiscoverable(req, state) {
1405
1592
  id: `discover:mount:${mount.key}`,
1406
1593
  label: mount.key,
1407
1594
  description: `Host bind \`${mount.hostPath ?? mount.containerPath}\` mounted at \`${mount.containerPath}\`.`,
1408
- suggestedKind: 'local',
1595
+ suggestedKind: HOST_SOURCE_KIND,
1409
1596
  suggestedScheme: scheme,
1410
1597
  suggestedConfig: {
1411
1598
  root: mount.containerPath,
@@ -1424,7 +1611,12 @@ async function handleSourcesDiscoverable(req, state) {
1424
1611
  // ADD gate in handleSourcesCreate enforces the boundary independently — the
1425
1612
  // client filter is UX only.
1426
1613
  const protectedRoots = buildProtectedRoots(stack);
1614
+ // The native folder fence: without a container no root outside the project
1615
+ // zone and the privileged rows is offered — that folder is the device's path.
1616
+ const fenceProject = nativeLocalFenceApplies(stack) ? (await state.infraFactory.getInfra(ctx.projectId)).projectRoot ?? '' : null;
1427
1617
  const visible = suggestions
1618
+ .filter((s) => fenceProject === null || s.suggestedKind !== HOST_SOURCE_KIND || !s.root
1619
+ || attachableNatively(nativeLocalPlacement(hostPathFlavour(s.root).resolve(s.root), fenceProject && resolvePath(fenceProject), protectedRoots.map((p) => p.root))))
1428
1620
  .map((s) => {
1429
1621
  // Union the authoritative protected-root features with any declaration
1430
1622
  // `visibility.seeRequires` already on the suggestion (defense-in-depth).
@@ -1443,131 +1635,51 @@ async function handleSourcesDiscoverable(req, state) {
1443
1635
  return { status: 200, body: { suggestions: visible } };
1444
1636
  }
1445
1637
  /**
1446
- * `GET /sources/browse?path=…` — one level of directory listing, the step that
1447
- * PRECEDES attaching a source.
1448
- *
1449
- * warded-wren (HOST-OSACC-FB9). This handler used to be the one route in the
1450
- * file that checked nothing: no `ensureAccess`, no in-handler feature, no
1451
- * protected-root table, and a raw `err.message` in the body. The route-level
1452
- * `feature = 'files.read'` is a VIEWER default, so on a native install any
1453
- * signed-in reader could walk the operator's host one level at a time
1454
- * (`?path=/` → `/home` → `/root`), and on win32 enumerate the drive letters.
1455
- *
1456
- * The rule is one sentence: **you may browse exactly what you would be allowed
1457
- * to attach.** The base feature is `sources.mount` — the same gate
1458
- * `handleSourcesCreate` gives the act this one prepares — and per-path the
1459
- * SHARED protected-root table decides, so `/`, the App Zone and the Neuralis
1460
- * tree cost `sources.privileged` here exactly as they do at attach time.
1461
- * `'see'` is the semantically right column; today both columns hold the same
1462
- * feature, so nothing in the suite proves the distinction — it is intent, not
1463
- * enforcement.
1638
+ * `GET /sources/browse?path=…[&device=dev_…]` — one level of folders, the step
1639
+ * that PRECEDES attaching a source: on this server, or on the caller's OWN
1640
+ * paired device. The work lives in `usecases/browseSourceFolders.ts`; this
1641
+ * handler supplies the gates this file owns (the protected-root table, the
1642
+ * native fence, the device attach gate). The rule: **you may browse exactly
1643
+ * what you would be allowed to attach.**
1464
1644
  */
1465
1645
  async function handleSourcesBrowse(req, state) {
1466
1646
  const ctx = extractCtx(req);
1647
+ // Not the security gate — the usecase's gates are. `ensureAccess` buys
1648
+ // uniformity with every sibling handler and the project-override 403 in
1649
+ // `extractCtx`, and it needs a real agent in context.
1467
1650
  await ensureAccess(ctx, state);
1468
- // Not the security gate — the gates are the feature check below and the
1469
- // per-path table. `ensureAccess` buys uniformity with every sibling handler
1470
- // and the project-override 403 in `extractCtx`.
1651
+ const rawPath = typeof req.query?.path === 'string' ? req.query.path : '';
1652
+ const device = typeof req.query?.device === 'string' ? req.query.device : '';
1653
+ if (device) {
1654
+ // The device plane's ONE attach gate, asked BEFORE the `sources.mount`
1655
+ // check: the owner attaches (and so browses) with `device.own` alone.
1656
+ const verdict = deviceAttachVerdict(state, ctx, { kind: HOST_SOURCE_KIND, config: { device } });
1657
+ const session = {
1658
+ userId: ctx.userId, projectId: ctx.projectId, ...(ctx.agentId ? { agentId: ctx.agentId } : {}),
1659
+ callerId: '@neuralis/brain-core', callerTrust: 'native', grantedFeatures: [...ctx.grantedFeatures],
1660
+ };
1661
+ const devices = state.ctx.packages?.getPackageApi('@neuralis/agent-core', session)?.devices;
1662
+ return browseDeviceFolders({ device, rawPath, allowed: verdict.allowed }, devices, { userId: ctx.userId, grantedFeatures: [...ctx.grantedFeatures] });
1663
+ }
1471
1664
  if (!hasRouteFeature(req.session, 'sources.mount')) {
1472
1665
  return { status: 403, body: { error: 'Browsing filesystem paths requires the sources.mount feature.' } };
1473
1666
  }
1474
- const { readdir, realpath } = await import('node:fs/promises');
1475
- const { resolve, join, dirname } = await import('node:path');
1476
- // Container deployments: `/mounts` is the only browsable zone and `path` is
1477
- // never read, so it is NOT gated on a path the handler ignores. The fail-soft
1478
- // empty list is deliberate — "no mounts configured yet" is a normal state for
1479
- // the add-source form, not an error.
1480
- if (process.env.NEURALIS_DOCKER === 'true') {
1481
- const mountsRoot = '/mounts';
1482
- try {
1483
- const entries = await readdir(mountsRoot, { withFileTypes: true });
1484
- const dirs = entries.filter((e) => e.isDirectory()).sort((a, b) => a.name.localeCompare(b.name))
1485
- .map((e) => ({ name: e.name, path: join(mountsRoot, e.name), type: 'directory' }));
1486
- return { status: 200, body: { entries: dirs } };
1487
- }
1488
- catch {
1489
- return { status: 200, body: { entries: [] } };
1490
- }
1491
- }
1492
- const rawPath = (req.query?.path ?? '');
1493
- if (!rawPath)
1494
- return { status: 400, body: { error: 'Missing path parameter' } };
1495
- // A NUL byte survives `resolve()` unchanged and would slip past every string
1496
- // prefix comparison in the table below before `readdir` rejects it.
1497
- if (rawPath.includes('\0'))
1498
- return { status: 400, body: { error: BROWSE_UNREADABLE } };
1499
- // A relative path would resolve against the SERVER process cwd — never what
1500
- // the caller meant, and it also closes the drive-relative `C:foo` form.
1501
- if (!isAbsolute(rawPath))
1502
- return { status: 400, body: { error: 'Path must be absolute' } };
1503
- // ONE canonical value from here on: the gate, the root test, the readdir and
1504
- // the child paths all use it. Two resolutions is how a gate ends up judging a
1505
- // different string than the syscall receives.
1506
- const canonical = resolve(rawPath);
1507
- const protectedRoots = buildProtectedRoots(state.ctx.platform.runtimeStack());
1508
- const denied = missingBrowseFeatures(canonical, protectedRoots, state.ctx.platform.runtimeStack(), req.session, dirname);
1509
- if (denied.length > 0) {
1510
- return { status: 403, body: { error: `Browsing this path requires the ${denied[0]} feature.` } };
1511
- }
1512
- // `resolve` is purely LEXICAL, so a symlink whose literal path is innocent can
1513
- // still point at `/` or the App Zone. Re-run the same gate on the real path.
1514
- let real;
1515
- try {
1516
- real = await realpath(canonical);
1517
- }
1518
- catch {
1519
- return { status: 400, body: { error: BROWSE_UNREADABLE } };
1520
- }
1521
- if (real !== canonical) {
1522
- const deniedReal = missingBrowseFeatures(real, protectedRoots, state.ctx.platform.runtimeStack(), req.session, dirname);
1523
- if (deniedReal.length > 0) {
1524
- return { status: 403, body: { error: `Browsing this path requires the ${deniedReal[0]} feature.` } };
1525
- }
1526
- }
1527
- // Whole-host discovery by definition — the drive letters ARE the host. The
1528
- // filesystem-root fixpoint above already required the privileged feature
1529
- // (`win32.resolve('/')` is `\`, whose dirname is itself), and this second
1530
- // check keeps that true even if the table below it ever changes.
1531
- const { platform } = await import('node:os');
1532
- if (platform() === 'win32' && dirname(real) === real) {
1533
- if (!hasRouteFeature(req.session, PRIVILEGED_MOUNT_FEATURE)) {
1534
- return { status: 403, body: { error: `Browsing this path requires the ${PRIVILEGED_MOUNT_FEATURE} feature.` } };
1535
- }
1536
- const { stat } = await import('node:fs/promises');
1537
- const drives = [];
1538
- for (const letter of 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') {
1539
- try {
1540
- await stat(`${letter}:\\`);
1541
- drives.push({ name: `${letter}:\\`, path: `${letter}:\\`, type: 'directory' });
1542
- }
1543
- catch { /* skip */ }
1544
- }
1545
- return { status: 200, body: { entries: drives } };
1546
- }
1547
- try {
1548
- const entries = await readdir(real, { withFileTypes: true });
1549
- const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith('.'))
1550
- .sort((a, b) => a.name.localeCompare(b.name))
1551
- .map((e) => ({ name: e.name, path: join(real, e.name), type: 'directory' }))
1552
- // Hidden-not-locked, the file's own S2 discipline: a child the caller may
1553
- // not see is simply absent, never a locked row. This is what lets the
1554
- // gate above match IS-or-UNDER instead of full containment — the parent
1555
- // stays walkable, the zone inside it does not appear.
1556
- .filter((e) => requiredFeaturesForRoot(e.path, protectedRoots, 'see', 'is-or-under')
1557
- .every((f) => hasRouteFeature(req.session, f)));
1558
- return { status: 200, body: { entries: dirs } };
1559
- }
1560
- catch (err) {
1561
- // Never the raw message: it carries the absolute path and distinguishes
1562
- // "does not exist" from "cannot be read", which together are an existence
1563
- // oracle over the host (invariant 9). The operator reads the detail in the
1564
- // server log; the caller gets one stable sentence for every failure.
1565
- console.warn('[sources/browse] readdir failed:', err instanceof Error ? err.message : String(err));
1566
- return { status: 400, body: { error: BROWSE_UNREADABLE } };
1567
- }
1667
+ // The kernel's runtime stack is the ONE Docker signal; it throws before the
1668
+ // platform facts exist (fail closed).
1669
+ const stack = state.ctx.platform.runtimeStack();
1670
+ const protectedRoots = buildProtectedRoots(stack);
1671
+ return browseServerFolders({ rawPath, isDocker: stack.isDocker === true }, {
1672
+ missingFeatures: (candidate) => missingBrowseFeatures(candidate, protectedRoots, stack, req.session, dirnameOf),
1673
+ childVisible: (child) => requiredFeaturesForRoot(child, protectedRoots, 'see', 'is-or-under').every((f) => hasRouteFeature(req.session, f)),
1674
+ mayBrowseDrives: () => hasRouteFeature(req.session, PRIVILEGED_MOUNT_FEATURE),
1675
+ privilegedFeature: PRIVILEGED_MOUNT_FEATURE,
1676
+ privilegedRoots: protectedRoots.map((p) => p.root),
1677
+ // The native folder fence, the browse half: the project zone and the
1678
+ // privileged rows are browsed as before, an ANCESTOR of the project zone
1679
+ // only on the way down to it, and anything else is the device's path.
1680
+ fenceProject: async () => (nativeLocalFenceApplies(stack) ? (await state.infraFactory.getInfra(ctx.projectId)).projectRoot ?? '' : null),
1681
+ });
1568
1682
  }
1569
- /** One stable sentence for every browse failure — see the catch above. */
1570
- const BROWSE_UNREADABLE = 'That path is not available.';
1571
1683
  /**
1572
1684
  * Features the caller is MISSING to enumerate `candidate`.
1573
1685
  *
@@ -1576,7 +1688,7 @@ const BROWSE_UNREADABLE = 'That path is not available.';
1576
1688
  * all while the runtime stack is unavailable. `buildProtectedRoots(null)`
1577
1689
  * returns `[]`, and an empty table means "unrestricted", so without this
1578
1690
  * the gate is fail-OPEN exactly when the host classification is missing.
1579
- * The create gate's host-plane arm is written the same way and says so.
1691
+ * The create gate's device-plane arm is written the same way and says so.
1580
1692
  * 2. the shared protected-root table, matched IS-or-UNDER (see `RootMatchMode`).
1581
1693
  *
1582
1694
  * The root test is the `dirname(p) === p` fixpoint rather than a comparison