@runblackcat/sdk 0.1.0-alpha.100

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.
@@ -0,0 +1,1020 @@
1
+ /**
2
+ * Server-side TypeScript client for the Blackcat gateway.
3
+ *
4
+ * The SDK intentionally contains transport primitives only. Workload-specific
5
+ * workflows (a consumer's dirty-worktree sync, say) compose file and exec
6
+ * operations in their own repository.
7
+ */
8
+ export declare const CAPABILITY_OPS: {
9
+ readonly EXEC: 1;
10
+ readonly READ: 2;
11
+ readonly WRITE: 4;
12
+ readonly SNAPSHOT: 8;
13
+ readonly DESTROY: 16;
14
+ readonly INSPECT: 32;
15
+ readonly SANDBOX_CREATE: 64;
16
+ readonly TEMPLATE_RW: 128;
17
+ readonly PUBLISH: 256;
18
+ readonly PROCESS_START: 512;
19
+ readonly PROCESS_INSPECT: 1024;
20
+ readonly PROCESS_CANCEL: 2048;
21
+ readonly RENEW: 4096;
22
+ };
23
+ export type CapabilityOps = number;
24
+ export declare const BLACKCAT_ERROR_CODES: {
25
+ readonly SANDBOX_TRANSITIONING: "sandbox_transitioning";
26
+ readonly FLEET_UPGRADE_QUIESCING: "fleet_upgrade_quiescing";
27
+ };
28
+ export type FetchFunction = (input: RequestInfo | URL, init?: RequestInit & {
29
+ tls?: {
30
+ ca: string;
31
+ };
32
+ timeout?: false;
33
+ }) => Promise<Response>;
34
+ export interface BlackcatClientOptions {
35
+ baseUrl: string;
36
+ /**
37
+ * A tenant API key. The gateway accepts it directly on every resource route
38
+ * for the tenant's own resources (limited to the key's operation set), so a
39
+ * trusted server-side caller needs no `tokens.mint` per call. Keep it
40
+ * server-side; hand less-trusted holders a minted `token` instead.
41
+ */
42
+ apiKey?: string;
43
+ /** A capability token; preferred over `apiKey` when both are set. */
44
+ token?: string;
45
+ /** Capability identity and expiry, using idiomatic TypeScript names. */
46
+ tokenId?: string;
47
+ tokenExpiresAt?: number;
48
+ /** Backward-compatible aliases retained for pre-publication callers. */
49
+ token_id?: string;
50
+ token_expires_at?: number;
51
+ /** Re-mint a capability when renewal must replace the token secret. */
52
+ renewCapability?: () => Promise<MintTokenResponse>;
53
+ /** Private gateway trust root. Bun forwards this through its fetch TLS option. */
54
+ caPem?: string;
55
+ fetch?: FetchFunction;
56
+ }
57
+ export interface TemplateDefaults {
58
+ vcpus: number;
59
+ memory_mib: number;
60
+ hibernate?: HibernateMode;
61
+ on_interruption?: "evacuate" | "destroy";
62
+ /** MiB of virtio-mem region booted into the checkpoint; a multiple of 128. Defaults to the template's memory. */
63
+ hotplug_max_mib?: number;
64
+ }
65
+ /**
66
+ * The size a stored template version carries: `memory_mib` is the size it was
67
+ * captured at (boot memory plus what was plugged), and a sandbox can later
68
+ * take any size from `base_memory_mib` to `base_memory_mib + hotplug_max_mib`.
69
+ */
70
+ export interface TemplateShapeDefaults extends TemplateDefaults {
71
+ base_memory_mib: number;
72
+ hotplug_max_mib: number;
73
+ }
74
+ export type HibernateMode = "memory";
75
+ export type TemplateEgressPolicy = {
76
+ mode: "deny_all";
77
+ } | {
78
+ mode: "allow_public";
79
+ } | {
80
+ mode: "rules";
81
+ rules: ReadonlyArray<{
82
+ protocol: "tcp" | "udp";
83
+ port: number;
84
+ target: {
85
+ kind: "cidr";
86
+ value: string;
87
+ } | {
88
+ kind: "dns_name";
89
+ value: string;
90
+ } | {
91
+ kind: "dns_suffix";
92
+ value: string;
93
+ };
94
+ }>;
95
+ };
96
+ export type TemplatePreparationValue = {
97
+ from: "literal";
98
+ value: string;
99
+ } | {
100
+ from: "parameter";
101
+ name: string;
102
+ } | {
103
+ from: "secret";
104
+ name: string;
105
+ };
106
+ export interface TemplateRestoreHealth {
107
+ command: string;
108
+ args: readonly string[];
109
+ cwd: string;
110
+ timeout_ms: number;
111
+ }
112
+ interface TemplateDockerSourceBase {
113
+ /** A prepared filesystem; each sandbox starts a fresh process tree. */
114
+ docker: {
115
+ image: string;
116
+ };
117
+ defaults: TemplateDefaults;
118
+ egress: TemplateEgressPolicy;
119
+ restore_health: TemplateRestoreHealth;
120
+ /** Runs in the guest after it is ready and before the capture, so every
121
+ * clone restores with the app's caches and JIT already hot. Must exit 0. */
122
+ warmup?: TemplateRestoreHealth;
123
+ }
124
+ interface TemplatePreparation {
125
+ parameters: Record<string, {
126
+ min_length: number;
127
+ max_length: number;
128
+ charset: "printable_ascii" | "lower_hex";
129
+ }>;
130
+ prepare: {
131
+ command: string;
132
+ args: TemplatePreparationValue[];
133
+ cwd: string;
134
+ environment: Record<string, TemplatePreparationValue>;
135
+ timeout_ms: number;
136
+ output_limit_bytes: number;
137
+ };
138
+ }
139
+ export type TemplateDockerSource = TemplateDockerSourceBase & (TemplatePreparation | {
140
+ parameters?: never;
141
+ prepare?: never;
142
+ });
143
+ /** A build whose image was uploaded rather than pulled from a registry. */
144
+ export type TemplateUploadedImageSource = Omit<TemplateDockerSourceBase, "docker"> & {
145
+ docker: UploadedImageSource;
146
+ } & (TemplatePreparation | {
147
+ parameters?: never;
148
+ prepare?: never;
149
+ });
150
+ export interface UploadedImageSource {
151
+ /** The `sha256:<hex>` digest returned by `templates.uploadImage`. */
152
+ uploaded_image: string;
153
+ }
154
+ export interface UploadedImage {
155
+ image: string;
156
+ bytes: number;
157
+ /** True when the control plane already held this exact image. */
158
+ unchanged: boolean;
159
+ }
160
+ export interface TemplateSandboxSource {
161
+ /** `warmup` runs in the sandbox just before it is captured; omitted, the
162
+ * sandbox's template's own warm-up (if any) runs. */
163
+ sandbox: {
164
+ id: string;
165
+ warmup?: TemplateRestoreHealth;
166
+ };
167
+ /**
168
+ * A promoted sandbox keeps the machine dimensions it was captured on, so
169
+ * the gateway rejects `defaults` here. Omit `egress` to inherit the source
170
+ * template's policy.
171
+ */
172
+ egress?: TemplateEgressPolicy;
173
+ }
174
+ export type TemplateSource = TemplateDockerSource | TemplateUploadedImageSource | TemplateSandboxSource;
175
+ /** What a promotion adds to a template create: how it is warmed and packed. */
176
+ export interface PromotionOptions {
177
+ /** Warm the new version onto this many more nodes in bulk, beyond the node
178
+ * that captured it (at most 99), or onto `"all"` the other healthy nodes.
179
+ * Nodes bound to a capacity reservation that follows the template are warmed
180
+ * regardless. */
181
+ prefetch_copies?: number | "all";
182
+ /** Millicores each clone of the new version reserves, at most 1000 per vCPU.
183
+ * Omitted, the version inherits what the sandbox's own template reserves. */
184
+ cpu_reservation_mcpu?: number;
185
+ }
186
+ export type CreateTemplateInput = {
187
+ name: string;
188
+ source: TemplateDockerSource;
189
+ } | {
190
+ name: string;
191
+ source: TemplateUploadedImageSource;
192
+ } | ({
193
+ name?: string;
194
+ source: TemplateSandboxSource;
195
+ } & PromotionOptions);
196
+ export interface Template {
197
+ id: string;
198
+ name: string;
199
+ version: number;
200
+ /** `local_ready` while a promotion's chunks are still being published. */
201
+ state?: "local_ready" | "published";
202
+ /** The gateway stores the complete source manifest as JSON text. */
203
+ source: string;
204
+ current: boolean;
205
+ }
206
+ /** Public record name retained for consumers that distinguish it from creation input. */
207
+ export type TemplateRecord = Template;
208
+ export interface CreateTemplateBuildInput {
209
+ name: string;
210
+ content_key: string;
211
+ activate_on_ready: boolean;
212
+ source: TemplateDockerSource;
213
+ }
214
+ export type TemplateBuildPhase = "queued" | "building" | "validating" | "ready" | "failed";
215
+ export interface TemplateBuildRecord {
216
+ id: string;
217
+ name: string;
218
+ content_key: string;
219
+ phase: TemplateBuildPhase;
220
+ attempt: number;
221
+ template_version_id: string | null;
222
+ error: string | null;
223
+ }
224
+ export interface PrepareSandboxResponse {
225
+ prepared?: boolean;
226
+ process: Pick<ProcessRecord, "id" | "state" | "exit_code">;
227
+ }
228
+ export interface CreateTemplateResponse {
229
+ id: string;
230
+ version: number;
231
+ /**
232
+ * `local_ready`: a promotion whose capture has been proved but whose chunks
233
+ * are still being published. The version is current and claimable, but only
234
+ * on the node that captured it; clones depend only on that node's files and
235
+ * are destroyed if the publication fails. `published`: every chunk is in the
236
+ * object store.
237
+ */
238
+ state?: "local_ready" | "published";
239
+ /**
240
+ * Capture/publish phase breakdown for a checkpoint promotion, when this
241
+ * create captured a running sandbox. Absent for a template created without
242
+ * a capture, and for a server that predates this field.
243
+ */
244
+ capture_timing?: {
245
+ capture_ms: number;
246
+ /** How long the source VM stood paused: the memory/state dump, the disk
247
+ * freeze and the resume. Sealing and upload happen after it. */
248
+ pause_ms: number;
249
+ /** Absent while the version is `local_ready`: publication is not over. */
250
+ publish_ms?: number;
251
+ /** Restoring a disposable clone from the capture and running the
252
+ * template's restore-health command in it; both overlap the upload. */
253
+ validation_restore_ms?: number;
254
+ validation_health_ms?: number;
255
+ /** Whether the capturing node was left holding the new version's memory
256
+ * and disk for fault-free restores. */
257
+ warmed?: {
258
+ memory: boolean;
259
+ disk: boolean;
260
+ };
261
+ };
262
+ /** Present for a promotion that asked for or achieved warming. */
263
+ warming?: {
264
+ capturing_node_warm: boolean;
265
+ prefetch_copies: number;
266
+ /** Pass to `templates.prefetchStatus` to follow the extra copies. */
267
+ prefetch_id?: string;
268
+ error?: string;
269
+ };
270
+ }
271
+ export type CreateSandboxInput = {
272
+ /**
273
+ * The template name or version id. Omitted, it is the tenant's `default`
274
+ * template, which the gateway builds on the tenant's first use: that first
275
+ * create fails with a 503 `default_template_building` until the build is
276
+ * ready, and repeating the same request is how to wait for it.
277
+ */
278
+ template?: string;
279
+ /** Must equal the checkpoint's vCPUs, which are fixed at capture; omit it. */
280
+ vcpus?: number;
281
+ /**
282
+ * Guest memory in MiB: the checkpoint's captured size or larger, up to its
283
+ * boot memory plus hotplug region (the extra is plugged right after restore).
284
+ */
285
+ memory_mib?: number;
286
+ /** CPU entitlement in millicores, at most the checkpoint's vCPUs * 1000. */
287
+ cpu_mcpu?: number;
288
+ /** Stable across retries of one acquisition; never reuse for another request. */
289
+ request_id: string;
290
+ ssh_public_key?: string;
291
+ lease: SandboxLeaseInput;
292
+ /** A capacity reservation this create should draw a bound slot from. */
293
+ reservation?: string;
294
+ /**
295
+ * Ask for a capability scoped to each sandbox this request creates, returned
296
+ * in the response (`capability` on the receipt, or on each batch member), so
297
+ * a caller driving many sandboxes mints none of its own. `ops` must be within
298
+ * the creating token's; `ttl_secs` is capped at 900 and renewable like any
299
+ * token. Only the request that creates a sandbox gets its capability: a
300
+ * repeat of the same `request_id` returns none.
301
+ */
302
+ capability?: {
303
+ ops: string[] | CapabilityOps;
304
+ ttl_secs: number;
305
+ };
306
+ };
307
+ /** A capability scoped to one sandbox, as returned by a create. */
308
+ export interface SandboxCapability {
309
+ token: string;
310
+ token_id: string;
311
+ ops: string[];
312
+ /** Epoch milliseconds. */
313
+ expires_at: number;
314
+ renewable_until: number;
315
+ }
316
+ /** A batch create: `request_id` names the batch, member `i` is `<request_id>.<i>`. */
317
+ export type CreateSandboxBatchInput = CreateSandboxInput & {
318
+ /** How many identical sandboxes to create, 1 to 1000. */
319
+ count: number;
320
+ };
321
+ export type SandboxBatchMemberState = "pending" | "placed" | "failed" | "destroyed";
322
+ export interface SandboxBatchMember {
323
+ /** Poll this member alone with `sandboxes.acquisition(request_id)`. */
324
+ request_id: string;
325
+ id: string;
326
+ state: SandboxBatchMemberState;
327
+ /** Present when the create asked for `capability`. */
328
+ capability?: SandboxCapability;
329
+ }
330
+ export interface SandboxBatch {
331
+ /** The batch's request id. */
332
+ request_id: string;
333
+ count: number;
334
+ /** Members in each state; a state with no members is absent. */
335
+ counts: Partial<Record<SandboxBatchMemberState, number>>;
336
+ members: SandboxBatchMember[];
337
+ }
338
+ export type SandboxBatchResponse = SandboxBatch | {
339
+ request_id: string;
340
+ state: "prevented";
341
+ };
342
+ export interface SandboxLeaseInput {
343
+ /** Initial lease duration, between 60 seconds and 24 hours. */
344
+ ttl_secs: number;
345
+ /** Hard lifetime from acquisition, at least ttl_secs and at most 30 days. */
346
+ renewable_for_secs: number;
347
+ }
348
+ export type CreateCapacityReservationInput = ({
349
+ /** One immutable template version, pinned for the reservation's life. */
350
+ template: string;
351
+ template_resource?: never;
352
+ } | {
353
+ /** A template resource name: the reservation follows whichever version
354
+ * is current, even one that does not exist yet, and its bound nodes are
355
+ * warmed with each new version as it is activated. */
356
+ template_resource: string;
357
+ template?: never;
358
+ }) & {
359
+ /** How many sandboxes of this template/shape to reserve. */
360
+ count: number;
361
+ vcpus: number;
362
+ memory_mib: number;
363
+ /** How long to hold the reservation, between 1 second and 4 hours. */
364
+ ttl_secs: number;
365
+ /** Defaults to `consumable`; `reusable` returns each slot after confirmed destruction.
366
+ * `warm` keeps `count` free slots on workers that cache a pinned template
367
+ * version; it takes `template` (not `template_resource`), is never held
368
+ * against other claims, and a sandbox create naming it is a 400. */
369
+ mode?: "consumable" | "reusable" | "warm";
370
+ /** When the capacity must be ready: seconds from now, or an RFC 3339 time
371
+ * within `ttl_secs`. Absent: as soon as it can be, at the lowest cost. */
372
+ needed_by?: number | string;
373
+ /** `shared` (default): any eligible node, free headroom first. `dedicated`:
374
+ * only hosts dedicated reservations hold, launched for them. A held host
375
+ * serves only dedicated reservations (of any tenant: they pack), never
376
+ * ordinary work; a sandbox create naming it waits for one of its slots. */
377
+ placement?: "shared" | "dedicated";
378
+ /** With `placement: "dedicated"`, the provider shape to launch: an exact
379
+ * shape or a family (`c7i`) the fleet can launch (direct EC2: the config's
380
+ * instance_types). Absent: the planner chooses. */
381
+ shape?: string;
382
+ /** Stable retry key, at most 107 ASCII letters, digits, `_` or `-`. */
383
+ idempotency_key?: string;
384
+ };
385
+ export interface CapacityReservation {
386
+ id: string;
387
+ state: "active" | "released" | "expired";
388
+ /** The version nodes are warmed with; null while a reservation following a
389
+ * template resource waits for the resource's first version. */
390
+ template: string | null;
391
+ /** The template resource this reservation follows, or null when pinned. */
392
+ template_resource: string | null;
393
+ count: number;
394
+ mode: "consumable" | "reusable" | "warm";
395
+ placement: "shared" | "dedicated";
396
+ /** The shape a dedicated reservation launches, or null. */
397
+ shape: string | null;
398
+ vcpus: number;
399
+ memory_mib: number;
400
+ /** Slots consumed by an actual sandbox create. */
401
+ claimed: number;
402
+ /** Current slot occupancy for reusable reservations; zero otherwise. */
403
+ occupied: number;
404
+ /** Slots bound to a specific node and not yet claimed. */
405
+ bound: number;
406
+ /** Bound slots whose node already has the template warm. For warm mode this
407
+ * counts member nodes at 1 each. */
408
+ warm: number;
409
+ /** Warm mode: free slots of the shape on member workers whose template is
410
+ * ready, as of the last reconcile; zero for other modes. */
411
+ warm_free: number;
412
+ expires_at: SystemTimeValue;
413
+ /** When the capacity must be ready (epoch milliseconds), or null. */
414
+ needed_by: number | null;
415
+ }
416
+ export interface WaitOptions {
417
+ /**
418
+ * Have the gateway hold a still-pending claim for up to this many seconds
419
+ * and answer as soon as it is placed or fails, with its ports, instead of
420
+ * answering `pending` at once. The gateway holds at most 55 seconds. A claim
421
+ * still pending after the hold is returned as `pending`; ask again.
422
+ */
423
+ wait_secs?: number;
424
+ }
425
+ /**
426
+ * Response returned by POST /v1/sandboxes (the gateway has no template field).
427
+ * The gateway sends a `failed` receipt with HTTP 409, so `create` and
428
+ * `acquisition` surface it as a `BlackcatApiError` whose `details` this
429
+ * parser accepts: placement was abandoned and its sandbox torn down, which is
430
+ * retryable under a new request id rather than a protocol fault.
431
+ */
432
+ export type CreateSandboxResponse = {
433
+ request_id: string;
434
+ state: "prevented";
435
+ } | ({
436
+ request_id: string;
437
+ id: string;
438
+ tier: "boot" | "warm_pool" | "restore";
439
+ expires_at: SystemTimeValue;
440
+ renewable_until: SystemTimeValue;
441
+ /**
442
+ * Phase-timing breakdown for the launch that placed this sandbox, once one
443
+ * has committed. Absent while still queued/pending, and for a server that
444
+ * predates this field.
445
+ */
446
+ restore_timing?: RestoreTiming;
447
+ /** Present when the create asked for `capability` and created this sandbox. */
448
+ capability?: SandboxCapability;
449
+ } & ({
450
+ state: "placed";
451
+ node: string;
452
+ } | {
453
+ state: "pending";
454
+ node: string | null;
455
+ } | {
456
+ state: "failed";
457
+ node: null;
458
+ error: string;
459
+ } | {
460
+ state: "destroyed";
461
+ node: string | null;
462
+ error: string;
463
+ }));
464
+ /**
465
+ * Restore phase breakdown a node reported for a checkpoint launch (Proposal
466
+ * F, docs/restore-performance.md). `phase_ms` mirrors the node's own
467
+ * `checkpoint vm timing` log line; `chunk_source` reports how much of the
468
+ * checkpoint's memory/disk was fetched remotely vs. served locally, how many
469
+ * UFFD faults and NBD reads this restore actually served (both zero on the
470
+ * file-restore path), and the checkpoint's logical sizes. `memory_source` is
471
+ * `"file"` when a node-local materialized copy let this restore skip
472
+ * UFFD/NBD lazy faulting entirely (fault-free warm restore,
473
+ * docs/restore-performance.md), or `"uffd"` for the lazy chunk-source path
474
+ * every checkpoint restore used before it.
475
+ */
476
+ export interface RestoreTiming {
477
+ proposal_ms: number;
478
+ restore_vm_ms: number;
479
+ commit_ms: number;
480
+ total_ms: number;
481
+ phase_ms: {
482
+ open: number;
483
+ network: number;
484
+ firecracker: number;
485
+ state: number;
486
+ disk: number;
487
+ pager: number;
488
+ load: number;
489
+ activation: number;
490
+ /** Milliseconds from the TAP being raised until the guest's gateway and resolver answered; absent from a node that predates it, 0 from a guest that does. */
491
+ network_ready?: number;
492
+ };
493
+ memory_source?: "file" | "uffd";
494
+ /** The node already held what it restores from as files (a warm restore), so no fetch ran for this claim: the memory, and the disk on a node that clones disks (one that cannot still pages its disk through NBD). Null when the node did not say; absent from a server that predates it. */
495
+ template_cached?: boolean | null;
496
+ chunk_source: {
497
+ remote_requests: number;
498
+ remote_bytes: number;
499
+ local_requests?: number;
500
+ local_bytes?: number;
501
+ uffd_faults?: number;
502
+ uffd_fault_bytes?: number;
503
+ nbd_reads?: number;
504
+ nbd_read_bytes?: number;
505
+ logical_memory_bytes: number;
506
+ logical_disk_bytes: number;
507
+ };
508
+ }
509
+ export interface Sandbox {
510
+ id: string;
511
+ template: string;
512
+ state: string;
513
+ node?: string;
514
+ /** vCPUs the guest has now: the template's, until a cold resize reboots it onto another count. */
515
+ vcpus: number;
516
+ /** Current guest memory in MiB (boot memory plus what is plugged). */
517
+ memory_mib: number;
518
+ /** Current CPU entitlement in millicores. */
519
+ cpu_mcpu: number;
520
+ expires_at: SystemTimeValue;
521
+ renewable_until: SystemTimeValue;
522
+ }
523
+ /** A short-lived SSH key to authorize on a running sandbox. */
524
+ export interface GrantSshKeyInput {
525
+ /** One `ssh-ed25519` public key line. */
526
+ public_key: string;
527
+ /** Lifetime in seconds, 1 to 86400. The key only matters while connecting. */
528
+ ttl_secs: number;
529
+ }
530
+ export interface SshKeyGrant {
531
+ /** Unix milliseconds at which the key stops authenticating. */
532
+ expires_at: number;
533
+ /** The login account in the sandbox. */
534
+ user: string;
535
+ /** The sandbox's SSH host public key; pin it before connecting. */
536
+ host_key: string;
537
+ }
538
+ /**
539
+ * Where and how to reach a port inside a sandbox through the gateway. Send
540
+ * `headers` with every request and with the WebSocket handshake; the gateway
541
+ * strips them before the application sees the request. A browser `WebSocket`
542
+ * cannot send headers, so the WebSocket form is for server-side clients.
543
+ */
544
+ export interface PortConnection {
545
+ /** HTTPS URL of the path on the port. */
546
+ url: string;
547
+ /** The same target as a `wss:` URL. */
548
+ websocketUrl: string;
549
+ headers: {
550
+ Authorization: string;
551
+ };
552
+ }
553
+ export interface RenewSandboxResponse {
554
+ id: string;
555
+ expires_at: SystemTimeValue;
556
+ renewable_until: SystemTimeValue;
557
+ }
558
+ /** The CPU a node runs on, as it reports it. */
559
+ export interface CpuIdentity {
560
+ vendor: string;
561
+ model_name: string;
562
+ family: number;
563
+ model: number;
564
+ stepping: number;
565
+ microcode: string;
566
+ /** Hash of the CPUID feature leaves a guest's CPUID is derived from. */
567
+ features_hash: string;
568
+ /** The host is itself a guest (nested virtualization). */
569
+ nested: boolean;
570
+ }
571
+ /**
572
+ * Tenant-scoped view returned by GET /v1/fleet. Residents and cached snapshots
573
+ * are already filtered by the gateway to the caller's tenant.
574
+ */
575
+ export interface FleetNode {
576
+ id: string;
577
+ addr: string;
578
+ total_mib: number;
579
+ reserved_mib: number;
580
+ vcpus: number;
581
+ health: string;
582
+ schedulable: boolean;
583
+ lease_until: SystemTimeValue;
584
+ committed_mib: number;
585
+ residents: string[];
586
+ cached_snapshots: string[];
587
+ interruption: FleetInterruption | null;
588
+ /** Charged versus measured memory and the node's pressure state. */
589
+ memory: FleetNodeMemory;
590
+ /** Whether the host runs with SMT active. */
591
+ smt_active: boolean;
592
+ /** Bytes the worker reserves for cached templates; 0 until it reports one. */
593
+ cache_budget_bytes: number;
594
+ /** The node's CPU; absent from nodes older than CPU-aware placement. */
595
+ cpu?: CpuIdentity | null;
596
+ /** 1000 charges file-backed clones in full; 2000 charges them at half. */
597
+ max_memory_ratio_milli: number;
598
+ max_vcpu_ratio_milli: number;
599
+ /** How the fleet decides what a sandbox costs. */
600
+ packing_mode?: "learned" | "manual";
601
+ }
602
+ export type MemoryPressure = "normal" | "high" | "critical";
603
+ export interface FleetNodeMemory {
604
+ /** What admission charged for the node's residents. */
605
+ charged_mib: number;
606
+ /** The residents' sizes before the overcommit discount. */
607
+ plugged_mib: number;
608
+ /** Resident memory of the residents' VMMs, shared pages counted per mapper. */
609
+ measured_rss_mib: number;
610
+ /** Proportional memory the residents cost; compare with `charged_mib`. */
611
+ measured_pss_mib: number;
612
+ /** Memory only one resident maps (Private_Clean + Private_Dirty). */
613
+ measured_uss_mib: number;
614
+ host_available_mib: number;
615
+ psi_some_avg10_milli: number;
616
+ pressure: MemoryPressure;
617
+ }
618
+ export type FleetTrust = "single_trust" | "multi_tenant";
619
+ export interface ResizeSandboxInput {
620
+ /**
621
+ * Target vCPU count. A count different from the sandbox's reboots the guest
622
+ * cold on its own disk: every process ends, template boot services restart,
623
+ * processes started by hand do not.
624
+ */
625
+ vcpus?: number;
626
+ /** Target guest memory in MiB: boot memory plus a multiple of 128. */
627
+ memory_mib?: number;
628
+ /** Target CPU entitlement in millicores. */
629
+ cpu_mcpu?: number;
630
+ /** Drop the guest's page caches before a shrink. Defaults to true. */
631
+ drop_caches?: boolean;
632
+ /** Stable across retries of one resize; generated when omitted. */
633
+ request_id?: string;
634
+ }
635
+ /** The size a resize reached. A shrink the guest could not fully honour reports what it reached. */
636
+ export interface SandboxResize {
637
+ id: string;
638
+ state: "placed";
639
+ request_id: string;
640
+ /** True when the guest was rebooted to change its vCPU count. */
641
+ cold: boolean;
642
+ vcpus: number;
643
+ memory_mib: number;
644
+ cpu_mcpu: number;
645
+ /** False when the guest stopped short of the requested memory. */
646
+ reached_target: boolean;
647
+ elapsed_ms: number;
648
+ /** Cold resizes only: durable processes the reboot ended. */
649
+ processes_ended?: number;
650
+ /** Cold resizes only: where the reboot spent its time, in milliseconds. */
651
+ phases?: {
652
+ quiesce_ms: number;
653
+ stop_ms: number;
654
+ boot_ms: number;
655
+ plug_ms: number;
656
+ handshake_ms: number;
657
+ health_ms: number;
658
+ };
659
+ /** Cold resizes only: unexpired SSH grants installed again, and ones that failed. */
660
+ ssh_grants_restored?: number;
661
+ ssh_grants_failed?: number;
662
+ }
663
+ export interface FleetInterruption {
664
+ reason: string;
665
+ deadline: SystemTimeValue;
666
+ }
667
+ export interface FleetResponse {
668
+ nodes: FleetNode[];
669
+ fleet_trust: FleetTrust;
670
+ /** `native`, or a CPU template (`t2`, `t2s`, `t2cl`, `t2a`). */
671
+ cpu_baseline?: string;
672
+ }
673
+ export interface TemplateRefreshPolicy {
674
+ interval_seconds: number;
675
+ plugin_installation: string;
676
+ }
677
+ export interface ConfigureTemplateResourceInput {
678
+ keep_versions: number;
679
+ refresh: TemplateRefreshPolicy | null;
680
+ }
681
+ export interface TemplateResource {
682
+ name: string;
683
+ current_version_id: string;
684
+ retention: {
685
+ keep_versions: number;
686
+ };
687
+ refresh: TemplateRefreshPolicy | null;
688
+ next_refresh_at: number | null;
689
+ last_refresh?: unknown;
690
+ }
691
+ export interface ExecInput {
692
+ command: string;
693
+ args: string[];
694
+ cwd: string;
695
+ /** Positive guest execution budget; effects before timeout are not undone. */
696
+ timeout_ms: number;
697
+ /** Optional bounded stdin for the streaming EXEC endpoint. */
698
+ stdin?: number[];
699
+ }
700
+ export interface ExecResult {
701
+ exit_code: number | null;
702
+ timed_out: boolean;
703
+ stdout: Uint8Array;
704
+ stderr: Uint8Array;
705
+ }
706
+ export interface ExecStreamChunk {
707
+ stdout: Uint8Array;
708
+ stderr: Uint8Array;
709
+ done: boolean;
710
+ exit_code: number | null;
711
+ timed_out: boolean;
712
+ }
713
+ export type ProcessState = "starting" | "running" | "succeeded" | "failed" | "timed_out" | "cancelled" | "interrupted";
714
+ export interface ProcessStartInput {
715
+ command: string;
716
+ args: string[];
717
+ cwd: string;
718
+ env: Array<[string, string]>;
719
+ timeout_ms: number;
720
+ output_limit_bytes: number;
721
+ /** Optional payload delivered once and followed by EOF. Maximum 512 KiB. */
722
+ stdin?: Uint8Array;
723
+ }
724
+ export interface ProcessRecord {
725
+ id: string;
726
+ sandbox_id: string;
727
+ state: ProcessState;
728
+ exit_code: number | null;
729
+ stdout_bytes: number;
730
+ stderr_bytes: number;
731
+ stdout_truncated: boolean;
732
+ stderr_truncated: boolean;
733
+ failure: string | null;
734
+ created_at: number;
735
+ updated_at: number;
736
+ terminal_at: number | null;
737
+ }
738
+ export interface ProcessLogs {
739
+ stdout: Uint8Array;
740
+ stderr: Uint8Array;
741
+ next_stdout_offset: number;
742
+ next_stderr_offset: number;
743
+ stdout_truncated: boolean;
744
+ stderr_truncated: boolean;
745
+ state: ProcessState;
746
+ }
747
+ export interface ProcessLogsResponse {
748
+ process: ProcessRecord;
749
+ logs: ProcessLogs;
750
+ }
751
+ export interface FileReadChunk {
752
+ data: Uint8Array;
753
+ offset: number;
754
+ eof: boolean;
755
+ }
756
+ export interface ReadFileInput {
757
+ path: string;
758
+ offset?: number;
759
+ /** Total bytes to read. Omit to read through EOF. */
760
+ length?: number;
761
+ /** Per-request chunk size; defaults to the gateway's maximum. */
762
+ chunk_size?: number;
763
+ }
764
+ export interface WriteFileInput {
765
+ path: string;
766
+ data: Uint8Array;
767
+ offset?: number;
768
+ truncate?: boolean;
769
+ }
770
+ export interface WriteFileStreamInput {
771
+ path: string;
772
+ data: AsyncIterable<Uint8Array>;
773
+ offset?: number;
774
+ truncate?: boolean;
775
+ }
776
+ export interface MintTokenInput {
777
+ sandbox?: string;
778
+ ops: CapabilityOps;
779
+ ttl_secs: number;
780
+ }
781
+ /** JSON representation emitted by serde for Rust's SystemTime. */
782
+ export interface SystemTimeValue {
783
+ secs_since_epoch: number;
784
+ nanos_since_epoch: number;
785
+ }
786
+ export interface MintTokenResponse {
787
+ token: string;
788
+ token_id: string;
789
+ /** Capability timestamps are epoch milliseconds. */
790
+ expires_at: number;
791
+ renewable_until: number;
792
+ }
793
+ export interface RenewTokenResponse {
794
+ expires_at: number;
795
+ }
796
+ export declare class BlackcatApiError extends Error {
797
+ readonly status: number;
798
+ /** Stable gateway error code when one is provided. */
799
+ readonly code?: string;
800
+ readonly retryAfter?: number;
801
+ readonly requestId?: string;
802
+ readonly details: unknown;
803
+ constructor(status: number, message: string, details: unknown, retryAfter?: number, requestId?: string);
804
+ }
805
+ declare const MAX_FILE_CHUNK: number;
806
+ declare const MAX_FILE_READ_CHUNK = 1000000;
807
+ declare const MAX_PROCESS_STDIN: number;
808
+ declare const MAX_EXEC_STDIN: number;
809
+ export declare function epochMillis(value: unknown, field: string): number;
810
+ export declare function parseCreateSandboxResponse(value: unknown, requestId: string): CreateSandboxResponse;
811
+ export declare function parseSandboxBatch(value: unknown, requestId: string): SandboxBatchResponse;
812
+ export declare function assertSecureBlackcatGatewayUrl(baseUrl: string): void;
813
+ declare class GatewayClient {
814
+ readonly baseUrl: string;
815
+ readonly apiKey?: string;
816
+ token?: string;
817
+ tokenId?: string;
818
+ tokenExpiresAt?: number;
819
+ readonly renewCapability?: () => Promise<MintTokenResponse>;
820
+ readonly caPem?: string;
821
+ readonly fetcher: FetchFunction;
822
+ private renewalInFlight?;
823
+ constructor(options: BlackcatClientOptions);
824
+ credential(kind: "apiKey" | "token" | "either"): string;
825
+ json<T>(method: string, path: string, body?: unknown, kind?: "apiKey" | "token" | "either", timeout?: false, signal?: AbortSignal, extraHeaders?: HeadersInit): Promise<T>;
826
+ raw(method: string, path: string, body?: BodyInit, kind?: "apiKey" | "token" | "either", contentType?: string, timeout?: false, signal?: AbortSignal): Promise<Response>;
827
+ renewIfNeeded(signal?: AbortSignal): Promise<void>;
828
+ stream(method: string, path: string, body?: BodyInit, kind?: "apiKey" | "token" | "either", contentType?: string, renew?: boolean, signal?: AbortSignal): AsyncGenerator<Uint8Array>;
829
+ }
830
+ declare class TemplatesResource {
831
+ private readonly client;
832
+ constructor(client: GatewayClient);
833
+ create(input: CreateTemplateInput): Promise<CreateTemplateResponse>;
834
+ list(): Promise<Template[]>;
835
+ /**
836
+ * Upload a `docker save` / OCI archive as a build input. The image is named
837
+ * by its own content, so re-uploading the same bytes is free, and a build
838
+ * that names it needs no registry credentials anywhere.
839
+ */
840
+ uploadImage(archive: BodyInit, options?: {
841
+ signal?: AbortSignal;
842
+ }): Promise<UploadedImage>;
843
+ activate(id: string): Promise<{
844
+ id: string;
845
+ name: string;
846
+ version: number;
847
+ }>;
848
+ }
849
+ declare class TemplateResourcesResource {
850
+ private readonly client;
851
+ constructor(client: GatewayClient);
852
+ get(name: string): Promise<TemplateResource>;
853
+ configure(name: string, input: ConfigureTemplateResourceInput): Promise<TemplateResource>;
854
+ delete(name: string): Promise<void>;
855
+ }
856
+ declare class TemplateBuildsResource {
857
+ private readonly client;
858
+ constructor(client: GatewayClient);
859
+ create(input: CreateTemplateBuildInput): Promise<{
860
+ build: TemplateBuildRecord;
861
+ }>;
862
+ get(id: string): Promise<{
863
+ build: TemplateBuildRecord;
864
+ }>;
865
+ retry(id: string): Promise<{
866
+ build: TemplateBuildRecord;
867
+ }>;
868
+ }
869
+ declare class TokensResource {
870
+ private readonly client;
871
+ constructor(client: GatewayClient);
872
+ mint(input: MintTokenInput): Promise<MintTokenResponse>;
873
+ renew(id: string): Promise<RenewTokenResponse>;
874
+ revoke(id: string): Promise<void>;
875
+ }
876
+ declare class FleetResource {
877
+ private readonly client;
878
+ constructor(client: GatewayClient);
879
+ /** List the nodes and this tenant's residents/snapshots visible to INSPECT. */
880
+ list(): Promise<FleetResponse>;
881
+ }
882
+ declare class ReservationsResource {
883
+ private readonly client;
884
+ constructor(client: GatewayClient);
885
+ /** Reserve capacity for a burst of sandboxes of one template/shape. */
886
+ create(input: CreateCapacityReservationInput): Promise<CapacityReservation>;
887
+ get(id: string): Promise<CapacityReservation>;
888
+ /** Release early. Unclaimed slots free immediately; bound nodes then fall
889
+ * back to the normal empty-worker scale-in grace period. */
890
+ release(id: string): Promise<CapacityReservation>;
891
+ renew(id: string, input: {
892
+ ttl_secs: number;
893
+ idempotency_key: string;
894
+ }): Promise<CapacityReservation>;
895
+ }
896
+ declare class SandboxesResource {
897
+ private readonly client;
898
+ constructor(client: GatewayClient);
899
+ /** List all of this tenant's non-destroyed sandboxes, including hibernated ones. */
900
+ list(): Promise<Sandbox[]>;
901
+ /**
902
+ * Acquire once under a caller-owned request ID. Repeat the exact request to
903
+ * recover a lost response. A pending receipt is not an executable sandbox;
904
+ * no automatic retry, new ID, or replacement allocation is synthesized here.
905
+ */
906
+ create(input: CreateSandboxInput, options?: WaitOptions): Promise<CreateSandboxResponse>;
907
+ /**
908
+ * Create `count` identical sandboxes under one request ID. Resolves as soon
909
+ * as the whole batch is durably recorded; nothing waits on a restore.
910
+ * Repeat the exact request to recover a lost response: member IDs are
911
+ * stable. Poll `batch` for counts by state, or a member with `acquisition`.
912
+ */
913
+ createBatch(input: CreateSandboxBatchInput): Promise<SandboxBatchResponse>;
914
+ /** Counts by state and every member of a batch created with `createBatch`. */
915
+ batch(requestId: string): Promise<SandboxBatchResponse>;
916
+ acquisition(requestId: string, options?: WaitOptions): Promise<CreateSandboxResponse>;
917
+ /**
918
+ * Fence an acquisition request ID before its create is recorded, or destroy
919
+ * the allocation when the create has already won.
920
+ */
921
+ cancelAcquisition(requestId: string): Promise<CreateSandboxResponse>;
922
+ get(id: string): Promise<Sandbox>;
923
+ /** Extend the soft expiry without moving the immutable hard deadline. */
924
+ renew(id: string, ttl_secs: number): Promise<RenewSandboxResponse>;
925
+ /**
926
+ * Authorize `public_key` on a running sandbox for `ttl_secs`, whether or not
927
+ * it was created with a key. Needs the sandbox-scoped exec capability. Connect
928
+ * with `CONNECT /v1/sandboxes/{id}/ssh`, or let `blackcat sandbox ssh` do it.
929
+ */
930
+ grantSshKey(id: string, input: GrantSshKeyInput): Promise<SshKeyGrant>;
931
+ /**
932
+ * Describe a tunnel to `port` (any TCP port the sandbox listens on, not only
933
+ * a declared preview port), at `path` on that port. Needs the sandbox-scoped
934
+ * exec capability, which is checked once when a connection opens: an open
935
+ * WebSocket outlives it. Renews a nearly expired capability first, so call
936
+ * this again for each new connection. A `CONNECT` to `url` instead returns
937
+ * a raw byte stream, as `blackcat sandbox port-forward` uses.
938
+ */
939
+ portConnection(id: string, port: number, path?: string): Promise<PortConnection>;
940
+ exec(id: string, input: ExecInput): Promise<ExecResult>;
941
+ execStream(id: string, input: ExecInput): AsyncGenerator<ExecStreamChunk>;
942
+ startProcess(id: string, requestId: string, input: ProcessStartInput): Promise<ProcessRecord>;
943
+ getProcess(id: string, processId: string): Promise<ProcessRecord>;
944
+ processLogs(id: string, processId: string, input?: number | {
945
+ stdout_offset?: number;
946
+ stderr_offset?: number;
947
+ max_bytes?: number;
948
+ }): Promise<ProcessLogsResponse>;
949
+ cancelProcess(id: string, processId: string): Promise<ProcessRecord>;
950
+ prepare(id: string, input: {
951
+ parameters: Record<string, string>;
952
+ secrets: Record<string, string>;
953
+ }, idempotencyKey: string): Promise<PrepareSandboxResponse>;
954
+ logs(id: string): AsyncGenerator<Uint8Array>;
955
+ readFileChunk(id: string, input: {
956
+ path: string;
957
+ offset: number;
958
+ length: number;
959
+ }): Promise<FileReadChunk>;
960
+ readFileStream(id: string, input: ReadFileInput): AsyncGenerator<FileReadChunk>;
961
+ readFile(id: string, input: ReadFileInput): Promise<Uint8Array>;
962
+ writeFileChunk(id: string, input: {
963
+ path: string;
964
+ offset: number;
965
+ data: Uint8Array;
966
+ truncate: boolean;
967
+ }): Promise<void>;
968
+ writeFileStream(id: string, input: WriteFileStreamInput): Promise<number>;
969
+ writeFile(id: string, input: WriteFileInput): Promise<number>;
970
+ /**
971
+ * Capture a restore point. The sandbox keeps running, and the returned
972
+ * template version is what it restarts from.
973
+ */
974
+ snapshot(id: string): Promise<{
975
+ template: string;
976
+ }>;
977
+ /**
978
+ * Pause a running sandbox's VM in memory: processes, memory and network
979
+ * state are kept, nothing is captured, and `resume` continues it where it
980
+ * stopped. Memory stays resident and charged (`hibernate` frees it); the CPU
981
+ * entitlement is released. Idempotent. Requests that need the guest are
982
+ * refused while it is paused.
983
+ */
984
+ pause(id: string): Promise<void>;
985
+ hibernate(id: string, input?: {
986
+ mode: HibernateMode;
987
+ }): Promise<void>;
988
+ /**
989
+ * Change a running sandbox's memory (live, within its checkpoint's hotplug
990
+ * region) and CPU entitlement; processes keep running. A `vcpus` different
991
+ * from the sandbox's is a cold reboot instead (`cold: true`). Repeating the
992
+ * same `request_id` replays the result.
993
+ */
994
+ resize(id: string, input: ResizeSandboxInput): Promise<SandboxResize>;
995
+ /** Continue a paused sandbox, or restore a hibernated one. */
996
+ resume(id: string): Promise<void>;
997
+ /**
998
+ * Destroy a sandbox. Resolves when its VM is dead and the node has released
999
+ * its capacity, not when the node has finished removing its network device,
1000
+ * jail and cgroup: that follows in the background, usually within a second.
1001
+ * Destroying a sandbox that is already destroyed succeeds.
1002
+ */
1003
+ destroy(id: string): Promise<void>;
1004
+ }
1005
+ export declare class BlackcatClient extends GatewayClient {
1006
+ readonly templates: TemplatesResource;
1007
+ readonly templateResources: TemplateResourcesResource;
1008
+ readonly templateBuilds: TemplateBuildsResource;
1009
+ readonly sandboxes: SandboxesResource;
1010
+ readonly tokens: TokensResource;
1011
+ readonly fleet: FleetResource;
1012
+ readonly reservations: ReservationsResource;
1013
+ constructor(options: BlackcatClientOptions);
1014
+ /**
1015
+ * Create a client that uses a sandbox capability for resource operations.
1016
+ * Passing the complete mint response enables renewal during long streams.
1017
+ */
1018
+ withToken(token: string | MintTokenResponse, renewCapability?: () => Promise<MintTokenResponse>): BlackcatClient;
1019
+ }
1020
+ export { MAX_EXEC_STDIN, MAX_FILE_CHUNK, MAX_FILE_CHUNK as MAX_FILE_WRITE_CHUNK, MAX_FILE_READ_CHUNK, MAX_PROCESS_STDIN, };