@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 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
- get(id: string): Promise<Sandbox>;
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
- async get(id) {
34
- const sandbox = await this.client.get(id);
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
- get(id: string): Promise<SandboxResource>;
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
  }
@@ -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
- const RETRYABLE_STATUS_CODES = new Set([429, 502, 503, 504]);
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 path = this.versioned("/sandboxes") + buildTagQuery(options?.tags);
155
- const response = await this.doJSON("GET", path);
156
- return response.map((item) => this.wrap(item));
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 response = await this.doJSON("GET", `${this.versionPrefix}/sandboxes/${id}`);
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" || value === "gvisor" || value === "kata") {
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 of CIDRs. When set, the sandbox may reach only these
142
- * destinations; all other outbound traffic is dropped by the host firewall.
143
- * Mutually exclusive with `networkDenyOut`. For a full block use
144
- * `networkBlockAll` instead.
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. When set, the sandbox may reach anything except
149
- * these destinations. Mutually exclusive with `networkAllowOut`.
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
- * Whether the sandbox may be exposed to the public internet. Defaults to
154
- * true; set `false` to make `exposePort` fail — the sandbox stays reachable
155
- * only via the toolbox proxy and SSH gateway, not a public URL.
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.5.3",
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.9.3",
25
- "typescript": "^6.0.3",
26
- "tsx": "^4.22.4"
29
+ "@types/node": "^26.6.3",
30
+ "typescript": "^7.0.2",
31
+ "tsx": "^4.23.15"
27
32
  }
28
33
  }