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/dist/cli/table.js CHANGED
@@ -1,4 +1,3 @@
1
- // Output in the style of the docker CLI (its tabwriter: min width 10, padding 3).
2
1
  const MIN_WIDTH = 10;
3
2
  const PADDING = 3;
4
3
  export function formatTable(headers, rows) {
@@ -11,7 +10,6 @@ export function formatTable(headers, rows) {
11
10
  .join("\n");
12
11
  }
13
12
  const BYTE_UNITS = ["B", "KiB", "MiB", "GiB", "TiB", "PiB"];
14
- /** 268435456 -> "256MiB", 1597440 -> "1.523MiB" (docker's units.BytesSize). */
15
13
  export function formatBytes(bytes) {
16
14
  let value = bytes;
17
15
  let unit = 0;
@@ -21,11 +19,9 @@ export function formatBytes(bytes) {
21
19
  }
22
20
  return `${Number(value.toPrecision(4))}${BYTE_UNITS[unit]}`;
23
21
  }
24
- /** 0.0512 cores -> "5.12%" (docker stats' CPU %: 100% is one full core). */
25
22
  export function formatCpuPercent(cores) {
26
23
  return `${(cores * 100).toFixed(2)}%`;
27
24
  }
28
- /** Seconds -> "3 minutes", "About an hour", ... (docker's units.HumanDuration). */
29
25
  export function formatDuration(seconds) {
30
26
  const minutes = seconds / 60;
31
27
  const hours = minutes / 60;
@@ -1,27 +1,19 @@
1
+ export { dimensionLayout } from "./paths.ts";
1
2
  export type MemoryLimit = number & {
2
3
  readonly __brand: "MemoryLimit";
3
4
  };
4
5
  export type CPULimit = number & {
5
6
  readonly __brand: "CPULimit";
6
7
  };
7
- /** Bytes. Accepts "512", "10M", "1.5G", "10Mi", "10MB", "10MiB" (binary units, like docker). */
8
8
  export declare function MemoryLimit(value: string): MemoryLimit;
9
- /** Cores. Accepts "2", "0.5", or kubernetes-style millicores "500m". */
10
9
  export declare function CPULimit(value: string): CPULimit;
11
10
  export type Dimension = {
12
11
  name: string;
13
- /**
14
- * The host directory mounted as its workspace. Relative ("work", "~/work") is the host user's,
15
- * under their home: resolved when the dimension is created, and recorded resolved. Made (by the
16
- * host user) if it doesn't exist.
17
- */
18
12
  directory: string;
19
13
  memoryLimit: MemoryLimit;
20
14
  cpuLimit: CPULimit;
21
15
  portals?: string[];
22
16
  };
23
- /** "creating": provisioning in progress. "missing": recorded but no container (retry create). Otherwise docker's container state. */
24
- /** What `updateDimension` can change on a running dimension; omitted limits stay as they are. */
25
17
  export type DimensionLimits = {
26
18
  memoryLimit?: MemoryLimit;
27
19
  cpuLimit?: CPULimit;
@@ -29,40 +21,60 @@ export type DimensionLimits = {
29
21
  export type DimensionStatus = "creating" | "missing" | "created" | "running" | "paused" | "restarting" | "removing" | "exited" | "dead";
30
22
  export type DimensionInfo = Dimension & {
31
23
  readonly status: DimensionStatus;
32
- /** Null unless running. */
33
24
  readonly uptimeSeconds: number | null;
34
- /** Bytes. Null unless running. */
35
- readonly memoryUsage: number | null;
36
- /** Cores. Null unless running. */
37
- readonly cpuUsage: number | null;
38
25
  };
26
+ export type DimensionFileInfo = {
27
+ readonly type: "file" | "directory" | "other";
28
+ readonly size: number;
29
+ };
30
+ export type DimensionUsage = {
31
+ readonly name: string;
32
+ readonly memoryUsage: number;
33
+ readonly cpuUsage: number;
34
+ };
35
+ export declare function isDimensionName(name: string): boolean;
36
+ export declare function dimensionShellCommand(dimensionName: string, { env, command }?: {
37
+ env?: Record<string, string>;
38
+ command?: string;
39
+ }): string;
40
+ export declare const DimensionState: {
41
+ readonly running: 0;
42
+ readonly stopped: 1;
43
+ readonly missing: 2;
44
+ };
45
+ export declare function dimensionStateCommand(dimensionName: string): string;
46
+ /** @sealed */
47
+ export interface DimensionLayout {
48
+ containerName(dimensionName: string): string;
49
+ workspacePath(): string;
50
+ runPath(): string;
51
+ statePath(): string;
52
+ caBundlePath(): string;
53
+ runDirectory(dataDirectory: string, dimensionName: string): string;
54
+ stateDirectory(dataDirectory: string, dimensionName: string): string;
55
+ transportSocketPath(dataDirectory: string, dimensionName: string): string;
56
+ }
39
57
  /** @sealed */
40
58
  export interface DimensionClient {
41
- /** POST /v1/dimensions */
42
59
  createDimension(dimension: Dimension): Promise<Dimension>;
43
- /** DELETE /v1/dimensions/:dimensionName */
44
60
  destroyDimension(dimensionName: string): Promise<void>;
45
- /** POST /v1/dimensions/:dimensionName/restart */
46
61
  restartDimension(dimensionName: string): Promise<void>;
47
- /** PATCH /v1/dimensions/:dimensionName — changes its limits in place, without a restart. */
48
62
  updateDimension(dimensionName: string, limits: DimensionLimits): Promise<Dimension>;
49
- /** GET /v1/dimensions */
50
63
  listDimensions(): Promise<DimensionInfo[]>;
64
+ getDimensionUsage(): Promise<DimensionUsage[]>;
65
+ readFile(dimensionName: string, path: string, options?: {
66
+ offset?: number;
67
+ length?: number;
68
+ }): Promise<ReadableStream<Uint8Array>>;
69
+ writeFile(dimensionName: string, path: string, data: Uint8Array | ReadableStream<Uint8Array>): Promise<void>;
70
+ statFile(dimensionName: string, path: string): Promise<DimensionFileInfo>;
51
71
  }
52
- /** The request can't be satisfied as sent (bad directory, ...). */
53
72
  export declare class DimensionInvalidError extends Error {
54
73
  name: string;
55
74
  }
56
- /** No dimension (or no container for it) by that name. */
57
75
  export declare class DimensionNotFoundError extends Error {
58
76
  name: string;
59
77
  }
60
- /** The name is taken by a dimension with a different spec, or by a container hyperbrane doesn't own. */
61
78
  export declare class DimensionConflictError extends Error {
62
79
  name: string;
63
80
  }
64
- export declare class DimensionApiError extends Error {
65
- readonly status: number;
66
- constructor(status: number, message: string);
67
- }
68
- export declare function createDimensionClient(baseUrl: string): DimensionClient;
@@ -1,3 +1,5 @@
1
+ import { containerName } from "./paths.js";
2
+ export { dimensionLayout } from "./paths.js";
1
3
  const MEMORY_UNITS = {
2
4
  "": 1,
3
5
  K: 1024,
@@ -5,7 +7,6 @@ const MEMORY_UNITS = {
5
7
  G: 1024 ** 3,
6
8
  T: 1024 ** 4,
7
9
  };
8
- /** Bytes. Accepts "512", "10M", "1.5G", "10Mi", "10MB", "10MiB" (binary units, like docker). */
9
10
  export function MemoryLimit(value) {
10
11
  const match = /^(\d+(?:\.\d+)?)\s*([KMGT]?)(?:iB|i|B)?$/i.exec(value.trim());
11
12
  if (!match)
@@ -15,7 +16,6 @@ export function MemoryLimit(value) {
15
16
  throw new Error(`Memory limit must be positive: "${value}"`);
16
17
  return bytes;
17
18
  }
18
- /** Cores. Accepts "2", "0.5", or kubernetes-style millicores "500m". */
19
19
  export function CPULimit(value) {
20
20
  const match = /^(\d+(?:\.\d+)?)\s*(m?)$/.exec(value.trim());
21
21
  if (!match)
@@ -25,57 +25,38 @@ export function CPULimit(value) {
25
25
  throw new Error(`CPU limit must be positive: "${value}"`);
26
26
  return cores;
27
27
  }
28
- /** The request can't be satisfied as sent (bad directory, ...). */
28
+ const DIMENSION_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,62}$/;
29
+ export function isDimensionName(name) {
30
+ return DIMENSION_NAME.test(name);
31
+ }
32
+ const quote = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
33
+ function checkedContainer(dimensionName) {
34
+ if (!isDimensionName(dimensionName))
35
+ throw new DimensionInvalidError(`invalid dimension name "${dimensionName}"`);
36
+ return containerName(dimensionName);
37
+ }
38
+ export function dimensionShellCommand(dimensionName, { env = {}, command } = {}) {
39
+ const container = checkedContainer(dimensionName);
40
+ const vars = Object.entries(env).map(([name, value]) => {
41
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
42
+ throw new DimensionInvalidError(`invalid variable name "${name}"`);
43
+ return ` -e ${quote(`${name}=${value}`)}`;
44
+ });
45
+ return command === undefined
46
+ ? `docker exec -it${vars.join("")} ${container} bash -l`
47
+ : `docker exec -i${vars.join("")} ${container} bash -lc ${quote(command)}`;
48
+ }
49
+ export const DimensionState = { running: 0, stopped: 1, missing: 2 };
50
+ export function dimensionStateCommand(dimensionName) {
51
+ const container = checkedContainer(dimensionName);
52
+ return `{ s="$(docker inspect -f '{{.State.Status}}' ${container} 2>/dev/null)" || exit ${DimensionState.missing}; [ "$s" = running ] || exit ${DimensionState.stopped}; }`;
53
+ }
29
54
  export class DimensionInvalidError extends Error {
30
55
  name = "DimensionInvalidError";
31
56
  }
32
- /** No dimension (or no container for it) by that name. */
33
57
  export class DimensionNotFoundError extends Error {
34
58
  name = "DimensionNotFoundError";
35
59
  }
36
- /** The name is taken by a dimension with a different spec, or by a container hyperbrane doesn't own. */
37
60
  export class DimensionConflictError extends Error {
38
61
  name = "DimensionConflictError";
39
62
  }
40
- export class DimensionApiError extends Error {
41
- status;
42
- constructor(status, message) {
43
- super(message);
44
- this.status = status;
45
- this.name = "DimensionApiError";
46
- }
47
- }
48
- export function createDimensionClient(baseUrl) {
49
- async function request(method, path, body) {
50
- const res = await fetch(new URL(path, baseUrl), {
51
- method,
52
- headers: body === undefined ? undefined : { "content-type": "application/json" },
53
- body: body === undefined ? undefined : JSON.stringify(body),
54
- });
55
- if (res.ok)
56
- return res;
57
- const text = await res.text();
58
- let message = text;
59
- try {
60
- message = JSON.parse(text).error ?? text;
61
- }
62
- catch { }
63
- throw new DimensionApiError(res.status, `${method} ${path} -> ${res.status}: ${message}`);
64
- }
65
- async function createDimension(dimension) {
66
- return (await request("POST", "/v1/dimensions", dimension)).json();
67
- }
68
- async function destroyDimension(dimensionName) {
69
- await request("DELETE", `/v1/dimensions/${encodeURIComponent(dimensionName)}`);
70
- }
71
- async function restartDimension(dimensionName) {
72
- await request("POST", `/v1/dimensions/${encodeURIComponent(dimensionName)}/restart`);
73
- }
74
- async function listDimensions() {
75
- return (await request("GET", "/v1/dimensions")).json();
76
- }
77
- async function updateDimension(dimensionName, limits) {
78
- return (await request("PATCH", `/v1/dimensions/${encodeURIComponent(dimensionName)}`, limits)).json();
79
- }
80
- return { createDimension, destroyDimension, restartDimension, listDimensions, updateDimension };
81
- }
@@ -0,0 +1,5 @@
1
+ import type { Sandbox } from "../sandbox/api.ts";
2
+ import type { DimensionClient } from "./api.ts";
3
+ export declare function createDimensionClient(sandbox: Sandbox, options?: {
4
+ dataDirectory?: string;
5
+ }): DimensionClient;
@@ -0,0 +1,4 @@
1
+ import { createDimensionService } from "./service.js";
2
+ export function createDimensionClient(sandbox, options = {}) {
3
+ return createDimensionService(sandbox, options);
4
+ }
@@ -0,0 +1,21 @@
1
+ import { type ControlOptions } from "../lib/control.ts";
2
+ import type { Sandbox } from "../sandbox/api.ts";
3
+ import { type Dimension, type DimensionLimits, type DimensionStatus, type DimensionUsage } from "./api.ts";
4
+ export type { ControlOptions };
5
+ export type DimensionRecord = Dimension & {
6
+ state: "creating" | "ready";
7
+ };
8
+ export type DimensionState = {
9
+ record: DimensionRecord;
10
+ status: DimensionStatus;
11
+ startedAt: string | null;
12
+ };
13
+ export declare function createDimensionScript(host: Sandbox, dimension: Dimension, options?: ControlOptions): Promise<DimensionRecord | false>;
14
+ export declare function destroyDimensionScript(host: Sandbox, dimensionName: string, options?: ControlOptions): Promise<void>;
15
+ export declare function restartDimensionScript(host: Sandbox, dimensionName: string, options?: ControlOptions): Promise<void>;
16
+ export declare function updateDimensionScript(host: Sandbox, dimensionName: string, limits: DimensionLimits, options?: ControlOptions): Promise<DimensionRecord>;
17
+ export declare function listDimensionsScript(host: Sandbox, options?: ControlOptions): Promise<DimensionState[]>;
18
+ export declare function dimensionUsageScript(host: Sandbox, options?: ControlOptions): Promise<DimensionUsage[]>;
19
+ export declare function startTransportScript(host: Sandbox, dimensionName: string, options: ControlOptions, { restart, }: {
20
+ restart: boolean;
21
+ }): Promise<void>;
@@ -0,0 +1,328 @@
1
+ import { exitEnv, records, runControl } from "../lib/control.js";
2
+ import { sh } from "../lib/sh.js";
3
+ import { HYPERBRANE_VERSION } from "../lib/version.js";
4
+ import { DimensionConflictError, DimensionInvalidError, DimensionNotFoundError } from "./api.js";
5
+ import { DIMENSION_IMAGE, DIMENSION_IMAGE_REPOSITORY } from "./image.js";
6
+ import { RUN_PATH, STATE_PATH, TRANSPORT_SOCKET, WORKSPACE_PATH, containerName } from "./paths.js";
7
+ import { dimensionTransportBundle } from "./transportBundle.js";
8
+ import { TRANSPORT_LOG, TRANSPORT_READY } from "./transportProtocol.js";
9
+ const EXIT = { invalid: 40, conflict: 41, notFound: 42, needImage: 43, needBundle: 44, thirdUser: 45 };
10
+ export async function createDimensionScript(host, dimension, options = {}) {
11
+ const portals = dimension.portals?.map((portal) => JSON.stringify(portal));
12
+ const env = {
13
+ ...dimensionEnv(dimension.name, options),
14
+ DIRECTORY: dimension.directory,
15
+ MEMORY: String(dimension.memoryLimit),
16
+ CPUS: String(dimension.cpuLimit),
17
+ SWAP: String(dimension.memoryLimit * 2),
18
+ PORTALS: portals ? [...portals].sort().join(",") : "",
19
+ PORTALS_LINE: portals ? ` "portals": ${JSON.stringify(dimension.portals)},\n` : "",
20
+ IMAGE: DIMENSION_IMAGE,
21
+ IMAGE_REPOSITORY: DIMENSION_IMAGE_REPOSITORY,
22
+ VERSION: HYPERBRANE_VERSION,
23
+ WORKSPACE_PATH,
24
+ };
25
+ const result = await run(host, CREATE_DIMENSION, env);
26
+ if (result.exitCode === EXIT.needImage)
27
+ return false;
28
+ return JSON.parse(check(result, `creating dimension ${dimension.name}`).stdout);
29
+ }
30
+ export async function destroyDimensionScript(host, dimensionName, options = {}) {
31
+ check(await run(host, DESTROY_DIMENSION, dimensionEnv(dimensionName, options)), `destroying dimension ${dimensionName}`);
32
+ }
33
+ export async function restartDimensionScript(host, dimensionName, options = {}) {
34
+ check(await run(host, RESTART_DIMENSION, dimensionEnv(dimensionName, options)), `restarting dimension ${dimensionName}`);
35
+ }
36
+ export async function updateDimensionScript(host, dimensionName, limits, options = {}) {
37
+ const env = { ...dimensionEnv(dimensionName, options), MEMORY: String(limits.memoryLimit ?? ""), CPUS: String(limits.cpuLimit ?? "") };
38
+ const result = check(await run(host, UPDATE_DIMENSION, env), `updating dimension ${dimensionName}`);
39
+ return JSON.parse(result.stdout);
40
+ }
41
+ export async function listDimensionsScript(host, options = {}) {
42
+ const result = check(await run(host, LIST_DIMENSIONS, dataEnv(options)), "listing dimensions");
43
+ const [recordsPart = "", containersPart = ""] = result.stdout.split(CONTAINERS_START);
44
+ const containers = new Map(containersPart
45
+ .split("\n")
46
+ .filter(Boolean)
47
+ .map((line) => {
48
+ const [name = "", status = "", startedAt = ""] = line.split("|");
49
+ return [name.replace(/^\//, ""), { status: status, startedAt }];
50
+ }));
51
+ return records(recordsPart).map(([, record]) => {
52
+ if (record.state === "creating")
53
+ return { record, status: "creating", startedAt: null };
54
+ const container = containers.get(containerName(record.name));
55
+ if (!container)
56
+ return { record, status: "missing", startedAt: null };
57
+ return { record, status: container.status, startedAt: container.status === "running" ? container.startedAt : null };
58
+ });
59
+ }
60
+ export async function dimensionUsageScript(host, options = {}) {
61
+ const result = check(await run(host, DIMENSION_USAGE, dataEnv(options)), "reading dimensions' usage");
62
+ return result.stdout
63
+ .split("\n")
64
+ .filter(Boolean)
65
+ .flatMap((line) => {
66
+ const [container = "", cpu = "", memory = ""] = line.split("\t");
67
+ const memoryUsage = bytes(memory.split("/")[0].trim());
68
+ const cpuUsage = Number.parseFloat(cpu) / 100;
69
+ if (memoryUsage === null || Number.isNaN(cpuUsage))
70
+ return [];
71
+ return [{ name: container.slice(containerName("").length), memoryUsage, cpuUsage }];
72
+ });
73
+ }
74
+ function bytes(text) {
75
+ const match = /^([\d.]+)\s*([KMGT]?)(i?)B$/i.exec(text);
76
+ if (!match)
77
+ return null;
78
+ const power = "_KMGT".indexOf(match[2].toUpperCase() || "_");
79
+ return Math.round(Number(match[1]) * (match[3] ? 1024 : 1000) ** power);
80
+ }
81
+ export async function startTransportScript(host, dimensionName, options, { restart, }) {
82
+ const { source, build } = await dimensionTransportBundle();
83
+ const env = {
84
+ ...dimensionEnv(dimensionName, options),
85
+ BUILD: build,
86
+ RESTART: restart ? "1" : "",
87
+ WORKSPACE_PATH,
88
+ TRANSPORT_SOCKET,
89
+ TRANSPORT_READY,
90
+ TRANSPORT_LOG,
91
+ };
92
+ let result = await run(host, START_TRANSPORT, env);
93
+ if (result.exitCode === EXIT.needBundle) {
94
+ await host.writeFile(`${result.stdout.trim()}/.transport-${build}.js`, source);
95
+ result = await run(host, START_TRANSPORT, env);
96
+ }
97
+ check(result, `starting dimension ${dimensionName}'s transport worker`);
98
+ }
99
+ function dataEnv({ dataDirectory }) {
100
+ return { ...(dataDirectory === undefined ? {} : { DATA: dataDirectory }), CONTAINER_PREFIX: containerName(""), RUN_PATH, STATE_PATH };
101
+ }
102
+ function dimensionEnv(dimensionName, options) {
103
+ return { ...dataEnv(options), DIMENSION: dimensionName };
104
+ }
105
+ function run(host, script, env) {
106
+ return runControl(host, PRELUDE, script, { ...exitEnv(EXIT), ...env }, {});
107
+ }
108
+ function check(result, what) {
109
+ const said = result.stderr.trim();
110
+ switch (result.exitCode) {
111
+ case 0:
112
+ return result;
113
+ case EXIT.invalid:
114
+ throw new DimensionInvalidError(said);
115
+ case EXIT.conflict:
116
+ throw new DimensionConflictError(said);
117
+ case EXIT.notFound:
118
+ throw new DimensionNotFoundError(said);
119
+ default:
120
+ throw new Error(`${what} failed (exit ${result.exitCode}): ${said || result.stdout.trim()}`);
121
+ }
122
+ }
123
+ const CONTAINERS_START = "\n\x1d\n";
124
+ const PRELUDE = sh `
125
+ # shellcheck disable=SC2034
126
+ dimension_paths() {
127
+ RECORD="$DATA/dimensions/$DIMENSION.json"
128
+ CONTAINER="$CONTAINER_PREFIX$DIMENSION"
129
+ RUN="$DATA/dimension-run/$DIMENSION"
130
+ STATE="$DATA/dimension-state/$DIMENSION"
131
+ }
132
+
133
+ # shellcheck disable=SC2034
134
+ container_exists() {
135
+ info="$(docker inspect -f '{{index .Config.Labels "hyperbrane.dimension"}}|{{.State.Running}}' "$CONTAINER" 2>/dev/null)" || return 1
136
+ label="\${info%|*}"
137
+ RUNNING="\${info##*|}"
138
+ [ "$label" = "$DIMENSION" ] || refuse "$E_CONFLICT" "container $CONTAINER exists but is not managed by hyperbrane"
139
+ }
140
+
141
+ require_dimension() {
142
+ [ -f "$RECORD" ] || refuse "$E_NOT_FOUND" "dimension $DIMENSION does not exist"
143
+ container_exists || refuse "$E_NOT_FOUND" "dimension $DIMENSION has no container; create it again"
144
+ }
145
+ `;
146
+ const CREATE_DIMENSION = sh `
147
+ set -eu
148
+ dimension_paths
149
+
150
+ # shellcheck disable=SC2153
151
+ case "$DIRECTORY" in
152
+ /*) directory="$DIRECTORY" ;;
153
+ \~) directory="$HOME" ;;
154
+ \~/*) directory="$HOME/\${DIRECTORY#\~/}" ;;
155
+ *) directory="$HOME/$DIRECTORY" ;;
156
+ esac
157
+ if [ ! -e "$directory" ] && ! made="$(mkdir -p "$directory" 2>&1)"; then
158
+ refuse "$E_INVALID" "directory $directory doesn't exist and can't be made: $made"
159
+ fi
160
+ [ -d "$directory" ] || refuse "$E_INVALID" "not a directory: $directory"
161
+ # shellcheck disable=SC2012
162
+ owner="$(ls -nd "$directory" | awk '{ print $3 ":" $4 }')"
163
+
164
+ container_exists || true
165
+
166
+ directory_json="$(printf '%s' "$directory" | sed 's/\\/\\\\/g; s/"/\\"/g')"
167
+ dimension_record() {
168
+ printf '{\n "name": "%s",\n "directory": "%s",\n "memoryLimit": %s,\n "cpuLimit": %s,\n%s "state": "%s"\n}\n' \
169
+ "$DIMENSION" "$directory_json" "$MEMORY" "$CPUS" "$PORTALS_LINE" "$1"
170
+ }
171
+ mkdir -p "$DATA/dimensions"
172
+ if [ ! -f "$RECORD" ]; then
173
+ dimension_record creating > "$RECORD.$$"
174
+ ln "$RECORD.$$" "$RECORD" 2>/dev/null || true
175
+ rm -f "$RECORD.$$"
176
+ fi
177
+ recorded_portals="$(tr -d ' \n' < "$RECORD" | sed -n 's/.*"portals":\[\([^]]*\)\].*/\1/p' | tr ',' '\n' | LC_ALL=C sort | paste -sd, -)"
178
+ # shellcheck disable=SC2153
179
+ if [ "$(record_field "$RECORD" directory)" != "\"$directory_json\"" ] ||
180
+ [ "$(record_field "$RECORD" memoryLimit)" != "$MEMORY" ] ||
181
+ [ "$(record_field "$RECORD" cpuLimit)" != "$CPUS" ] ||
182
+ [ "$recorded_portals" != "$PORTALS" ]; then
183
+ refuse "$E_CONFLICT" "dimension $DIMENSION already exists with a different spec"
184
+ fi
185
+
186
+ docker image inspect "$IMAGE" > /dev/null 2>&1 || exit "$E_NEED_IMAGE"
187
+
188
+ mkdir -p "$RUN" "$STATE"
189
+ if ! container_exists; then
190
+ if ! created="$(docker create --name "$CONTAINER" --hostname "$CONTAINER" --user "$owner" --workdir "$WORKSPACE_PATH" \
191
+ --label hyperbrane=true --label "hyperbrane.version=$VERSION" --label "hyperbrane.dimension=$DIMENSION" \
192
+ --volume "$directory:$WORKSPACE_PATH" --volume "$RUN:$RUN_PATH" --volume "$STATE:$STATE_PATH" \
193
+ --cap-add SYSLOG --memory "$MEMORY" --memory-swap "$SWAP" --cpus "$CPUS" --network none \
194
+ --add-host "$CONTAINER:127.0.1.1" --init --restart unless-stopped \
195
+ "$IMAGE" sleep infinity 2>&1)"; then
196
+ case "$created" in
197
+ *"already in use"*) ;;
198
+ *) refuse 1 "$created" ;;
199
+ esac
200
+ waited=0
201
+ until container_exists; do
202
+ [ "$waited" -lt 50 ] || refuse 1 "$created"
203
+ sleep 0.1
204
+ waited=$((waited + 1))
205
+ done
206
+ fi
207
+ fi
208
+ docker start "$CONTAINER" > /dev/null
209
+ docker exec -u root -e HYB_UID="\${owner%:*}" -e HYB_GID="\${owner#*:}" "$CONTAINER" hyb-ensure-user
210
+ grep -q '"state": "ready"' "$RECORD" || dimension_record ready | write_whole "$RECORD"
211
+
212
+ docker images --filter label=hyperbrane --filter "reference=$IMAGE_REPOSITORY" --format '{{.Repository}}:{{.Tag}}' |
213
+ while read -r tag; do
214
+ [ "$tag" = "$IMAGE" ] || docker rmi "$tag" > /dev/null 2>&1 || true
215
+ done
216
+ cat "$RECORD"
217
+ `;
218
+ const DESTROY_DIMENSION = sh `
219
+ set -eu
220
+ dimension_paths
221
+ if container_exists; then
222
+ [ "$RUNNING" = true ] || docker start "$CONTAINER" > /dev/null 2>&1 || true
223
+ docker exec -u root -e R="$RUN_PATH" -e S="$STATE_PATH" "$CONTAINER" sh -c 'find "$R" "$S" -mindepth 1 -delete' > /dev/null 2>&1 || true
224
+ if ! removed="$(docker rm -f "$CONTAINER" 2>&1)" && docker inspect "$CONTAINER" > /dev/null 2>&1; then refuse 1 "$removed"; fi
225
+ fi
226
+ rm -rf "$RUN" "$STATE"
227
+ rm -f "$RECORD"
228
+ `;
229
+ const START_TRANSPORT = sh `
230
+ set -eu
231
+ dimension_paths
232
+ require_dimension
233
+ [ "$RUNNING" = true ] || refuse "$E_CONFLICT" "dimension $DIMENSION is not running"
234
+ mkdir -p "$RUN"
235
+ exec 8> "$RUN/.transport.lock"
236
+ flock -w 60 8
237
+
238
+ transport_running() {
239
+ TRANSPORT_PID=""
240
+ TRANSPORT_BUILD=""
241
+ [ -f "$RUN/$TRANSPORT_READY" ] && [ -S "$RUN/$TRANSPORT_SOCKET" ] || return 1
242
+ read -r TRANSPORT_PID TRANSPORT_BUILD < "$RUN/$TRANSPORT_READY" || return 1
243
+ docker exec -u 0 "$CONTAINER" grep -qs transport "/proc/$TRANSPORT_PID/cmdline"
244
+ }
245
+
246
+ if [ -z "$RESTART" ] && transport_running && [ "$TRANSPORT_BUILD" = "$BUILD" ]; then exit 0; fi
247
+ [ -f "$RUN/.transport-$BUILD.js" ] || { printf '%s\n' "$RUN"; exit "$E_NEED_BUNDLE"; }
248
+
249
+ user="$(docker inspect -f '{{.Config.User}}' "$CONTAINER")"
250
+ user="\${user:-0}"
251
+ socket_owner=""
252
+ if [ "\${user%%:*}" = 0 ]; then
253
+ socket_owner="$(id -u):$(id -g)"
254
+ elif [ "\${user%%:*}" != "$(id -u)" ]; then
255
+ refuse "$E_THIRD_USER" "dimension $DIMENSION runs as uid \${user%%:*}, whose transport socket uid $(id -u) can't connect to"
256
+ fi
257
+
258
+ if transport_running; then
259
+ docker exec -u 0 "$CONTAINER" kill "$TRANSPORT_PID" 2> /dev/null || true
260
+ for _ in 1 2 3 4 5 6 7 8 9 10; do
261
+ docker exec -u 0 "$CONTAINER" kill -0 "$TRANSPORT_PID" 2> /dev/null || break
262
+ sleep 0.5
263
+ done
264
+ docker exec -u 0 "$CONTAINER" kill -9 "$TRANSPORT_PID" 2> /dev/null || true
265
+ fi
266
+ rm -f "$RUN/$TRANSPORT_READY"
267
+ # shellcheck disable=SC2016
268
+ docker exec -d -u "$user" -e HYB_WORKSPACE="$WORKSPACE_PATH" -e HYB_TRANSPORT_SOCKET="$RUN_PATH/$TRANSPORT_SOCKET" \
269
+ -e HYB_TRANSPORT_READY="$RUN_PATH/$TRANSPORT_READY" -e HYB_TRANSPORT_BUILD="$BUILD" -e HYB_SOCKET_OWNER="$socket_owner" \
270
+ -e SCRIPT="$RUN_PATH/.transport-$BUILD.js" -e LOG="$RUN_PATH/$TRANSPORT_LOG" \
271
+ "$CONTAINER" sh -c 'exec bun "$SCRIPT" >> "$LOG" 2>&1 < /dev/null'
272
+
273
+ waited=0
274
+ until transport_running && [ "$TRANSPORT_BUILD" = "$BUILD" ]; do
275
+ if [ "$waited" -ge 100 ]; then
276
+ echo "dimension $DIMENSION's transport worker did not come up:" >&2
277
+ tail -c 2000 "$RUN/$TRANSPORT_LOG" >&2 2> /dev/null || true
278
+ exit 1
279
+ fi
280
+ sleep 0.2
281
+ waited=$((waited + 1))
282
+ done
283
+ find "$RUN" -maxdepth 1 -name '.transport-*.js' ! -name ".transport-$BUILD.js" -exec rm -f {} + 2> /dev/null || true
284
+ `;
285
+ const RESTART_DIMENSION = sh `
286
+ set -eu
287
+ dimension_paths
288
+ require_dimension
289
+ docker restart "$CONTAINER" > /dev/null
290
+ `;
291
+ const UPDATE_DIMENSION = sh `
292
+ set -eu
293
+ dimension_paths
294
+ require_dimension
295
+ MEMORY="\${MEMORY:-$(record_field "$RECORD" memoryLimit)}"
296
+ CPUS="\${CPUS:-$(record_field "$RECORD" cpuLimit)}"
297
+ SWAP=$((MEMORY * 2))
298
+ docker update --memory "$MEMORY" --memory-swap "$SWAP" --cpus "$CPUS" "$CONTAINER" > /dev/null
299
+ sed 's/^ "memoryLimit": .*,$/ "memoryLimit": '"$MEMORY"',/; s/^ "cpuLimit": .*,$/ "cpuLimit": '"$CPUS"',/' "$RECORD" | write_whole "$RECORD"
300
+ cat "$RECORD"
301
+ `;
302
+ const LIST_DIMENSIONS = sh `
303
+ containers=""
304
+ for record in "$DATA"/dimensions/*.json; do
305
+ [ -f "$record" ] || continue
306
+ name="\${record##*/}"
307
+ name="\${name%.json}"
308
+ printf '%s\n' "$name"
309
+ cat "$record"
310
+ printf '\n\036\n'
311
+ containers="$containers $CONTAINER_PREFIX$name"
312
+ done
313
+ printf '\n\035\n'
314
+ # shellcheck disable=SC2086
315
+ [ -z "$containers" ] || docker inspect -f '{{.Name}}|{{.State.Status}}|{{.State.StartedAt}}' $containers 2>/dev/null || true
316
+ `;
317
+ const DIMENSION_USAGE = sh `
318
+ running="$(docker ps --filter label=hyperbrane.dimension --format '{{.Names}}')"
319
+ containers=""
320
+ for record in "$DATA"/dimensions/*.json; do
321
+ [ -f "$record" ] || continue
322
+ name="\${record##*/}"
323
+ container="$CONTAINER_PREFIX\${name%.json}"
324
+ if printf '%s\n' "$running" | grep -qx "$container"; then containers="$containers $container"; fi
325
+ done
326
+ # shellcheck disable=SC2086
327
+ [ -z "$containers" ] || docker stats --no-stream --format '{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}' $containers
328
+ `;
@@ -1,13 +1,8 @@
1
1
  import { type DockerClient } from "../lib/docker.ts";
2
- /**
3
- * Bump whenever anything under ./image changes (and run `bun run embed:images`); the tag is how an
4
- * existing build is recognised.
5
- */
6
2
  export declare const DIMENSION_IMAGE_VERSION = "v6";
7
3
  export declare const DIMENSION_IMAGE_REPOSITORY = "hyperbrane-dimension";
8
4
  export declare const DIMENSION_IMAGE = "hyperbrane-dimension:v6";
9
5
  export interface DimensionImage {
10
- /** Builds the base image if it isn't present yet, removes outdated ones, and returns its tag. */
11
6
  ensure(): Promise<string>;
12
7
  }
13
8
  export declare function createDimensionImage(docker: DockerClient): DimensionImage;
@@ -1,17 +1,10 @@
1
1
  import { DockerError } from "../lib/docker.js";
2
- // The files under ./image, as strings (scripts/embed-image-files.ts), so they travel inside the module
3
- // graph under any runtime or bundler instead of being looked up on disk at runtime.
4
2
  import { IMAGE_FILES } from "./imageFiles.generated.js";
5
- /**
6
- * Bump whenever anything under ./image changes (and run `bun run embed:images`); the tag is how an
7
- * existing build is recognised.
8
- */
9
3
  export const DIMENSION_IMAGE_VERSION = "v6";
10
4
  export const DIMENSION_IMAGE_REPOSITORY = "hyperbrane-dimension";
11
5
  export const DIMENSION_IMAGE = `${DIMENSION_IMAGE_REPOSITORY}:${DIMENSION_IMAGE_VERSION}`;
12
6
  const BUILD_CONTEXT = IMAGE_FILES;
13
7
  export function createDimensionImage(docker) {
14
- // Concurrent builds of the same tag (from any process) are harmless: they produce the same image.
15
8
  async function ensure() {
16
9
  if (!(await docker.imageExists(DIMENSION_IMAGE))) {
17
10
  await docker.buildImage({
@@ -23,7 +16,6 @@ export function createDimensionImage(docker) {
23
16
  await removeOutdatedImages().catch((error) => console.warn("removing outdated dimension images failed:", error));
24
17
  return DIMENSION_IMAGE;
25
18
  }
26
- // Images still used by a container are skipped (docker answers 409) and retried on a later ensure().
27
19
  async function removeOutdatedImages() {
28
20
  const images = await docker.listImages({ label: ["hyperbrane"], reference: [DIMENSION_IMAGE_REPOSITORY] });
29
21
  const outdated = images
@@ -1,5 +1,5 @@
1
1
  // Generated by scripts/embed-image-files.ts from dimension/image/ — do not edit; run `bun run embed:images`.
2
2
  export const IMAGE_FILES = {
3
3
  "Dockerfile": "# bun runs the agent session worker (agent/session/), and the agent CLIs it installs.\nFROM oven/bun:1.3.13-slim AS bun\nFROM debian:13-slim\nRUN apt-get update \\\n && apt-get install -y --no-install-recommends bash sudo git jq curl wget iputils-ping ca-certificates procps \\\n && rm -rf /var/lib/apt/lists/*\nCOPY --from=bun /usr/local/bin/bun /usr/local/bin/bun\nCOPY ensure-user.sh /usr/local/bin/hyb-ensure-user\nRUN chmod 0755 /usr/local/bin/hyb-ensure-user\nWORKDIR /workspace\nCMD [\"sleep\", \"infinity\"]\n",
4
- "ensure-user.sh": "#!/bin/sh\n# Creates the dimension user for HYB_UID/HYB_GID (the host directory owner) with passwordless sudo.\n# Safe to re-run: every step checks before it acts, and concurrent runs are serialized by the lock.\nset -eu\n\nexec 9>/run/hyb-ensure-user.lock\nflock 9\n\nuid=\"$HYB_UID\"\ngid=\"$HYB_GID\"\n[ \"$uid\" = 0 ] && exit 0\n\ngroup=\"$(getent group \"$gid\" | cut -d: -f1 || true)\"\nif [ -z \"$group\" ]; then\n groupadd -g \"$gid\" dimension\n group=dimension\nfi\n\nif ! getent passwd \"$uid\" >/dev/null; then\n useradd -m -u \"$uid\" -g \"$group\" -d /home/dimension -s /bin/bash dimension\nfi\n\necho \"#$uid ALL=(ALL) NOPASSWD: ALL\" > /etc/sudoers.d/dimension\nchmod 0440 /etc/sudoers.d/dimension\n",
4
+ "ensure-user.sh": "#!/bin/sh\nset -eu\n\nexec 9>/run/hyb-ensure-user.lock\nflock 9\n\nuid=\"$HYB_UID\"\ngid=\"$HYB_GID\"\n[ \"$uid\" = 0 ] && exit 0\n\ngroup=\"$(getent group \"$gid\" | cut -d: -f1 || true)\"\nif [ -z \"$group\" ]; then\n groupadd -g \"$gid\" dimension\n group=dimension\nfi\n\nif ! getent passwd \"$uid\" >/dev/null; then\n useradd -m -u \"$uid\" -g \"$group\" -d /home/dimension -s /bin/bash dimension\nfi\n\necho \"#$uid ALL=(ALL) NOPASSWD: ALL\" > /etc/sudoers.d/dimension\nchmod 0440 /etc/sudoers.d/dimension\n",
5
5
  };