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
package/README.md CHANGED
@@ -24,18 +24,56 @@ hyperbrane dimension create --dir ~/work -m 4G --cpus 2 work
24
24
  import { createServices, createSshSandbox, MemoryLimit, CPULimit } from "hyperbrane";
25
25
 
26
26
  const host = createSshSandbox("server.example.com", { user: "me", privateKey });
27
- const { dimensions, agents } = createServices(host);
27
+ const { gateways, dimensions, portals, agents } = createServices(host);
28
+ // The gateway keeps the sign-ins, under its name: a gateway by another name starts with none.
29
+ await gateways.createGateway({ name: "main" });
28
30
  await dimensions.createDimension({ name: "work", directory: "work", memoryLimit: MemoryLimit("4G"), cpuLimit: CPULimit("2") });
31
+ // A dimension's only way out: before installing anything in it.
32
+ await portals.createPortal({ name: "work-main", dimensionName: "work", gatewayName: "main" });
29
33
  await agents.createAgent({ name: "dev", dimensionName: "work" });
34
+ await agents.install("dev", "claude");
30
35
  ```
31
36
 
37
+ Each call resolves once what it made is usable: a gateway once it intercepts, a portal once traffic
38
+ flows through it, an install once the backend runs. Re-running any of them converges.
39
+
40
+ hyperbrane runs where the application does and drives the machine from there: over SSH, or for Boat
41
+ through Boat's API, so the application needs nothing from the machine's own network or DNS. A
42
+ browser reaches an agent through `agents.exposeAgent`'s endpoint, and give it the page's origin:
43
+ origins add up, so a call without one removes nothing. The endpoint's URL lasts, but its proxy stops
44
+ with a reboot or Boat archive, and a page connecting then sees a CORS error. `exposeAgent` starts it
45
+ again, at the same URL: give `workerSseConnector` a function that calls it (through the page's server),
46
+ and it does so again whenever a connection fails to open, then throws `EndpointUnreachableError` if
47
+ that didn't help.
48
+
49
+ What lasts:
50
+
51
+ | | Container restart | Container recreated | Machine reboot, Boat resume |
52
+ |---|---|---|---|
53
+ | Workspace, sessions, sign-ins, installed backends | kept | kept | kept |
54
+ | Agent workers | started again by the next call (or `agents.restoreAgents()`) | same | same |
55
+ | Portals | re-wired by `restartDimension` and `restartGateway`; otherwise as on the right | re-wired by the next agent call, `portals.restorePortals()`, or `createPortalWatcher` (running) | same |
56
+
57
+ A Boat sandbox stops itself after a while; `hasExpiration(sandbox)` tells a sandbox that does, and
58
+ `setExpiration` moves it (`createBoatSandbox`'s own type has both).
59
+
32
60
  The root module exports the services, the sandboxes (`createLocalSandbox`, `createSshSandbox`,
33
- `createContainerSandbox`, `createBoatSandbox`), the endpoint auths and the agent client. Every module
34
- is also reachable by its path, e.g. `hyperbrane/sandbox/sshService`.
61
+ `createContainerSandbox`, `createBoatSandbox`), the endpoint auths and the agent client. For a
62
+ browser, which can't take the server side along, the package also exports each part's API
63
+ (`hyperbrane/sandbox/api`, `hyperbrane/dimension/api`, `hyperbrane/portal-gateway/api`,
64
+ `hyperbrane/agent/api`) and the agent's worker client (`hyperbrane/agent/client`). Nothing else is
65
+ importable.
66
+
67
+ What a client may rely on, as the types say: a `readonly` field is set by the package, for the client
68
+ to read; a type marked `@sealed` (the services, sandboxes, clients) is made by the package, for the
69
+ client to use, not implement. Both may gain members in any release; anything else gains only optional
70
+ ones, until a breaking release (`bun run api:diff --breaking` shows what would break).
35
71
 
36
72
  ## Building
37
73
 
38
74
  ```sh
39
75
  bun run build # dist/
40
76
  bun run check:package # packs it, installs the tarball elsewhere and uses it under Node and Bun
