@neuralis/brain-core 0.3.2 → 0.4.1

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 (388) hide show
  1. package/README.md +80 -1410
  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 +53 -48
  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/config/env.d.ts +8 -0
  52. package/dist/src/config/env.d.ts.map +1 -1
  53. package/dist/src/config/env.js +17 -6
  54. package/dist/src/config/env.js.map +1 -1
  55. package/dist/src/connectors/AppSourceConfigStore.d.ts +17 -3
  56. package/dist/src/connectors/AppSourceConfigStore.d.ts.map +1 -1
  57. package/dist/src/connectors/AppSourceConfigStore.js +46 -6
  58. package/dist/src/connectors/AppSourceConfigStore.js.map +1 -1
  59. package/dist/src/connectors/HostConnector.d.ts +201 -0
  60. package/dist/src/connectors/HostConnector.d.ts.map +1 -0
  61. package/dist/src/connectors/HostConnector.js +903 -0
  62. package/dist/src/connectors/HostConnector.js.map +1 -0
  63. package/dist/src/connectors/inProcessTransport.d.ts +131 -0
  64. package/dist/src/connectors/inProcessTransport.d.ts.map +1 -0
  65. package/dist/src/connectors/inProcessTransport.js +447 -0
  66. package/dist/src/connectors/inProcessTransport.js.map +1 -0
  67. package/dist/src/domain/BrainError.d.ts +14 -0
  68. package/dist/src/domain/BrainError.d.ts.map +1 -1
  69. package/dist/src/domain/BrainError.js +21 -0
  70. package/dist/src/domain/BrainError.js.map +1 -1
  71. package/dist/src/domain/access/callerPermissions.d.ts +29 -0
  72. package/dist/src/domain/access/callerPermissions.d.ts.map +1 -1
  73. package/dist/src/domain/access/callerPermissions.js +85 -5
  74. package/dist/src/domain/access/callerPermissions.js.map +1 -1
  75. package/dist/src/domain/access/nativeLocalFence.d.ts +65 -0
  76. package/dist/src/domain/access/nativeLocalFence.d.ts.map +1 -0
  77. package/dist/src/domain/access/nativeLocalFence.js +171 -0
  78. package/dist/src/domain/access/nativeLocalFence.js.map +1 -0
  79. package/dist/src/domain/access/uriPermission.js +3 -3
  80. package/dist/src/domain/access/uriPermission.js.map +1 -1
  81. package/dist/src/domain/byteRange.d.ts +15 -0
  82. package/dist/src/domain/byteRange.d.ts.map +1 -0
  83. package/dist/src/domain/byteRange.js +30 -0
  84. package/dist/src/domain/byteRange.js.map +1 -0
  85. package/dist/src/domain/defaultPermissions.d.ts.map +1 -1
  86. package/dist/src/domain/defaultPermissions.js +29 -0
  87. package/dist/src/domain/defaultPermissions.js.map +1 -1
  88. package/dist/src/domain/mime.d.ts +8 -16
  89. package/dist/src/domain/mime.d.ts.map +1 -1
  90. package/dist/src/domain/mime.js +10 -99
  91. package/dist/src/domain/mime.js.map +1 -1
  92. package/dist/src/domain/paths.d.ts.map +1 -1
  93. package/dist/src/domain/paths.js +5 -3
  94. package/dist/src/domain/paths.js.map +1 -1
  95. package/dist/src/domain/sourceConfig.d.ts +52 -19
  96. package/dist/src/domain/sourceConfig.d.ts.map +1 -1
  97. package/dist/src/domain/sourceConfig.js +103 -19
  98. package/dist/src/domain/sourceConfig.js.map +1 -1
  99. package/dist/src/domain/sourcePackages.d.ts +8 -8
  100. package/dist/src/domain/sourcePackages.d.ts.map +1 -1
  101. package/dist/src/domain/sourceRootPair.d.ts +88 -0
  102. package/dist/src/domain/sourceRootPair.d.ts.map +1 -0
  103. package/dist/src/domain/sourceRootPair.js +108 -0
  104. package/dist/src/domain/sourceRootPair.js.map +1 -0
  105. package/dist/src/domain/syncTypes.d.ts +8 -1
  106. package/dist/src/domain/syncTypes.d.ts.map +1 -1
  107. package/dist/src/domain/syncTypes.js +1 -0
  108. package/dist/src/domain/syncTypes.js.map +1 -1
  109. package/dist/src/domain/types.d.ts +29 -1
  110. package/dist/src/domain/types.d.ts.map +1 -1
  111. package/dist/src/domain/uri.d.ts.map +1 -1
  112. package/dist/src/domain/uri.js +3 -2
  113. package/dist/src/domain/uri.js.map +1 -1
  114. package/dist/src/embeddings/clients/GeminiEmbedder.d.ts.map +1 -1
  115. package/dist/src/embeddings/clients/GeminiEmbedder.js +2 -5
  116. package/dist/src/embeddings/clients/GeminiEmbedder.js.map +1 -1
  117. package/dist/src/embeddings/clients/OpenAIEmbedder.d.ts.map +1 -1
  118. package/dist/src/embeddings/clients/OpenAIEmbedder.js +2 -3
  119. package/dist/src/embeddings/clients/OpenAIEmbedder.js.map +1 -1
  120. package/dist/src/embeddings/clients/QwenEmbedder.d.ts +2 -0
  121. package/dist/src/embeddings/clients/QwenEmbedder.d.ts.map +1 -1
  122. package/dist/src/embeddings/clients/QwenEmbedder.js +10 -2
  123. package/dist/src/embeddings/clients/QwenEmbedder.js.map +1 -1
  124. package/dist/src/embeddings/clients/VoyageEmbedder.d.ts +2 -0
  125. package/dist/src/embeddings/clients/VoyageEmbedder.d.ts.map +1 -1
  126. package/dist/src/embeddings/clients/VoyageEmbedder.js +8 -1
  127. package/dist/src/embeddings/clients/VoyageEmbedder.js.map +1 -1
  128. package/dist/src/embeddings/factory.d.ts +5 -3
  129. package/dist/src/embeddings/factory.d.ts.map +1 -1
  130. package/dist/src/embeddings/factory.js +7 -3
  131. package/dist/src/embeddings/factory.js.map +1 -1
  132. package/dist/src/embeddings/models.d.ts +2 -3
  133. package/dist/src/embeddings/models.d.ts.map +1 -1
  134. package/dist/src/embeddings/models.js +1 -2
  135. package/dist/src/embeddings/models.js.map +1 -1
  136. package/dist/src/embeddings/retry.js +1 -1
  137. package/dist/src/embeddings/retry.js.map +1 -1
  138. package/dist/src/embeddings/types.d.ts +6 -0
  139. package/dist/src/embeddings/types.d.ts.map +1 -1
  140. package/dist/src/embeddings/types.js +11 -0
  141. package/dist/src/embeddings/types.js.map +1 -1
  142. package/dist/src/git/GitRunner.d.ts +110 -30
  143. package/dist/src/git/GitRunner.d.ts.map +1 -1
  144. package/dist/src/git/GitRunner.js +511 -199
  145. package/dist/src/git/GitRunner.js.map +1 -1
  146. package/dist/src/git/GitWatchRegistry.d.ts +41 -35
  147. package/dist/src/git/GitWatchRegistry.d.ts.map +1 -1
  148. package/dist/src/git/GitWatchRegistry.js +61 -79
  149. package/dist/src/git/GitWatchRegistry.js.map +1 -1
  150. package/dist/src/git/gitPlace.d.ts +61 -0
  151. package/dist/src/git/gitPlace.d.ts.map +1 -0
  152. package/dist/src/git/gitPlace.js +74 -0
  153. package/dist/src/git/gitPlace.js.map +1 -0
  154. package/dist/src/git/sourceGitControl.d.ts +31 -19
  155. package/dist/src/git/sourceGitControl.d.ts.map +1 -1
  156. package/dist/src/git/sourceGitControl.js +208 -92
  157. package/dist/src/git/sourceGitControl.js.map +1 -1
  158. package/dist/src/git/sourceGitStatus.js +1 -1
  159. package/dist/src/git/sourceGitStatus.js.map +1 -1
  160. package/dist/src/git/untrackedStats.js +1 -1
  161. package/dist/src/git/untrackedStats.js.map +1 -1
  162. package/dist/src/index.d.ts +2 -1
  163. package/dist/src/index.d.ts.map +1 -1
  164. package/dist/src/index.js +2 -1
  165. package/dist/src/index.js.map +1 -1
  166. package/dist/src/lifecycle.d.ts +12 -16
  167. package/dist/src/lifecycle.d.ts.map +1 -1
  168. package/dist/src/lifecycle.js +2 -1
  169. package/dist/src/lifecycle.js.map +1 -1
  170. package/dist/src/routes/approve.d.ts.map +1 -1
  171. package/dist/src/routes/approve.js +3 -3
  172. package/dist/src/routes/approve.js.map +1 -1
  173. package/dist/src/routes/git.d.ts.map +1 -1
  174. package/dist/src/routes/git.js +183 -54
  175. package/dist/src/routes/git.js.map +1 -1
  176. package/dist/src/routes/health.d.ts.map +1 -1
  177. package/dist/src/routes/health.js +8 -5
  178. package/dist/src/routes/health.js.map +1 -1
  179. package/dist/src/routes/helpers/uriGuard.d.ts +1 -1
  180. package/dist/src/routes/helpers/uriGuard.d.ts.map +1 -1
  181. package/dist/src/routes/helpers/uriGuard.js +3 -2
  182. package/dist/src/routes/helpers/uriGuard.js.map +1 -1
  183. package/dist/src/routes/helpers.d.ts +20 -4
  184. package/dist/src/routes/helpers.d.ts.map +1 -1
  185. package/dist/src/routes/helpers.js +38 -5
  186. package/dist/src/routes/helpers.js.map +1 -1
  187. package/dist/src/routes/list.d.ts.map +1 -1
  188. package/dist/src/routes/list.js +5 -6
  189. package/dist/src/routes/list.js.map +1 -1
  190. package/dist/src/routes/pending.d.ts.map +1 -1
  191. package/dist/src/routes/pending.js +5 -5
  192. package/dist/src/routes/pending.js.map +1 -1
  193. package/dist/src/routes/policies.d.ts +6 -4
  194. package/dist/src/routes/policies.d.ts.map +1 -1
  195. package/dist/src/routes/policies.js +22 -16
  196. package/dist/src/routes/policies.js.map +1 -1
  197. package/dist/src/routes/raw.d.ts.map +1 -1
  198. package/dist/src/routes/raw.js +68 -14
  199. package/dist/src/routes/raw.js.map +1 -1
  200. package/dist/src/routes/read.d.ts.map +1 -1
  201. package/dist/src/routes/read.js +3 -3
  202. package/dist/src/routes/read.js.map +1 -1
  203. package/dist/src/routes/search.d.ts.map +1 -1
  204. package/dist/src/routes/search.js +3 -3
  205. package/dist/src/routes/search.js.map +1 -1
  206. package/dist/src/routes/sources.d.ts +60 -18
  207. package/dist/src/routes/sources.d.ts.map +1 -1
  208. package/dist/src/routes/sources.js +420 -308
  209. package/dist/src/routes/sources.js.map +1 -1
  210. package/dist/src/routes/upload.d.ts.map +1 -1
  211. package/dist/src/routes/upload.js +3 -3
  212. package/dist/src/routes/upload.js.map +1 -1
  213. package/dist/src/services/BridgeService.d.ts +15 -35
  214. package/dist/src/services/BridgeService.d.ts.map +1 -1
  215. package/dist/src/services/BridgeService.js +30 -88
  216. package/dist/src/services/BridgeService.js.map +1 -1
  217. package/dist/src/services/FilesystemService.d.ts +3 -2
  218. package/dist/src/services/FilesystemService.d.ts.map +1 -1
  219. package/dist/src/services/FilesystemService.js +116 -32
  220. package/dist/src/services/FilesystemService.js.map +1 -1
  221. package/dist/src/services/FilesystemUiService.d.ts +4 -1
  222. package/dist/src/services/FilesystemUiService.d.ts.map +1 -1
  223. package/dist/src/services/FilesystemUiService.js +71 -27
  224. package/dist/src/services/FilesystemUiService.js.map +1 -1
  225. package/dist/src/services/SyncScheduler.d.ts +12 -4
  226. package/dist/src/services/SyncScheduler.d.ts.map +1 -1
  227. package/dist/src/services/SyncScheduler.js +45 -10
  228. package/dist/src/services/SyncScheduler.js.map +1 -1
  229. package/dist/src/services/baselineRepair.d.ts +13 -3
  230. package/dist/src/services/baselineRepair.d.ts.map +1 -1
  231. package/dist/src/services/baselineRepair.js +13 -3
  232. package/dist/src/services/baselineRepair.js.map +1 -1
  233. package/dist/src/services/declaredSources.d.ts +125 -42
  234. package/dist/src/services/declaredSources.d.ts.map +1 -1
  235. package/dist/src/services/declaredSources.js +117 -45
  236. package/dist/src/services/declaredSources.js.map +1 -1
  237. package/dist/src/store/dataFormats.d.ts +17 -2
  238. package/dist/src/store/dataFormats.d.ts.map +1 -1
  239. package/dist/src/store/dataFormats.js +18 -3
  240. package/dist/src/store/dataFormats.js.map +1 -1
  241. package/dist/src/sync/DeltaSyncRunner.d.ts.map +1 -1
  242. package/dist/src/sync/DeltaSyncRunner.js +144 -4
  243. package/dist/src/sync/DeltaSyncRunner.js.map +1 -1
  244. package/dist/src/sync/SyncJobQueue.d.ts +14 -2
  245. package/dist/src/sync/SyncJobQueue.d.ts.map +1 -1
  246. package/dist/src/sync/SyncJobQueue.js +56 -10
  247. package/dist/src/sync/SyncJobQueue.js.map +1 -1
  248. package/dist/src/sync/SyncWatchService.d.ts +21 -4
  249. package/dist/src/sync/SyncWatchService.d.ts.map +1 -1
  250. package/dist/src/sync/SyncWatchService.js +115 -10
  251. package/dist/src/sync/SyncWatchService.js.map +1 -1
  252. package/dist/src/sync/categoryInference.d.ts +1 -1
  253. package/dist/src/sync/categoryInference.js +1 -1
  254. package/dist/src/tools/execCtx.d.ts +6 -0
  255. package/dist/src/tools/execCtx.d.ts.map +1 -1
  256. package/dist/src/tools/execCtx.js +11 -0
  257. package/dist/src/tools/execCtx.js.map +1 -1
  258. package/dist/src/tools/fs_list.js +2 -2
  259. package/dist/src/tools/fs_list.js.map +1 -1
  260. package/dist/src/tools/fs_read.js +2 -2
  261. package/dist/src/tools/fs_read.js.map +1 -1
  262. package/dist/src/tools/fs_search.js +2 -2
  263. package/dist/src/tools/fs_search.js.map +1 -1
  264. package/dist/src/tools/fs_write.js +2 -2
  265. package/dist/src/tools/fs_write.js.map +1 -1
  266. package/dist/src/usecases/admitSourceRoot.d.ts +65 -0
  267. package/dist/src/usecases/admitSourceRoot.d.ts.map +1 -0
  268. package/dist/src/usecases/admitSourceRoot.js +85 -0
  269. package/dist/src/usecases/admitSourceRoot.js.map +1 -0
  270. package/dist/src/usecases/browseSourceFolders.d.ts +85 -0
  271. package/dist/src/usecases/browseSourceFolders.d.ts.map +1 -0
  272. package/dist/src/usecases/browseSourceFolders.js +210 -0
  273. package/dist/src/usecases/browseSourceFolders.js.map +1 -0
  274. package/dist/src/usecases/createDeclaredSources.d.ts +13 -2
  275. package/dist/src/usecases/createDeclaredSources.d.ts.map +1 -1
  276. package/dist/src/usecases/createDeclaredSources.js +39 -0
  277. package/dist/src/usecases/createDeclaredSources.js.map +1 -1
  278. package/dist/src/usecases/deprovisionProject.d.ts +4 -5
  279. package/dist/src/usecases/deprovisionProject.d.ts.map +1 -1
  280. package/dist/src/usecases/deprovisionProject.js +3 -8
  281. package/dist/src/usecases/deprovisionProject.js.map +1 -1
  282. package/dist/src/usecases/list.d.ts.map +1 -1
  283. package/dist/src/usecases/list.js +83 -26
  284. package/dist/src/usecases/list.js.map +1 -1
  285. package/dist/src/usecases/listAttachableDevices.d.ts +13 -0
  286. package/dist/src/usecases/listAttachableDevices.d.ts.map +1 -0
  287. package/dist/src/usecases/listAttachableDevices.js +10 -0
  288. package/dist/src/usecases/listAttachableDevices.js.map +1 -0
  289. package/dist/src/usecases/listSourcePackages.d.ts.map +1 -1
  290. package/dist/src/usecases/listSourcePackages.js +95 -79
  291. package/dist/src/usecases/listSourcePackages.js.map +1 -1
  292. package/dist/src/usecases/purgeExcludedPoints.d.ts.map +1 -1
  293. package/dist/src/usecases/purgeExcludedPoints.js +3 -2
  294. package/dist/src/usecases/purgeExcludedPoints.js.map +1 -1
  295. package/dist/src/usecases/readFiles.d.ts.map +1 -1
  296. package/dist/src/usecases/readFiles.js +17 -1
  297. package/dist/src/usecases/readFiles.js.map +1 -1
  298. package/dist/src/usecases/reconcileSourcePackageRoots.d.ts +1 -1
  299. package/dist/src/usecases/reconcileSourcePackageRoots.d.ts.map +1 -1
  300. package/dist/src/usecases/reconcileSourcePackageRoots.js +2 -1
  301. package/dist/src/usecases/reconcileSourcePackageRoots.js.map +1 -1
  302. package/dist/src/usecases/revokePrincipal.d.ts +7 -5
  303. package/dist/src/usecases/revokePrincipal.d.ts.map +1 -1
  304. package/dist/src/usecases/revokePrincipal.js +29 -7
  305. package/dist/src/usecases/revokePrincipal.js.map +1 -1
  306. package/dist/src/usecases/scheduleSourceSync.d.ts +7 -0
  307. package/dist/src/usecases/scheduleSourceSync.d.ts.map +1 -1
  308. package/dist/src/usecases/scheduleSourceSync.js +6 -5
  309. package/dist/src/usecases/scheduleSourceSync.js.map +1 -1
  310. package/dist/src/usecases/searchFiles.d.ts.map +1 -1
  311. package/dist/src/usecases/searchFiles.js +13 -3
  312. package/dist/src/usecases/searchFiles.js.map +1 -1
  313. package/dist/src/usecases/sourcePackageInventory.d.ts +10 -2
  314. package/dist/src/usecases/sourcePackageInventory.d.ts.map +1 -1
  315. package/dist/src/usecases/sourcePackageInventory.js +24 -7
  316. package/dist/src/usecases/sourcePackageInventory.js.map +1 -1
  317. package/dist/src/usecases/triggerSourceSync.d.ts +46 -0
  318. package/dist/src/usecases/triggerSourceSync.d.ts.map +1 -0
  319. package/dist/src/usecases/triggerSourceSync.js +75 -0
  320. package/dist/src/usecases/triggerSourceSync.js.map +1 -0
  321. package/dist/src/usecases/uploadBinary.d.ts +2 -1
  322. package/dist/src/usecases/uploadBinary.d.ts.map +1 -1
  323. package/dist/src/usecases/uploadBinary.js +2 -2
  324. package/dist/src/usecases/uploadBinary.js.map +1 -1
  325. package/dist/src/usecases/vector/endpointGuard.d.ts +1 -1
  326. package/dist/src/usecases/vector/endpointGuard.js +1 -1
  327. package/dist/src/usecases/vectorActivityWatch.d.ts +20 -13
  328. package/dist/src/usecases/vectorActivityWatch.d.ts.map +1 -1
  329. package/dist/src/usecases/vectorActivityWatch.js +61 -36
  330. package/dist/src/usecases/vectorActivityWatch.js.map +1 -1
  331. package/dist/src/usecases/vectorMaintenance.d.ts.map +1 -1
  332. package/dist/src/usecases/vectorMaintenance.js +16 -1
  333. package/dist/src/usecases/vectorMaintenance.js.map +1 -1
  334. package/dist/src/usecases/write/index.d.ts.map +1 -1
  335. package/dist/src/usecases/write/index.js +17 -2
  336. package/dist/src/usecases/write/index.js.map +1 -1
  337. package/dist/src/usecases/write/occupancy.d.ts +5 -1
  338. package/dist/src/usecases/write/occupancy.d.ts.map +1 -1
  339. package/dist/src/usecases/write/occupancy.js +13 -4
  340. package/dist/src/usecases/write/occupancy.js.map +1 -1
  341. package/dist/src/usecases/write/structure.d.ts.map +1 -1
  342. package/dist/src/usecases/write/structure.js +20 -5
  343. package/dist/src/usecases/write/structure.js.map +1 -1
  344. package/dist/src/vector/FilesystemVectorIndex.d.ts +72 -2
  345. package/dist/src/vector/FilesystemVectorIndex.d.ts.map +1 -1
  346. package/dist/src/vector/FilesystemVectorIndex.js +211 -27
  347. package/dist/src/vector/FilesystemVectorIndex.js.map +1 -1
  348. package/dist/src/vector/InMemoryVectorIndex.d.ts.map +1 -1
  349. package/dist/src/vector/InMemoryVectorIndex.js +55 -7
  350. package/dist/src/vector/InMemoryVectorIndex.js.map +1 -1
  351. package/dist/src/vector/qdrantClient.d.ts +18 -1
  352. package/dist/src/vector/qdrantClient.d.ts.map +1 -1
  353. package/dist/src/vector/qdrantClient.js +48 -6
  354. package/dist/src/vector/qdrantClient.js.map +1 -1
  355. package/dist/src/wiring.d.ts +10 -12
  356. package/dist/src/wiring.d.ts.map +1 -1
  357. package/dist/src/wiring.js +101 -61
  358. package/dist/src/wiring.js.map +1 -1
  359. package/package.json +166 -54
  360. package/skills/connect-business-data/SKILL.md +2 -2
  361. package/skills/create-package/SKILL.md +18 -18
  362. package/skills/create-package/references/config-and-credentials.md +16 -13
  363. package/skills/create-package/references/connectors-and-uri-policies.md +36 -27
  364. package/skills/create-package/references/manifest-and-discovery.md +12 -12
  365. package/skills/create-package/references/runtime-and-build.md +18 -18
  366. package/skills/create-package/references/skills-and-context.md +35 -19
  367. package/skills/create-package/references/surfaces-and-design.md +16 -16
  368. package/skills/create-package/references/team-and-workflows.md +2 -2
  369. package/skills/create-package/references/test-and-ship.md +17 -17
  370. package/skills/create-package/references/tools-and-schemas.md +5 -5
  371. package/skills/create-package/scripts/{check-package.sh → check-package.mjs} +78 -64
  372. package/skills/create-package/scripts/{verify-loaded.sh → verify-loaded.mjs} +156 -162
  373. package/skills/git/SKILL.md +31 -29
  374. package/skills/manage-files/SKILL.md +11 -14
  375. package/skills/manage-sources/SKILL.md +85 -45
  376. package/skills/manage-uri-policy/SKILL.md +9 -6
  377. package/skills/sync/SKILL.md +2 -2
  378. package/dist/src/connectors/LocalDiskConnector.d.ts +0 -171
  379. package/dist/src/connectors/LocalDiskConnector.d.ts.map +0 -1
  380. package/dist/src/connectors/LocalDiskConnector.js +0 -1233
  381. package/dist/src/connectors/LocalDiskConnector.js.map +0 -1
  382. package/skills/manage-files/scripts/copy.sh +0 -7
  383. package/skills/manage-files/scripts/create.sh +0 -7
  384. package/skills/manage-files/scripts/mkdir.sh +0 -7
  385. package/skills/manage-files/scripts/modify.sh +0 -7
  386. package/skills/manage-files/scripts/upload.sh +0 -16
  387. package/skills/manage-files/scripts/write.sh +0 -29
  388. package/skills/manage-sources/scripts/reembed.sh +0 -23
