hyperbrane 0.1.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 +41 -0
- package/dist/agent/agentService.d.ts +31 -0
- package/dist/agent/agentService.js +388 -0
- package/dist/agent/agentStorage.d.ts +27 -0
- package/dist/agent/agentStorage.js +52 -0
- package/dist/agent/api.d.ts +264 -0
- package/dist/agent/api.js +14 -0
- package/dist/agent/gatewaySignIns.d.ts +5 -0
- package/dist/agent/gatewaySignIns.js +11 -0
- package/dist/agent/session/client.d.ts +14 -0
- package/dist/agent/session/client.js +47 -0
- package/dist/agent/session/sessionStorage.d.ts +66 -0
- package/dist/agent/session/sessionStorage.js +89 -0
- package/dist/agent/session/sharedConnection.d.ts +13 -0
- package/dist/agent/session/sharedConnection.js +55 -0
- package/dist/agent/session/worker/jsonRpc.d.ts +39 -0
- package/dist/agent/session/worker/jsonRpc.js +102 -0
- package/dist/agent/session/worker/protocol.d.ts +120 -0
- package/dist/agent/session/worker/protocol.js +32 -0
- package/dist/agent/session/workerConnection.d.ts +29 -0
- package/dist/agent/session/workerConnection.js +44 -0
- package/dist/agent/session/workerSseConnection.d.ts +15 -0
- package/dist/agent/session/workerSseConnection.js +26 -0
- package/dist/agent/workerBundle.d.ts +19 -0
- package/dist/agent/workerBundle.generated.d.ts +1 -0
- package/dist/agent/workerBundle.generated.js +2 -0
- package/dist/agent/workerBundle.js +31 -0
- package/dist/cli/agent.d.ts +3 -0
- package/dist/cli/agent.js +423 -0
- package/dist/cli/command.d.ts +46 -0
- package/dist/cli/command.js +98 -0
- package/dist/cli/dimension.d.ts +2 -0
- package/dist/cli/dimension.js +85 -0
- package/dist/cli/gateway.d.ts +6 -0
- package/dist/cli/gateway.js +245 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +125 -0
- package/dist/cli/portal.d.ts +2 -0
- package/dist/cli/portal.js +39 -0
- package/dist/cli/services.d.ts +18 -0
- package/dist/cli/services.js +33 -0
- package/dist/cli/streamPrinter.d.ts +23 -0
- package/dist/cli/streamPrinter.js +251 -0
- package/dist/cli/table.d.ts +7 -0
- package/dist/cli/table.js +54 -0
- package/dist/dimension/api.d.ts +67 -0
- package/dist/dimension/api.js +81 -0
- package/dist/dimension/http.d.ts +7 -0
- package/dist/dimension/http.js +77 -0
- package/dist/dimension/image.d.ts +13 -0
- package/dist/dimension/image.js +43 -0
- package/dist/dimension/imageFiles.generated.d.ts +1 -0
- package/dist/dimension/imageFiles.generated.js +5 -0
- package/dist/dimension/paths.d.ts +5 -0
- package/dist/dimension/paths.js +7 -0
- package/dist/dimension/service.d.ts +22 -0
- package/dist/dimension/service.js +242 -0
- package/dist/dimension/storage.d.ts +39 -0
- package/dist/dimension/storage.js +78 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +25 -0
- package/dist/lib/docker-api.d.ts +13787 -0
- package/dist/lib/docker-api.js +5 -0
- package/dist/lib/docker.d.ts +77 -0
- package/dist/lib/docker.js +214 -0
- package/dist/lib/image.d.ts +14 -0
- package/dist/lib/image.js +29 -0
- package/dist/lib/jsonDirectory.d.ts +28 -0
- package/dist/lib/jsonDirectory.js +102 -0
- package/dist/lib/sleep.d.ts +2 -0
- package/dist/lib/sleep.js +4 -0
- package/dist/lib/tar.d.ts +2 -0
- package/dist/lib/tar.js +65 -0
- package/dist/lib/version.d.ts +1 -0
- package/dist/lib/version.js +1 -0
- package/dist/portal-gateway/api.d.ts +98 -0
- package/dist/portal-gateway/api.js +12 -0
- package/dist/portal-gateway/fakes.d.ts +3 -0
- package/dist/portal-gateway/fakes.js +11 -0
- package/dist/portal-gateway/gatewayService.d.ts +37 -0
- package/dist/portal-gateway/gatewayService.js +380 -0
- package/dist/portal-gateway/gatewayStorage.d.ts +66 -0
- package/dist/portal-gateway/gatewayStorage.js +125 -0
- package/dist/portal-gateway/image.d.ts +6 -0
- package/dist/portal-gateway/image.js +13 -0
- package/dist/portal-gateway/imageFiles.generated.d.ts +1 -0
- package/dist/portal-gateway/imageFiles.generated.js +8 -0
- package/dist/portal-gateway/portalService.d.ts +48 -0
- package/dist/portal-gateway/portalService.js +327 -0
- package/dist/portal-gateway/portalStorage.d.ts +34 -0
- package/dist/portal-gateway/portalStorage.js +44 -0
- package/dist/portal-gateway/upstream.d.ts +157 -0
- package/dist/portal-gateway/upstream.js +4 -0
- package/dist/sandbox/api.d.ts +184 -0
- package/dist/sandbox/api.js +14 -0
- package/dist/sandbox/boatService.d.ts +52 -0
- package/dist/sandbox/boatService.js +335 -0
- package/dist/sandbox/containerService.d.ts +15 -0
- package/dist/sandbox/containerService.js +177 -0
- package/dist/sandbox/endpointProxy.d.ts +90 -0
- package/dist/sandbox/endpointProxy.js +701 -0
- package/dist/sandbox/exeDevAuth.d.ts +16 -0
- package/dist/sandbox/exeDevAuth.js +49 -0
- package/dist/sandbox/localService.d.ts +19 -0
- package/dist/sandbox/localService.js +182 -0
- package/dist/sandbox/sseConnection.d.ts +41 -0
- package/dist/sandbox/sseConnection.js +166 -0
- package/dist/sandbox/sshService.d.ts +30 -0
- package/dist/sandbox/sshService.js +647 -0
- package/dist/upstreams/anthropic.d.ts +7 -0
- package/dist/upstreams/anthropic.js +27 -0
- package/dist/upstreams/index.d.ts +6 -0
- package/dist/upstreams/index.js +7 -0
- package/dist/upstreams/openai.d.ts +7 -0
- package/dist/upstreams/openai.js +40 -0
- package/dist/vendor-types/claude/agent-sdk/agentSdkTypes.d.ts +1 -0
- package/dist/vendor-types/claude/agent-sdk/bridge.d.ts +388 -0
- package/dist/vendor-types/claude/agent-sdk/browser-sdk.d.ts +184 -0
- package/dist/vendor-types/claude/agent-sdk/core.d.ts +20 -0
- package/dist/vendor-types/claude/agent-sdk/extractFromBunfs.d.ts +1 -0
- package/dist/vendor-types/claude/agent-sdk/sdk-tools.d.ts +4153 -0
- package/dist/vendor-types/claude/agent-sdk/sdk.d.ts +9833 -0
- package/package.json +55 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { isAbsolute, posix } from "node:path";
|
|
2
|
+
import { DockerError } from "../lib/docker.js";
|
|
3
|
+
import { SandboxNotFoundError } from "../sandbox/api.js";
|
|
4
|
+
import { sleep } from "../lib/sleep.js";
|
|
5
|
+
import { HYPERBRANE_VERSION } from "../lib/version.js";
|
|
6
|
+
import { DimensionConflictError, DimensionInvalidError, DimensionNotFoundError, } from "./api.js";
|
|
7
|
+
import { RUN_PATH, STATE_PATH, WORKSPACE_PATH } from "./paths.js";
|
|
8
|
+
export { RUN_PATH, STATE_PATH, WORKSPACE_PATH };
|
|
9
|
+
// Docker's container-name alphabet; also keeps names safe to use as storage file names.
|
|
10
|
+
const NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,62}$/;
|
|
11
|
+
export function containerName(dimensionName) {
|
|
12
|
+
return `hyb-${dimensionName}`;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* `host` is the machine the docker daemon runs on, rooted at "/" so dimension directories
|
|
16
|
+
* (bind-mount sources) resolve the same way docker resolves them.
|
|
17
|
+
*/
|
|
18
|
+
export function createDimensionService(storage, docker, image, host) {
|
|
19
|
+
async function getDimensions() {
|
|
20
|
+
return Promise.all((await storage.getDimensions()).map(describeDimension));
|
|
21
|
+
}
|
|
22
|
+
async function describeDimension({ state, ...dimension }) {
|
|
23
|
+
if (state === "creating")
|
|
24
|
+
return withoutUsage(dimension, "creating");
|
|
25
|
+
const container = await docker.inspectContainer(containerName(dimension.name));
|
|
26
|
+
if (!container)
|
|
27
|
+
return withoutUsage(dimension, "missing");
|
|
28
|
+
const containerState = container.State;
|
|
29
|
+
if (!containerState?.Running || !containerState.StartedAt) {
|
|
30
|
+
return withoutUsage(dimension, containerState?.Status ?? "exited");
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const stats = await docker.containerStats(containerName(dimension.name));
|
|
34
|
+
return {
|
|
35
|
+
...dimension,
|
|
36
|
+
status: "running",
|
|
37
|
+
uptimeSeconds: Math.max(0, Math.floor((Date.now() - Date.parse(containerState.StartedAt)) / 1000)),
|
|
38
|
+
memoryUsage: stats.memoryUsage,
|
|
39
|
+
cpuUsage: stats.cpuUsage,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error instanceof DockerError && error.status === 404)
|
|
44
|
+
return withoutUsage(dimension, "missing");
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Keeps no state between calls: every step re-reads storage or docker and uses operations that are
|
|
49
|
+
// atomic across processes, so a create that crashed halfway, or runs twice at once, converges.
|
|
50
|
+
// A relative directory ("work", "~/work") is the host user's: resolved against their home, and
|
|
51
|
+
// recorded resolved. A missing one is made.
|
|
52
|
+
async function createDimension(requested) {
|
|
53
|
+
assertValidName(requested.name);
|
|
54
|
+
const dimension = { ...requested, directory: await resolveDirectory(requested.directory) };
|
|
55
|
+
const owner = await directoryOwner(dimension.directory);
|
|
56
|
+
const existing = await docker.inspectContainer(containerName(dimension.name));
|
|
57
|
+
if (existing)
|
|
58
|
+
assertOwnedContainer(existing, dimension.name);
|
|
59
|
+
const record = await storage.insertDimension({ ...dimension, state: "creating" });
|
|
60
|
+
assertSameSpec(record, dimension);
|
|
61
|
+
const imageTag = await image.ensure();
|
|
62
|
+
const runDirectory = await storage.ensureRunDirectory(dimension.name);
|
|
63
|
+
const stateDirectory = await storage.ensureStateDirectory(dimension.name);
|
|
64
|
+
await ensureContainer(dimension, owner, imageTag, runDirectory, stateDirectory);
|
|
65
|
+
await docker.startContainer(containerName(dimension.name));
|
|
66
|
+
await ensureUser(dimension, owner);
|
|
67
|
+
if (record.state !== "ready")
|
|
68
|
+
await storage.saveDimension({ ...dimension, state: "ready" });
|
|
69
|
+
}
|
|
70
|
+
async function ensureContainer(dimension, owner, imageTag, runDirectory, stateDirectory) {
|
|
71
|
+
const name = containerName(dimension.name);
|
|
72
|
+
const existing = await docker.inspectContainer(name);
|
|
73
|
+
if (existing)
|
|
74
|
+
return assertOwnedContainer(existing, dimension.name);
|
|
75
|
+
try {
|
|
76
|
+
await docker.createContainer(name, {
|
|
77
|
+
Image: imageTag,
|
|
78
|
+
Cmd: ["sleep", "infinity"],
|
|
79
|
+
Hostname: name,
|
|
80
|
+
User: `${owner.uid}:${owner.gid}`,
|
|
81
|
+
WorkingDir: WORKSPACE_PATH,
|
|
82
|
+
Labels: {
|
|
83
|
+
hyperbrane: "true",
|
|
84
|
+
"hyperbrane.version": HYPERBRANE_VERSION,
|
|
85
|
+
"hyperbrane.dimension": dimension.name,
|
|
86
|
+
},
|
|
87
|
+
HostConfig: {
|
|
88
|
+
Binds: [`${dimension.directory}:${WORKSPACE_PATH}`, `${runDirectory}:${RUN_PATH}`, `${stateDirectory}:${STATE_PATH}`],
|
|
89
|
+
// dmesg: docker drops CAP_SYSLOG, and its seccomp profile refuses the syslog call without it.
|
|
90
|
+
// The log is the host's kernel log, shared by every dimension.
|
|
91
|
+
CapAdd: ["SYSLOG"],
|
|
92
|
+
...containerResources(dimension),
|
|
93
|
+
NetworkMode: "none",
|
|
94
|
+
// Without a network docker doesn't map the hostname, and sudo warns on every call.
|
|
95
|
+
ExtraHosts: [`${name}:127.0.1.1`],
|
|
96
|
+
Init: true,
|
|
97
|
+
RestartPolicy: { Name: "unless-stopped" },
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
// Lost a race with another creator: fine as long as the winner is ours.
|
|
103
|
+
if (!(error instanceof DockerError && error.status === 409))
|
|
104
|
+
throw error;
|
|
105
|
+
assertOwnedContainer(await awaitContainer(name, error), dimension.name);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Docker reserves the name before the winner's container becomes inspectable, so give it a moment.
|
|
109
|
+
async function awaitContainer(name, conflict) {
|
|
110
|
+
for (let attempt = 0; attempt < 50; attempt++) {
|
|
111
|
+
const container = await docker.inspectContainer(name);
|
|
112
|
+
if (container)
|
|
113
|
+
return container;
|
|
114
|
+
await sleep(100);
|
|
115
|
+
}
|
|
116
|
+
throw conflict;
|
|
117
|
+
}
|
|
118
|
+
async function ensureUser(dimension, owner) {
|
|
119
|
+
const result = await docker.exec(containerName(dimension.name), {
|
|
120
|
+
cmd: ["hyb-ensure-user"],
|
|
121
|
+
user: "root",
|
|
122
|
+
env: { HYB_UID: String(owner.uid), HYB_GID: String(owner.gid) },
|
|
123
|
+
});
|
|
124
|
+
if (result.exitCode !== 0) {
|
|
125
|
+
throw new Error(`user setup in ${containerName(dimension.name)} failed: ${result.stderr.trim()}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Container first: if this crashes before the record is removed, the dimension still shows up
|
|
129
|
+
// (as "missing") and the destroy can be retried. Its run and state directories go with the record,
|
|
130
|
+
// emptied from inside first: what a dimension running as root left there (an agent worker's socket
|
|
131
|
+
// and log) the host user couldn't remove.
|
|
132
|
+
async function destroyDimension(dimensionName) {
|
|
133
|
+
assertValidName(dimensionName);
|
|
134
|
+
const name = containerName(dimensionName);
|
|
135
|
+
const existing = await docker.inspectContainer(name);
|
|
136
|
+
if (existing) {
|
|
137
|
+
assertOwnedContainer(existing, dimensionName);
|
|
138
|
+
if (!existing.State?.Running)
|
|
139
|
+
await docker.startContainer(name).catch(() => { });
|
|
140
|
+
// Best effort: if it fails, removing them from the host below says why.
|
|
141
|
+
await docker
|
|
142
|
+
.exec(name, { cmd: ["sh", "-c", 'find "$R" "$S" -mindepth 1 -delete'], user: "root", env: { R: RUN_PATH, S: STATE_PATH } })
|
|
143
|
+
.catch(() => { });
|
|
144
|
+
await docker.removeContainer(name);
|
|
145
|
+
}
|
|
146
|
+
await storage.removeRunDirectory(dimensionName);
|
|
147
|
+
await storage.removeStateDirectory(dimensionName);
|
|
148
|
+
await storage.removeDimension(dimensionName);
|
|
149
|
+
}
|
|
150
|
+
// Same container, so the user and anything installed outside the workspace survive; the workspace
|
|
151
|
+
// is a bind mount either way.
|
|
152
|
+
async function restartDimension(dimensionName) {
|
|
153
|
+
assertValidName(dimensionName);
|
|
154
|
+
if (!(await storage.getDimensions()).some((dimension) => dimension.name === dimensionName)) {
|
|
155
|
+
throw new DimensionNotFoundError(`dimension ${dimensionName} does not exist`);
|
|
156
|
+
}
|
|
157
|
+
const existing = await docker.inspectContainer(containerName(dimensionName));
|
|
158
|
+
if (!existing)
|
|
159
|
+
throw new DimensionNotFoundError(`dimension ${dimensionName} has no container; create it again`);
|
|
160
|
+
assertOwnedContainer(existing, dimensionName);
|
|
161
|
+
await docker.restartContainer(containerName(dimensionName));
|
|
162
|
+
}
|
|
163
|
+
// Container first: a limit docker refuses leaves the record as it was.
|
|
164
|
+
async function updateDimension(dimensionName, limits) {
|
|
165
|
+
assertValidName(dimensionName);
|
|
166
|
+
for (const [name, value] of Object.entries(limits)) {
|
|
167
|
+
if (value !== undefined && !(typeof value === "number" && value > 0))
|
|
168
|
+
throw new DimensionInvalidError(`${name} must be positive`);
|
|
169
|
+
}
|
|
170
|
+
const record = (await storage.getDimensions()).find((dimension) => dimension.name === dimensionName);
|
|
171
|
+
if (!record)
|
|
172
|
+
throw new DimensionNotFoundError(`dimension ${dimensionName} does not exist`);
|
|
173
|
+
const existing = await docker.inspectContainer(containerName(dimensionName));
|
|
174
|
+
if (!existing)
|
|
175
|
+
throw new DimensionNotFoundError(`dimension ${dimensionName} has no container; create it again`);
|
|
176
|
+
assertOwnedContainer(existing, dimensionName);
|
|
177
|
+
const { state, ...current } = record;
|
|
178
|
+
const updated = {
|
|
179
|
+
...current,
|
|
180
|
+
memoryLimit: limits.memoryLimit ?? record.memoryLimit,
|
|
181
|
+
cpuLimit: limits.cpuLimit ?? record.cpuLimit,
|
|
182
|
+
};
|
|
183
|
+
await docker.updateContainer(containerName(dimensionName), containerResources(updated));
|
|
184
|
+
await storage.saveDimension({ ...updated, state });
|
|
185
|
+
return updated;
|
|
186
|
+
}
|
|
187
|
+
async function resolveDirectory(directory) {
|
|
188
|
+
if (!directory.trim())
|
|
189
|
+
throw new DimensionInvalidError("directory is required");
|
|
190
|
+
if (isAbsolute(directory))
|
|
191
|
+
return directory;
|
|
192
|
+
const relative = directory === "~" ? "." : directory.replace(/^~\//, "");
|
|
193
|
+
return posix.join(await host.homeDirectory(), relative);
|
|
194
|
+
}
|
|
195
|
+
async function directoryOwner(directory) {
|
|
196
|
+
if (!isAbsolute(directory))
|
|
197
|
+
throw new DimensionInvalidError(`directory must be absolute: ${directory}`);
|
|
198
|
+
// A missing one is made, by the host user: theirs, so the dimension runs as them.
|
|
199
|
+
const info = await host.stat(directory).catch(async (error) => {
|
|
200
|
+
if (!(error instanceof SandboxNotFoundError))
|
|
201
|
+
throw error;
|
|
202
|
+
await host.createDirectory(directory).catch((cause) => {
|
|
203
|
+
throw new DimensionInvalidError(`directory ${directory} doesn't exist and can't be made: ${cause.message}`);
|
|
204
|
+
});
|
|
205
|
+
return host.stat(directory);
|
|
206
|
+
});
|
|
207
|
+
if (info.type !== "directory")
|
|
208
|
+
throw new DimensionInvalidError(`not a directory: ${directory}`);
|
|
209
|
+
return { uid: info.uid, gid: info.gid };
|
|
210
|
+
}
|
|
211
|
+
async function getDimensionNames() {
|
|
212
|
+
return (await storage.getDimensions()).map((dimension) => dimension.name);
|
|
213
|
+
}
|
|
214
|
+
return { getDimensions, getDimensionNames, createDimension, destroyDimension, restartDimension, updateDimension };
|
|
215
|
+
}
|
|
216
|
+
function assertValidName(dimensionName) {
|
|
217
|
+
if (!NAME_PATTERN.test(dimensionName)) {
|
|
218
|
+
throw new DimensionInvalidError(`invalid dimension name "${dimensionName}": use letters, digits, "_", "." or "-", starting with a letter or digit`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function assertSameSpec(existing, requested) {
|
|
222
|
+
const differs = existing.directory !== requested.directory ||
|
|
223
|
+
existing.memoryLimit !== requested.memoryLimit ||
|
|
224
|
+
existing.cpuLimit !== requested.cpuLimit ||
|
|
225
|
+
[...(existing.portals ?? [])].sort().join("\0") !== [...(requested.portals ?? [])].sort().join("\0");
|
|
226
|
+
if (differs) {
|
|
227
|
+
throw new DimensionConflictError(`dimension ${requested.name} already exists with a different spec`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function assertOwnedContainer(container, dimensionName) {
|
|
231
|
+
if (container.Config?.Labels?.["hyperbrane.dimension"] !== dimensionName) {
|
|
232
|
+
throw new DimensionConflictError(`container ${containerName(dimensionName)} exists but is not managed by hyperbrane`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// Swap stays at twice the memory, which is what docker gives a container created with only a
|
|
236
|
+
// memory limit; setting both keeps an update valid whichever way the limit moves.
|
|
237
|
+
function containerResources({ memoryLimit, cpuLimit }) {
|
|
238
|
+
return { Memory: memoryLimit, MemorySwap: memoryLimit * 2, NanoCpus: Math.round(cpuLimit * 1e9) };
|
|
239
|
+
}
|
|
240
|
+
function withoutUsage(dimension, status) {
|
|
241
|
+
return { ...dimension, status, uptimeSeconds: null, memoryUsage: null, cpuUsage: null };
|
|
242
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type Sandbox } from "../sandbox/api.ts";
|
|
2
|
+
import type { Dimension } from "./api.ts";
|
|
3
|
+
/** "creating" until provisioning has finished once; a create that crashed stays "creating" until retried. */
|
|
4
|
+
export type DimensionRecord = Dimension & {
|
|
5
|
+
state: "creating" | "ready";
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* One JSON file per dimension, so no operation rewrites a shared file and every write is a single
|
|
9
|
+
* atomic sandbox operation. Safe to use from several processes at once.
|
|
10
|
+
*/
|
|
11
|
+
export interface Storage {
|
|
12
|
+
getDimensions(): Promise<DimensionRecord[]>;
|
|
13
|
+
/** Stores the record unless one with that name exists; returns whichever record is stored afterwards. */
|
|
14
|
+
insertDimension(record: DimensionRecord): Promise<DimensionRecord>;
|
|
15
|
+
/** Replaces the record. */
|
|
16
|
+
saveDimension(record: DimensionRecord): Promise<void>;
|
|
17
|
+
/** No-op if the record doesn't exist. */
|
|
18
|
+
removeDimension(dimensionName: string): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Sandbox path of the dimension's run directory, mounted into it at RUN_PATH so processes on both
|
|
21
|
+
* sides can meet at unix sockets in it (the agent session worker). Creates it.
|
|
22
|
+
*/
|
|
23
|
+
ensureRunDirectory(dimensionName: string): Promise<string>;
|
|
24
|
+
/** Removes the run directory and everything in it. No-op if missing. */
|
|
25
|
+
removeRunDirectory(dimensionName: string): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Sandbox path of the dimension's state directory, mounted into it at STATE_PATH: what hyperbrane
|
|
28
|
+
* keeps for what runs in the dimension (the agent session worker's records), on the host with the
|
|
29
|
+
* rest of its storage. Creates it.
|
|
30
|
+
*/
|
|
31
|
+
ensureStateDirectory(dimensionName: string): Promise<string>;
|
|
32
|
+
/** Removes the state directory and everything in it. No-op if missing. */
|
|
33
|
+
removeStateDirectory(dimensionName: string): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* `dir` is a sandbox path; defaults to ~/.local/share/hyperbrane/dimensions of the sandbox's user.
|
|
37
|
+
* Run and state directories go next to it, in dimension-run/<dimension> and dimension-state/<dimension>.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createFileStorage(sandbox: Sandbox, dir?: string): Storage;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { dirname } from "node:path";
|
|
2
|
+
import { createJsonDirectory } from "../lib/jsonDirectory.js";
|
|
3
|
+
import { SandboxNotFoundError } from "../sandbox/api.js";
|
|
4
|
+
const EXTENSION = ".json";
|
|
5
|
+
/**
|
|
6
|
+
* `dir` is a sandbox path; defaults to ~/.local/share/hyperbrane/dimensions of the sandbox's user.
|
|
7
|
+
* Run and state directories go next to it, in dimension-run/<dimension> and dimension-state/<dimension>.
|
|
8
|
+
*/
|
|
9
|
+
export function createFileStorage(sandbox, dir) {
|
|
10
|
+
async function storageDir() {
|
|
11
|
+
return dir ?? `${await sandbox.homeDirectory()}/.local/share/hyperbrane/dimensions`;
|
|
12
|
+
}
|
|
13
|
+
async function pathFor(dimensionName) {
|
|
14
|
+
return `${await storageDir()}/${dimensionName}${EXTENSION}`;
|
|
15
|
+
}
|
|
16
|
+
function serialize(record) {
|
|
17
|
+
return JSON.stringify(record, null, 2) + "\n";
|
|
18
|
+
}
|
|
19
|
+
async function readRecord(path) {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(new TextDecoder().decode(await sandbox.readFile(path)));
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error instanceof SandboxNotFoundError)
|
|
25
|
+
return null;
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// All at once (lib/jsonDirectory.ts): a call per record adds up on a remote machine.
|
|
30
|
+
async function getDimensions() {
|
|
31
|
+
return createJsonDirectory(sandbox, storageDir).list();
|
|
32
|
+
}
|
|
33
|
+
async function insertDimension(record) {
|
|
34
|
+
const path = await pathFor(record.name);
|
|
35
|
+
if (await sandbox.writeFileExclusive(path, serialize(record)))
|
|
36
|
+
return record;
|
|
37
|
+
// Lost to an existing record; if that one was removed in the meantime, try again.
|
|
38
|
+
return (await readRecord(path)) ?? insertDimension(record);
|
|
39
|
+
}
|
|
40
|
+
async function saveDimension(record) {
|
|
41
|
+
await sandbox.writeFile(await pathFor(record.name), serialize(record));
|
|
42
|
+
}
|
|
43
|
+
async function removeDimension(dimensionName) {
|
|
44
|
+
await sandbox.removeFile(await pathFor(dimensionName));
|
|
45
|
+
}
|
|
46
|
+
async function runDirectory(dimensionName) {
|
|
47
|
+
return `${dirname(await storageDir())}/dimension-run/${dimensionName}`;
|
|
48
|
+
}
|
|
49
|
+
async function ensureRunDirectory(dimensionName) {
|
|
50
|
+
const path = await runDirectory(dimensionName);
|
|
51
|
+
await sandbox.createDirectory(path);
|
|
52
|
+
return path;
|
|
53
|
+
}
|
|
54
|
+
async function removeRunDirectory(dimensionName) {
|
|
55
|
+
await sandbox.removeDirectory(await runDirectory(dimensionName));
|
|
56
|
+
}
|
|
57
|
+
async function stateDirectory(dimensionName) {
|
|
58
|
+
return `${dirname(await storageDir())}/dimension-state/${dimensionName}`;
|
|
59
|
+
}
|
|
60
|
+
async function ensureStateDirectory(dimensionName) {
|
|
61
|
+
const path = await stateDirectory(dimensionName);
|
|
62
|
+
await sandbox.createDirectory(path);
|
|
63
|
+
return path;
|
|
64
|
+
}
|
|
65
|
+
async function removeStateDirectory(dimensionName) {
|
|
66
|
+
await sandbox.removeDirectory(await stateDirectory(dimensionName));
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
getDimensions,
|
|
70
|
+
insertDimension,
|
|
71
|
+
saveDimension,
|
|
72
|
+
removeDimension,
|
|
73
|
+
ensureRunDirectory,
|
|
74
|
+
removeRunDirectory,
|
|
75
|
+
ensureStateDirectory,
|
|
76
|
+
removeStateDirectory,
|
|
77
|
+
};
|
|
78
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export * from "./sandbox/api.ts";
|
|
2
|
+
export * from "./dimension/api.ts";
|
|
3
|
+
export * from "./portal-gateway/api.ts";
|
|
4
|
+
export * from "./agent/api.ts";
|
|
5
|
+
export { createServices, type Services } from "./cli/services.ts";
|
|
6
|
+
export { createLocalSandbox, type LocalSandboxOptions } from "./sandbox/localService.ts";
|
|
7
|
+
export { createSshSandbox, type SshSandboxOptions } from "./sandbox/sshService.ts";
|
|
8
|
+
export { createContainerSandbox, type ContainerSandboxOptions } from "./sandbox/containerService.ts";
|
|
9
|
+
export { BOAT_API_URL, BoatError, archiveBoatMachine, authorizeBoatSshKey, boatHostPortAuth, createBoatApi, createBoatMachine, createBoatSandbox, ensureBoatRunning, type BoatSandboxOptions, } from "./sandbox/boatService.ts";
|
|
10
|
+
export { parseAccessibleHost, proxyCredentialsAuth, type SandboxEndpointAuth, type SandboxEndpointsOptions } from "./sandbox/endpointProxy.ts";
|
|
11
|
+
export { exeDevProxyAuth, type ExeDevAuthOptions } from "./sandbox/exeDevAuth.ts";
|
|
12
|
+
export { EndpointLoginRequiredError, endpointRequest, exeDevLoginUrl, openSseConnection, type EndpointAccess } from "./sandbox/sseConnection.ts";
|
|
13
|
+
export { createWorkerClient, type WorkerClient } from "./agent/session/client.ts";
|
|
14
|
+
export { workerSseConnector, type WorkerEndpoint } from "./agent/session/workerSseConnection.ts";
|
|
15
|
+
export { sharedConnector, type SharedConnector } from "./agent/session/sharedConnection.ts";
|
|
16
|
+
export { createDockerClient, DockerError, type DockerClient } from "./lib/docker.ts";
|
|
17
|
+
export { BUILTIN_UPSTREAMS } from "./upstreams/index.ts";
|
|
18
|
+
export { HYPERBRANE_VERSION } from "./lib/version.ts";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// The hyperbrane SDK's entry point: what an application drives a server with. Everything else is
|
|
2
|
+
// reachable by its path too (`hyperbrane/sandbox/sshService`, ...).
|
|
3
|
+
// The APIs: types, errors and helpers of each part.
|
|
4
|
+
export * from "./sandbox/api.js";
|
|
5
|
+
export * from "./dimension/api.js";
|
|
6
|
+
export * from "./portal-gateway/api.js";
|
|
7
|
+
export * from "./agent/api.js";
|
|
8
|
+
// All of a machine's services over one sandbox (this machine by default).
|
|
9
|
+
export { createServices } from "./cli/services.js";
|
|
10
|
+
// Sandboxes: the machine the services drive, however it is reached.
|
|
11
|
+
export { createLocalSandbox } from "./sandbox/localService.js";
|
|
12
|
+
export { createSshSandbox } from "./sandbox/sshService.js";
|
|
13
|
+
export { createContainerSandbox } from "./sandbox/containerService.js";
|
|
14
|
+
export { BOAT_API_URL, BoatError, archiveBoatMachine, authorizeBoatSshKey, boatHostPortAuth, createBoatApi, createBoatMachine, createBoatSandbox, ensureBoatRunning, } from "./sandbox/boatService.js";
|
|
15
|
+
// Accessible endpoints: how they are authenticated, and reaching them.
|
|
16
|
+
export { parseAccessibleHost, proxyCredentialsAuth } from "./sandbox/endpointProxy.js";
|
|
17
|
+
export { exeDevProxyAuth } from "./sandbox/exeDevAuth.js";
|
|
18
|
+
export { EndpointLoginRequiredError, endpointRequest, exeDevLoginUrl, openSseConnection } from "./sandbox/sseConnection.js";
|
|
19
|
+
// An agent's worker, reached through its endpoint (AgentService.exposeAgent), from a server or a browser.
|
|
20
|
+
export { createWorkerClient } from "./agent/session/client.js";
|
|
21
|
+
export { workerSseConnector } from "./agent/session/workerSseConnection.js";
|
|
22
|
+
export { sharedConnector } from "./agent/session/sharedConnection.js";
|
|
23
|
+
export { createDockerClient, DockerError } from "./lib/docker.js";
|
|
24
|
+
export { BUILTIN_UPSTREAMS } from "./upstreams/index.js";
|
|
25
|
+
export { HYPERBRANE_VERSION } from "./lib/version.js";
|