hyperbrane 0.1.0 → 0.1.2

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 (150) hide show
  1. package/README.md +41 -3
  2. package/dist/agent/agentService.d.ts +11 -3
  3. package/dist/agent/agentService.js +63 -19
  4. package/dist/agent/agentStorage.d.ts +2 -0
  5. package/dist/agent/agentStorage.js +4 -2
  6. package/dist/agent/api.d.ts +83 -32
  7. package/dist/agent/api.js +7 -0
  8. package/dist/agent/client.d.ts +3 -0
  9. package/dist/agent/client.js +5 -0
  10. package/dist/agent/hostTranscript.d.ts +13 -0
  11. package/dist/agent/hostTranscript.js +190 -0
  12. package/dist/agent/session/backend.d.ts +82 -0
  13. package/dist/agent/session/backend.js +1 -0
  14. package/dist/agent/session/claude/home.d.ts +2 -0
  15. package/dist/agent/session/claude/home.js +4 -0
  16. package/dist/agent/session/claude/transcript.d.ts +13 -0
  17. package/dist/agent/session/claude/transcript.js +29 -0
  18. package/dist/agent/session/client.d.ts +5 -1
  19. package/dist/agent/session/codex/home.d.ts +2 -0
  20. package/dist/agent/session/codex/home.js +4 -0
  21. package/dist/agent/session/codex/messages.d.ts +30 -0
  22. package/dist/agent/session/codex/messages.js +135 -0
  23. package/dist/agent/session/codex/transcript.d.ts +12 -0
  24. package/dist/agent/session/codex/transcript.js +22 -0
  25. package/dist/agent/session/fileTranscript.d.ts +92 -0
  26. package/dist/agent/session/fileTranscript.js +454 -0
  27. package/dist/agent/session/sdkMessages.d.ts +15 -0
  28. package/dist/agent/session/sdkMessages.js +28 -0
  29. package/dist/agent/session/sessionStorage.d.ts +4 -0
  30. package/dist/agent/session/sessionStorage.js +13 -2
  31. package/dist/agent/session/sharedConnection.d.ts +1 -0
  32. package/dist/agent/session/storedTranscripts.d.ts +13 -0
  33. package/dist/agent/session/storedTranscripts.js +8 -0
  34. package/dist/agent/session/worker/protocol.d.ts +5 -1
  35. package/dist/agent/session/worker/protocol.js +6 -1
  36. package/dist/agent/session/worker/workerParams.d.ts +11 -0
  37. package/dist/agent/session/worker/workerParams.js +163 -0
  38. package/dist/agent/session/workerSseConnection.d.ts +20 -4
  39. package/dist/agent/session/workerSseConnection.js +57 -7
  40. package/dist/agent/vendor-types/codex/app-server/AbsolutePathBuf.d.ts +10 -0
  41. package/dist/agent/vendor-types/codex/app-server/AbsolutePathBuf.js +2 -0
  42. package/dist/agent/vendor-types/codex/app-server/FunctionCallOutputBody.d.ts +2 -0
  43. package/dist/agent/vendor-types/codex/app-server/FunctionCallOutputBody.js +2 -0
  44. package/dist/agent/vendor-types/codex/app-server/FunctionCallOutputContentItem.d.ts +22 -0
  45. package/dist/agent/vendor-types/codex/app-server/FunctionCallOutputContentItem.js +2 -0
  46. package/dist/agent/vendor-types/codex/app-server/ImageDetail.d.ts +1 -0
  47. package/dist/agent/vendor-types/codex/app-server/ImageDetail.js +2 -0
  48. package/dist/agent/vendor-types/codex/app-server/ImageGenerationFailure.d.ts +5 -0
  49. package/dist/agent/vendor-types/codex/app-server/ImageGenerationFailure.js +2 -0
  50. package/dist/agent/vendor-types/codex/app-server/ImageGenerationItem.d.ts +11 -0
  51. package/dist/agent/vendor-types/codex/app-server/ImageGenerationItem.js +2 -0
  52. package/dist/agent/vendor-types/codex/app-server/LegacyAppPathString.d.ts +23 -0
  53. package/dist/agent/vendor-types/codex/app-server/LegacyAppPathString.js +2 -0
  54. package/dist/agent/vendor-types/codex/app-server/MessagePhase.d.ts +7 -0
  55. package/dist/agent/vendor-types/codex/app-server/MessagePhase.js +2 -0
  56. package/dist/agent/vendor-types/codex/app-server/ReasoningEffort.d.ts +4 -0
  57. package/dist/agent/vendor-types/codex/app-server/ReasoningEffort.js +2 -0
  58. package/dist/agent/vendor-types/codex/app-server/SleepItem.d.ts +7 -0
  59. package/dist/agent/vendor-types/codex/app-server/SleepItem.js +2 -0
  60. package/dist/agent/vendor-types/codex/app-server/WebSearchItem.d.ts +14 -0
  61. package/dist/agent/vendor-types/codex/app-server/WebSearchItem.js +2 -0
  62. package/dist/agent/vendor-types/codex/app-server/serde_json/JsonValue.d.ts +3 -0
  63. package/dist/agent/vendor-types/codex/app-server/serde_json/JsonValue.js +2 -0
  64. package/dist/agent/vendor-types/codex/app-server/v2/AgentMessageDelivery.d.ts +1 -0
  65. package/dist/agent/vendor-types/codex/app-server/v2/AgentMessageDelivery.js +2 -0
  66. package/dist/agent/vendor-types/codex/app-server/v2/AsyncUserInputQuestion.d.ts +4 -0
  67. package/dist/agent/vendor-types/codex/app-server/v2/AsyncUserInputQuestion.js +2 -0
  68. package/dist/agent/vendor-types/codex/app-server/v2/ByteRange.d.ts +4 -0
  69. package/dist/agent/vendor-types/codex/app-server/v2/ByteRange.js +2 -0
  70. package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentState.d.ts +5 -0
  71. package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentState.js +2 -0
  72. package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentStatus.d.ts +1 -0
  73. package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentStatus.js +2 -0
  74. package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentTool.d.ts +1 -0
  75. package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentTool.js +2 -0
  76. package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentToolCallStatus.d.ts +1 -0
  77. package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentToolCallStatus.js +2 -0
  78. package/dist/agent/vendor-types/codex/app-server/v2/CommandAction.d.ts +19 -0
  79. package/dist/agent/vendor-types/codex/app-server/v2/CommandAction.js +2 -0
  80. package/dist/agent/vendor-types/codex/app-server/v2/CommandExecutionSource.d.ts +1 -0
  81. package/dist/agent/vendor-types/codex/app-server/v2/CommandExecutionSource.js +2 -0
  82. package/dist/agent/vendor-types/codex/app-server/v2/CommandExecutionStatus.d.ts +1 -0
  83. package/dist/agent/vendor-types/codex/app-server/v2/CommandExecutionStatus.js +2 -0
  84. package/dist/agent/vendor-types/codex/app-server/v2/DynamicToolCallOutputContentItem.d.ts +10 -0
  85. package/dist/agent/vendor-types/codex/app-server/v2/DynamicToolCallOutputContentItem.js +2 -0
  86. package/dist/agent/vendor-types/codex/app-server/v2/DynamicToolCallStatus.d.ts +1 -0
  87. package/dist/agent/vendor-types/codex/app-server/v2/DynamicToolCallStatus.js +2 -0
  88. package/dist/agent/vendor-types/codex/app-server/v2/FileUpdateChange.d.ts +6 -0
  89. package/dist/agent/vendor-types/codex/app-server/v2/FileUpdateChange.js +2 -0
  90. package/dist/agent/vendor-types/codex/app-server/v2/HookPromptFragment.d.ts +4 -0
  91. package/dist/agent/vendor-types/codex/app-server/v2/HookPromptFragment.js +2 -0
  92. package/dist/agent/vendor-types/codex/app-server/v2/McpAppDisplayMode.d.ts +1 -0
  93. package/dist/agent/vendor-types/codex/app-server/v2/McpAppDisplayMode.js +2 -0
  94. package/dist/agent/vendor-types/codex/app-server/v2/McpAppUi.d.ts +8 -0
  95. package/dist/agent/vendor-types/codex/app-server/v2/McpAppUi.js +2 -0
  96. package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallAppContext.d.ts +7 -0
  97. package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallAppContext.js +2 -0
  98. package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallError.d.ts +3 -0
  99. package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallError.js +2 -0
  100. package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallResult.d.ts +6 -0
  101. package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallResult.js +2 -0
  102. package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallStatus.d.ts +1 -0
  103. package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallStatus.js +2 -0
  104. package/dist/agent/vendor-types/codex/app-server/v2/MemoryCitation.d.ts +5 -0
  105. package/dist/agent/vendor-types/codex/app-server/v2/MemoryCitation.js +2 -0
  106. package/dist/agent/vendor-types/codex/app-server/v2/MemoryCitationEntry.d.ts +6 -0
  107. package/dist/agent/vendor-types/codex/app-server/v2/MemoryCitationEntry.js +2 -0
  108. package/dist/agent/vendor-types/codex/app-server/v2/PatchApplyStatus.d.ts +1 -0
  109. package/dist/agent/vendor-types/codex/app-server/v2/PatchApplyStatus.js +2 -0
  110. package/dist/agent/vendor-types/codex/app-server/v2/PatchChangeKind.d.ts +8 -0
  111. package/dist/agent/vendor-types/codex/app-server/v2/PatchChangeKind.js +2 -0
  112. package/dist/agent/vendor-types/codex/app-server/v2/SubAgentActivityKind.d.ts +1 -0
  113. package/dist/agent/vendor-types/codex/app-server/v2/SubAgentActivityKind.js +2 -0
  114. package/dist/agent/vendor-types/codex/app-server/v2/TextElement.d.ts +11 -0
  115. package/dist/agent/vendor-types/codex/app-server/v2/TextElement.js +2 -0
  116. package/dist/agent/vendor-types/codex/app-server/v2/ThreadItem.d.ts +215 -0
  117. package/dist/agent/vendor-types/codex/app-server/v2/ThreadItem.js +2 -0
  118. package/dist/agent/vendor-types/codex/app-server/v2/UserInput.d.ts +35 -0
  119. package/dist/agent/vendor-types/codex/app-server/v2/UserInput.js +2 -0
  120. package/dist/agent/vendor-types/codex/app-server/v2/WebSearchAction.d.ts +14 -0
  121. package/dist/agent/vendor-types/codex/app-server/v2/WebSearchAction.js +2 -0
  122. package/dist/agent/workerBundle.generated.js +1 -1
  123. package/dist/cli/agent.js +30 -4
  124. package/dist/cli/services.d.ts +6 -5
  125. package/dist/cli/services.js +4 -3
  126. package/dist/dimension/api.d.ts +5 -4
  127. package/dist/dimension/paths.d.ts +4 -0
  128. package/dist/dimension/paths.js +10 -2
  129. package/dist/dimension/service.d.ts +5 -1
  130. package/dist/dimension/service.js +5 -1
  131. package/dist/dimension/storage.js +3 -2
  132. package/dist/index.d.ts +2 -3
  133. package/dist/index.js +7 -5
  134. package/dist/lib/docker.d.ts +1 -0
  135. package/dist/portal-gateway/api.d.ts +6 -5
  136. package/dist/portal-gateway/gatewayService.d.ts +5 -1
  137. package/dist/portal-gateway/gatewayService.js +25 -13
  138. package/dist/portal-gateway/image.d.ts +1 -1
  139. package/dist/portal-gateway/image.js +1 -1
  140. package/dist/portal-gateway/imageFiles.generated.js +1 -1
  141. package/dist/portal-gateway/portalService.d.ts +2 -0
  142. package/dist/portal-gateway/portalService.js +61 -29
  143. package/dist/portal-gateway/upstream.d.ts +22 -11
  144. package/dist/sandbox/api.d.ts +60 -23
  145. package/dist/sandbox/boatService.js +11 -3
  146. package/dist/sandbox/containerService.js +10 -3
  147. package/dist/sandbox/endpointProxy.js +16 -1
  148. package/dist/sandbox/localService.js +20 -2
  149. package/dist/sandbox/sshService.js +37 -4
  150. package/package.json +25 -8