@@ -2,19 +2,22 @@
2
2
  * GitRunner — the single, security-hardened entry point to git for a
3
3
  * git-backed source (porcelain-marten / G0).
4
4
  *
5
- * It is keyed on a RESOLVED CONTAINER CHECKOUT ROOT (`resolveLocalRoot()` — the
6
- * exact value the `local` connector mounts and the shell uri-policy gate keys
7
- * on) so git can never operate outside the source boundary. Every later git
8
- * increment (G1 badges, G2 SCM view, G3 hosted tools, G4 remote, G5 branches)
9
- * goes through this class — it is the floor they all build on.
5
+ * It is keyed on a RESOLVED CHECKOUT ROOT — the `host` connector's root, on
6
+ * this machine or on a paired device — so git can never operate outside the
7
+ * source boundary. Every git surface (badges, the Git Control panel, the
8
+ * routes-based git skill, remote ops) goes through this class: it is the floor
9
+ * they all build on.
10
10
  *
11
- * Trust: git runs IN-PROCESS as first-party trusted host code (D2), NOT inside
12
- * the Landlock shell sandbox. That makes the four containment requirements
13
- * below non-negotiable — a working tree (especially one cloned from an
14
- * untrusted remote in G4) is attacker-influenced data.
11
+ * Every op is ONE argv run through `#spawnGit` — never a shell, never a wrapper
12
+ * library. On this machine git runs IN-PROCESS as native trusted host code
13
+ * (D2), NOT inside the Landlock shell sandbox; on a device it runs through that
14
+ * device's broker (`transport.exec`), under the owner's ceiling and, for anyone
15
+ * but the device's owner, a live sandbox. The four containment requirements
16
+ * below hold IDENTICALLY on both — a working tree (especially one cloned from
17
+ * an untrusted remote) is attacker-influenced data.
15
18
  *
