hyperbrane 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (287) hide show
  1. package/README.md +81 -11
  2. package/dist/agent/agentClient.d.ts +5 -0
  3. package/dist/agent/agentClient.js +4 -0
  4. package/dist/agent/agentService.d.ts +9 -30
  5. package/dist/agent/agentService.js +62 -293
  6. package/dist/agent/agentStorage.d.ts +1 -20
  7. package/dist/agent/agentStorage.js +0 -30
  8. package/dist/agent/api.d.ts +89 -133
  9. package/dist/agent/api.js +4 -4
  10. package/dist/agent/client.d.ts +3 -1
  11. package/dist/agent/client.js +2 -3
  12. package/dist/agent/control.d.ts +23 -0
  13. package/dist/agent/control.js +479 -0
  14. package/dist/agent/hostTranscript.d.ts +0 -6
  15. package/dist/agent/hostTranscript.js +3 -14
  16. package/dist/agent/session/backend.d.ts +17 -30
  17. package/dist/agent/session/claude/home.d.ts +0 -1
  18. package/dist/agent/session/claude/home.js +0 -1
  19. package/dist/agent/session/claude/provision.d.ts +10 -0
  20. package/dist/agent/session/claude/provision.js +17 -0
  21. package/dist/agent/session/claude/transcript.d.ts +2 -7
  22. package/dist/agent/session/claude/transcript.js +68 -14
  23. package/dist/agent/session/client.d.ts +1 -10
  24. package/dist/agent/session/client.js +0 -3
  25. package/dist/agent/session/codex/provision.d.ts +14 -0
  26. package/dist/agent/session/codex/provision.js +25 -0
  27. package/dist/agent/session/fileTranscript.d.ts +6 -49
  28. package/dist/agent/session/fileTranscript.js +26 -82
  29. package/dist/agent/session/install.d.ts +5 -0
  30. package/dist/agent/session/install.js +35 -0
  31. package/dist/agent/session/sessionStorage.d.ts +4 -35
  32. package/dist/agent/session/sessionStorage.js +10 -16
  33. package/dist/agent/session/sharedConnection.d.ts +0 -8
  34. package/dist/agent/session/sharedConnection.js +0 -7
  35. package/dist/agent/session/storedTranscripts.d.ts +1 -6
  36. package/dist/agent/session/storedTranscripts.js +0 -3
  37. package/dist/agent/session/worker/jsonRpc.d.ts +2 -13
  38. package/dist/agent/session/worker/jsonRpc.js +2 -11
  39. package/dist/agent/session/worker/protocol.d.ts +27 -18
  40. package/dist/agent/session/worker/protocol.js +32 -16
  41. package/dist/agent/session/worker/workerParams.d.ts +0 -5
  42. package/dist/agent/session/worker/workerParams.js +15 -14
  43. package/dist/agent/session/workerConnection.d.ts +2 -13
  44. package/dist/agent/session/workerConnection.js +7 -12
  45. package/dist/agent/session/workerGatewayConnection.d.ts +18 -0
  46. package/dist/agent/session/workerGatewayConnection.js +86 -0
  47. package/dist/agent/vendor-types/manifest.json +87 -0
  48. package/dist/agent/workerBundle.d.ts +4 -19
  49. package/dist/agent/workerBundle.generated.js +1 -1
  50. package/dist/agent/workerBundle.js +3 -30
  51. package/dist/agent/workerReplacement.d.ts +3 -0
  52. package/dist/agent/workerReplacement.js +49 -0
  53. package/dist/cli/agent.js +44 -16
  54. package/dist/cli/command.d.ts +10 -11
  55. package/dist/cli/command.js +0 -4
  56. package/dist/cli/dimension.js +6 -6
  57. package/dist/cli/gateway.js +3 -6
  58. package/dist/cli/main.js +14 -31
  59. package/dist/cli/streamPrinter.d.ts +2 -10
  60. package/dist/cli/streamPrinter.js +16 -46
  61. package/dist/cli/table.d.ts +0 -3
  62. package/dist/cli/table.js +0 -4
  63. package/dist/dimension/api.d.ts +39 -27
  64. package/dist/dimension/api.js +28 -47
  65. package/dist/dimension/client.d.ts +5 -0
  66. package/dist/dimension/client.js +4 -0
  67. package/dist/dimension/control.d.ts +21 -0
  68. package/dist/dimension/control.js +328 -0
  69. package/dist/dimension/image.d.ts +0 -5
  70. package/dist/dimension/image.js +0 -8
  71. package/dist/dimension/imageFiles.generated.js +1 -1
  72. package/dist/dimension/paths.d.ts +5 -4
  73. package/dist/dimension/paths.js +15 -6
  74. package/dist/dimension/service.d.ts +8 -21
  75. package/dist/dimension/service.js +60 -224
  76. package/dist/dimension/transport-worker.d.ts +1 -0
  77. package/dist/dimension/transport-worker.js +151 -0
  78. package/dist/dimension/transportBundle.d.ts +2 -0
  79. package/dist/dimension/transportBundle.generated.d.ts +1 -0
  80. package/dist/dimension/transportBundle.generated.js +2 -0
  81. package/dist/dimension/transportBundle.js +4 -0
  82. package/dist/dimension/transportClient.d.ts +10 -0
  83. package/dist/dimension/transportClient.js +115 -0
  84. package/dist/dimension/transportProtocol.d.ts +53 -0
  85. package/dist/dimension/transportProtocol.js +8 -0
  86. package/dist/index.d.ts +5 -8
  87. package/dist/index.js +5 -16
  88. package/dist/lib/control.d.ts +8 -0
  89. package/dist/lib/control.js +62 -0
  90. package/dist/lib/docker.d.ts +0 -22
  91. package/dist/lib/docker.js +0 -10
  92. package/dist/lib/image.d.ts +0 -3
  93. package/dist/lib/image.js +0 -2
  94. package/dist/lib/jsonDirectory.d.ts +0 -13
  95. package/dist/lib/jsonDirectory.js +10 -15
  96. package/dist/lib/scriptBundle.d.ts +6 -0
  97. package/dist/lib/scriptBundle.js +28 -0
  98. package/dist/lib/sh.d.ts +2 -0
  99. package/dist/lib/sh.js +8 -0
  100. package/dist/lib/sleep.d.ts +0 -1
  101. package/dist/lib/sleep.js +0 -1
  102. package/dist/lib/streamFetch.d.ts +15 -0
  103. package/dist/lib/streamFetch.js +150 -0
  104. package/dist/lib/tar.d.ts +0 -1
  105. package/dist/lib/tar.js +5 -11
  106. package/dist/portal-gateway/api.d.ts +13 -44
  107. package/dist/portal-gateway/api.js +1 -3
  108. package/dist/portal-gateway/control.d.ts +42 -0
  109. package/dist/portal-gateway/control.js +657 -0
  110. package/dist/portal-gateway/gatewayClient.d.ts +5 -0
  111. package/dist/portal-gateway/gatewayClient.js +4 -0
  112. package/dist/portal-gateway/gatewayService.d.ts +8 -35
  113. package/dist/portal-gateway/gatewayService.js +79 -253
  114. package/dist/portal-gateway/image.d.ts +1 -2
  115. package/dist/portal-gateway/image.js +1 -3
  116. package/dist/portal-gateway/imageFiles.generated.js +3 -3
  117. package/dist/portal-gateway/portalClient.d.ts +5 -0
  118. package/dist/portal-gateway/portalClient.js +4 -0
  119. package/dist/portal-gateway/portalService.d.ts +5 -47
  120. package/dist/portal-gateway/portalService.js +19 -317
  121. package/dist/portal-gateway/transport-worker.d.ts +1 -0
  122. package/dist/portal-gateway/transport-worker.js +205 -0
  123. package/dist/portal-gateway/transportBundle.d.ts +2 -0
  124. package/dist/portal-gateway/transportBundle.generated.d.ts +1 -0
  125. package/dist/portal-gateway/transportBundle.generated.js +2 -0
  126. package/dist/portal-gateway/transportBundle.js +4 -0
  127. package/dist/portal-gateway/upstream.d.ts +12 -82
  128. package/dist/portal-gateway/upstream.js +0 -1
  129. package/dist/sandbox/api.d.ts +121 -178
  130. package/dist/sandbox/api.js +5 -7
  131. package/dist/sandbox/client.d.ts +2 -0
  132. package/dist/sandbox/client.js +4 -0
  133. package/dist/sandbox/data-endpoints/dataEndpoints.d.ts +13 -0
  134. package/dist/sandbox/data-endpoints/dataEndpoints.js +100 -0
  135. package/dist/sandbox/data-endpoints/firewalledPorts.d.ts +4 -0
  136. package/dist/sandbox/data-endpoints/firewalledPorts.js +28 -0
  137. package/dist/sandbox/lib/httpOverStream.d.ts +3 -0
  138. package/dist/sandbox/lib/httpOverStream.js +21 -0
  139. package/dist/sandbox/lib/remoteFileOperations.d.ts +10 -0
  140. package/dist/sandbox/lib/remoteFileOperations.js +70 -0
  141. package/dist/sandbox/lib/shellCommand.d.ts +3 -0
  142. package/dist/sandbox/lib/shellCommand.js +7 -0
  143. package/dist/sandbox/providers/boat/boatService.d.ts +2 -0
  144. package/dist/sandbox/providers/boat/boatService.js +321 -0
  145. package/dist/sandbox/providers/exedev/exeDevService.d.ts +2 -0
  146. package/dist/sandbox/providers/exedev/exeDevService.js +127 -0
  147. package/dist/sandbox/providers/exedev/exeDevToken.d.ts +1 -0
  148. package/dist/sandbox/providers/exedev/exeDevToken.js +54 -0
  149. package/dist/sandbox/providers/local/localService.d.ts +2 -0
  150. package/dist/sandbox/providers/local/localService.js +219 -0
  151. package/dist/sandbox/providers/ssh/sshService.d.ts +2 -0
  152. package/dist/sandbox/providers/ssh/sshService.js +82 -0
  153. package/dist/sandbox/providers/ssh/sshTransport.d.ts +12 -0
  154. package/dist/sandbox/providers/ssh/sshTransport.js +216 -0
  155. package/dist/sandbox/service.d.ts +2 -0
  156. package/dist/sandbox/service.js +18 -0
  157. package/dist/sandbox/storage/descriptors.d.ts +6 -0
  158. package/dist/sandbox/storage/descriptors.js +25 -0
  159. package/dist/sandbox/storage/sandboxStorage.d.ts +11 -0
  160. package/dist/sandbox/storage/sandboxStorage.js +42 -0
  161. package/dist/upstreams/anthropic.d.ts +0 -5
  162. package/dist/upstreams/anthropic.js +1 -8
  163. package/dist/upstreams/index.d.ts +2 -6
  164. package/dist/upstreams/index.js +0 -4
  165. package/dist/upstreams/openai.d.ts +0 -5
  166. package/dist/upstreams/openai.js +1 -13
  167. package/package.json +12 -4
  168. package/dist/agent/gatewaySignIns.d.ts +0 -5
  169. package/dist/agent/gatewaySignIns.js +0 -11
  170. package/dist/agent/session/codex/home.d.ts +0 -2
  171. package/dist/agent/session/codex/home.js +0 -4
  172. package/dist/agent/session/codex/messages.d.ts +0 -30
  173. package/dist/agent/session/codex/messages.js +0 -135
  174. package/dist/agent/session/codex/transcript.d.ts +0 -12
  175. package/dist/agent/session/codex/transcript.js +0 -22
  176. package/dist/agent/session/sdkMessages.d.ts +0 -15
  177. package/dist/agent/session/sdkMessages.js +0 -28
  178. package/dist/agent/session/workerSseConnection.d.ts +0 -15
  179. package/dist/agent/session/workerSseConnection.js +0 -26
  180. package/dist/agent/vendor-types/codex/app-server/AbsolutePathBuf.d.ts +0 -10
  181. package/dist/agent/vendor-types/codex/app-server/AbsolutePathBuf.js +0 -2
  182. package/dist/agent/vendor-types/codex/app-server/FunctionCallOutputBody.d.ts +0 -2
  183. package/dist/agent/vendor-types/codex/app-server/FunctionCallOutputBody.js +0 -2
  184. package/dist/agent/vendor-types/codex/app-server/FunctionCallOutputContentItem.d.ts +0 -22
  185. package/dist/agent/vendor-types/codex/app-server/FunctionCallOutputContentItem.js +0 -2
  186. package/dist/agent/vendor-types/codex/app-server/ImageDetail.d.ts +0 -1
  187. package/dist/agent/vendor-types/codex/app-server/ImageDetail.js +0 -2
  188. package/dist/agent/vendor-types/codex/app-server/ImageGenerationFailure.d.ts +0 -5
  189. package/dist/agent/vendor-types/codex/app-server/ImageGenerationFailure.js +0 -2
  190. package/dist/agent/vendor-types/codex/app-server/ImageGenerationItem.d.ts +0 -11
  191. package/dist/agent/vendor-types/codex/app-server/ImageGenerationItem.js +0 -2
  192. package/dist/agent/vendor-types/codex/app-server/LegacyAppPathString.d.ts +0 -23
  193. package/dist/agent/vendor-types/codex/app-server/LegacyAppPathString.js +0 -2
  194. package/dist/agent/vendor-types/codex/app-server/MessagePhase.d.ts +0 -7
  195. package/dist/agent/vendor-types/codex/app-server/MessagePhase.js +0 -2
  196. package/dist/agent/vendor-types/codex/app-server/ReasoningEffort.d.ts +0 -4
  197. package/dist/agent/vendor-types/codex/app-server/ReasoningEffort.js +0 -2
  198. package/dist/agent/vendor-types/codex/app-server/SleepItem.d.ts +0 -7
  199. package/dist/agent/vendor-types/codex/app-server/SleepItem.js +0 -2
  200. package/dist/agent/vendor-types/codex/app-server/WebSearchItem.d.ts +0 -14
  201. package/dist/agent/vendor-types/codex/app-server/WebSearchItem.js +0 -2
  202. package/dist/agent/vendor-types/codex/app-server/serde_json/JsonValue.d.ts +0 -3
  203. package/dist/agent/vendor-types/codex/app-server/serde_json/JsonValue.js +0 -2
  204. package/dist/agent/vendor-types/codex/app-server/v2/AgentMessageDelivery.d.ts +0 -1
  205. package/dist/agent/vendor-types/codex/app-server/v2/AgentMessageDelivery.js +0 -2
  206. package/dist/agent/vendor-types/codex/app-server/v2/AsyncUserInputQuestion.d.ts +0 -4
  207. package/dist/agent/vendor-types/codex/app-server/v2/AsyncUserInputQuestion.js +0 -2
  208. package/dist/agent/vendor-types/codex/app-server/v2/ByteRange.d.ts +0 -4
  209. package/dist/agent/vendor-types/codex/app-server/v2/ByteRange.js +0 -2
  210. package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentState.d.ts +0 -5
  211. package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentState.js +0 -2
  212. package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentStatus.d.ts +0 -1
  213. package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentStatus.js +0 -2
  214. package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentTool.d.ts +0 -1
  215. package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentTool.js +0 -2
  216. package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentToolCallStatus.d.ts +0 -1
  217. package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentToolCallStatus.js +0 -2
  218. package/dist/agent/vendor-types/codex/app-server/v2/CommandAction.d.ts +0 -19
  219. package/dist/agent/vendor-types/codex/app-server/v2/CommandAction.js +0 -2
  220. package/dist/agent/vendor-types/codex/app-server/v2/CommandExecutionSource.d.ts +0 -1
  221. package/dist/agent/vendor-types/codex/app-server/v2/CommandExecutionSource.js +0 -2
  222. package/dist/agent/vendor-types/codex/app-server/v2/CommandExecutionStatus.d.ts +0 -1
  223. package/dist/agent/vendor-types/codex/app-server/v2/CommandExecutionStatus.js +0 -2
  224. package/dist/agent/vendor-types/codex/app-server/v2/DynamicToolCallOutputContentItem.d.ts +0 -10
  225. package/dist/agent/vendor-types/codex/app-server/v2/DynamicToolCallOutputContentItem.js +0 -2
  226. package/dist/agent/vendor-types/codex/app-server/v2/DynamicToolCallStatus.d.ts +0 -1
  227. package/dist/agent/vendor-types/codex/app-server/v2/DynamicToolCallStatus.js +0 -2
  228. package/dist/agent/vendor-types/codex/app-server/v2/FileUpdateChange.d.ts +0 -6
  229. package/dist/agent/vendor-types/codex/app-server/v2/FileUpdateChange.js +0 -2
  230. package/dist/agent/vendor-types/codex/app-server/v2/HookPromptFragment.d.ts +0 -4
  231. package/dist/agent/vendor-types/codex/app-server/v2/HookPromptFragment.js +0 -2
  232. package/dist/agent/vendor-types/codex/app-server/v2/McpAppDisplayMode.d.ts +0 -1
  233. package/dist/agent/vendor-types/codex/app-server/v2/McpAppDisplayMode.js +0 -2
  234. package/dist/agent/vendor-types/codex/app-server/v2/McpAppUi.d.ts +0 -8
  235. package/dist/agent/vendor-types/codex/app-server/v2/McpAppUi.js +0 -2
  236. package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallAppContext.d.ts +0 -7
  237. package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallAppContext.js +0 -2
  238. package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallError.d.ts +0 -3
  239. package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallError.js +0 -2
  240. package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallResult.d.ts +0 -6
  241. package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallResult.js +0 -2
  242. package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallStatus.d.ts +0 -1
  243. package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallStatus.js +0 -2
  244. package/dist/agent/vendor-types/codex/app-server/v2/MemoryCitation.d.ts +0 -5
  245. package/dist/agent/vendor-types/codex/app-server/v2/MemoryCitation.js +0 -2
  246. package/dist/agent/vendor-types/codex/app-server/v2/MemoryCitationEntry.d.ts +0 -6
  247. package/dist/agent/vendor-types/codex/app-server/v2/MemoryCitationEntry.js +0 -2
  248. package/dist/agent/vendor-types/codex/app-server/v2/PatchApplyStatus.d.ts +0 -1
  249. package/dist/agent/vendor-types/codex/app-server/v2/PatchApplyStatus.js +0 -2
  250. package/dist/agent/vendor-types/codex/app-server/v2/PatchChangeKind.d.ts +0 -8
  251. package/dist/agent/vendor-types/codex/app-server/v2/PatchChangeKind.js +0 -2
  252. package/dist/agent/vendor-types/codex/app-server/v2/SubAgentActivityKind.d.ts +0 -1
  253. package/dist/agent/vendor-types/codex/app-server/v2/SubAgentActivityKind.js +0 -2
  254. package/dist/agent/vendor-types/codex/app-server/v2/TextElement.d.ts +0 -11
  255. package/dist/agent/vendor-types/codex/app-server/v2/TextElement.js +0 -2
  256. package/dist/agent/vendor-types/codex/app-server/v2/ThreadItem.d.ts +0 -215
  257. package/dist/agent/vendor-types/codex/app-server/v2/ThreadItem.js +0 -2
  258. package/dist/agent/vendor-types/codex/app-server/v2/UserInput.d.ts +0 -35
  259. package/dist/agent/vendor-types/codex/app-server/v2/UserInput.js +0 -2
  260. package/dist/agent/vendor-types/codex/app-server/v2/WebSearchAction.d.ts +0 -14
  261. package/dist/agent/vendor-types/codex/app-server/v2/WebSearchAction.js +0 -2
  262. package/dist/cli/services.d.ts +0 -19
  263. package/dist/cli/services.js +0 -33
  264. package/dist/dimension/http.d.ts +0 -7
  265. package/dist/dimension/http.js +0 -77
  266. package/dist/dimension/storage.d.ts +0 -39
  267. package/dist/dimension/storage.js +0 -79
  268. package/dist/portal-gateway/fakes.d.ts +0 -3
  269. package/dist/portal-gateway/fakes.js +0 -11
  270. package/dist/portal-gateway/gatewayStorage.d.ts +0 -66
  271. package/dist/portal-gateway/gatewayStorage.js +0 -125
  272. package/dist/portal-gateway/portalStorage.d.ts +0 -34
  273. package/dist/portal-gateway/portalStorage.js +0 -44
  274. package/dist/sandbox/boatService.d.ts +0 -52
  275. package/dist/sandbox/boatService.js +0 -343
  276. package/dist/sandbox/containerService.d.ts +0 -15
  277. package/dist/sandbox/containerService.js +0 -184
  278. package/dist/sandbox/endpointProxy.d.ts +0 -90
  279. package/dist/sandbox/endpointProxy.js +0 -701
  280. package/dist/sandbox/exeDevAuth.d.ts +0 -16
  281. package/dist/sandbox/exeDevAuth.js +0 -49
  282. package/dist/sandbox/localService.d.ts +0 -19
  283. package/dist/sandbox/localService.js +0 -200
  284. package/dist/sandbox/sseConnection.d.ts +0 -41
  285. package/dist/sandbox/sseConnection.js +0 -166
  286. package/dist/sandbox/sshService.d.ts +0 -30
  287. package/dist/sandbox/sshService.js +0 -680
