@aerol-ai/aerolvm-sdk 0.5.3 → 0.6.4
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/MicroVM.d.ts +30 -3
- package/dist/MicroVM.js +46 -2
- package/dist/index.d.ts +1 -1
- package/dist/internal/client.d.ts +47 -4
- package/dist/internal/client.js +281 -8
- package/dist/types.d.ts +321 -14
- package/package.json +9 -4
package/dist/MicroVM.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type APIVersion, type RetryConfig } from "./internal/client.js";
|
|
2
2
|
import { Sandbox } from "./Sandbox.js";
|
|
3
3
|
import { Image } from "./Image.js";
|
|
4
|
-
import type { BuildImageOptions, BuildImageResult, CloneGeneration, CreateOptions, CreateSessionOptions, CreateTemplateOptions, CreateWasmModuleOptions, PushWasmModuleOptions, PushWasmModuleResult, ExecStreamHandle, ExecStreamOptions, HealthStatus, IngressTarget, Lifecycle, ListOptions, MountSpecRedacted, RegisterSnapshotOptions, ResizeOptions, SandboxSnapshot, Session, SessionAttachHandle, SessionAttachOptions, Template, WasmModule } from "./types.js";
|
|
4
|
+
import type { BuildImageOptions, BuildImageResult, CloneGeneration, CreateOptions, CreateSessionOptions, CreateTemplateOptions, CreateWasmModuleOptions, PushWasmModuleOptions, PushWasmModuleResult, ExecStreamHandle, ExecStreamOptions, HealthStatus, EgressProfile, EgressProfileList, EgressProfileOptions, ListEgressProfilesOptions, NetworkPolicyCheckOptions, NetworkPolicyCheckResult, IngressTarget, Lifecycle, GetOptions, ListOptions, MountSpecRedacted, RegisterSnapshotOptions, ResizeOptions, SandboxSnapshot, Session, SessionAttachHandle, SessionAttachOptions, Template, WasmModule } from "./types.js";
|
|
5
5
|
type FetchLike = typeof fetch;
|
|
6
6
|
export interface MicroVMConfig {
|
|
7
7
|
patToken?: string;
|
|
@@ -16,7 +16,7 @@ export interface MicroVMConfig {
|
|
|
16
16
|
apiVersion?: APIVersion;
|
|
17
17
|
/**
|
|
18
18
|
* Retry policy for transient transport errors (socket closed, connection
|
|
19
|
-
* reset) and retryable HTTP status codes (429, 502, 503, 504). The SDK
|
|
19
|
+
* reset) and retryable HTTP status codes (421, 429, 502, 503, 504). The SDK
|
|
20
20
|
* retries up to 3 times with exponential backoff by default. Pass
|
|
21
21
|
* `{ maxRetries: 0 }` to disable.
|
|
22
22
|
*/
|
|
@@ -29,7 +29,18 @@ export declare class MicroVM {
|
|
|
29
29
|
constructor(config?: MicroVMConfig);
|
|
30
30
|
create(options: CreateOptions): Promise<Sandbox>;
|
|
31
31
|
list(options?: ListOptions): Promise<Sandbox[]>;
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Iterates one server page at a time. Use this for large fleet inventories so
|
|
34
|
+
* the SDK does not retain every sandbox in memory.
|
|
35
|
+
*/
|
|
36
|
+
listPages(options?: ListOptions): AsyncGenerator<Sandbox[]>;
|
|
37
|
+
get(id: string, options?: GetOptions): Promise<Sandbox>;
|
|
38
|
+
/**
|
|
39
|
+
* Returns the caller's sandbox with this name, or `null` when there is none.
|
|
40
|
+
* Names are unique per owner. Throws when the server predates name lookup
|
|
41
|
+
* (it ignores `?name=`), rather than guessing from an unfiltered list.
|
|
42
|
+
*/
|
|
43
|
+
getByName(name: string, options?: GetOptions): Promise<Sandbox | null>;
|
|
33
44
|
start(id: string): Promise<Sandbox>;
|
|
34
45
|
stop(id: string): Promise<Sandbox>;
|
|
35
46
|
createSnapshot(id: string, name: string): Promise<SandboxSnapshot>;
|
|
@@ -47,6 +58,22 @@ export declare class MicroVM {
|
|
|
47
58
|
*/
|
|
48
59
|
buildImage(image: Image, options?: BuildImageOptions): Promise<BuildImageResult>;
|
|
49
60
|
health(): Promise<HealthStatus>;
|
|
61
|
+
/**
|
|
62
|
+
* Asks whether a sandbox created with these egress fields would reach a
|
|
63
|
+
* destination, using the same matcher the filter enforces. No sandbox is
|
|
64
|
+
* needed.
|
|
65
|
+
*/
|
|
66
|
+
checkNetworkPolicy(options: NetworkPolicyCheckOptions): Promise<NetworkPolicyCheckResult>;
|
|
67
|
+
/**
|
|
68
|
+
* Creates or replaces a named egress profile (a full replace: the same body
|
|
69
|
+
* twice is a no-op). Sandboxes reference it through `egressProfiles`, and a
|
|
70
|
+
* change reaches every one of them.
|
|
71
|
+
*/
|
|
72
|
+
putEgressProfile(name: string, options: EgressProfileOptions): Promise<EgressProfile>;
|
|
73
|
+
getEgressProfile(name: string): Promise<EgressProfile>;
|
|
74
|
+
listEgressProfiles(options?: ListEgressProfilesOptions): Promise<EgressProfileList>;
|
|
75
|
+
/** Deletes a profile; one that sandboxes still reference is refused (409). */
|
|
76
|
+
deleteEgressProfile(name: string): Promise<void>;
|
|
50
77
|
mounts(sandboxID: string): Promise<MountSpecRedacted[]>;
|
|
51
78
|
/**
|
|
52
79
|
* Read a sandbox's clone-generation token. The token changes whenever the
|
package/dist/MicroVM.js
CHANGED
|
@@ -30,10 +30,28 @@ export class MicroVM {
|
|
|
30
30
|
const sandboxes = await this.client.list(options);
|
|
31
31
|
return sandboxes.map((sandbox) => this.wrap(sandbox.toJSON()));
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Iterates one server page at a time. Use this for large fleet inventories so
|
|
35
|
+
* the SDK does not retain every sandbox in memory.
|
|
36
|
+
*/
|
|
37
|
+
async *listPages(options) {
|
|
38
|
+
for await (const page of this.client.listPages(options)) {
|
|
39
|
+
yield page.map((sandbox) => this.wrap(sandbox.toJSON()));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async get(id, options) {
|
|
43
|
+
const sandbox = await this.client.get(id, options);
|
|
35
44
|
return this.wrap(sandbox.toJSON());
|
|
36
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Returns the caller's sandbox with this name, or `null` when there is none.
|
|
48
|
+
* Names are unique per owner. Throws when the server predates name lookup
|
|
49
|
+
* (it ignores `?name=`), rather than guessing from an unfiltered list.
|
|
50
|
+
*/
|
|
51
|
+
async getByName(name, options) {
|
|
52
|
+
const sandbox = await this.client.getByName(name, options);
|
|
53
|
+
return sandbox === null ? null : this.wrap(sandbox.toJSON());
|
|
54
|
+
}
|
|
37
55
|
async start(id) {
|
|
38
56
|
const sandbox = await this.client.start(id);
|
|
39
57
|
return this.wrap(sandbox.toJSON());
|
|
@@ -77,6 +95,32 @@ export class MicroVM {
|
|
|
77
95
|
async health() {
|
|
78
96
|
return this.client.health();
|
|
79
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Asks whether a sandbox created with these egress fields would reach a
|
|
100
|
+
* destination, using the same matcher the filter enforces. No sandbox is
|
|
101
|
+
* needed.
|
|
102
|
+
*/
|
|
103
|
+
async checkNetworkPolicy(options) {
|
|
104
|
+
return this.client.checkNetworkPolicy(options);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Creates or replaces a named egress profile (a full replace: the same body
|
|
108
|
+
* twice is a no-op). Sandboxes reference it through `egressProfiles`, and a
|
|
109
|
+
* change reaches every one of them.
|
|
110
|
+
*/
|
|
111
|
+
async putEgressProfile(name, options) {
|
|
112
|
+
return this.client.putEgressProfile(name, options);
|
|
113
|
+
}
|
|
114
|
+
async getEgressProfile(name) {
|
|
115
|
+
return this.client.getEgressProfile(name);
|
|
116
|
+
}
|
|
117
|
+
async listEgressProfiles(options) {
|
|
118
|
+
return this.client.listEgressProfiles(options);
|
|
119
|
+
}
|
|
120
|
+
/** Deletes a profile; one that sandboxes still reference is refused (409). */
|
|
121
|
+
async deleteEgressProfile(name) {
|
|
122
|
+
return this.client.deleteEgressProfile(name);
|
|
123
|
+
}
|
|
80
124
|
async mounts(sandboxID) {
|
|
81
125
|
return this.client.mounts(sandboxID);
|
|
82
126
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,4 +3,4 @@ export { Sandbox } from "./Sandbox.js";
|
|
|
3
3
|
export { Image } from "./Image.js";
|
|
4
4
|
export type { MicroVMConfig } from "./MicroVM.js";
|
|
5
5
|
export type { RetryConfig } from "./internal/client.js";
|
|
6
|
-
export type { AddCustomDomainOptions, BinaryLike, BuildImageOptions, BuildImagePushOptions, BuildImageResult, CloneGeneration, CreateOptions, CreateSessionOptions, CreateTemplateOptions, CreateWasmModuleOptions, CustomDomain, CustomDomainDNSRecords, CustomDomainStatus, DNSRecord, DNSRecordType, ExecRequest, ExecResult, ExecExitInfo, ExposedPort, ExposePortOptions, ExposeProtocol, ExposeResult, Failover, FailoverPolicy, HealthStatus, IngressTarget, IngressTargetSource, Lifecycle, MountSpec, MountSpecRedacted, MountType, RegisterSnapshotOptions, RegistryAuth, ResizeOptions, Sandbox as SandboxData, SandboxSnapshot, SandboxStatus, Session, SessionAttachHandle, SessionAttachOptions, SessionStatus, Template, TemplatePushState, TemplateStatus, WasmModule, WasmModuleStatus, PushWasmModuleOptions, PushWasmModuleResult, UpdateLifecycleOptions, } from "./types.js";
|
|
6
|
+
export type { AddCustomDomainOptions, AuditEvent, AuditOptions, AuditPage, EgressProfile, EgressProfileList, EgressProfileOptions, EgressProfileRef, EgressInject, EgressRule, ListEgressProfilesOptions, NetworkLearned, NetworkLearnedEntry, NetworkPolicy, NetworkPolicyCheckOptions, NetworkPolicyCheckResult, NetworkPolicyOptions, BinaryLike, BuildImageOptions, BuildImagePushOptions, BuildImageResult, CloneGeneration, CreateOptions, CreateSessionOptions, CreateTemplateOptions, CreateWasmModuleOptions, CustomDomain, CustomDomainDNSRecords, CustomDomainStatus, DNSRecord, DNSRecordType, ExecRequest, ExecResult, ExecExitInfo, ExposedPort, ExposePortOptions, ExposeProtocol, ExposeResult, Failover, FailoverPolicy, GetOptions, HealthStatus, IngressTarget, IngressTargetSource, Lifecycle, ListOptions, MountSpec, MountSpecRedacted, MountType, PlatformVolumeMount, RegisterSnapshotOptions, RegistryAuth, ResizeOptions, Sandbox as SandboxData, SandboxSnapshot, SandboxStatus, Session, SessionAttachHandle, SessionAttachOptions, SessionStatus, Template, TemplatePushState, TemplateStatus, WasmModule, WasmModuleStatus, PushWasmModuleOptions, PushWasmModuleResult, UpdateLifecycleOptions, } from "./types.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Image } from "../Image.js";
|
|
2
|
-
import type { AddCustomDomainOptions, BinaryLike, BuildImageOptions, BuildImageResult, CreateOptions, CreateSessionOptions, CreateTemplateOptions, CreateWasmModuleOptions, PushWasmModuleOptions, PushWasmModuleResult, CustomDomain, CloneGeneration, CustomDomainDNSRecords, ExecRequest, ExecResult, ExecStreamHandle, ExecStreamOptions, ExposedPort, ExposePortOptions, ExposeResult, Failover, HealthStatus, IngressTarget, Lifecycle, ListOptions, MountSpecRedacted, NetworkUsage, RegisterSnapshotOptions, SetNetworkLimitsOptions, ResizeOptions, Sandbox, SandboxSnapshot, Session, SessionAttachHandle, SessionAttachOptions, Template, WasmModule } from "../types.js";
|
|
2
|
+
import type { AddCustomDomainOptions, BinaryLike, BuildImageOptions, BuildImageResult, CreateOptions, CreateSessionOptions, CreateTemplateOptions, CreateWasmModuleOptions, PushWasmModuleOptions, PushWasmModuleResult, CustomDomain, CloneGeneration, CustomDomainDNSRecords, ExecRequest, ExecResult, ExecStreamHandle, ExecStreamOptions, ExposedPort, ExposePortOptions, ExposeResult, Failover, HealthStatus, IngressTarget, Lifecycle, GetOptions, ListOptions, MountSpecRedacted, NetworkUsage, AuditOptions, AuditPage, EgressProfile, EgressProfileList, EgressProfileOptions, EgressProfileRef, EgressRule, ListEgressProfilesOptions, NetworkLearned, NetworkPolicy, NetworkPolicyCheckOptions, NetworkPolicyCheckResult, NetworkPolicyOptions, RegisterSnapshotOptions, SetNetworkLimitsOptions, ResizeOptions, Sandbox, SandboxSnapshot, Session, SessionAttachHandle, SessionAttachOptions, Template, WasmModule } from "../types.js";
|
|
3
3
|
type FetchLike = typeof fetch;
|
|
4
4
|
/**
|
|
5
5
|
* Wire version of the v1-style sandbox endpoints (`/v1/sandboxes`, etc.) this
|
|
@@ -49,7 +49,7 @@ export interface APIClientConfig {
|
|
|
49
49
|
apiVersion?: APIVersion;
|
|
50
50
|
/**
|
|
51
51
|
* Retry policy for transient transport errors (socket closed, connection
|
|
52
|
-
* reset) and retryable HTTP status codes (429, 502, 503, 504). Pass
|
|
52
|
+
* reset) and retryable HTTP status codes (421, 429, 502, 503, 504). Pass
|
|
53
53
|
* `{ maxRetries: 0 }` to disable retry entirely.
|
|
54
54
|
*/
|
|
55
55
|
retry?: RetryConfig;
|
|
@@ -90,7 +90,17 @@ export declare class APIClient {
|
|
|
90
90
|
buildImage(image: Image, options?: BuildImageOptions): Promise<BuildImageResult>;
|
|
91
91
|
private resolveImage;
|
|
92
92
|
list(options?: ListOptions): Promise<SandboxResource[]>;
|
|
93
|
-
|
|
93
|
+
/** Streams bounded cluster-list pages and never accumulates the full fleet. */
|
|
94
|
+
listPages(options?: ListOptions): AsyncGenerator<SandboxResource[]>;
|
|
95
|
+
/**
|
|
96
|
+
* Looks up the caller's sandbox by name with one request. The reply is only
|
|
97
|
+
* trusted when it holds at most one sandbox and that sandbox carries the
|
|
98
|
+
* requested name: a server that predates `?name=` ignores the filter and
|
|
99
|
+
* returns an ordinary list page, and acting on its first row would target
|
|
100
|
+
* the wrong sandbox.
|
|
101
|
+
*/
|
|
102
|
+
getByName(name: string, options?: GetOptions): Promise<SandboxResource | null>;
|
|
103
|
+
get(id: string, options?: GetOptions): Promise<SandboxResource>;
|
|
94
104
|
start(id: string): Promise<SandboxResource>;
|
|
95
105
|
stop(id: string): Promise<SandboxResource>;
|
|
96
106
|
createSnapshot(id: string, name: string): Promise<SandboxSnapshot>;
|
|
@@ -149,10 +159,18 @@ export declare class APIClient {
|
|
|
149
159
|
customDomainDNS(id: string): Promise<CustomDomainDNSRecords>;
|
|
150
160
|
reconcile(): Promise<void>;
|
|
151
161
|
health(): Promise<HealthStatus>;
|
|
162
|
+
checkNetworkPolicy(options: NetworkPolicyCheckOptions): Promise<NetworkPolicyCheckResult>;
|
|
152
163
|
mounts(id: string): Promise<MountSpecRedacted[]>;
|
|
153
164
|
cloneGeneration(id: string): Promise<CloneGeneration>;
|
|
154
165
|
getNetworkUsage(id: string): Promise<NetworkUsage>;
|
|
166
|
+
getAudit(id: string, options?: AuditOptions): Promise<AuditPage>;
|
|
155
167
|
setNetworkLimits(id: string, options: SetNetworkLimitsOptions): Promise<NetworkUsage>;
|
|
168
|
+
setNetworkPolicy(id: string, options: NetworkPolicyOptions): Promise<NetworkPolicy>;
|
|
169
|
+
getNetworkLearned(id: string): Promise<NetworkLearned>;
|
|
170
|
+
putEgressProfile(name: string, options: EgressProfileOptions): Promise<EgressProfile>;
|
|
171
|
+
getEgressProfile(name: string): Promise<EgressProfile>;
|
|
172
|
+
listEgressProfiles(options?: ListEgressProfilesOptions): Promise<EgressProfileList>;
|
|
173
|
+
deleteEgressProfile(name: string): Promise<void>;
|
|
156
174
|
createTemplate(options: CreateTemplateOptions): Promise<Template>;
|
|
157
175
|
listTemplates(): Promise<Template[]>;
|
|
158
176
|
getTemplate(id: string): Promise<Template>;
|
|
@@ -173,7 +191,7 @@ export declare class APIClient {
|
|
|
173
191
|
* always retried regardless of HTTP method — the request never reached the
|
|
174
192
|
* server so there is no idempotency concern.
|
|
175
193
|
*
|
|
176
|
-
* HTTP-level retryable codes (429, 502, 503, 504) are retried for ALL
|
|
194
|
+
* HTTP-level retryable codes (421, 429, 502, 503, 504) are retried for ALL
|
|
177
195
|
* methods because every mutating endpoint in the daemon is already
|
|
178
196
|
* designed for idempotent retry (e.g. INSERT OR IGNORE + disambiguation).
|
|
179
197
|
*/
|
|
@@ -192,6 +210,11 @@ export declare class SandboxResource implements Sandbox {
|
|
|
192
210
|
osUser: string;
|
|
193
211
|
env?: Record<string, string>;
|
|
194
212
|
networkBlockAll: boolean;
|
|
213
|
+
egressStatus?: string;
|
|
214
|
+
egressProfiles?: string[];
|
|
215
|
+
egressProfilesApplied?: EgressProfileRef[];
|
|
216
|
+
networkEgressMode?: string;
|
|
217
|
+
networkEgressRules?: EgressRule[];
|
|
195
218
|
toolboxEnabled: boolean;
|
|
196
219
|
sshPublicKey?: string;
|
|
197
220
|
sshPrivateKey?: string;
|
|
@@ -199,6 +222,8 @@ export declare class SandboxResource implements Sandbox {
|
|
|
199
222
|
createdAt: string;
|
|
200
223
|
updatedAt: string;
|
|
201
224
|
lastActiveAt: string;
|
|
225
|
+
name?: string;
|
|
226
|
+
tags?: Record<string, string>;
|
|
202
227
|
lastError?: string;
|
|
203
228
|
containerCommand?: string[];
|
|
204
229
|
lifecycle: Lifecycle;
|
|
@@ -251,6 +276,24 @@ export declare class SandboxResource implements Sandbox {
|
|
|
251
276
|
updateLifecycle(lifecycle: Lifecycle): Promise<this>;
|
|
252
277
|
getNetworkUsage(): Promise<NetworkUsage>;
|
|
253
278
|
setNetworkLimits(options: SetNetworkLimitsOptions): Promise<NetworkUsage>;
|
|
279
|
+
/**
|
|
280
|
+
* Replaces this sandbox's egress policy while it runs. The call returns
|
|
281
|
+
* once the new policy is enforced; sending the same policy again is a
|
|
282
|
+
* no-op, so it is safe to retry.
|
|
283
|
+
*/
|
|
284
|
+
setNetworkPolicy(options: NetworkPolicyOptions): Promise<NetworkPolicy>;
|
|
285
|
+
/**
|
|
286
|
+
* Reads what this sandbox reached in learn mode and the allow list that
|
|
287
|
+
* would have allowed it. A recording stays readable after the sandbox
|
|
288
|
+
* switches to enforce, until it is destroyed.
|
|
289
|
+
*/
|
|
290
|
+
learned(): Promise<NetworkLearned>;
|
|
291
|
+
/**
|
|
292
|
+
* Reads this sandbox's audit log: outbound connections and egress denials
|
|
293
|
+
* (`kind: "egress"`), and secret reads. Denials carry `result: "failure"`
|
|
294
|
+
* and the policy `reason`.
|
|
295
|
+
*/
|
|
296
|
+
audit(options?: AuditOptions): Promise<AuditPage>;
|
|
254
297
|
toJSON(): Sandbox;
|
|
255
298
|
private apply;
|
|
256
299
|
}
|
package/dist/internal/client.js
CHANGED
|
@@ -5,8 +5,30 @@ const DEFAULT_API_VERSION = "v1";
|
|
|
5
5
|
const PATH_PREFIXES = {
|
|
6
6
|
v1: V1_PATH_PREFIX,
|
|
7
7
|
};
|
|
8
|
+
function fromApiEgressProfile(p) {
|
|
9
|
+
const out = {
|
|
10
|
+
name: p.name,
|
|
11
|
+
allowOut: p.allow_out ?? [],
|
|
12
|
+
generation: p.generation,
|
|
13
|
+
createdAt: p.created_at,
|
|
14
|
+
updatedAt: p.updated_at,
|
|
15
|
+
};
|
|
16
|
+
if (p.description)
|
|
17
|
+
out.description = p.description;
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
function toApiEgressRule({ inject, ...rule }) {
|
|
21
|
+
return inject ? { ...rule, inject: { header: inject.header, secret_ref: inject.secretRef } } : rule;
|
|
22
|
+
}
|
|
23
|
+
function fromApiEgressRule({ inject, ...rule }) {
|
|
24
|
+
return inject ? { ...rule, inject: { header: inject.header, secretRef: inject.secret_ref } } : rule;
|
|
25
|
+
}
|
|
8
26
|
/** HTTP status codes the retry loop considers transient. */
|
|
9
|
-
|
|
27
|
+
// 421 Misdirected Request: an owner answered for a sandbox it doesn't hold
|
|
28
|
+
// (HTTP/2 connection coalescing, or a stale ingress route after failover).
|
|
29
|
+
// The server closes the connection, so the retry reconnects and the ingress
|
|
30
|
+
// re-routes it (plans/ingress-proxy-routing.md, review 2A).
|
|
31
|
+
const RETRYABLE_STATUS_CODES = new Set([421, 429, 502, 503, 504]);
|
|
10
32
|
/** Default retry settings when the caller doesn't supply a RetryConfig. */
|
|
11
33
|
const DEFAULT_RETRY = {
|
|
12
34
|
maxRetries: 3,
|
|
@@ -151,12 +173,62 @@ export class APIClient {
|
|
|
151
173
|
return { ...options, image: result.image };
|
|
152
174
|
}
|
|
153
175
|
async list(options) {
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
|
|
176
|
+
const items = [];
|
|
177
|
+
for await (const page of this.listPages(options)) {
|
|
178
|
+
items.push(...page);
|
|
179
|
+
}
|
|
180
|
+
return items;
|
|
181
|
+
}
|
|
182
|
+
/** Streams bounded cluster-list pages and never accumulates the full fleet. */
|
|
183
|
+
async *listPages(options) {
|
|
184
|
+
const basePath = this.versioned("/sandboxes") + buildSandboxListQuery(options);
|
|
185
|
+
let pageToken = "";
|
|
186
|
+
for (let page = 0; page < 100000; page++) {
|
|
187
|
+
const path = appendQueryParam(basePath, "page_token", pageToken);
|
|
188
|
+
const response = await this.request("GET", path);
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
throw await decodeError(response);
|
|
191
|
+
}
|
|
192
|
+
const partial = response.headers.get("X-Cluster-List-Partial");
|
|
193
|
+
const placementReady = response.headers.get("X-Cluster-List-Placement-Ready");
|
|
194
|
+
if (partial === "true" || placementReady === "false") {
|
|
195
|
+
throw new Error("incomplete cluster list");
|
|
196
|
+
}
|
|
197
|
+
const body = (await response.json());
|
|
198
|
+
yield (body ?? []).map((item) => this.wrap(item));
|
|
199
|
+
pageToken = (response.headers.get("X-Cluster-List-Next-Page-Token") ?? "").trim();
|
|
200
|
+
if (pageToken === "") {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
throw new Error("incomplete cluster list: exceeded max pages");
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Looks up the caller's sandbox by name with one request. The reply is only
|
|
208
|
+
* trusted when it holds at most one sandbox and that sandbox carries the
|
|
209
|
+
* requested name: a server that predates `?name=` ignores the filter and
|
|
210
|
+
* returns an ordinary list page, and acting on its first row would target
|
|
211
|
+
* the wrong sandbox.
|
|
212
|
+
*/
|
|
213
|
+
async getByName(name, options) {
|
|
214
|
+
const trimmed = name.trim();
|
|
215
|
+
if (trimmed === "") {
|
|
216
|
+
throw new Error("sandbox name is required");
|
|
217
|
+
}
|
|
218
|
+
const path = this.versioned("/sandboxes") + buildSandboxListQuery({ name: trimmed, includeEnv: options?.includeEnv });
|
|
219
|
+
const body = await this.doJSON("GET", path);
|
|
220
|
+
const items = body ?? [];
|
|
221
|
+
if (items.length === 0) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
if (items.length > 1 || items[0].name !== trimmed) {
|
|
225
|
+
throw new Error(`${this.baseURL} does not support sandbox name lookup; use the sandbox ID or upgrade the server`);
|
|
226
|
+
}
|
|
227
|
+
return this.wrap(items[0]);
|
|
157
228
|
}
|
|
158
|
-
async get(id) {
|
|
159
|
-
const
|
|
229
|
+
async get(id, options) {
|
|
230
|
+
const q = buildIncludeEnvQuery(options?.includeEnv);
|
|
231
|
+
const response = await this.doJSON("GET", `${this.versionPrefix}/sandboxes/${id}${q}`);
|
|
160
232
|
return this.wrap(response);
|
|
161
233
|
}
|
|
162
234
|
async start(id) {
|
|
@@ -328,6 +400,22 @@ export class APIClient {
|
|
|
328
400
|
const response = await this.doJSON("GET", "/health");
|
|
329
401
|
return fromApiHealthStatus(response);
|
|
330
402
|
}
|
|
403
|
+
async checkNetworkPolicy(options) {
|
|
404
|
+
const response = await this.doJSON("POST", this.versioned("/network/policy/check"), {
|
|
405
|
+
network_block_all: options.networkBlockAll,
|
|
406
|
+
network_allow_out: options.networkAllowOut,
|
|
407
|
+
network_deny_out: options.networkDenyOut,
|
|
408
|
+
destination: options.destination,
|
|
409
|
+
});
|
|
410
|
+
const result = {
|
|
411
|
+
allowed: response.allowed,
|
|
412
|
+
matchedRule: response.matched_rule ?? "",
|
|
413
|
+
defaultVerdict: response.default_verdict,
|
|
414
|
+
};
|
|
415
|
+
if (response.outside_ceiling)
|
|
416
|
+
result.outsideCeiling = response.outside_ceiling;
|
|
417
|
+
return result;
|
|
418
|
+
}
|
|
331
419
|
async mounts(id) {
|
|
332
420
|
const response = await this.doJSON("GET", `${this.versionPrefix}/sandboxes/${id}/mounts`);
|
|
333
421
|
return response.mounts.map(fromApiMountSpecRedacted);
|
|
@@ -340,10 +428,86 @@ export class APIClient {
|
|
|
340
428
|
const response = await this.doJSON("GET", `${this.versionPrefix}/sandboxes/${id}/network/usage`);
|
|
341
429
|
return fromApiNetworkUsage(response);
|
|
342
430
|
}
|
|
431
|
+
async getAudit(id, options = {}) {
|
|
432
|
+
const params = new URLSearchParams();
|
|
433
|
+
if (options.kind)
|
|
434
|
+
params.set("kind", options.kind);
|
|
435
|
+
if (options.limit !== undefined)
|
|
436
|
+
params.set("limit", String(options.limit));
|
|
437
|
+
if (options.cursor)
|
|
438
|
+
params.set("cursor", options.cursor);
|
|
439
|
+
if (options.incarnationID)
|
|
440
|
+
params.set("incarnation_id", options.incarnationID);
|
|
441
|
+
const query = params.toString();
|
|
442
|
+
const response = await this.doJSON("GET", `${this.versionPrefix}/sandboxes/${id}/audit${query ? `?${query}` : ""}`);
|
|
443
|
+
return fromApiAuditPage(response);
|
|
444
|
+
}
|
|
343
445
|
async setNetworkLimits(id, options) {
|
|
344
446
|
const response = await this.doJSON("PATCH", `${this.versionPrefix}/sandboxes/${id}/network/limits`, toApiSetNetworkLimitsOptions(options));
|
|
345
447
|
return fromApiNetworkUsage(response);
|
|
346
448
|
}
|
|
449
|
+
async setNetworkPolicy(id, options) {
|
|
450
|
+
const response = await this.doJSON("PUT", `${this.versionPrefix}/sandboxes/${id}/network/policy`, {
|
|
451
|
+
network_block_all: options.networkBlockAll ?? false,
|
|
452
|
+
network_allow_out: options.networkAllowOut ?? [],
|
|
453
|
+
network_deny_out: options.networkDenyOut ?? [],
|
|
454
|
+
egress_profiles: options.egressProfiles ?? [],
|
|
455
|
+
network_egress_mode: options.networkEgressMode,
|
|
456
|
+
network_egress_rules: options.networkEgressRules?.map(toApiEgressRule),
|
|
457
|
+
});
|
|
458
|
+
const policy = {
|
|
459
|
+
networkBlockAll: response.network_block_all,
|
|
460
|
+
networkAllowOut: response.network_allow_out ?? [],
|
|
461
|
+
networkDenyOut: response.network_deny_out ?? [],
|
|
462
|
+
egressProfiles: response.egress_profiles ?? [],
|
|
463
|
+
networkEgressMode: response.network_egress_mode ?? "enforce",
|
|
464
|
+
networkEgressRules: (response.network_egress_rules ?? []).map(fromApiEgressRule),
|
|
465
|
+
effectiveHostnameCount: response.effective_hostname_count ?? 0,
|
|
466
|
+
};
|
|
467
|
+
if (response.egress_status)
|
|
468
|
+
policy.egressStatus = response.egress_status;
|
|
469
|
+
return policy;
|
|
470
|
+
}
|
|
471
|
+
async getNetworkLearned(id) {
|
|
472
|
+
const r = await this.doJSON("GET", `${this.versionPrefix}/sandboxes/${id}/network/learned`);
|
|
473
|
+
const learned = {
|
|
474
|
+
mode: r.mode,
|
|
475
|
+
truncated: r.truncated,
|
|
476
|
+
entries: (r.entries ?? []).map((e) => ({ host: e.host, ports: e.ports ?? [], firstSeen: e.first_seen, lastSeen: e.last_seen, hits: e.hits })),
|
|
477
|
+
cidrs: r.cidrs ?? [],
|
|
478
|
+
suggestedAllowOut: r.suggested_allow_out ?? [],
|
|
479
|
+
};
|
|
480
|
+
if (r.suggested_profile) {
|
|
481
|
+
learned.suggestedProfile = { allowOut: r.suggested_profile.allow_out ?? [], description: r.suggested_profile.description };
|
|
482
|
+
}
|
|
483
|
+
return learned;
|
|
484
|
+
}
|
|
485
|
+
async putEgressProfile(name, options) {
|
|
486
|
+
const response = await this.doJSON("PUT", this.versioned(`/egress-profiles/${encodeURIComponent(name)}`), {
|
|
487
|
+
allow_out: options.allowOut,
|
|
488
|
+
description: options.description,
|
|
489
|
+
});
|
|
490
|
+
return fromApiEgressProfile(response);
|
|
491
|
+
}
|
|
492
|
+
async getEgressProfile(name) {
|
|
493
|
+
return fromApiEgressProfile(await this.doJSON("GET", this.versioned(`/egress-profiles/${encodeURIComponent(name)}`)));
|
|
494
|
+
}
|
|
495
|
+
async listEgressProfiles(options = {}) {
|
|
496
|
+
const params = new URLSearchParams();
|
|
497
|
+
if (options.cursor)
|
|
498
|
+
params.set("cursor", options.cursor);
|
|
499
|
+
if (options.limit)
|
|
500
|
+
params.set("limit", String(options.limit));
|
|
501
|
+
const query = params.toString();
|
|
502
|
+
const response = await this.doJSON("GET", this.versioned(`/egress-profiles${query ? `?${query}` : ""}`));
|
|
503
|
+
const list = { profiles: (response.profiles ?? []).map(fromApiEgressProfile) };
|
|
504
|
+
if (response.next_cursor)
|
|
505
|
+
list.nextCursor = response.next_cursor;
|
|
506
|
+
return list;
|
|
507
|
+
}
|
|
508
|
+
async deleteEgressProfile(name) {
|
|
509
|
+
await this.doJSON("DELETE", this.versioned(`/egress-profiles/${encodeURIComponent(name)}`));
|
|
510
|
+
}
|
|
347
511
|
async createTemplate(options) {
|
|
348
512
|
const response = await this.doJSON("POST", this.versioned("/templates"), {
|
|
349
513
|
id: options.id,
|
|
@@ -441,7 +605,7 @@ export class APIClient {
|
|
|
441
605
|
* always retried regardless of HTTP method — the request never reached the
|
|
442
606
|
* server so there is no idempotency concern.
|
|
443
607
|
*
|
|
444
|
-
* HTTP-level retryable codes (429, 502, 503, 504) are retried for ALL
|
|
608
|
+
* HTTP-level retryable codes (421, 429, 502, 503, 504) are retried for ALL
|
|
445
609
|
* methods because every mutating endpoint in the daemon is already
|
|
446
610
|
* designed for idempotent retry (e.g. INSERT OR IGNORE + disambiguation).
|
|
447
611
|
*/
|
|
@@ -594,6 +758,36 @@ export class SandboxResource {
|
|
|
594
758
|
async setNetworkLimits(options) {
|
|
595
759
|
return this.client.setNetworkLimits(this.id, options);
|
|
596
760
|
}
|
|
761
|
+
/**
|
|
762
|
+
* Replaces this sandbox's egress policy while it runs. The call returns
|
|
763
|
+
* once the new policy is enforced; sending the same policy again is a
|
|
764
|
+
* no-op, so it is safe to retry.
|
|
765
|
+
*/
|
|
766
|
+
async setNetworkPolicy(options) {
|
|
767
|
+
const policy = await this.client.setNetworkPolicy(this.id, options);
|
|
768
|
+
this.networkBlockAll = policy.networkBlockAll;
|
|
769
|
+
this.egressStatus = policy.egressStatus;
|
|
770
|
+
this.egressProfiles = policy.egressProfiles.length ? policy.egressProfiles : undefined;
|
|
771
|
+
this.networkEgressMode = policy.networkEgressMode === "learn" ? "learn" : undefined;
|
|
772
|
+
this.networkEgressRules = policy.networkEgressRules.length ? policy.networkEgressRules : undefined;
|
|
773
|
+
return policy;
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Reads what this sandbox reached in learn mode and the allow list that
|
|
777
|
+
* would have allowed it. A recording stays readable after the sandbox
|
|
778
|
+
* switches to enforce, until it is destroyed.
|
|
779
|
+
*/
|
|
780
|
+
async learned() {
|
|
781
|
+
return this.client.getNetworkLearned(this.id);
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Reads this sandbox's audit log: outbound connections and egress denials
|
|
785
|
+
* (`kind: "egress"`), and secret reads. Denials carry `result: "failure"`
|
|
786
|
+
* and the policy `reason`.
|
|
787
|
+
*/
|
|
788
|
+
async audit(options) {
|
|
789
|
+
return this.client.getAudit(this.id, options);
|
|
790
|
+
}
|
|
597
791
|
toJSON() {
|
|
598
792
|
return cloneSandbox(this);
|
|
599
793
|
}
|
|
@@ -603,6 +797,8 @@ export class SandboxResource {
|
|
|
603
797
|
}
|
|
604
798
|
function toApiCreateOptions(options) {
|
|
605
799
|
return {
|
|
800
|
+
name: options.name,
|
|
801
|
+
tags: options.tags,
|
|
606
802
|
image: options.image,
|
|
607
803
|
cpu: options.cpu,
|
|
608
804
|
memory_mb: options.memoryMB,
|
|
@@ -612,6 +808,9 @@ function toApiCreateOptions(options) {
|
|
|
612
808
|
network_block_all: options.networkBlockAll,
|
|
613
809
|
network_allow_out: options.networkAllowOut,
|
|
614
810
|
network_deny_out: options.networkDenyOut,
|
|
811
|
+
egress_profiles: options.egressProfiles,
|
|
812
|
+
network_egress_mode: options.networkEgressMode,
|
|
813
|
+
network_egress_rules: options.networkEgressRules?.map(toApiEgressRule),
|
|
615
814
|
allow_public_traffic: options.allowPublicTraffic,
|
|
616
815
|
mask_request_host: options.maskRequestHost,
|
|
617
816
|
network_bytes_in_limit: options.networkBytesInLimit,
|
|
@@ -619,9 +818,13 @@ function toApiCreateOptions(options) {
|
|
|
619
818
|
registry: options.registry,
|
|
620
819
|
container_command: options.containerCommand,
|
|
621
820
|
mounts: options.mounts?.map(toApiMountSpec),
|
|
821
|
+
platform_volumes: options.platformVolumes?.map(toApiPlatformVolumeMount),
|
|
622
822
|
lifecycle: options.lifecycle ? toApiLifecycle(options.lifecycle) : undefined,
|
|
623
823
|
failover: options.failover ? toApiFailover(options.failover) : undefined,
|
|
624
824
|
runtime: options.runtime,
|
|
825
|
+
durability: options.durability,
|
|
826
|
+
module_ref: options.moduleRef,
|
|
827
|
+
tenant_id: options.tenantId,
|
|
625
828
|
custom_domains: options.customDomains,
|
|
626
829
|
};
|
|
627
830
|
}
|
|
@@ -683,6 +886,8 @@ function toApiFailover(failover) {
|
|
|
683
886
|
function fromApiSandbox(sandbox) {
|
|
684
887
|
return {
|
|
685
888
|
id: sandbox.id,
|
|
889
|
+
name: sandbox.name || undefined,
|
|
890
|
+
tags: sandbox.tags,
|
|
686
891
|
image: sandbox.image,
|
|
687
892
|
status: sandbox.status,
|
|
688
893
|
publicURL: sandbox.public_url,
|
|
@@ -694,6 +899,11 @@ function fromApiSandbox(sandbox) {
|
|
|
694
899
|
osUser: sandbox.os_user,
|
|
695
900
|
env: sandbox.env,
|
|
696
901
|
networkBlockAll: sandbox.network_block_all,
|
|
902
|
+
egressStatus: sandbox.egress_status || undefined,
|
|
903
|
+
egressProfiles: sandbox.egress_profiles?.length ? sandbox.egress_profiles : undefined,
|
|
904
|
+
egressProfilesApplied: sandbox.egress_profiles_applied?.length ? sandbox.egress_profiles_applied : undefined,
|
|
905
|
+
networkEgressMode: sandbox.network_egress_mode || undefined,
|
|
906
|
+
networkEgressRules: sandbox.network_egress_rules?.length ? sandbox.network_egress_rules.map(fromApiEgressRule) : undefined,
|
|
697
907
|
toolboxEnabled: sandbox.toolbox_enabled,
|
|
698
908
|
sshPublicKey: sandbox.ssh_public_key,
|
|
699
909
|
exposedPorts: sandbox.exposed_ports?.map(fromApiExposedPort),
|
|
@@ -705,13 +915,22 @@ function fromApiSandbox(sandbox) {
|
|
|
705
915
|
lifecycle: fromApiLifecycle(sandbox.lifecycle),
|
|
706
916
|
failover: fromApiFailover(sandbox.failover),
|
|
707
917
|
runtime: normalizeRuntime(sandbox.runtime),
|
|
918
|
+
durability: sandbox.durability,
|
|
919
|
+
moduleRef: sandbox.module_ref,
|
|
920
|
+
moduleDigest: sandbox.module_digest,
|
|
921
|
+
tenantId: sandbox.tenant_id,
|
|
708
922
|
};
|
|
709
923
|
}
|
|
710
924
|
// normalizeRuntime narrows the wire-level string to the union we expose, while
|
|
711
925
|
// tolerating older sandboxd versions that don't send the field at all (treat
|
|
712
926
|
// as "" — i.e. host default at start time).
|
|
713
927
|
function normalizeRuntime(value) {
|
|
714
|
-
if (value === "docker" ||
|
|
928
|
+
if (value === "docker" ||
|
|
929
|
+
value === "gvisor" ||
|
|
930
|
+
value === "kata" ||
|
|
931
|
+
value === "firecracker" ||
|
|
932
|
+
value === "wasm" ||
|
|
933
|
+
value === "isolate") {
|
|
715
934
|
return value;
|
|
716
935
|
}
|
|
717
936
|
return "";
|
|
@@ -856,6 +1075,13 @@ function toApiMountSpec(mount) {
|
|
|
856
1075
|
read_only: mount.readOnly,
|
|
857
1076
|
};
|
|
858
1077
|
}
|
|
1078
|
+
function toApiPlatformVolumeMount(volume) {
|
|
1079
|
+
return {
|
|
1080
|
+
name: volume.name,
|
|
1081
|
+
path: volume.path,
|
|
1082
|
+
read_only: volume.readOnly,
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
859
1085
|
function fromApiMountSpecRedacted(mount) {
|
|
860
1086
|
return {
|
|
861
1087
|
type: mount.type,
|
|
@@ -866,6 +1092,31 @@ function fromApiMountSpecRedacted(mount) {
|
|
|
866
1092
|
hasCredentials: mount.has_credentials,
|
|
867
1093
|
};
|
|
868
1094
|
}
|
|
1095
|
+
function fromApiAuditPage(page) {
|
|
1096
|
+
const out = {
|
|
1097
|
+
events: (page.events ?? []).map((e) => ({
|
|
1098
|
+
time: e.time,
|
|
1099
|
+
kind: e.kind,
|
|
1100
|
+
result: e.result,
|
|
1101
|
+
reason: e.reason,
|
|
1102
|
+
destination: e.destination,
|
|
1103
|
+
network: e.network,
|
|
1104
|
+
actor: e.actor,
|
|
1105
|
+
ref: e.ref,
|
|
1106
|
+
eventID: e.event_id,
|
|
1107
|
+
incarnationID: e.incarnation_id,
|
|
1108
|
+
dropped: e.dropped,
|
|
1109
|
+
})),
|
|
1110
|
+
coverage: {
|
|
1111
|
+
answered: page.coverage?.answered ?? [],
|
|
1112
|
+
missing: page.coverage?.missing ?? [],
|
|
1113
|
+
partial: page.coverage?.partial ?? false,
|
|
1114
|
+
},
|
|
1115
|
+
};
|
|
1116
|
+
if (page.next_cursor)
|
|
1117
|
+
out.nextCursor = page.next_cursor;
|
|
1118
|
+
return out;
|
|
1119
|
+
}
|
|
869
1120
|
function fromApiNetworkUsage(usage) {
|
|
870
1121
|
return {
|
|
871
1122
|
sandboxID: usage.sandbox_id,
|
|
@@ -925,10 +1176,32 @@ function buildTagQuery(tags) {
|
|
|
925
1176
|
const parts = entries.map(([key, value]) => `tag.${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
|
|
926
1177
|
return "?" + parts.join("&");
|
|
927
1178
|
}
|
|
1179
|
+
function buildIncludeEnvQuery(includeEnv) {
|
|
1180
|
+
return includeEnv ? "?include_env=true" : "";
|
|
1181
|
+
}
|
|
1182
|
+
function buildSandboxListQuery(options) {
|
|
1183
|
+
let query = buildTagQuery(options?.tags);
|
|
1184
|
+
if (options?.includeEnv) {
|
|
1185
|
+
query = appendQueryParam(query, "include_env", "true");
|
|
1186
|
+
}
|
|
1187
|
+
const name = options?.name?.trim();
|
|
1188
|
+
if (name) {
|
|
1189
|
+
query = appendQueryParam(query, "name", name);
|
|
1190
|
+
}
|
|
1191
|
+
return query;
|
|
1192
|
+
}
|
|
1193
|
+
function appendQueryParam(path, key, value) {
|
|
1194
|
+
if (!value) {
|
|
1195
|
+
return path;
|
|
1196
|
+
}
|
|
1197
|
+
const sep = path.includes("?") ? "&" : "?";
|
|
1198
|
+
return `${path}${sep}${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
|
1199
|
+
}
|
|
928
1200
|
function cloneSandbox(sandbox) {
|
|
929
1201
|
return {
|
|
930
1202
|
...sandbox,
|
|
931
1203
|
env: sandbox.env ? { ...sandbox.env } : undefined,
|
|
1204
|
+
tags: sandbox.tags ? { ...sandbox.tags } : undefined,
|
|
932
1205
|
exposedPorts: sandbox.exposedPorts?.map((port) => ({ ...port })),
|
|
933
1206
|
containerCommand: sandbox.containerCommand ? [...sandbox.containerCommand] : undefined,
|
|
934
1207
|
lifecycle: { ...sandbox.lifecycle },
|
package/dist/types.d.ts
CHANGED
|
@@ -65,6 +65,20 @@ export interface MountSpecRedacted {
|
|
|
65
65
|
readOnly: boolean;
|
|
66
66
|
hasCredentials: boolean;
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* A named, operator-backed persistent volume to attach by name. The operator
|
|
70
|
+
* configures the shared backend (S3/NFS); you never supply storage coordinates
|
|
71
|
+
* or credentials. Persists across the sandbox lifecycle and can be re-attached
|
|
72
|
+
* by name from another sandbox.
|
|
73
|
+
*/
|
|
74
|
+
export interface PlatformVolumeMount {
|
|
75
|
+
/** Volume name, scoped to your tenant. */
|
|
76
|
+
name: string;
|
|
77
|
+
/** Absolute path to mount it at inside the sandbox. */
|
|
78
|
+
path: string;
|
|
79
|
+
/** Mount read-only. Defaults to read-write. */
|
|
80
|
+
readOnly?: boolean;
|
|
81
|
+
}
|
|
68
82
|
export interface Lifecycle {
|
|
69
83
|
/** Duration in integer nanoseconds. */
|
|
70
84
|
stopIfIdleFor?: number;
|
|
@@ -121,6 +135,15 @@ export interface GPUOptions {
|
|
|
121
135
|
}
|
|
122
136
|
import type { Image } from "./Image.js";
|
|
123
137
|
export interface CreateOptions {
|
|
138
|
+
/**
|
|
139
|
+
* Optional name, unique among the caller's sandboxes (another account may
|
|
140
|
+
* use the same name). A repeat create with a name the caller already holds
|
|
141
|
+
* returns HTTP 409. Names starting with `owner:` or shaped like a sandbox
|
|
142
|
+
* ID (`sb-` plus 16 hex) are rejected.
|
|
143
|
+
*/
|
|
144
|
+
name?: string;
|
|
145
|
+
/** Free-form key/value labels; filter on them with `ListOptions.tags`. */
|
|
146
|
+
tags?: Record<string, string>;
|
|
124
147
|
/**
|
|
125
148
|
* The base image for this sandbox. A bare image reference (e.g.
|
|
126
149
|
* `"ubuntu:22.04"`) is pulled by the daemon as-is. An {@link Image} builder
|
|
@@ -138,21 +161,42 @@ export interface CreateOptions {
|
|
|
138
161
|
osUser?: string;
|
|
139
162
|
networkBlockAll?: boolean;
|
|
140
163
|
/**
|
|
141
|
-
* Egress allowlist
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* `
|
|
164
|
+
* Egress allowlist: CIDRs, hostnames (`pypi.org`), `*.suffix` wildcards and
|
|
165
|
+
* `host:port` entries. Alone, the sandbox may reach only these. With
|
|
166
|
+
* `networkDenyOut`, allow wins, then deny, then the default is allow; a
|
|
167
|
+
* deny of `0.0.0.0/0` makes it an allowlist. For a full block use
|
|
168
|
+
* `networkBlockAll`.
|
|
145
169
|
*/
|
|
146
170
|
networkAllowOut?: string[];
|
|
147
171
|
/**
|
|
148
|
-
* Egress blocklist of CIDRs.
|
|
149
|
-
* these destinations.
|
|
172
|
+
* Egress blocklist of CIDRs (never hostnames). Alone, the sandbox may reach
|
|
173
|
+
* anything except these destinations.
|
|
150
174
|
*/
|
|
151
175
|
networkDenyOut?: string[];
|
|
152
176
|
/**
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
177
|
+
* Named egress profiles whose entries join `networkAllowOut` (see
|
|
178
|
+
* `putEgressProfile`). A profile change reaches every sandbox that
|
|
179
|
+
* references it.
|
|
180
|
+
*/
|
|
181
|
+
egressProfiles?: string[];
|
|
182
|
+
/**
|
|
183
|
+
* `"learn"` gives the sandbox open egress and records what it reaches, so
|
|
184
|
+
* `learned()` can suggest an allow list. Trusted runs only. Needs no lists,
|
|
185
|
+
* profiles or block-all. Omitted is `"enforce"`.
|
|
186
|
+
*/
|
|
187
|
+
networkEgressMode?: "enforce" | "learn";
|
|
188
|
+
/**
|
|
189
|
+
* Method and path rules that refine hosts the allow list already admits
|
|
190
|
+
* (at most 32). A rule with `inspect: true` makes the egress gateway
|
|
191
|
+
* terminate TLS on 443 with the node's CA, which only a sandbox created
|
|
192
|
+
* with such a rule trusts, so set inspect rules here rather than adding
|
|
193
|
+
* them later.
|
|
194
|
+
*/
|
|
195
|
+
networkEgressRules?: EgressRule[];
|
|
196
|
+
/**
|
|
197
|
+
* Whether the sandbox may be exposed to the public internet. Omitted defaults
|
|
198
|
+
* to private (no public URL, `exposePort` fails). Set `true` to opt in to
|
|
199
|
+
* public exposure; `false` permanently refuses it for this sandbox.
|
|
156
200
|
*/
|
|
157
201
|
allowPublicTraffic?: boolean;
|
|
158
202
|
/**
|
|
@@ -179,6 +223,12 @@ export interface CreateOptions {
|
|
|
179
223
|
registry?: RegistryAuth;
|
|
180
224
|
containerCommand?: string[];
|
|
181
225
|
mounts?: MountSpec[];
|
|
226
|
+
/**
|
|
227
|
+
* Named, operator-backed persistent volumes to attach by name. Requires the
|
|
228
|
+
* operator to have enabled platform volumes; otherwise the create is rejected
|
|
229
|
+
* with 412. Only supported on the docker/gvisor runtimes.
|
|
230
|
+
*/
|
|
231
|
+
platformVolumes?: PlatformVolumeMount[];
|
|
182
232
|
lifecycle?: Lifecycle;
|
|
183
233
|
failover?: Failover;
|
|
184
234
|
/**
|
|
@@ -191,15 +241,22 @@ export interface CreateOptions {
|
|
|
191
241
|
*
|
|
192
242
|
* GPU access is not supported with `"gvisor"`.
|
|
193
243
|
*/
|
|
194
|
-
runtime?: "docker" | "gvisor" | "kata" | "firecracker" | "wasm";
|
|
244
|
+
runtime?: "docker" | "gvisor" | "kata" | "firecracker" | "wasm" | "isolate";
|
|
195
245
|
/**
|
|
196
246
|
* Survival class across daemon restarts. Omit for the runtime default:
|
|
197
|
-
* `passivatable` for docker/gvisor/firecracker, `ephemeral` for wasm.
|
|
247
|
+
* `passivatable` for docker/gvisor/firecracker, `ephemeral` for wasm/isolate.
|
|
198
248
|
* `durable` is wasm-only and not yet implemented.
|
|
199
249
|
*/
|
|
200
250
|
durability?: Durability;
|
|
201
|
-
/** WASM module reference. When runtime is wasm, may be used instead of image. */
|
|
251
|
+
/** WASM module / isolate bundle reference. When runtime is wasm or isolate, may be used instead of image. */
|
|
202
252
|
moduleRef?: string;
|
|
253
|
+
/**
|
|
254
|
+
* Isolate-group key for `runtime: "isolate"`. Sandboxes with the same
|
|
255
|
+
* tenant share one workerd process. Server-authorized; when omitted the
|
|
256
|
+
* group key falls back to the authenticated identity. Ignored by other
|
|
257
|
+
* runtimes.
|
|
258
|
+
*/
|
|
259
|
+
tenantId?: string;
|
|
203
260
|
/**
|
|
204
261
|
* Attach GPU resources to the sandbox. Omit for CPU-only workloads.
|
|
205
262
|
* Not compatible with runtime="gvisor".
|
|
@@ -227,6 +284,25 @@ export interface ListOptions {
|
|
|
227
284
|
* or pass an empty map to list every sandbox visible to the PAT.
|
|
228
285
|
*/
|
|
229
286
|
tags?: Record<string, string>;
|
|
287
|
+
/**
|
|
288
|
+
* When true, include sandbox env vars in the response (`?include_env=true`).
|
|
289
|
+
* Env is omitted by default — opt-in reads are audited server-side.
|
|
290
|
+
*/
|
|
291
|
+
includeEnv?: boolean;
|
|
292
|
+
/**
|
|
293
|
+
* Filter to the caller's sandbox with exactly this name (`?name=<name>`);
|
|
294
|
+
* the result has zero or one entries. Names are unique per owner. Servers
|
|
295
|
+
* that predate name lookup ignore the filter and return a normal list, so
|
|
296
|
+
* prefer `MicroVM.getByName`, which checks the reply.
|
|
297
|
+
*/
|
|
298
|
+
name?: string;
|
|
299
|
+
}
|
|
300
|
+
export interface GetOptions {
|
|
301
|
+
/**
|
|
302
|
+
* When true, include sandbox env vars in the response (`?include_env=true`).
|
|
303
|
+
* Env is omitted by default — opt-in reads are audited server-side.
|
|
304
|
+
*/
|
|
305
|
+
includeEnv?: boolean;
|
|
230
306
|
}
|
|
231
307
|
export interface CreateSessionOptions {
|
|
232
308
|
name?: string;
|
|
@@ -389,6 +465,10 @@ export type ExposeResult = {
|
|
|
389
465
|
};
|
|
390
466
|
export interface Sandbox {
|
|
391
467
|
id: string;
|
|
468
|
+
/** Name set at create time, unique per owner. Absent for unnamed sandboxes. */
|
|
469
|
+
name?: string;
|
|
470
|
+
/** Labels set at create time. */
|
|
471
|
+
tags?: Record<string, string>;
|
|
392
472
|
image: string;
|
|
393
473
|
status: SandboxStatus;
|
|
394
474
|
publicURL: string;
|
|
@@ -400,6 +480,23 @@ export interface Sandbox {
|
|
|
400
480
|
osUser: string;
|
|
401
481
|
env?: Record<string, string>;
|
|
402
482
|
networkBlockAll: boolean;
|
|
483
|
+
/**
|
|
484
|
+
* Hostname-egress state on `get` for a sandbox whose allow list names
|
|
485
|
+
* hosts (container runtimes): "active", "held" or "unavailable". Absent
|
|
486
|
+
* otherwise, and on list results.
|
|
487
|
+
*/
|
|
488
|
+
egressStatus?: string;
|
|
489
|
+
/** Egress profiles this sandbox references. */
|
|
490
|
+
egressProfiles?: string[];
|
|
491
|
+
/**
|
|
492
|
+
* The generation of each referenced profile that is live on the sandbox,
|
|
493
|
+
* so a caller can see when a profile change has reached it.
|
|
494
|
+
*/
|
|
495
|
+
egressProfilesApplied?: EgressProfileRef[];
|
|
496
|
+
/** `"learn"` while the sandbox records its egress; absent otherwise. */
|
|
497
|
+
networkEgressMode?: string;
|
|
498
|
+
/** Method and path rules on the sandbox's egress; absent when it has none. */
|
|
499
|
+
networkEgressRules?: EgressRule[];
|
|
403
500
|
toolboxEnabled: boolean;
|
|
404
501
|
sshPublicKey?: string;
|
|
405
502
|
sshPrivateKey?: string;
|
|
@@ -415,13 +512,15 @@ export interface Sandbox {
|
|
|
415
512
|
* Container runtime this sandbox is running under. Empty string indicates
|
|
416
513
|
* a pre-migration row that resolves to the host default at start time.
|
|
417
514
|
*/
|
|
418
|
-
runtime: "" | "docker" | "gvisor" | "kata" | "firecracker" | "wasm";
|
|
515
|
+
runtime: "" | "docker" | "gvisor" | "kata" | "firecracker" | "wasm" | "isolate";
|
|
419
516
|
/** Survival class this sandbox was created with. */
|
|
420
517
|
durability?: Durability;
|
|
421
|
-
/** Resolved WASM module reference when runtime is wasm. */
|
|
518
|
+
/** Resolved WASM module / isolate bundle reference when runtime is wasm or isolate. */
|
|
422
519
|
moduleRef?: string;
|
|
423
520
|
/** sha256 hex digest of the resolved WASM module bytes. */
|
|
424
521
|
moduleDigest?: string;
|
|
522
|
+
/** Isolate-group key this sandbox was created under (`runtime: "isolate"` only). */
|
|
523
|
+
tenantId?: string;
|
|
425
524
|
/** GPU configuration this sandbox was created with. Absent means no GPU. */
|
|
426
525
|
gpus?: GPUOptions;
|
|
427
526
|
}
|
|
@@ -443,6 +542,214 @@ export interface NetworkUsage {
|
|
|
443
542
|
/** Absent until the netstats poller has produced at least one sample. */
|
|
444
543
|
lastSampledAt?: string;
|
|
445
544
|
}
|
|
545
|
+
/** One record from a sandbox's audit log (`sandbox.audit()`). */
|
|
546
|
+
export interface AuditEvent {
|
|
547
|
+
time: string;
|
|
548
|
+
/** "egress" for outbound connections and denials; secret kinds otherwise. */
|
|
549
|
+
kind?: string;
|
|
550
|
+
/** "success", or "failure" for a denial. */
|
|
551
|
+
result: string;
|
|
552
|
+
/** Why it was denied, e.g. "host_not_allowed", "sni_not_allowed". */
|
|
553
|
+
reason?: string;
|
|
554
|
+
/** host:port (or host) the sandbox tried to reach. */
|
|
555
|
+
destination?: string;
|
|
556
|
+
network?: string;
|
|
557
|
+
actor?: string;
|
|
558
|
+
ref?: string;
|
|
559
|
+
eventID?: string;
|
|
560
|
+
incarnationID?: string;
|
|
561
|
+
/** Records lost at this point (a gap record). */
|
|
562
|
+
dropped?: number;
|
|
563
|
+
}
|
|
564
|
+
export interface AuditPage {
|
|
565
|
+
events: AuditEvent[];
|
|
566
|
+
/** Which nodes answered; `partial` is true when some could not. */
|
|
567
|
+
coverage: {
|
|
568
|
+
answered: string[];
|
|
569
|
+
missing: string[];
|
|
570
|
+
partial: boolean;
|
|
571
|
+
};
|
|
572
|
+
/** Pass to `audit({ cursor })` for the next page. */
|
|
573
|
+
nextCursor?: string;
|
|
574
|
+
}
|
|
575
|
+
export interface AuditOptions {
|
|
576
|
+
/** Only this kind, e.g. "egress". */
|
|
577
|
+
kind?: string;
|
|
578
|
+
limit?: number;
|
|
579
|
+
cursor?: string;
|
|
580
|
+
incarnationID?: string;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Would a sandbox created with these egress fields reach `destination`?
|
|
584
|
+
* `destination` is "host", "host:port", "IP" or "IP:port"; a bare host is
|
|
585
|
+
* checked as the web ports.
|
|
586
|
+
*/
|
|
587
|
+
export interface NetworkPolicyCheckOptions {
|
|
588
|
+
networkBlockAll?: boolean;
|
|
589
|
+
networkAllowOut?: string[];
|
|
590
|
+
networkDenyOut?: string[];
|
|
591
|
+
destination: string;
|
|
592
|
+
}
|
|
593
|
+
export interface NetworkPolicyCheckResult {
|
|
594
|
+
allowed: boolean;
|
|
595
|
+
/** The entry that decided; "" when the default verdict did. */
|
|
596
|
+
matchedRule: string;
|
|
597
|
+
/** "allow" or "deny": what happens to a destination no entry matches. */
|
|
598
|
+
defaultVerdict: string;
|
|
599
|
+
/** The first allow entry outside this deployment's ceiling, if any. */
|
|
600
|
+
outsideCeiling?: string;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* A sandbox's whole egress policy, for `setNetworkPolicy`. It replaces the
|
|
604
|
+
* current policy: a field left out is cleared, so `{}` means open egress.
|
|
605
|
+
* The grammar is the create one (hostnames, `*.` wildcards, `host:port` and
|
|
606
|
+
* CIDRs in the allow list; CIDRs only in the deny list).
|
|
607
|
+
*/
|
|
608
|
+
export interface NetworkPolicyOptions {
|
|
609
|
+
networkBlockAll?: boolean;
|
|
610
|
+
networkAllowOut?: string[];
|
|
611
|
+
networkDenyOut?: string[];
|
|
612
|
+
egressProfiles?: string[];
|
|
613
|
+
networkEgressMode?: "enforce" | "learn";
|
|
614
|
+
/**
|
|
615
|
+
* Replaces the method and path rules. Adding an inspect rule to a
|
|
616
|
+
* container sandbox created without one is refused with 409: recreate it
|
|
617
|
+
* with the rule.
|
|
618
|
+
*/
|
|
619
|
+
networkEgressRules?: EgressRule[];
|
|
620
|
+
}
|
|
621
|
+
/** The policy a sandbox enforces after `setNetworkPolicy`. */
|
|
622
|
+
export interface NetworkPolicy {
|
|
623
|
+
networkBlockAll: boolean;
|
|
624
|
+
networkAllowOut: string[];
|
|
625
|
+
networkDenyOut: string[];
|
|
626
|
+
egressProfiles: string[];
|
|
627
|
+
/** `"enforce"` or `"learn"`. */
|
|
628
|
+
networkEgressMode: string;
|
|
629
|
+
networkEgressRules: EgressRule[];
|
|
630
|
+
/** Hostname entries in force: inline plus every profile's (at most 1024). */
|
|
631
|
+
effectiveHostnameCount: number;
|
|
632
|
+
/** "active", "held" or "unavailable" for hostname rules on a container. */
|
|
633
|
+
egressStatus?: string;
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* One method, path or program rule. Rules refine a host the allow list
|
|
637
|
+
* already admits: a request to a ruled host passes when some rule for that
|
|
638
|
+
* host admits its program, method and path, and gets a 403 otherwise. A host
|
|
639
|
+
* no rule names keeps its allow-list decision.
|
|
640
|
+
*/
|
|
641
|
+
export interface EgressRule {
|
|
642
|
+
/** An exact name or `*.` wildcard, without a port. */
|
|
643
|
+
host: string;
|
|
644
|
+
/**
|
|
645
|
+
* `[80]` by default, or `[443]` with `inspect`; only 80 and 443, except
|
|
646
|
+
* that a rule with only `binaries` may name any port the allow list opens.
|
|
647
|
+
*/
|
|
648
|
+
ports?: number[];
|
|
649
|
+
/** Exact, upper case (`GET`, `POST`); empty allows any. */
|
|
650
|
+
methods?: string[];
|
|
651
|
+
/**
|
|
652
|
+
* Path globs: `*` within one segment, `**` as a whole segment for any
|
|
653
|
+
* number of them. Empty allows any path.
|
|
654
|
+
*/
|
|
655
|
+
paths?: string[];
|
|
656
|
+
/**
|
|
657
|
+
* Terminate TLS on 443 with the node's CA so the rule can see requests.
|
|
658
|
+
* The request's Host must then equal the TLS server name.
|
|
659
|
+
*/
|
|
660
|
+
inspect?: boolean;
|
|
661
|
+
/**
|
|
662
|
+
* Replace a header on the requests this rule allows with a secret from
|
|
663
|
+
* the sandbox's own `env`, which the sandbox itself only sees as a
|
|
664
|
+
* placeholder. Needs `inspect: true`.
|
|
665
|
+
*/
|
|
666
|
+
inject?: EgressInject;
|
|
667
|
+
/**
|
|
668
|
+
* Limit the rule to connections opened by these executables: clean
|
|
669
|
+
* absolute paths inside the sandbox, at most 16. For an interpreter
|
|
670
|
+
* (python, node, a shell) the script it runs counts too, so
|
|
671
|
+
* `/usr/local/bin/pip` works. A rule with only `binaries` decides whole
|
|
672
|
+
* connections, on any port the allow list opens. Runc sandboxes only
|
|
673
|
+
* (docker and containerd); least privilege for trusted tooling, not a
|
|
674
|
+
* security boundary.
|
|
675
|
+
*/
|
|
676
|
+
binaries?: string[];
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* A rule's credential injection. The sandbox's env holds
|
|
680
|
+
* `aerolvm-placeholder:<KEY>` in place of the value, and the egress gateway
|
|
681
|
+
* replaces `header` with the real value on each request the rule allows, so
|
|
682
|
+
* code in the sandbox never holds the secret.
|
|
683
|
+
*/
|
|
684
|
+
export interface EgressInject {
|
|
685
|
+
/**
|
|
686
|
+
* The header to replace, such as `Authorization`. It is replaced, never
|
|
687
|
+
* added to a body or URL. Headers that frame or route the request, such as
|
|
688
|
+
* `Host` or `Content-Length`, can't be injected.
|
|
689
|
+
*/
|
|
690
|
+
header: string;
|
|
691
|
+
/**
|
|
692
|
+
* `"env:<KEY>"`: a key in the create's `env`, whose value is the whole
|
|
693
|
+
* header value (for example `Bearer ghp_...`). Rotating it means
|
|
694
|
+
* recreating the sandbox.
|
|
695
|
+
*/
|
|
696
|
+
secretRef: string;
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* A named allowlist sandboxes reference through `egressProfiles`. Profiles
|
|
700
|
+
* belong to your account; `generation` goes up by one on every change.
|
|
701
|
+
*/
|
|
702
|
+
export interface EgressProfile {
|
|
703
|
+
name: string;
|
|
704
|
+
/** Hostnames, `*.` wildcards, `host:port` entries and CIDRs (at most 512 hostnames). */
|
|
705
|
+
allowOut: string[];
|
|
706
|
+
description?: string;
|
|
707
|
+
generation: number;
|
|
708
|
+
createdAt: string;
|
|
709
|
+
updatedAt: string;
|
|
710
|
+
}
|
|
711
|
+
/** The body of `putEgressProfile`: a full replace. */
|
|
712
|
+
export interface EgressProfileOptions {
|
|
713
|
+
allowOut: string[];
|
|
714
|
+
description?: string;
|
|
715
|
+
}
|
|
716
|
+
export interface ListEgressProfilesOptions {
|
|
717
|
+
cursor?: string;
|
|
718
|
+
limit?: number;
|
|
719
|
+
}
|
|
720
|
+
export interface EgressProfileList {
|
|
721
|
+
profiles: EgressProfile[];
|
|
722
|
+
/** Pass back as `cursor` for the next page; absent on the last one. */
|
|
723
|
+
nextCursor?: string;
|
|
724
|
+
}
|
|
725
|
+
/** One destination a learn-mode sandbox reached. */
|
|
726
|
+
export interface NetworkLearnedEntry {
|
|
727
|
+
host: string;
|
|
728
|
+
/** Connection ports; empty when the name was only resolved. */
|
|
729
|
+
ports: number[];
|
|
730
|
+
firstSeen: string;
|
|
731
|
+
lastSeen: string;
|
|
732
|
+
hits: number;
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* What a sandbox reached in learn mode, and the allow list that would have
|
|
736
|
+
* allowed it: `suggestedAllowOut` when it fits 64 hostnames, otherwise
|
|
737
|
+
* `suggestedProfile` (a body for `putEgressProfile`).
|
|
738
|
+
*/
|
|
739
|
+
export interface NetworkLearned {
|
|
740
|
+
mode: string;
|
|
741
|
+
/** Recording stopped at its cap. */
|
|
742
|
+
truncated: boolean;
|
|
743
|
+
entries: NetworkLearnedEntry[];
|
|
744
|
+
cidrs: string[];
|
|
745
|
+
suggestedAllowOut: string[];
|
|
746
|
+
suggestedProfile?: EgressProfileOptions;
|
|
747
|
+
}
|
|
748
|
+
/** A referenced profile and the generation of it that is live on a sandbox. */
|
|
749
|
+
export interface EgressProfileRef {
|
|
750
|
+
name: string;
|
|
751
|
+
generation: number;
|
|
752
|
+
}
|
|
446
753
|
export interface SetNetworkLimitsOptions {
|
|
447
754
|
/** Omit (undefined) to leave unchanged. `0` means unlimited. */
|
|
448
755
|
networkBytesInLimit?: number;
|
package/package.json
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aerol-ai/aerolvm-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"private": false,
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/aerol-ai/microvm.git",
|
|
8
|
+
"directory": "sdk/typescript"
|
|
9
|
+
},
|
|
5
10
|
"type": "module",
|
|
6
11
|
"main": "./dist/index.js",
|
|
7
12
|
"types": "./dist/index.d.ts",
|
|
@@ -21,8 +26,8 @@
|
|
|
21
26
|
"test": "tsc -p tsconfig.test.json --outDir dist-test && node --test dist-test/*.test.js dist-test/internal/*.test.js"
|
|
22
27
|
},
|
|
23
28
|
"devDependencies": {
|
|
24
|
-
"@types/node": "^
|
|
25
|
-
"typescript": "^
|
|
26
|
-
"tsx": "^4.
|
|
29
|
+
"@types/node": "^26.6.3",
|
|
30
|
+
"typescript": "^7.0.2",
|
|
31
|
+
"tsx": "^4.23.15"
|
|
27
32
|
}
|
|
28
33
|
}
|