hyperbrane 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +12 -2
  2. package/dist/agent/agentService.d.ts +1 -0
  3. package/dist/agent/agentService.js +16 -1
  4. package/dist/agent/agentStorage.d.ts +2 -0
  5. package/dist/agent/agentStorage.js +4 -2
  6. package/dist/agent/api.d.ts +51 -27
  7. package/dist/agent/client.d.ts +3 -0
  8. package/dist/agent/client.js +5 -0
  9. package/dist/agent/hostTranscript.d.ts +13 -0
  10. package/dist/agent/hostTranscript.js +190 -0
  11. package/dist/agent/session/backend.d.ts +80 -0
  12. package/dist/agent/session/backend.js +1 -0
  13. package/dist/agent/session/claude/home.d.ts +2 -0
  14. package/dist/agent/session/claude/home.js +4 -0
  15. package/dist/agent/session/claude/transcript.d.ts +13 -0
  16. package/dist/agent/session/claude/transcript.js +29 -0
  17. package/dist/agent/session/client.d.ts +5 -1
  18. package/dist/agent/session/codex/home.d.ts +2 -0
  19. package/dist/agent/session/codex/home.js +4 -0
  20. package/dist/agent/session/codex/messages.d.ts +30 -0
  21. package/dist/agent/session/codex/messages.js +135 -0
  22. package/dist/agent/session/codex/transcript.d.ts +12 -0
  23. package/dist/agent/session/codex/transcript.js +22 -0
  24. package/dist/agent/session/fileTranscript.d.ts +92 -0
  25. package/dist/agent/session/fileTranscript.js +454 -0
  26. package/dist/agent/session/sdkMessages.d.ts +15 -0
  27. package/dist/agent/session/sdkMessages.js +28 -0
  28. package/dist/agent/session/sessionStorage.d.ts +4 -0
  29. package/dist/agent/session/sessionStorage.js +13 -2
  30. package/dist/agent/session/sharedConnection.d.ts +1 -0
  31. package/dist/agent/session/storedTranscripts.d.ts +13 -0
  32. package/dist/agent/session/storedTranscripts.js +8 -0
  33. package/dist/agent/session/worker/protocol.d.ts +1 -0
  34. package/dist/agent/session/worker/workerParams.d.ts +11 -0
  35. package/dist/agent/session/worker/workerParams.js +162 -0
  36. package/dist/agent/vendor-types/codex/app-server/AbsolutePathBuf.d.ts +10 -0
  37. package/dist/agent/vendor-types/codex/app-server/AbsolutePathBuf.js +2 -0
  38. package/dist/agent/vendor-types/codex/app-server/FunctionCallOutputBody.d.ts +2 -0
  39. package/dist/agent/vendor-types/codex/app-server/FunctionCallOutputBody.js +2 -0
  40. package/dist/agent/vendor-types/codex/app-server/FunctionCallOutputContentItem.d.ts +22 -0
  41. package/dist/agent/vendor-types/codex/app-server/FunctionCallOutputContentItem.js +2 -0
  42. package/dist/agent/vendor-types/codex/app-server/ImageDetail.d.ts +1 -0
  43. package/dist/agent/vendor-types/codex/app-server/ImageDetail.js +2 -0
  44. package/dist/agent/vendor-types/codex/app-server/ImageGenerationFailure.d.ts +5 -0
  45. package/dist/agent/vendor-types/codex/app-server/ImageGenerationFailure.js +2 -0
  46. package/dist/agent/vendor-types/codex/app-server/ImageGenerationItem.d.ts +11 -0
  47. package/dist/agent/vendor-types/codex/app-server/ImageGenerationItem.js +2 -0
  48. package/dist/agent/vendor-types/codex/app-server/LegacyAppPathString.d.ts +23 -0
  49. package/dist/agent/vendor-types/codex/app-server/LegacyAppPathString.js +2 -0
  50. package/dist/agent/vendor-types/codex/app-server/MessagePhase.d.ts +7 -0
  51. package/dist/agent/vendor-types/codex/app-server/MessagePhase.js +2 -0
  52. package/dist/agent/vendor-types/codex/app-server/ReasoningEffort.d.ts +4 -0
  53. package/dist/agent/vendor-types/codex/app-server/ReasoningEffort.js +2 -0
  54. package/dist/agent/vendor-types/codex/app-server/SleepItem.d.ts +7 -0
  55. package/dist/agent/vendor-types/codex/app-server/SleepItem.js +2 -0
  56. package/dist/agent/vendor-types/codex/app-server/WebSearchItem.d.ts +14 -0
  57. package/dist/agent/vendor-types/codex/app-server/WebSearchItem.js +2 -0
  58. package/dist/agent/vendor-types/codex/app-server/serde_json/JsonValue.d.ts +3 -0
  59. package/dist/agent/vendor-types/codex/app-server/serde_json/JsonValue.js +2 -0
  60. package/dist/agent/vendor-types/codex/app-server/v2/AgentMessageDelivery.d.ts +1 -0
  61. package/dist/agent/vendor-types/codex/app-server/v2/AgentMessageDelivery.js +2 -0
  62. package/dist/agent/vendor-types/codex/app-server/v2/AsyncUserInputQuestion.d.ts +4 -0
  63. package/dist/agent/vendor-types/codex/app-server/v2/AsyncUserInputQuestion.js +2 -0
  64. package/dist/agent/vendor-types/codex/app-server/v2/ByteRange.d.ts +4 -0
  65. package/dist/agent/vendor-types/codex/app-server/v2/ByteRange.js +2 -0
  66. package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentState.d.ts +5 -0
  67. package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentState.js +2 -0
  68. package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentStatus.d.ts +1 -0
  69. package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentStatus.js +2 -0
  70. package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentTool.d.ts +1 -0
  71. package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentTool.js +2 -0
  72. package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentToolCallStatus.d.ts +1 -0
  73. package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentToolCallStatus.js +2 -0
  74. package/dist/agent/vendor-types/codex/app-server/v2/CommandAction.d.ts +19 -0
  75. package/dist/agent/vendor-types/codex/app-server/v2/CommandAction.js +2 -0
  76. package/dist/agent/vendor-types/codex/app-server/v2/CommandExecutionSource.d.ts +1 -0
  77. package/dist/agent/vendor-types/codex/app-server/v2/CommandExecutionSource.js +2 -0
  78. package/dist/agent/vendor-types/codex/app-server/v2/CommandExecutionStatus.d.ts +1 -0
  79. package/dist/agent/vendor-types/codex/app-server/v2/CommandExecutionStatus.js +2 -0
  80. package/dist/agent/vendor-types/codex/app-server/v2/DynamicToolCallOutputContentItem.d.ts +10 -0
  81. package/dist/agent/vendor-types/codex/app-server/v2/DynamicToolCallOutputContentItem.js +2 -0
  82. package/dist/agent/vendor-types/codex/app-server/v2/DynamicToolCallStatus.d.ts +1 -0
  83. package/dist/agent/vendor-types/codex/app-server/v2/DynamicToolCallStatus.js +2 -0
  84. package/dist/agent/vendor-types/codex/app-server/v2/FileUpdateChange.d.ts +6 -0
  85. package/dist/agent/vendor-types/codex/app-server/v2/FileUpdateChange.js +2 -0
  86. package/dist/agent/vendor-types/codex/app-server/v2/HookPromptFragment.d.ts +4 -0
  87. package/dist/agent/vendor-types/codex/app-server/v2/HookPromptFragment.js +2 -0
  88. package/dist/agent/vendor-types/codex/app-server/v2/McpAppDisplayMode.d.ts +1 -0
  89. package/dist/agent/vendor-types/codex/app-server/v2/McpAppDisplayMode.js +2 -0
  90. package/dist/agent/vendor-types/codex/app-server/v2/McpAppUi.d.ts +8 -0
  91. package/dist/agent/vendor-types/codex/app-server/v2/McpAppUi.js +2 -0
  92. package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallAppContext.d.ts +7 -0
  93. package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallAppContext.js +2 -0
  94. package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallError.d.ts +3 -0
  95. package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallError.js +2 -0
  96. package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallResult.d.ts +6 -0
  97. package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallResult.js +2 -0
  98. package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallStatus.d.ts +1 -0
  99. package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallStatus.js +2 -0
  100. package/dist/agent/vendor-types/codex/app-server/v2/MemoryCitation.d.ts +5 -0
  101. package/dist/agent/vendor-types/codex/app-server/v2/MemoryCitation.js +2 -0
  102. package/dist/agent/vendor-types/codex/app-server/v2/MemoryCitationEntry.d.ts +6 -0
  103. package/dist/agent/vendor-types/codex/app-server/v2/MemoryCitationEntry.js +2 -0
  104. package/dist/agent/vendor-types/codex/app-server/v2/PatchApplyStatus.d.ts +1 -0
  105. package/dist/agent/vendor-types/codex/app-server/v2/PatchApplyStatus.js +2 -0
  106. package/dist/agent/vendor-types/codex/app-server/v2/PatchChangeKind.d.ts +8 -0
  107. package/dist/agent/vendor-types/codex/app-server/v2/PatchChangeKind.js +2 -0
  108. package/dist/agent/vendor-types/codex/app-server/v2/SubAgentActivityKind.d.ts +1 -0
  109. package/dist/agent/vendor-types/codex/app-server/v2/SubAgentActivityKind.js +2 -0
  110. package/dist/agent/vendor-types/codex/app-server/v2/TextElement.d.ts +11 -0
  111. package/dist/agent/vendor-types/codex/app-server/v2/TextElement.js +2 -0
  112. package/dist/agent/vendor-types/codex/app-server/v2/ThreadItem.d.ts +215 -0
  113. package/dist/agent/vendor-types/codex/app-server/v2/ThreadItem.js +2 -0
  114. package/dist/agent/vendor-types/codex/app-server/v2/UserInput.d.ts +35 -0
  115. package/dist/agent/vendor-types/codex/app-server/v2/UserInput.js +2 -0
  116. package/dist/agent/vendor-types/codex/app-server/v2/WebSearchAction.d.ts +14 -0
  117. package/dist/agent/vendor-types/codex/app-server/v2/WebSearchAction.js +2 -0
  118. package/dist/agent/workerBundle.generated.js +1 -1
  119. package/dist/cli/agent.js +17 -4
  120. package/dist/cli/services.d.ts +6 -5
  121. package/dist/dimension/api.d.ts +5 -4
  122. package/dist/dimension/paths.d.ts +4 -0
  123. package/dist/dimension/paths.js +10 -2
  124. package/dist/dimension/service.d.ts +1 -0
  125. package/dist/dimension/storage.js +3 -2
  126. package/dist/index.d.ts +1 -3
  127. package/dist/index.js +4 -5
  128. package/dist/lib/docker.d.ts +1 -0
  129. package/dist/portal-gateway/api.d.ts +6 -5
  130. package/dist/portal-gateway/gatewayService.d.ts +1 -0
  131. package/dist/portal-gateway/portalService.d.ts +2 -0
  132. package/dist/sandbox/api.d.ts +49 -21
  133. package/dist/sandbox/boatService.js +11 -3
  134. package/dist/sandbox/containerService.js +10 -3
  135. package/dist/sandbox/localService.js +20 -2
  136. package/dist/sandbox/sshService.js +37 -4
  137. package/package.json +25 -8