77
+ bun run api:diff [REF] # how the public interface changed since REF (HEAD): git diff of just that
78
+ bun run api:diff --breaking [REF] # only its breaking changes: removed, renamed, newly required, retyped
41
79
  ```
@@ -3,12 +3,18 @@ import type { Sandbox, SandboxAccessibleEndpoint } from "../sandbox/api.ts";
3
3
  import type { AgentStorage } from "./agentStorage.ts";
4
4
  import type { GatewaySignIns } from "./gatewaySignIns.ts";
5
5
  import { type AgentClient, type AgentEndpoint, type AgentProvision, type AgentSessionCapabilities } from "./api.ts";
6
+ import type { PortalService } from "../portal-gateway/portalService.ts";
7
+ /** @sealed */
6
8
  export interface AgentService extends AgentClient, AgentProvision, AgentSessionCapabilities, AgentEndpoint {
7
9
  /** Starts the workers that a restart took down (their dimension runs again); returns their agents' names. */
8
10
  restoreAgents(): Promise<string[]>;
9
11
  }
10
- /** In the dimension: where the backends are installed (a package with its node_modules). Shared by agents. */
11
- export declare const WORKER_DIR = "/opt/hyperbrane/worker";
12
+ /**
13
+ * In the dimension: where the backends are installed (a package with its node_modules). Shared by
14
+ * agents. In the state directory, so they last as long as the dimension: a container recreated (or a
15
+ * machine whose docker kept nothing) doesn't take them, and nothing is installed twice.
16
+ */
17
+ export declare const WORKER_DIR = "/var/lib/hyperbrane/worker";
12
18
  /**
13
19
  * In the dimension: the worker's script. Outside WORKER_DIR on purpose: bun caches the listing of the
14
20
  * directory a script is loaded from, and packages installed there afterwards didn't resolve.
@@ -28,4 +34,6 @@ export declare const WORKER_SCRIPT = "/opt/hyperbrane/worker.js";
28
34
  * `host` is the machine docker runs on: workers' sockets and logs are reached through it, and agents
29
35
  * are exposed through its endpoints when it declares them.
30
36
  */
31
- export declare function createAgentService(storage: AgentStorage, docker: DockerClient, host: Sandbox | (Sandbox & SandboxAccessibleEndpoint), gatewaySignIns?: GatewaySignIns): AgentService;
37
+ export declare function createAgentService(storage: AgentStorage, docker: DockerClient, host: Sandbox | (Sandbox & SandboxAccessibleEndpoint), gatewaySignIns?: GatewaySignIns,
38
+ /** Re-wires a dimension's portals: what a restart (its container's, the machine's) took. */
39
+ restoring?: Pick<PortalService, "restorePortals">): AgentService;
@@ -1,3 +1,4 @@
1
+ import { dimensionStateDirectory } from "../dimension/paths.js";
1
2
  import { RUN_PATH, STATE_PATH, containerName as dimensionContainerName } from "../dimension/service.js";
2
3
  import { createContainerSandbox } from "../sandbox/containerService.js";
3
4
  import { InvalidNameError, checkName } from "../lib/jsonDirectory.js";
@@ -6,10 +7,18 @@ import { OS_CA_BUNDLE } from "../portal-gateway/portalService.js";
6
7
  import { AgentConflictError, AgentInvalidError, AgentNotFoundError, } from "./api.js";
7
8
  import { createWorkerClient } from "./session/client.js";
8
9
  import { workerSocketConnector } from "./session/workerConnection.js";
10
+ import { readHostTranscript } from "./hostTranscript.js";
9
11
  import { workerSocketPath } from "./session/sessionStorage.js";
12
+ import { WORKER_PARAMS } from "./session/worker/workerParams.js";
10
13
  import { workerBundle } from "./workerBundle.js";
11
- /** In the dimension: where the backends are installed (a package with its node_modules). Shared by agents. */
12
- export const WORKER_DIR = "/opt/hyperbrane/worker";
14
+ /**
15
+ * In the dimension: where the backends are installed (a package with its node_modules). Shared by
16
+ * agents. In the state directory, so they last as long as the dimension: a container recreated (or a
17
+ * machine whose docker kept nothing) doesn't take them, and nothing is installed twice.
18
+ */
19
+ export const WORKER_DIR = `${STATE_PATH}/worker`;
20
+ /** Where they were before: moved to WORKER_DIR when a worker starts (ensureWorker). */
21
+ const OLD_WORKER_DIR = "/opt/hyperbrane/worker";
13
22
  /**
14
23
  * In the dimension: the worker's script. Outside WORKER_DIR on purpose: bun caches the listing of the
15
24
  * directory a script is loaded from, and packages installed there afterwards didn't resolve.
@@ -50,7 +59,9 @@ done
50
59
  * `host` is the machine docker runs on: workers' sockets and logs are reached through it, and agents
51
60
  * are exposed through its endpoints when it declares them.
52
61
  */
53
- export function createAgentService(storage, docker, host, gatewaySignIns) {
62
+ export function createAgentService(storage, docker, host, gatewaySignIns,
63
+ /** Re-wires a dimension's portals: what a restart (its container's, the machine's) took. */
64
+ restoring) {
54
65
  const endpoints = "exposeEndpoint" in host ? host : null;
55
66
  async function runningDimension(dimensionName) {
56
67
  const container = dimensionContainerName(dimensionName);
@@ -65,7 +76,7 @@ export function createAgentService(storage, docker, host, gatewaySignIns) {
65
76
  if (!runDirectory || !hasState) {
66
77
  throw new AgentConflictError(`dimension ${dimensionName} lacks the ${RUN_PATH} and ${STATE_PATH} mounts agents need (it predates them); recreate it`);
67
78
  }
68
- return { container, owner: inspected.Config.User || "0:0", runDirectory };
79
+ return { name: dimensionName, container, owner: inspected.Config.User || "0:0", runDirectory };
69
80
  }
70
81
  /** The worker as it is: for starting, stopping and checking on it. */
71
82
  function rawWorkerClient(dimension, agentName) {
@@ -94,11 +105,16 @@ export function createAgentService(storage, docker, host, gatewaySignIns) {
94
105
  function reviveWorker(agentName, dimension) {
95
106
  let started = reviving.get(agentName);
96
107
  if (!started) {
97
- started = ensureWorker(agentName, dimension)
108
+ // Whatever took the worker down (a restart, a reboot, a Boat resume) took the dimension's
109
+ // portals too: without them the worker, and the backends it installs, can't reach anything.
110
+ const restorePortals = () => restoring?.restorePortals({ dimensionName: dimension.name });
111
+ started = Promise.resolve(restorePortals())
112
+ .then(() => ensureWorker(agentName, dimension))
98
113
  .catch(async () => {
99
114
  // Unreachable even freshly started: the container's mounts may no longer be the host's
100
115
  // directories (it started while they were something else). A restart binds them again.
101
116
  await docker.restartContainer(dimension.container);
117
+ await restorePortals();
102
118
  await ensureWorker(agentName, dimension);
103
119
  })
104
120
  .finally(() => reviving.delete(agentName));
@@ -160,9 +176,13 @@ export function createAgentService(storage, docker, host, gatewaySignIns) {
160
176
  return;
161
177
  const root = createContainerSandbox(docker, container, { user: "root" });
162
178
  // The run and state directories too: the host created them, possibly as another user than the dimension's.
163
- const setup = await root.runCommand(["sh", "-c", 'mkdir -p "$W" && chown "$OWNER" "$(dirname "$W")" "$W" "$R" "$S"'], {
164
- env: { W: WORKER_DIR, R: RUN_PATH, S: STATE_PATH, OWNER: owner },
165
- });
179
+ // Backends installed where they used to be move along, once.
180
+ const setup = await root.runCommand([
181
+ "sh",
182
+ "-c",
183
+ 'mkdir -p "$W" "$(dirname "$SCRIPT")" && chown "$OWNER" "$(dirname "$SCRIPT")" "$W" "$R" "$S" && ' +
184
+ '{ [ -e "$W/node_modules" ] || [ ! -d "$OLD/node_modules" ] || { cp -a "$OLD/." "$W/" && rm -rf "$OLD"; }; }',
185
+ ], { env: { W: WORKER_DIR, SCRIPT: WORKER_SCRIPT, OLD: OLD_WORKER_DIR, R: RUN_PATH, S: STATE_PATH, OWNER: owner } });
166
186
  if (setup.exitCode !== 0)
167
187
  throw new Error(`preparing ${container} for agent ${agentName} failed: ${setup.stderr.trim()}`);
168
188
  const sandbox = createContainerSandbox(docker, container);
@@ -275,23 +295,34 @@ export function createAgentService(storage, docker, host, gatewaySignIns) {
275
295
  // Lazy: the stored endpoint is served again if its proxy stopped (a reboot), with the same URL and
276
296
  // credentials. The proxy connects to the worker's socket per stream, so a worker restart is fine.
277
297
  // Each way the host is reached (its accessible host) has an endpoint of its own: several server
278
- // records may drive the same machine.
298
+ // records may drive the same machine. Origins only add up: the endpoint is shared, and one caller's
299
+ // origins must not cut off another's pages (a narrower set closes nothing). What can run at once does.
279
300
  async function exposeAgent(agentName, scheme, allowedOrigins = []) {
280
- const agent = await requireAgent(agentName);
281
301
  if (!endpoints)
282
302
  throw new AgentInvalidError("this host doesn't declare accessible endpoints");
303
+ const key = endpoints.accessibleHost;
304
+ const [agent, stored] = await Promise.all([requireAgent(agentName), storage.readEndpoints(agentName).then((all) => all[key])]);
283
305
  const dimension = await runningDimension(agent.dimensionName);
306
+ const [healthy, served] = await Promise.all([
307
+ rawWorkerClient(dimension, agentName).call("worker/health", {}).then(() => true, () => false),
308
+ stored && stored.credentials.scheme === scheme ? endpoints.ensureEndpoint(stored.id) : false,
309
+ ]);
284
310
  // The client connects to the worker's socket through the endpoint: nothing on its way could start
285
311
  // a worker a restart took down, so it's started here.
286
- if (!(await rawWorkerClient(dimension, agentName).call("worker/health", {}).then(() => true, () => false))) {
312
+ if (!healthy)
287
313
  await reviveWorker(agentName, dimension);
314
+ const origins = [...new Set([...(stored?.allowedOrigins ?? []), ...allowedOrigins])];
315
+ if (stored && served) {
316
+ if (origins.every((origin) => stored.allowedOrigins.includes(origin)))
317
+ return stored;
318
+ // Same URL and credentials, so pages already connected stay connected.
319
+ if (await endpoints.updateEndpoint(stored.id, { allowedOrigins: origins })) {
320
+ const updated = { ...stored, allowedOrigins: origins };
321
+ await storage.saveEndpoint(agentName, key, updated);
322
+ return updated;
323
+ }
288
324
  }
289
- const key = endpoints.accessibleHost;
290
- const stored = (await storage.readEndpoints(agentName))[key];
291
- const sameOrigins = (origins) => [...(origins ?? [])].sort().join("\n") === [...allowedOrigins].sort().join("\n");
292
- if (stored && stored.credentials.scheme === scheme && sameOrigins(stored.allowedOrigins) && (await endpoints.ensureEndpoint(stored.id))) {
293
- return stored;
294
- }
325
+ // Another scheme, or it can't be served as it was given out (closed, its port taken).
295
326
  if (stored) {
296
327
  await endpoints.closeEndpoint(stored.id);
297
328
  await storage.removeEndpoint(agentName, key);
@@ -300,7 +331,7 @@ export function createAgentService(storage, docker, host, gatewaySignIns) {
300
331
  target: { unix: workerSocketPath(dimension.runDirectory, agentName) },
301
332
  scheme,
302
333
  transport: "sse",
303
- allowedOrigins,
334
+ allowedOrigins: origins,
304
335
  });
305
336
  await storage.saveEndpoint(agentName, key, endpoint);
306
337
  return endpoint;
@@ -320,6 +351,18 @@ export function createAgentService(storage, docker, host, gatewaySignIns) {
320
351
  async function sessionClient({ agentName }) {
321
352
  return clientFor(agentName);
322
353
  }
354
+ /**
355
+ * Read on the host from the dimension's state directory (hostTranscript.ts), so nothing has to run,
356
+ * neither the worker nor the dimension; only a session whose backend's home is elsewhere asks its
357
+ * worker.
358
+ */
359
+ async function getTranscript(ref, request = {}) {
360
+ const { session, limit, before } = WORKER_PARAMS["session/transcript"]({ session: ref.sessionName, ...request });
361
+ const agent = await requireAgent(ref.agentName);
362
+ const stateDirectory = dimensionStateDirectory(await storage.dataDirectory(), agent.dimensionName);
363
+ const page = await readHostTranscript(host, { stateDirectory, agentName: agent.name, sessionName: session }, { limit, before });
364
+ return page ?? (await sessionClient(ref)).call("session/transcript", { session, limit, before });
365
+ }
323
366
  // Agents whose dimension or worker is down have no sessions to show right now.
324
367
  async function listSessions() {
325
368
  const perAgent = await Promise.all((await storage.listAgents()).map(async (agent) => {
@@ -356,10 +399,11 @@ export function createAgentService(storage, docker, host, gatewaySignIns) {
356
399
  exposeAgent,
357
400
  restoreAgents,
358
401
  install: async (agentName, backend) => void (await (await clientFor(agentName)).call("backend/install", { backend })),
402
+ listBackends: async (agentName) => (await clientFor(agentName)).call("backend/list", {}),
359
403
  createSession: async (agentName, session) => (await clientFor(agentName)).call("session/create", { session }),
360
404
  updateSession: async (ref, limits) => (await sessionClient(ref)).call("session/update", { session: ref.sessionName, limits }),
361
405
  stopSession: async (ref) => void (await (await sessionClient(ref)).call("session/stop", { session: ref.sessionName })),
362
- getTranscript: async (ref, request = {}) => (await sessionClient(ref)).call("session/transcript", { session: ref.sessionName, ...request }),
406
+ getTranscript,
363
407
  cancel: async (ref) => void (await (await sessionClient(ref)).call("session/interrupt", { session: ref.sessionName })),
364
408
  sendMessage: async (ref, message) => void (await (await sessionClient(ref)).call("session/send", { session: ref.sessionName, message })),
365
409
  getSessionStatus: async (ref) => (await sessionClient(ref)).call("session/status", { session: ref.sessionName }),
@@ -9,6 +9,8 @@ export interface AgentStorage {
9
9
  /** No-op if missing. */
10
10
  removeAgent(agentName: string): Promise<void>;
11
11
  listAgents(): Promise<Agent[]>;
12
+ /** Hyperbrane's data directory these are kept in, with the dimensions' (see dimension/paths.ts). */
13
+ dataDirectory(): Promise<string>;
12
14
  /**
13
15
  * The agent's accessible endpoints (AgentService.exposeAgent), credentials included, by the
14
16
  * accessible host each was exposed for: one per way the machine is reached.
@@ -4,9 +4,10 @@ import { createJsonDirectory, defaultDataDir } from "../lib/jsonDirectory.js";
4
4
  * agents/<agent>.json, agent-endpoints/<agent>.json.
5
5
  */
6
6
  export function createAgentStorage(sandbox, dir) {
7
- const agents = createJsonDirectory(sandbox, async () => `${dir ?? (await defaultDataDir(sandbox))}/agents`);
7
+ const dataDirectory = async () => dir ?? (await defaultDataDir(sandbox));
8
+ const agents = createJsonDirectory(sandbox, async () => `${await dataDirectory()}/agents`);
8
9
  // A record used to hold one endpoint; it is read as the entry for its URL's host:port.
9
- const endpoints = createJsonDirectory(sandbox, async () => `${dir ?? (await defaultDataDir(sandbox))}/agent-endpoints`);
10
+ const endpoints = createJsonDirectory(sandbox, async () => `${await dataDirectory()}/agent-endpoints`);
10
11
  async function readEndpoints(agentName) {
11
12
  const record = await endpoints.read(agentName);
12
13
  if (!record)
@@ -44,6 +45,7 @@ export function createAgentStorage(sandbox, dir) {
44
45
  readAgent,
45
46
  removeAgent,
46
47
  listAgents,
48
+ dataDirectory,
47
49
  readEndpoints,
48
50
  saveEndpoint,
49
51
  removeEndpoint,
@@ -30,18 +30,19 @@ export type AgentSessionLimits = Partial<Pick<AgentSession, "memoryLimit" | "cpu
30
30
  */
31
31
  export type AgentSessionState = "stopped" | "starting" | "idle" | "running" | "waiting" | "error";
32
32
  export type AgentSessionInfo = AgentSession & {
33
- agentName: string;
34
- status: AgentSessionState;
33
+ readonly agentName: string;
34
+ readonly status: AgentSessionState;
35
35
  /** The backend's own id for the conversation (claude's session id, codex's thread id), once known. */
36
- nativeId: string | null;
36
+ readonly nativeId: string | null;
37
37
  /** Chosen through AgentModels; null for the backend's default. */
38
- model: string | null;
38
+ readonly model: string | null;
39
39
  };
40
40
  /** Where a session lives: sessions are named per agent. */
41
41
  export type SessionRef = {
42
42
  agentName: string;
43
43
  sessionName: string;
44
44
  };
45
+ /** @sealed */
45
46
  export interface AgentClient {
46
47
  /** PUT /v1/agents/:agentName — starts (or updates) the worker in the dimension. */
47
48
  createAgent(agent: Agent): Promise<Agent>;
@@ -83,13 +84,23 @@ export type AgentTranscriptMessage = Extract<AgentStreamingMessage, {
83
84
  export type AgentTranscriptRequest = {
84
85
  /** How many messages a page holds, about (a tool call and its result share a page); default 50, at most 500. */
85
86
  limit?: number;
86
- /** A page's `nextCursor`, for the page before it; omitted for the newest page. */
87
+ /**
88
+ * An ISO 8601 time: the page holds the newest messages from before it (a page's `nextBefore`, for
89
+ * the page before that one); omitted for the newest page.
90
+ */
91
+ before?: string;
92
+ /** @deprecated `before` by its old name: a page's `nextCursor` is its `nextBefore`. */
87
93
  cursor?: string;
88
94
  };
89
- /** Newest pages first; each page's messages oldest first. */
95
+ /**
96
+ * Newest pages first; each page's messages oldest first. `nextBefore` is the time of its oldest
97
+ * message, when there are older ones: every message of that time is on this page.
98
+ */
90
99
  export type AgentTranscriptPage = {
91
- messages: AgentTranscriptMessage[];
92
- nextCursor: string | null;
100
+ readonly messages: AgentTranscriptMessage[];
101
+ readonly nextBefore: string | null;
102
+ /** @deprecated `nextBefore` by its old name, for requests that give it as `cursor`. */
103
+ readonly nextCursor: string | null;
93
104
  };
94
105
  export type UserMessage = SDKUserMessage;
95
106
  /**
@@ -97,24 +108,24 @@ export type UserMessage = SDKUserMessage;
97
108
  * `can_use_tool` control request) isn't exported, so this mirrors it.
98
109
  */
99
110
  export type AgentPermissionRequest = {
100
- id: string;
101
- tool_name: string;
102
- input: Record<string, unknown>;
103
- tool_use_id: string;
111
+ readonly id: string;
112
+ readonly tool_name: string;
113
+ readonly input: Record<string, unknown>;
114
+ readonly tool_use_id: string;
104
115
  /** Why the agent asks, when it says (e.g. codex's reason for wanting network access). */
105
- decision_reason?: string;
116
+ readonly decision_reason?: string;
106
117
  };
107
118
  export type AgentPermissionResult = PermissionResult;
108
119
  type AskUserQuestion = AskUserQuestionInput["questions"][number];
109
120
  /** A question of claude's AskUserQuestion tool; codex's may come with any number of options. */
110
121
  export type AgentQuestion = Omit<AskUserQuestion, "options"> & {
111
- options: AskUserQuestion["options"][number][];
122
+ readonly options: AskUserQuestion["options"][number][];
112
123
  };
113
124
  /** The agent asking the user something (claude's AskUserQuestion, codex's request_user_input). */
114
125
  export type AgentQuestionRequest = {
115
- id: string;
116
- tool_use_id: string;
117
- questions: AgentQuestion[];
126
+ readonly id: string;
127
+ readonly tool_use_id: string;
128
+ readonly questions: AgentQuestion[];
118
129
  };
119
130
  /**
120
131
  * AskUserQuestion's own answer: question text -> the chosen label (labels joined with ", " for a
@@ -138,31 +149,54 @@ export type AgentBackgroundTask = SDKBackgroundTasksChangedMessage["tasks"][numb
138
149
  * the code the page shows, when `needsCode`).
139
150
  */
140
151
  export type AgentAuthStatus = {
141
- state: "signedIn";
152
+ readonly state: "signedIn";
142
153
  } | {
143
- state: "signedOut"; /** Why the last sign-in failed, if one did. */
144
- error?: string;
154
+ readonly state: "signedOut"; /** Why the last sign-in failed, if one did. */
155
+ readonly error?: string;
145
156
  } | {
146
- state: "pending";
147
- url: string;
148
- userCode?: string;
149
- needsCode: boolean;
157
+ readonly state: "pending";
158
+ readonly url: string;
159
+ readonly userCode?: string;
160
+ readonly needsCode: boolean;
161
+ };
162
+ /**
163
+ * A backend in an agent's dimension: the version installed (null: none), and whether it's ready to
164
+ * run, i.e. this release's version, complete. Kept in the dimension's state directory, so it lasts as
165
+ * long as the dimension, its container recreated or not.
166
+ */
167
+ export type AgentBackendInfo = {
168
+ readonly backend: AgentBackend;
169
+ readonly version: string | null;
170
+ readonly ready: boolean;
150
171
  };
172
+ /** @sealed */
151
173
  export interface AgentProvision {
152
- /** POST /v1/agents/:agentName/backends/:backend/install — installs the backend in the dimension, or updates it. */
174
+ /**
175
+ * POST /v1/agents/:agentName/backends/:backend/install — installs the backend in the dimension, or
176
+ * updates it, or completes it; nothing to do when it's ready. Downloads through the dimension's portals.
177
+ */
153
178
  install(agentName: string, backend: AgentBackend): Promise<void>;
179
+ /** GET /v1/agents/:agentName/backends — each backend, as installed in the agent's dimension. */
180
+ listBackends(agentName: string): Promise<AgentBackendInfo[]>;
154
181
  }
182
+ /** @sealed */
155
183
  export interface AgentEndpoint {
156
184
  /**
157
185
  * POST /v1/agents/:agentName/endpoint — the agent's worker, reachable from the client over the
158
186
  * network (workerSseConnector speaks to it), authenticated by `scheme`, callable from pages of
159
- * `allowedOrigins`. Call it before each connection: it starts the endpoint's proxy again if that
160
- * stopped, keeping the same URL and credentials; a new endpoint only for another scheme or origins,
161
- * or once the old one can't be served (closed, its port taken). Closed with the agent. Needs a host
162
- * that declares endpoints.
187
+ * `allowedOrigins`. Its URL lasts, but the proxy behind it doesn't: an archived and resumed Boat
188
+ * sandbox, or a reboot, stops it, and a page connecting then gets what a browser reports as a CORS
189
+ * error. This call (three round trips to the host when nothing changed) starts the agent's worker
190
+ * and the endpoint's proxy again if they stopped, keeping the same URL and credentials: give
191
+ * workerSseConnector a function that calls it, and it calls it again whenever a connection fails
192
+ * to open; an endpoint kept elsewhere goes stale. Origins add up: those given join the endpoint's own, in place, so
193
+ * pages already connected stay connected, and a call with fewer (or none) removes nothing. A new
194
+ * endpoint only for another scheme, or once the old one can't be served (its port taken); the old
195
+ * one's connections end then. Closed with the agent. Needs a host that declares endpoints.
163
196
  */
164
197
  exposeAgent(agentName: string, scheme: SandboxEndpointScheme, allowedOrigins?: string[]): Promise<SandboxEndpoint>;
165
198
  }
199
+ /** @sealed */
166
200
  export interface AgentStream {
167
201
  /**
168
202
  * GET /v1/agents/:agentName/sessions/:sessionName/stream?after_id= — the complete messages after
@@ -172,18 +206,21 @@ export interface AgentStream {
172
206
  */
173
207
  getStream(ref: SessionRef, req: AgentStreamingRequest): AsyncGenerator<AgentStreamingMessage>;
174
208
  }
209
+ /** @sealed */
175
210
  export interface AgentTranscript {
176
211
  /**
177
- * GET /v1/agents/:agentName/sessions/:sessionName/transcript?limit=&cursor= — a page of the
212
+ * GET /v1/agents/:agentName/sessions/:sessionName/transcript?limit=&before= (or &cursor=, its old name) — a page of the
178
213
  * conversation, read from the backend's own storage a piece at a time, so a conversation of any
179
214
  * length can be paged through.
180
215
  */
181
216
  getTranscript(ref: SessionRef, req?: AgentTranscriptRequest): Promise<AgentTranscriptPage>;
182
217
  }
218
+ /** @sealed */
183
219
  export interface AgentInterrupt {
184
220
  /** POST /v1/agents/:agentName/sessions/:sessionName/interrupt — ends the current turn; pending permission requests are denied and questions dropped. */
185
221
  cancel(ref: SessionRef): Promise<void>;
186
222
  }
223
+ /** @sealed */
187
224
  export interface AgentInput {
188
225
  /**
189
226
  * POST /v1/agents/:agentName/sessions/:sessionName/messages — resolves once the agent confirms it
@@ -192,10 +229,12 @@ export interface AgentInput {
192
229
  */
193
230
  sendMessage(ref: SessionRef, message: UserMessage): Promise<void>;
194
231
  }
232
+ /** @sealed */
195
233
  export interface AgentSessionStatus {
196
234
  /** GET /v1/agents/:agentName/sessions/:sessionName/status */
197
235
  getSessionStatus(ref: SessionRef): Promise<AgentSessionState>;
198
236
  }
237
+ /** @sealed */
199
238
  export interface AgentMessageQueue {
200
239
  /**
201
240
  * GET /v1/agents/:agentName/sessions/:sessionName/queue — from the agent (claude's command
@@ -205,24 +244,28 @@ export interface AgentMessageQueue {
205
244
  */
206
245
  getLastUserMessages(ref: SessionRef): Promise<AgentLastUserMessage[]>;
207
246
  }
247
+ /** @sealed */
208
248
  export interface AgentPermissions {
209
249
  /** GET /v1/agents/:agentName/sessions/:sessionName/permission-requests — the ones still waiting. */
210
250
  listPermissionRequests(ref: SessionRef): Promise<AgentPermissionRequest[]>;
211
251
  /** POST /v1/agents/:agentName/sessions/:sessionName/permission-requests/:requestId */
212
252
  respondPermissionRequest(ref: SessionRef, requestId: string, result: AgentPermissionResult): Promise<void>;
213
253
  }
254
+ /** @sealed */
214
255
  export interface AgentQuestions {
215
256
  /** GET /v1/agents/:agentName/sessions/:sessionName/question-requests — the ones still waiting. */
216
257
  listQuestionRequests(ref: SessionRef): Promise<AgentQuestionRequest[]>;
217
258
  /** POST /v1/agents/:agentName/sessions/:sessionName/question-requests/:requestId */
218
259
  respondQuestionRequest(ref: SessionRef, requestId: string, response: AgentQuestionResponse): Promise<void>;
219
260
  }
261
+ /** @sealed */
220
262
  export interface AgentBackgroundTasks {
221
263
  /** GET /v1/agents/:agentName/sessions/:sessionName/background-tasks — those still running; none while the CLI isn't. */
222
264
  listBackgroundTasks(ref: SessionRef): Promise<AgentBackgroundTask[]>;
223
265
  /** POST /v1/agents/:agentName/sessions/:sessionName/background-tasks/:taskId/stop — ends the task. */
224
266
  stopTask(ref: SessionRef, taskId: string): Promise<void>;
225
267
  }
268
+ /** @sealed */
226
269
  export interface AgentAuth {
227
270
  /** GET /v1/agents/:agentName/sessions/:sessionName/auth */
228
271
  getAuthStatus(ref: SessionRef): Promise<AgentAuthStatus>;
@@ -236,6 +279,7 @@ export interface AgentAuth {
236
279
  /** POST /v1/agents/:agentName/sessions/:sessionName/auth/code — finishes a pending sign-in that needs the page's code. */
237
280
  submitAuthCode(ref: SessionRef, code: string): Promise<AgentAuthStatus>;
238
281
  }
282
+ /** @sealed */
239
283
  export interface AgentModels {
240
284
  /** GET /v1/agents/:agentName/sessions/:sessionName/models — starts the CLI if it isn't running. */
241
285
  listModels(ref: SessionRef): Promise<AgentModelInfo[]>;
@@ -246,8 +290,8 @@ export interface AgentModels {
246
290
  export type AgentSessionCapabilities = AgentStream & AgentTranscript & AgentInterrupt & AgentInput & AgentSessionStatus & AgentMessageQueue & AgentPermissions & AgentQuestions & AgentBackgroundTasks & AgentAuth & AgentModels;
247
291
  /** A message sent to a session, and whether its agent still holds it queued (not yet taken into a turn). */
248
292
  export type AgentLastUserMessage = {
249
- message: UserMessage;
250
- queued: boolean;
293
+ readonly message: UserMessage;
294
+ readonly queued: boolean;
251
295
  };
252
296
  /** The request can't be satisfied as sent (bad name, unknown backend, unknown after_id, ...). */
253
297
  export declare class AgentInvalidError extends Error {
@@ -261,4 +305,11 @@ export declare class AgentNotFoundError extends Error {
261
305
  export declare class AgentConflictError extends Error {
262
306
  name: string;
263
307
  }
308
+ /**
309
+ * The session's backend isn't installed in its dimension, or not completely (AgentBackendInfo.ready):
310
+ * AgentProvision.install puts it there. A conflict with the dimension's state, so caught as one too.
311
+ */
312
+ export declare class AgentBackendNotInstalledError extends AgentConflictError {
313
+ name: string;
314
+ }
264
315
  export {};
package/dist/agent/api.js CHANGED
@@ -12,3 +12,10 @@ export class AgentNotFoundError extends Error {
12
12
  export class AgentConflictError extends Error {
13
13
  name = "AgentConflictError";
14
14
  }
15
+ /**
16
+ * The session's backend isn't installed in its dimension, or not completely (AgentBackendInfo.ready):
17
+ * AgentProvision.install puts it there. A conflict with the dimension's state, so caught as one too.
18
+ */
19
+ export class AgentBackendNotInstalledError extends AgentConflictError {
20
+ name = "AgentBackendNotInstalledError";
21
+ }
@@ -0,0 +1,3 @@
1
+ export { createWorkerClient, type WorkerClient } from "./session/client.ts";
2
+ export { sharedConnector, type SharedConnector } from "./session/sharedConnection.ts";
3
+ export { EndpointLoginRequiredError, EndpointUnreachableError, exeDevLoginUrl, workerSseConnector, type WorkerEndpoint } from "./session/workerSseConnection.ts";
@@ -0,0 +1,5 @@
1
+ // An agent's worker, reached through its endpoint (AgentService.exposeAgent): what a browser can
2
+ // import (hyperbrane/agent/client), since the root module brings the server side with it.
3
+ export { createWorkerClient } from "./session/client.js";
4
+ export { sharedConnector } from "./session/sharedConnection.js";
5
+ export { EndpointLoginRequiredError, EndpointUnreachableError, exeDevLoginUrl, workerSseConnector } from "./session/workerSseConnection.js";
@@ -0,0 +1,13 @@
1
+ import type { Sandbox } from "../sandbox/api.ts";
2
+ import { type AgentTranscriptPage, type AgentTranscriptRequest } from "./api.ts";
3
+ /**
4
+ * A page of the session's transcript, read on `host` from the dimension's state directory (its host
5
+ * side) by the backend's own layout and parsing, so nothing has to run: one command, plus a read
6
+ * for each further chunk the rare page that needs more takes. Null for a session whose env moves the backend's home
7
+ * elsewhere (into the container, where only its worker reaches it).
8
+ */
9
+ export declare function readHostTranscript(host: Sandbox, { stateDirectory, agentName, sessionName }: {
10
+ stateDirectory: string;
11
+ agentName: string;
12
+ sessionName: string;
13
+ }, { limit, before, cursor }: AgentTranscriptRequest): Promise<AgentTranscriptPage | null>;