apex-code 0.0.1-alpha.8 → 0.0.4

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 (634) hide show
  1. package/CHANGELOG.md +231 -0
  2. package/README.md +9 -8
  3. package/dist/cli/args.d.ts +26 -1
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +72 -10
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/cli/file-processor.d.ts.map +1 -1
  8. package/dist/cli/file-processor.js +2 -1
  9. package/dist/cli/file-processor.js.map +1 -1
  10. package/dist/cli/mcp-command.d.ts +9 -0
  11. package/dist/cli/mcp-command.d.ts.map +1 -0
  12. package/dist/cli/mcp-command.js +53 -0
  13. package/dist/cli/mcp-command.js.map +1 -0
  14. package/dist/cli/project-trust.d.ts.map +1 -1
  15. package/dist/cli/project-trust.js +3 -1
  16. package/dist/cli/project-trust.js.map +1 -1
  17. package/dist/cli/startup-ui.d.ts.map +1 -1
  18. package/dist/cli/startup-ui.js +2 -1
  19. package/dist/cli/startup-ui.js.map +1 -1
  20. package/dist/cli.d.ts.map +1 -1
  21. package/dist/cli.js +94 -5
  22. package/dist/cli.js.map +1 -1
  23. package/dist/config.d.ts +3 -2
  24. package/dist/config.d.ts.map +1 -1
  25. package/dist/config.js +45 -18
  26. package/dist/config.js.map +1 -1
  27. package/dist/core/agent-session-services.d.ts +3 -0
  28. package/dist/core/agent-session-services.d.ts.map +1 -1
  29. package/dist/core/agent-session-services.js +1 -0
  30. package/dist/core/agent-session-services.js.map +1 -1
  31. package/dist/core/agent-session.d.ts +183 -15
  32. package/dist/core/agent-session.d.ts.map +1 -1
  33. package/dist/core/agent-session.js +728 -117
  34. package/dist/core/agent-session.js.map +1 -1
  35. package/dist/core/auth-storage.d.ts.map +1 -1
  36. package/dist/core/auth-storage.js +6 -7
  37. package/dist/core/auth-storage.js.map +1 -1
  38. package/dist/core/bash-executor.d.ts.map +1 -1
  39. package/dist/core/bash-executor.js +7 -0
  40. package/dist/core/bash-executor.js.map +1 -1
  41. package/dist/core/checkpoints/git-checkpoints.d.ts +39 -0
  42. package/dist/core/checkpoints/git-checkpoints.d.ts.map +1 -0
  43. package/dist/core/checkpoints/git-checkpoints.js +251 -0
  44. package/dist/core/checkpoints/git-checkpoints.js.map +1 -0
  45. package/dist/core/checkpoints/session-checkpoints.d.ts +23 -0
  46. package/dist/core/checkpoints/session-checkpoints.d.ts.map +1 -0
  47. package/dist/core/checkpoints/session-checkpoints.js +36 -0
  48. package/dist/core/checkpoints/session-checkpoints.js.map +1 -0
  49. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  50. package/dist/core/compaction/branch-summarization.js +7 -3
  51. package/dist/core/compaction/branch-summarization.js.map +1 -1
  52. package/dist/core/compaction/compaction.d.ts +9 -3
  53. package/dist/core/compaction/compaction.d.ts.map +1 -1
  54. package/dist/core/compaction/compaction.js +59 -36
  55. package/dist/core/compaction/compaction.js.map +1 -1
  56. package/dist/core/defaults.d.ts +1 -0
  57. package/dist/core/defaults.d.ts.map +1 -1
  58. package/dist/core/defaults.js +9 -0
  59. package/dist/core/defaults.js.map +1 -1
  60. package/dist/core/delegation/runtime.d.ts +6 -5
  61. package/dist/core/delegation/runtime.d.ts.map +1 -1
  62. package/dist/core/delegation/runtime.js +4 -3
  63. package/dist/core/delegation/runtime.js.map +1 -1
  64. package/dist/core/durable-state/sqlite.d.ts.map +1 -1
  65. package/dist/core/durable-state/sqlite.js +26 -2
  66. package/dist/core/durable-state/sqlite.js.map +1 -1
  67. package/dist/core/exec.d.ts.map +1 -1
  68. package/dist/core/exec.js +5 -2
  69. package/dist/core/exec.js.map +1 -1
  70. package/dist/core/extensions/index.d.ts +2 -2
  71. package/dist/core/extensions/index.d.ts.map +1 -1
  72. package/dist/core/extensions/index.js +1 -1
  73. package/dist/core/extensions/index.js.map +1 -1
  74. package/dist/core/extensions/loader.d.ts.map +1 -1
  75. package/dist/core/extensions/loader.js +123 -50
  76. package/dist/core/extensions/loader.js.map +1 -1
  77. package/dist/core/extensions/runner.d.ts +5 -0
  78. package/dist/core/extensions/runner.d.ts.map +1 -1
  79. package/dist/core/extensions/runner.js +45 -1
  80. package/dist/core/extensions/runner.js.map +1 -1
  81. package/dist/core/extensions/source-runtime.d.ts +14 -0
  82. package/dist/core/extensions/source-runtime.d.ts.map +1 -0
  83. package/dist/core/extensions/source-runtime.js +56 -0
  84. package/dist/core/extensions/source-runtime.js.map +1 -0
  85. package/dist/core/extensions/types.d.ts +60 -11
  86. package/dist/core/extensions/types.d.ts.map +1 -1
  87. package/dist/core/extensions/types.js +3 -0
  88. package/dist/core/extensions/types.js.map +1 -1
  89. package/dist/core/footer-data-provider.d.ts +1 -1
  90. package/dist/core/footer-data-provider.d.ts.map +1 -1
  91. package/dist/core/footer-data-provider.js +1 -1
  92. package/dist/core/footer-data-provider.js.map +1 -1
  93. package/dist/core/formatter-lifecycle.d.ts +69 -0
  94. package/dist/core/formatter-lifecycle.d.ts.map +1 -0
  95. package/dist/core/formatter-lifecycle.js +360 -0
  96. package/dist/core/formatter-lifecycle.js.map +1 -0
  97. package/dist/core/hooks/command-handler.d.ts +19 -0
  98. package/dist/core/hooks/command-handler.d.ts.map +1 -0
  99. package/dist/core/hooks/command-handler.js +112 -0
  100. package/dist/core/hooks/command-handler.js.map +1 -0
  101. package/dist/core/hooks/http-handler.d.ts +10 -0
  102. package/dist/core/hooks/http-handler.d.ts.map +1 -0
  103. package/dist/core/hooks/http-handler.js +36 -0
  104. package/dist/core/hooks/http-handler.js.map +1 -0
  105. package/dist/core/hooks/loader.d.ts +29 -0
  106. package/dist/core/hooks/loader.d.ts.map +1 -0
  107. package/dist/core/hooks/loader.js +139 -0
  108. package/dist/core/hooks/loader.js.map +1 -0
  109. package/dist/core/hooks/runtime.d.ts +13 -0
  110. package/dist/core/hooks/runtime.d.ts.map +1 -0
  111. package/dist/core/hooks/runtime.js +63 -0
  112. package/dist/core/hooks/runtime.js.map +1 -0
  113. package/dist/core/hooks/types.d.ts +83 -0
  114. package/dist/core/hooks/types.d.ts.map +1 -0
  115. package/dist/core/hooks/types.js +24 -0
  116. package/dist/core/hooks/types.js.map +1 -0
  117. package/dist/core/keybindings.d.ts +20 -19
  118. package/dist/core/keybindings.d.ts.map +1 -1
  119. package/dist/core/keybindings.js +26 -5
  120. package/dist/core/keybindings.js.map +1 -1
  121. package/dist/core/mcp/config.d.ts +16 -0
  122. package/dist/core/mcp/config.d.ts.map +1 -0
  123. package/dist/core/mcp/config.js +166 -0
  124. package/dist/core/mcp/config.js.map +1 -0
  125. package/dist/core/mcp/connector.d.ts +35 -0
  126. package/dist/core/mcp/connector.d.ts.map +1 -0
  127. package/dist/core/mcp/connector.js +105 -0
  128. package/dist/core/mcp/connector.js.map +1 -0
  129. package/dist/core/mcp/contract.d.ts +19 -0
  130. package/dist/core/mcp/contract.d.ts.map +1 -0
  131. package/dist/core/mcp/contract.js +89 -0
  132. package/dist/core/mcp/contract.js.map +1 -0
  133. package/dist/core/mcp/mcp-tool.d.ts +32 -0
  134. package/dist/core/mcp/mcp-tool.d.ts.map +1 -0
  135. package/dist/core/mcp/mcp-tool.js +171 -0
  136. package/dist/core/mcp/mcp-tool.js.map +1 -0
  137. package/dist/core/mcp/metadata-cache.d.ts +29 -0
  138. package/dist/core/mcp/metadata-cache.d.ts.map +1 -0
  139. package/dist/core/mcp/metadata-cache.js +99 -0
  140. package/dist/core/mcp/metadata-cache.js.map +1 -0
  141. package/dist/core/mcp/oauth/authorize.d.ts +21 -0
  142. package/dist/core/mcp/oauth/authorize.d.ts.map +1 -0
  143. package/dist/core/mcp/oauth/authorize.js +32 -0
  144. package/dist/core/mcp/oauth/authorize.js.map +1 -0
  145. package/dist/core/mcp/oauth/discover.d.ts +27 -0
  146. package/dist/core/mcp/oauth/discover.d.ts.map +1 -0
  147. package/dist/core/mcp/oauth/discover.js +116 -0
  148. package/dist/core/mcp/oauth/discover.js.map +1 -0
  149. package/dist/core/mcp/oauth/flow.d.ts +31 -0
  150. package/dist/core/mcp/oauth/flow.d.ts.map +1 -0
  151. package/dist/core/mcp/oauth/flow.js +175 -0
  152. package/dist/core/mcp/oauth/flow.js.map +1 -0
  153. package/dist/core/mcp/oauth/mcp-token.d.ts +51 -0
  154. package/dist/core/mcp/oauth/mcp-token.d.ts.map +1 -0
  155. package/dist/core/mcp/oauth/mcp-token.js +129 -0
  156. package/dist/core/mcp/oauth/mcp-token.js.map +1 -0
  157. package/dist/core/mcp/oauth/pkce.d.ts +12 -0
  158. package/dist/core/mcp/oauth/pkce.d.ts.map +1 -0
  159. package/dist/core/mcp/oauth/pkce.js +11 -0
  160. package/dist/core/mcp/oauth/pkce.js.map +1 -0
  161. package/dist/core/mcp/oauth/register.d.ts +16 -0
  162. package/dist/core/mcp/oauth/register.d.ts.map +1 -0
  163. package/dist/core/mcp/oauth/register.js +42 -0
  164. package/dist/core/mcp/oauth/register.js.map +1 -0
  165. package/dist/core/mcp/oauth/token-client.d.ts +33 -0
  166. package/dist/core/mcp/oauth/token-client.d.ts.map +1 -0
  167. package/dist/core/mcp/oauth/token-client.js +65 -0
  168. package/dist/core/mcp/oauth/token-client.js.map +1 -0
  169. package/dist/core/mcp/runtime.d.ts +20 -0
  170. package/dist/core/mcp/runtime.d.ts.map +1 -0
  171. package/dist/core/mcp/runtime.js +37 -0
  172. package/dist/core/mcp/runtime.js.map +1 -0
  173. package/dist/core/mcp/schema.d.ts +22 -0
  174. package/dist/core/mcp/schema.d.ts.map +1 -0
  175. package/dist/core/mcp/schema.js +20 -0
  176. package/dist/core/mcp/schema.js.map +1 -0
  177. package/dist/core/mcp/server-manager.d.ts +43 -0
  178. package/dist/core/mcp/server-manager.d.ts.map +1 -0
  179. package/dist/core/mcp/server-manager.js +142 -0
  180. package/dist/core/mcp/server-manager.js.map +1 -0
  181. package/dist/core/mcp/types.d.ts +100 -0
  182. package/dist/core/mcp/types.d.ts.map +1 -0
  183. package/dist/core/mcp/types.js +8 -0
  184. package/dist/core/mcp/types.js.map +1 -0
  185. package/dist/core/model-config.d.ts +5 -0
  186. package/dist/core/model-config.d.ts.map +1 -1
  187. package/dist/core/model-config.js +3 -1
  188. package/dist/core/model-config.js.map +1 -1
  189. package/dist/core/model-resolver.d.ts +1 -0
  190. package/dist/core/model-resolver.d.ts.map +1 -1
  191. package/dist/core/model-resolver.js +14 -8
  192. package/dist/core/model-resolver.js.map +1 -1
  193. package/dist/core/models-store.d.ts.map +1 -1
  194. package/dist/core/models-store.js +2 -1
  195. package/dist/core/models-store.js.map +1 -1
  196. package/dist/core/package-manager.d.ts.map +1 -1
  197. package/dist/core/package-manager.js +22 -14
  198. package/dist/core/package-manager.js.map +1 -1
  199. package/dist/core/permissions/gate.d.ts.map +1 -1
  200. package/dist/core/permissions/gate.js +45 -4
  201. package/dist/core/permissions/gate.js.map +1 -1
  202. package/dist/core/permissions/operations.d.ts +20 -0
  203. package/dist/core/permissions/operations.d.ts.map +1 -0
  204. package/dist/core/permissions/operations.js +9 -0
  205. package/dist/core/permissions/operations.js.map +1 -0
  206. package/dist/core/permissions/policy-command.d.ts +42 -0
  207. package/dist/core/permissions/policy-command.d.ts.map +1 -0
  208. package/dist/core/permissions/policy-command.js +44 -0
  209. package/dist/core/permissions/policy-command.js.map +1 -0
  210. package/dist/core/permissions/responder.d.ts +49 -2
  211. package/dist/core/permissions/responder.d.ts.map +1 -1
  212. package/dist/core/permissions/responder.js +20 -8
  213. package/dist/core/permissions/responder.js.map +1 -1
  214. package/dist/core/permissions/rules.d.ts.map +1 -1
  215. package/dist/core/permissions/rules.js +11 -1
  216. package/dist/core/permissions/rules.js.map +1 -1
  217. package/dist/core/permissions/store.d.ts +18 -0
  218. package/dist/core/permissions/store.d.ts.map +1 -1
  219. package/dist/core/permissions/store.js +89 -8
  220. package/dist/core/permissions/store.js.map +1 -1
  221. package/dist/core/pi-manifest.d.ts.map +1 -1
  222. package/dist/core/pi-manifest.js +2 -1
  223. package/dist/core/pi-manifest.js.map +1 -1
  224. package/dist/core/policy-executor.d.ts +47 -0
  225. package/dist/core/policy-executor.d.ts.map +1 -0
  226. package/dist/core/policy-executor.js +228 -0
  227. package/dist/core/policy-executor.js.map +1 -0
  228. package/dist/core/policy-loader.d.ts +72 -0
  229. package/dist/core/policy-loader.d.ts.map +1 -0
  230. package/dist/core/policy-loader.js +195 -0
  231. package/dist/core/policy-loader.js.map +1 -0
  232. package/dist/core/project-trust.d.ts +1 -1
  233. package/dist/core/project-trust.d.ts.map +1 -1
  234. package/dist/core/project-trust.js.map +1 -1
  235. package/dist/core/remote-catalog-provider.d.ts.map +1 -1
  236. package/dist/core/remote-catalog-provider.js +2 -1
  237. package/dist/core/remote-catalog-provider.js.map +1 -1
  238. package/dist/core/resource-loader.d.ts.map +1 -1
  239. package/dist/core/resource-loader.js +3 -2
  240. package/dist/core/resource-loader.js.map +1 -1
  241. package/dist/core/sandbox/bwrap-arguments.d.ts +41 -0
  242. package/dist/core/sandbox/bwrap-arguments.d.ts.map +1 -0
  243. package/dist/core/sandbox/bwrap-arguments.js +136 -0
  244. package/dist/core/sandbox/bwrap-arguments.js.map +1 -0
  245. package/dist/core/sandbox/child-entry.d.ts.map +1 -1
  246. package/dist/core/sandbox/child-entry.js +3 -1
  247. package/dist/core/sandbox/child-entry.js.map +1 -1
  248. package/dist/core/sandbox/cli-launch.d.ts +84 -4
  249. package/dist/core/sandbox/cli-launch.d.ts.map +1 -1
  250. package/dist/core/sandbox/cli-launch.js +115 -16
  251. package/dist/core/sandbox/cli-launch.js.map +1 -1
  252. package/dist/core/sandbox/cli-supervisor.d.ts +2 -0
  253. package/dist/core/sandbox/cli-supervisor.d.ts.map +1 -1
  254. package/dist/core/sandbox/cli-supervisor.js +50 -2
  255. package/dist/core/sandbox/cli-supervisor.js.map +1 -1
  256. package/dist/core/sandbox/full-access.d.ts +28 -0
  257. package/dist/core/sandbox/full-access.d.ts.map +1 -0
  258. package/dist/core/sandbox/full-access.js +63 -0
  259. package/dist/core/sandbox/full-access.js.map +1 -0
  260. package/dist/core/sandbox/git-identity.d.ts +57 -0
  261. package/dist/core/sandbox/git-identity.d.ts.map +1 -0
  262. package/dist/core/sandbox/git-identity.js +78 -0
  263. package/dist/core/sandbox/git-identity.js.map +1 -0
  264. package/dist/core/sandbox/host-approval.d.ts +42 -0
  265. package/dist/core/sandbox/host-approval.d.ts.map +1 -0
  266. package/dist/core/sandbox/host-approval.js +99 -0
  267. package/dist/core/sandbox/host-approval.js.map +1 -0
  268. package/dist/core/sandbox/linux-backend.d.ts +14 -0
  269. package/dist/core/sandbox/linux-backend.d.ts.map +1 -1
  270. package/dist/core/sandbox/linux-backend.js +158 -80
  271. package/dist/core/sandbox/linux-backend.js.map +1 -1
  272. package/dist/core/sandbox/macos-backend.d.ts +5 -0
  273. package/dist/core/sandbox/macos-backend.d.ts.map +1 -1
  274. package/dist/core/sandbox/macos-backend.js +164 -5
  275. package/dist/core/sandbox/macos-backend.js.map +1 -1
  276. package/dist/core/sandbox/network-proxy.d.ts +19 -0
  277. package/dist/core/sandbox/network-proxy.d.ts.map +1 -1
  278. package/dist/core/sandbox/network-proxy.js +93 -19
  279. package/dist/core/sandbox/network-proxy.js.map +1 -1
  280. package/dist/core/sandbox/policy.d.ts +8 -0
  281. package/dist/core/sandbox/policy.d.ts.map +1 -1
  282. package/dist/core/sandbox/policy.js +29 -12
  283. package/dist/core/sandbox/policy.js.map +1 -1
  284. package/dist/core/sandbox/profiles.d.ts +25 -0
  285. package/dist/core/sandbox/profiles.d.ts.map +1 -0
  286. package/dist/core/sandbox/profiles.js +23 -0
  287. package/dist/core/sandbox/profiles.js.map +1 -0
  288. package/dist/core/sandbox/rpc/command-client.d.ts +28 -0
  289. package/dist/core/sandbox/rpc/command-client.d.ts.map +1 -0
  290. package/dist/core/sandbox/rpc/command-client.js +86 -0
  291. package/dist/core/sandbox/rpc/command-client.js.map +1 -0
  292. package/dist/core/sandbox/rpc/command-proxy.d.ts +48 -0
  293. package/dist/core/sandbox/rpc/command-proxy.d.ts.map +1 -0
  294. package/dist/core/sandbox/rpc/command-proxy.js +120 -0
  295. package/dist/core/sandbox/rpc/command-proxy.js.map +1 -0
  296. package/dist/core/sandbox/rpc/credential-proxy.d.ts.map +1 -1
  297. package/dist/core/sandbox/rpc/credential-proxy.js +2 -96
  298. package/dist/core/sandbox/rpc/credential-proxy.js.map +1 -1
  299. package/dist/core/sandbox/rpc/framing.d.ts +35 -0
  300. package/dist/core/sandbox/rpc/framing.d.ts.map +1 -0
  301. package/dist/core/sandbox/rpc/framing.js +114 -0
  302. package/dist/core/sandbox/rpc/framing.js.map +1 -0
  303. package/dist/core/sandbox/rpc/git-credential-helper.d.ts +47 -0
  304. package/dist/core/sandbox/rpc/git-credential-helper.d.ts.map +1 -0
  305. package/dist/core/sandbox/rpc/git-credential-helper.js +222 -0
  306. package/dist/core/sandbox/rpc/git-credential-helper.js.map +1 -0
  307. package/dist/core/sandbox/rpc/git-credential-proxy.d.ts +66 -0
  308. package/dist/core/sandbox/rpc/git-credential-proxy.d.ts.map +1 -0
  309. package/dist/core/sandbox/rpc/git-credential-proxy.js +209 -0
  310. package/dist/core/sandbox/rpc/git-credential-proxy.js.map +1 -0
  311. package/dist/core/sandbox/supervisor-temp.d.ts +18 -0
  312. package/dist/core/sandbox/supervisor-temp.d.ts.map +1 -0
  313. package/dist/core/sandbox/supervisor-temp.js +21 -0
  314. package/dist/core/sandbox/supervisor-temp.js.map +1 -0
  315. package/dist/core/sandbox/supervisor.d.ts +12 -1
  316. package/dist/core/sandbox/supervisor.d.ts.map +1 -1
  317. package/dist/core/sandbox/supervisor.js +15 -1
  318. package/dist/core/sandbox/supervisor.js.map +1 -1
  319. package/dist/core/sandbox/terminal-handoff.d.ts +68 -0
  320. package/dist/core/sandbox/terminal-handoff.d.ts.map +1 -0
  321. package/dist/core/sandbox/terminal-handoff.js +221 -0
  322. package/dist/core/sandbox/terminal-handoff.js.map +1 -0
  323. package/dist/core/sandbox/terminal-size.d.ts +13 -0
  324. package/dist/core/sandbox/terminal-size.d.ts.map +1 -1
  325. package/dist/core/sandbox/terminal-size.js +23 -2
  326. package/dist/core/sandbox/terminal-size.js.map +1 -1
  327. package/dist/core/sdk.d.ts +24 -2
  328. package/dist/core/sdk.d.ts.map +1 -1
  329. package/dist/core/sdk.js +63 -4
  330. package/dist/core/sdk.js.map +1 -1
  331. package/dist/core/session-export.d.ts +4 -0
  332. package/dist/core/session-export.d.ts.map +1 -0
  333. package/dist/core/session-export.js +32 -0
  334. package/dist/core/session-export.js.map +1 -0
  335. package/dist/core/session-manager.d.ts +13 -1
  336. package/dist/core/session-manager.d.ts.map +1 -1
  337. package/dist/core/session-manager.js +34 -6
  338. package/dist/core/session-manager.js.map +1 -1
  339. package/dist/core/settings-diagnostics.d.ts +9 -0
  340. package/dist/core/settings-diagnostics.d.ts.map +1 -0
  341. package/dist/core/settings-diagnostics.js +21 -0
  342. package/dist/core/settings-diagnostics.js.map +1 -0
  343. package/dist/core/settings-manager.d.ts +172 -1
  344. package/dist/core/settings-manager.d.ts.map +1 -1
  345. package/dist/core/settings-manager.js +160 -11
  346. package/dist/core/settings-manager.js.map +1 -1
  347. package/dist/core/skills.d.ts.map +1 -1
  348. package/dist/core/skills.js +51 -33
  349. package/dist/core/skills.js.map +1 -1
  350. package/dist/core/slash-commands.d.ts.map +1 -1
  351. package/dist/core/slash-commands.js +4 -1
  352. package/dist/core/slash-commands.js.map +1 -1
  353. package/dist/core/system-prompt.d.ts.map +1 -1
  354. package/dist/core/system-prompt.js +8 -2
  355. package/dist/core/system-prompt.js.map +1 -1
  356. package/dist/core/tools/background-shell.d.ts +39 -0
  357. package/dist/core/tools/background-shell.d.ts.map +1 -0
  358. package/dist/core/tools/background-shell.js +81 -0
  359. package/dist/core/tools/background-shell.js.map +1 -0
  360. package/dist/core/tools/bash-command-segments.d.ts.map +1 -1
  361. package/dist/core/tools/bash-command-segments.js +2 -0
  362. package/dist/core/tools/bash-command-segments.js.map +1 -1
  363. package/dist/core/tools/bash.d.ts +44 -16
  364. package/dist/core/tools/bash.d.ts.map +1 -1
  365. package/dist/core/tools/bash.js +264 -32
  366. package/dist/core/tools/bash.js.map +1 -1
  367. package/dist/core/tools/contract-snapshot.d.ts +58 -0
  368. package/dist/core/tools/contract-snapshot.d.ts.map +1 -0
  369. package/dist/core/tools/contract-snapshot.js +51 -0
  370. package/dist/core/tools/contract-snapshot.js.map +1 -0
  371. package/dist/core/tools/contract.d.ts +53 -5
  372. package/dist/core/tools/contract.d.ts.map +1 -1
  373. package/dist/core/tools/contract.js +22 -2
  374. package/dist/core/tools/contract.js.map +1 -1
  375. package/dist/core/tools/delegate.d.ts +6 -4
  376. package/dist/core/tools/delegate.d.ts.map +1 -1
  377. package/dist/core/tools/delegate.js +21 -11
  378. package/dist/core/tools/delegate.js.map +1 -1
  379. package/dist/core/tools/edit-diff.d.ts +48 -5
  380. package/dist/core/tools/edit-diff.d.ts.map +1 -1
  381. package/dist/core/tools/edit-diff.js +217 -8
  382. package/dist/core/tools/edit-diff.js.map +1 -1
  383. package/dist/core/tools/edit.d.ts +8 -0
  384. package/dist/core/tools/edit.d.ts.map +1 -1
  385. package/dist/core/tools/edit.js +72 -10
  386. package/dist/core/tools/edit.js.map +1 -1
  387. package/dist/core/tools/find.d.ts.map +1 -1
  388. package/dist/core/tools/find.js +5 -2
  389. package/dist/core/tools/find.js.map +1 -1
  390. package/dist/core/tools/grep.d.ts.map +1 -1
  391. package/dist/core/tools/grep.js +5 -2
  392. package/dist/core/tools/grep.js.map +1 -1
  393. package/dist/core/tools/index.d.ts +14 -2
  394. package/dist/core/tools/index.d.ts.map +1 -1
  395. package/dist/core/tools/index.js +12 -0
  396. package/dist/core/tools/index.js.map +1 -1
  397. package/dist/core/tools/ls.d.ts.map +1 -1
  398. package/dist/core/tools/ls.js +5 -2
  399. package/dist/core/tools/ls.js.map +1 -1
  400. package/dist/core/tools/lsp.d.ts +19 -1
  401. package/dist/core/tools/lsp.d.ts.map +1 -1
  402. package/dist/core/tools/lsp.js +163 -10
  403. package/dist/core/tools/lsp.js.map +1 -1
  404. package/dist/core/tools/path-permission.d.ts +3 -0
  405. package/dist/core/tools/path-permission.d.ts.map +1 -1
  406. package/dist/core/tools/path-permission.js +16 -4
  407. package/dist/core/tools/path-permission.js.map +1 -1
  408. package/dist/core/tools/path-utils.d.ts +27 -2
  409. package/dist/core/tools/path-utils.d.ts.map +1 -1
  410. package/dist/core/tools/path-utils.js +221 -4
  411. package/dist/core/tools/path-utils.js.map +1 -1
  412. package/dist/core/tools/powershell.d.ts +16 -0
  413. package/dist/core/tools/powershell.d.ts.map +1 -0
  414. package/dist/core/tools/powershell.js +39 -0
  415. package/dist/core/tools/powershell.js.map +1 -0
  416. package/dist/core/tools/read.d.ts.map +1 -1
  417. package/dist/core/tools/read.js +20 -11
  418. package/dist/core/tools/read.js.map +1 -1
  419. package/dist/core/tools/test.d.ts +54 -9
  420. package/dist/core/tools/test.d.ts.map +1 -1
  421. package/dist/core/tools/test.js +239 -19
  422. package/dist/core/tools/test.js.map +1 -1
  423. package/dist/core/tools/write.d.ts +7 -0
  424. package/dist/core/tools/write.d.ts.map +1 -1
  425. package/dist/core/tools/write.js +64 -9
  426. package/dist/core/tools/write.js.map +1 -1
  427. package/dist/core/trust-manager.d.ts.map +1 -1
  428. package/dist/core/trust-manager.js +2 -1
  429. package/dist/core/trust-manager.js.map +1 -1
  430. package/dist/core/verification-lifecycle.d.ts +77 -0
  431. package/dist/core/verification-lifecycle.d.ts.map +1 -0
  432. package/dist/core/verification-lifecycle.js +146 -0
  433. package/dist/core/verification-lifecycle.js.map +1 -0
  434. package/dist/core/workspace/artifacts.d.ts +45 -0
  435. package/dist/core/workspace/artifacts.d.ts.map +1 -0
  436. package/dist/core/workspace/artifacts.js +165 -0
  437. package/dist/core/workspace/artifacts.js.map +1 -0
  438. package/dist/core/workspace/comparison.d.ts +27 -0
  439. package/dist/core/workspace/comparison.d.ts.map +1 -0
  440. package/dist/core/workspace/comparison.js +94 -0
  441. package/dist/core/workspace/comparison.js.map +1 -0
  442. package/dist/core/workspace/git-observer.d.ts +48 -0
  443. package/dist/core/workspace/git-observer.d.ts.map +1 -0
  444. package/dist/core/workspace/git-observer.js +346 -0
  445. package/dist/core/workspace/git-observer.js.map +1 -0
  446. package/dist/core/workspace/projection.d.ts +3 -0
  447. package/dist/core/workspace/projection.d.ts.map +1 -0
  448. package/dist/core/workspace/projection.js +56 -0
  449. package/dist/core/workspace/projection.js.map +1 -0
  450. package/dist/core/workspace/state.d.ts +133 -0
  451. package/dist/core/workspace/state.d.ts.map +1 -0
  452. package/dist/core/workspace/state.js +96 -0
  453. package/dist/core/workspace/state.js.map +1 -0
  454. package/dist/extensions/llama/client.d.ts +6 -0
  455. package/dist/extensions/llama/client.d.ts.map +1 -1
  456. package/dist/extensions/llama/client.js +7 -0
  457. package/dist/extensions/llama/client.js.map +1 -1
  458. package/dist/extensions/llama/index.d.ts.map +1 -1
  459. package/dist/extensions/llama/index.js +2 -0
  460. package/dist/extensions/llama/index.js.map +1 -1
  461. package/dist/extensions/llama/provider.d.ts +3 -1
  462. package/dist/extensions/llama/provider.d.ts.map +1 -1
  463. package/dist/extensions/llama/provider.js +29 -4
  464. package/dist/extensions/llama/provider.js.map +1 -1
  465. package/dist/index.d.ts +8 -5
  466. package/dist/index.d.ts.map +1 -1
  467. package/dist/index.js +7 -4
  468. package/dist/index.js.map +1 -1
  469. package/dist/main.d.ts +6 -0
  470. package/dist/main.d.ts.map +1 -1
  471. package/dist/main.js +58 -23
  472. package/dist/main.js.map +1 -1
  473. package/dist/migrations.d.ts.map +1 -1
  474. package/dist/migrations.js +4 -3
  475. package/dist/migrations.js.map +1 -1
  476. package/dist/modes/acp/server.d.ts +55 -0
  477. package/dist/modes/acp/server.d.ts.map +1 -0
  478. package/dist/modes/acp/server.js +244 -0
  479. package/dist/modes/acp/server.js.map +1 -0
  480. package/dist/modes/acp/translate.d.ts +37 -0
  481. package/dist/modes/acp/translate.d.ts.map +1 -0
  482. package/dist/modes/acp/translate.js +50 -0
  483. package/dist/modes/acp/translate.js.map +1 -0
  484. package/dist/modes/index.d.ts +1 -0
  485. package/dist/modes/index.d.ts.map +1 -1
  486. package/dist/modes/index.js +1 -0
  487. package/dist/modes/index.js.map +1 -1
  488. package/dist/modes/interactive/components/assistant-message.d.ts +29 -0
  489. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  490. package/dist/modes/interactive/components/assistant-message.js +182 -23
  491. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  492. package/dist/modes/interactive/components/bash-execution.d.ts +7 -0
  493. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  494. package/dist/modes/interactive/components/bash-execution.js +20 -0
  495. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  496. package/dist/modes/interactive/components/custom-editor.d.ts +9 -4
  497. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  498. package/dist/modes/interactive/components/custom-editor.js +20 -9
  499. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  500. package/dist/modes/interactive/components/extension-selector.d.ts +7 -1
  501. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  502. package/dist/modes/interactive/components/extension-selector.js +7 -3
  503. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  504. package/dist/modes/interactive/components/footer.d.ts +19 -0
  505. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  506. package/dist/modes/interactive/components/footer.js +70 -10
  507. package/dist/modes/interactive/components/footer.js.map +1 -1
  508. package/dist/modes/interactive/components/model-selector.d.ts +10 -3
  509. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  510. package/dist/modes/interactive/components/model-selector.js +56 -14
  511. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  512. package/dist/modes/interactive/components/permission-preview.d.ts +15 -0
  513. package/dist/modes/interactive/components/permission-preview.d.ts.map +1 -0
  514. package/dist/modes/interactive/components/permission-preview.js +33 -0
  515. package/dist/modes/interactive/components/permission-preview.js.map +1 -0
  516. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  517. package/dist/modes/interactive/components/session-selector.js +2 -2
  518. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  519. package/dist/modes/interactive/components/settings-selector.d.ts +9 -2
  520. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  521. package/dist/modes/interactive/components/settings-selector.js +128 -56
  522. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  523. package/dist/modes/interactive/components/settings-submenu.d.ts +72 -0
  524. package/dist/modes/interactive/components/settings-submenu.d.ts.map +1 -0
  525. package/dist/modes/interactive/components/settings-submenu.js +165 -0
  526. package/dist/modes/interactive/components/settings-submenu.js.map +1 -0
  527. package/dist/modes/interactive/components/thinking-selector.d.ts +15 -3
  528. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  529. package/dist/modes/interactive/components/thinking-selector.js +74 -17
  530. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  531. package/dist/modes/interactive/components/tool-execution.d.ts +23 -1
  532. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  533. package/dist/modes/interactive/components/tool-execution.js +64 -10
  534. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  535. package/dist/modes/interactive/external-editor.d.ts.map +1 -1
  536. package/dist/modes/interactive/external-editor.js +2 -1
  537. package/dist/modes/interactive/external-editor.js.map +1 -1
  538. package/dist/modes/interactive/interactive-mode.d.ts +45 -0
  539. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  540. package/dist/modes/interactive/interactive-mode.js +395 -43
  541. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  542. package/dist/modes/interactive/theme/theme.d.ts +12 -0
  543. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  544. package/dist/modes/interactive/theme/theme.js +20 -9
  545. package/dist/modes/interactive/theme/theme.js.map +1 -1
  546. package/dist/modes/json-event.d.ts +16 -13
  547. package/dist/modes/json-event.d.ts.map +1 -1
  548. package/dist/modes/json-event.js +20 -6
  549. package/dist/modes/json-event.js.map +1 -1
  550. package/dist/modes/print-mode.d.ts.map +1 -1
  551. package/dist/modes/print-mode.js +27 -12
  552. package/dist/modes/print-mode.js.map +1 -1
  553. package/dist/modes/rpc/rpc-client.d.ts +7 -0
  554. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  555. package/dist/modes/rpc/rpc-client.js +7 -0
  556. package/dist/modes/rpc/rpc-client.js.map +1 -1
  557. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  558. package/dist/modes/rpc/rpc-mode.js +3 -0
  559. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  560. package/dist/modes/rpc/rpc-types.d.ts +12 -0
  561. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  562. package/dist/modes/rpc/rpc-types.js.map +1 -1
  563. package/dist/package-manager-cli.d.ts +1 -0
  564. package/dist/package-manager-cli.d.ts.map +1 -1
  565. package/dist/package-manager-cli.js +196 -10
  566. package/dist/package-manager-cli.js.map +1 -1
  567. package/dist/utils/frontmatter.d.ts.map +1 -1
  568. package/dist/utils/frontmatter.js +2 -1
  569. package/dist/utils/frontmatter.js.map +1 -1
  570. package/dist/utils/management-http.d.ts +5 -3
  571. package/dist/utils/management-http.d.ts.map +1 -1
  572. package/dist/utils/management-http.js +14 -10
  573. package/dist/utils/management-http.js.map +1 -1
  574. package/dist/utils/shell.d.ts +3 -0
  575. package/dist/utils/shell.d.ts.map +1 -1
  576. package/dist/utils/shell.js +23 -9
  577. package/dist/utils/shell.js.map +1 -1
  578. package/dist/utils/syntax-highlight.d.ts +1 -0
  579. package/dist/utils/syntax-highlight.d.ts.map +1 -1
  580. package/dist/utils/syntax-highlight.js +60 -1
  581. package/dist/utils/syntax-highlight.js.map +1 -1
  582. package/dist/utils/text.d.ts +8 -0
  583. package/dist/utils/text.d.ts.map +1 -0
  584. package/dist/utils/text.js +9 -0
  585. package/dist/utils/text.js.map +1 -0
  586. package/dist/utils/version-check.d.ts.map +1 -1
  587. package/dist/utils/version-check.js +6 -4
  588. package/dist/utils/version-check.js.map +1 -1
  589. package/docs/compaction.md +19 -0
  590. package/docs/custom-provider.md +12 -9
  591. package/docs/environment-variables.md +1 -1
  592. package/docs/extensions.md +35 -7
  593. package/docs/json.md +7 -2
  594. package/docs/keybindings.md +9 -9
  595. package/docs/llama-cpp.md +2 -0
  596. package/docs/models.md +8 -2
  597. package/docs/packages.md +2 -2
  598. package/docs/quickstart.md +1 -1
  599. package/docs/rpc.md +32 -3
  600. package/docs/sdk.md +32 -2
  601. package/docs/settings.md +23 -7
  602. package/docs/skills.md +4 -3
  603. package/docs/terminal-setup.md +23 -7
  604. package/docs/usage.md +10 -5
  605. package/docs/windows.md +24 -2
  606. package/examples/extensions/border-status-editor.ts +1 -1
  607. package/examples/extensions/built-in-tool-renderer.ts +8 -4
  608. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  609. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  610. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  611. package/examples/extensions/git-checkpoint.ts +24 -36
  612. package/examples/extensions/gondolin/package-lock.json +2 -2
  613. package/examples/extensions/gondolin/package.json +1 -1
  614. package/examples/extensions/minimal-mode.ts +2 -2
  615. package/examples/extensions/notify.ts +3 -1
  616. package/examples/extensions/permission-gate.ts +1 -1
  617. package/examples/extensions/plan-mode/index.ts +1 -1
  618. package/examples/extensions/rpc-demo.ts +1 -1
  619. package/examples/extensions/sandbox/package-lock.json +2 -2
  620. package/examples/extensions/sandbox/package.json +1 -1
  621. package/examples/extensions/subagent/README.md +1 -1
  622. package/examples/extensions/subagent/index.ts +6 -1
  623. package/examples/extensions/titlebar-spinner.ts +1 -1
  624. package/examples/extensions/with-deps/package-lock.json +2 -2
  625. package/examples/extensions/with-deps/package.json +1 -1
  626. package/examples/rpc-extension-ui.ts +1 -1
  627. package/examples/sdk/06-extensions.ts +1 -1
  628. package/examples/sdk/README.md +1 -1
  629. package/npm-shrinkwrap.json +439 -405
  630. package/package.json +17 -17
  631. package/dist/server/create-harness.d.ts +0 -26
  632. package/dist/server/create-harness.d.ts.map +0 -1
  633. package/dist/server/create-harness.js +0 -107
  634. package/dist/server/create-harness.js.map +0 -1