package/dist/cli/agent.js CHANGED
@@ -375,7 +375,11 @@ export const sessionCommands = {
375
375
  args: ["AGENT/SESSION"],
376
376
  flags: {
377
377
  limit: { type: "string", short: "n", value: "N", description: "How many messages, about (default 50)" },
378
- before: { type: "string", value: "CURSOR", description: "The page before this cursor (printed after each page that has older ones)" },
378
+ before: {
379
+ type: "string",
380
+ value: "TIME",
381
+ description: "Messages from before this time: ISO 8601 (2026-10-09T14:00:00Z, 2026-10-09), or how long ago (90s, 30m, 2h, 3d)",
382
+ },
379
383
  },
380
384
  async run([name], flags, { agents }) {
381
385
  const limit = typeof flags.limit === "string" ? Number(flags.limit) : undefined;
@@ -383,14 +387,14 @@ export const sessionCommands = {
383
387
  throw new UsageError(`--limit takes a positive number, not ${flags.limit}`);
384
388
  const page = await agents.getTranscript(sessionRef(name), {
385
389
  limit,
386
- cursor: typeof flags.before === "string" ? flags.before : undefined,
390
+ before: typeof flags.before === "string" ? timeFlag(flags.before) : undefined,
387
391
  });
388
392
  const printer = createTerminalPrinter({ paced: false });
389
393
  for (const { type, message } of page.messages)
390
394
  printer.printTranscript(type, message.content);
391
395
  printer.close();
392
- if (page.nextCursor)
393
- process.stderr.write(`(older: hyperbrane session logs ${name} --before ${page.nextCursor})\n`);
396
+ if (page.nextBefore)
397
+ process.stderr.write(`(older: hyperbrane session logs ${name} --before ${page.nextBefore})\n`);
394
398
  },
395
399
  },
396
400
  status: {
@@ -421,3 +425,12 @@ export const sessionCommands = {
421
425
  question: questionCommands,
422
426
  },
423
427
  };
428
+ const AGO_UNITS = { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 };
429
+ /** --before's TIME as an ISO 8601 time: a date or time, or how long ago ("30m"). */
430
+ function timeFlag(value) {
431
+ const ago = /^(\d+)([smhd])$/.exec(value);
432
+ const time = ago ? Date.now() - Number(ago[1]) * AGO_UNITS[ago[2]] : Date.parse(value);
433
+ if (Number.isNaN(time))
434
+ throw new UsageError(`--before takes a time (2026-10-09T14:00:00Z) or how long ago (2h), not ${value}`);
435
+ return new Date(time).toISOString();
436
+ }
@@ -4,12 +4,13 @@ import { type DockerClient } from "../lib/docker.ts";
4
4
  import type { Sandbox } from "../sandbox/api.ts";
5
5
  import { type GatewayService } from "../portal-gateway/gatewayService.ts";
6
6
  import { type PortalService } from "../portal-gateway/portalService.ts";
7
+ /** @sealed */
7
8
  export type Services = {
8
- docker: DockerClient;
9
- dimensions: DimensionService;
10
- gateways: GatewayService;
11
- portals: PortalService;
12
- agents: AgentService;
9
+ readonly docker: DockerClient;
10
+ readonly dimensions: DimensionService;
11
+ readonly gateways: GatewayService;
12
+ readonly portals: PortalService;
13
+ readonly agents: AgentService;
13
14
  };
14
15
  /**
15
16
  * Everything runs against `host`'s docker, with state in ~/.local/share/hyperbrane of its user: this
@@ -28,14 +28,15 @@ export type DimensionLimits = {
28
28
  };
29
29
  export type DimensionStatus = "creating" | "missing" | "created" | "running" | "paused" | "restarting" | "removing" | "exited" | "dead";
30
30
  export type DimensionInfo = Dimension & {
31
- status: DimensionStatus;
31
+ readonly status: DimensionStatus;
32
32
  /** Null unless running. */
33
- uptimeSeconds: number | null;
33
+ readonly uptimeSeconds: number | null;
34
34
  /** Bytes. Null unless running. */
35
- memoryUsage: number | null;
35
+ readonly memoryUsage: number | null;
36
36
  /** Cores. Null unless running. */
37
- cpuUsage: number | null;
37
+ readonly cpuUsage: number | null;
38
38
  };
