hyperbrane 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -2
- package/dist/agent/agentService.d.ts +1 -0
- package/dist/agent/agentService.js +16 -1
- package/dist/agent/agentStorage.d.ts +2 -0
- package/dist/agent/agentStorage.js +4 -2
- package/dist/agent/api.d.ts +51 -27
- package/dist/agent/client.d.ts +3 -0
- package/dist/agent/client.js +5 -0
- package/dist/agent/hostTranscript.d.ts +13 -0
- package/dist/agent/hostTranscript.js +190 -0
- package/dist/agent/session/backend.d.ts +80 -0
- package/dist/agent/session/backend.js +1 -0
- package/dist/agent/session/claude/home.d.ts +2 -0
- package/dist/agent/session/claude/home.js +4 -0
- package/dist/agent/session/claude/transcript.d.ts +13 -0
- package/dist/agent/session/claude/transcript.js +29 -0
- package/dist/agent/session/client.d.ts +5 -1
- package/dist/agent/session/codex/home.d.ts +2 -0
- package/dist/agent/session/codex/home.js +4 -0
- package/dist/agent/session/codex/messages.d.ts +30 -0
- package/dist/agent/session/codex/messages.js +135 -0
- package/dist/agent/session/codex/transcript.d.ts +12 -0
- package/dist/agent/session/codex/transcript.js +22 -0
- package/dist/agent/session/fileTranscript.d.ts +92 -0
- package/dist/agent/session/fileTranscript.js +454 -0
- package/dist/agent/session/sdkMessages.d.ts +15 -0
- package/dist/agent/session/sdkMessages.js +28 -0
- package/dist/agent/session/sessionStorage.d.ts +4 -0
- package/dist/agent/session/sessionStorage.js +13 -2
- package/dist/agent/session/sharedConnection.d.ts +1 -0
- package/dist/agent/session/storedTranscripts.d.ts +13 -0
- package/dist/agent/session/storedTranscripts.js +8 -0
- package/dist/agent/session/worker/protocol.d.ts +1 -0
- package/dist/agent/session/worker/workerParams.d.ts +11 -0
- package/dist/agent/session/worker/workerParams.js +162 -0
- package/dist/agent/vendor-types/codex/app-server/AbsolutePathBuf.d.ts +10 -0
- package/dist/agent/vendor-types/codex/app-server/AbsolutePathBuf.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/FunctionCallOutputBody.d.ts +2 -0
- package/dist/agent/vendor-types/codex/app-server/FunctionCallOutputBody.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/FunctionCallOutputContentItem.d.ts +22 -0
- package/dist/agent/vendor-types/codex/app-server/FunctionCallOutputContentItem.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/ImageDetail.d.ts +1 -0
- package/dist/agent/vendor-types/codex/app-server/ImageDetail.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/ImageGenerationFailure.d.ts +5 -0
- package/dist/agent/vendor-types/codex/app-server/ImageGenerationFailure.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/ImageGenerationItem.d.ts +11 -0
- package/dist/agent/vendor-types/codex/app-server/ImageGenerationItem.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/LegacyAppPathString.d.ts +23 -0
- package/dist/agent/vendor-types/codex/app-server/LegacyAppPathString.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/MessagePhase.d.ts +7 -0
- package/dist/agent/vendor-types/codex/app-server/MessagePhase.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/ReasoningEffort.d.ts +4 -0
- package/dist/agent/vendor-types/codex/app-server/ReasoningEffort.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/SleepItem.d.ts +7 -0
- package/dist/agent/vendor-types/codex/app-server/SleepItem.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/WebSearchItem.d.ts +14 -0
- package/dist/agent/vendor-types/codex/app-server/WebSearchItem.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/serde_json/JsonValue.d.ts +3 -0
- package/dist/agent/vendor-types/codex/app-server/serde_json/JsonValue.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/AgentMessageDelivery.d.ts +1 -0
- package/dist/agent/vendor-types/codex/app-server/v2/AgentMessageDelivery.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/AsyncUserInputQuestion.d.ts +4 -0
- package/dist/agent/vendor-types/codex/app-server/v2/AsyncUserInputQuestion.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/ByteRange.d.ts +4 -0
- package/dist/agent/vendor-types/codex/app-server/v2/ByteRange.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentState.d.ts +5 -0
- package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentState.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentStatus.d.ts +1 -0
- package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentStatus.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentTool.d.ts +1 -0
- package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentTool.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentToolCallStatus.d.ts +1 -0
- package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentToolCallStatus.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/CommandAction.d.ts +19 -0
- package/dist/agent/vendor-types/codex/app-server/v2/CommandAction.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/CommandExecutionSource.d.ts +1 -0
- package/dist/agent/vendor-types/codex/app-server/v2/CommandExecutionSource.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/CommandExecutionStatus.d.ts +1 -0
- package/dist/agent/vendor-types/codex/app-server/v2/CommandExecutionStatus.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/DynamicToolCallOutputContentItem.d.ts +10 -0
- package/dist/agent/vendor-types/codex/app-server/v2/DynamicToolCallOutputContentItem.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/DynamicToolCallStatus.d.ts +1 -0
- package/dist/agent/vendor-types/codex/app-server/v2/DynamicToolCallStatus.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/FileUpdateChange.d.ts +6 -0
- package/dist/agent/vendor-types/codex/app-server/v2/FileUpdateChange.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/HookPromptFragment.d.ts +4 -0
- package/dist/agent/vendor-types/codex/app-server/v2/HookPromptFragment.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/McpAppDisplayMode.d.ts +1 -0
- package/dist/agent/vendor-types/codex/app-server/v2/McpAppDisplayMode.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/McpAppUi.d.ts +8 -0
- package/dist/agent/vendor-types/codex/app-server/v2/McpAppUi.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallAppContext.d.ts +7 -0
- package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallAppContext.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallError.d.ts +3 -0
- package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallError.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallResult.d.ts +6 -0
- package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallResult.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallStatus.d.ts +1 -0
- package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallStatus.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/MemoryCitation.d.ts +5 -0
- package/dist/agent/vendor-types/codex/app-server/v2/MemoryCitation.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/MemoryCitationEntry.d.ts +6 -0
- package/dist/agent/vendor-types/codex/app-server/v2/MemoryCitationEntry.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/PatchApplyStatus.d.ts +1 -0
- package/dist/agent/vendor-types/codex/app-server/v2/PatchApplyStatus.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/PatchChangeKind.d.ts +8 -0
- package/dist/agent/vendor-types/codex/app-server/v2/PatchChangeKind.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/SubAgentActivityKind.d.ts +1 -0
- package/dist/agent/vendor-types/codex/app-server/v2/SubAgentActivityKind.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/TextElement.d.ts +11 -0
- package/dist/agent/vendor-types/codex/app-server/v2/TextElement.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/ThreadItem.d.ts +215 -0
- package/dist/agent/vendor-types/codex/app-server/v2/ThreadItem.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/UserInput.d.ts +35 -0
- package/dist/agent/vendor-types/codex/app-server/v2/UserInput.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/WebSearchAction.d.ts +14 -0
- package/dist/agent/vendor-types/codex/app-server/v2/WebSearchAction.js +2 -0
- package/dist/agent/workerBundle.generated.js +1 -1
- package/dist/cli/agent.js +17 -4
- package/dist/cli/services.d.ts +6 -5
- package/dist/dimension/api.d.ts +5 -4
- package/dist/dimension/paths.d.ts +4 -0
- package/dist/dimension/paths.js +10 -2
- package/dist/dimension/service.d.ts +1 -0
- package/dist/dimension/storage.js +3 -2
- package/dist/index.d.ts +1 -3
- package/dist/index.js +4 -5
- package/dist/lib/docker.d.ts +1 -0
- package/dist/portal-gateway/api.d.ts +6 -5
- package/dist/portal-gateway/gatewayService.d.ts +1 -0
- package/dist/portal-gateway/portalService.d.ts +2 -0
- package/dist/sandbox/api.d.ts +49 -21
- package/dist/sandbox/boatService.js +11 -3
- package/dist/sandbox/containerService.js +10 -3
- package/dist/sandbox/localService.js +20 -2
- package/dist/sandbox/sshService.js +37 -4
- package/package.json +25 -8
package/dist/cli/agent.js
CHANGED
|
@@ -375,7 +375,11 @@ export const sessionCommands = {
|
|
|
375
375
|
args: ["AGENT/SESSION"],
|
|
376
376
|
flags: {
|
|
377
377
|
limit: { type: "string", short: "n", value: "N", description: "How many messages, about (default 50)" },
|
|
378
|
-
before: {
|
|
378
|
+
before: {
|
|
379
|
+
type: "string",
|
|
380
|
+
value: "TIME",
|
|
381
|
+
description: "Messages from before this time: ISO 8601 (2026-10-09T14:00:00Z, 2026-10-09), or how long ago (90s, 30m, 2h, 3d)",
|
|
382
|
+
},
|
|
379
383
|
},
|
|
380
384
|
async run([name], flags, { agents }) {
|
|
381
385
|
const limit = typeof flags.limit === "string" ? Number(flags.limit) : undefined;
|
|
@@ -383,14 +387,14 @@ export const sessionCommands = {
|
|
|
383
387
|
throw new UsageError(`--limit takes a positive number, not ${flags.limit}`);
|
|
384
388
|
const page = await agents.getTranscript(sessionRef(name), {
|
|
385
389
|
limit,
|
|
386
|
-
|
|
390
|
+
before: typeof flags.before === "string" ? timeFlag(flags.before) : undefined,
|
|
387
391
|
});
|
|
388
392
|
const printer = createTerminalPrinter({ paced: false });
|
|
389
393
|
for (const { type, message } of page.messages)
|
|
390
394
|
printer.printTranscript(type, message.content);
|
|
391
395
|
printer.close();
|
|
392
|
-
if (page.
|
|
393
|
-
process.stderr.write(`(older: hyperbrane session logs ${name} --before ${page.
|
|
396
|
+
if (page.nextBefore)
|
|
397
|
+
process.stderr.write(`(older: hyperbrane session logs ${name} --before ${page.nextBefore})\n`);
|
|
394
398
|
},
|
|
395
399
|
},
|
|
396
400
|
status: {
|
|
@@ -421,3 +425,12 @@ export const sessionCommands = {
|
|
|
421
425
|
question: questionCommands,
|
|
422
426
|
},
|
|
423
427
|
};
|
|
428
|
+
const AGO_UNITS = { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 };
|
|
429
|
+
/** --before's TIME as an ISO 8601 time: a date or time, or how long ago ("30m"). */
|
|
430
|
+
function timeFlag(value) {
|
|
431
|
+
const ago = /^(\d+)([smhd])$/.exec(value);
|
|
432
|
+
const time = ago ? Date.now() - Number(ago[1]) * AGO_UNITS[ago[2]] : Date.parse(value);
|
|
433
|
+
if (Number.isNaN(time))
|
|
434
|
+
throw new UsageError(`--before takes a time (2026-10-09T14:00:00Z) or how long ago (2h), not ${value}`);
|
|
435
|
+
return new Date(time).toISOString();
|
|
436
|
+
}
|
package/dist/cli/services.d.ts
CHANGED
|
@@ -4,12 +4,13 @@ import { type DockerClient } from "../lib/docker.ts";
|
|
|
4
4
|
import type { Sandbox } from "../sandbox/api.ts";
|
|
5
5
|
import { type GatewayService } from "../portal-gateway/gatewayService.ts";
|
|
6
6
|
import { type PortalService } from "../portal-gateway/portalService.ts";
|
|
7
|
+
/** @sealed */
|
|
7
8
|
export type Services = {
|
|
8
|
-
docker: DockerClient;
|
|
9
|
-
dimensions: DimensionService;
|
|
10
|
-
gateways: GatewayService;
|
|
11
|
-
portals: PortalService;
|
|
12
|
-
agents: AgentService;
|
|
9
|
+
readonly docker: DockerClient;
|
|
10
|
+
readonly dimensions: DimensionService;
|
|
11
|
+
readonly gateways: GatewayService;
|
|
12
|
+
readonly portals: PortalService;
|
|
13
|
+
readonly agents: AgentService;
|
|
13
14
|
};
|
|
14
15
|
/**
|
|
15
16
|
* Everything runs against `host`'s docker, with state in ~/.local/share/hyperbrane of its user: this
|
package/dist/dimension/api.d.ts
CHANGED
|
@@ -28,14 +28,15 @@ export type DimensionLimits = {
|
|
|
28
28
|
};
|
|
29
29
|
export type DimensionStatus = "creating" | "missing" | "created" | "running" | "paused" | "restarting" | "removing" | "exited" | "dead";
|
|
30
30
|
export type DimensionInfo = Dimension & {
|
|
31
|
-
status: DimensionStatus;
|
|
31
|
+
readonly status: DimensionStatus;
|
|
32
32
|
/** Null unless running. */
|
|
33
|
-
uptimeSeconds: number | null;
|
|
33
|
+
readonly uptimeSeconds: number | null;
|
|
34
34
|
/** Bytes. Null unless running. */
|
|
35
|
-
memoryUsage: number | null;
|
|
35
|
+
readonly memoryUsage: number | null;
|
|
36
36
|
/** Cores. Null unless running. */
|
|
37
|
-
cpuUsage: number | null;
|
|
37
|
+
readonly cpuUsage: number | null;
|
|
38
38
|
};
|
|
39
|
+
/** @sealed */
|
|
39
40
|
export interface DimensionClient {
|
|
40
41
|
/** POST /v1/dimensions */
|
|
41
42
|
createDimension(dimension: Dimension): Promise<Dimension>;
|
|
@@ -3,3 +3,7 @@ export declare const WORKSPACE_PATH = "/workspace";
|
|
|
3
3
|
export declare const RUN_PATH = "/run/hyperbrane";
|
|
4
4
|
/** Where the dimension's state directory (see Storage.ensureStateDirectory) is mounted. */
|
|
5
5
|
export declare const STATE_PATH = "/var/lib/hyperbrane";
|
|
6
|
+
/** On the host, under hyperbrane's data directory: a dimension's run directory, mounted at RUN_PATH. */
|
|
7
|
+
export declare function dimensionRunDirectory(dataDirectory: string, dimensionName: string): string;
|
|
8
|
+
/** On the host, under hyperbrane's data directory: a dimension's state directory, mounted at STATE_PATH. */
|
|
9
|
+
export declare function dimensionStateDirectory(dataDirectory: string, dimensionName: string): string;
|
package/dist/dimension/paths.js
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
// Where things are inside every dimension. Its own module so code running in a
|
|
2
|
-
// worker) can use them without the docker client service.ts brings along.
|
|
1
|
+
// Where things are inside every dimension, and on the host. Its own module so code running in a
|
|
2
|
+
// dimension (the agent worker) can use them without the docker client service.ts brings along.
|
|
3
3
|
export const WORKSPACE_PATH = "/workspace";
|
|
4
4
|
/** Where the dimension's run directory (see Storage.ensureRunDirectory) is mounted. */
|
|
5
5
|
export const RUN_PATH = "/run/hyperbrane";
|
|
6
6
|
/** Where the dimension's state directory (see Storage.ensureStateDirectory) is mounted. */
|
|
7
7
|
export const STATE_PATH = "/var/lib/hyperbrane";
|
|
8
|
+
/** On the host, under hyperbrane's data directory: a dimension's run directory, mounted at RUN_PATH. */
|
|
9
|
+
export function dimensionRunDirectory(dataDirectory, dimensionName) {
|
|
10
|
+
return `${dataDirectory}/dimension-run/${dimensionName}`;
|
|
11
|
+
}
|
|
12
|
+
/** On the host, under hyperbrane's data directory: a dimension's state directory, mounted at STATE_PATH. */
|
|
13
|
+
export function dimensionStateDirectory(dataDirectory, dimensionName) {
|
|
14
|
+
return `${dataDirectory}/dimension-state/${dimensionName}`;
|
|
15
|
+
}
|
|
@@ -5,6 +5,7 @@ import type { DimensionImage } from "./image.ts";
|
|
|
5
5
|
import type { Storage } from "./storage.ts";
|
|
6
6
|
import { RUN_PATH, STATE_PATH, WORKSPACE_PATH } from "./paths.ts";
|
|
7
7
|
export { RUN_PATH, STATE_PATH, WORKSPACE_PATH };
|
|
8
|
+
/** @sealed */
|
|
8
9
|
export interface DimensionService {
|
|
9
10
|
getDimensions(): Promise<DimensionInfo[]>;
|
|
10
11
|
/** The dimensions' names, from the records alone: none of getDimensions' container status and usage. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { dirname } from "node:path";
|
|
2
2
|
import { createJsonDirectory } from "../lib/jsonDirectory.js";
|
|
3
3
|
import { SandboxNotFoundError } from "../sandbox/api.js";
|
|
4
|
+
import { dimensionRunDirectory, dimensionStateDirectory } from "./paths.js";
|
|
4
5
|
const EXTENSION = ".json";
|
|
5
6
|
/**
|
|
6
7
|
* `dir` is a sandbox path; defaults to ~/.local/share/hyperbrane/dimensions of the sandbox's user.
|
|
@@ -44,7 +45,7 @@ export function createFileStorage(sandbox, dir) {
|
|
|
44
45
|
await sandbox.removeFile(await pathFor(dimensionName));
|
|
45
46
|
}
|
|
46
47
|
async function runDirectory(dimensionName) {
|
|
47
|
-
return
|
|
48
|
+
return dimensionRunDirectory(dirname(await storageDir()), dimensionName);
|
|
48
49
|
}
|
|
49
50
|
async function ensureRunDirectory(dimensionName) {
|
|
50
51
|
const path = await runDirectory(dimensionName);
|
|
@@ -55,7 +56,7 @@ export function createFileStorage(sandbox, dir) {
|
|
|
55
56
|
await sandbox.removeDirectory(await runDirectory(dimensionName));
|
|
56
57
|
}
|
|
57
58
|
async function stateDirectory(dimensionName) {
|
|
58
|
-
return
|
|
59
|
+
return dimensionStateDirectory(dirname(await storageDir()), dimensionName);
|
|
59
60
|
}
|
|
60
61
|
async function ensureStateDirectory(dimensionName) {
|
|
61
62
|
const path = await stateDirectory(dimensionName);
|
package/dist/index.d.ts
CHANGED
|
@@ -10,9 +10,7 @@ export { BOAT_API_URL, BoatError, archiveBoatMachine, authorizeBoatSshKey, boatH
|
|
|
10
10
|
export { parseAccessibleHost, proxyCredentialsAuth, type SandboxEndpointAuth, type SandboxEndpointsOptions } from "./sandbox/endpointProxy.ts";
|
|
11
11
|
export { exeDevProxyAuth, type ExeDevAuthOptions } from "./sandbox/exeDevAuth.ts";
|
|
12
12
|
export { EndpointLoginRequiredError, endpointRequest, exeDevLoginUrl, openSseConnection, type EndpointAccess } from "./sandbox/sseConnection.ts";
|
|
13
|
-
export
|
|
14
|
-
export { workerSseConnector, type WorkerEndpoint } from "./agent/session/workerSseConnection.ts";
|
|
15
|
-
export { sharedConnector, type SharedConnector } from "./agent/session/sharedConnection.ts";
|
|
13
|
+
export * from "./agent/client.ts";
|
|
16
14
|
export { createDockerClient, DockerError, type DockerClient } from "./lib/docker.ts";
|
|
17
15
|
export { BUILTIN_UPSTREAMS } from "./upstreams/index.ts";
|
|
18
16
|
export { HYPERBRANE_VERSION } from "./lib/version.ts";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// The hyperbrane SDK's entry point: what an application drives a server with.
|
|
2
|
-
//
|
|
1
|
+
// The hyperbrane SDK's entry point: what an application drives a server with. The package exports
|
|
2
|
+
// this and, for browsers (which can't take the server side along), each part's api and the worker
|
|
3
|
+
// client: hyperbrane/{sandbox,dimension,portal-gateway,agent}/api, hyperbrane/agent/client.
|
|
3
4
|
// The APIs: types, errors and helpers of each part.
|
|
4
5
|
export * from "./sandbox/api.js";
|
|
5
6
|
export * from "./dimension/api.js";
|
|
@@ -17,9 +18,7 @@ export { parseAccessibleHost, proxyCredentialsAuth } from "./sandbox/endpointPro
|
|
|
17
18
|
export { exeDevProxyAuth } from "./sandbox/exeDevAuth.js";
|
|
18
19
|
export { EndpointLoginRequiredError, endpointRequest, exeDevLoginUrl, openSseConnection } from "./sandbox/sseConnection.js";
|
|
19
20
|
// An agent's worker, reached through its endpoint (AgentService.exposeAgent), from a server or a browser.
|
|
20
|
-
export
|
|
21
|
-
export { workerSseConnector } from "./agent/session/workerSseConnection.js";
|
|
22
|
-
export { sharedConnector } from "./agent/session/sharedConnection.js";
|
|
21
|
+
export * from "./agent/client.js";
|
|
23
22
|
export { createDockerClient, DockerError } from "./lib/docker.js";
|
|
24
23
|
export { BUILTIN_UPSTREAMS } from "./upstreams/index.js";
|
|
25
24
|
export { HYPERBRANE_VERSION } from "./lib/version.js";
|
package/dist/lib/docker.d.ts
CHANGED
|
@@ -18,10 +18,10 @@ export type Upstream = UpstreamDefinition;
|
|
|
18
18
|
* with the key's fake, which is what a client in a dimension is given in place of the key.
|
|
19
19
|
*/
|
|
20
20
|
export type GatewayCredential = {
|
|
21
|
-
name: string;
|
|
22
|
-
gatewayName: string;
|
|
23
|
-
type: "oauth" | "apiKey";
|
|
24
|
-
fake?: string;
|
|
21
|
+
readonly name: string;
|
|
22
|
+
readonly gatewayName: string;
|
|
23
|
+
readonly type: "oauth" | "apiKey";
|
|
24
|
+
readonly fake?: string;
|
|
25
25
|
};
|
|
26
26
|
/**
|
|
27
27
|
* Applies to traffic arriving through the gateway's portals. "tcp://<address>:<port>" covers every
|
|
@@ -39,6 +39,7 @@ export type TrafficRule = {
|
|
|
39
39
|
action: "BLOCK" | "DELAY" | "ASK" | "INTERCEPT";
|
|
40
40
|
delayMs: number;
|
|
41
41
|
};
|
|
42
|
+
/** @sealed */
|
|
42
43
|
export interface PortalGatewayClient {
|
|
43
44
|
/** POST /v1/gateways — also replaces a container made from an older gateway image, keeping its state. */
|
|
44
45
|
createGateway(gateway: Gateway): Promise<Gateway>;
|
|
@@ -63,7 +64,7 @@ export interface PortalGatewayClient {
|
|
|
63
64
|
* to give the client in the dimension (e.g. as its API key env var) in place of the key.
|
|
64
65
|
*/
|
|
65
66
|
setApiKey(gatewayName: string, upstreamName: string, keyName: string, key: string): Promise<{
|
|
66
|
-
fake: string;
|
|
67
|
+
readonly fake: string;
|
|
67
68
|
}>;
|
|
68
69
|
/** DELETE /v1/gateways/:gatewayName/upstreams/:upstreamName/api-keys/:keyName */
|
|
69
70
|
deleteApiKey(gatewayName: string, upstreamName: string, keyName: string): Promise<void>;
|
|
@@ -4,6 +4,7 @@ import { type Sandbox } from "../sandbox/api.ts";
|
|
|
4
4
|
import { type Gateway, type GatewayCredential, type TrafficRule, type Upstream } from "./api.ts";
|
|
5
5
|
import type { GatewayStorage } from "./gatewayStorage.ts";
|
|
6
6
|
import type { PortalStorage } from "./portalStorage.ts";
|
|
7
|
+
/** @sealed */
|
|
7
8
|
export interface GatewayService {
|
|
8
9
|
createGateway(gateway: Gateway): Promise<Gateway>;
|
|
9
10
|
destroyGateway(gatewayName: string): Promise<void>;
|
|
@@ -4,6 +4,7 @@ import type { Sandbox } from "../sandbox/api.ts";
|
|
|
4
4
|
import { type Portal } from "./api.ts";
|
|
5
5
|
import type { GatewayStorage } from "./gatewayStorage.ts";
|
|
6
6
|
import type { PortalStorage } from "./portalStorage.ts";
|
|
7
|
+
/** @sealed */
|
|
7
8
|
export interface PortalService {
|
|
8
9
|
createPortal(portal: Portal): Promise<Portal>;
|
|
9
10
|
destroyPortal(portalName: string): Promise<void>;
|
|
@@ -38,6 +39,7 @@ export declare const OS_CA_BUNDLE = "/etc/ssl/certs/ca-certificates.crt";
|
|
|
38
39
|
* `image` is the gateway image: it carries the wiring script run by the privileged helper.
|
|
39
40
|
*/
|
|
40
41
|
export declare function createPortalService(storage: PortalStorage, gateways: GatewayStorage, docker: DockerClient, image: ManagedImage, host: Sandbox): PortalService;
|
|
42
|
+
/** @sealed */
|
|
41
43
|
export interface PortalWatcher {
|
|
42
44
|
/** Restores every portal, then keeps restoring those of each hyperbrane container that starts. */
|
|
43
45
|
start(): void;
|
package/dist/sandbox/api.d.ts
CHANGED
|
@@ -5,14 +5,16 @@ export type RunCommandOptions = {
|
|
|
5
5
|
stdin?: string | Uint8Array;
|
|
6
6
|
};
|
|
7
7
|
export type RunCommandResult = {
|
|
8
|
-
exitCode: number;
|
|
9
|
-
stdout: string;
|
|
10
|
-
stderr: string;
|
|
8
|
+
readonly exitCode: number;
|
|
9
|
+
readonly stdout: string;
|
|
10
|
+
readonly stderr: string;
|
|
11
11
|
};
|
|
12
12
|
export type SandboxFileInfo = {
|
|
13
|
-
type: "file" | "directory" | "other";
|
|
14
|
-
uid: number;
|
|
15
|
-
gid: number;
|
|
13
|
+
readonly type: "file" | "directory" | "other";
|
|
14
|
+
readonly uid: number;
|
|
15
|
+
readonly gid: number;
|
|
16
|
+
/** In bytes; what the system reports for anything that isn't a file. */
|
|
17
|
+
readonly size: number;
|
|
16
18
|
};
|
|
17
19
|
export type SandboxFetchInit = {
|
|
18
20
|
method?: string;
|
|
@@ -27,7 +29,11 @@ export type SandboxFetchInit = {
|
|
|
27
29
|
*/
|
|
28
30
|
timeout?: number | false;
|
|
29
31
|
};
|
|
30
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* A stream connection to a unix socket inside a sandbox (see Sandbox.connect).
|
|
34
|
+
*
|
|
35
|
+
* @sealed
|
|
36
|
+
*/
|
|
31
37
|
export type SandboxConnection = {
|
|
32
38
|
write(data: Uint8Array): void;
|
|
33
39
|
/** Receives every chunk read from the socket, in order. */
|
|
@@ -49,14 +55,14 @@ export type SandboxEndpointTarget = {
|
|
|
49
55
|
export type SandboxEndpointCredentials =
|
|
50
56
|
/** `Authorization: Bearer <token>`. */
|
|
51
57
|
{
|
|
52
|
-
scheme: "bearer";
|
|
53
|
-
token: string;
|
|
58
|
+
readonly scheme: "bearer";
|
|
59
|
+
readonly token: string;
|
|
54
60
|
}
|
|
55
61
|
/** `Authorization: Basic ...`; browsers prompt for it on their own. */
|
|
56
62
|
| {
|
|
57
|
-
scheme: "basic";
|
|
58
|
-
username: string;
|
|
59
|
-
password: string;
|
|
63
|
+
readonly scheme: "basic";
|
|
64
|
+
readonly username: string;
|
|
65
|
+
readonly password: string;
|
|
60
66
|
}
|
|
61
67
|
/**
|
|
62
68
|
* Logged in with exe.dev, as a user the VM is shared with; exe.dev's proxy checks it (exeDevAuth.ts).
|
|
@@ -64,8 +70,8 @@ export type SandboxEndpointCredentials =
|
|
|
64
70
|
* first (exeDevLoginUrl in agent/session/workerSseConnection.ts).
|
|
65
71
|
*/
|
|
66
72
|
| {
|
|
67
|
-
scheme: "exe.dev";
|
|
68
|
-
loginUrl: string;
|
|
73
|
+
readonly scheme: "exe.dev";
|
|
74
|
+
readonly loginUrl: string;
|
|
69
75
|
};
|
|
70
76
|
export type SandboxEndpointScheme = SandboxEndpointCredentials["scheme"];
|
|
71
77
|
export type SandboxEndpointRequest = {
|
|
@@ -88,24 +94,31 @@ export type SandboxEndpointRequest = {
|
|
|
88
94
|
allowedOrigins?: string[];
|
|
89
95
|
};
|
|
90
96
|
export type SandboxEndpoint = {
|
|
91
|
-
id: string;
|
|
97
|
+
readonly id: string;
|
|
92
98
|
/**
|
|
93
99
|
* Where the client reaches the endpoint, directly over the network (not through the backend's own
|
|
94
100
|
* connection), at the host the sandbox was created with.
|
|
95
101
|
*/
|
|
96
|
-
url: string;
|
|
102
|
+
readonly url: string;
|
|
97
103
|
/** What the client presents; requests without it are refused before reaching the target. */
|
|
98
|
-
credentials: SandboxEndpointCredentials;
|
|
104
|
+
readonly credentials: SandboxEndpointCredentials;
|
|
99
105
|
/** As requested: fixed for the endpoint's life. */
|
|
100
|
-
allowedOrigins: string[];
|
|
106
|
+
readonly allowedOrigins: string[];
|
|
101
107
|
};
|
|
102
108
|
/**
|
|
103
109
|
* Paths are sandbox paths: relative or absolute, both resolved against the sandbox root.
|
|
104
110
|
* Operations that report a missing path throw SandboxNotFoundError, whatever the backend.
|
|
111
|
+
*
|
|
112
|
+
* @sealed
|
|
105
113
|
*/
|
|
106
114
|
export interface SandboxFileOperations {
|
|
107
115
|
/** Throws SandboxNotFoundError if missing. */
|
|
108
116
|
readFile(path: string): Promise<Uint8Array>;
|
|
117
|
+
/**
|
|
118
|
+
* Bytes `start` up to `end` (exclusive) of the file, fewer where the file ends sooner, so a large
|
|
119
|
+
* file can be read a part at a time. Throws SandboxNotFoundError if missing.
|
|
120
|
+
*/
|
|
121
|
+
readFileRange(path: string, start: number, end: number): Promise<Uint8Array>;
|
|
109
122
|
/** Atomically replaces the file (readers never see partial content). Creates missing parent directories. */
|
|
110
123
|
writeFile(path: string, data: string | Uint8Array): Promise<void>;
|
|
111
124
|
/**
|
|
@@ -126,10 +139,15 @@ export interface SandboxFileOperations {
|
|
|
126
139
|
/** Sandbox path of the home directory of the user operations run as. */
|
|
127
140
|
homeDirectory(): Promise<string>;
|
|
128
141
|
}
|
|
142
|
+
/** @sealed */
|
|
129
143
|
export interface SandboxCommands {
|
|
130
144
|
runCommand(cmd: string[], options?: RunCommandOptions): Promise<RunCommandResult>;
|
|
131
145
|
}
|
|
132
|
-
/**
|
|
146
|
+
/**
|
|
147
|
+
* Reaching into the sandbox from the caller's side.
|
|
148
|
+
*
|
|
149
|
+
* @sealed
|
|
150
|
+
*/
|
|
133
151
|
export interface SandboxNetwork {
|
|
134
152
|
/** HTTP request made from inside the sandbox. */
|
|
135
153
|
fetch(url: string, init?: SandboxFetchInit): Promise<Response>;
|
|
@@ -142,6 +160,8 @@ export interface SandboxNetwork {
|
|
|
142
160
|
* clients reach it at), by the backends that can. Always authenticated: a scheme the sandbox's auth
|
|
143
161
|
* can't enforce is refused with SandboxEndpointError. An endpoint lasts until it is closed, past the
|
|
144
162
|
* sandbox object that exposed it.
|
|
163
|
+
*
|
|
164
|
+
* @sealed
|
|
145
165
|
*/
|
|
146
166
|
export interface SandboxAccessibleEndpoint {
|
|
147
167
|
/**
|
|
@@ -158,7 +178,11 @@ export interface SandboxAccessibleEndpoint {
|
|
|
158
178
|
*/
|
|
159
179
|
ensureEndpoint(endpointId: string): Promise<boolean>;
|
|
160
180
|
}
|
|
161
|
-
/**
|
|
181
|
+
/**
|
|
182
|
+
* A sandbox that stops itself on a timer (Boat).
|
|
183
|
+
*
|
|
184
|
+
* @sealed
|
|
185
|
+
*/
|
|
162
186
|
export interface SandboxExpiration {
|
|
163
187
|
/** When it stops itself; null if it never does. */
|
|
164
188
|
getExpiration(): Promise<Date | null>;
|
|
@@ -167,7 +191,11 @@ export interface SandboxExpiration {
|
|
|
167
191
|
}
|
|
168
192
|
/** Whether the sandbox has the SandboxExpiration capability. */
|
|
169
193
|
export declare function hasExpiration<T extends object>(sandbox: T): sandbox is T & SandboxExpiration;
|
|
170
|
-
/**
|
|
194
|
+
/**
|
|
195
|
+
* What every backend provides.
|
|
196
|
+
*
|
|
197
|
+
* @sealed
|
|
198
|
+
*/
|
|
171
199
|
export type Sandbox = SandboxFileOperations & SandboxCommands & SandboxNetwork & {
|
|
172
200
|
/** Releases what the sandbox holds open (a remote connection). Optional: most hold nothing. */
|
|
173
201
|
close?(): void;
|
|
@@ -103,6 +103,13 @@ export function createBoatSandbox(api, sandboxId, { proxyPort = DEFAULT_PROXY_PO
|
|
|
103
103
|
throw error;
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
|
+
// Boat's file call reads whole files only.
|
|
107
|
+
async function readFileRange(path, start, end) {
|
|
108
|
+
const target = full(path);
|
|
109
|
+
const result = await sh(`[ -e "$1" ] || exit ${NOT_FOUND}; tail -c +$(($2 + 1)) -- "$1" | head -c "$3" | base64 -w0`, target, String(start), String(Math.max(0, end - start)));
|
|
110
|
+
check(result, "read", target);
|
|
111
|
+
return new Uint8Array(Buffer.from(result.stdout, "base64"));
|
|
112
|
+
}
|
|
106
113
|
/**
|
|
107
114
|
* Uploads `data`, copies it beside `target` (creating its directory) and runs `publish` on the copy
|
|
108
115
|
* ($3) and the target ($2), so readers never see part of it.
|
|
@@ -140,11 +147,11 @@ export function createBoatSandbox(api, sandboxId, { proxyPort = DEFAULT_PROXY_PO
|
|
|
140
147
|
return result.stdout.split("\n").filter(Boolean);
|
|
141
148
|
}
|
|
142
149
|
async function stat(path) {
|
|
143
|
-
const result = await sh(`[ -e "$1" ] || exit ${NOT_FOUND}; stat -c '%u:%g:%F' -- "$1"`, full(path));
|
|
150
|
+
const result = await sh(`[ -e "$1" ] || exit ${NOT_FOUND}; stat -c '%u:%g:%s:%F' -- "$1"`, full(path));
|
|
144
151
|
check(result, "stat", full(path));
|
|
145
|
-
const [uid = "", gid = "", kind = ""] = result.stdout.trim().split(":");
|
|
152
|
+
const [uid = "", gid = "", size = "", kind = ""] = result.stdout.trim().split(":");
|
|
146
153
|
const type = kind === "directory" ? "directory" : kind.startsWith("regular") ? "file" : "other";
|
|
147
|
-
return { type, uid: Number(uid), gid: Number(gid) };
|
|
154
|
+
return { type, uid: Number(uid), gid: Number(gid), size: Number(size) };
|
|
148
155
|
}
|
|
149
156
|
let home = null;
|
|
150
157
|
function homeDirectory() {
|
|
@@ -233,6 +240,7 @@ export function createBoatSandbox(api, sandboxId, { proxyPort = DEFAULT_PROXY_PO
|
|
|
233
240
|
getExpiration,
|
|
234
241
|
setExpiration,
|
|
235
242
|
readFile,
|
|
243
|
+
readFileRange,
|
|
236
244
|
writeFile,
|
|
237
245
|
writeFileExclusive,
|
|
238
246
|
removeFile,
|
|
@@ -38,6 +38,12 @@ export function createContainerSandbox(docker, container, { user } = {}) {
|
|
|
38
38
|
const result = check(await sh(`[ -e "$P" ] || exit ${NOT_FOUND}; base64 -w0 -- "$P"`, { P: full }), "read", full);
|
|
39
39
|
return new Uint8Array(Buffer.from(result.stdout, "base64"));
|
|
40
40
|
}
|
|
41
|
+
async function readFileRange(path, start, end) {
|
|
42
|
+
const full = containerPath(path);
|
|
43
|
+
const script = `[ -e "$P" ] || exit ${NOT_FOUND}; tail -c +$(($S + 1)) -- "$P" | head -c "$L" | base64 -w0`;
|
|
44
|
+
const result = check(await sh(script, { P: full, S: String(start), L: String(Math.max(0, end - start)) }), "read", full);
|
|
45
|
+
return new Uint8Array(Buffer.from(result.stdout, "base64"));
|
|
46
|
+
}
|
|
41
47
|
/**
|
|
42
48
|
* Extracts `data` next to `full` under a unique hidden name, handed to the sandbox user, and runs
|
|
43
49
|
* `publish` (as root, with $T the temp file and $F the target) to move it into place.
|
|
@@ -81,10 +87,10 @@ export function createContainerSandbox(docker, container, { user } = {}) {
|
|
|
81
87
|
}
|
|
82
88
|
async function stat(path) {
|
|
83
89
|
const full = containerPath(path);
|
|
84
|
-
const result = check(await sh(`[ -e "$P" ] || exit ${NOT_FOUND}; stat -c '%u:%g:%F' -- "$P"`, { P: full }), "stat", full);
|
|
85
|
-
const [uid = "", gid = "", kind = ""] = result.stdout.trim().split(":");
|
|
90
|
+
const result = check(await sh(`[ -e "$P" ] || exit ${NOT_FOUND}; stat -c '%u:%g:%s:%F' -- "$P"`, { P: full }), "stat", full);
|
|
91
|
+
const [uid = "", gid = "", size = "", kind = ""] = result.stdout.trim().split(":");
|
|
86
92
|
const type = kind === "directory" ? "directory" : kind.startsWith("regular") ? "file" : "other";
|
|
87
|
-
return { type, uid: Number(uid), gid: Number(gid) };
|
|
93
|
+
return { type, uid: Number(uid), gid: Number(gid), size: Number(size) };
|
|
88
94
|
}
|
|
89
95
|
// docker exec doesn't always set HOME for a numeric user, so ask the user database.
|
|
90
96
|
async function homeDirectory() {
|
|
@@ -136,6 +142,7 @@ export function createContainerSandbox(docker, container, { user } = {}) {
|
|
|
136
142
|
}
|
|
137
143
|
return {
|
|
138
144
|
readFile,
|
|
145
|
+
readFileRange,
|
|
139
146
|
writeFile,
|
|
140
147
|
writeFileExclusive,
|
|
141
148
|
removeFile,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { link, mkdir, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { link, mkdir, open, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
3
3
|
import { request as httpRequest } from "node:http";
|
|
4
4
|
import { createConnection } from "node:net";
|
|
5
5
|
import { homedir } from "node:os";
|
|
@@ -36,6 +36,23 @@ export function createLocalSandbox(root, { endpoints } = {}) {
|
|
|
36
36
|
async function readFileLocal(path) {
|
|
37
37
|
return new Uint8Array(await notFoundAs(path, readFile(hostPath(path))));
|
|
38
38
|
}
|
|
39
|
+
async function readFileRangeLocal(path, start, end) {
|
|
40
|
+
const file = await notFoundAs(path, open(hostPath(path), "r"));
|
|
41
|
+
try {
|
|
42
|
+
const buffer = new Uint8Array(Math.max(0, end - start));
|
|
43
|
+
let filled = 0;
|
|
44
|
+
while (filled < buffer.length) {
|
|
45
|
+
const { bytesRead } = await file.read(buffer, filled, buffer.length - filled, start + filled);
|
|
46
|
+
if (bytesRead === 0)
|
|
47
|
+
break;
|
|
48
|
+
filled += bytesRead;
|
|
49
|
+
}
|
|
50
|
+
return buffer.subarray(0, filled);
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
await file.close();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
39
56
|
async function writeFileLocal(path, data) {
|
|
40
57
|
const full = hostPath(path);
|
|
41
58
|
await rename(await writeTemp(full, data), full);
|
|
@@ -72,7 +89,7 @@ export function createLocalSandbox(root, { endpoints } = {}) {
|
|
|
72
89
|
async function statLocal(path) {
|
|
73
90
|
const stats = await notFoundAs(path, stat(hostPath(path)));
|
|
74
91
|
const type = stats.isFile() ? "file" : stats.isDirectory() ? "directory" : "other";
|
|
75
|
-
return { type, uid: stats.uid, gid: stats.gid };
|
|
92
|
+
return { type, uid: stats.uid, gid: stats.gid, size: stats.size };
|
|
76
93
|
}
|
|
77
94
|
async function homeDirectory() {
|
|
78
95
|
const rel = relative(base, homedir());
|
|
@@ -112,6 +129,7 @@ export function createLocalSandbox(root, { endpoints } = {}) {
|
|
|
112
129
|
}
|
|
113
130
|
const sandbox = {
|
|
114
131
|
readFile: readFileLocal,
|
|
132
|
+
readFileRange: readFileRangeLocal,
|
|
115
133
|
writeFile: writeFileLocal,
|
|
116
134
|
writeFileExclusive,
|
|
117
135
|
removeFile,
|
|
@@ -5,6 +5,7 @@ import { createProxyEndpoints } from "./endpointProxy.js";
|
|
|
5
5
|
import { socketConnection } from "./localService.js";
|
|
6
6
|
/** SFTP status codes (draft-ietf-secsh-filexfer-02). */
|
|
7
7
|
const SFTP_NO_SUCH_FILE = 2;
|
|
8
|
+
const SFTP_EOF = 1;
|
|
8
9
|
/**
|
|
9
10
|
* Commands running at once on the connection. sshd refuses sessions past its MaxSessions (10 by
|
|
10
11
|
* default) per connection, and counts a closed one until it has cleaned up after it, so this stays
|
|
@@ -181,6 +182,30 @@ export function createSshSandbox(host, options) {
|
|
|
181
182
|
const data = await sftpCall("read", full, (channel, done) => channel.readFile(full, done));
|
|
182
183
|
return new Uint8Array(data);
|
|
183
184
|
}
|
|
185
|
+
async function sftpReadFileRange(path, start, end) {
|
|
186
|
+
const full = remotePath(path);
|
|
187
|
+
const channel = await sftp();
|
|
188
|
+
const handle = await sftpCall("read", full, (sftpChannel, done) => sftpChannel.open(full, "r", done));
|
|
189
|
+
try {
|
|
190
|
+
const buffer = Buffer.alloc(Math.max(0, end - start));
|
|
191
|
+
let filled = 0;
|
|
192
|
+
while (filled < buffer.length) {
|
|
193
|
+
const read = await new Promise((resolve, reject) => channel.read(handle, buffer, filled, buffer.length - filled, start + filled, (error, bytesRead) => {
|
|
194
|
+
// The end of the file comes as an EOF status rather than a short read.
|
|
195
|
+
if (error && error.code !== SFTP_EOF)
|
|
196
|
+
return reject(new Error(`read ${full} on ${host} failed: ${error.message}`));
|
|
197
|
+
resolve(error ? 0 : bytesRead);
|
|
198
|
+
}));
|
|
199
|
+
if (read === 0)
|
|
200
|
+
break;
|
|
201
|
+
filled += read;
|
|
202
|
+
}
|
|
203
|
+
return new Uint8Array(buffer.subarray(0, filled));
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
channel.close(handle, () => { });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
184
209
|
/** Writes `data` to a unique hidden file next to `full` (creating its directory) and returns its path. */
|
|
185
210
|
async function writeTemp(full, data) {
|
|
186
211
|
await createDirectory(posix.dirname(full));
|
|
@@ -268,7 +293,7 @@ export function createSshSandbox(host, options) {
|
|
|
268
293
|
async function sftpStat(path) {
|
|
269
294
|
const info = await statOf(remotePath(path));
|
|
270
295
|
const type = info.isDirectory() ? "directory" : info.isFile() ? "file" : "other";
|
|
271
|
-
return { type, uid: info.uid, gid: info.gid };
|
|
296
|
+
return { type, uid: info.uid, gid: info.gid, size: info.size };
|
|
272
297
|
}
|
|
273
298
|
// SFTP starts in the user's home directory. Asked once: storage paths start from it on every call.
|
|
274
299
|
let home = null;
|
|
@@ -301,6 +326,12 @@ export function createSshSandbox(host, options) {
|
|
|
301
326
|
checkShell(result, "read", full);
|
|
302
327
|
return new Uint8Array(result.stdout);
|
|
303
328
|
},
|
|
329
|
+
async readFileRange(full, start, end) {
|
|
330
|
+
const script = `[ -e "$P" ] || exit ${NOT_FOUND}; tail -c +$(($S + 1)) -- "$P" | head -c "$L"`;
|
|
331
|
+
const result = await runBytes(commandLine(["sh", "-c", script], { P: full, S: String(start), L: String(Math.max(0, end - start)) }));
|
|
332
|
+
checkShell(result, "read", full);
|
|
333
|
+
return new Uint8Array(result.stdout);
|
|
334
|
+
},
|
|
304
335
|
/** Streams `data` into a temp file beside `full`, then runs `publish` ($T the temp file, $F the target). */
|
|
305
336
|
writeVia(full, data, publish) {
|
|
306
337
|
const tmp = posix.join(posix.dirname(full), `.${posix.basename(full)}.${crypto.randomUUID()}.tmp`);
|
|
@@ -329,11 +360,11 @@ export function createSshSandbox(host, options) {
|
|
|
329
360
|
return result.stdout.split("\n").filter(Boolean);
|
|
330
361
|
},
|
|
331
362
|
async stat(full) {
|
|
332
|
-
const result = await sh(`[ -e "$P" ] || exit ${NOT_FOUND}; stat -c '%u:%g:%F' -- "$P"`, { P: full });
|
|
363
|
+
const result = await sh(`[ -e "$P" ] || exit ${NOT_FOUND}; stat -c '%u:%g:%s:%F' -- "$P"`, { P: full });
|
|
333
364
|
checkShell(result, "stat", full);
|
|
334
|
-
const [uid = "", gid = "", kind = ""] = result.stdout.trim().split(":");
|
|
365
|
+
const [uid = "", gid = "", size = "", kind = ""] = result.stdout.trim().split(":");
|
|
335
366
|
const type = kind === "directory" ? "directory" : kind.startsWith("regular") ? "file" : "other";
|
|
336
|
-
return { type, uid: Number(uid), gid: Number(gid) };
|
|
367
|
+
return { type, uid: Number(uid), gid: Number(gid), size: Number(size) };
|
|
337
368
|
},
|
|
338
369
|
// sshd always sets HOME for the session.
|
|
339
370
|
async homeDirectory() {
|
|
@@ -343,6 +374,7 @@ export function createSshSandbox(host, options) {
|
|
|
343
374
|
},
|
|
344
375
|
};
|
|
345
376
|
const readFile = async (path) => ((await sftpAvailable()) ? sftpReadFile(path) : shell.readFile(remotePath(path)));
|
|
377
|
+
const readFileRange = async (path, start, end) => (await sftpAvailable()) ? sftpReadFileRange(path, start, end) : shell.readFileRange(remotePath(path), start, end);
|
|
346
378
|
const writeFile = async (path, data) => (await sftpAvailable()) ? sftpWriteFile(path, data) : shell.writeFile(remotePath(path), data);
|
|
347
379
|
const writeFileExclusive = async (path, data) => (await sftpAvailable()) ? sftpWriteFileExclusive(path, data) : shell.writeFileExclusive(remotePath(path), data);
|
|
348
380
|
const removeFile = async (path) => ((await sftpAvailable()) ? sftpRemoveFile(path) : shell.removeFile(remotePath(path)));
|
|
@@ -592,6 +624,7 @@ export function createSshSandbox(host, options) {
|
|
|
592
624
|
}
|
|
593
625
|
const sandbox = {
|
|
594
626
|
readFile,
|
|
627
|
+
readFileRange,
|
|
595
628
|
writeFile,
|
|
596
629
|
writeFileExclusive,
|
|
597
630
|
removeFile,
|