@runblackcat/sdk 0.1.0-alpha.88 → 0.1.0-alpha.90

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/README.md CHANGED
@@ -168,6 +168,12 @@ GET /v1/sandboxes/acquisitions/{id} -> 202 pending … -> 200 {"state":"p
168
168
  POST /v1/sandboxes/{id}/renew {ttl_secs} -> 200 {"expires_at":…, "renewable_until":…}
169
169
  ```
170
170
 
171
+ Add `?wait_secs=N` (held at most 55 s) to the create or the lookup to have the gateway
172
+ hold a still-pending claim and answer as soon as it is placed or fails; the
173
+ placed receipt carries `ports`, so there is nothing to poll and no second call.
174
+ A claim still pending when the hold ends is answered `pending`; ask again. It
175
+ does not apply to a `count` batch: wait on each member by `request_id`.
176
+
171
177
  To start many identical sandboxes, add `"count": N` (1 to 1000) to the create.
172
178
  `request_id` then names the batch and member `i` is `request_id.i`. The whole batch
173
179
  is recorded in one durable transaction and answered `202` at once, with no
@@ -187,7 +193,7 @@ sandbox.
187
193
  Backpressure is fleet capacity and the durable queue; there are no per-tenant
188
194
  quotas. A queued acquisition that waits past the queue deadline is torn down by
189
195
  the ordinary expiry reconciler. Request-rate limits are operator-set fleet state
190
- (`blackcat admin rate-limits get|set`): token mint and OIDC limits are on by
196
+ (`blackcat admin rate-limits get|set`): session token-mint and OIDC limits are on by
191
197
  default, every workload limit is unlimited until set.
192
198
 
193
199
  The placer sizes the fleet from that backlog through the `CapacityProvider`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runblackcat/sdk",
3
- "version": "0.1.0-alpha.88",
3
+ "version": "0.1.0-alpha.90",
4
4
  "description": "TypeScript SDK for the Blackcat sandbox gateway",
5
5
  "license": "Apache-2.0",
6
6
  "repository": "https://github.com/TreebeardHQ/blackcat",
@@ -33,7 +33,14 @@ export type FetchFunction = (input: RequestInfo | URL, init?: RequestInit & {
33
33
  }) => Promise<Response>;
34
34
  export interface BlackcatClientOptions {
35
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
+ */
36
42
  apiKey?: string;
43
+ /** A capability token; preferred over `apiKey` when both are set. */
37
44
  token?: string;
38
45
  /** Capability identity and expiry, using idiomatic TypeScript names. */
39
46
  tokenId?: string;
@@ -369,8 +376,8 @@ export type CreateCapacityReservationInput = ({
369
376
  * ordinary work; a sandbox create naming it waits for one of its slots. */
370
377
  placement?: "shared" | "dedicated";
371
378
  /** With `placement: "dedicated"`, the provider shape to launch: an exact
372
- * shape or a family (`c7i`) of the fleet's capacity shapes. Absent: the
373
- * planner chooses. */
379
+ * shape or a family (`c7i`) the fleet can launch (direct EC2: the config's
380
+ * instance_types). Absent: the planner chooses. */
374
381
  shape?: string;
375
382
  /** Stable retry key, at most 107 ASCII letters, digits, `_` or `-`. */
376
383
  idempotency_key?: string;
@@ -406,6 +413,15 @@ export interface CapacityReservation {
406
413
  /** When the capacity must be ready (epoch milliseconds), or null. */
407
414
  needed_by: number | null;
408
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
+ }
409
425
  /**
410
426
  * Response returned by POST /v1/sandboxes (the gateway has no template field).
411
427
  * The gateway sends a `failed` receipt with HTTP 409, so `create` and
@@ -872,7 +888,7 @@ declare class SandboxesResource {
872
888
  * recover a lost response. A pending receipt is not an executable sandbox;
873
889
  * no automatic retry, new ID, or replacement allocation is synthesized here.
874
890
  */
875
- create(input: CreateSandboxInput): Promise<CreateSandboxResponse>;
891
+ create(input: CreateSandboxInput, options?: WaitOptions): Promise<CreateSandboxResponse>;
876
892
  /**
877
893
  * Create `count` identical sandboxes under one request ID. Resolves as soon
878
894
  * as the whole batch is durably recorded; nothing waits on a restore.
@@ -882,7 +898,7 @@ declare class SandboxesResource {
882
898
  createBatch(input: CreateSandboxBatchInput): Promise<SandboxBatchResponse>;
883
899
  /** Counts by state and every member of a batch created with `createBatch`. */
884
900
  batch(requestId: string): Promise<SandboxBatchResponse>;
885
- acquisition(requestId: string): Promise<CreateSandboxResponse>;
901
+ acquisition(requestId: string, options?: WaitOptions): Promise<CreateSandboxResponse>;
886
902
  /**
887
903
  * Fence an acquisition request ID before its create is recorded, or destroy
888
904
  * the allocation when the create has already won.
package/sdk/dist/index.js CHANGED
@@ -21,6 +21,9 @@ var BLACKCAT_ERROR_CODES = {
21
21
  function withTemplate(input) {
22
22
  return { ...input, template: input.template ?? "default" };
23
23
  }
24
+ function waitQuery({ wait_secs }) {
25
+ return wait_secs === undefined ? "" : `?wait_secs=${wait_secs}`;
26
+ }
24
27
 
25
28
  class BlackcatApiError extends Error {
26
29
  status;
@@ -351,17 +354,17 @@ class TemplatesResource {
351
354
  this.client = client;
352
355
  }
353
356
  create(input) {
354
- return this.client.json("POST", "/v1/templates", input, "token");
357
+ return this.client.json("POST", "/v1/templates", input, "either");
355
358
  }
356
359
  list() {
357
- return this.client.json("GET", "/v1/templates", undefined, "token");
360
+ return this.client.json("GET", "/v1/templates", undefined, "either");
358
361
  }
359
362
  async uploadImage(archive, options) {
360
363
  const response = await this.client.raw("POST", "/v1/template-images", archive, "either", "application/octet-stream", false, options?.signal);
361
364
  return await response.json();
362
365
  }
363
366
  activate(id) {
364
- return this.client.json("POST", `/v1/templates/${pathSegment(id)}/activate`, undefined, "token");
367
+ return this.client.json("POST", `/v1/templates/${pathSegment(id)}/activate`, undefined, "either");
365
368
  }
366
369
  }
367
370
 
@@ -371,13 +374,13 @@ class TemplateResourcesResource {
371
374
  this.client = client;
372
375
  }
373
376
  get(name) {
374
- return this.client.json("GET", `/v1/template-resources/${pathSegment(name)}`, undefined, "token");
377
+ return this.client.json("GET", `/v1/template-resources/${pathSegment(name)}`, undefined, "either");
375
378
  }
376
379
  configure(name, input) {
377
- return this.client.json("PUT", `/v1/template-resources/${pathSegment(name)}`, input, "token");
380
+ return this.client.json("PUT", `/v1/template-resources/${pathSegment(name)}`, input, "either");
378
381
  }
379
382
  delete(name) {
380
- return this.client.json("DELETE", `/v1/template-resources/${pathSegment(name)}`, undefined, "token");
383
+ return this.client.json("DELETE", `/v1/template-resources/${pathSegment(name)}`, undefined, "either");
381
384
  }
382
385
  }
383
386
 
@@ -387,13 +390,13 @@ class TemplateBuildsResource {
387
390
  this.client = client;
388
391
  }
389
392
  create(input) {
390
- return this.client.json("POST", "/v1/template-builds", input, "token");
393
+ return this.client.json("POST", "/v1/template-builds", input, "either");
391
394
  }
392
395
  get(id) {
393
- return this.client.json("GET", `/v1/template-builds/${pathSegment(id)}`, undefined, "token");
396
+ return this.client.json("GET", `/v1/template-builds/${pathSegment(id)}`, undefined, "either");
394
397
  }
395
398
  retry(id) {
396
- return this.client.json("POST", `/v1/template-builds/${pathSegment(id)}/retry`, undefined, "token");
399
+ return this.client.json("POST", `/v1/template-builds/${pathSegment(id)}/retry`, undefined, "either");
397
400
  }
398
401
  }
399
402
 
@@ -424,7 +427,7 @@ class FleetResource {
424
427
  this.client = client;
425
428
  }
426
429
  list() {
427
- return this.client.json("GET", "/v1/fleet", undefined, "token");
430
+ return this.client.json("GET", "/v1/fleet", undefined, "either");
428
431
  }
429
432
  }
430
433
 
@@ -434,16 +437,16 @@ class ReservationsResource {
434
437
  this.client = client;
435
438
  }
436
439
  create(input) {
437
- return this.client.json("POST", "/v1/capacity-reservations", input, "token");
440
+ return this.client.json("POST", "/v1/capacity-reservations", input, "either");
438
441
  }
439
442
  get(id) {
440
- return this.client.json("GET", `/v1/capacity-reservations/${pathSegment(id)}`, undefined, "token");
443
+ return this.client.json("GET", `/v1/capacity-reservations/${pathSegment(id)}`, undefined, "either");
441
444
  }
442
445
  release(id) {
443
- return this.client.json("DELETE", `/v1/capacity-reservations/${pathSegment(id)}`, undefined, "token");
446
+ return this.client.json("DELETE", `/v1/capacity-reservations/${pathSegment(id)}`, undefined, "either");
444
447
  }
445
448
  renew(id, input) {
446
- return this.client.json("POST", `/v1/capacity-reservations/${pathSegment(id)}/renew`, input, "token");
449
+ return this.client.json("POST", `/v1/capacity-reservations/${pathSegment(id)}/renew`, input, "either");
447
450
  }
448
451
  }
449
452
 
@@ -453,34 +456,34 @@ class SandboxesResource {
453
456
  this.client = client;
454
457
  }
455
458
  list() {
456
- return this.client.json("GET", "/v1/sandboxes", undefined, "token");
459
+ return this.client.json("GET", "/v1/sandboxes", undefined, "either");
457
460
  }
458
- async create(input) {
459
- return parseCreateSandboxResponse(await this.client.json("POST", "/v1/sandboxes", withTemplate(input), "token", false), input.request_id);
461
+ async create(input, options = {}) {
462
+ return parseCreateSandboxResponse(await this.client.json("POST", `/v1/sandboxes${waitQuery(options)}`, withTemplate(input), "either", false), input.request_id);
460
463
  }
461
464
  async createBatch(input) {
462
- return parseSandboxBatch(await this.client.json("POST", "/v1/sandboxes", withTemplate(input), "token", false), input.request_id);
465
+ return parseSandboxBatch(await this.client.json("POST", "/v1/sandboxes", withTemplate(input), "either", false), input.request_id);
463
466
  }
464
467
  async batch(requestId) {
465
- return parseSandboxBatch(await this.client.json("GET", `/v1/sandboxes/batches/${pathSegment(requestId)}`, undefined, "token"), requestId);
468
+ return parseSandboxBatch(await this.client.json("GET", `/v1/sandboxes/batches/${pathSegment(requestId)}`, undefined, "either"), requestId);
466
469
  }
467
- async acquisition(requestId) {
468
- return parseCreateSandboxResponse(await this.client.json("GET", `/v1/sandboxes/acquisitions/${pathSegment(requestId)}`, undefined, "token"), requestId);
470
+ async acquisition(requestId, options = {}) {
471
+ return parseCreateSandboxResponse(await this.client.json("GET", `/v1/sandboxes/acquisitions/${pathSegment(requestId)}${waitQuery(options)}`, undefined, "either", options.wait_secs === undefined ? undefined : false), requestId);
469
472
  }
470
473
  async cancelAcquisition(requestId) {
471
- return parseCreateSandboxResponse(await this.client.json("DELETE", `/v1/sandboxes/acquisitions/${pathSegment(requestId)}`, undefined, "token"), requestId);
474
+ return parseCreateSandboxResponse(await this.client.json("DELETE", `/v1/sandboxes/acquisitions/${pathSegment(requestId)}`, undefined, "either"), requestId);
472
475
  }
473
476
  get(id) {
474
- return this.client.json("GET", `/v1/sandboxes/${pathSegment(id)}`, undefined, "token");
477
+ return this.client.json("GET", `/v1/sandboxes/${pathSegment(id)}`, undefined, "either");
475
478
  }
476
479
  renew(id, ttl_secs) {
477
- return this.client.json("POST", `/v1/sandboxes/${pathSegment(id)}/renew`, { ttl_secs }, "token");
480
+ return this.client.json("POST", `/v1/sandboxes/${pathSegment(id)}/renew`, { ttl_secs }, "either");
478
481
  }
479
482
  grantSshKey(id, input) {
480
- return this.client.json("POST", `/v1/sandboxes/${pathSegment(id)}/ssh-keys`, input, "token");
483
+ return this.client.json("POST", `/v1/sandboxes/${pathSegment(id)}/ssh-keys`, input, "either");
481
484
  }
482
485
  async exec(id, input) {
483
- return this.client.json("POST", `/v1/sandboxes/${pathSegment(id)}/exec`, input, "token", false, execDeadline(input)).then(execResult);
486
+ return this.client.json("POST", `/v1/sandboxes/${pathSegment(id)}/exec`, input, "either", false, execDeadline(input)).then(execResult);
484
487
  }
485
488
  async* execStream(id, input) {
486
489
  if (input.stdin !== undefined && input.stdin.length > MAX_EXEC_STDIN) {
@@ -496,7 +499,7 @@ class SandboxesResource {
496
499
  return chunk;
497
500
  };
498
501
  const decoder = new TextDecoder;
499
- for await (const chunk of this.client.stream("POST", `/v1/sandboxes/${pathSegment(id)}/exec/stream`, JSON.stringify(input), "token", "application/json", true, execDeadline(input))) {
502
+ for await (const chunk of this.client.stream("POST", `/v1/sandboxes/${pathSegment(id)}/exec/stream`, JSON.stringify(input), "either", "application/json", true, execDeadline(input))) {
500
503
  lineBuffer += decoder.decode(chunk, { stream: true });
501
504
  const lines = lineBuffer.split(`
502
505
  `);
@@ -519,10 +522,10 @@ class SandboxesResource {
519
522
  return this.client.json("POST", `/v1/sandboxes/${pathSegment(id)}/processes`, {
520
523
  ...input,
521
524
  ...input.stdin === undefined ? {} : { stdin: Array.from(input.stdin) }
522
- }, "token", false, undefined, { "idempotency-key": requestId });
525
+ }, "either", false, undefined, { "idempotency-key": requestId });
523
526
  }
524
527
  getProcess(id, processId) {
525
- return this.client.json("GET", `/v1/sandboxes/${pathSegment(id)}/processes/${pathSegment(processId)}`, undefined, "token");
528
+ return this.client.json("GET", `/v1/sandboxes/${pathSegment(id)}/processes/${pathSegment(processId)}`, undefined, "either");
526
529
  }
527
530
  async processLogs(id, processId, input = {}) {
528
531
  const options = typeof input === "number" ? { stdout_offset: 0, stderr_offset: 0, max_bytes: input } : input;
@@ -534,7 +537,7 @@ class SandboxesResource {
534
537
  if (options.max_bytes !== undefined)
535
538
  query.set("max_bytes", String(options.max_bytes));
536
539
  const suffix = query.size === 0 ? "" : `?${query.toString()}`;
537
- const response = await this.client.json("GET", `/v1/sandboxes/${pathSegment(id)}/processes/${pathSegment(processId)}/logs${suffix}`, undefined, "token");
540
+ const response = await this.client.json("GET", `/v1/sandboxes/${pathSegment(id)}/processes/${pathSegment(processId)}/logs${suffix}`, undefined, "either");
538
541
  return {
539
542
  ...response,
540
543
  logs: {
@@ -545,13 +548,13 @@ class SandboxesResource {
545
548
  };
546
549
  }
547
550
  cancelProcess(id, processId) {
548
- return this.client.json("DELETE", `/v1/sandboxes/${pathSegment(id)}/processes/${pathSegment(processId)}`, undefined, "token");
551
+ return this.client.json("DELETE", `/v1/sandboxes/${pathSegment(id)}/processes/${pathSegment(processId)}`, undefined, "either");
549
552
  }
550
553
  prepare(id, input, idempotencyKey) {
551
- return this.client.json("POST", `/v1/sandboxes/${pathSegment(id)}/prepare`, input, "token", undefined, undefined, { "idempotency-key": idempotencyKey });
554
+ return this.client.json("POST", `/v1/sandboxes/${pathSegment(id)}/prepare`, input, "either", undefined, undefined, { "idempotency-key": idempotencyKey });
552
555
  }
553
556
  logs(id) {
554
- return this.client.stream("GET", `/v1/sandboxes/${pathSegment(id)}/logs`, undefined, "token", undefined, true);
557
+ return this.client.stream("GET", `/v1/sandboxes/${pathSegment(id)}/logs`, undefined, "either", undefined, true);
555
558
  }
556
559
  async readFileChunk(id, input) {
557
560
  if (input.offset < 0 || input.length <= 0 || input.length > MAX_FILE_READ_CHUNK) {
@@ -562,7 +565,7 @@ class SandboxesResource {
562
565
  offset: String(input.offset),
563
566
  length: String(input.length)
564
567
  });
565
- const response = await this.client.raw("GET", `/v1/sandboxes/${pathSegment(id)}/files/read?${query.toString()}`, undefined, "token");
568
+ const response = await this.client.raw("GET", `/v1/sandboxes/${pathSegment(id)}/files/read?${query.toString()}`, undefined, "either");
566
569
  const data = new Uint8Array(await response.arrayBuffer());
567
570
  const offset = Number(response.headers.get("x-blackcat-offset") ?? input.offset);
568
571
  const eof = response.headers.get("x-blackcat-eof") === "true";
@@ -613,7 +616,7 @@ class SandboxesResource {
613
616
  offset: String(input.offset),
614
617
  truncate: String(input.truncate)
615
618
  });
616
- await this.client.raw("POST", `/v1/sandboxes/${pathSegment(id)}/files/write?${query.toString()}`, requestBody(input.data), "token", "application/octet-stream");
619
+ await this.client.raw("POST", `/v1/sandboxes/${pathSegment(id)}/files/write?${query.toString()}`, requestBody(input.data), "either", "application/octet-stream");
617
620
  }
618
621
  async writeFileStream(id, input) {
619
622
  const start = input.offset ?? 0;
@@ -658,28 +661,28 @@ class SandboxesResource {
658
661
  });
659
662
  }
660
663
  snapshot(id) {
661
- return this.client.json("POST", `/v1/sandboxes/${pathSegment(id)}/snapshot`, undefined, "token");
664
+ return this.client.json("POST", `/v1/sandboxes/${pathSegment(id)}/snapshot`, undefined, "either");
662
665
  }
663
666
  pause(id) {
664
- return this.client.raw("POST", `/v1/sandboxes/${pathSegment(id)}/pause`, undefined, "token").then(() => {
667
+ return this.client.raw("POST", `/v1/sandboxes/${pathSegment(id)}/pause`, undefined, "either").then(() => {
665
668
  return;
666
669
  });
667
670
  }
668
671
  hibernate(id, input) {
669
- return this.client.raw("POST", `/v1/sandboxes/${pathSegment(id)}/hibernate`, input === undefined ? undefined : JSON.stringify(input), "token", input === undefined ? undefined : "application/json", false).then(() => {
672
+ return this.client.raw("POST", `/v1/sandboxes/${pathSegment(id)}/hibernate`, input === undefined ? undefined : JSON.stringify(input), "either", input === undefined ? undefined : "application/json", false).then(() => {
670
673
  return;
671
674
  });
672
675
  }
673
676
  resize(id, input) {
674
- return this.client.json("POST", `/v1/sandboxes/${pathSegment(id)}/resize`, { ...input, request_id: input.request_id ?? `resize-${crypto.randomUUID()}` }, "token", false);
677
+ return this.client.json("POST", `/v1/sandboxes/${pathSegment(id)}/resize`, { ...input, request_id: input.request_id ?? `resize-${crypto.randomUUID()}` }, "either", false);
675
678
  }
676
679
  resume(id) {
677
- return this.client.raw("POST", `/v1/sandboxes/${pathSegment(id)}/resume`, undefined, "token").then(() => {
680
+ return this.client.raw("POST", `/v1/sandboxes/${pathSegment(id)}/resume`, undefined, "either").then(() => {
678
681
  return;
679
682
  });
680
683
  }
681
684
  destroy(id) {
682
- return this.client.json("DELETE", `/v1/sandboxes/${pathSegment(id)}`, undefined, "token");
685
+ return this.client.json("DELETE", `/v1/sandboxes/${pathSegment(id)}`, undefined, "either");
683
686
  }
684
687
  }
685
688