39
+ /** @sealed */
39
40
  export interface DimensionClient {
40
41
  /** POST /v1/dimensions */
41
42
  createDimension(dimension: Dimension): Promise<Dimension>;
@@ -3,3 +3,7 @@ export declare const WORKSPACE_PATH = "/workspace";
3
3
  export declare const RUN_PATH = "/run/hyperbrane";
4
4
  /** Where the dimension's state directory (see Storage.ensureStateDirectory) is mounted. */
5
5
  export declare const STATE_PATH = "/var/lib/hyperbrane";
6
+ /** On the host, under hyperbrane's data directory: a dimension's run directory, mounted at RUN_PATH. */
7
+ export declare function dimensionRunDirectory(dataDirectory: string, dimensionName: string): string;
8
+ /** On the host, under hyperbrane's data directory: a dimension's state directory, mounted at STATE_PATH. */
9
+ export declare function dimensionStateDirectory(dataDirectory: string, dimensionName: string): string;
@@ -1,7 +1,15 @@
1
- // Where things are inside every dimension. Its own module so code running in a dimension (the agent
2
- // worker) can use them without the docker client service.ts brings along.
1
+ // Where things are inside every dimension, and on the host. Its own module so code running in a
2
+ // dimension (the agent worker) can use them without the docker client service.ts brings along.
3
3
  export const WORKSPACE_PATH = "/workspace";
4
4
  /** Where the dimension's run directory (see Storage.ensureRunDirectory) is mounted. */
5
5
  export const RUN_PATH = "/run/hyperbrane";
6
6
  /** Where the dimension's state directory (see Storage.ensureStateDirectory) is mounted. */
7
7
  export const STATE_PATH = "/var/lib/hyperbrane";
8
+ /** On the host, under hyperbrane's data directory: a dimension's run directory, mounted at RUN_PATH. */
9
+ export function dimensionRunDirectory(dataDirectory, dimensionName) {
10
+ return `${dataDirectory}/dimension-run/${dimensionName}`;
11
+ }
12
+ /** On the host, under hyperbrane's data directory: a dimension's state directory, mounted at STATE_PATH. */
13
+ export function dimensionStateDirectory(dataDirectory, dimensionName) {
14
+ return `${dataDirectory}/dimension-state/${dimensionName}`;
15
+ }
@@ -5,6 +5,7 @@ import type { DimensionImage } from "./image.ts";
5
5
  import type { Storage } from "./storage.ts";
6
6
  import { RUN_PATH, STATE_PATH, WORKSPACE_PATH } from "./paths.ts";
7
7
  export { RUN_PATH, STATE_PATH, WORKSPACE_PATH };
8
+ /** @sealed */
8
9
  export interface DimensionService {
9
10
  getDimensions(): Promise<DimensionInfo[]>;
10
11
  /** The dimensions' names, from the records alone: none of getDimensions' container status and usage. */
@@ -1,6 +1,7 @@
1
1
  import { dirname } from "node:path";
2
2
  import { createJsonDirectory } from "../lib/jsonDirectory.js";
3
3
  import { SandboxNotFoundError } from "../sandbox/api.js";
4
+ import { dimensionRunDirectory, dimensionStateDirectory } from "./paths.js";
4
5
  const EXTENSION = ".json";
5
6
  /**
6
7
  * `dir` is a sandbox path; defaults to ~/.local/share/hyperbrane/dimensions of the sandbox's user.
@@ -44,7 +45,7 @@ export function createFileStorage(sandbox, dir) {
44
45
  await sandbox.removeFile(await pathFor(dimensionName));
45
46
  }
46
47
  async function runDirectory(dimensionName) {
47
- return `${dirname(await storageDir())}/dimension-run/${dimensionName}`;
48
+ return dimensionRunDirectory(dirname(await storageDir()), dimensionName);
48
49
  }
49
50
  async function ensureRunDirectory(dimensionName) {
50
51
  const path = await runDirectory(dimensionName);
@@ -55,7 +56,7 @@ export function createFileStorage(sandbox, dir) {
55
56
  await sandbox.removeDirectory(await runDirectory(dimensionName));
56
57
  }
57
58
  async function stateDirectory(dimensionName) {
58
- return `${dirname(await storageDir())}/dimension-state/${dimensionName}`;
59
+ return dimensionStateDirectory(dirname(await storageDir()), dimensionName);
59
60
  }
60
61
  async function ensureStateDirectory(dimensionName) {
61
62
  const path = await stateDirectory(dimensionName);
package/dist/index.d.ts CHANGED
@@ -10,9 +10,7 @@ export { BOAT_API_URL, BoatError, archiveBoatMachine, authorizeBoatSshKey, boatH
10
10
  export { parseAccessibleHost, proxyCredentialsAuth, type SandboxEndpointAuth, type SandboxEndpointsOptions } from "./sandbox/endpointProxy.ts";
11
11
  export { exeDevProxyAuth, type ExeDevAuthOptions } from "./sandbox/exeDevAuth.ts";
12
12
  export { EndpointLoginRequiredError, endpointRequest, exeDevLoginUrl, openSseConnection, type EndpointAccess } from "./sandbox/sseConnection.ts";
13
- export { createWorkerClient, type WorkerClient } from "./agent/session/client.ts";
14
- export { workerSseConnector, type WorkerEndpoint } from "./agent/session/workerSseConnection.ts";
15
- export { sharedConnector, type SharedConnector } from "./agent/session/sharedConnection.ts";
13
+ export * from "./agent/client.ts";
16
14
  export { createDockerClient, DockerError, type DockerClient } from "./lib/docker.ts";
17
15
  export { BUILTIN_UPSTREAMS } from "./upstreams/index.ts";
18
16
  export { HYPERBRANE_VERSION } from "./lib/version.ts";
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
- // The hyperbrane SDK's entry point: what an application drives a server with. Everything else is
2
- // reachable by its path too (`hyperbrane/sandbox/sshService`, ...).
1
+ // The hyperbrane SDK's entry point: what an application drives a server with. The package exports
2
+ // this and, for browsers (which can't take the server side along), each part's api and the worker
3
+ // client: hyperbrane/{sandbox,dimension,portal-gateway,agent}/api, hyperbrane/agent/client.
3
4
  // The APIs: types, errors and helpers of each part.
4
5
  export * from "./sandbox/api.js";
5
6
  export * from "./dimension/api.js";
@@ -17,9 +18,7 @@ export { parseAccessibleHost, proxyCredentialsAuth } from "./sandbox/endpointPro
17
18
  export { exeDevProxyAuth } from "./sandbox/exeDevAuth.js";
18
19
  export { EndpointLoginRequiredError, endpointRequest, exeDevLoginUrl, openSseConnection } from "./sandbox/sseConnection.js";
19
20
  // An agent's worker, reached through its endpoint (AgentService.exposeAgent), from a server or a browser.
20
- export { createWorkerClient } from "./agent/session/client.js";
21
- export { workerSseConnector } from "./agent/session/workerSseConnection.js";
22
- export { sharedConnector } from "./agent/session/sharedConnection.js";
21
+ export * from "./agent/client.js";
23
22
  export { createDockerClient, DockerError } from "./lib/docker.js";
24
23
  export { BUILTIN_UPSTREAMS } from "./upstreams/index.js";
25
24
  export { HYPERBRANE_VERSION } from "./lib/version.js";
@@ -34,6 +34,7 @@ export type BuildOptions = {
34
34
  context: Record<string, string>;
35
35
  labels?: Record<string, string>;
36
36
  };
37
+ /** @sealed */
37
38
  export interface DockerClient {
38
39
  imageExists(image: string): Promise<boolean>;
39
40
  buildImage(options: BuildOptions): Promise<void>;
@@ -18,10 +18,10 @@ export type Upstream = UpstreamDefinition;
18
18
  * with the key's fake, which is what a client in a dimension is given in place of the key.
19
19
  */
20
20
  export type GatewayCredential = {
21
- name: string;
22
- gatewayName: string;
23
- type: "oauth" | "apiKey";
24
- fake?: string;
21
+ readonly name: string;
22
+ readonly gatewayName: string;
23
+ readonly type: "oauth" | "apiKey";
24
+ readonly fake?: string;
25
25
  };
26
26
  /**
27
27
  * Applies to traffic arriving through the gateway's portals. "tcp://<address>:<port>" covers every
@@ -39,6 +39,7 @@ export type TrafficRule = {
39
39
  action: "BLOCK" | "DELAY" | "ASK" | "INTERCEPT";
40
40
  delayMs: number;
41
41
  };
42
+ /** @sealed */
42
43
  export interface PortalGatewayClient {
43
44
  /** POST /v1/gateways — also replaces a container made from an older gateway image, keeping its state. */
44
45
  createGateway(gateway: Gateway): Promise<Gateway>;
@@ -63,7 +64,7 @@ export interface PortalGatewayClient {
63
64
  * to give the client in the dimension (e.g. as its API key env var) in place of the key.
64
65
  */
65
66
  setApiKey(gatewayName: string, upstreamName: string, keyName: string, key: string): Promise<{
66
- fake: string;
67
+ readonly fake: string;
67
68
  }>;
68
69
  /** DELETE /v1/gateways/:gatewayName/upstreams/:upstreamName/api-keys/:keyName */
69
70
  deleteApiKey(gatewayName: string, upstreamName: string, keyName: string): Promise<void>;
@@ -4,6 +4,7 @@ import { type Sandbox } from "../sandbox/api.ts";
4
4
  import { type Gateway, type GatewayCredential, type TrafficRule, type Upstream } from "./api.ts";
5
5
  import type { GatewayStorage } from "./gatewayStorage.ts";
6
6
  import type { PortalStorage } from "./portalStorage.ts";
7
+ /** @sealed */
7
8
  export interface GatewayService {
8
9
  createGateway(gateway: Gateway): Promise<Gateway>;
9
10
  destroyGateway(gatewayName: string): Promise<void>;
@@ -4,6 +4,7 @@ import type { Sandbox } from "../sandbox/api.ts";
4
4
  import { type Portal } from "./api.ts";
5
5
  import type { GatewayStorage } from "./gatewayStorage.ts";
6
6
  import type { PortalStorage } from "./portalStorage.ts";
7
+ /** @sealed */
7
8
  export interface PortalService {
8
9
  createPortal(portal: Portal): Promise<Portal>;
9
10
  destroyPortal(portalName: string): Promise<void>;
@@ -38,6 +39,7 @@ export declare const OS_CA_BUNDLE = "/etc/ssl/certs/ca-certificates.crt";
38
39
  * `image` is the gateway image: it carries the wiring script run by the privileged helper.
39
40
  */
40
41
  export declare function createPortalService(storage: PortalStorage, gateways: GatewayStorage, docker: DockerClient, image: ManagedImage, host: Sandbox): PortalService;
42
+ /** @sealed */
41
43
  export interface PortalWatcher {
42
44
  /** Restores every portal, then keeps restoring those of each hyperbrane container that starts. */
43
45
  start(): void;
@@ -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,31 @@ 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
100
  * connection), at the host the sandbox was created with.
95
101
  */
96
- url: string;
102
+ readonly url: string;
97
103
  /** What the client presents; requests without it are refused before reaching the target. */
98
- credentials: SandboxEndpointCredentials;
104
+ readonly credentials: SandboxEndpointCredentials;
99
105
  /** As requested: fixed for the endpoint's life. */
100
- allowedOrigins: string[];
106
+ readonly allowedOrigins: string[];
101
107
  };
102
108
  /**
103
109
  * Paths are sandbox paths: relative or absolute, both resolved against the sandbox root.
104
110
  * Operations that report a missing path throw SandboxNotFoundError, whatever the backend.
111
+ *
112
+ * @sealed
105
113
  */
106
114
  export interface SandboxFileOperations {
107
115
  /** Throws SandboxNotFoundError if missing. */
108
116
  readFile(path: string): Promise<Uint8Array>;
117
+ /**
118
+ * Bytes `start` up to `end` (exclusive) of the file, fewer where the file ends sooner, so a large
119
+ * file can be read a part at a time. Throws SandboxNotFoundError if missing.
120
+ */
121
+ readFileRange(path: string, start: number, end: number): Promise<Uint8Array>;
109
122
  /** Atomically replaces the file (readers never see partial content). Creates missing parent directories. */
110
123
  writeFile(path: string, data: string | Uint8Array): Promise<void>;
111
124
  /**
@@ -126,10 +139,15 @@ export interface SandboxFileOperations {
126
139
  /** Sandbox path of the home directory of the user operations run as. */
127
140
  homeDirectory(): Promise<string>;
128
141
  }
142
+ /** @sealed */
129
143
  export interface SandboxCommands {
130
144
  runCommand(cmd: string[], options?: RunCommandOptions): Promise<RunCommandResult>;
131
145
  }
132
- /** Reaching into the sandbox from the caller's side. */
146
+ /**
147
+ * Reaching into the sandbox from the caller's side.
148
+ *
149
+ * @sealed
150
+ */
133
151
  export interface SandboxNetwork {
134
152
  /** HTTP request made from inside the sandbox. */
135
153
  fetch(url: string, init?: SandboxFetchInit): Promise<Response>;
@@ -142,6 +160,8 @@ export interface SandboxNetwork {
142
160
  * clients reach it at), by the backends that can. Always authenticated: a scheme the sandbox's auth
143
161
  * can't enforce is refused with SandboxEndpointError. An endpoint lasts until it is closed, past the
144
162
  * sandbox object that exposed it.
163
+ *
164
+ * @sealed
145
165
  */
146
166
  export interface SandboxAccessibleEndpoint {
147
167
  /**
@@ -158,7 +178,11 @@ export interface SandboxAccessibleEndpoint {
158
178
  */
159
179
  ensureEndpoint(endpointId: string): Promise<boolean>;
160
180
  }
161
- /** A sandbox that stops itself on a timer (Boat). */
181
+ /**
182
+ * A sandbox that stops itself on a timer (Boat).
183
+ *
184
+ * @sealed
185
+ */
162
186
  export interface SandboxExpiration {
163
187
  /** When it stops itself; null if it never does. */
164
188
  getExpiration(): Promise<Date | null>;
@@ -167,7 +191,11 @@ export interface SandboxExpiration {
167
191
  }
168
192
  /** Whether the sandbox has the SandboxExpiration capability. */
169
193
  export declare function hasExpiration<T extends object>(sandbox: T): sandbox is T & SandboxExpiration;
170
- /** What every backend provides. */
194
+ /**
195
+ * What every backend provides.
196
+ *
197
+ * @sealed
198
+ */
171
199
  export type Sandbox = SandboxFileOperations & SandboxCommands & SandboxNetwork & {
172
200
  /** Releases what the sandbox holds open (a remote connection). Optional: most hold nothing. */
173
201
  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,
@@ -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,