package/README.md CHANGED
@@ -1,14 +1,15 @@
1
1
  # hyperbrane
2
2
 
3
3
  Dimensions (containers bound to a host directory), the portal gateways they reach the outside
4
- through, and agents (Claude Code, Codex) running in them, on a machine reached locally, over SSH, or
5
- as a Boat sandbox.
4
+ through, and agents (Claude Code, Codex) running in them, on a machine reached locally, over SSH, as
5
+ an exe.dev VM or as a Boat sandbox.
6
6
 
7
7
  ## Requirements
8
8
 
9
9
  - The library: Node ≥ 20.11 or Bun.
10
10
  - The CLI: Bun.
11
- - On the machine driven: docker, and python3 for accessible endpoints.
11
+ - On the machine driven: docker, python3, and curl (or wget) to fetch bun once for gateways' transport
12
+ workers, unless the machine has bun 1.4.3 already.
12
13
 
13
14
  ## CLI
14
15
 
@@ -21,23 +22,91 @@ hyperbrane dimension create --dir ~/work -m 4G --cpus 2 work
21
22
  ## Library
22
23
 
23
24
  ```ts
24
- import { createServices, createSshSandbox, MemoryLimit, CPULimit } from "hyperbrane";
25
+ import { createAgentClient, createDimensionClient, createGatewayClient, createPortalClient, createSandboxClient, MemoryLimit, CPULimit } from "hyperbrane";
25
26
 
26
- const host = createSshSandbox("server.example.com", { user: "me", privateKey });
27
- const { dimensions, agents } = createServices(host);
27
+ const host = createSandboxClient({ kind: "ssh", sshHost: "server.example.com", sshUser: "me", sshKey: privateKey });
28
+ // A client per part, over the sandbox: making one does nothing.
29
+ const gateways = createGatewayClient(host);
30
+ const dimensions = createDimensionClient(host);
31
+ const portals = createPortalClient(host);
32
+ const agents = createAgentClient(host);
33
+ // The gateway keeps the sign-ins, under its name: a gateway by another name starts with none.
34
+ await gateways.createGateway({ name: "main" });
28
35
  await dimensions.createDimension({ name: "work", directory: "work", memoryLimit: MemoryLimit("4G"), cpuLimit: CPULimit("2") });
36
+ // A dimension's only way out: before installing anything in it.
37
+ await portals.createPortal({ name: "work-main", dimensionName: "work", gatewayName: "main" });
29
38
  await agents.createAgent({ name: "dev", dimensionName: "work" });
39
+ await agents.install("dev", "claude");
30
40
  ```