@@ -0,0 +1,114 @@
1
+ /**
2
+ * The framing both supervisor-owned RPC channels share.
3
+ *
4
+ * Extracted from `credential-proxy.ts` when the git credential channel needed the same
5
+ * reader. A second hand-written copy of a byte-bounded parser sitting on a socket that
6
+ * every process inside the sandbox can reach is exactly the divergence ADR 0010 exists to
7
+ * prevent for tool contracts: a hardening applied to one copy and not the other is
8
+ * invisible until it matters. One implementation, named per channel only in its messages.
9
+ */
10
+ export const MAX_FRAME_BYTES = 64 * 1024;
11
+ export function writeFrame(socket, response) {
12
+ if (socket.destroyed)
13
+ return;
14
+ const frame = Buffer.from(`${JSON.stringify(response)}\n`, "utf8");
15
+ if (frame.length > MAX_FRAME_BYTES) {
16
+ throw new Error("Channel response exceeded its byte limit.");
17
+ }
18
+ socket.write(frame);
19
+ }
20
+ export function isRequestObject(value) {
21
+ return typeof value === "object" && value !== null && !Array.isArray(value);
22
+ }
23
+ /**
24
+ * One request frame at a time, newline-delimited JSON, bounded in bytes.
25
+ *
26
+ * Pipelining is refused rather than queued: a channel that answers a second request before
27
+ * the first is served makes the audit tail ambiguous about which frame a refusal belonged
28
+ * to, and neither channel has a caller that needs it.
29
+ */
30
+ export class FrameReader {
31
+ socket;
32
+ channelName;
33
+ onProtocolRefusal;
34
+ buffer = Buffer.alloc(0);
35
+ queued = [];
36
+ waiting = [];
37
+ terminalError;
38
+ constructor(socket, channelName, onProtocolRefusal) {
39
+ this.socket = socket;
40
+ this.channelName = channelName;
41
+ this.onProtocolRefusal = onProtocolRefusal;
42
+ socket.on("data", (chunk) => this.push(chunk));
43
+ socket.on("close", () => this.fail(new Error(`${channelName} client disconnected.`)));
44
+ socket.on("error", (error) => this.fail(error));
45
+ }
46
+ next() {
47
+ const queued = this.queued.shift();
48
+ if (queued !== undefined)
49
+ return Promise.resolve(queued);
50
+ if (this.terminalError)
51
+ return Promise.reject(this.terminalError);
52
+ return new Promise((resolve, reject) => this.waiting.push({ resolve, reject }));
53
+ }
54
+ push(chunk) {
55
+ if (this.terminalError)
56
+ return;
57
+ this.buffer = Buffer.concat([this.buffer, chunk]);
58
+ while (true) {
59
+ const newline = this.buffer.indexOf(0x0a);
60
+ if (newline < 0) {
61
+ if (this.buffer.length > MAX_FRAME_BYTES)
62
+ this.rejectOversizedFrame();
63
+ return;
64
+ }
65
+ if (newline > MAX_FRAME_BYTES) {
66
+ this.rejectOversizedFrame();
67
+ return;
68
+ }
69
+ const frame = this.buffer.subarray(0, newline);
70
+ this.buffer = this.buffer.subarray(newline + 1);
71
+ if (frame.length === 0)
72
+ continue;
73
+ let value;
74
+ try {
75
+ value = JSON.parse(frame.toString("utf8"));
76
+ }
77
+ catch {
78
+ this.onProtocolRefusal("Invalid request frame: not JSON.");
79
+ writeFrame(this.socket, { ok: false, error: "Invalid request frame: not JSON." });
80
+ this.fail(new Error(`${this.channelName} received an invalid JSON frame.`));
81
+ this.socket.end();
82
+ return;
83
+ }
84
+ const waiter = this.waiting.shift();
85
+ if (waiter) {
86
+ waiter.resolve(value);
87
+ }
88
+ else if (this.queued.length === 0) {
89
+ this.queued.push(value);
90
+ }
91
+ else {
92
+ this.onProtocolRefusal("Unexpected pipelined request frame.");
93
+ writeFrame(this.socket, { ok: false, error: "Unexpected pipelined request frame." });
94
+ this.fail(new Error(`${this.channelName} received an unexpected pipelined frame.`));
95
+ this.socket.end();
96
+ return;
97
+ }
98
+ }
99
+ }
100
+ rejectOversizedFrame() {
101
+ this.onProtocolRefusal("Request frame exceeded the 64 KiB byte limit.");
102
+ writeFrame(this.socket, { ok: false, error: "Request frame is too large." });
103
+ this.fail(new Error(`${this.channelName} frame exceeded its byte limit.`));
104
+ this.socket.end();
105
+ }
106
+ fail(error) {
107
+ if (this.terminalError)
108
+ return;
109
+ this.terminalError = error;
110
+ for (const waiter of this.waiting.splice(0))
111
+ waiter.reject(error);
112
+ }
113
+ }
114
+ //# sourceMappingURL=framing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"framing.js","sourceRoot":"","sources":["../../../../src/core/sandbox/rpc/framing.ts"],"names":[],"mappings":"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","sourcesContent":["import type * as net from \"node:net\";\n\n/**\n * The framing both supervisor-owned RPC channels share.\n *\n * Extracted from `credential-proxy.ts` when the git credential channel needed the same\n * reader. A second hand-written copy of a byte-bounded parser sitting on a socket that\n * every process inside the sandbox can reach is exactly the divergence ADR 0010 exists to\n * prevent for tool contracts: a hardening applied to one copy and not the other is\n * invisible until it matters. One implementation, named per channel only in its messages.\n */\n\nexport const MAX_FRAME_BYTES = 64 * 1024;\n\nexport function writeFrame(socket: net.Socket, response: object): void {\n\tif (socket.destroyed) return;\n\tconst frame = Buffer.from(`${JSON.stringify(response)}\\n`, \"utf8\");\n\tif (frame.length > MAX_FRAME_BYTES) {\n\t\tthrow new Error(\"Channel response exceeded its byte limit.\");\n\t}\n\tsocket.write(frame);\n}\n\nexport function isRequestObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * One request frame at a time, newline-delimited JSON, bounded in bytes.\n *\n * Pipelining is refused rather than queued: a channel that answers a second request before\n * the first is served makes the audit tail ambiguous about which frame a refusal belonged\n * to, and neither channel has a caller that needs it.\n */\nexport class FrameReader {\n\tprivate readonly socket: net.Socket;\n\tprivate readonly channelName: string;\n\tprivate readonly onProtocolRefusal: (detail: string) => void;\n\tprivate buffer = Buffer.alloc(0);\n\tprivate readonly queued: unknown[] = [];\n\tprivate readonly waiting: Array<{ resolve(value: unknown): void; reject(error: Error): void }> = [];\n\tprivate terminalError: Error | undefined;\n\n\tconstructor(socket: net.Socket, channelName: string, onProtocolRefusal: (detail: string) => void) {\n\t\tthis.socket = socket;\n\t\tthis.channelName = channelName;\n\t\tthis.onProtocolRefusal = onProtocolRefusal;\n\t\tsocket.on(\"data\", (chunk: Buffer) => this.push(chunk));\n\t\tsocket.on(\"close\", () => this.fail(new Error(`${channelName} client disconnected.`)));\n\t\tsocket.on(\"error\", (error) => this.fail(error));\n\t}\n\n\tnext(): Promise<unknown> {\n\t\tconst queued = this.queued.shift();\n\t\tif (queued !== undefined) return Promise.resolve(queued);\n\t\tif (this.terminalError) return Promise.reject(this.terminalError);\n\t\treturn new Promise((resolve, reject) => this.waiting.push({ resolve, reject }));\n\t}\n\n\tprivate push(chunk: Buffer): void {\n\t\tif (this.terminalError) return;\n\t\tthis.buffer = Buffer.concat([this.buffer, chunk]);\n\t\twhile (true) {\n\t\t\tconst newline = this.buffer.indexOf(0x0a);\n\t\t\tif (newline < 0) {\n\t\t\t\tif (this.buffer.length > MAX_FRAME_BYTES) this.rejectOversizedFrame();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (newline > MAX_FRAME_BYTES) {\n\t\t\t\tthis.rejectOversizedFrame();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst frame = this.buffer.subarray(0, newline);\n\t\t\tthis.buffer = this.buffer.subarray(newline + 1);\n\t\t\tif (frame.length === 0) continue;\n\t\t\tlet value: unknown;\n\t\t\ttry {\n\t\t\t\tvalue = JSON.parse(frame.toString(\"utf8\")) as unknown;\n\t\t\t} catch {\n\t\t\t\tthis.onProtocolRefusal(\"Invalid request frame: not JSON.\");\n\t\t\t\twriteFrame(this.socket, { ok: false, error: \"Invalid request frame: not JSON.\" });\n\t\t\t\tthis.fail(new Error(`${this.channelName} received an invalid JSON frame.`));\n\t\t\t\tthis.socket.end();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst waiter = this.waiting.shift();\n\t\t\tif (waiter) {\n\t\t\t\twaiter.resolve(value);\n\t\t\t} else if (this.queued.length === 0) {\n\t\t\t\tthis.queued.push(value);\n\t\t\t} else {\n\t\t\t\tthis.onProtocolRefusal(\"Unexpected pipelined request frame.\");\n\t\t\t\twriteFrame(this.socket, { ok: false, error: \"Unexpected pipelined request frame.\" });\n\t\t\t\tthis.fail(new Error(`${this.channelName} received an unexpected pipelined frame.`));\n\t\t\t\tthis.socket.end();\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate rejectOversizedFrame(): void {\n\t\tthis.onProtocolRefusal(\"Request frame exceeded the 64 KiB byte limit.\");\n\t\twriteFrame(this.socket, { ok: false, error: \"Request frame is too large.\" });\n\t\tthis.fail(new Error(`${this.channelName} frame exceeded its byte limit.`));\n\t\tthis.socket.end();\n\t}\n\n\tprivate fail(error: Error): void {\n\t\tif (this.terminalError) return;\n\t\tthis.terminalError = error;\n\t\tfor (const waiter of this.waiting.splice(0)) waiter.reject(error);\n\t}\n}\n"]}
@@ -0,0 +1,47 @@
1
+ import { type GitCredential, type GitCredentialRequest } from "./git-credential-proxy.ts";
2
+ /** Env var naming the child-side socket the helper talks to. */
3
+ export declare const GIT_CREDENTIAL_SOCKET_VARIABLE = "APEX_GIT_CREDENTIAL_PATH";
4
+ export interface GitCredentialChannelPaths {
5
+ readonly hostSocketDirectory: string;
6
+ readonly hostSocketPath: string;
7
+ readonly childSocketPath: string;
8
+ }
9
+ /**
10
+ * Where the git credential socket lives on each side, mirroring the credential channel's
11
+ * own resolution. TMPDIR is deliberately not honoured: it can point inside the workspace
12
+ * on macOS, which the sandbox may write. The child-side path sits under `/home` on Linux
13
+ * because the `--tmpfs /home` is the only writable mount when bwrap creates the mountpoint.
14
+ */
15
+ export declare function resolveGitCredentialChannelPaths(): GitCredentialChannelPaths;
16
+ /**
17
+ * Where the helper lives, derived rather than passed.
18
+ *
19
+ * The supervisor writes `credential.helper` into the projected git config before the
20
+ * platform backend exists, and the backend writes the file itself. Deriving both from the
21
+ * state directory is what keeps the config from naming a path nothing ever created.
22
+ */
23
+ export declare function gitCredentialHelperPath(stateDirectory: string): string;
24
+ /** The `credential.helper` value naming that helper, run under the supervisor's own node. */
25
+ export declare function gitCredentialHelperCommand(stateDirectory: string, nodePath?: string): string;
26
+ /** Write the helper into a directory the child can read, returning its path. */
27
+ export declare function writeGitCredentialHelper(directory: string): string;
28
+ /**
29
+ * Resolve a credential on the host, through the host's own git configuration.
30
+ *
31
+ * `git credential fill` rather than reading any particular store, because the answer may
32
+ * come from `gh`'s helper, libsecret, the macOS keychain, or a plain file, and only git
33
+ * knows which of those this host is configured to ask. Reimplementing that resolution
34
+ * would be a second, quietly diverging copy of it.
35
+ *
36
+ * Two things about *where* it runs are part of the boundary rather than housekeeping. It
37
+ * runs in a private empty directory, never in whatever directory the supervisor happened to
38
+ * be started from, because a repository's own `credential.helper` is a command git would
39
+ * otherwise execute on the host. And it runs without the four variables that would point it
40
+ * back at such a repository. The request is re-parsed here too: this function serializes
41
+ * into git's line protocol, so it refuses anything that could rewrite the identity it was
42
+ * asked for, whether or not the proxy already checked.
43
+ */
44
+ export declare function fillHostGitCredential(request: GitCredentialRequest, options?: {
45
+ environment?: NodeJS.ProcessEnv;
46
+ }): Promise<GitCredential | undefined>;
47
+ //# sourceMappingURL=git-credential-helper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-credential-helper.d.ts","sourceRoot":"","sources":["../../../../src/core/sandbox/rpc/git-credential-helper.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,oBAAoB,EAA6B,MAAM,2BAA2B,CAAC;AAErH,gEAAgE;AAChE,eAAO,MAAM,8BAA8B,6BAA6B,CAAC;AAwEzE,MAAM,WAAW,yBAAyB;IACzC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CACjC;AAED;;;;;GAKG;AACH,wBAAgB,gCAAgC,IAAI,yBAAyB,CAW5E;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAEtE;AAED,6FAA6F;AAC7F,wBAAgB,0BAA0B,CAAC,cAAc,EAAE,MAAM,EAAE,QAAQ,SAAmB,GAAG,MAAM,CAEtG;AAED,gFAAgF;AAChF,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAIlE;AAqDD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,qBAAqB,CAC1C,OAAO,EAAE,oBAAoB,EAC7B,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;CAAE,GAC3C,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CA+CpC","sourcesContent":["import { chmodSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { supervisorTempDirectory } from \"../supervisor-temp.ts\";\nimport { type GitCredential, type GitCredentialRequest, parseGitCredentialRequest } from \"./git-credential-proxy.ts\";\n\n/** Env var naming the child-side socket the helper talks to. */\nexport const GIT_CREDENTIAL_SOCKET_VARIABLE = \"APEX_GIT_CREDENTIAL_PATH\";\n\n/**\n * The credential helper git runs inside the sandbox.\n *\n * `.cjs` for the same reason the network relay is: a plain `.js` under a workspace whose\n * package.json declares `\"type\": \"module\"` is parsed as ESM and dies on `require`.\n *\n * It answers `get` and nothing else. `store` and `erase` exit silently and successfully,\n * which is what tells git the credential was handled and stops it falling through to\n * another helper. Writing is deliberately not served: ADR 0015 keeps credential mutation\n * an explicit host operation, and a channel that let the child rewrite the host's store\n * would be a way out of the boundary rather than a way to work inside it.\n *\n * Any failure exits 0 having printed nothing, which git reads as \"this helper has no\n * credential\" and handles normally. Exiting non-zero would surface as a git error that\n * looks like a repository problem rather than a refused release.\n */\nconst HELPER_SOURCE = `\nconst net = require(\"node:net\");\n\nconst operation = process.argv[2];\nif (operation !== \"get\") process.exit(0);\n\nlet input = \"\";\nprocess.stdin.setEncoding(\"utf8\");\nprocess.stdin.on(\"data\", (chunk) => {\n\tinput += chunk;\n});\nprocess.stdin.on(\"end\", () => {\n\tconst request = {};\n\tfor (const line of input.split(\"\\\\n\")) {\n\t\tconst separator = line.indexOf(\"=\");\n\t\tif (separator > 0) request[line.slice(0, separator)] = line.slice(separator + 1).trim();\n\t}\n\tconst socketPath = process.env.${GIT_CREDENTIAL_SOCKET_VARIABLE};\n\tif (!socketPath || !request.host) process.exit(0);\n\n\tconst socket = net.connect(socketPath);\n\tlet buffer = \"\";\n\tconst giveUp = () => {\n\t\tsocket.destroy();\n\t\tprocess.exit(0);\n\t};\n\tsocket.on(\"connect\", () => {\n\t\tsocket.write(JSON.stringify({ op: \"get\", protocol: request.protocol || \"https\", host: request.host }) + \"\\\\n\");\n\t});\n\tsocket.on(\"data\", (chunk) => {\n\t\tbuffer += chunk.toString();\n\t\tconst newline = buffer.indexOf(\"\\\\n\");\n\t\tif (newline < 0) return;\n\t\tlet response;\n\t\ttry {\n\t\t\tresponse = JSON.parse(buffer.slice(0, newline));\n\t\t} catch {\n\t\t\tgiveUp();\n\t\t\treturn;\n\t\t}\n\t\tif (response && response.ok) {\n\t\t\tprocess.stdout.write(\"username=\" + response.username + \"\\\\n\");\n\t\t\tprocess.stdout.write(\"password=\" + response.password + \"\\\\n\");\n\t\t}\n\t\tsocket.destroy();\n\t\tprocess.exit(0);\n\t});\n\tsocket.on(\"error\", giveUp);\n});\n`.trim();\n\n/** AF_UNIX `sun_path` is 108 bytes on Linux, including the terminating NUL. */\nconst SUN_PATH_LIMIT = 108;\n\nexport interface GitCredentialChannelPaths {\n\treadonly hostSocketDirectory: string;\n\treadonly hostSocketPath: string;\n\treadonly childSocketPath: string;\n}\n\n/**\n * Where the git credential socket lives on each side, mirroring the credential channel's\n * own resolution. TMPDIR is deliberately not honoured: it can point inside the workspace\n * on macOS, which the sandbox may write. The child-side path sits under `/home` on Linux\n * because the `--tmpfs /home` is the only writable mount when bwrap creates the mountpoint.\n */\nexport function resolveGitCredentialChannelPaths(): GitCredentialChannelPaths {\n\tconst hostSocketDirectory = mkdtempSync(join(supervisorTempDirectory(), `apex-gitcred-${process.pid}-`), {\n\t\tencoding: \"utf8\",\n\t});\n\tchmodSync(hostSocketDirectory, 0o700);\n\tconst hostSocketPath = join(hostSocketDirectory, \"channel.sock\");\n\tif (Buffer.byteLength(hostSocketPath) + 1 > SUN_PATH_LIMIT) {\n\t\tthrow new Error(\"Git credential channel path exceeds the Unix socket path limit.\");\n\t}\n\tconst childSocketPath = process.platform === \"linux\" ? \"/home/apex-git-credential-channel.sock\" : hostSocketPath;\n\treturn { hostSocketDirectory, hostSocketPath, childSocketPath };\n}\n\n/**\n * Where the helper lives, derived rather than passed.\n *\n * The supervisor writes `credential.helper` into the projected git config before the\n * platform backend exists, and the backend writes the file itself. Deriving both from the\n * state directory is what keeps the config from naming a path nothing ever created.\n */\nexport function gitCredentialHelperPath(stateDirectory: string): string {\n\treturn join(stateDirectory, \"git-credential-apex.cjs\");\n}\n\n/** The `credential.helper` value naming that helper, run under the supervisor's own node. */\nexport function gitCredentialHelperCommand(stateDirectory: string, nodePath = process.execPath): string {\n\treturn `!\"${nodePath}\" \"${gitCredentialHelperPath(stateDirectory)}\"`;\n}\n\n/** Write the helper into a directory the child can read, returning its path. */\nexport function writeGitCredentialHelper(directory: string): string {\n\tconst path = gitCredentialHelperPath(directory);\n\twriteFileSync(path, HELPER_SOURCE);\n\treturn path;\n}\n\n/**\n * The environment variables that decide which repository git is standing in, and which\n * config it reads.\n *\n * `credential.helper` is a shell command git runs on whichever machine resolves the\n * credential -- the host, here. Any of these four lets a repository the agent cloned choose\n * that command, so a credential fill that inherits them is a host execution primitive with\n * a socket in front of it.\n *\n * `HOME` and the global and system config are deliberately kept. They are how `gh`,\n * libsecret, the macOS keychain and a plain `credential.helper` in `--global` are found,\n * and they belong to the human running the supervisor rather than to any repository.\n */\nconst REPOSITORY_DISCOVERY_VARIABLES = [\"GIT_DIR\", \"GIT_WORK_TREE\", \"GIT_INDEX_FILE\", \"GIT_CONFIG\"] as const;\n\ninterface PrivateFillDirectory {\n\t/** Where git runs: empty, 0700, and inside no repository. */\n\treadonly cwd: string;\n\t/** Its parent, so git's upward search for a repository stops before leaving it. */\n\treadonly ceiling: string;\n\treadonly dispose: () => void;\n}\n\nfunction createPrivateFillDirectory(): PrivateFillDirectory {\n\tconst base = mkdtempSync(join(supervisorTempDirectory(), `apex-gitcred-fill-${process.pid}-`), { encoding: \"utf8\" });\n\tchmodSync(base, 0o700);\n\tconst cwd = join(base, \"empty\");\n\tmkdirSync(cwd, { mode: 0o700 });\n\t// Resolved, because git ignores a ceiling entry that would need symlink resolution and\n\t// `/tmp` is a symlink on macOS.\n\tlet ceiling = base;\n\ttry {\n\t\tceiling = realpathSync(base);\n\t} catch {\n\t\t// A ceiling that does not resolve is a weaker second guard, not a broken fill; the\n\t\t// empty cwd is what does the work.\n\t}\n\treturn {\n\t\tcwd,\n\t\tceiling,\n\t\tdispose: () => {\n\t\t\ttry {\n\t\t\t\trmSync(base, { force: true, recursive: true });\n\t\t\t} catch {\n\t\t\t\t// A leftover empty directory under the supervisor's own temp root is not worth\n\t\t\t\t// failing a credential fill over.\n\t\t\t}\n\t\t},\n\t};\n}\n\n/**\n * Resolve a credential on the host, through the host's own git configuration.\n *\n * `git credential fill` rather than reading any particular store, because the answer may\n * come from `gh`'s helper, libsecret, the macOS keychain, or a plain file, and only git\n * knows which of those this host is configured to ask. Reimplementing that resolution\n * would be a second, quietly diverging copy of it.\n *\n * Two things about *where* it runs are part of the boundary rather than housekeeping. It\n * runs in a private empty directory, never in whatever directory the supervisor happened to\n * be started from, because a repository's own `credential.helper` is a command git would\n * otherwise execute on the host. And it runs without the four variables that would point it\n * back at such a repository. The request is re-parsed here too: this function serializes\n * into git's line protocol, so it refuses anything that could rewrite the identity it was\n * asked for, whether or not the proxy already checked.\n */\nexport async function fillHostGitCredential(\n\trequest: GitCredentialRequest,\n\toptions?: { environment?: NodeJS.ProcessEnv },\n): Promise<GitCredential | undefined> {\n\tconst parsed = parseGitCredentialRequest({ protocol: request.protocol, host: request.host });\n\tif (!parsed.ok) return undefined;\n\tconst identity = parsed.request;\n\n\tconst { spawn } = await import(\"node:child_process\");\n\tconst directory = createPrivateFillDirectory();\n\tconst environment: NodeJS.ProcessEnv = { ...(options?.environment ?? process.env) };\n\tfor (const variable of REPOSITORY_DISCOVERY_VARIABLES) delete environment[variable];\n\tenvironment.GIT_CEILING_DIRECTORIES = directory.ceiling;\n\n\treturn new Promise((resolve) => {\n\t\tlet settled = false;\n\t\tconst finish = (credential: GitCredential | undefined): void => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tdirectory.dispose();\n\t\t\tresolve(credential);\n\t\t};\n\t\tconst child = spawn(\"git\", [\"credential\", \"fill\"], {\n\t\t\tcwd: directory.cwd,\n\t\t\tenv: environment,\n\t\t\tstdio: [\"pipe\", \"pipe\", \"ignore\"],\n\t\t});\n\t\tlet stdout = \"\";\n\t\tchild.stdout.setEncoding(\"utf8\");\n\t\tchild.stdout.on(\"data\", (chunk: string) => {\n\t\t\tstdout += chunk;\n\t\t});\n\t\tchild.on(\"error\", () => finish(undefined));\n\t\tchild.on(\"close\", (code) => {\n\t\t\tif (code !== 0) {\n\t\t\t\tfinish(undefined);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst fields = new Map<string, string>();\n\t\t\tfor (const line of stdout.split(\"\\n\")) {\n\t\t\t\tconst separator = line.indexOf(\"=\");\n\t\t\t\tif (separator > 0) fields.set(line.slice(0, separator), line.slice(separator + 1).trim());\n\t\t\t}\n\t\t\tconst username = fields.get(\"username\");\n\t\t\tconst password = fields.get(\"password\");\n\t\t\tfinish(username && password ? { username, password } : undefined);\n\t\t});\n\t\tchild.stdin.on(\"error\", () => finish(undefined));\n\t\tchild.stdin.end(`protocol=${identity.protocol}\\nhost=${identity.host}\\n\\n`);\n\t});\n}\n"]}
@@ -0,0 +1,222 @@
1
+ import { chmodSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { supervisorTempDirectory } from "../supervisor-temp.js";
4
+ import { parseGitCredentialRequest } from "./git-credential-proxy.js";
5
+ /** Env var naming the child-side socket the helper talks to. */
6
+ export const GIT_CREDENTIAL_SOCKET_VARIABLE = "APEX_GIT_CREDENTIAL_PATH";
7
+ /**
8
+ * The credential helper git runs inside the sandbox.
9
+ *
10
+ * `.cjs` for the same reason the network relay is: a plain `.js` under a workspace whose
11
+ * package.json declares `"type": "module"` is parsed as ESM and dies on `require`.
12
+ *
13
+ * It answers `get` and nothing else. `store` and `erase` exit silently and successfully,
14
+ * which is what tells git the credential was handled and stops it falling through to
15
+ * another helper. Writing is deliberately not served: ADR 0015 keeps credential mutation
16
+ * an explicit host operation, and a channel that let the child rewrite the host's store
17
+ * would be a way out of the boundary rather than a way to work inside it.
18
+ *
19
+ * Any failure exits 0 having printed nothing, which git reads as "this helper has no
20
+ * credential" and handles normally. Exiting non-zero would surface as a git error that
21
+ * looks like a repository problem rather than a refused release.
22
+ */
23
+ const HELPER_SOURCE = `
24
+ const net = require("node:net");
25
+
26
+ const operation = process.argv[2];
27
+ if (operation !== "get") process.exit(0);
28
+
29
+ let input = "";
30
+ process.stdin.setEncoding("utf8");
31
+ process.stdin.on("data", (chunk) => {
32
+ input += chunk;
33
+ });
34
+ process.stdin.on("end", () => {
35
+ const request = {};
36
+ for (const line of input.split("\\n")) {
37
+ const separator = line.indexOf("=");
38
+ if (separator > 0) request[line.slice(0, separator)] = line.slice(separator + 1).trim();
39
+ }
40
+ const socketPath = process.env.${GIT_CREDENTIAL_SOCKET_VARIABLE};
41
+ if (!socketPath || !request.host) process.exit(0);
42
+
43
+ const socket = net.connect(socketPath);
44
+ let buffer = "";
45
+ const giveUp = () => {
46
+ socket.destroy();
47
+ process.exit(0);
48
+ };
49
+ socket.on("connect", () => {
50
+ socket.write(JSON.stringify({ op: "get", protocol: request.protocol || "https", host: request.host }) + "\\n");
51
+ });
52
+ socket.on("data", (chunk) => {
53
+ buffer += chunk.toString();
54
+ const newline = buffer.indexOf("\\n");
55
+ if (newline < 0) return;
56
+ let response;
57
+ try {
58
+ response = JSON.parse(buffer.slice(0, newline));
59
+ } catch {
60
+ giveUp();
61
+ return;
62
+ }
63
+ if (response && response.ok) {
64
+ process.stdout.write("username=" + response.username + "\\n");
65
+ process.stdout.write("password=" + response.password + "\\n");
66
+ }
67
+ socket.destroy();
68
+ process.exit(0);
69
+ });
70
+ socket.on("error", giveUp);
71
+ });
72
+ `.trim();
73
+ /** AF_UNIX `sun_path` is 108 bytes on Linux, including the terminating NUL. */
74
+ const SUN_PATH_LIMIT = 108;
75
+ /**
76
+ * Where the git credential socket lives on each side, mirroring the credential channel's
77
+ * own resolution. TMPDIR is deliberately not honoured: it can point inside the workspace
78
+ * on macOS, which the sandbox may write. The child-side path sits under `/home` on Linux
79
+ * because the `--tmpfs /home` is the only writable mount when bwrap creates the mountpoint.
80
+ */
81
+ export function resolveGitCredentialChannelPaths() {
82
+ const hostSocketDirectory = mkdtempSync(join(supervisorTempDirectory(), `apex-gitcred-${process.pid}-`), {
83
+ encoding: "utf8",
84
+ });
85
+ chmodSync(hostSocketDirectory, 0o700);
86
+ const hostSocketPath = join(hostSocketDirectory, "channel.sock");
87
+ if (Buffer.byteLength(hostSocketPath) + 1 > SUN_PATH_LIMIT) {
88
+ throw new Error("Git credential channel path exceeds the Unix socket path limit.");
89
+ }
90
+ const childSocketPath = process.platform === "linux" ? "/home/apex-git-credential-channel.sock" : hostSocketPath;
91
+ return { hostSocketDirectory, hostSocketPath, childSocketPath };
92
+ }
93
+ /**
94
+ * Where the helper lives, derived rather than passed.
95
+ *
96
+ * The supervisor writes `credential.helper` into the projected git config before the
97
+ * platform backend exists, and the backend writes the file itself. Deriving both from the
98
+ * state directory is what keeps the config from naming a path nothing ever created.
99
+ */
100
+ export function gitCredentialHelperPath(stateDirectory) {
101
+ return join(stateDirectory, "git-credential-apex.cjs");
102
+ }
103
+ /** The `credential.helper` value naming that helper, run under the supervisor's own node. */
104
+ export function gitCredentialHelperCommand(stateDirectory, nodePath = process.execPath) {
105
+ return `!"${nodePath}" "${gitCredentialHelperPath(stateDirectory)}"`;
106
+ }
107
+ /** Write the helper into a directory the child can read, returning its path. */
108
+ export function writeGitCredentialHelper(directory) {
109
+ const path = gitCredentialHelperPath(directory);
110
+ writeFileSync(path, HELPER_SOURCE);
111
+ return path;
112
+ }
113
+ /**
114
+ * The environment variables that decide which repository git is standing in, and which
115
+ * config it reads.
116
+ *
117
+ * `credential.helper` is a shell command git runs on whichever machine resolves the
118
+ * credential -- the host, here. Any of these four lets a repository the agent cloned choose
119
+ * that command, so a credential fill that inherits them is a host execution primitive with
120
+ * a socket in front of it.
121
+ *
122
+ * `HOME` and the global and system config are deliberately kept. They are how `gh`,
123
+ * libsecret, the macOS keychain and a plain `credential.helper` in `--global` are found,
124
+ * and they belong to the human running the supervisor rather than to any repository.
125
+ */
126
+ const REPOSITORY_DISCOVERY_VARIABLES = ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_CONFIG"];
127
+ function createPrivateFillDirectory() {
128
+ const base = mkdtempSync(join(supervisorTempDirectory(), `apex-gitcred-fill-${process.pid}-`), { encoding: "utf8" });
129
+ chmodSync(base, 0o700);
130
+ const cwd = join(base, "empty");
131
+ mkdirSync(cwd, { mode: 0o700 });
132
+ // Resolved, because git ignores a ceiling entry that would need symlink resolution and
133
+ // `/tmp` is a symlink on macOS.
134
+ let ceiling = base;
135
+ try {
136
+ ceiling = realpathSync(base);
137
+ }
138
+ catch {
139
+ // A ceiling that does not resolve is a weaker second guard, not a broken fill; the
140
+ // empty cwd is what does the work.
141
+ }
142
+ return {
143
+ cwd,
144
+ ceiling,
145
+ dispose: () => {
146
+ try {
147
+ rmSync(base, { force: true, recursive: true });
148
+ }
149
+ catch {
150
+ // A leftover empty directory under the supervisor's own temp root is not worth
151
+ // failing a credential fill over.
152
+ }
153
+ },
154
+ };
155
+ }
156
+ /**
157
+ * Resolve a credential on the host, through the host's own git configuration.
158
+ *
159
+ * `git credential fill` rather than reading any particular store, because the answer may
160
+ * come from `gh`'s helper, libsecret, the macOS keychain, or a plain file, and only git
161
+ * knows which of those this host is configured to ask. Reimplementing that resolution
162
+ * would be a second, quietly diverging copy of it.
163
+ *
164
+ * Two things about *where* it runs are part of the boundary rather than housekeeping. It
165
+ * runs in a private empty directory, never in whatever directory the supervisor happened to
166
+ * be started from, because a repository's own `credential.helper` is a command git would
167
+ * otherwise execute on the host. And it runs without the four variables that would point it
168
+ * back at such a repository. The request is re-parsed here too: this function serializes
169
+ * into git's line protocol, so it refuses anything that could rewrite the identity it was
170
+ * asked for, whether or not the proxy already checked.
171
+ */
172
+ export async function fillHostGitCredential(request, options) {
173
+ const parsed = parseGitCredentialRequest({ protocol: request.protocol, host: request.host });
174
+ if (!parsed.ok)
175
+ return undefined;
176
+ const identity = parsed.request;
177
+ const { spawn } = await import("node:child_process");
178
+ const directory = createPrivateFillDirectory();
179
+ const environment = { ...(options?.environment ?? process.env) };
180
+ for (const variable of REPOSITORY_DISCOVERY_VARIABLES)
181
+ delete environment[variable];
182
+ environment.GIT_CEILING_DIRECTORIES = directory.ceiling;
183
+ return new Promise((resolve) => {
184
+ let settled = false;
185
+ const finish = (credential) => {
186
+ if (settled)
187
+ return;
188
+ settled = true;
189
+ directory.dispose();
190
+ resolve(credential);
191
+ };
192
+ const child = spawn("git", ["credential", "fill"], {
193
+ cwd: directory.cwd,
194
+ env: environment,
195
+ stdio: ["pipe", "pipe", "ignore"],
196
+ });
197
+ let stdout = "";
198
+ child.stdout.setEncoding("utf8");
199
+ child.stdout.on("data", (chunk) => {
200
+ stdout += chunk;
201
+ });
202
+ child.on("error", () => finish(undefined));
203
+ child.on("close", (code) => {
204
+ if (code !== 0) {
205
+ finish(undefined);
206
+ return;
207
+ }
208
+ const fields = new Map();
209
+ for (const line of stdout.split("\n")) {
210
+ const separator = line.indexOf("=");
211
+ if (separator > 0)
212
+ fields.set(line.slice(0, separator), line.slice(separator + 1).trim());
213
+ }
214
+ const username = fields.get("username");
215
+ const password = fields.get("password");
216
+ finish(username && password ? { username, password } : undefined);
217
+ });
218
+ child.stdin.on("error", () => finish(undefined));
219
+ child.stdin.end(`protocol=${identity.protocol}\nhost=${identity.host}\n\n`);
220
+ });
221
+ }
222
+ //# sourceMappingURL=git-credential-helper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-credential-helper.js","sourceRoot":"","sources":["../../../../src/core/sandbox/rpc/git-credential-helper.ts"],"names":[],"mappings":"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","sourcesContent":["import { chmodSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { supervisorTempDirectory } from \"../supervisor-temp.ts\";\nimport { type GitCredential, type GitCredentialRequest, parseGitCredentialRequest } from \"./git-credential-proxy.ts\";\n\n/** Env var naming the child-side socket the helper talks to. */\nexport const GIT_CREDENTIAL_SOCKET_VARIABLE = \"APEX_GIT_CREDENTIAL_PATH\";\n\n/**\n * The credential helper git runs inside the sandbox.\n *\n * `.cjs` for the same reason the network relay is: a plain `.js` under a workspace whose\n * package.json declares `\"type\": \"module\"` is parsed as ESM and dies on `require`.\n *\n * It answers `get` and nothing else. `store` and `erase` exit silently and successfully,\n * which is what tells git the credential was handled and stops it falling through to\n * another helper. Writing is deliberately not served: ADR 0015 keeps credential mutation\n * an explicit host operation, and a channel that let the child rewrite the host's store\n * would be a way out of the boundary rather than a way to work inside it.\n *\n * Any failure exits 0 having printed nothing, which git reads as \"this helper has no\n * credential\" and handles normally. Exiting non-zero would surface as a git error that\n * looks like a repository problem rather than a refused release.\n */\nconst HELPER_SOURCE = `\nconst net = require(\"node:net\");\n\nconst operation = process.argv[2];\nif (operation !== \"get\") process.exit(0);\n\nlet input = \"\";\nprocess.stdin.setEncoding(\"utf8\");\nprocess.stdin.on(\"data\", (chunk) => {\n\tinput += chunk;\n});\nprocess.stdin.on(\"end\", () => {\n\tconst request = {};\n\tfor (const line of input.split(\"\\\\n\")) {\n\t\tconst separator = line.indexOf(\"=\");\n\t\tif (separator > 0) request[line.slice(0, separator)] = line.slice(separator + 1).trim();\n\t}\n\tconst socketPath = process.env.${GIT_CREDENTIAL_SOCKET_VARIABLE};\n\tif (!socketPath || !request.host) process.exit(0);\n\n\tconst socket = net.connect(socketPath);\n\tlet buffer = \"\";\n\tconst giveUp = () => {\n\t\tsocket.destroy();\n\t\tprocess.exit(0);\n\t};\n\tsocket.on(\"connect\", () => {\n\t\tsocket.write(JSON.stringify({ op: \"get\", protocol: request.protocol || \"https\", host: request.host }) + \"\\\\n\");\n\t});\n\tsocket.on(\"data\", (chunk) => {\n\t\tbuffer += chunk.toString();\n\t\tconst newline = buffer.indexOf(\"\\\\n\");\n\t\tif (newline < 0) return;\n\t\tlet response;\n\t\ttry {\n\t\t\tresponse = JSON.parse(buffer.slice(0, newline));\n\t\t} catch {\n\t\t\tgiveUp();\n\t\t\treturn;\n\t\t}\n\t\tif (response && response.ok) {\n\t\t\tprocess.stdout.write(\"username=\" + response.username + \"\\\\n\");\n\t\t\tprocess.stdout.write(\"password=\" + response.password + \"\\\\n\");\n\t\t}\n\t\tsocket.destroy();\n\t\tprocess.exit(0);\n\t});\n\tsocket.on(\"error\", giveUp);\n});\n`.trim();\n\n/** AF_UNIX `sun_path` is 108 bytes on Linux, including the terminating NUL. */\nconst SUN_PATH_LIMIT = 108;\n\nexport interface GitCredentialChannelPaths {\n\treadonly hostSocketDirectory: string;\n\treadonly hostSocketPath: string;\n\treadonly childSocketPath: string;\n}\n\n/**\n * Where the git credential socket lives on each side, mirroring the credential channel's\n * own resolution. TMPDIR is deliberately not honoured: it can point inside the workspace\n * on macOS, which the sandbox may write. The child-side path sits under `/home` on Linux\n * because the `--tmpfs /home` is the only writable mount when bwrap creates the mountpoint.\n */\nexport function resolveGitCredentialChannelPaths(): GitCredentialChannelPaths {\n\tconst hostSocketDirectory = mkdtempSync(join(supervisorTempDirectory(), `apex-gitcred-${process.pid}-`), {\n\t\tencoding: \"utf8\",\n\t});\n\tchmodSync(hostSocketDirectory, 0o700);\n\tconst hostSocketPath = join(hostSocketDirectory, \"channel.sock\");\n\tif (Buffer.byteLength(hostSocketPath) + 1 > SUN_PATH_LIMIT) {\n\t\tthrow new Error(\"Git credential channel path exceeds the Unix socket path limit.\");\n\t}\n\tconst childSocketPath = process.platform === \"linux\" ? \"/home/apex-git-credential-channel.sock\" : hostSocketPath;\n\treturn { hostSocketDirectory, hostSocketPath, childSocketPath };\n}\n\n/**\n * Where the helper lives, derived rather than passed.\n *\n * The supervisor writes `credential.helper` into the projected git config before the\n * platform backend exists, and the backend writes the file itself. Deriving both from the\n * state directory is what keeps the config from naming a path nothing ever created.\n */\nexport function gitCredentialHelperPath(stateDirectory: string): string {\n\treturn join(stateDirectory, \"git-credential-apex.cjs\");\n}\n\n/** The `credential.helper` value naming that helper, run under the supervisor's own node. */\nexport function gitCredentialHelperCommand(stateDirectory: string, nodePath = process.execPath): string {\n\treturn `!\"${nodePath}\" \"${gitCredentialHelperPath(stateDirectory)}\"`;\n}\n\n/** Write the helper into a directory the child can read, returning its path. */\nexport function writeGitCredentialHelper(directory: string): string {\n\tconst path = gitCredentialHelperPath(directory);\n\twriteFileSync(path, HELPER_SOURCE);\n\treturn path;\n}\n\n/**\n * The environment variables that decide which repository git is standing in, and which\n * config it reads.\n *\n * `credential.helper` is a shell command git runs on whichever machine resolves the\n * credential -- the host, here. Any of these four lets a repository the agent cloned choose\n * that command, so a credential fill that inherits them is a host execution primitive with\n * a socket in front of it.\n *\n * `HOME` and the global and system config are deliberately kept. They are how `gh`,\n * libsecret, the macOS keychain and a plain `credential.helper` in `--global` are found,\n * and they belong to the human running the supervisor rather than to any repository.\n */\nconst REPOSITORY_DISCOVERY_VARIABLES = [\"GIT_DIR\", \"GIT_WORK_TREE\", \"GIT_INDEX_FILE\", \"GIT_CONFIG\"] as const;\n\ninterface PrivateFillDirectory {\n\t/** Where git runs: empty, 0700, and inside no repository. */\n\treadonly cwd: string;\n\t/** Its parent, so git's upward search for a repository stops before leaving it. */\n\treadonly ceiling: string;\n\treadonly dispose: () => void;\n}\n\nfunction createPrivateFillDirectory(): PrivateFillDirectory {\n\tconst base = mkdtempSync(join(supervisorTempDirectory(), `apex-gitcred-fill-${process.pid}-`), { encoding: \"utf8\" });\n\tchmodSync(base, 0o700);\n\tconst cwd = join(base, \"empty\");\n\tmkdirSync(cwd, { mode: 0o700 });\n\t// Resolved, because git ignores a ceiling entry that would need symlink resolution and\n\t// `/tmp` is a symlink on macOS.\n\tlet ceiling = base;\n\ttry {\n\t\tceiling = realpathSync(base);\n\t} catch {\n\t\t// A ceiling that does not resolve is a weaker second guard, not a broken fill; the\n\t\t// empty cwd is what does the work.\n\t}\n\treturn {\n\t\tcwd,\n\t\tceiling,\n\t\tdispose: () => {\n\t\t\ttry {\n\t\t\t\trmSync(base, { force: true, recursive: true });\n\t\t\t} catch {\n\t\t\t\t// A leftover empty directory under the supervisor's own temp root is not worth\n\t\t\t\t// failing a credential fill over.\n\t\t\t}\n\t\t},\n\t};\n}\n\n/**\n * Resolve a credential on the host, through the host's own git configuration.\n *\n * `git credential fill` rather than reading any particular store, because the answer may\n * come from `gh`'s helper, libsecret, the macOS keychain, or a plain file, and only git\n * knows which of those this host is configured to ask. Reimplementing that resolution\n * would be a second, quietly diverging copy of it.\n *\n * Two things about *where* it runs are part of the boundary rather than housekeeping. It\n * runs in a private empty directory, never in whatever directory the supervisor happened to\n * be started from, because a repository's own `credential.helper` is a command git would\n * otherwise execute on the host. And it runs without the four variables that would point it\n * back at such a repository. The request is re-parsed here too: this function serializes\n * into git's line protocol, so it refuses anything that could rewrite the identity it was\n * asked for, whether or not the proxy already checked.\n */\nexport async function fillHostGitCredential(\n\trequest: GitCredentialRequest,\n\toptions?: { environment?: NodeJS.ProcessEnv },\n): Promise<GitCredential | undefined> {\n\tconst parsed = parseGitCredentialRequest({ protocol: request.protocol, host: request.host });\n\tif (!parsed.ok) return undefined;\n\tconst identity = parsed.request;\n\n\tconst { spawn } = await import(\"node:child_process\");\n\tconst directory = createPrivateFillDirectory();\n\tconst environment: NodeJS.ProcessEnv = { ...(options?.environment ?? process.env) };\n\tfor (const variable of REPOSITORY_DISCOVERY_VARIABLES) delete environment[variable];\n\tenvironment.GIT_CEILING_DIRECTORIES = directory.ceiling;\n\n\treturn new Promise((resolve) => {\n\t\tlet settled = false;\n\t\tconst finish = (credential: GitCredential | undefined): void => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tdirectory.dispose();\n\t\t\tresolve(credential);\n\t\t};\n\t\tconst child = spawn(\"git\", [\"credential\", \"fill\"], {\n\t\t\tcwd: directory.cwd,\n\t\t\tenv: environment,\n\t\t\tstdio: [\"pipe\", \"pipe\", \"ignore\"],\n\t\t});\n\t\tlet stdout = \"\";\n\t\tchild.stdout.setEncoding(\"utf8\");\n\t\tchild.stdout.on(\"data\", (chunk: string) => {\n\t\t\tstdout += chunk;\n\t\t});\n\t\tchild.on(\"error\", () => finish(undefined));\n\t\tchild.on(\"close\", (code) => {\n\t\t\tif (code !== 0) {\n\t\t\t\tfinish(undefined);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst fields = new Map<string, string>();\n\t\t\tfor (const line of stdout.split(\"\\n\")) {\n\t\t\t\tconst separator = line.indexOf(\"=\");\n\t\t\t\tif (separator > 0) fields.set(line.slice(0, separator), line.slice(separator + 1).trim());\n\t\t\t}\n\t\t\tconst username = fields.get(\"username\");\n\t\t\tconst password = fields.get(\"password\");\n\t\t\tfinish(username && password ? { username, password } : undefined);\n\t\t});\n\t\tchild.stdin.on(\"error\", () => finish(undefined));\n\t\tchild.stdin.end(`protocol=${identity.protocol}\\nhost=${identity.host}\\n\\n`);\n\t});\n}\n"]}
@@ -0,0 +1,66 @@
1
+ import type { SandboxViolationStore } from "../violations.ts";
2
+ export interface GitCredentialRequest {
3
+ readonly protocol: string;
4
+ readonly host: string;
5
+ }
6
+ export interface GitCredential {
7
+ readonly username: string;
8
+ readonly password: string;
9
+ }
10
+ export interface GitCredentialProxy {
11
+ close(): Promise<void>;
12
+ }
13
+ /** A request that survived validation, or the reason it did not. */
14
+ export type GitCredentialRequestParse = {
15
+ readonly ok: true;
16
+ readonly request: GitCredentialRequest;
17
+ } | {
18
+ readonly ok: false;
19
+ readonly error: string;
20
+ readonly audit: string;
21
+ };
22
+ /**
23
+ * Parse one credential request into the single structured identity everything downstream
24
+ * uses.
25
+ *
26
+ * The caller authorizes, releases, and serializes *this* value and never the raw fields,
27
+ * because the defect this closes is precisely that the string checked against the release
28
+ * was not the string handed to git.
29
+ */
30
+ export declare function parseGitCredentialRequest(request: Record<string, unknown>): GitCredentialRequestParse;
31
+ /**
32
+ * Serve git credentials to the sandboxed child without ever putting one inside it.
33
+ *
34
+ * ADR 0015 keeps credentials host-owned and hands the child a read-only projection. A git
35
+ * credential cannot be projected the same way: it is not one file with a stable shape, it
36
+ * is whatever the host's own helper produces per host. So the credential stays on the host
37
+ * and only the answer to one question crosses, for one host, once it has been released.
38
+ *
39
+ * Two gates, and the first is the one that matters most.
40
+ *
41
+ * The host must be reachable by this session. git only asks for a credential after a
42
+ * server challenged it, so in the ordinary flow the host was already allowed or approved
43
+ * at the network layer. A request for a host the session cannot reach is therefore not git
44
+ * doing its job; it is something that went looking for a token. Refusing it costs nothing
45
+ * real and removes the case where this channel is more useful to an attacker than to git.
46
+ *
47
+ * The human must release it. Per ADR 0023 that decision is the supervisor's, because this
48
+ * socket has no peer authentication -- every descendant in the child's namespace can reach
49
+ * it, which is precisely the code the boundary exists to contain. Without a releaser the
50
+ * channel refuses outright, so a headless session cannot hand out a credential at all.
51
+ *
52
+ * A release covers one host for the session. It is never persisted and never widened to a
53
+ * second host, which is what keeps an approval for a push from also releasing a token to
54
+ * whatever else the session later contacts.
55
+ */
56
+ export declare function createGitCredentialProxy(options: {
57
+ socketPath: string;
58
+ /** Whether this session may reach the host at all. */
59
+ isHostAllowed: (host: string) => boolean;
60
+ /** Ask a human to release the host's credential. Absent means refuse without asking. */
61
+ requestRelease?: (host: string) => Promise<boolean>;
62
+ /** Resolve the credential on the host, using the host's own configuration. */
63
+ fillCredential: (request: GitCredentialRequest) => Promise<GitCredential | undefined>;
64
+ violationStore?: SandboxViolationStore;
65
+ }): Promise<GitCredentialProxy>;
66
+ //# sourceMappingURL=git-credential-proxy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-credential-proxy.d.ts","sourceRoot":"","sources":["../../../../src/core/sandbox/rpc/git-credential-proxy.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAK9D,MAAM,WAAW,oBAAoB;IACpC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAkB;IAClC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,oEAAoE;AACpE,MAAM,MAAM,yBAAyB,GAClC;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,oBAAoB,CAAA;CAAE,GAC7D;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAsB1E;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,yBAAyB,CAoCrG;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IACzC,wFAAwF;IACxF,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACpD,8EAA8E;IAC9E,cAAc,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAAC;IACtF,cAAc,CAAC,EAAE,qBAAqB,CAAC;CACvC,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAkH9B","sourcesContent":["import { chmodSync, existsSync, unlinkSync } from \"node:fs\";\nimport * as net from \"node:net\";\nimport type { SandboxViolationStore } from \"../violations.ts\";\nimport { FrameReader, isRequestObject, writeFrame } from \"./framing.ts\";\n\nconst MAX_CONNECTIONS = 32;\n\nexport interface GitCredentialRequest {\n\treadonly protocol: string;\n\treadonly host: string;\n}\n\nexport interface GitCredential {\n\treadonly username: string;\n\treadonly password: string;\n}\n\nexport interface GitCredentialProxy {\n\tclose(): Promise<void>;\n}\n\n/** A request that survived validation, or the reason it did not. */\nexport type GitCredentialRequestParse =\n\t| { readonly ok: true; readonly request: GitCredentialRequest }\n\t| { readonly ok: false; readonly error: string; readonly audit: string };\n\n/**\n * git's credential protocol is `key=value` lines terminated by a blank line, so any byte\n * that can end a line or a field can rewrite the request downstream of this point. A\n * `protocol` of `https\\nhost=other.invalid\\n\\n` is authorized as one identity and served as\n * another; the same is true of a carriage return, a NUL, or a bare space.\n */\nconst LINE_OR_FIELD_BREAKING = /[\\s\\u0000-\\u001f\\u007f-\\u009f]/u;\n\n/**\n * Structural, not an allowlist. Hardcoding `https` would refuse `ssh` and every custom\n * scheme a host helper answers for, which is a working feature rather than an attack. What\n * is refused is anything that is not a scheme: RFC 3986's shape, lowercased first so that\n * the released identity and the served one cannot differ by case.\n */\nconst SCHEME_SHAPE = /^[a-z][a-z0-9+.-]*$/;\n\n/** Bounds, so a frame that is within the reader's byte limit is still not an essay. */\nconst MAX_HOST_LENGTH = 255;\nconst MAX_PROTOCOL_LENGTH = 32;\n\n/**\n * Parse one credential request into the single structured identity everything downstream\n * uses.\n *\n * The caller authorizes, releases, and serializes *this* value and never the raw fields,\n * because the defect this closes is precisely that the string checked against the release\n * was not the string handed to git.\n */\nexport function parseGitCredentialRequest(request: Record<string, unknown>): GitCredentialRequestParse {\n\tconst rawProtocol = request.protocol;\n\t// git's own helper always sends one; an absent field means \"whatever git defaults to\",\n\t// and https is what this channel has always assumed. A present non-string is a client\n\t// that is not git.\n\tif (rawProtocol !== undefined && typeof rawProtocol !== \"string\") {\n\t\treturn { ok: false, error: \"Request protocol was not a string.\", audit: \"protocol field was not a string\" };\n\t}\n\tif (typeof request.host !== \"string\") {\n\t\treturn { ok: false, error: \"Request named no host.\", audit: \"host field was absent or not a string\" };\n\t}\n\n\tconst protocol = (rawProtocol ?? \"https\").toLowerCase();\n\tconst host = request.host;\n\tif (!host) return { ok: false, error: \"Request named no host.\", audit: \"host field was empty\" };\n\tif (host.length > MAX_HOST_LENGTH) {\n\t\treturn { ok: false, error: \"Request host is too long.\", audit: \"host field exceeded its length bound\" };\n\t}\n\tif (LINE_OR_FIELD_BREAKING.test(host)) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\terror: \"Request host contains a control or whitespace character.\",\n\t\t\taudit: \"host field carried a control or whitespace character\",\n\t\t};\n\t}\n\tif (protocol.length > MAX_PROTOCOL_LENGTH) {\n\t\treturn { ok: false, error: \"Request protocol is too long.\", audit: \"protocol field exceeded its length bound\" };\n\t}\n\tif (!SCHEME_SHAPE.test(protocol)) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\terror: \"Request protocol is not a scheme.\",\n\t\t\taudit: \"protocol field was not a scheme\",\n\t\t};\n\t}\n\treturn { ok: true, request: { protocol, host } };\n}\n\n/**\n * Serve git credentials to the sandboxed child without ever putting one inside it.\n *\n * ADR 0015 keeps credentials host-owned and hands the child a read-only projection. A git\n * credential cannot be projected the same way: it is not one file with a stable shape, it\n * is whatever the host's own helper produces per host. So the credential stays on the host\n * and only the answer to one question crosses, for one host, once it has been released.\n *\n * Two gates, and the first is the one that matters most.\n *\n * The host must be reachable by this session. git only asks for a credential after a\n * server challenged it, so in the ordinary flow the host was already allowed or approved\n * at the network layer. A request for a host the session cannot reach is therefore not git\n * doing its job; it is something that went looking for a token. Refusing it costs nothing\n * real and removes the case where this channel is more useful to an attacker than to git.\n *\n * The human must release it. Per ADR 0023 that decision is the supervisor's, because this\n * socket has no peer authentication -- every descendant in the child's namespace can reach\n * it, which is precisely the code the boundary exists to contain. Without a releaser the\n * channel refuses outright, so a headless session cannot hand out a credential at all.\n *\n * A release covers one host for the session. It is never persisted and never widened to a\n * second host, which is what keeps an approval for a push from also releasing a token to\n * whatever else the session later contacts.\n */\nexport function createGitCredentialProxy(options: {\n\tsocketPath: string;\n\t/** Whether this session may reach the host at all. */\n\tisHostAllowed: (host: string) => boolean;\n\t/** Ask a human to release the host's credential. Absent means refuse without asking. */\n\trequestRelease?: (host: string) => Promise<boolean>;\n\t/** Resolve the credential on the host, using the host's own configuration. */\n\tfillCredential: (request: GitCredentialRequest) => Promise<GitCredential | undefined>;\n\tviolationStore?: SandboxViolationStore;\n}): Promise<GitCredentialProxy> {\n\t/** Hosts released this session. Never persisted, never widened. */\n\tconst released = new Set<string>();\n\tconst pending = new Map<string, Promise<boolean>>();\n\n\tfunction audit(host: string, detail: string): void {\n\t\toptions.violationStore?.add({\n\t\t\tkind: \"network\",\n\t\t\tcommand: `git credential ${host}`,\n\t\t\tdetail,\n\t\t\ttimestamp: new Date(),\n\t\t});\n\t}\n\n\tasync function release(host: string): Promise<boolean> {\n\t\tif (released.has(host)) return true;\n\t\tif (!options.requestRelease) return false;\n\t\tconst inFlight = pending.get(host);\n\t\tif (inFlight) return inFlight;\n\t\tconst question = (async () => {\n\t\t\ttry {\n\t\t\t\tconst approved = await (options.requestRelease as (h: string) => Promise<boolean>)(host);\n\t\t\t\tif (approved) released.add(host);\n\t\t\t\treturn approved;\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t} finally {\n\t\t\t\tpending.delete(host);\n\t\t\t}\n\t\t})();\n\t\tpending.set(host, question);\n\t\treturn question;\n\t}\n\n\tasync function answer(request: unknown): Promise<object> {\n\t\tif (!isRequestObject(request)) return { ok: false, error: \"Malformed request.\" };\n\t\tif (request.op !== \"get\") {\n\t\t\t// Only `get` crosses. `store` and `erase` would let the child rewrite the host's\n\t\t\t// credential store, which ADR 0015 keeps as an explicit host operation.\n\t\t\treturn { ok: false, error: \"Only credential reads are served over this channel.\" };\n\t\t}\n\t\tconst parsed = parseGitCredentialRequest(request);\n\t\tif (!parsed.ok) {\n\t\t\t// The refused bytes are deliberately not echoed. This tail is read by a human and\n\t\t\t// rendered in a terminal, and the whole point of the refused field is that it\n\t\t\t// carries line breaks and control characters.\n\t\t\taudit(\"(protocol)\", `Refused a credential request whose ${parsed.audit}.`);\n\t\t\treturn { ok: false, error: parsed.error };\n\t\t}\n\t\tconst { host, protocol } = parsed.request;\n\n\t\tif (!options.isHostAllowed(host)) {\n\t\t\taudit(host, `Refused a credential for ${host}, which this session may not reach.`);\n\t\t\treturn { ok: false, error: \"Host is not reachable by this session.\" };\n\t\t}\n\t\tif (!(await release(host))) {\n\t\t\taudit(host, `Credential for ${host} was not released.`);\n\t\t\treturn { ok: false, error: \"Credential was not released.\" };\n\t\t}\n\t\tconst credential = await options.fillCredential({ host, protocol });\n\t\tif (!credential) {\n\t\t\taudit(host, `The host credential store had nothing for ${host}.`);\n\t\t\treturn { ok: false, error: \"No credential is configured for that host.\" };\n\t\t}\n\t\treturn { ok: true, username: credential.username, password: credential.password };\n\t}\n\n\treturn new Promise((resolve, reject) => {\n\t\t// A prior launch that never closed leaves a stale socket file that listen() refuses\n\t\t// to bind over, exactly as the network proxy handles.\n\t\tif (existsSync(options.socketPath)) unlinkSync(options.socketPath);\n\n\t\tconst sockets = new Set<net.Socket>();\n\t\tconst server = net.createServer((socket) => {\n\t\t\tif (sockets.size >= MAX_CONNECTIONS) {\n\t\t\t\tsocket.destroy();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsockets.add(socket);\n\t\t\tsocket.on(\"close\", () => sockets.delete(socket));\n\t\t\tsocket.on(\"error\", () => socket.destroy());\n\t\t\tconst reader = new FrameReader(socket, \"Git credential channel\", (detail) =>\n\t\t\t\taudit(\"(protocol)\", `Refused: ${detail}`),\n\t\t\t);\n\t\t\tvoid (async () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst response = await answer(await reader.next());\n\t\t\t\t\twriteFrame(socket, response);\n\t\t\t\t} catch {\n\t\t\t\t\t// A disconnected or malformed client is the reader's business; there is\n\t\t\t\t\t// nothing to answer and nothing to record beyond what it already did.\n\t\t\t\t} finally {\n\t\t\t\t\tsocket.end();\n\t\t\t\t}\n\t\t\t})();\n\t\t});\n\n\t\tserver.on(\"error\", reject);\n\t\tserver.listen(options.socketPath, () => {\n\t\t\tserver.off(\"error\", reject);\n\t\t\ttry {\n\t\t\t\tchmodSync(options.socketPath, 0o600);\n\t\t\t} catch {\n\t\t\t\t// The containing directory is already 0700; the mode here is defence in depth.\n\t\t\t}\n\t\t\tresolve({\n\t\t\t\tclose: () =>\n\t\t\t\t\tnew Promise((closed) => {\n\t\t\t\t\t\tfor (const socket of sockets) socket.destroy();\n\t\t\t\t\t\tserver.close(() => closed());\n\t\t\t\t\t}),\n\t\t\t});\n\t\t});\n\t});\n}\n"]}