@aerol-ai/aerolvm-sdk 0.5.4 → 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 +273 -8
- package/dist/types.d.ts +301 -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, PlatformVolumeMount, 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");
|
|
157
205
|
}
|
|
158
|
-
|
|
159
|
-
|
|
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]);
|
|
228
|
+
}
|
|
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,
|
|
@@ -623,6 +822,9 @@ function toApiCreateOptions(options) {
|
|
|
623
822
|
lifecycle: options.lifecycle ? toApiLifecycle(options.lifecycle) : undefined,
|
|
624
823
|
failover: options.failover ? toApiFailover(options.failover) : undefined,
|
|
625
824
|
runtime: options.runtime,
|
|
825
|
+
durability: options.durability,
|
|
826
|
+
module_ref: options.moduleRef,
|
|
827
|
+
tenant_id: options.tenantId,
|
|
626
828
|
custom_domains: options.customDomains,
|
|
627
829
|
};
|
|
628
830
|
}
|
|
@@ -684,6 +886,8 @@ function toApiFailover(failover) {
|
|
|
684
886
|
function fromApiSandbox(sandbox) {
|
|
685
887
|
return {
|
|
686
888
|
id: sandbox.id,
|
|
889
|
+
name: sandbox.name || undefined,
|
|
890
|
+
tags: sandbox.tags,
|
|
687
891
|
image: sandbox.image,
|
|
688
892
|
status: sandbox.status,
|
|
689
893
|
publicURL: sandbox.public_url,
|
|
@@ -695,6 +899,11 @@ function fromApiSandbox(sandbox) {
|
|
|
695
899
|
osUser: sandbox.os_user,
|
|
696
900
|
env: sandbox.env,
|
|
697
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,
|
|
698
907
|
toolboxEnabled: sandbox.toolbox_enabled,
|
|
699
908
|
sshPublicKey: sandbox.ssh_public_key,
|
|
700
909
|
exposedPorts: sandbox.exposed_ports?.map(fromApiExposedPort),
|
|
@@ -706,13 +915,22 @@ function fromApiSandbox(sandbox) {
|
|
|
706
915
|
lifecycle: fromApiLifecycle(sandbox.lifecycle),
|
|
707
916
|
failover: fromApiFailover(sandbox.failover),
|
|
708
917
|
runtime: normalizeRuntime(sandbox.runtime),
|
|
918
|
+
durability: sandbox.durability,
|
|
919
|
+
moduleRef: sandbox.module_ref,
|
|
920
|
+
moduleDigest: sandbox.module_digest,
|
|
921
|
+
tenantId: sandbox.tenant_id,
|
|
709
922
|
};
|
|
710
923
|
}
|
|
711
924
|
// normalizeRuntime narrows the wire-level string to the union we expose, while
|
|
712
925
|
// tolerating older sandboxd versions that don't send the field at all (treat
|
|
713
926
|
// as "" — i.e. host default at start time).
|
|
714
927
|
function normalizeRuntime(value) {
|
|
715
|
-
if (value === "docker" ||
|
|
928
|
+
if (value === "docker" ||
|
|
929
|
+
value === "gvisor" ||
|
|
930
|
+
value === "kata" ||
|
|
931
|
+
value === "firecracker" ||
|
|
932
|
+
value === "wasm" ||
|
|
933
|
+
value === "isolate") {
|
|
716
934
|
return value;
|
|
717
935
|
}
|
|
718
936
|
return "";
|
|
@@ -874,6 +1092,31 @@ function fromApiMountSpecRedacted(mount) {
|
|
|
874
1092
|
hasCredentials: mount.has_credentials,
|
|
875
1093
|
};
|
|
876
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
|
+
}
|
|
877
1120
|
function fromApiNetworkUsage(usage) {
|
|
878
1121
|
return {
|
|
879
1122
|
sandboxID: usage.sandbox_id,
|
|
@@ -933,10 +1176,32 @@ function buildTagQuery(tags) {
|
|
|
933
1176
|
const parts = entries.map(([key, value]) => `tag.${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
|
|
934
1177
|
return "?" + parts.join("&");
|
|
935
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
|
+
}
|
|
936
1200
|
function cloneSandbox(sandbox) {
|
|
937
1201
|
return {
|
|
938
1202
|
...sandbox,
|
|
939
1203
|
env: sandbox.env ? { ...sandbox.env } : undefined,
|
|
1204
|
+
tags: sandbox.tags ? { ...sandbox.tags } : undefined,
|
|
940
1205
|
exposedPorts: sandbox.exposedPorts?.map((port) => ({ ...port })),
|
|
941
1206
|
containerCommand: sandbox.containerCommand ? [...sandbox.containerCommand] : undefined,
|
|
942
1207
|
lifecycle: { ...sandbox.lifecycle },
|
package/dist/types.d.ts
CHANGED
|
@@ -135,6 +135,15 @@ export interface GPUOptions {
|
|
|
135
135
|
}
|
|
136
136
|
import type { Image } from "./Image.js";
|
|
137
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>;
|
|
138
147
|
/**
|
|
139
148
|
* The base image for this sandbox. A bare image reference (e.g.
|
|
140
149
|
* `"ubuntu:22.04"`) is pulled by the daemon as-is. An {@link Image} builder
|
|
@@ -152,21 +161,42 @@ export interface CreateOptions {
|
|
|
152
161
|
osUser?: string;
|
|
153
162
|
networkBlockAll?: boolean;
|
|
154
163
|
/**
|
|
155
|
-
* Egress allowlist
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* `
|
|
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`.
|
|
159
169
|
*/
|
|
160
170
|
networkAllowOut?: string[];
|
|
161
171
|
/**
|
|
162
|
-
* Egress blocklist of CIDRs.
|
|
163
|
-
* these destinations.
|
|
172
|
+
* Egress blocklist of CIDRs (never hostnames). Alone, the sandbox may reach
|
|
173
|
+
* anything except these destinations.
|
|
164
174
|
*/
|
|
165
175
|
networkDenyOut?: string[];
|
|
166
176
|
/**
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
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.
|
|
170
200
|
*/
|
|
171
201
|
allowPublicTraffic?: boolean;
|
|
172
202
|
/**
|
|
@@ -211,15 +241,22 @@ export interface CreateOptions {
|
|
|
211
241
|
*
|
|
212
242
|
* GPU access is not supported with `"gvisor"`.
|
|
213
243
|
*/
|
|
214
|
-
runtime?: "docker" | "gvisor" | "kata" | "firecracker" | "wasm";
|
|
244
|
+
runtime?: "docker" | "gvisor" | "kata" | "firecracker" | "wasm" | "isolate";
|
|
215
245
|
/**
|
|
216
246
|
* Survival class across daemon restarts. Omit for the runtime default:
|
|
217
|
-
* `passivatable` for docker/gvisor/firecracker, `ephemeral` for wasm.
|
|
247
|
+
* `passivatable` for docker/gvisor/firecracker, `ephemeral` for wasm/isolate.
|
|
218
248
|
* `durable` is wasm-only and not yet implemented.
|
|
219
249
|
*/
|
|
220
250
|
durability?: Durability;
|
|
221
|
-
/** 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. */
|
|
222
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;
|
|
223
260
|
/**
|
|
224
261
|
* Attach GPU resources to the sandbox. Omit for CPU-only workloads.
|
|
225
262
|
* Not compatible with runtime="gvisor".
|
|
@@ -247,6 +284,25 @@ export interface ListOptions {
|
|
|
247
284
|
* or pass an empty map to list every sandbox visible to the PAT.
|
|
248
285
|
*/
|
|
249
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;
|
|
250
306
|
}
|
|
251
307
|
export interface CreateSessionOptions {
|
|
252
308
|
name?: string;
|
|
@@ -409,6 +465,10 @@ export type ExposeResult = {
|
|
|
409
465
|
};
|
|
410
466
|
export interface Sandbox {
|
|
411
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>;
|
|
412
472
|
image: string;
|
|
413
473
|
status: SandboxStatus;
|
|
414
474
|
publicURL: string;
|
|
@@ -420,6 +480,23 @@ export interface Sandbox {
|
|
|
420
480
|
osUser: string;
|
|
421
481
|
env?: Record<string, string>;
|
|
422
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[];
|
|
423
500
|
toolboxEnabled: boolean;
|
|
424
501
|
sshPublicKey?: string;
|
|
425
502
|
sshPrivateKey?: string;
|
|
@@ -435,13 +512,15 @@ export interface Sandbox {
|
|
|
435
512
|
* Container runtime this sandbox is running under. Empty string indicates
|
|
436
513
|
* a pre-migration row that resolves to the host default at start time.
|
|
437
514
|
*/
|
|
438
|
-
runtime: "" | "docker" | "gvisor" | "kata" | "firecracker" | "wasm";
|
|
515
|
+
runtime: "" | "docker" | "gvisor" | "kata" | "firecracker" | "wasm" | "isolate";
|
|
439
516
|
/** Survival class this sandbox was created with. */
|
|
440
517
|
durability?: Durability;
|
|
441
|
-
/** Resolved WASM module reference when runtime is wasm. */
|
|
518
|
+
/** Resolved WASM module / isolate bundle reference when runtime is wasm or isolate. */
|
|
442
519
|
moduleRef?: string;
|
|
443
520
|
/** sha256 hex digest of the resolved WASM module bytes. */
|
|
444
521
|
moduleDigest?: string;
|
|
522
|
+
/** Isolate-group key this sandbox was created under (`runtime: "isolate"` only). */
|
|
523
|
+
tenantId?: string;
|
|
445
524
|
/** GPU configuration this sandbox was created with. Absent means no GPU. */
|
|
446
525
|
gpus?: GPUOptions;
|
|
447
526
|
}
|
|
@@ -463,6 +542,214 @@ export interface NetworkUsage {
|
|
|
463
542
|
/** Absent until the netstats poller has produced at least one sample. */
|
|
464
543
|
lastSampledAt?: string;
|
|
465
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
|
+
}
|
|
466
753
|
export interface SetNetworkLimitsOptions {
|
|
467
754
|
/** Omit (undefined) to leave unchanged. `0` means unlimited. */
|
|
468
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
|
}
|