@aerol-ai/aerolvm-sdk 0.1.7 → 0.1.9
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/dist/Image.d.ts +37 -0
- package/dist/Image.js +107 -0
- package/dist/MicroVM.d.ts +10 -1
- package/dist/MicroVM.js +12 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/internal/api/v1/paths.js +3 -3
- package/dist/internal/client.d.ts +34 -6
- package/dist/internal/client.js +107 -2
- package/dist/types.d.ts +77 -1
- package/package.json +3 -2
package/dist/Image.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image is a fluent Dockerfile builder. The shape mirrors the Daytona SDK's
|
|
3
|
+
* Image API so examples port across by changing the import. The output is a
|
|
4
|
+
* Dockerfile string accessible via `.dockerfile` — the SDK ships it to
|
|
5
|
+
* `POST /v1/images/build` when an Image is passed to `MicroVM.create`.
|
|
6
|
+
*
|
|
7
|
+
* Caller-side context (COPY/ADD from local files) needs the
|
|
8
|
+
* SB_IMAGE_BUILD_CONTEXT_ENABLED operator flag on the daemon. Without that,
|
|
9
|
+
* use only `runCommands`, `env`, `workdir`, `entrypoint`, `cmd`, `user`, and
|
|
10
|
+
* `expose` — every step they emit is RUN/ENV/WORKDIR/etc. against the base
|
|
11
|
+
* image, no local files involved.
|
|
12
|
+
*/
|
|
13
|
+
export declare class Image {
|
|
14
|
+
private _dockerfile;
|
|
15
|
+
private constructor();
|
|
16
|
+
get dockerfile(): string;
|
|
17
|
+
/** Start a new image from `FROM <image>`. */
|
|
18
|
+
static base(image: string): Image;
|
|
19
|
+
/**
|
|
20
|
+
* Build from a raw Dockerfile string. Use when you need full control
|
|
21
|
+
* (multi-stage builds, ARG, ONBUILD, HEALTHCHECK, etc.) that the fluent
|
|
22
|
+
* API doesn't expose. Caller is responsible for the Dockerfile validity.
|
|
23
|
+
*/
|
|
24
|
+
static fromDockerfile(dockerfile: string): Image;
|
|
25
|
+
/**
|
|
26
|
+
* Append one or more RUN steps. Each argument becomes its own RUN line;
|
|
27
|
+
* an array argument is joined with `&&` so the commands share one layer.
|
|
28
|
+
*/
|
|
29
|
+
runCommands(...commands: (string | string[])[]): Image;
|
|
30
|
+
/** Append ENV K=V pairs (one ENV line per call, all vars on it). */
|
|
31
|
+
env(envVars: Record<string, string>): Image;
|
|
32
|
+
workdir(dirPath: string): Image;
|
|
33
|
+
entrypoint(entrypointCommands: string[]): Image;
|
|
34
|
+
cmd(cmd: string[]): Image;
|
|
35
|
+
user(username: string): Image;
|
|
36
|
+
expose(port: number): Image;
|
|
37
|
+
}
|
package/dist/Image.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image is a fluent Dockerfile builder. The shape mirrors the Daytona SDK's
|
|
3
|
+
* Image API so examples port across by changing the import. The output is a
|
|
4
|
+
* Dockerfile string accessible via `.dockerfile` — the SDK ships it to
|
|
5
|
+
* `POST /v1/images/build` when an Image is passed to `MicroVM.create`.
|
|
6
|
+
*
|
|
7
|
+
* Caller-side context (COPY/ADD from local files) needs the
|
|
8
|
+
* SB_IMAGE_BUILD_CONTEXT_ENABLED operator flag on the daemon. Without that,
|
|
9
|
+
* use only `runCommands`, `env`, `workdir`, `entrypoint`, `cmd`, `user`, and
|
|
10
|
+
* `expose` — every step they emit is RUN/ENV/WORKDIR/etc. against the base
|
|
11
|
+
* image, no local files involved.
|
|
12
|
+
*/
|
|
13
|
+
export class Image {
|
|
14
|
+
_dockerfile = "";
|
|
15
|
+
constructor(dockerfile) {
|
|
16
|
+
this._dockerfile = dockerfile;
|
|
17
|
+
}
|
|
18
|
+
get dockerfile() {
|
|
19
|
+
return this._dockerfile;
|
|
20
|
+
}
|
|
21
|
+
/** Start a new image from `FROM <image>`. */
|
|
22
|
+
static base(image) {
|
|
23
|
+
if (typeof image !== "string" || image.trim() === "") {
|
|
24
|
+
throw new TypeError("Image.base requires a non-empty image string");
|
|
25
|
+
}
|
|
26
|
+
return new Image(`FROM ${image.trim()}\n`);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Build from a raw Dockerfile string. Use when you need full control
|
|
30
|
+
* (multi-stage builds, ARG, ONBUILD, HEALTHCHECK, etc.) that the fluent
|
|
31
|
+
* API doesn't expose. Caller is responsible for the Dockerfile validity.
|
|
32
|
+
*/
|
|
33
|
+
static fromDockerfile(dockerfile) {
|
|
34
|
+
if (typeof dockerfile !== "string" || dockerfile.trim() === "") {
|
|
35
|
+
throw new TypeError("Image.fromDockerfile requires a non-empty Dockerfile string");
|
|
36
|
+
}
|
|
37
|
+
return new Image(dockerfile.endsWith("\n") ? dockerfile : dockerfile + "\n");
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Append one or more RUN steps. Each argument becomes its own RUN line;
|
|
41
|
+
* an array argument is joined with `&&` so the commands share one layer.
|
|
42
|
+
*/
|
|
43
|
+
runCommands(...commands) {
|
|
44
|
+
for (const entry of commands) {
|
|
45
|
+
if (Array.isArray(entry)) {
|
|
46
|
+
const joined = entry.map((c) => c.trim()).filter(Boolean).join(" && ");
|
|
47
|
+
if (joined !== "") {
|
|
48
|
+
this._dockerfile += `RUN ${joined}\n`;
|
|
49
|
+
}
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const cmd = entry.trim();
|
|
53
|
+
if (cmd !== "") {
|
|
54
|
+
this._dockerfile += `RUN ${cmd}\n`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
/** Append ENV K=V pairs (one ENV line per call, all vars on it). */
|
|
60
|
+
env(envVars) {
|
|
61
|
+
const parts = Object.entries(envVars).map(([k, v]) => `${k}=${dockerQuote(v)}`);
|
|
62
|
+
if (parts.length > 0) {
|
|
63
|
+
this._dockerfile += `ENV ${parts.join(" ")}\n`;
|
|
64
|
+
}
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
workdir(dirPath) {
|
|
68
|
+
if (dirPath.trim() === "") {
|
|
69
|
+
throw new TypeError("Image.workdir requires a non-empty path");
|
|
70
|
+
}
|
|
71
|
+
this._dockerfile += `WORKDIR ${dirPath}\n`;
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
entrypoint(entrypointCommands) {
|
|
75
|
+
this._dockerfile += `ENTRYPOINT ${jsonExecForm(entrypointCommands)}\n`;
|
|
76
|
+
return this;
|
|
77
|
+
}
|
|
78
|
+
cmd(cmd) {
|
|
79
|
+
this._dockerfile += `CMD ${jsonExecForm(cmd)}\n`;
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
user(username) {
|
|
83
|
+
if (username.trim() === "") {
|
|
84
|
+
throw new TypeError("Image.user requires a non-empty username");
|
|
85
|
+
}
|
|
86
|
+
this._dockerfile += `USER ${username}\n`;
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
expose(port) {
|
|
90
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
91
|
+
throw new RangeError(`Image.expose: port ${port} is out of range`);
|
|
92
|
+
}
|
|
93
|
+
this._dockerfile += `EXPOSE ${port}\n`;
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function dockerQuote(value) {
|
|
98
|
+
// Quote when the value contains whitespace or a special char; otherwise
|
|
99
|
+
// leave bare to match canonical Dockerfile style.
|
|
100
|
+
if (/^[A-Za-z0-9_\-./:@]+$/.test(value)) {
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
104
|
+
}
|
|
105
|
+
function jsonExecForm(parts) {
|
|
106
|
+
return JSON.stringify(parts);
|
|
107
|
+
}
|
package/dist/MicroVM.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type APIVersion } from "./internal/client.js";
|
|
2
2
|
import { Sandbox } from "./Sandbox.js";
|
|
3
|
-
import
|
|
3
|
+
import { Image } from "./Image.js";
|
|
4
|
+
import type { BuildImageOptions, BuildImageResult, CreateOptions, CreateSessionOptions, ExecStreamHandle, ExecStreamOptions, HealthStatus, Lifecycle, MountSpecRedacted, ResizeOptions, SandboxSnapshot, Session, SessionAttachHandle, SessionAttachOptions } from "./types.js";
|
|
4
5
|
type FetchLike = typeof fetch;
|
|
5
6
|
export interface MicroVMConfig {
|
|
6
7
|
patToken?: string;
|
|
@@ -24,10 +25,18 @@ export declare class MicroVM {
|
|
|
24
25
|
get(id: string): Promise<Sandbox>;
|
|
25
26
|
start(id: string): Promise<Sandbox>;
|
|
26
27
|
stop(id: string): Promise<Sandbox>;
|
|
28
|
+
createSnapshot(id: string, name: string): Promise<SandboxSnapshot>;
|
|
27
29
|
destroy(id: string): Promise<void>;
|
|
28
30
|
resize(id: string, options: ResizeOptions): Promise<Sandbox>;
|
|
29
31
|
updateLifecycle(id: string, lifecycle: Lifecycle): Promise<Sandbox>;
|
|
30
32
|
reconcile(): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Build an {@link Image} on the daemon and optionally push the result to a
|
|
35
|
+
* remote registry. Push credentials are forwarded per request and never
|
|
36
|
+
* persisted by the daemon. Returns the local content-addressed tag and,
|
|
37
|
+
* when push was requested, the pushed reference.
|
|
38
|
+
*/
|
|
39
|
+
buildImage(image: Image, options?: BuildImageOptions): Promise<BuildImageResult>;
|
|
31
40
|
health(): Promise<HealthStatus>;
|
|
32
41
|
mounts(sandboxID: string): Promise<MountSpecRedacted[]>;
|
|
33
42
|
execStream(sandboxID: string, options: ExecStreamOptions): ExecStreamHandle;
|
package/dist/MicroVM.js
CHANGED
|
@@ -41,6 +41,9 @@ export class MicroVM {
|
|
|
41
41
|
const sandbox = await this.client.stop(id);
|
|
42
42
|
return this.wrap(sandbox.toJSON());
|
|
43
43
|
}
|
|
44
|
+
async createSnapshot(id, name) {
|
|
45
|
+
return this.client.createSnapshot(id, name);
|
|
46
|
+
}
|
|
44
47
|
async destroy(id) {
|
|
45
48
|
await this.client.destroy(id);
|
|
46
49
|
}
|
|
@@ -55,6 +58,15 @@ export class MicroVM {
|
|
|
55
58
|
async reconcile() {
|
|
56
59
|
await this.client.reconcile();
|
|
57
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Build an {@link Image} on the daemon and optionally push the result to a
|
|
63
|
+
* remote registry. Push credentials are forwarded per request and never
|
|
64
|
+
* persisted by the daemon. Returns the local content-addressed tag and,
|
|
65
|
+
* when push was requested, the pushed reference.
|
|
66
|
+
*/
|
|
67
|
+
async buildImage(image, options) {
|
|
68
|
+
return this.client.buildImage(image, options);
|
|
69
|
+
}
|
|
58
70
|
async health() {
|
|
59
71
|
return this.client.health();
|
|
60
72
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { MicroVM } from "./MicroVM.js";
|
|
2
2
|
export { Sandbox } from "./Sandbox.js";
|
|
3
|
+
export { Image } from "./Image.js";
|
|
3
4
|
export type { MicroVMConfig } from "./MicroVM.js";
|
|
4
|
-
export type { BinaryLike, CreateOptions, CreateSessionOptions, ExecRequest, ExecResult, ExecExitInfo, ExposedPort, ExposePortOptions, ExposeProtocol, ExposeResult, HealthStatus, Lifecycle, MountSpec, MountSpecRedacted, MountType, RegistryAuth, ResizeOptions, Sandbox as SandboxData, SandboxStatus, Session, SessionAttachHandle, SessionAttachOptions, SessionStatus, UpdateLifecycleOptions, } from "./types.js";
|
|
5
|
+
export type { BinaryLike, BuildImageOptions, BuildImagePushOptions, BuildImageResult, CreateOptions, CreateSessionOptions, ExecRequest, ExecResult, ExecExitInfo, ExposedPort, ExposePortOptions, ExposeProtocol, ExposeResult, HealthStatus, Lifecycle, MountSpec, MountSpecRedacted, MountType, RegistryAuth, ResizeOptions, Sandbox as SandboxData, SandboxSnapshot, SandboxStatus, Session, SessionAttachHandle, SessionAttachOptions, SessionStatus, UpdateLifecycleOptions, } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// v1 path-prefix constant. The TypeScript SDK uses this to build URLs against
|
|
2
|
-
// the v1 surface of the sandbox daemon. When
|
|
3
|
-
// internal/api/
|
|
4
|
-
// constructor's apiVersion option selects which one to use.
|
|
2
|
+
// the v1 surface of the sandbox daemon. When a new wire version lands, a
|
|
3
|
+
// sibling internal/api/vN/paths.ts will export its own PATH_PREFIX, and the
|
|
4
|
+
// client constructor's apiVersion option selects which one to use.
|
|
5
5
|
//
|
|
6
6
|
// Keep this in sync with pkg/api/v1/dto.go::PathPrefix on the server.
|
|
7
7
|
export const PATH_PREFIX = "/v1";
|
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Image } from "../Image.js";
|
|
2
|
+
import type { BinaryLike, BuildImageOptions, BuildImageResult, CreateOptions, CreateSessionOptions, ExecRequest, ExecResult, ExecStreamHandle, ExecStreamOptions, ExposedPort, ExposePortOptions, ExposeResult, HealthStatus, Lifecycle, MountSpecRedacted, NetworkUsage, SetNetworkLimitsOptions, ResizeOptions, Sandbox, SandboxSnapshot, Session, SessionAttachHandle, SessionAttachOptions } from "../types.js";
|
|
2
3
|
type FetchLike = typeof fetch;
|
|
3
4
|
/**
|
|
4
|
-
* Wire version of the sandbox
|
|
5
|
-
*
|
|
6
|
-
* wire version. Today only "v1" exists;
|
|
7
|
-
* breaking
|
|
5
|
+
* Wire version of the v1-style sandbox endpoints (`/v1/sandboxes`, etc.) this
|
|
6
|
+
* client speaks. The SDK and the server version independently — bumping the
|
|
7
|
+
* SDK package does not move the wire version. Today only "v1" exists; future
|
|
8
|
+
* breaking changes to the sandbox endpoints will land in a new version
|
|
9
|
+
* prefix.
|
|
10
|
+
*
|
|
11
|
+
* The `/v1/images/build` endpoint used by {@link Image}-shaped image inputs
|
|
12
|
+
* is called unconditionally when an Image is supplied to
|
|
13
|
+
* {@link APIClient.create}, regardless of `apiVersion`. Daemons older than
|
|
14
|
+
* the image-build feature return 404; the SDK turns that into a clear
|
|
15
|
+
* "daemon does not support image builds" error so the user can switch to a
|
|
16
|
+
* string image.
|
|
8
17
|
*/
|
|
9
18
|
export type APIVersion = "v1";
|
|
10
19
|
export interface APIClientConfig {
|
|
@@ -28,15 +37,29 @@ export declare class APIClient {
|
|
|
28
37
|
/**
|
|
29
38
|
* Build a versioned API path. Pass the suffix beginning with "/" (e.g.
|
|
30
39
|
* "/sandboxes") and the active version's prefix is prepended. Use this for
|
|
31
|
-
* every versioned API call so
|
|
40
|
+
* every versioned API call so a future wire version can be selected by the
|
|
32
41
|
* apiVersion option without touching call sites.
|
|
33
42
|
*/
|
|
34
43
|
private versioned;
|
|
35
44
|
create(options: CreateOptions): Promise<SandboxResource>;
|
|
45
|
+
/**
|
|
46
|
+
* Compile a fluent {@link Image} into a content-addressed image tag by
|
|
47
|
+
* POSTing its Dockerfile to `/v1/images/build`. The daemon caches by
|
|
48
|
+
* content hash, so repeated calls with the same Dockerfile are a no-op.
|
|
49
|
+
* String images are passed through unchanged.
|
|
50
|
+
*
|
|
51
|
+
* Daemons predating the image-build feature do not register the route and
|
|
52
|
+
* return 404; this method translates that into a clear error telling the
|
|
53
|
+
* caller to pass a string image instead, rather than the generic "request
|
|
54
|
+
* failed with status 404" the JSON decoder would otherwise produce.
|
|
55
|
+
*/
|
|
56
|
+
buildImage(image: Image, options?: BuildImageOptions): Promise<BuildImageResult>;
|
|
57
|
+
private resolveImage;
|
|
36
58
|
list(): Promise<SandboxResource[]>;
|
|
37
59
|
get(id: string): Promise<SandboxResource>;
|
|
38
60
|
start(id: string): Promise<SandboxResource>;
|
|
39
61
|
stop(id: string): Promise<SandboxResource>;
|
|
62
|
+
createSnapshot(id: string, name: string): Promise<SandboxSnapshot>;
|
|
40
63
|
destroy(id: string): Promise<void>;
|
|
41
64
|
resize(id: string, options: ResizeOptions): Promise<SandboxResource>;
|
|
42
65
|
updateLifecycle(id: string, lifecycle: Lifecycle): Promise<SandboxResource>;
|
|
@@ -71,6 +94,8 @@ export declare class APIClient {
|
|
|
71
94
|
reconcile(): Promise<void>;
|
|
72
95
|
health(): Promise<HealthStatus>;
|
|
73
96
|
mounts(id: string): Promise<MountSpecRedacted[]>;
|
|
97
|
+
getNetworkUsage(id: string): Promise<NetworkUsage>;
|
|
98
|
+
setNetworkLimits(id: string, options: SetNetworkLimitsOptions): Promise<NetworkUsage>;
|
|
74
99
|
private wrap;
|
|
75
100
|
private doJSON;
|
|
76
101
|
private doBytes;
|
|
@@ -118,11 +143,14 @@ export declare class SandboxResource implements Sandbox {
|
|
|
118
143
|
downloadFile(targetPath: string): Promise<Uint8Array>;
|
|
119
144
|
exposePort(port: number, options?: ExposePortOptions): Promise<ExposeResult>;
|
|
120
145
|
unexposePort(port: number): Promise<void>;
|
|
146
|
+
createSnapshot(name: string): Promise<SandboxSnapshot>;
|
|
121
147
|
start(): Promise<this>;
|
|
122
148
|
stop(): Promise<this>;
|
|
123
149
|
destroy(): Promise<void>;
|
|
124
150
|
resize(options: ResizeOptions): Promise<this>;
|
|
125
151
|
updateLifecycle(lifecycle: Lifecycle): Promise<this>;
|
|
152
|
+
getNetworkUsage(): Promise<NetworkUsage>;
|
|
153
|
+
setNetworkLimits(options: SetNetworkLimitsOptions): Promise<NetworkUsage>;
|
|
126
154
|
toJSON(): Sandbox;
|
|
127
155
|
private apply;
|
|
128
156
|
}
|
package/dist/internal/client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { basename } from "node:path";
|
|
2
2
|
import { PATH_PREFIX as V1_PATH_PREFIX } from "./api/v1/paths.js";
|
|
3
|
+
import { Image } from "../Image.js";
|
|
3
4
|
const DEFAULT_API_VERSION = "v1";
|
|
4
5
|
const PATH_PREFIXES = {
|
|
5
6
|
v1: V1_PATH_PREFIX,
|
|
@@ -20,16 +21,70 @@ export class APIClient {
|
|
|
20
21
|
/**
|
|
21
22
|
* Build a versioned API path. Pass the suffix beginning with "/" (e.g.
|
|
22
23
|
* "/sandboxes") and the active version's prefix is prepended. Use this for
|
|
23
|
-
* every versioned API call so
|
|
24
|
+
* every versioned API call so a future wire version can be selected by the
|
|
24
25
|
* apiVersion option without touching call sites.
|
|
25
26
|
*/
|
|
26
27
|
versioned(suffix) {
|
|
27
28
|
return `${this.versionPrefix}${suffix}`;
|
|
28
29
|
}
|
|
29
30
|
async create(options) {
|
|
30
|
-
const
|
|
31
|
+
const resolved = await this.resolveImage(options);
|
|
32
|
+
const response = await this.doJSON("POST", this.versioned("/sandboxes"), toApiCreateOptions(resolved));
|
|
31
33
|
return new SandboxResource(this, fromApiCreateSandboxResponse(response));
|
|
32
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Compile a fluent {@link Image} into a content-addressed image tag by
|
|
37
|
+
* POSTing its Dockerfile to `/v1/images/build`. The daemon caches by
|
|
38
|
+
* content hash, so repeated calls with the same Dockerfile are a no-op.
|
|
39
|
+
* String images are passed through unchanged.
|
|
40
|
+
*
|
|
41
|
+
* Daemons predating the image-build feature do not register the route and
|
|
42
|
+
* return 404; this method translates that into a clear error telling the
|
|
43
|
+
* caller to pass a string image instead, rather than the generic "request
|
|
44
|
+
* failed with status 404" the JSON decoder would otherwise produce.
|
|
45
|
+
*/
|
|
46
|
+
async buildImage(image, options) {
|
|
47
|
+
const body = { dockerfile_content: image.dockerfile };
|
|
48
|
+
if (options?.push) {
|
|
49
|
+
const p = options.push;
|
|
50
|
+
if (!p.registry) {
|
|
51
|
+
throw new Error("buildImage: push.registry is required when push is set");
|
|
52
|
+
}
|
|
53
|
+
if (!p.username || !p.password) {
|
|
54
|
+
throw new Error("buildImage: push.username and push.password are required when push is set");
|
|
55
|
+
}
|
|
56
|
+
body.push = {
|
|
57
|
+
registry: p.registry,
|
|
58
|
+
tag: p.tag,
|
|
59
|
+
server: p.server,
|
|
60
|
+
username: p.username,
|
|
61
|
+
password: p.password,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const response = await this.request("POST", "/v1/images/build", {
|
|
65
|
+
body: JSON.stringify(body),
|
|
66
|
+
headers: { "Content-Type": "application/json" },
|
|
67
|
+
});
|
|
68
|
+
if (response.status === 404) {
|
|
69
|
+
await response.text().catch(() => undefined);
|
|
70
|
+
throw new Error("this daemon does not support Image builds (POST /v1/images/build is not registered) — pass a string image reference (e.g. \"ubuntu:22.04\") instead, or upgrade the daemon");
|
|
71
|
+
}
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
throw await decodeError(response);
|
|
74
|
+
}
|
|
75
|
+
const payload = (await response.json());
|
|
76
|
+
return { image: payload.image, pushed: payload.pushed };
|
|
77
|
+
}
|
|
78
|
+
async resolveImage(options) {
|
|
79
|
+
if (typeof options.image === "string") {
|
|
80
|
+
return options;
|
|
81
|
+
}
|
|
82
|
+
if (!(options.image instanceof Image)) {
|
|
83
|
+
throw new TypeError("CreateOptions.image must be a string or Image");
|
|
84
|
+
}
|
|
85
|
+
const result = await this.buildImage(options.image);
|
|
86
|
+
return { ...options, image: result.image };
|
|
87
|
+
}
|
|
33
88
|
async list() {
|
|
34
89
|
const response = await this.doJSON("GET", this.versioned("/sandboxes"));
|
|
35
90
|
return response.map((item) => this.wrap(item));
|
|
@@ -46,6 +101,10 @@ export class APIClient {
|
|
|
46
101
|
const response = await this.doJSON("POST", `${this.versionPrefix}/sandboxes/${id}/stop`);
|
|
47
102
|
return this.wrap(response);
|
|
48
103
|
}
|
|
104
|
+
async createSnapshot(id, name) {
|
|
105
|
+
const response = await this.doJSON("POST", `${this.versionPrefix}/sandboxes/${id}/snapshot`, { name });
|
|
106
|
+
return fromApiSandboxSnapshot(response);
|
|
107
|
+
}
|
|
49
108
|
async destroy(id) {
|
|
50
109
|
await this.doJSON("DELETE", `${this.versionPrefix}/sandboxes/${id}`);
|
|
51
110
|
}
|
|
@@ -142,6 +201,14 @@ export class APIClient {
|
|
|
142
201
|
const response = await this.doJSON("GET", `${this.versionPrefix}/sandboxes/${id}/mounts`);
|
|
143
202
|
return response.mounts.map(fromApiMountSpecRedacted);
|
|
144
203
|
}
|
|
204
|
+
async getNetworkUsage(id) {
|
|
205
|
+
const response = await this.doJSON("GET", `${this.versionPrefix}/sandboxes/${id}/network/usage`);
|
|
206
|
+
return fromApiNetworkUsage(response);
|
|
207
|
+
}
|
|
208
|
+
async setNetworkLimits(id, options) {
|
|
209
|
+
const response = await this.doJSON("PATCH", `${this.versionPrefix}/sandboxes/${id}/network/limits`, toApiSetNetworkLimitsOptions(options));
|
|
210
|
+
return fromApiNetworkUsage(response);
|
|
211
|
+
}
|
|
145
212
|
wrap(sandbox) {
|
|
146
213
|
return new SandboxResource(this, fromApiSandbox(sandbox));
|
|
147
214
|
}
|
|
@@ -237,6 +304,9 @@ export class SandboxResource {
|
|
|
237
304
|
async unexposePort(port) {
|
|
238
305
|
await this.client.unexposePort(this.id, port);
|
|
239
306
|
}
|
|
307
|
+
async createSnapshot(name) {
|
|
308
|
+
return this.client.createSnapshot(this.id, name);
|
|
309
|
+
}
|
|
240
310
|
async start() {
|
|
241
311
|
const updated = await this.client.start(this.id);
|
|
242
312
|
this.apply(updated.toJSON());
|
|
@@ -260,6 +330,12 @@ export class SandboxResource {
|
|
|
260
330
|
this.apply(updated.toJSON());
|
|
261
331
|
return this;
|
|
262
332
|
}
|
|
333
|
+
async getNetworkUsage() {
|
|
334
|
+
return this.client.getNetworkUsage(this.id);
|
|
335
|
+
}
|
|
336
|
+
async setNetworkLimits(options) {
|
|
337
|
+
return this.client.setNetworkLimits(this.id, options);
|
|
338
|
+
}
|
|
263
339
|
toJSON() {
|
|
264
340
|
return cloneSandbox(this);
|
|
265
341
|
}
|
|
@@ -276,6 +352,8 @@ function toApiCreateOptions(options) {
|
|
|
276
352
|
env: options.env,
|
|
277
353
|
os_user: options.osUser,
|
|
278
354
|
network_block_all: options.networkBlockAll,
|
|
355
|
+
network_bytes_in_limit: options.networkBytesInLimit,
|
|
356
|
+
network_bytes_out_limit: options.networkBytesOutLimit,
|
|
279
357
|
registry: options.registry,
|
|
280
358
|
container_command: options.containerCommand,
|
|
281
359
|
mounts: options.mounts?.map(toApiMountSpec),
|
|
@@ -359,6 +437,15 @@ function fromApiCreateSandboxResponse(response) {
|
|
|
359
437
|
sshPrivateKey: response.ssh_private_key,
|
|
360
438
|
};
|
|
361
439
|
}
|
|
440
|
+
function fromApiSandboxSnapshot(snapshot) {
|
|
441
|
+
return {
|
|
442
|
+
name: snapshot.name,
|
|
443
|
+
image: snapshot.image,
|
|
444
|
+
imageID: snapshot.image_id,
|
|
445
|
+
sourceSandboxID: snapshot.source_sandbox_id,
|
|
446
|
+
createdAt: snapshot.created_at,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
362
449
|
function fromApiSession(session) {
|
|
363
450
|
return {
|
|
364
451
|
id: session.id,
|
|
@@ -438,6 +525,24 @@ function fromApiMountSpecRedacted(mount) {
|
|
|
438
525
|
hasCredentials: mount.has_credentials,
|
|
439
526
|
};
|
|
440
527
|
}
|
|
528
|
+
function fromApiNetworkUsage(usage) {
|
|
529
|
+
return {
|
|
530
|
+
sandboxID: usage.sandbox_id,
|
|
531
|
+
bytesIn: usage.bytes_in,
|
|
532
|
+
bytesOut: usage.bytes_out,
|
|
533
|
+
bytesInLimit: usage.bytes_in_limit,
|
|
534
|
+
bytesOutLimit: usage.bytes_out_limit,
|
|
535
|
+
quotaExceeded: usage.quota_exceeded,
|
|
536
|
+
quotaExceededAt: usage.quota_exceeded_at ?? undefined,
|
|
537
|
+
lastSampledAt: usage.last_sampled_at ?? undefined,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
function toApiSetNetworkLimitsOptions(options) {
|
|
541
|
+
return {
|
|
542
|
+
network_bytes_in_limit: options.networkBytesInLimit,
|
|
543
|
+
network_bytes_out_limit: options.networkBytesOutLimit,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
441
546
|
function fromApiLifecycle(lifecycle) {
|
|
442
547
|
const result = {};
|
|
443
548
|
if (lifecycle?.stop_if_idle_for !== undefined) {
|
package/dist/types.d.ts
CHANGED
|
@@ -4,6 +4,30 @@ export interface RegistryAuth {
|
|
|
4
4
|
username: string;
|
|
5
5
|
password: string;
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* Per-request push directive for `MicroVM.buildImage`. Credentials are
|
|
9
|
+
* forwarded to the daemon as a one-shot `X-Registry-Auth` header on the
|
|
10
|
+
* push call and are never persisted.
|
|
11
|
+
*/
|
|
12
|
+
export interface BuildImagePushOptions {
|
|
13
|
+
/** Destination repository, e.g. "ghcr.io/my-org/my-image". */
|
|
14
|
+
registry: string;
|
|
15
|
+
/** Destination tag. Defaults to "latest" on the daemon when omitted. */
|
|
16
|
+
tag?: string;
|
|
17
|
+
/** Registry serveraddress, e.g. "ghcr.io". Sent inside X-Registry-Auth. */
|
|
18
|
+
server?: string;
|
|
19
|
+
username: string;
|
|
20
|
+
password: string;
|
|
21
|
+
}
|
|
22
|
+
export interface BuildImageOptions {
|
|
23
|
+
push?: BuildImagePushOptions;
|
|
24
|
+
}
|
|
25
|
+
export interface BuildImageResult {
|
|
26
|
+
/** Local content-addressed tag (always returned). */
|
|
27
|
+
image: string;
|
|
28
|
+
/** Pushed reference, e.g. "ghcr.io/my-org/my-image:v1.2.3". */
|
|
29
|
+
pushed?: string;
|
|
30
|
+
}
|
|
7
31
|
export type MountType = "s3" | "nfs" | "sshfs" | "rclone";
|
|
8
32
|
export interface MountSpec {
|
|
9
33
|
type: MountType;
|
|
@@ -60,8 +84,17 @@ export interface GPUOptions {
|
|
|
60
84
|
*/
|
|
61
85
|
deviceIDs?: string[];
|
|
62
86
|
}
|
|
87
|
+
import type { Image } from "./Image.js";
|
|
63
88
|
export interface CreateOptions {
|
|
64
|
-
|
|
89
|
+
/**
|
|
90
|
+
* The base image for this sandbox. A bare image reference (e.g.
|
|
91
|
+
* `"ubuntu:22.04"`) is pulled by the daemon as-is. An {@link Image} builder
|
|
92
|
+
* is compiled to a Dockerfile and sent to the daemon's
|
|
93
|
+
* `POST /v1/images/build` endpoint; the resulting content-addressed tag is
|
|
94
|
+
* used for the sandbox. The daemon must have an image builder configured
|
|
95
|
+
* (every official deployment does); otherwise the build call returns 503.
|
|
96
|
+
*/
|
|
97
|
+
image: string | Image;
|
|
65
98
|
/** Number of CPU cores to allocate. Fractional values are supported (e.g. 0.5 = half a core). */
|
|
66
99
|
cpu?: number;
|
|
67
100
|
memoryMB?: number;
|
|
@@ -69,6 +102,19 @@ export interface CreateOptions {
|
|
|
69
102
|
env?: Record<string, string>;
|
|
70
103
|
osUser?: string;
|
|
71
104
|
networkBlockAll?: boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Cap on bytes the sandbox may receive from outside the container before its
|
|
107
|
+
* ingress is dropped via per-IP iptables rule. `0` (default) is unlimited.
|
|
108
|
+
* Limits can be raised or lifted at runtime via `setNetworkLimits`.
|
|
109
|
+
*/
|
|
110
|
+
networkBytesInLimit?: number;
|
|
111
|
+
/**
|
|
112
|
+
* Cap on bytes the sandbox may send to outside the container before its
|
|
113
|
+
* egress is dropped. `0` (default) is unlimited. The block reuses the same
|
|
114
|
+
* iptables row as `networkBlockAll`; clearing the quota does not lift an
|
|
115
|
+
* operator-set blanket egress block.
|
|
116
|
+
*/
|
|
117
|
+
networkBytesOutLimit?: number;
|
|
72
118
|
registry?: RegistryAuth;
|
|
73
119
|
containerCommand?: string[];
|
|
74
120
|
mounts?: MountSpec[];
|
|
@@ -140,6 +186,13 @@ export interface ExposePortOptions {
|
|
|
140
186
|
/** Defaults to "http" when omitted. */
|
|
141
187
|
protocol?: ExposeProtocol;
|
|
142
188
|
}
|
|
189
|
+
export interface SandboxSnapshot {
|
|
190
|
+
name: string;
|
|
191
|
+
image: string;
|
|
192
|
+
imageID?: string;
|
|
193
|
+
sourceSandboxID: string;
|
|
194
|
+
createdAt: string;
|
|
195
|
+
}
|
|
143
196
|
/**
|
|
144
197
|
* Discriminated result from `exposePort`. Branch on `protocol` to access the
|
|
145
198
|
* fields that are meaningful for the chosen wire protocol — only the raw-TCP
|
|
@@ -189,6 +242,29 @@ export interface Sandbox {
|
|
|
189
242
|
/** GPU configuration this sandbox was created with. Absent means no GPU. */
|
|
190
243
|
gpus?: GPUOptions;
|
|
191
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Per-sandbox network byte counters and the configured caps that drive the
|
|
247
|
+
* quota enforcer. `bytesIn` is traffic the container received (ingress);
|
|
248
|
+
* `bytesOut` is traffic the container sent (egress). A `*Limit` of `0` means
|
|
249
|
+
* unlimited. `quotaExceeded` flips true the first time the meter crosses
|
|
250
|
+
* either configured cap and stays true until the operator raises the limit.
|
|
251
|
+
*/
|
|
252
|
+
export interface NetworkUsage {
|
|
253
|
+
sandboxID: string;
|
|
254
|
+
bytesIn: number;
|
|
255
|
+
bytesOut: number;
|
|
256
|
+
bytesInLimit: number;
|
|
257
|
+
bytesOutLimit: number;
|
|
258
|
+
quotaExceeded: boolean;
|
|
259
|
+
quotaExceededAt?: string;
|
|
260
|
+
/** Absent until the netstats poller has produced at least one sample. */
|
|
261
|
+
lastSampledAt?: string;
|
|
262
|
+
}
|
|
263
|
+
export interface SetNetworkLimitsOptions {
|
|
264
|
+
/** Omit (undefined) to leave unchanged. `0` means unlimited. */
|
|
265
|
+
networkBytesInLimit?: number;
|
|
266
|
+
networkBytesOutLimit?: number;
|
|
267
|
+
}
|
|
192
268
|
export interface ExecRequest {
|
|
193
269
|
command: string;
|
|
194
270
|
workDir?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aerol-ai/aerolvm-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -13,11 +13,12 @@
|
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
15
|
"build": "tsc -p tsconfig.json",
|
|
16
|
+
"example:browser-isolation:src": "npx tsx src/examples/browserIsolation.ts",
|
|
16
17
|
"example:create": "npm run build && node dist/examples/createSandbox.js",
|
|
17
18
|
"example:create:src": "npx tsx src/examples/createSandbox.ts",
|
|
18
19
|
"example:delete": "npm run build && node dist/examples/deleteSandbox.js",
|
|
19
20
|
"example:delete:src": "npx tsx src/examples/deleteSandbox.ts",
|
|
20
|
-
"test": "tsc -p tsconfig.test.json --outDir dist-test && node --test dist-test/*.test.js"
|
|
21
|
+
"test": "tsc -p tsconfig.test.json --outDir dist-test && node --test dist-test/*.test.js dist-test/internal/*.test.js"
|
|
21
22
|
},
|
|
22
23
|
"devDependencies": {
|
|
23
24
|
"@types/node": "^25.6.2",
|