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