16
19
  * The four containment floors (architect Finding 2, porcelain-marten):
17
- * 1. ROOT BINDING — every op runs with `cwd = containerRoot`; sub-path args
20
+ * 1. ROOT BINDING — every op runs with `cwd = root`; sub-path args
18
21
  * are guarded to stay inside it; `git rev-parse --show-toplevel` is
19
22
  * clamped so a `.git` ABOVE the source is rejected, AND (G2.5) the
20
23
  * EFFECTIVE `--absolute-git-dir` is clamped so a `.git` FILE / symlink
@@ -28,25 +31,31 @@
28
31
  * hooks + fsmonitor + pager, closing the malicious-checkout RCE class.
29
32
  * 4. CONTROLLED ENV — ambient `GIT_DIR`/`GIT_WORK_TREE`/`GIT_CONFIG*` etc.
30
33
  * are stripped and global/system config is neutralised, so no inherited
31
- * env var can redirect the repo or inject config.
34
+ * env var can redirect the repo or inject config (on a device the broker
35
+ * strips them from its own base for every argv run).
36
+ *
37
+ * The hooks path and the neutralised config files name the NULL DEVICE of the
38
+ * machine git runs on (`gitNullDevice`: `/dev/null`, `NUL` on a Windows device).
32
39
  *
33
40
  * G4 (keyring-kestrel Slice 2): remote ops (fetch/push/pull) ARE implemented —
34
- * `fetch`/`push`/`pull`/`mergeFetched` route through `#spawnGitRemote` (raw
35
- * `node:child_process`, NEVER simple-git — J3), which prepends the full
41
+ * `fetch`/`push`/`pull`/`mergeFetched` route through `#spawnGitRemote` (this
42
+ * machine's raw `node:child_process`; on a device checkout they run on the device for its owner
43
+ * only, with the owner's own credential helpers and never a server token), which prepends the full
36
44
  * protected-config-aware remote-hardening `-c` set (S1 empty-reset-then-constant
37
45
  * credential helper, S3 askPass off, `protocol.{file,ext,git}.allow=never`,
38
- * pinned `remote.<n>.{upload,receive}pack`, `core.sshCommand=`/`core.gitProxy=`)
46
+ * `push.gpgSign=false`, pinned `remote.<n>.{upload,receive}pack`,
47
+ * `core.sshCommand=`/`core.gitProxy=`)
39
48
  * plus a CONSTANT host-matching credential helper reading the token from the
40
49
  * child env. NO clone, NO force-push (D6). The token is present ONLY on
41
50
  * network-only invocations (fetch/push), never a working-tree checkout/merge
42
51
  * (S4 — `pull` = `fetch` + a token-free `mergeFetched`).
43
52
  */
44
53
  var _a;
45
- import path from 'node:path';
46
54
  import { realpath } from 'node:fs/promises';
47
55
  import { spawn } from 'node:child_process';
48
- import { simpleGit } from 'simple-git';
49
56
  import { resolveSearchBinaries } from '../search/binaryResolver.js';
57
+ import { placePath, toGitPlace } from './gitPlace.js';
58
+ import { transportRefusalReason } from '../connectors/HostConnector.js';
50
59
  // ---------------------------------------------------------------------------
51
60
  // Structured errors
52
61
  // ---------------------------------------------------------------------------