31
41
 
32
- The root module exports the services, the sandboxes (`createLocalSandbox`, `createSshSandbox`,
33
- `createContainerSandbox`, `createBoatSandbox`), the endpoint auths and the agent client. For a
42
+ Each call resolves once what it made is usable: a gateway once it intercepts, a portal once traffic
43
+ flows through it, an install once the backend runs. Re-running any of them converges.
44
+
45
+ hyperbrane runs where the application does and drives the machine from there, each call over a
46
+ connection of its own (SSH, Boat's API): nothing is kept between calls, so it runs in serverless
47
+ functions too. A browser reaches the machine through data endpoints instead: ports the sandbox makes
48
+ reachable, one per gateway, each served by its gateway's transport worker. Where they're reached is
49
+ given when the sandbox is created:
50
+
51
+ ```ts
52
+ // Data endpoints at server.example.com, on ports 8443-8542, each opened in ufw.
53
+ await host.create({ name: "server", dataEndpointOptions: { url: "https://server.example.com", ports: { from: 8443, to: 8542 }, needsFirewall: true } });
54
+ // An exe.dev VM, over exe.dev's SSH with a key added to the account; its endpoints behind exe.dev's proxy.
55
+ const vm = createSandboxClient({ kind: "exe.dev", id: "bloggy", sshKey });
56
+ // Boat's machine is the SDK's, created with its state saved in it.
57
+ const boat = createSandboxClient({ kind: "boat", sshKey }); // BOAT_API_KEY; reaches nothing yet
58
+ const [ctx] = await boat.create({ name: "box" }); // ctx, now with the sandbox's id, is what's kept
59
+ // Elsewhere, with ctx alone: its operations read its state; open() resumes it first.
60
+ const again = createSandboxClient(ctx);
61
+ ```
62
+
63
+ Under a `url` with a root path, every endpoint is at `<url>/<name>` (with
64
+ `dataEndpointOptions: { url: "https://server.example.com/hyb" }`, the gateway `main`'s is at
65
+ `https://server.example.com/hyb/gateway-main`): whatever serves that path routes it to the endpoint's
66
+ port, which `listEndpoints()` gives.
67
+
68
+ A browser streams an agent through the gateway its dimension is wired to: hand the page
69
+ `await gateways.endpoint("main")` (the gateway's URL and credentials, which expire in 12 hours) and
70
+ the dimension's name, and it connects with
71
+ `createWorkerClient(workerGatewayConnector(access, { dimensionName, agentName }))` from
72
+ `hyperbrane/agent/client`. The agent's worker must be running (any call through `agents` starts it).
73
+ On exe.dev the credentials carry a VM token, sent in `X-Exedev-Authorization` as exe.dev documents
74
+ ([HTTPS tokens for VMs](https://exe.dev/docs/https-tokens-for-vms.md)). A browser on another site
75
+ can't get through to a private port: exe.dev answers the header's CORS preflight with a redirect to
76
+ its login. There, give the endpoints a `url` with a root path on the VM's public port (the
77
+ `https://<vm>.exe.xyz/hyb` of a server routing `/hyb/<name>` to each endpoint's port). A page
78
+ can be told `EndpointUnreachableError` or `EndpointCredentialsError` (get new ones). Clients of one
79
+ agent can share one connection: `sharedConnector(workerGatewayConnector(...))` opens it on first use
80
+ and keeps it until `close()`.
81
+
82
+ What lasts:
83
+
84
+ | | Container restart | Container recreated | Machine reboot, Boat resume |
85
+ |---|---|---|---|
86
+ | Workspace, sessions, sign-ins, installed backends | kept | kept | kept |
87
+ | Agent workers | started again by the next call (or `agents.restoreAgents()`) | same | same |
88
+ | A gateway's transport worker (on the machine) | kept | kept | started again by `restartGateway` |
89
+ | A dimension's transport worker (in it) | started again by `restartDimension` | started by `createDimension` | started again by `restartDimension` |
90
+ | Portals | re-wired by `restartDimension` and `restartGateway`; otherwise as on the right | re-wired by the next agent call, or `portals.restorePortals()` | same |
91
+
92
+ A Boat sandbox stops itself after a while; `sandbox.hasExpiration()` tells a sandbox that does, and
93
+ `setExpiration` moves it.
94
+
95
+ The root module exports the clients (`createDimensionClient`, `createGatewayClient`,
96
+ `createPortalClient`, `createAgentClient`), the sandbox of any kind (`createSandboxClient`) and the
97
+ agent's worker client. An upstream of your own is a `defineUpstream({ name, modes })`, given to a
98
+ gateway with `registerUpstream` (the built-in ones are `BUILTIN_UPSTREAMS`). `dimensionLayout` says
99
+ where a dimension keeps things: its workspace, run and state paths and CA bundle in the container,
100
+ its directories and transport worker's socket on the machine, and its container's name.
101
+ A client is an interface (`DimensionClient`, ...); these drive the machine from this process, through
102
+ the services behind them, and another transport may implement the same interfaces. For a
34
103
  browser, which can't take the server side along, the package also exports each part's API
35
104
  (`hyperbrane/sandbox/api`, `hyperbrane/dimension/api`, `hyperbrane/portal-gateway/api`,
36
105
  `hyperbrane/agent/api`) and the agent's worker client (`hyperbrane/agent/client`). Nothing else is
37
106
  importable.
38
107
 
39
108
  What a client may rely on, as the types say: a `readonly` field is set by the package, for the client
40
- to read; a type marked `@sealed` (the services, sandboxes, clients) is made by the package, for the
109
+ to read; a type marked `@sealed` (the clients, sandboxes, services) is made by the package, for the
41
110
  client to use, not implement. Both may gain members in any release; anything else gains only optional
42
111
  ones, until a breaking release (`bun run api:diff --breaking` shows what would break).
43
112
 
@@ -46,6 +115,7 @@ ones, until a breaking release (`bun run api:diff --breaking` shows what would b
46
115
  ```sh
47
116
  bun run build # dist/
48
117
  bun run check:package # packs it, installs the tarball elsewhere and uses it under Node and Bun
49
- bun run api:diff [REF] # how the public interface changed since REF (HEAD): git diff of just that
50
- bun run api:diff --breaking [REF] # only its breaking changes: removed, renamed, newly required, retyped
118
+ bun run api:diff [FROM [TO]] # how the public interface changed from FROM (HEAD) to TO (the working tree): git diff of just that
119
+ bun run api:diff FROM..TO # the same over a range of commits, e.g. v0.1.2..HEAD
120
+ bun run api:diff --breaking [FROM [TO] | FROM..TO] # only its breaking changes: removed, renamed, newly required, retyped
51
121
  ```
@@ -0,0 +1,5 @@
1
+ import type { Sandbox } from "../sandbox/api.ts";
2
+ import type { AgentClient, AgentProvision, AgentSessionCapabilities } from "./api.ts";
3
+ export declare function createAgentClient(sandbox: Sandbox, options?: {
4
+ dataDirectory?: string;
5
+ }): AgentClient & AgentProvision & AgentSessionCapabilities;
@@ -0,0 +1,4 @@
1
+ import { createAgentService } from "./agentService.js";
2
+ export function createAgentClient(sandbox, options = {}) {
3
+ return createAgentService(sandbox, options);
4
+ }
@@ -1,32 +1,11 @@
1
- import type { DockerClient } from "../lib/docker.ts";
2
- import type { Sandbox, SandboxAccessibleEndpoint } from "../sandbox/api.ts";
3
- import type { AgentStorage } from "./agentStorage.ts";
4
- import type { GatewaySignIns } from "./gatewaySignIns.ts";
5
- import { type AgentClient, type AgentEndpoint, type AgentProvision, type AgentSessionCapabilities } from "./api.ts";
1
+ import type { Sandbox } from "../sandbox/api.ts";
2
+ import { type AgentClient, type AgentProvision, type AgentSessionCapabilities } from "./api.ts";
6
3
  /** @sealed */
7
- export interface AgentService extends AgentClient, AgentProvision, AgentSessionCapabilities, AgentEndpoint {
8
- /** Starts the workers that a restart took down (their dimension runs again); returns their agents' names. */
9
- restoreAgents(): Promise<string[]>;
4
+ export interface AgentService extends AgentClient, AgentProvision, AgentSessionCapabilities {
10
5
  }
11
- /** In the dimension: where the backends are installed (a package with its node_modules). Shared by agents. */
12
- export declare const WORKER_DIR = "/opt/hyperbrane/worker";
13
- /**
14
- * In the dimension: the worker's script. Outside WORKER_DIR on purpose: bun caches the listing of the
15
- * directory a script is loaded from, and packages installed there afterwards didn't resolve.
16
- */
17
- export declare const WORKER_SCRIPT = "/opt/hyperbrane/worker.js";
18
- /**
19
- * Runs each agent as a session worker inside its dimension (agent/session/main.ts) and talks
20
- * JSON-RPC to it over the worker's unix socket, which the host reaches through the dimension's
21
- * mounted run directory (through `host`). Keeps no state between calls beyond the agent records: every call finds
22
- * the worker again.
23
- */
24
- /**
25
- * `gatewaySignIns` tells whether a dimension's gateway already holds a backend's sign-in, so a
26
- * session's login can complete without anyone; without it every login waits for the user.
27
- */
28
- /**
29
- * `host` is the machine docker runs on: workers' sockets and logs are reached through it, and agents
30
- * are exposed through its endpoints when it declares them.
31
- */
32
- export declare function createAgentService(storage: AgentStorage, docker: DockerClient, host: Sandbox | (Sandbox & SandboxAccessibleEndpoint), gatewaySignIns?: GatewaySignIns): AgentService;
6
+ export type AgentServiceWithSockets = AgentService & {
7
+ workerSocket(agentName: string): Promise<string>;
8
+ };
9
+ export declare function createAgentService(host: Sandbox, { dataDirectory }?: {
10
+ dataDirectory?: string;
11
+ }): AgentServiceWithSockets;
@@ -1,127 +1,61 @@
1
- import { dimensionStateDirectory } from "../dimension/paths.js";
2
- import { RUN_PATH, STATE_PATH, containerName as dimensionContainerName } from "../dimension/service.js";
3
- import { createContainerSandbox } from "../sandbox/containerService.js";
1
+ import { dimensionLayout } from "../dimension/api.js";
2
+ import { createDockerClient } from "../lib/docker.js";
4
3
  import { InvalidNameError, checkName } from "../lib/jsonDirectory.js";
5
- import { sleep } from "../lib/sleep.js";
6
- import { OS_CA_BUNDLE } from "../portal-gateway/portalService.js";
4
+ import { createAgentStorage } from "./agentStorage.js";
7
5
  import { AgentConflictError, AgentInvalidError, AgentNotFoundError, } from "./api.js";
8
6
  import { createWorkerClient } from "./session/client.js";
9
7
  import { workerSocketConnector } from "./session/workerConnection.js";
10
8
  import { readHostTranscript } from "./hostTranscript.js";
11
9
  import { workerSocketPath } from "./session/sessionStorage.js";
10
+ import { WORKER_PROTOCOL } from "./session/worker/protocol.js";
11
+ import { replacingWorkerClient } from "./workerReplacement.js";
12
+ import { agentsDownScript, createAgentScript, installScript, listAgentsScript, listBackendsScript, workerSocketScript, listSessionsScript, loginStateScript, stopAgentScript, } from "./control.js";
12
13
  import { WORKER_PARAMS } from "./session/worker/workerParams.js";
13
- import { workerBundle } from "./workerBundle.js";
14
- /** In the dimension: where the backends are installed (a package with its node_modules). Shared by agents. */
15
- export const WORKER_DIR = "/opt/hyperbrane/worker";
16
- /**
17
- * In the dimension: the worker's script. Outside WORKER_DIR on purpose: bun caches the listing of the
18
- * directory a script is loaded from, and packages installed there afterwards didn't resolve.
19
- */
20
- export const WORKER_SCRIPT = "/opt/hyperbrane/worker.js";
21
- const START_TIMEOUT_MS = 30_000;
22
- const STOP_TIMEOUT_MS = 10_000;
23
- /** The gateway upstream each backend's CLI signs in to (upstreams/). */
24
- const AUTH_UPSTREAMS = { claude: "anthropic", codex: "openai" };
25
- /** The worker's log in the dimension's run directory (RUN_PATH or its host side), next to its socket. */
26
- function workerLogPath(runDirectory, agentName) {
27
- return `${runDirectory}/${agentName}.log`;
28
- }
29
- // Runs as the dimension's user and returns at once: the worker is detached from the exec, so it
30
- // outlives it (and every later command), until the container stops.
31
- const START_WORKER_SCRIPT = `set -eu
32
- nohup setsid bun "$HYB_WORKER_SCRIPT" >> "$HYB_LOG" 2>&1 < /dev/null &
33
- `;
34
- // Run when a worker won't stop, as the dimension's user, whom the worker and the CLIs it started run
35
- // as: they carry its agent in their environment, which root can't read in a container (docker gives
36
- // it no ptrace capability, and reading another user's environ takes one). The name comes in as
37
- // AGENT: this script itself mustn't match.
38
- const KILL_WORKER_SCRIPT = `for process in /proc/[0-9]*; do
39
- if tr '\\0' '\\n' < "$process/environ" 2>/dev/null | grep -qx "HYB_AGENT=$AGENT"; then kill -9 "\${process#/proc/}" 2>/dev/null || true; fi
40
- done
41
- `;
42
- /**
43
- * Runs each agent as a session worker inside its dimension (agent/session/main.ts) and talks
44
- * JSON-RPC to it over the worker's unix socket, which the host reaches through the dimension's
45
- * mounted run directory (through `host`). Keeps no state between calls beyond the agent records: every call finds
46
- * the worker again.
47
- */
48
- /**
49
- * `gatewaySignIns` tells whether a dimension's gateway already holds a backend's sign-in, so a
50
- * session's login can complete without anyone; without it every login waits for the user.
51
- */
52
- /**
53
- * `host` is the machine docker runs on: workers' sockets and logs are reached through it, and agents
54
- * are exposed through its endpoints when it declares them.
55
- */
56
- export function createAgentService(storage, docker, host, gatewaySignIns) {
57
- const endpoints = "exposeEndpoint" in host ? host : null;
14
+ import { createPortalClient } from "../portal-gateway/portalClient.js";
15
+ export function createAgentService(host, { dataDirectory } = {}) {
16
+ const storage = createAgentStorage(host, dataDirectory);
17
+ const docker = createDockerClient(host);
18
+ const restoring = createPortalClient(host, { dataDirectory });
19
+ const controlOptions = { dataDirectory };
58
20
  async function runningDimension(dimensionName) {
59
- const container = dimensionContainerName(dimensionName);
21
+ const container = dimensionLayout.containerName(dimensionName);
60
22
  const inspected = await docker.inspectContainer(container);
61
23
  if (inspected?.Config?.Labels?.["hyperbrane.dimension"] !== dimensionName) {
62
24
  throw new AgentNotFoundError(`dimension ${dimensionName} does not exist`);
63
25
  }
64
26
  if (!inspected.State?.Running)
65
27
  throw new AgentConflictError(`dimension ${dimensionName} is not running`);
66
- const runDirectory = inspected.Mounts?.find((mount) => mount.Destination === RUN_PATH)?.Source;
67
- const hasState = inspected.Mounts?.some((mount) => mount.Destination === STATE_PATH);
28
+ const runDirectory = inspected.Mounts?.find((mount) => mount.Destination === dimensionLayout.runPath())?.Source;
29
+ const hasState = inspected.Mounts?.some((mount) => mount.Destination === dimensionLayout.statePath());
68
30
  if (!runDirectory || !hasState) {
69
- throw new AgentConflictError(`dimension ${dimensionName} lacks the ${RUN_PATH} and ${STATE_PATH} mounts agents need (it predates them); recreate it`);
31
+ throw new AgentConflictError(`dimension ${dimensionName} lacks the ${dimensionLayout.runPath()} and ${dimensionLayout.statePath()} mounts agents need (it predates them); recreate it`);
70
32
  }
71
- return { container, owner: inspected.Config.User || "0:0", runDirectory };
33
+ return { name: dimensionName, container, owner: inspected.Config.User || "0:0", runDirectory };
72
34
  }
73
- /** The worker as it is: for starting, stopping and checking on it. */
74
35
  function rawWorkerClient(dimension, agentName) {
75
36
  return createWorkerClient(workerSocketConnector(host, workerSocketPath(dimension.runDirectory, agentName)));
76
37
  }
77
- /**
78
- * The worker, started again first if it isn't running: it doesn't come back with its container
79
- * (a restart, a host reboot, a Boat sandbox resumed after its auto-stop). A connection that can't
80
- * be made starts it, once, and is tried again.
81
- */
82
- function workerClient(dimension, agentName) {
83
- const connect = workerSocketConnector(host, workerSocketPath(dimension.runDirectory, agentName));
84
- const reviving = async (onNotification) => {
85
- try {
86
- return await connect(onNotification);
87
- }
88
- catch {
89
- await reviveWorker(agentName, dimension);
90
- return connect(onNotification);
91
- }
92
- };
93
- return createWorkerClient(reviving);
94
- }
95
- // One start at a time per agent, however many calls find its worker gone.
96
38
  const reviving = new Map();
97
- function reviveWorker(agentName, dimension) {
39
+ function reviveWorker(agentName, dimensionName, { replaceBuild = false } = {}) {
98
40
  let started = reviving.get(agentName);
99
41
  if (!started) {
100
- started = ensureWorker(agentName, dimension)
42
+ const restorePortals = async () => restoring.restorePortals({ dimensionName });
43
+ const start = () => createAgentScript(host, { name: agentName, dimensionName }, { ...controlOptions, replaceBuild });
44
+ started = Promise.all([restorePortals(), start()])
45
+ .then(() => { })
101
46
  .catch(async () => {
102
- // Unreachable even freshly started: the container's mounts may no longer be the host's
103
- // directories (it started while they were something else). A restart binds them again.
104
- await docker.restartContainer(dimension.container);
105
- await ensureWorker(agentName, dimension);
47
+ await docker.restartContainer(dimensionLayout.containerName(dimensionName));
48
+ await Promise.all([restorePortals(), start()]);
106
49
  })
107
50
  .finally(() => reviving.delete(agentName));
108
51
  reviving.set(agentName, started);
109
52
  }
110
53
  return started;
111
54
  }
112
- /**
113
- * Starts the worker of every agent whose dimension runs but whose worker doesn't answer (after a
114
- * restart took them down); returns their names. Agents whose dimension isn't running are left.
115
- */
116
55
  async function restoreAgents() {
117
- const restored = await Promise.all((await storage.listAgents()).map(async ({ name, dimensionName }) => {
118
- const dimension = await runningDimension(dimensionName).catch(() => null);
119
- if (!dimension)
120
- return null;
121
- if (await rawWorkerClient(dimension, name).call("worker/health", {}).then(() => true, () => false))
122
- return null;
56
+ const restored = await Promise.all((await agentsDownScript(host, controlOptions)).map(async ({ name, dimensionName }) => {
123
57
  try {
124
- await reviveWorker(name, dimension);
58
+ await reviveWorker(name, dimensionName);
125
59
  return name;
126
60
  }
127
61
  catch (error) {
@@ -139,238 +73,72 @@ export function createAgentService(storage, docker, host, gatewaySignIns) {
139
73
  return agent;
140
74
  }
141
75
  async function clientFor(agentName) {
142
- const agent = await requireAgent(agentName);
143
- return workerClient(await runningDimension(agent.dimensionName), agentName);
76
+ assertValidName("agent", agentName);
77
+ const { dimensionName, socket } = await workerSocketScript(host, agentName, controlOptions);
78
+ return replacingWorkerClient(workerSocketConnector(host, socket), async (replaceBuild) => {
79
+ await runningDimension(dimensionName);
80
+ await reviveWorker(agentName, dimensionName, { replaceBuild });
81
+ });
144
82
  }
145
- // Re-running replaces a worker running older code, and starts one that stopped with its container.
146
83
  async function createAgent(agent) {
147
84
  assertValidName("agent", agent.name);
148
85
  assertValidName("dimension", agent.dimensionName);
149
- const dimension = await runningDimension(agent.dimensionName);
150
- const record = await storage.insertAgent({ name: agent.name, dimensionName: agent.dimensionName });
151
- if (record.dimensionName !== agent.dimensionName) {
152
- throw new AgentConflictError(`agent ${agent.name} already exists in dimension ${record.dimensionName}`);
153
- }
154
- await ensureWorker(agent.name, dimension);
86
+ await createAgentScript(host, agent, controlOptions);
155
87
  return { name: agent.name, dimensionName: agent.dimensionName };
156
88
  }
157
- async function ensureWorker(agentName, dimension) {
158
- const { container, owner } = dimension;
159
- const bundle = await workerBundle();
160
- const client = rawWorkerClient(dimension, agentName);
161
- const health = await client.call("worker/health", {}).catch(() => null);
162
- if (health?.build === bundle.build)
163
- return;
164
- const root = createContainerSandbox(docker, container, { user: "root" });
165
- // The run and state directories too: the host created them, possibly as another user than the dimension's.
166
- const setup = await root.runCommand(["sh", "-c", 'mkdir -p "$W" && chown "$OWNER" "$(dirname "$W")" "$W" "$R" "$S"'], {
167
- env: { W: WORKER_DIR, R: RUN_PATH, S: STATE_PATH, OWNER: owner },
168
- });
169
- if (setup.exitCode !== 0)
170
- throw new Error(`preparing ${container} for agent ${agentName} failed: ${setup.stderr.trim()}`);
171
- const sandbox = createContainerSandbox(docker, container);
172
- await sandbox.writeFile(WORKER_SCRIPT, bundle.source);
173
- await sandbox.writeFileExclusive(`${WORKER_DIR}/package.json`, JSON.stringify({ name: "hyperbrane-worker", private: true }, null, 2) + "\n");
174
- // The gateway intercepts every connection, so installs must trust the bundle portals put its CA in.
175
- await sandbox.writeFile(`${WORKER_DIR}/bunfig.toml`, `[install]\ncafile = "${OS_CA_BUNDLE}"\n`);
176
- if (health)
177
- await stopWorker(agentName, dimension);
178
- // One that doesn't answer may still run (unreachable): it goes, rather than piling up.
179
- else
180
- await killWorker(agentName, dimension);
181
- await startWorker(agentName, dimension, bundle, await socketOwner(agentName, dimension));
182
- }
183
- // The host user (uid:gid on the machine), asked once: it connects to workers' sockets.
184
- let hostUser = null;
185
- function hostUserIds() {
186
- hostUser ??= host.runCommand(["sh", "-c", 'printf "%s:%s" "$(id -u)" "$(id -g)"']).then((result) => {
187
- const [uid, gid] = result.stdout.trim().split(":").map(Number);
188
- if (result.exitCode !== 0 || !Number.isInteger(uid) || !Number.isInteger(gid)) {
189
- throw new Error(`reading the host user's ids failed: ${result.stderr.trim() || result.stdout.trim()}`);
190
- }
191
- return { uid: uid, gid: gid };
192
- });
193
- hostUser.catch(() => (hostUser = null));
194
- return hostUser;
195
- }
196
- /**
197
- * Who the worker's socket must belong to for the host user to connect (a unix socket takes write
198
- * permission): no one else when the dimension runs as the host user; the host user when it runs
199
- * as root (its directory belongs to root), which the worker then hands the socket to. A dimension
200
- * of a third user can't be reached.
201
- */
202
- async function socketOwner(agentName, dimension) {
203
- const runsAs = Number(dimension.owner.split(":")[0]);
204
- const { uid, gid } = await hostUserIds();
205
- if (runsAs === uid)
206
- return undefined;
207
- if (runsAs === 0)
208
- return `${uid}:${gid}`;
209
- throw new AgentConflictError(`agent ${agentName}'s worker would run as uid ${runsAs} (its dimension's directory belongs to that user), whose socket the host user (uid ${uid}) can't connect to; use a directory owned by uid ${uid} or root`);
210
- }
211
- async function startWorker(agentName, dimension, bundle, socketOwnerIds) {
212
- const sandbox = createContainerSandbox(docker, dimension.container);
213
- const home = await sandbox.homeDirectory();
214
- const started = await sandbox.runCommand(["sh", "-c", START_WORKER_SCRIPT], {
215
- cwd: home,
216
- env: {
217
- HOME: home,
218
- HYB_AGENT: agentName,
219
- HYB_LOG: workerLogPath(RUN_PATH, agentName),
220
- HYB_INSTALL_DIR: WORKER_DIR,
221
- HYB_WORKER_SCRIPT: WORKER_SCRIPT,
222
- HYB_WORKER_BUILD: bundle.build,
223
- ...(socketOwnerIds ? { HYB_SOCKET_OWNER: socketOwnerIds } : {}),
224
- NODE_EXTRA_CA_CERTS: OS_CA_BUNDLE,
225
- SSL_CERT_FILE: OS_CA_BUNDLE,
226
- },
227
- });
228
- if (started.exitCode !== 0)
229
- throw new Error(`starting agent ${agentName}'s worker failed: ${started.stderr.trim()}`);
230
- const client = rawWorkerClient(dimension, agentName);
231
- const deadline = Date.now() + START_TIMEOUT_MS;
232
- let lastError = "";
233
- while (Date.now() < deadline) {
234
- const health = await client.call("worker/health", {}).catch((error) => {
235
- lastError = error.message;
236
- return null;
237
- });
238
- if (health?.build === bundle.build)
239
- return;
240
- await sleep(250);
241
- }
242
- const log = await host
243
- .readFile(workerLogPath(dimension.runDirectory, agentName))
244
- .then((bytes) => new TextDecoder().decode(bytes))
245
- .catch(() => "");
246
- // A socket the host user can't write to refuses it; Bun reports that as ENOENT, so say who owns it.
247
- const socket = await host.stat(workerSocketPath(dimension.runDirectory, agentName)).catch(() => null);
248
- const { uid } = await hostUserIds();
249
- const owner = socket && socket.uid !== uid ? `; its socket belongs to uid ${socket.uid}, not the host user's ${uid}` : "";
250
- throw new Error(`agent ${agentName}'s worker did not come up${lastError ? ` (reaching it: ${lastError}${owner})` : owner}:\n${log.slice(-2000)}`);
251
- }
252
- // Asked first, so sessions end cleanly; killed if it doesn't go.
253
- async function stopWorker(agentName, dimension) {
254
- const client = rawWorkerClient(dimension, agentName);
255
- await client.call("worker/shutdown", {}).catch(() => { });
256
- const deadline = Date.now() + STOP_TIMEOUT_MS;
257
- while (Date.now() < deadline) {
258
- if ((await client.call("worker/health", {}).catch(() => null)) === null)
259
- return;
260
- await sleep(250);
261
- }
262
- await killWorker(agentName, dimension);
263
- }
264
- async function killWorker(agentName, dimension) {
265
- await createContainerSandbox(docker, dimension.container).runCommand(["sh", "-c", KILL_WORKER_SCRIPT], { env: { AGENT: agentName } });
266
- }
267
89
  async function stopAgent(agentName) {
268
90
  assertValidName("agent", agentName);
269
- const agent = await storage.readAgent(agentName);
270
- if (!agent)
271
- return;
272
- await closeAgentEndpoints(agentName);
273
- const dimension = await runningDimension(agent.dimensionName).catch(() => null);
274
- if (dimension)
275
- await stopWorker(agentName, dimension);
276
- await storage.removeAgent(agentName);
91
+ await stopAgentScript(host, agentName, controlOptions);
277
92
  }
278
- // Lazy: the stored endpoint is served again if its proxy stopped (a reboot), with the same URL and
279
- // credentials. The proxy connects to the worker's socket per stream, so a worker restart is fine.
280
- // Each way the host is reached (its accessible host) has an endpoint of its own: several server
281
- // records may drive the same machine.
282
- async function exposeAgent(agentName, scheme, allowedOrigins = []) {
93
+ async function workerSocket(agentName) {
283
94
  const agent = await requireAgent(agentName);
284
- if (!endpoints)
285
- throw new AgentInvalidError("this host doesn't declare accessible endpoints");
286
95
  const dimension = await runningDimension(agent.dimensionName);
287
- // The client connects to the worker's socket through the endpoint: nothing on its way could start
288
- // a worker a restart took down, so it's started here.
289
- if (!(await rawWorkerClient(dimension, agentName).call("worker/health", {}).then(() => true, () => false))) {
290
- await reviveWorker(agentName, dimension);
291
- }
292
- const key = endpoints.accessibleHost;
293
- const stored = (await storage.readEndpoints(agentName))[key];
294
- const sameOrigins = (origins) => [...(origins ?? [])].sort().join("\n") === [...allowedOrigins].sort().join("\n");
295
- if (stored && stored.credentials.scheme === scheme && sameOrigins(stored.allowedOrigins) && (await endpoints.ensureEndpoint(stored.id))) {
296
- return stored;
297
- }
298
- if (stored) {
299
- await endpoints.closeEndpoint(stored.id);
300
- await storage.removeEndpoint(agentName, key);
301
- }
302
- const endpoint = await endpoints.exposeEndpoint({
303
- target: { unix: workerSocketPath(dimension.runDirectory, agentName) },
304
- scheme,
305
- transport: "sse",
306
- allowedOrigins,
307
- });
308
- await storage.saveEndpoint(agentName, key, endpoint);
309
- return endpoint;
310
- }
311
- // Every way the host is reached: any sandbox on the machine can close them.
312
- async function closeAgentEndpoints(agentName) {
313
- const stored = Object.values(await storage.readEndpoints(agentName));
314
- if (endpoints)
315
- for (const endpoint of stored)
316
- await endpoints.closeEndpoint(endpoint.id);
317
- await storage.removeEndpoints(agentName);
96
+ const health = await rawWorkerClient(dimension, agentName).call("worker/health", {}).catch(() => null);
97
+ if (health?.protocol !== WORKER_PROTOCOL)
98
+ await reviveWorker(agentName, dimension.name);
99
+ return workerSocketPath(dimension.runDirectory, agentName);
318
100
  }
319
101
  async function listAgents() {
320
- return (await storage.listAgents()).sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
102
+ return (await listAgentsScript(host, controlOptions)).sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
321
103
  }
322
- /** The client of the worker running `ref`'s agent. */
323
104
  async function sessionClient({ agentName }) {
324
105
  return clientFor(agentName);
325
106
  }
326
- /**
327
- * Read on the host from the dimension's state directory (hostTranscript.ts), so nothing has to run,
328
- * neither the worker nor the dimension; only a session whose backend's home is elsewhere asks its
329
- * worker.
330
- */
331
107
  async function getTranscript(ref, request = {}) {
332
108
  const { session, limit, before } = WORKER_PARAMS["session/transcript"]({ session: ref.sessionName, ...request });
109
+ const { toolUseId } = request;
333
110
  const agent = await requireAgent(ref.agentName);
334
- const stateDirectory = dimensionStateDirectory(await storage.dataDirectory(), agent.dimensionName);
335
- const page = await readHostTranscript(host, { stateDirectory, agentName: agent.name, sessionName: session }, { limit, before });
336
- return page ?? (await sessionClient(ref)).call("session/transcript", { session, limit, before });
337
- }
338
- // Agents whose dimension or worker is down have no sessions to show right now.
339
- async function listSessions() {
340
- const perAgent = await Promise.all((await storage.listAgents()).map(async (agent) => {
341
- try {
342
- return await (await clientFor(agent.name)).call("session/list", {});
343
- }
344
- catch (error) {
345
- console.warn(`listing agent ${agent.name}'s sessions failed:`, error.message);
346
- return [];
347
- }
348
- }));
349
- return perAgent.flat();
111
+ const stateDirectory = dimensionLayout.stateDirectory(await storage.dataDirectory(), agent.dimensionName);
112
+ const page = toolUseId === undefined ? await readHostTranscript(host, { stateDirectory, agentName: agent.name, sessionName: session }, { limit, before }) : null;
113
+ if (page)
114
+ return page;
115
+ const worker = await sessionClient(ref);
116
+ if (toolUseId !== undefined)
117
+ return worker.call("session/subagentTranscript", { session, toolUseId, limit, before });
118
+ return worker.call("session/transcript", { session, limit, before });
119
+ }
120
+ function listSessions() {
121
+ return listSessionsScript(host, controlOptions);
350
122
  }
351
123
  async function* getStream(ref, request) {
352
124
  yield* (await sessionClient(ref)).stream(ref.sessionName, request.after_id);
353
125
  }
354
- // Only the host knows whether the dimension's gateway is signed in.
355
126
  async function login(ref) {
356
- const agent = await requireAgent(ref.agentName);
357
- const client = workerClient(await runningDimension(agent.dimensionName), ref.agentName);
358
- const session = (await client.call("session/list", {})).find((candidate) => candidate.name === ref.sessionName);
359
- if (!session)
360
- throw new AgentNotFoundError(`session ${ref.sessionName} does not exist`);
361
- const gatewaySignedIn = (await gatewaySignIns?.(agent.dimensionName, AUTH_UPSTREAMS[session.backend])) ?? false;
362
- return client.call("auth/login", { session: ref.sessionName, gatewaySignedIn });
127
+ assertValidName("session", ref.sessionName);
128
+ const { gatewaySignedIn } = await loginStateScript(host, ref, controlOptions);
129
+ return (await clientFor(ref.agentName)).call("auth/login", { session: ref.sessionName, gatewaySignedIn });
363
130
  }
364
131
  return {
365
- createAgent,
366
- stopAgent,
132
+ createAgent: (agent) => createAgent(agent),
133
+ stopAgent: (agentName) => stopAgent(agentName),
367
134
  listAgents,
368
135
  listSessions,
369
136
  getStream,
370
- login,
371
- exposeAgent,
137
+ login: (ref) => login(ref),
138
+ workerSocket,
372
139
  restoreAgents,
373
- install: async (agentName, backend) => void (await (await clientFor(agentName)).call("backend/install", { backend })),
140
+ install: (agentName, backend) => installScript(host, agentName, backend, controlOptions),
141
+ listBackends: (agentName) => listBackendsScript(host, agentName, controlOptions),
374
142
  createSession: async (agentName, session) => (await clientFor(agentName)).call("session/create", { session }),
375
143
  updateSession: async (ref, limits) => (await sessionClient(ref)).call("session/update", { session: ref.sessionName, limits }),
376
144
  stopSession: async (ref) => void (await (await sessionClient(ref)).call("session/stop", { session: ref.sessionName })),
@@ -385,6 +153,7 @@ export function createAgentService(storage, docker, host, gatewaySignIns) {
385
153
  respondQuestionRequest: async (ref, request, response) => void (await (await sessionClient(ref)).call("question/respond", { session: ref.sessionName, request, response })),
386
154
  listModels: async (ref) => (await sessionClient(ref)).call("model/list", { session: ref.sessionName }),
387
155
  setModel: async (ref, model) => void (await (await sessionClient(ref)).call("model/set", { session: ref.sessionName, model })),
156
+ setPermissionMode: async (ref, mode) => void (await (await sessionClient(ref)).call("permission/mode", { session: ref.sessionName, mode })),
388
157
  listBackgroundTasks: async (ref) => (await sessionClient(ref)).call("task/list", { session: ref.sessionName }),
389
158
  stopTask: async (ref, task) => void (await (await sessionClient(ref)).call("task/stop", { session: ref.sessionName, task })),
390
159
  getAuthStatus: async (ref) => (await sessionClient(ref)).call("auth/status", { session: ref.sessionName }),