@@ -180,17 +180,25 @@ export function createPortalService(storage, gateways, docker, image, host) {
180
180
  return;
181
181
  if (record.state !== "destroying")
182
182
  await storage.savePortal({ ...record, state: "destroying" });
183
+ // A restore under way finishes first (seeing the mark, it undoes what it wired), so nothing it
184
+ // does lands after this returns; one that starts from now on sees the mark and does nothing.
185
+ while (restoring.has(portalName))
186
+ await restoring.get(portalName).catch(() => { });
183
187
  if (record.subnet !== undefined) {
184
188
  await ignoringStopped(() => unwire(record.gatewayName, portalLink(portalName, record.subnet)));
185
189
  await storage.releaseSubnet(record.subnet);
186
190
  }
187
- const sameGateway = (await storage.listPortals()).some((other) => other.name !== portalName &&
191
+ await untrustUnlessShared(record);
192
+ await storage.removePortal(portalName);
193
+ }
194
+ /** Takes the gateway's CA out of the dimension, unless another of its portals goes to that gateway. */
195
+ async function untrustUnlessShared(record) {
196
+ const sameGateway = (await storage.listPortals()).some((other) => other.name !== record.name &&
188
197
  other.dimensionName === record.dimensionName &&
189
198
  other.gatewayName === record.gatewayName);
190
199
  if (!sameGateway) {
191
200
  await ignoringStopped(() => trustGateway(dimensionContainerName(record.dimensionName), record.gatewayName, false));
192
201
  }
193
- await storage.removePortal(portalName);
194
202
  }
195
203
  async function unwire(gatewayName, link) {
196
204
  const result = await docker.exec(gatewayContainerName(gatewayName), {
@@ -217,38 +225,62 @@ export function createPortalService(storage, gateways, docker, image, host) {
217
225
  await Promise.all(records.map(async (record) => {
218
226
  if ((await gatewayInterfaces(record.gatewayName))?.has(portalLink(record.name, record.subnet).gatewayIf))
219
227
  return;
220
- let pids;
228
+ await restoreOnce(record);
229
+ }));
230
+ }
231
+ // One restore of a portal at a time, however many ask (a restart's own and the watcher's): two
232
+ // wiring the same link at once fail each other, and the retry could land after a destroy. Each
233
+ // waits for the one before rather than joining it: that one may have given up on a container that
234
+ // has since restarted, so once it's done the link is looked at again.
235
+ const restoring = new Map();
236
+ async function restoreOnce(record) {
237
+ while (restoring.has(record.name)) {
238
+ await restoring.get(record.name).catch(() => { });
239
+ const link = portalLink(record.name, record.subnet);
240
+ if ((await listInterfaces(gatewayContainerName(record.gatewayName)))?.has(link.gatewayIf))
241
+ return;
242
+ }
243
+ const restored = restore(record).finally(() => restoring.delete(record.name));
244
+ restoring.set(record.name, restored);
245
+ return restored;
246
+ }
247
+ async function restore(listed) {
248
+ // As it is now: one being destroyed is left alone.
249
+ const record = await storage.readPortal(listed.name);
250
+ if (record?.state !== "ready")
251
+ return;
252
+ let pids;
253
+ try {
254
+ pids = await runningPids(record);
255
+ }
256
+ catch (error) {
257
+ // A stopped or removed container gets wired when it starts again, or never.
258
+ if (error instanceof GatewayConflictError || error instanceof GatewayNotFoundError)
259
+ return;
260
+ throw error;
261
+ }
262
+ for (let attempt = 1;; attempt++) {
221
263
  try {
222
- pids = await runningPids(record);
264
+ await connect(record, pids);
265
+ // Destroyed meanwhile: neither the link nor the trust just made may outlive it (see destroyPortal).
266
+ if ((await storage.readPortal(record.name))?.state !== "ready") {
267
+ await ignoringStopped(() => unwire(record.gatewayName, portalLink(record.name, record.subnet)));
268
+ await untrustUnlessShared(record);
269
+ }
270
+ return;
223
271
  }
224
272
  catch (error) {
225
- // A stopped or removed container gets wired when it starts again, or never.
226
- if (error instanceof GatewayConflictError || error instanceof GatewayNotFoundError)
273
+ // A container that stopped or restarted meanwhile (killing what ran in it) is restored by
274
+ // its own start event. Docker kills a restarting container's processes before its state
275
+ // changes, so give it a moment before deciding it's still the same container.
276
+ await sleep(RESTORE_RECHECK_MS);
277
+ const now = await runningPids(record).catch(() => null);
278
+ if (now?.gateway !== pids.gateway || now?.dimension !== pids.dimension)
227
279
  return;
228
- throw error;
280
+ if (attempt === 2)
281
+ return console.warn(`restoring portal ${record.name} failed:`, error);
229
282
  }
230
- for (let attempt = 1;; attempt++) {
231
- try {
232
- await connect(record, pids);
233
- // Destroyed meanwhile: the link just made must not outlive it (see destroyPortal).
234
- if ((await storage.readPortal(record.name))?.state !== "ready") {
235
- await ignoringStopped(() => unwire(record.gatewayName, portalLink(record.name, record.subnet)));
236
- }
237
- return;
238
- }
239
- catch (error) {
240
- // A container that stopped or restarted meanwhile (killing what ran in it) is restored by
241
- // its own start event. Docker kills a restarting container's processes before its state
242
- // changes, so give it a moment before deciding it's still the same container.
243
- await sleep(RESTORE_RECHECK_MS);
244
- const now = await runningPids(record).catch(() => null);
245
- if (now?.gateway !== pids.gateway || now?.dimension !== pids.dimension)
246
- return;
247
- if (attempt === 2)
248
- return console.warn(`restoring portal ${record.name} failed:`, error);
249
- }
250
- }
251
- }));
283
+ }
252
284
  }
253
285
  /** Null when the container isn't running. */
254
286
  async function listInterfaces(container) {
@@ -19,8 +19,20 @@ export type UpstreamDefinition = {
19
19
  modes: {
20
20
  oauth?: OAuthMode;
21
21
  apiKey?: ApiKeyMode;
22
+ forced?: ForcedMode;
22
23
  };
23
24
  };
25
+ /** What to force onto every matching request, whatever the client sent. */
26
+ export type UpstreamForce = {
27
+ /** Retarget the request, e.g. "https://api.internal:8443". */
28
+ target?: string;
29
+ /** Prefix prepended to the request path, so dimensions needn't know where the API is mounted. */
30
+ path?: string;
31
+ /** Query params the client doesn't get to choose. */
32
+ query?: Record<string, string>;
33
+ /** Headers whose values the dimension never sees. */
34
+ headers?: Record<string, string>;
35
+ };
24
36
  type ModeBase = {
25
37
  /**
26
38
  * Requests to these hosts get the credential. Matched on host alone: v1 also matched the
@@ -34,17 +46,16 @@ type ModeBase = {
34
46
  } | {
35
47
  query: string;
36
48
  };
37
- /** What to force onto every matching request, whatever the client sent. */
38
- force?: {
39
- /** Retarget the request, e.g. "https://api.internal:8443". */
40
- target?: string;
41
- /** Prefix prepended to the request path, so dimensions needn't know where the API is mounted. */
42
- path?: string;
43
- /** Query params the client doesn't get to choose. */
44
- query?: Record<string, string>;
45
- /** Headers the client never sees the values of. */
46
- headers?: Record<string, string>;
47
- };
49
+ force?: UpstreamForce;
50
+ };
51
+ /**
52
+ * No credential of the gateway's own, so nothing to present and no fakes: only what `force` writes
53
+ * onto every request to `hosts`, e.g. a service's own auth header, set here and never seen by the
54
+ * dimension. Applies as soon as it's registered.
55
+ */
56
+ export type ForcedMode = {
57
+ hosts: string[];
58
+ force: UpstreamForce;
48
59
  };
49
60
  export type OAuthMode = ModeBase & {
50
61
  /**
@@ -5,14 +5,16 @@ export type RunCommandOptions = {
5
5
  stdin?: string | Uint8Array;
6
6
  };
7
7
  export type RunCommandResult = {
8
- exitCode: number;
9
- stdout: string;
10
- stderr: string;
8
+ readonly exitCode: number;
9
+ readonly stdout: string;
10
+ readonly stderr: string;
11
11
  };
12
12
  export type SandboxFileInfo = {
13
- type: "file" | "directory" | "other";
14
- uid: number;
15
- gid: number;
13
+ readonly type: "file" | "directory" | "other";
14
+ readonly uid: number;
15
+ readonly gid: number;
16
+ /** In bytes; what the system reports for anything that isn't a file. */
17
+ readonly size: number;
16
18
  };
17
19
  export type SandboxFetchInit = {
18
20
  method?: string;
@@ -27,7 +29,11 @@ export type SandboxFetchInit = {
27
29
  */
28
30
  timeout?: number | false;
29
31
  };
30
- /** A stream connection to a unix socket inside a sandbox (see Sandbox.connect). */
32
+ /**
33
+ * A stream connection to a unix socket inside a sandbox (see Sandbox.connect).
34
+ *
35
+ * @sealed
36
+ */
31
37
  export type SandboxConnection = {
32
38
  write(data: Uint8Array): void;
33
39
  /** Receives every chunk read from the socket, in order. */
@@ -49,14 +55,14 @@ export type SandboxEndpointTarget = {
49
55
  export type SandboxEndpointCredentials =
50
56
  /** `Authorization: Bearer <token>`. */
51
57
  {
52
- scheme: "bearer";
53
- token: string;
58
+ readonly scheme: "bearer";
59
+ readonly token: string;
54
60
  }
55
61
  /** `Authorization: Basic ...`; browsers prompt for it on their own. */
56
62
  | {
57
- scheme: "basic";
58
- username: string;
59
- password: string;
63
+ readonly scheme: "basic";
64
+ readonly username: string;
65
+ readonly password: string;
60
66
  }
61
67
  /**
62
68
  * Logged in with exe.dev, as a user the VM is shared with; exe.dev's proxy checks it (exeDevAuth.ts).
@@ -64,8 +70,8 @@ export type SandboxEndpointCredentials =
64
70
  * first (exeDevLoginUrl in agent/session/workerSseConnection.ts).
65
71
  */
66
72
  | {
67
- scheme: "exe.dev";
68
- loginUrl: string;
73
+ readonly scheme: "exe.dev";
74
+ readonly loginUrl: string;
69
75
  };
70
76
  export type SandboxEndpointScheme = SandboxEndpointCredentials["scheme"];
71
77
  export type SandboxEndpointRequest = {
@@ -88,24 +94,33 @@ export type SandboxEndpointRequest = {
88
94
  allowedOrigins?: string[];
89
95
  };
90
96
  export type SandboxEndpoint = {
91
- id: string;
97
+ readonly id: string;
92
98
  /**
93
99
  * Where the client reaches the endpoint, directly over the network (not through the backend's own
94
- * connection), at the host the sandbox was created with.
100
+ * connection), at the host the sandbox was created with. It lasts until the endpoint is closed,
101
+ * but what serves it may stop (a reboot, an archived Boat sandbox): ensureEndpoint starts it again.
102
+ * Until then a browser reports a request to it as a CORS error, since nothing of the endpoint's answers.
95
103
  */
96
- url: string;
104
+ readonly url: string;
97
105
  /** What the client presents; requests without it are refused before reaching the target. */
98
- credentials: SandboxEndpointCredentials;
99
- /** As requested: fixed for the endpoint's life. */
100
- allowedOrigins: string[];
106
+ readonly credentials: SandboxEndpointCredentials;
107
+ /** As requested, or as updateEndpoint changed them since. */
108
+ readonly allowedOrigins: string[];
101
109
  };
102
110
  /**
103
111
  * Paths are sandbox paths: relative or absolute, both resolved against the sandbox root.
104
112
  * Operations that report a missing path throw SandboxNotFoundError, whatever the backend.
113
+ *
114
+ * @sealed
105
115
  */
106
116
  export interface SandboxFileOperations {
107
117
  /** Throws SandboxNotFoundError if missing. */
108
118
  readFile(path: string): Promise<Uint8Array>;
119
+ /**
120
+ * Bytes `start` up to `end` (exclusive) of the file, fewer where the file ends sooner, so a large
121
+ * file can be read a part at a time. Throws SandboxNotFoundError if missing.
122
+ */
123
+ readFileRange(path: string, start: number, end: number): Promise<Uint8Array>;
109
124
  /** Atomically replaces the file (readers never see partial content). Creates missing parent directories. */
110
125
  writeFile(path: string, data: string | Uint8Array): Promise<void>;
111
126
  /**
@@ -126,10 +141,15 @@ export interface SandboxFileOperations {
126
141
  /** Sandbox path of the home directory of the user operations run as. */
127
142
  homeDirectory(): Promise<string>;
128
143
  }
144
+ /** @sealed */
129
145
  export interface SandboxCommands {
130
146
  runCommand(cmd: string[], options?: RunCommandOptions): Promise<RunCommandResult>;
131
147
  }
132
- /** Reaching into the sandbox from the caller's side. */
148
+ /**
149
+ * Reaching into the sandbox from the caller's side.
150
+ *
151
+ * @sealed
152
+ */
133
153
  export interface SandboxNetwork {
134
154
  /** HTTP request made from inside the sandbox. */
135
155
  fetch(url: string, init?: SandboxFetchInit): Promise<Response>;
@@ -142,6 +162,8 @@ export interface SandboxNetwork {
142
162
  * clients reach it at), by the backends that can. Always authenticated: a scheme the sandbox's auth
143
163
  * can't enforce is refused with SandboxEndpointError. An endpoint lasts until it is closed, past the
144
164
  * sandbox object that exposed it.
165
+ *
166
+ * @sealed
145
167
  */
146
168
  export interface SandboxAccessibleEndpoint {
147
169
  /**
@@ -157,8 +179,19 @@ export interface SandboxAccessibleEndpoint {
157
179
  * and credentials; false if it was closed, or its port has since been taken (expose another).
158
180
  */
159
181
  ensureEndpoint(endpointId: string): Promise<boolean>;
182
+ /**
183
+ * Changes which web origins may call the endpoint, in place: its URL and credentials stay, and so
184
+ * do connections already open. False if it was closed.
185
+ */
186
+ updateEndpoint(endpointId: string, update: {
187
+ allowedOrigins: string[];
188
+ }): Promise<boolean>;
160
189
  }
161
- /** A sandbox that stops itself on a timer (Boat). */
190
+ /**
191
+ * A sandbox that stops itself on a timer (Boat).
192
+ *
193
+ * @sealed
194
+ */
162
195
  export interface SandboxExpiration {
163
196
  /** When it stops itself; null if it never does. */
164
197
  getExpiration(): Promise<Date | null>;
@@ -167,7 +200,11 @@ export interface SandboxExpiration {
167
200
  }
168
201
  /** Whether the sandbox has the SandboxExpiration capability. */
169
202
  export declare function hasExpiration<T extends object>(sandbox: T): sandbox is T & SandboxExpiration;
170
- /** What every backend provides. */
203
+ /**
204
+ * What every backend provides.
205
+ *
206
+ * @sealed
207
+ */
171
208
  export type Sandbox = SandboxFileOperations & SandboxCommands & SandboxNetwork & {
172
209
  /** Releases what the sandbox holds open (a remote connection). Optional: most hold nothing. */
173
210
  close?(): void;
@@ -103,6 +103,13 @@ export function createBoatSandbox(api, sandboxId, { proxyPort = DEFAULT_PROXY_PO
103
103
  throw error;
104
104
  }
105
105
  }
106
+ // Boat's file call reads whole files only.
107
+ async function readFileRange(path, start, end) {
108
+ const target = full(path);
109
+ const result = await sh(`[ -e "$1" ] || exit ${NOT_FOUND}; tail -c +$(($2 + 1)) -- "$1" | head -c "$3" | base64 -w0`, target, String(start), String(Math.max(0, end - start)));
110
+ check(result, "read", target);
111
+ return new Uint8Array(Buffer.from(result.stdout, "base64"));
112
+ }
106
113
  /**
107
114
  * Uploads `data`, copies it beside `target` (creating its directory) and runs `publish` on the copy
108
115
  * ($3) and the target ($2), so readers never see part of it.
@@ -140,11 +147,11 @@ export function createBoatSandbox(api, sandboxId, { proxyPort = DEFAULT_PROXY_PO
140
147
  return result.stdout.split("\n").filter(Boolean);
141
148
  }
142
149
  async function stat(path) {
143
- const result = await sh(`[ -e "$1" ] || exit ${NOT_FOUND}; stat -c '%u:%g:%F' -- "$1"`, full(path));
150
+ const result = await sh(`[ -e "$1" ] || exit ${NOT_FOUND}; stat -c '%u:%g:%s:%F' -- "$1"`, full(path));
144
151
  check(result, "stat", full(path));
145
- const [uid = "", gid = "", kind = ""] = result.stdout.trim().split(":");
152
+ const [uid = "", gid = "", size = "", kind = ""] = result.stdout.trim().split(":");
146
153
  const type = kind === "directory" ? "directory" : kind.startsWith("regular") ? "file" : "other";
147
- return { type, uid: Number(uid), gid: Number(gid) };
154
+ return { type, uid: Number(uid), gid: Number(gid), size: Number(size) };
148
155
  }
149
156
  let home = null;
150
157
  function homeDirectory() {
@@ -233,6 +240,7 @@ export function createBoatSandbox(api, sandboxId, { proxyPort = DEFAULT_PROXY_PO
233
240
  getExpiration,
234
241
  setExpiration,
235
242
  readFile,
243
+ readFileRange,
236
244
  writeFile,
237
245
  writeFileExclusive,
238
246
  removeFile,
@@ -38,6 +38,12 @@ export function createContainerSandbox(docker, container, { user } = {}) {
38
38
  const result = check(await sh(`[ -e "$P" ] || exit ${NOT_FOUND}; base64 -w0 -- "$P"`, { P: full }), "read", full);
39
39
  return new Uint8Array(Buffer.from(result.stdout, "base64"));
40
40
  }
41
+ async function readFileRange(path, start, end) {
42
+ const full = containerPath(path);
43
+ const script = `[ -e "$P" ] || exit ${NOT_FOUND}; tail -c +$(($S + 1)) -- "$P" | head -c "$L" | base64 -w0`;
44
+ const result = check(await sh(script, { P: full, S: String(start), L: String(Math.max(0, end - start)) }), "read", full);
45
+ return new Uint8Array(Buffer.from(result.stdout, "base64"));
46
+ }
41
47
  /**
42
48
  * Extracts `data` next to `full` under a unique hidden name, handed to the sandbox user, and runs
43
49
  * `publish` (as root, with $T the temp file and $F the target) to move it into place.
@@ -81,10 +87,10 @@ export function createContainerSandbox(docker, container, { user } = {}) {
81
87
  }
82
88
  async function stat(path) {
83
89
  const full = containerPath(path);
84
- const result = check(await sh(`[ -e "$P" ] || exit ${NOT_FOUND}; stat -c '%u:%g:%F' -- "$P"`, { P: full }), "stat", full);
85
- const [uid = "", gid = "", kind = ""] = result.stdout.trim().split(":");
90
+ const result = check(await sh(`[ -e "$P" ] || exit ${NOT_FOUND}; stat -c '%u:%g:%s:%F' -- "$P"`, { P: full }), "stat", full);
91
+ const [uid = "", gid = "", size = "", kind = ""] = result.stdout.trim().split(":");
86
92
  const type = kind === "directory" ? "directory" : kind.startsWith("regular") ? "file" : "other";
87
- return { type, uid: Number(uid), gid: Number(gid) };
93
+ return { type, uid: Number(uid), gid: Number(gid), size: Number(size) };
88
94
  }
89
95
  // docker exec doesn't always set HOME for a numeric user, so ask the user database.
90
96
  async function homeDirectory() {
@@ -136,6 +142,7 @@ export function createContainerSandbox(docker, container, { user } = {}) {
136
142
  }
137
143
  return {
138
144
  readFile,
145
+ readFileRange,
139
146
  writeFile,
140
147
  writeFileExclusive,
141
148
  removeFile,
@@ -185,6 +185,21 @@ export function createProxyEndpoints(commands, options, where) {
185
185
  if (result.exitCode === 10 && firewall)
186
186
  await ufw("delete", "allow", `${port}/tcp`);
187
187
  }
188
+ // The proxy reads the record on every request, so the change applies to the next one.
189
+ async function updateEndpoint(endpointId, { allowedOrigins }) {
190
+ const { port, name } = parseEndpointId(endpointId);
191
+ const read = await sh(READ_SCRIPT, { P: port, N: name });
192
+ if (read.exitCode !== 0)
193
+ return false;
194
+ const record = { ...JSON.parse(read.stdout), origins: allowedOrigins };
195
+ const written = await sh(WRITE_SCRIPT, { P: port, N: name }, JSON.stringify(record));
196
+ if (written.exitCode === 44)
197
+ return false; // its proxy's directory went with its last endpoint
198
+ if (written.exitCode !== 0) {
199
+ throw new Error(`updating endpoint ${endpointId} on ${where} failed (exit ${written.exitCode}): ${written.stderr.trim()}`);
200
+ }
201
+ return true;
202
+ }
188
203
  // Its record says what to serve; the proxy is started again on the port the endpoint's URL names.
189
204
  async function ensureEndpoint(endpointId) {
190
205
  const { port, name } = parseEndpointId(endpointId);
@@ -202,7 +217,7 @@ export function createProxyEndpoints(commands, options, where) {
202
217
  await closeEndpoint(endpointId);
203
218
  return false;
204
219
  }
205
- return { accessibleHost: options.accessibleHost, exposeEndpoint, exposeEndpointAs, closeEndpoint, ensureEndpoint };
220
+ return { accessibleHost: options.accessibleHost, exposeEndpoint, exposeEndpointAs, closeEndpoint, ensureEndpoint, updateEndpoint };
206
221
  }
207
222
  /** The credentials a client presents for a recorded check; `none` when the endpoint checks nothing itself. */
208
223
  function credentialsOf(check, none) {
@@ -1,5 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
- import { link, mkdir, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
2
+ import { link, mkdir, open, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
3
3
  import { request as httpRequest } from "node:http";
4
4
  import { createConnection } from "node:net";
5
5
  import { homedir } from "node:os";
@@ -36,6 +36,23 @@ export function createLocalSandbox(root, { endpoints } = {}) {
36
36
  async function readFileLocal(path) {
37
37
  return new Uint8Array(await notFoundAs(path, readFile(hostPath(path))));
38
38
  }
39
+ async function readFileRangeLocal(path, start, end) {
40
+ const file = await notFoundAs(path, open(hostPath(path), "r"));
41
+ try {
42
+ const buffer = new Uint8Array(Math.max(0, end - start));
43
+ let filled = 0;
44
+ while (filled < buffer.length) {
45
+ const { bytesRead } = await file.read(buffer, filled, buffer.length - filled, start + filled);
46
+ if (bytesRead === 0)
47
+ break;
48
+ filled += bytesRead;
49
+ }
50
+ return buffer.subarray(0, filled);
51
+ }
52
+ finally {
53
+ await file.close();
54
+ }
55
+ }
39
56
  async function writeFileLocal(path, data) {
40
57
  const full = hostPath(path);
41
58
  await rename(await writeTemp(full, data), full);
@@ -72,7 +89,7 @@ export function createLocalSandbox(root, { endpoints } = {}) {
72
89
  async function statLocal(path) {
73
90
  const stats = await notFoundAs(path, stat(hostPath(path)));
74
91
  const type = stats.isFile() ? "file" : stats.isDirectory() ? "directory" : "other";
75
- return { type, uid: stats.uid, gid: stats.gid };
92
+ return { type, uid: stats.uid, gid: stats.gid, size: stats.size };
76
93
  }
77
94
  async function homeDirectory() {
78
95
  const rel = relative(base, homedir());
@@ -112,6 +129,7 @@ export function createLocalSandbox(root, { endpoints } = {}) {
112
129
  }
113
130
  const sandbox = {
114
131
  readFile: readFileLocal,
132
+ readFileRange: readFileRangeLocal,
115
133
  writeFile: writeFileLocal,
116
134
  writeFileExclusive,
117
135
  removeFile,
@@ -5,6 +5,7 @@ import { createProxyEndpoints } from "./endpointProxy.js";
5
5
  import { socketConnection } from "./localService.js";
6
6
  /** SFTP status codes (draft-ietf-secsh-filexfer-02). */
7
7
  const SFTP_NO_SUCH_FILE = 2;
8
+ const SFTP_EOF = 1;
8
9
  /**
9
10
  * Commands running at once on the connection. sshd refuses sessions past its MaxSessions (10 by
10
11
  * default) per connection, and counts a closed one until it has cleaned up after it, so this stays
@@ -181,6 +182,30 @@ export function createSshSandbox(host, options) {
181
182
  const data = await sftpCall("read", full, (channel, done) => channel.readFile(full, done));
182
183
  return new Uint8Array(data);
183
184
  }
185
+ async function sftpReadFileRange(path, start, end) {
186
+ const full = remotePath(path);
187
+ const channel = await sftp();
188
+ const handle = await sftpCall("read", full, (sftpChannel, done) => sftpChannel.open(full, "r", done));
189
+ try {
190
+ const buffer = Buffer.alloc(Math.max(0, end - start));
191
+ let filled = 0;
192
+ while (filled < buffer.length) {
193
+ const read = await new Promise((resolve, reject) => channel.read(handle, buffer, filled, buffer.length - filled, start + filled, (error, bytesRead) => {
194
+ // The end of the file comes as an EOF status rather than a short read.
195
+ if (error && error.code !== SFTP_EOF)
196
+ return reject(new Error(`read ${full} on ${host} failed: ${error.message}`));
197
+ resolve(error ? 0 : bytesRead);
198
+ }));
199
+ if (read === 0)
200
+ break;
201
+ filled += read;
202
+ }
203
+ return new Uint8Array(buffer.subarray(0, filled));
204
+ }
205
+ finally {
206
+ channel.close(handle, () => { });
207
+ }
208
+ }
184
209
  /** Writes `data` to a unique hidden file next to `full` (creating its directory) and returns its path. */
185
210
  async function writeTemp(full, data) {
186
211
  await createDirectory(posix.dirname(full));
@@ -268,7 +293,7 @@ export function createSshSandbox(host, options) {
268
293
  async function sftpStat(path) {
269
294
  const info = await statOf(remotePath(path));
270
295
  const type = info.isDirectory() ? "directory" : info.isFile() ? "file" : "other";
271
- return { type, uid: info.uid, gid: info.gid };
296
+ return { type, uid: info.uid, gid: info.gid, size: info.size };
272
297
  }
273
298
  // SFTP starts in the user's home directory. Asked once: storage paths start from it on every call.
274
299
  let home = null;
@@ -301,6 +326,12 @@ export function createSshSandbox(host, options) {
301
326
  checkShell(result, "read", full);
302
327
  return new Uint8Array(result.stdout);
303
328
  },
329
+ async readFileRange(full, start, end) {
330
+ const script = `[ -e "$P" ] || exit ${NOT_FOUND}; tail -c +$(($S + 1)) -- "$P" | head -c "$L"`;
331
+ const result = await runBytes(commandLine(["sh", "-c", script], { P: full, S: String(start), L: String(Math.max(0, end - start)) }));
332
+ checkShell(result, "read", full);
333
+ return new Uint8Array(result.stdout);
334
+ },
304
335
  /** Streams `data` into a temp file beside `full`, then runs `publish` ($T the temp file, $F the target). */
305
336
  writeVia(full, data, publish) {
306
337
  const tmp = posix.join(posix.dirname(full), `.${posix.basename(full)}.${crypto.randomUUID()}.tmp`);
@@ -329,11 +360,11 @@ export function createSshSandbox(host, options) {
329
360
  return result.stdout.split("\n").filter(Boolean);
330
361
  },
331
362
  async stat(full) {
332
- const result = await sh(`[ -e "$P" ] || exit ${NOT_FOUND}; stat -c '%u:%g:%F' -- "$P"`, { P: full });
363
+ const result = await sh(`[ -e "$P" ] || exit ${NOT_FOUND}; stat -c '%u:%g:%s:%F' -- "$P"`, { P: full });
333
364
  checkShell(result, "stat", full);
334
- const [uid = "", gid = "", kind = ""] = result.stdout.trim().split(":");
365
+ const [uid = "", gid = "", size = "", kind = ""] = result.stdout.trim().split(":");
335
366
  const type = kind === "directory" ? "directory" : kind.startsWith("regular") ? "file" : "other";
336
- return { type, uid: Number(uid), gid: Number(gid) };
367
+ return { type, uid: Number(uid), gid: Number(gid), size: Number(size) };
337
368
  },
338
369
  // sshd always sets HOME for the session.
339
370
  async homeDirectory() {
@@ -343,6 +374,7 @@ export function createSshSandbox(host, options) {
343
374
  },
344
375
  };
345
376
  const readFile = async (path) => ((await sftpAvailable()) ? sftpReadFile(path) : shell.readFile(remotePath(path)));
377
+ const readFileRange = async (path, start, end) => (await sftpAvailable()) ? sftpReadFileRange(path, start, end) : shell.readFileRange(remotePath(path), start, end);
346
378
  const writeFile = async (path, data) => (await sftpAvailable()) ? sftpWriteFile(path, data) : shell.writeFile(remotePath(path), data);
347
379
  const writeFileExclusive = async (path, data) => (await sftpAvailable()) ? sftpWriteFileExclusive(path, data) : shell.writeFileExclusive(remotePath(path), data);
348
380
  const removeFile = async (path) => ((await sftpAvailable()) ? sftpRemoveFile(path) : shell.removeFile(remotePath(path)));
@@ -592,6 +624,7 @@ export function createSshSandbox(host, options) {
592
624
  }
593
625
  const sandbox = {
594
626
  readFile,
627
+ readFileRange,
595
628
  writeFile,
596
629
  writeFileExclusive,
597
630
  removeFile,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperbrane",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Dimensions, gateways and agents on a machine reached locally, over SSH or on Boat: SDK and CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -11,11 +11,27 @@
11
11
  "types": "./dist/index.d.ts",
12
12
  "import": "./dist/index.js"
13
13
  },
14
- "./package.json": "./package.json",
15
- "./*": {
16
- "types": "./dist/*.d.ts",
17
- "import": "./dist/*.js"
18
- }
14
+ "./sandbox/api": {
15
+ "types": "./dist/sandbox/api.d.ts",
16
+ "import": "./dist/sandbox/api.js"
17
+ },
18
+ "./dimension/api": {
19
+ "types": "./dist/dimension/api.d.ts",
20
+ "import": "./dist/dimension/api.js"
21
+ },
22
+ "./portal-gateway/api": {
23
+ "types": "./dist/portal-gateway/api.d.ts",
24
+ "import": "./dist/portal-gateway/api.js"
25
+ },
26
+ "./agent/api": {
27
+ "types": "./dist/agent/api.d.ts",
28
+ "import": "./dist/agent/api.js"
29
+ },
30
+ "./agent/client": {
31
+ "types": "./dist/agent/client.d.ts",
32
+ "import": "./dist/agent/client.js"
33
+ },
34
+ "./package.json": "./package.json"
19
35
  },
20
36
  "bin": {
21
37
  "hyperbrane": "./dist/cli/main.js"
@@ -36,9 +52,10 @@
36
52
  "embed:images": "bun run scripts/embed-image-files.ts",
37
53
  "bundle:worker": "bun run scripts/bundle-worker.ts",
38
54
  "build": "bun run build:vendor && bun run bundle:worker && bun run embed:images && rm -rf dist && tsc -p tsconfig.build.json && bun run scripts/finish-build.ts",
39
- "build:vendor": "test -d agent/vendor-types/claude || bun run scripts/pull-vendor-types.ts claude",
55
+ "build:vendor": "test -d agent/vendor-types/claude && test -d agent/vendor-types/codex || bun run scripts/pull-vendor-types.ts claude codex",
40
56
  "prepack": "bun run build",
41
- "check:package": "bun run scripts/check-package.ts"
57
+ "check:package": "bun run scripts/check-package.ts",
58
+ "api:diff": "bun run scripts/api-diff.ts"
42
59
  },
43
60
  "dependencies": {
44
61
  "@boatdev/sdk": "1.5.0",