@@ -211,15 +220,61 @@ export function classifyGitOp(op, args = []) {
211
220
  // ---------------------------------------------------------------------------
212
221
  /**
213
222
  * `-c` overrides applied to EVERY git invocation (highest precedence — beats a
214
- * malicious repo-local `.git/config`). G4 additionally hardens the remote/clone
215
- * class (`protocol.file.allow=never`, `core.sshCommand`, `filter.*`,
216
- * `diff.external`) when it wires fetch/pull/push/clone.
223
+ * malicious repo-local `.git/config`). The remote ops add their own set on top
224
+ * (`#spawnGitRemote`).
225
+ */
226
+ function hardeningConfig(nullDevice) {
227
+ return [
228
+ `core.hooksPath=${nullDevice}`, // no repo-local hook executes as the host process
229
+ 'core.fsmonitor=false', // no repo-local fsmonitor command runs
230
+ 'core.pager=cat', // never spawn a pager (belt; non-interactive anyway)
231
+ // A repo-local `commit.gpgSign` / `tag.gpgSign` + `gpg.program` would run a
232
+ // program of the repo's choosing on commit / tag (measured, git 2.43.0).
233
+ 'commit.gpgSign=false',
234
+ 'tag.gpgSign=false',
235
+ ];
236
+ }
237
+ /**
238
+ * Every CONFINED call also points git's default global ignore and attributes
239
+ * files at the null device. A null GIT_CONFIG_GLOBAL still leaves those defaults
240
+ * (`$XDG_CONFIG_HOME/git/…`, `~/.config/git/…`) in force; a sandboxed device
241
+ * spawn cannot open them, and git then dies with "cannot use … as an exclude
242
+ * file" on every `status` / `diff --cached`. Kept apart from `hardeningConfig`
243
+ * because the device owner's own fetch / push runs unconfined and its argv must
244
+ * equal the broker's fixed set (`host-broker/ownerRemoteGit.mjs`).
245
+ */
246
+ function noGlobalFilesConfig(nullDevice) {
247
+ return [`core.excludesFile=${nullDevice}`, `core.attributesFile=${nullDevice}`];
248
+ }
249
+ /**
250
+ * The null device floors 3 and 4 point hooks and the global/system config at,
251
+ * in the flavour of the machine git RUNS on. A Windows device takes `NUL`: a
252
+ * `/dev/null` hooks directory there is an ordinary path on the current drive
253
+ * (`\dev\null\<hook>`) whoever can write that drive could fill, while `NUL` is
254
+ * a reserved device name no directory can carry. The Windows half is pinned by
255
+ * a unit on the argv the device transport receives; the on-machine run is a
256
+ * Windows owner row.
217
257
  */
218
- const HARDENING_CONFIG = [
219
- 'core.hooksPath=/dev/null', // no repo-local hook executes as the host process
220
- 'core.fsmonitor=false', // no repo-local fsmonitor command runs
221
- 'core.pager=cat', // never spawn a pager (belt; non-interactive anyway)
222
- ];
258
+ export function gitNullDevice(flavour) {
259
+ return flavour === 'win32' ? 'NUL' : '/dev/null';
260
+ }
261
+ /**
262
+ * The four fixed `GIT_*` values every call carries (floor 4) — constants, never
263
+ * an inherited value. Locally they ride `sanitizedGitEnv()`; on a device they are
264
+ * the request's named env, over the broker's base from which every ambient
265
+ * `GIT_*` and `UNSAFE_PLAIN_ENV` name is already gone (`host-broker/server.mjs`
266
+ * argv runs).
267
+ */
268
+ export function gitFloorEnv(nullDevice) {
269
+ return {
270
+ GIT_CONFIG_GLOBAL: nullDevice,
271
+ GIT_CONFIG_SYSTEM: nullDevice,
272
+ // Never block on an interactive credential/askpass prompt.
273
+ GIT_TERMINAL_PROMPT: '0',
274
+ // Read-only-status floor — see `sanitizedGitEnv`.
275
+ GIT_OPTIONAL_LOCKS: '0',
276
+ };
277
+ }
223
278
  /**
224
279
  * The CONSTANT (S2) credential-helper snippet installed on remote ops. It is
225
280
  * NEVER interpolated with any dynamic value — username/host/token arrive via the
@@ -284,8 +339,8 @@ const GIT_LOG_FS = '\x1f';
284
339
  /**
285
340
  * Env names OUTSIDE the `GIT_*` namespace that still drive git command
286
341
  * execution (git falls back to them when the GIT_ form is absent). These
287
- * command-bearing variables are stripped alongside the GIT_* sweep, never
288
- * allowed through simple-git's environment guard: a non-interactive hardened
342
+ * command-bearing variables are stripped alongside the GIT_* sweep (the device
343
+ * broker strips the same set for an argv run): a non-interactive hardened
289
344
  * runner needs none of them, even when its parent exports an editor command.
290
345
  */
291
346
  const UNSAFE_PLAIN_ENV = new Set(['EDITOR', 'VISUAL', 'PAGER', 'PREFIX', 'SSH_ASKPASS']);
@@ -305,11 +360,9 @@ function sanitizedGitEnv() {
305
360
  continue;
306
361
  env[key] = value;
307
362
  }
308
- // Neutralise ambient global/system config injection (~/.gitconfig, /etc/gitconfig).
309
- env.GIT_CONFIG_GLOBAL = '/dev/null';
310
- env.GIT_CONFIG_SYSTEM = '/dev/null';
311
- // Never block on an interactive credential/askpass prompt (G4 floor, harmless now).
312
- env.GIT_TERMINAL_PROMPT = '0';
363
+ // Neutralise ambient global/system config injection (~/.gitconfig, /etc/gitconfig),
364
+ // never prompt, never take optional locks — the four constants of `gitFloorEnv`.
365
+ Object.assign(env, gitFloorEnv(gitNullDevice('posix')));
313
366
  // Read-only-status floor (MAJOR-4a, G2). git opportunistically takes the
314
367
  // `.git/index.lock` and REWRITES `.git/index` (stat-cache refresh) even on a
315
368
  // pure `status`/`diff` read — which, once the G2.2 `.git`-watcher is live,
@@ -317,41 +370,95 @@ function sanitizedGitEnv() {
317
370
  // loop. Disabling OPTIONAL locks makes read queries truly index-read-only; the
318
371
  // mutating ops (add/commit/…) still take their real locks, so this is harmless
319
372
  // to writes. Set here (env) rather than per-call so EVERY read path inherits it.
320
- env.GIT_OPTIONAL_LOCKS = '0';
321
373
  return env;
322
374
  }
323
- /** Best-effort detection of simple-git's block-timeout rejection. */
324
- function isGitTimeout(err) {
325
- if (!err || typeof err !== 'object')
326
- return false;
327
- const anyErr = err;
328
- if (anyErr.plugin === 'timeout')
329
- return true;
330
- return typeof anyErr.message === 'string' && /timeout/i.test(anyErr.message);
375
+ /**
376
+ * A git call on a paired device could not be asked (offline, outdated broker,
377
+ * the device's ceiling, an unconfined device for a non-owner). `reason` is the
378
+ * transport's FIXED code — never a path, never the broker's text.
379
+ */
380
+ export class GitDeviceUnavailableError extends Error {
381
+ reason;
382
+ code = 'git_device_unavailable';
383
+ constructor(reason) {
384
+ super(`git could not run on the device (${reason})`);
385
+ this.reason = reason;
386
+ this.name = 'GitDeviceUnavailableError';
387
+ }
388
+ }
389
+ /**
390
+ * A server credential handed to a remote op on a paired device's folder. THIS
391
+ * server's push token (`NRS_GIT_TOKEN`) never crosses a device link — a
392
+ * device's remote ops use its owner's own git credentials there.
393
+ */
394
+ export class GitRemoteOnDeviceError extends Error {
395
+ code = 'git_remote_on_device';
396
+ constructor() {
397
+ super("a server git credential never reaches a paired device");
398
+ this.name = 'GitRemoteOnDeviceError';
399
+ }
400
+ }
401
+ /**
402
+ * fetch / pull / push on a paired device's folder by anyone but the device's
403
+ * owner. The only credential there is the OWNER's own (their git credential
404
+ * helper on their machine); running it for another person would borrow it.
405
+ */
406
+ export class GitRemoteOwnerOnlyError extends Error {
407
+ code = 'git_remote_owner_only';
408
+ constructor() {
409
+ super("only the device's owner fetches, pulls and pushes a folder on it");
410
+ this.name = 'GitRemoteOwnerOnlyError';
411
+ }
412
+ }
413
+ /** The credential-helper keys an owner's own git config may name — plain and URL-scoped (`credential.<url>.helper`). */
414
+ const OWNER_HELPER_KEY_RE = /^credential(\.[^=\s]+)?\.helper$/i;
415
+ /** A fixed transport code (`device_offline`, `others_denied`, …) off a thrown device error, else a generic one. */
416
+ function deviceReasonOf(error) {
417
+ return transportRefusalReason(error) ?? 'device_unreachable';
331
418
  }
332
- // ---------------------------------------------------------------------------
333
- // GitRunner
334
- // ---------------------------------------------------------------------------
335
419
  export class GitRunner {
336
420
  #root;
337
- #git;
338
- /** The resolved git binary — reused by the direct-spawn stdin/capture path (`#spawnGit`). */
421
+ /** The machine's path arithmetic (a Windows device answers in `win32`). */
422
+ #path;
423
+ /** The resolved LOCAL git binary; `null` on a device checkout (the device's own `git` runs). */
339
424
  #bin;
340
- /** Inactivity cap mirrored into `#spawnGit` (simple-git's `timeout.block` only covers its task API). */
425
+ /** A device checkout's exec seat; `null` for a checkout on this machine. */
426
+ #device;
427
+ #nullDevice;
428
+ /** The per-op timeout (an inactivity cap locally, the broker's wall-clock cap on a device). */
341
429
  #timeoutMs;
342
430
  #repoChecked = null;
343
- constructor(root, git, bin, timeoutMs) {
344
- this.#root = root;
345
- this.#git = git;
346
- this.#bin = bin;
347
- this.#timeoutMs = timeoutMs && timeoutMs > 0 ? timeoutMs : undefined;
431
+ constructor(init) {
432
+ this.#root = init.root;
433
+ this.#path = init.path;
434
+ this.#bin = init.bin;
435
+ this.#device = init.device;
436
+ this.#nullDevice = gitNullDevice(init.device?.transport.flavour ?? 'posix');
437
+ this.#timeoutMs = init.timeoutMs && init.timeoutMs > 0 ? init.timeoutMs : undefined;
348
438
  }
349
- /** The resolved container checkout root this runner is bound to. */
439
+ /** The checkout root this runner is bound to (a device path for a device checkout). */
350
440
  get root() {
351
441
  return this.#root;
352
442
  }
353
443
  static async create(options) {
354
- const root = path.resolve(options.containerRoot);
444
+ const place = toGitPlace(options.root);
445
+ const p = placePath(place);
446
+ if (place.device) {
447
+ // A device spawn follows the device gate, never precedes it: a place built
448
+ // for a read-only probe carries no verdict and cannot run anything.
449
+ const { transport, owner } = place.device;
450
+ if (owner === undefined)
451
+ throw new GitArgumentError('a device checkout needs the device gate verdict');
452
+ if (!transport.exec)
453
+ throw new GitUnavailableError();
454
+ return new _a({
455
+ root: p.resolve(place.root),
456
+ path: p,
457
+ bin: null,
458
+ device: { transport, exec: transport.exec.bind(transport), owner },
459
+ ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
460
+ });
461
+ }
355
462
  let bin = options.gitBin;
356
463
  if (!bin) {
357
464
  const bins = await resolveSearchBinaries();
@@ -359,74 +466,92 @@ export class GitRunner {
359
466
  }
360
467
  if (!bin)
361
468
  throw new GitUnavailableError();
362
- const git = simpleGit({
363
- baseDir: root,
364
- binary: bin,
365
- maxConcurrentProcesses: 1,
366
- trimmed: false,
367
- // Inactivity cap (G1 perf floor): if git produces no output for `block` ms
368
- // it is killed and the op rejects — for a silent whole-tree scan this is an
369
- // effective wall-clock cap. Only set when the caller asked (G1); untimed
370
- // otherwise so G4 clone/fetch (legitimately long, bursty) is unaffected.
371
- ...(options.timeoutMs && options.timeoutMs > 0
372
- ? { timeout: { block: options.timeoutMs } }
373
- : {}),
374
- config: HARDENING_CONFIG,
375
- // Only the sanitizer's four defensive Git overrides may cross v4's gate.
376
- allowEnvironment: [
377
- 'GIT_CONFIG_GLOBAL',
378
- 'GIT_CONFIG_SYSTEM',
379
- 'GIT_TERMINAL_PROMPT',
380
- 'GIT_OPTIONAL_LOCKS',
381
- ],
382
- // simple-git blocks `-c core.hooksPath|fsmonitor|pager` because those are
383
- // the exact keys an ATTACKER-supplied arg would abuse. Here the values are
384
- // HARDCODED safe constants (HARDENING_CONFIG) set DEFENSIVELY, never
385
- // caller-influenced — so the explicit opt-in is correct and required.
386
- unsafe: {
387
- allowUnsafeHooksPath: true,
388
- allowUnsafeFsMonitor: true,
389
- allowUnsafePager: true,
390
- // We set GIT_CONFIG_GLOBAL/SYSTEM=/dev/null in the env (hardcoded safe
391
- // constants) to neutralise ambient global/system gitconfig — simple-git
392
- // flags that env as a config-path override, so opt in for our own value.
393
- allowUnsafeConfigPaths: true,
394
- },
395
- }).env(sanitizedGitEnv());
396
- return new _a(root, git, bin, options.timeoutMs);
469
+ return new _a({
470
+ root: p.resolve(place.root),
471
+ path: p,
472
+ bin,
473
+ device: null,
474
+ ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
475
+ });
397
476
  }
398
477
  /**
399
- * Run git DIRECTLY (bypassing the simple-git task API) for the ops that need
400
- * real stdin/stdout plumbing (`git apply` fed a patch on STDIN, `git diff
401
- * --no-index` which exits 1 when the files differ — callers pass
402
- * `allowExitCodes:[1]`) AND (G2.4, MINOR-6) the potentially-large read paths
403
- * (`diff`/`logGraph`/`show`/`cat-file`), which need the output accumulation
404
- * bound: the whole output is buffered in-process, so the child is KILLED once
405
- * stdout+stderr exceed `GIT_MAX_OUTPUT_BYTES` (→ `GitOutputLimitError`). The
406
- * create-time `timeoutMs` is replicated here as the same INACTIVITY semantics
407
- * simple-git's `timeout.block` has (resets per chunk → `GitTimeoutError`).
408
- * The four containment floors are replicated by hand: cwd = the bound root,
409
- * the hardcoded `-c` hardening prefix, and `sanitizedGitEnv()` (GIT_*
410
- * stripped, global/system config neutralised). No shell — argv array,
411
- * `shell:false` (spawn default). Output is chunk-concatenated as BYTES and
412
- * decoded once (a per-chunk decode would corrupt multibyte content at chunk
413
- * boundaries — `show` serves file content verbatim).
478
+ * Run ONE git command — every op of this class goes through here, argv only,
479
+ * never a shell. The four containment floors ride every call: cwd = the bound
480
+ * root, the hardcoded `-c` hardening prefix (in the flavour of the machine git
481
+ * runs on), and the controlled env (`sanitizedGitEnv()` locally; the
482
+ * `gitFloorEnv` constants over the broker's GIT-free base on a device). The
483
+ * output is bounded by `GIT_MAX_OUTPUT_BYTES` (→ `GitOutputLimitError`) and
484
+ * the runner's timeout (→ `GitTimeoutError`) on BOTH transports; callers that
485
+ * tolerate a nonzero exit (`git diff --no-index` exits 1 when the files differ)
486
+ * pass `allowExitCodes`.
414
487
  */
415
488
  #spawnGit(args, opts = {}) {
416
- const fullArgs = [...HARDENING_CONFIG.flatMap((c) => ['-c', c]), ...args];
417
- return this.#execSpawn(fullArgs, sanitizedGitEnv(), opts);
489
+ const config = [...hardeningConfig(this.#nullDevice), ...noGlobalFilesConfig(this.#nullDevice)];
490
+ const fullArgs = [...config.flatMap((c) => ['-c', c]), ...args];
491
+ return this.#exec(fullArgs, {}, opts);
492
+ }
493
+ /** The transport fork: this machine's spawn, or the device's broker. */
494
+ #exec(fullArgs, extraEnv, opts) {
495
+ if (this.#device)
496
+ return this.#execDevice(this.#device, fullArgs, extraEnv, opts);
497
+ return this.#execSpawn(fullArgs, { ...sanitizedGitEnv(), ...extraEnv }, opts);
498
+ }
499
+ /**
500
+ * The device half: `git <argv>` through the broker's ONE spawn planner
501
+ * (`/exec` argv mode) — the owner's ceiling, and for anyone but the device's
502
+ * owner a live sandbox plus the owner's per-folder "others" level
503
+ * (`actor:'other'`, derived by the client from `requireConfinement`).
504
+ */
505
+ async #execDevice(seat, fullArgs, extraEnv, opts) {
506
+ let result;
507
+ // `ownerGlobalConfig` = the ONE read that may see the owner's own global and
508
+ // system config (`#ownerCredentialHelpers`, owner-only); every other call
509
+ // neutralises both.
510
+ const floor = gitFloorEnv(this.#nullDevice);
511
+ if (opts.ownerGlobalConfig) {
512
+ delete floor.GIT_CONFIG_GLOBAL;
513
+ delete floor.GIT_CONFIG_SYSTEM;
514
+ }
515
+ try {
516
+ result = await seat.exec({
517
+ argv: ['git', ...fullArgs],
518
+ cwd: this.#root,
519
+ env: { ...floor, ...extraEnv },
520
+ ...(opts.stdin !== undefined ? { stdin: opts.stdin } : {}),
521
+ ...(this.#timeoutMs !== undefined ? { timeoutMs: this.#timeoutMs } : {}),
522
+ maxOutputBytes: GIT_MAX_OUTPUT_BYTES,
523
+ // `ownerRemote` = the owner's fetch / push / helper read: the device's
524
+ // broker runs exactly that fixed argv outside its sandbox (the owner's
525
+ // helpers live in their home and keychain); every other op stays confined.
526
+ ...(opts.ownerRemote && seat.owner ? { ownerRemoteGit: true } : {}),
527
+ }, { owner: seat.owner });
528
+ }
529
+ catch (error) {
530
+ throw new GitDeviceUnavailableError(deviceReasonOf(error));
531
+ }
532
+ if (result.timedOut)
533
+ throw new GitTimeoutError(this.#root);
534
+ if (result.truncated)
535
+ throw new GitOutputLimitError(this.#root);
536
+ const ok = result.exitCode === 0 || (opts.allowExitCodes?.includes(result.exitCode) ?? false);
537
+ if (ok)
538
+ return result.stdout;
539
+ const cmd = fullArgs.find((a, i) => fullArgs[i - 1] !== '-c' && !a.startsWith('-')) ?? '';
540
+ throw new GitApplyError(result.stderr.trim() || `git ${cmd} exited with code ${result.exitCode}`);
418
541
  }
419
542
  /**
420
- * The shared spawn executor for BOTH the local direct-spawn path (`#spawnGit`)
543
+ * The local spawn executor for BOTH the local direct-spawn path (`#spawnGit`)
421
544
  * and the remote-op path (`#spawnGitRemote`): the timeout + output-accumulation
422
545
  * bound + exit-code handling, over a caller-composed argv + env. Isolating it
423
546
  * keeps the two callers from drifting on the containment plumbing while letting
424
- * the remote path compose its OWN hardening `-c` set + credential env (which
425
- * MUST NOT go through simple-git — J3).
547
+ * the remote path compose its OWN hardening `-c` set + credential env.
426
548
  */
427
549
  #execSpawn(fullArgs, env, opts = {}) {
550
+ const bin = this.#bin;
551
+ if (bin === null)
552
+ return Promise.reject(new GitUnavailableError());
428
553
  return new Promise((resolve, reject) => {
429
- const child = spawn(this.#bin, fullArgs, {
554
+ const child = spawn(bin, fullArgs, {
430
555
  cwd: this.#root,
431
556
  env,
432
557
  stdio: [opts.stdin !== undefined ? 'pipe' : 'ignore', 'pipe', 'pipe'],
@@ -490,15 +615,20 @@ export class GitRunner {
490
615
  });
491
616
  }
492
617
  /**
493
- * The REMOTE-op spawn variant (G4 keyring-kestrel Slice 2). Raw
494
- * `node:child_process` — NEVER simple-git: `HARDENING_CONFIG` is the simple-git
495
- * constructor's `config:`, and simple-git v3 blocks `-c credential.helper` /
496
- * `core.sshCommand` / `filter.*` / `protocol.*` unless opted in, so routing the
497
- * remote `-c` set through simple-git would throw EVERY task (incl.
498
- * `#assertRepoInsideSource`'s `rev-parse`) — unit-green, boot-dead (J3). The
499
- * raw path needs ZERO simple-git opt-ins.
618
+ * The REMOTE-op spawn variant (G4 keyring-kestrel Slice 2) — this machine's
619
+ * raw `node:child_process`, or a paired device's broker for its OWNER only
620
+ * (`GitRemoteOwnerOnlyError` for anyone else). A server credential on a
621
+ * device refuses (`GitRemoteOnDeviceError`): the token never crosses a link.
622
+ * On a device the owner's own credential helpers (`ownerHelpers`, read from
623
+ * their global/system config) take the place of the constant helper, after
624
+ * the same empty reset; every other key below is identical on both. That op
625
+ * (and the helper read) is the one a SANDBOXED device's broker runs outside
626
+ * its sandbox, after judging it exactly this fixed argv
627
+ * (`host-broker/ownerRemoteGit.mjs`) — the owner's own trust: nothing an agent
628
+ * types reaches the argv or its config, and the keys below neutralise the
629
+ * repo-local exec vectors. The GAP-HTTP probe and the pull's merge stay confined.
500
630
  *
501
- * It prepends, IN ADDITION to `HARDENING_CONFIG`, the full protected-config-aware
631
+ * It prepends, IN ADDITION to `hardeningConfig(<null device>)`, the full protected-config-aware
502
632
  * remote-hardening `-c` set (only 4 git config keys are protected; every other
503
633
  * key is honored from a hostile repo-local `.git/config`, and `-c` = command
504
634
  * scope wins over repo-local):
@@ -510,7 +640,8 @@ export class GitRunner {
510
640
  * `core.askPass` is consulted BEFORE the `GIT_TERMINAL_PROMPT=0` gate).
511
641
  * - `core.sshCommand=` / `core.gitProxy=` (RCE keys; HTTPS-only makes them moot,
512
642
  * pinned anyway); `protocol.{file,ext,git}.allow=never` (a repo-local
513
- * `protocol.ext.allow=always` self-enables `ext::` RCE).
643
+ * `protocol.ext.allow=always` self-enables `ext::` RCE); `push.gpgSign=false`
644
+ * (a repo-local signing program runs on push, the credential in its env).
514
645
  * - `remote.<name>.{upload,receive}pack` pinned (the remote NAME is
515
646
  * server-known, so these ARE pinnable) — belt with `protocol.file.allow=never`.
516
647
  * NOT `url.*.insteadOf`/`pushInsteadOf` (un-`-c`-clearable — closed by the
@@ -524,10 +655,25 @@ export class GitRunner {
524
655
  * (fetch/push). `mergeFetched` calls this with NO credential (S4).
525
656
  */
526
657
  #spawnGitRemote(args, opts = {}) {
527
- const hardening = [...HARDENING_CONFIG];
658
+ if (this.#device) {
659
+ if (opts.credential)
660
+ return Promise.reject(new GitRemoteOnDeviceError());
661
+ if (!this.#device.owner)
662
+ return Promise.reject(new GitRemoteOwnerOnlyError());
663
+ }
664
+ else if (opts.ownerHelpers) {
665
+ return Promise.reject(new GitArgumentError('owner credential helpers are a device-only input'));
666
+ }
667
+ const hardening = hardeningConfig(this.#nullDevice);
668
+ if (opts.ownerHelpers === undefined)
669
+ hardening.push(...noGlobalFilesConfig(this.#nullDevice));
528
670
  hardening.push('core.askPass=', 'credential.interactive=false'); // S3
529
671
  hardening.push('core.sshCommand=', 'core.gitProxy=');
530
672
  hardening.push('protocol.file.allow=never', 'protocol.ext.allow=never', 'protocol.git.allow=never');
673
+ // A repo-local `push.gpgSign` + `gpg.program` runs a program of the repo's
674
+ // choosing on push (measured, git 2.43.0, against a receiver that asks for
675
+ // a push certificate) — with the push credential in its environment.
676
+ hardening.push('push.gpgSign=false');
531
677
  if (opts.remoteName !== undefined) {
532
678
  const remote = _a.#guardRemoteName(opts.remoteName);
533
679
  hardening.push(`remote.${remote}.uploadpack=git-upload-pack`, `remote.${remote}.receivepack=git-receive-pack`);
@@ -556,8 +702,18 @@ export class GitRunner {
556
702
  env.NRS_GIT_USER = opts.credential.username;
557
703
  env.NRS_GIT_TOKEN = opts.credential.token;
558
704
  }
705
+ if (opts.ownerHelpers) {
706
+ // S1 on the device: the same empty reset FIRST (a hostile repo-local helper
707
+ // never runs and never receives the owner's credential), then the owner's own.
708
+ hardening.push('credential.helper=', ...opts.ownerHelpers);
709
+ }
559
710
  const fullArgs = [...hardening.flatMap((c) => ['-c', c]), ...args];
560
- return this.#execSpawn(fullArgs, env, opts.allowExitCodes ? { allowExitCodes: opts.allowExitCodes } : {});
711
+ const exit = opts.allowExitCodes ? { allowExitCodes: opts.allowExitCodes } : {};
712
+ // The owner's own fetch / push (it alone carries their helpers) is the op a
713
+ // sandboxed device runs unconfined; the GAP-HTTP probe and the pull's merge stay confined.
714
+ if (this.#device)
715
+ return this.#execDevice(this.#device, fullArgs, {}, { ...exit, ownerRemote: opts.ownerHelpers !== undefined });
716
+ return this.#execSpawn(fullArgs, env, exit);
561
717
  }
562
718
  // --- Containment guards (floors 1 & 2) ----------------------------------
563
719
  /** Reject a caller ref that could be parsed as an option or carries a NUL. */
@@ -594,12 +750,24 @@ export class GitRunner {
594
750
  if (p.startsWith('-')) {
595
751
  throw new GitArgumentError(`git path may not begin with '-': "${p}"`);
596
752
  }
597
- const resolved = path.resolve(this.#root, p);
598
- if (resolved !== this.#root && !resolved.startsWith(this.#root + path.sep)) {
753
+ const resolved = this.#path.resolve(this.#root, p);
754
+ if (!this.#within(this.#root, resolved)) {
599
755
  throw new GitOutsideSourceError(this.#root, resolved);
600
756
  }
601
757
  return p; // pass the ORIGINAL token to git (cwd is already the root)
602
758
  }
759
+ /**
760
+ * `child` at or below `parent` in the checkout machine's own path arithmetic —
761
+ * a Windows device compares case-insensitively, and git answers there with
762
+ * forward slashes (`C:/x`), which `win32.resolve` normalizes.
763
+ */
764
+ #within(parent, child) {
765
+ const p = this.#path;
766
+ const fold = (s) => (p.sep === '\\' ? p.resolve(s).toLowerCase() : p.resolve(s));
767
+ const a = fold(parent);
768
+ const b = fold(child);
769
+ return a === b || b.startsWith(a.endsWith(p.sep) ? a : a + p.sep);
770
+ }
603
771
  /**
604
772
  * Verify the repo's toplevel is at or below the source root — a `.git`
605
773
  * ABOVE the source (source is a subdir of a bigger repo) is rejected so git
@@ -617,11 +785,14 @@ export class GitRunner {
617
785
  // realpath check on the `.git` entry's own location — is caught here.
618
786
  let lines;
619
787
  try {
620
- lines = (await this.#git.raw(['rev-parse', '--show-toplevel', '--absolute-git-dir']))
788
+ lines = (await this.#spawnGit(['rev-parse', '--show-toplevel', '--absolute-git-dir']))
621
789
  .trim()
622
- .split('\n');
790
+ .split(/\r?\n/);
623
791
  }
624
- catch {
792
+ catch (error) {
793
+ // "Could not ask the device" is not "not a repository".
794
+ if (error instanceof GitDeviceUnavailableError)
795
+ throw error;
625
796
  throw new GitNotARepoError(this.#root);
626
797
  }
627
798
  const toplevel = (lines[0] ?? '').trim();
@@ -634,8 +805,8 @@ export class GitRunner {
634
805
  // (bind-mount REAL paths, no symlink component) — floor 1b below is what
635
806
  // makes containment symlink-robust, canonicalizing the root for the
636
807
  // git-dir clamp (`--absolute-git-dir` is itself git-canonicalized).
637
- const realTop = path.resolve(toplevel);
638
- if (realTop !== this.#root && !realTop.startsWith(this.#root + path.sep)) {
808
+ const realTop = this.#path.resolve(toplevel);
809
+ if (!this.#within(this.#root, realTop)) {
639
810
  throw new GitOutsideSourceError(this.#root, realTop);
640
811
  }
641
812
  // Floor 1b (G2.5) — the EFFECTIVE git-dir at/below the source root.
@@ -644,9 +815,12 @@ export class GitRunner {
644
815
  // legitimately symlinked mount path (the same reason discovery realpaths
645
816
  // its root) would false-reject a normal `<root>/.git`. Fail-closed: a
646
817
  // redirect OUTSIDE the canonical root throws (readers degrade to null).
647
- const realGitDir = path.resolve(gitDir);
648
- const canonRoot = await realpath(this.#root).catch(() => this.#root);
649
- if (realGitDir !== canonRoot && !realGitDir.startsWith(canonRoot + path.sep)) {
818
+ // On a device this process cannot `realpath` the root: it is compared as
819
+ // stored, so a device root spelled through a symlink fails CLOSED (no git
820
+ // there), never open.
821
+ const realGitDir = this.#path.resolve(gitDir);
822
+ const canonRoot = this.#device ? this.#root : await realpath(this.#root).catch(() => this.#root);
823
+ if (!this.#within(canonRoot, realGitDir)) {
650
824
  throw new GitOutsideSourceError(this.#root, realGitDir);
651
825
  }
652
826
  })();
@@ -668,20 +842,11 @@ export class GitRunner {
668
842
  */
669
843
  async status(subPath) {
670
844
  await this.#assertRepoInsideSource();
671
- const args = ['--untracked-files=all'];
845
+ const args = ['status', '--porcelain=v2', '--branch', '--untracked-files=all', '-z'];
672
846
  if (subPath !== undefined && subPath !== '') {
673
847
  args.push('--', this.#guardPath(subPath));
674
848
  }
675
- let result;
676
- try {
677
- result = await this.#git.status(args);
678
- }
679
- catch (err) {
680
- if (isGitTimeout(err))
681
- throw new GitTimeoutError(this.#root);
682
- throw err;
683
- }
684
- return normalizeStatus(result);
849
+ return parseStatusPorcelainV2(await this.#spawnGit(args));
685
850
  }
686
851
  /**
687
852
  * Diff WT↔Index (default), Index↔HEAD (`staged`), or ref↔ref. Paths (if any)
@@ -814,7 +979,7 @@ export class GitRunner {
814
979
  async isUntracked(p) {
815
980
  await this.#assertRepoInsideSource();
816
981
  const safe = this.#guardPath(p);
817
- const out = await this.#git.raw(['ls-files', '--', safe]);
982
+ const out = await this.#spawnGit(['ls-files', '--', safe]);
818
983
  return out.trim().length === 0;
819
984
  }
820
985
  /**
@@ -831,19 +996,23 @@ export class GitRunner {
831
996
  }
832
997
  async log(opts = {}) {
833
998
  await this.#assertRepoInsideSource();
834
- const options = {};
999
+ const args = ['log', `--pretty=format:%H${GIT_LOG_FS}%ai${GIT_LOG_FS}%s${GIT_LOG_FS}%an${GIT_LOG_FS}%ae%x00`];
835
1000
  if (opts.maxCount && opts.maxCount > 0)
836
- options.maxCount = opts.maxCount;
1001
+ args.push(`--max-count=${Math.floor(opts.maxCount)}`);
837
1002
  if (opts.ref)
838
- options.from = _a.#guardRef(opts.ref);
839
- const result = await this.#git.log(options);
840
- return result.all.map((entry) => ({
841
- hash: entry.hash,
842
- date: entry.date,
843
- message: entry.message,
844
- author: entry.author_name,
845
- email: entry.author_email,
846
- }));
1003
+ args.push(_a.#guardRef(opts.ref));
1004
+ const out = await this.#spawnGit(args);
1005
+ const entries = [];
1006
+ for (const raw of out.split('\x00')) {
1007
+ const record = raw.replace(/^\n+/, '');
1008
+ if (!record)
1009
+ continue;
1010
+ const [hash, date, message, author, email] = record.split(GIT_LOG_FS);
1011
+ if (!hash)
1012
+ continue;
1013
+ entries.push({ hash, date: date ?? '', message: message ?? '', author: author ?? '', email: email ?? '' });
1014
+ }
1015
+ return entries;
847
1016
  }
848
1017
  /**
849
1018
  * The commit DAG for the custom SVG commit graph (G2.3 / `GET /git/log`), WITH
@@ -987,7 +1156,7 @@ export class GitRunner {
987
1156
  await this.#assertRepoInsideSource();
988
1157
  let headSha;
989
1158
  try {
990
- headSha = (await this.#git.revparse(['--verify', '--quiet', 'HEAD'])).trim();
1159
+ headSha = (await this.#spawnGit(['rev-parse', '--verify', '--quiet', 'HEAD'])).trim();
991
1160
  }
992
1161
  catch {
993
1162
  return null;
@@ -996,7 +1165,7 @@ export class GitRunner {
996
1165
  return null;
997
1166
  let upstreamSha = null;
998
1167
  try {
999
- const up = (await this.#git.revparse(['--verify', '--quiet', '@{u}'])).trim();
1168
+ const up = (await this.#spawnGit(['rev-parse', '--verify', '--quiet', '@{u}'])).trim();
1000
1169
  upstreamSha = up || null;
1001
1170
  }
1002
1171
  catch {
@@ -1066,12 +1235,12 @@ export class GitRunner {
1066
1235
  }
1067
1236
  async branchList() {
1068
1237
  await this.#assertRepoInsideSource();
1069
- const summary = await this.#git.branchLocal();
1070
- return summary.all.map((name) => ({
1071
- name,
1072
- current: name === summary.current,
1073
- commit: summary.branches[name]?.commit ?? '',
1074
- }));
1238
+ const out = await this.#spawnGit([
1239
+ 'for-each-ref',
1240
+ `--format=%(HEAD)${GIT_LOG_FS}%(refname:short)${GIT_LOG_FS}%(objectname:short)`,
1241
+ 'refs/heads',
1242
+ ]);
1243
+ return parseBranchList(out);
1075
1244
  }
1076
1245
  /**
1077
1246
  * The current branch name — CHEAP: `git rev-parse --abbrev-ref HEAD` reads
@@ -1084,7 +1253,7 @@ export class GitRunner {
1084
1253
  async currentBranch() {
1085
1254
  await this.#assertRepoInsideSource();
1086
1255
  try {
1087
- const out = (await this.#git.revparse(['--abbrev-ref', 'HEAD'])).trim();
1256
+ const out = (await this.#spawnGit(['rev-parse', '--abbrev-ref', 'HEAD'])).trim();
1088
1257
  if (!out || out === 'HEAD')
1089
1258
  return null;
1090
1259
  return out;
@@ -1099,7 +1268,7 @@ export class GitRunner {
1099
1268
  async add(paths) {
1100
1269
  await this.#assertRepoInsideSource();
1101
1270
  const safe = paths.map((p) => this.#guardPath(p));
1102
- await this.#git.add(safe);
1271
+ await this.#spawnGit(['add', '--', ...safe]);
1103
1272
  }
1104
1273
  /**
1105
1274
  * Reset the index. Whole-tree mode/ref reset (`git reset [--mode] [ref]`) OR —
@@ -1111,7 +1280,7 @@ export class GitRunner {
1111
1280
  await this.#assertRepoInsideSource();
1112
1281
  if (opts.paths?.length) {
1113
1282
  const safe = opts.paths.map((p) => this.#guardPath(p));
1114
- await this.#git.raw(['reset', '-q', '--', ...safe]);
1283
+ await this.#spawnGit(['reset', '-q', '--', ...safe]);
1115
1284
  return;
1116
1285
  }
1117
1286
  const args = [];
@@ -1119,14 +1288,14 @@ export class GitRunner {
1119
1288
  args.push(`--${opts.mode}`);
1120
1289
  if (opts.ref)
1121
1290
  args.push(_a.#guardRef(opts.ref));
1122
- await this.#git.raw(['reset', ...args]);
1291
+ await this.#spawnGit(['reset', ...args]);
1123
1292
  }
1124
1293
  async checkout(ref, paths) {
1125
1294
  await this.#assertRepoInsideSource();
1126
1295
  const args = [_a.#guardRef(ref)];
1127
1296
  if (paths?.length)
1128
1297
  args.push('--', ...paths.map((p) => this.#guardPath(p)));
1129
- await this.#git.checkout(args);
1298
+ await this.#spawnGit(['checkout', ...args]);
1130
1299
  }
1131
1300
  /**
1132
1301
  * Discard working-tree changes for `paths` by restoring them from the index
@@ -1140,15 +1309,15 @@ export class GitRunner {
1140
1309
  if (!paths.length)
1141
1310
  throw new GitArgumentError('restore requires at least one path');
1142
1311
  const safe = paths.map((p) => this.#guardPath(p));
1143
- await this.#git.raw(['checkout', '-q', '--', ...safe]);
1312
+ await this.#spawnGit(['checkout', '-q', '--', ...safe]);
1144
1313
  }
1145
1314
  async commit(message, opts = {}) {
1146
1315
  await this.#assertRepoInsideSource();
1147
1316
  // `-m` consumes the message as its value → a leading-dash message is safe.
1148
- const result = opts.amend
1149
- ? await this.#git.commit(message, [], { '--amend': null })
1150
- : await this.#git.commit(message);
1151
- return result.commit;
1317
+ if (typeof message !== 'string')
1318
+ throw new GitArgumentError('commit message must be a string');
1319
+ await this.#spawnGit(['commit', '-q', '-m', message, ...(opts.amend ? ['--amend'] : [])]);
1320
+ return (await this.#spawnGit(['rev-parse', '--short', 'HEAD'])).trim();
1152
1321
  }
1153
1322
  /**
1154
1323
  * Apply a unified-diff PATCH fed on STDIN (`git apply`) — the G2.3 whole-hunk
@@ -1188,9 +1357,9 @@ export class GitRunner {
1188
1357
  let sha = null;
1189
1358
  try {
1190
1359
  const target = opts.ref ? _a.#guardRef(opts.ref) : 'HEAD';
1191
- const resolved = (await this.#git.raw(['rev-parse', '--verify', '--quiet', target])).trim();
1360
+ const resolved = (await this.#spawnGit(['rev-parse', '--verify', '--quiet', target])).trim();
1192
1361
  if (resolved) {
1193
- await this.#git.raw(['update-ref', ref, resolved]);
1362
+ await this.#spawnGit(['update-ref', ref, resolved]);
1194
1363
  sha = resolved;
1195
1364
  }
1196
1365
  }
@@ -1200,9 +1369,9 @@ export class GitRunner {
1200
1369
  // 2. Capture working-tree+index content WITHOUT mutating the tree (discard recovery).
1201
1370
  let stash = null;
1202
1371
  try {
1203
- const created = (await this.#git.raw(['stash', 'create', `nrs-rescue/${safe}`])).trim();
1372
+ const created = (await this.#spawnGit(['stash', 'create', `nrs-rescue/${safe}`])).trim();
1204
1373
  if (created) {
1205
- await this.#git.raw(['stash', 'store', '-m', `nrs-rescue/${safe}`, created]);
1374
+ await this.#spawnGit(['stash', 'store', '-m', `nrs-rescue/${safe}`, created]);
1206
1375
  stash = created;
1207
1376
  }
1208
1377
  }
@@ -1227,7 +1396,7 @@ export class GitRunner {
1227
1396
  */
1228
1397
  async restoreBranch(name, ref) {
1229
1398
  await this.#assertRepoInsideSource();
1230
- await this.#git.raw(['branch', _a.#guardRef(name), _a.#guardRef(ref)]);
1399
+ await this.#spawnGit(['branch', _a.#guardRef(name), _a.#guardRef(ref)]);
1231
1400
  }
1232
1401
  /**
1233
1402
  * Re-materialize working-tree content discarded by `discard`, from its rescue
@@ -1237,23 +1406,23 @@ export class GitRunner {
1237
1406
  */
1238
1407
  async restoreStash(stashRef) {
1239
1408
  await this.#assertRepoInsideSource();
1240
- await this.#git.raw(['stash', 'apply', _a.#guardRef(stashRef)]);
1409
+ await this.#spawnGit(['stash', 'apply', _a.#guardRef(stashRef)]);
1241
1410
  }
1242
1411
  async stash(args = ['push']) {
1243
1412
  await this.#assertRepoInsideSource();
1244
1413
  // Only the leading subcommand is caller-influenceable in G3; refs guarded there.
1245
- await this.#git.stash(args);
1414
+ await this.#spawnGit(['stash', ...args]);
1246
1415
  }
1247
1416
  async branchCreate(name, startPoint) {
1248
1417
  await this.#assertRepoInsideSource();
1249
1418
  const args = [_a.#guardRef(name)];
1250
1419
  if (startPoint)
1251
1420
  args.push(_a.#guardRef(startPoint));
1252
- await this.#git.branch(args);
1421
+ await this.#spawnGit(['branch', ...args]);
1253
1422
  }
1254
1423
  async branchDelete(name, force = false) {
1255
1424
  await this.#assertRepoInsideSource();
1256
- await this.#git.branch([force ? '-D' : '-d', _a.#guardRef(name)]);
1425
+ await this.#spawnGit(['branch', force ? '-D' : '-d', _a.#guardRef(name)]);
1257
1426
  }
1258
1427
  // --- Remote primitives (G4 keyring-kestrel Slice 2) ----------------------
1259
1428
  // fetch / push / pull, each through `#spawnGitRemote` (raw child_process,
@@ -1357,8 +1526,12 @@ export class GitRunner {
1357
1526
  }
1358
1527
  }
1359
1528
  async fetch(opts = {}) {
1360
- await this.#assertRepoInsideSource();
1361
1529
  const remote = opts.remote ?? 'origin';
1530
+ if (this.#device) {
1531
+ await this.#deviceOwnerRemote(['fetch', _a.#guardRemoteName(remote)], remote, false, opts.credential);
1532
+ return;
1533
+ }
1534
+ await this.#assertRepoInsideSource();
1362
1535
  if (opts.credential)
1363
1536
  await this.#assertSafeRemoteHttpConfig(remote, opts.credential.host, false);
1364
1537
  await this.#spawnGitRemote(['fetch', _a.#guardRemoteName(remote)], {
@@ -1366,14 +1539,78 @@ export class GitRunner {
1366
1539
  ...(opts.credential ? { credential: opts.credential } : {}),
1367
1540
  });
1368
1541
  }
1542
+ /**
1543
+ * fetch / push on a paired device's folder: its OWNER only, with the owner's
1544
+ * own credential helpers, the same hardening `-c` set, the same HTTPS-only
1545
+ * remote and the same GAP-HTTP probe as this machine's ops. A server
1546
+ * credential is refused before anything runs; a non-owner is refused before
1547
+ * the owner's config is read.
1548
+ */
1549
+ async #deviceOwnerRemote(args, remote, push, credential) {
1550
+ if (credential)
1551
+ throw new GitRemoteOnDeviceError();
1552
+ if (!this.#device?.owner)
1553
+ throw new GitRemoteOwnerOnlyError();
1554
+ const host = await this.remoteHost(remote, { push });
1555
+ if (!host)
1556
+ throw new GitArgumentError('a device remote op needs an HTTPS remote');
1557
+ await this.#assertSafeRemoteHttpConfig(remote, host, push);
1558
+ const ownerHelpers = await this.#ownerCredentialHelpers();
1559
+ await this.#spawnGitRemote(args, { remoteName: remote, httpHost: host, ownerHelpers });
1560
+ }
1561
+ /**
1562
+ * The device owner's own credential helpers — the ONE place the global and
1563
+ * system config neutralisation is lifted, and only for the
1564
+ * `credential[.<url>].helper` keys of those two scopes (`--system`, then
1565
+ * `--global`: git's own order). Every other key of the owner's config stays
1566
+ * unread; the op itself runs with both neutralised. The read is `-z` (key,
1567
+ * newline, value, NUL), so a value carrying a newline is refused WHOLE — a
1568
+ * line read would carry its first line as a truncated helper. A device that
1569
+ * cannot run the read refuses the op (`GitDeviceUnavailableError`); any other
1570
+ * failure of one scope carries nothing from it.
1571
+ */
1572
+ async #ownerCredentialHelpers() {
1573
+ const seat = this.#device;
1574
+ if (!seat?.owner)
1575
+ throw new GitRemoteOwnerOnlyError();
1576
+ const helpers = [];
1577
+ for (const scope of ['--system', '--global']) {
1578
+ let out = '';
1579
+ try {
1580
+ out = await this.#execDevice(seat, [...hardeningConfig(this.#nullDevice).flatMap((c) => ['-c', c]), 'config', scope, '-z', '--get-regexp', '^credential(\\..+)?\\.helper$'], {}, { allowExitCodes: [1], ownerGlobalConfig: true, ownerRemote: true });
1581
+ }
1582
+ catch (error) {
1583
+ if (error instanceof GitDeviceUnavailableError)
1584
+ throw error;
1585
+ continue; // no such scope on this machine — nothing to carry
1586
+ }
1587
+ for (const entry of out.split('\0')) {
1588
+ const nl = entry.indexOf('\n');
1589
+ if (nl <= 0)
1590
+ continue;
1591
+ const key = entry.slice(0, nl);
1592
+ const value = entry.slice(nl + 1);
1593
+ if (!OWNER_HELPER_KEY_RE.test(key) || /[\0\r\n]/.test(value))
1594
+ continue;
1595
+ helpers.push(`${key}=${value}`);
1596
+ }
1597
+ }
1598
+ return helpers;
1599
+ }
1369
1600
  /**
1370
1601
  * Push the CURRENT branch to `remote` (`git push <remote> HEAD` — no refspec
1371
1602
  * from the caller, no `--force`/`--force-with-lease`, D6). The token rides the
1372
1603
  * child env (network-only invocation). `remote` defaults to `origin`, guarded.
1373
1604
  */
1374
1605
  async push(opts) {
1375
- await this.#assertRepoInsideSource();
1376
1606
  const remote = opts.remote ?? 'origin';
1607
+ if (this.#device) {
1608
+ await this.#deviceOwnerRemote(['push', _a.#guardRemoteName(remote), 'HEAD'], remote, true, opts.credential);
1609
+ return;
1610
+ }
1611
+ if (!opts.credential)
1612
+ throw new GitArgumentError('a push needs the connected remote credential');
1613
+ await this.#assertRepoInsideSource();
1377
1614
  await this.#assertSafeRemoteHttpConfig(remote, opts.credential.host, true);
1378
1615
  await this.#spawnGitRemote(['push', _a.#guardRemoteName(remote), 'HEAD'], {
1379
1616
  remoteName: remote,
@@ -1398,6 +1635,8 @@ export class GitRunner {
1398
1635
  * spawning a merge-driver / creating a merge commit silently.
1399
1636
  */
1400
1637
  async mergeFetched() {
1638
+ if (this.#device && !this.#device.owner)
1639
+ throw new GitRemoteOwnerOnlyError();
1401
1640
  await this.#assertRepoInsideSource();
1402
1641
  await this.#spawnGitRemote(['merge', '--ff-only', 'FETCH_HEAD']);
1403
1642
  }
@@ -1519,28 +1758,101 @@ export function parseHttpsRemoteHost(url) {
1519
1758
  }
1520
1759
  /** A syntactically-plausible remote host (defense-in-depth on the parse). */
1521
1760
  const GIT_REMOTE_HOST_RE = /^[a-z0-9.-]{1,255}$/;
1522
- /** Map a simple-git StatusResult into our staged/unstaged-explicit shape. */
1523
- export function normalizeStatus(result) {
1524
- const entries = result.files.map((file) => {
1525
- const index = file.index ?? ' ';
1526
- const workingTree = file.working_dir ?? ' ';
1527
- const untracked = index === '?' && workingTree === '?';
1528
- return {
1529
- path: file.path,
1761
+ /** `.` (unmodified) in a porcelain-v2 XY pair reads as the v1 blank. */
1762
+ function xyChar(c) {
1763
+ return !c || c === '.' ? ' ' : c;
1764
+ }
1765
+ /**
1766
+ * Parse `git status --porcelain=v2 --branch --untracked-files=all -z` into our
1767
+ * staged/unstaged-explicit shape. Pure — unit-tested standalone, and the ONE
1768
+ * status reader on both transports.
1769
+ *
1770
+ * The `-z` stream is NUL-separated records: `# branch.head|upstream|ab` headers,
1771
+ * `1 XY sub mH mI mW hH hI <path>` (ordinary), `2 XY … X<score> <path>` FOLLOWED
1772
+ * BY one more record, the ORIGINAL path (rename/copy — the row carries the NEW
1773
+ * path, as before), `u XY sub m1 m2 m3 mW h1 h2 h3 <path>` (unmerged), `? <path>`
1774
+ * (untracked) and `! <path>` (ignored — never listed). A path may contain
1775
+ * spaces, so the fixed fields are taken by position and the rest is the path.
1776
+ * `branch` is `null` for a detached HEAD; `upstream`/`ahead`/`behind` only when
1777
+ * git reported an upstream.
1778
+ */
1779
+ export function parseStatusPorcelainV2(out) {
1780
+ let branch = null;
1781
+ let upstream = null;
1782
+ let ahead = 0;
1783
+ let behind = 0;
1784
+ const entries = [];
1785
+ const records = out.split('\0');
1786
+ const push = (xy, filePath) => {
1787
+ const index = xyChar(xy[0]);
1788
+ const workingTree = xyChar(xy[1]);
1789
+ entries.push({
1790
+ path: filePath,
1530
1791
  index,
1531
1792
  workingTree,
1532
- staged: !untracked && index !== ' ',
1533
- unstaged: !untracked && workingTree !== ' ',
1534
- untracked,
1535
- };
1536
- });
1793
+ staged: index !== ' ',
1794
+ unstaged: workingTree !== ' ',
1795
+ untracked: false,
1796
+ });
1797
+ };
1798
+ for (let i = 0; i < records.length; i++) {
1799
+ const record = records[i];
1800
+ if (!record)
1801
+ continue;
1802
+ if (record.startsWith('# ')) {
1803
+ const [key, ...rest] = record.slice(2).split(' ');
1804
+ const value = rest.join(' ');
1805
+ if (key === 'branch.head')
1806
+ branch = value === '(detached)' ? null : value;
1807
+ else if (key === 'branch.upstream')
1808
+ upstream = value;
1809
+ else if (key === 'branch.ab') {
1810
+ const m = /^\+(\d+) -(\d+)$/.exec(value);
1811
+ if (m) {
1812
+ ahead = Number(m[1]);
1813
+ behind = Number(m[2]);
1814
+ }
1815
+ }
1816
+ continue;
1817
+ }
1818
+ const kind = record[0];
1819
+ const fields = record.split(' ');
1820
+ if (kind === '1')
1821
+ push(fields[1] ?? '..', fields.slice(8).join(' '));
1822
+ else if (kind === '2') {
1823
+ push(fields[1] ?? '..', fields.slice(9).join(' '));
1824
+ i += 1; // the next record is the rename/copy SOURCE path
1825
+ }
1826
+ else if (kind === 'u')
1827
+ push(fields[1] ?? '..', fields.slice(10).join(' '));
1828
+ else if (kind === '?') {
1829
+ entries.push({ path: record.slice(2), index: '?', workingTree: '?', staged: false, unstaged: false, untracked: true });
1830
+ }
1831
+ }
1537
1832
  return {
1538
- branch: result.current ?? null,
1539
- upstream: result.tracking ?? null,
1540
- ahead: result.ahead ?? 0,
1541
- behind: result.behind ?? 0,
1833
+ branch,
1834
+ upstream,
1835
+ ahead: upstream ? ahead : 0,
1836
+ behind: upstream ? behind : 0,
1542
1837
  entries,
1543
1838
  clean: entries.length === 0,
1544
1839
  };
1545
1840
  }
1841
+ /**
1842
+ * Parse the `for-each-ref --format=%(HEAD)<US>%(refname:short)<US>%(objectname:short)`
1843
+ * listing of `refs/heads` (one branch per line; `*` marks the checked-out one).
1844
+ * Pure — unit-tested standalone.
1845
+ */
1846
+ export function parseBranchList(out) {
1847
+ const branches = [];
1848
+ for (const line of out.split(/\r?\n/)) {
1849
+ if (!line)
1850
+ continue;
1851
+ const [head, name, commit] = line.split(GIT_LOG_FS);
1852
+ if (!name)
1853
+ continue;
1854
+ branches.push({ name, current: head === '*', commit: commit ?? '' });
1855
+ }
1856
+ return branches;
1857
+ }
1546
1858
  //# sourceMappingURL=GitRunner.js.map