@openkaiden/opnshll-sdk 0.0.116 → 0.1.2
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/client.d.ts +654 -21
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +484 -97
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +17 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +23 -0
- package/dist/errors.js.map +1 -1
- package/dist/gen/datamodel_pb.d.ts +68 -13
- package/dist/gen/datamodel_pb.d.ts.map +1 -1
- package/dist/gen/datamodel_pb.js +17 -6
- package/dist/gen/datamodel_pb.js.map +1 -1
- package/dist/gen/google/rpc/error_details_pb.d.ts +581 -0
- package/dist/gen/google/rpc/error_details_pb.d.ts.map +1 -0
- package/dist/gen/google/rpc/error_details_pb.js +90 -0
- package/dist/gen/google/rpc/error_details_pb.js.map +1 -0
- package/dist/gen/openshell_pb.d.ts +3333 -1355
- package/dist/gen/openshell_pb.d.ts.map +1 -1
- package/dist/gen/openshell_pb.js +848 -220
- package/dist/gen/openshell_pb.js.map +1 -1
- package/dist/gen/sandbox_pb.d.ts +172 -39
- package/dist/gen/sandbox_pb.d.ts.map +1 -1
- package/dist/gen/sandbox_pb.js +109 -4
- package/dist/gen/sandbox_pb.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/raw.d.ts +1 -0
- package/dist/raw.d.ts.map +1 -1
- package/dist/raw.js +1 -0
- package/dist/raw.js.map +1 -1
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -1,13 +1,39 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
import * as net from 'node:net';
|
|
4
|
+
import { durationFromMs } from '@bufbuild/protobuf/wkt';
|
|
4
5
|
import { createClient } from '@connectrpc/connect';
|
|
5
6
|
import { errorCode, fromConnect, SdkError } from './errors.js';
|
|
6
7
|
import { OpenShell, SandboxPhase, ServiceStatus, } from './gen/openshell_pb.js';
|
|
7
8
|
import { PolicySource, SettingScope } from './gen/sandbox_pb.js';
|
|
8
9
|
import { validateSshResponse } from './ssh-validate.js';
|
|
9
10
|
import { buildTransport } from './transport.js';
|
|
11
|
+
function durationFromSeconds(seconds) {
|
|
12
|
+
if (!Number.isFinite(seconds) || seconds < 0) {
|
|
13
|
+
throw new RangeError('timeoutSecs must be a finite, non-negative number');
|
|
14
|
+
}
|
|
15
|
+
return seconds === 0 ? undefined : durationFromMs(seconds * 1000);
|
|
16
|
+
}
|
|
17
|
+
function timestampMillis(timestamp) {
|
|
18
|
+
if (!timestamp)
|
|
19
|
+
return undefined;
|
|
20
|
+
const millis = timestamp.seconds * 1000n + BigInt(Math.trunc(timestamp.nanos / 1_000_000));
|
|
21
|
+
return millis === 0n ? undefined : millis.toString();
|
|
22
|
+
}
|
|
10
23
|
export { errorCode };
|
|
24
|
+
function deletionResult(response) {
|
|
25
|
+
const names = {
|
|
26
|
+
0: 'unspecified',
|
|
27
|
+
1: 'completed',
|
|
28
|
+
2: 'accepted',
|
|
29
|
+
3: 'already_absent',
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
outcome: names[response.outcome] ?? 'unknown',
|
|
33
|
+
rawOutcome: response.outcome,
|
|
34
|
+
...(response.sandboxId ? { sandboxId: response.sandboxId } : {}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
11
37
|
// ---- enum → lowercase string -----------------------------------------------
|
|
12
38
|
// Exported for the enum-name drift test only; not re-exported from index.ts, so
|
|
13
39
|
// they are not part of the public package API.
|
|
@@ -21,6 +47,7 @@ export const PHASE_NAMES = {
|
|
|
21
47
|
[SandboxPhase.STOPPING]: 'stopping',
|
|
22
48
|
[SandboxPhase.STOPPED]: 'stopped',
|
|
23
49
|
[SandboxPhase.STARTING]: 'starting',
|
|
50
|
+
[SandboxPhase.COMPLETED]: 'completed',
|
|
24
51
|
};
|
|
25
52
|
export const STATUS_NAMES = {
|
|
26
53
|
[ServiceStatus.UNSPECIFIED]: 'unspecified',
|
|
@@ -50,7 +77,7 @@ function scopeName(s) {
|
|
|
50
77
|
function policySourceName(s) {
|
|
51
78
|
return POLICY_SOURCE_NAMES[s] ?? 'unspecified';
|
|
52
79
|
}
|
|
53
|
-
function sandboxRef(sandbox) {
|
|
80
|
+
function sandboxRef(sandbox, serviceUrls = {}) {
|
|
54
81
|
if (!sandbox)
|
|
55
82
|
throw new SdkError('invalid_config', 'sandbox missing from gateway response');
|
|
56
83
|
const meta = sandbox.metadata;
|
|
@@ -60,13 +87,26 @@ function sandboxRef(sandbox) {
|
|
|
60
87
|
return {
|
|
61
88
|
id: meta.id,
|
|
62
89
|
name: meta.name,
|
|
90
|
+
workspace: meta.workspace,
|
|
63
91
|
phase: phaseName(sandbox.status?.phase ?? SandboxPhase.UNSPECIFIED),
|
|
64
92
|
labels: meta?.labels ?? {},
|
|
65
93
|
resourceVersion: (meta?.resourceVersion ?? 0n).toString(),
|
|
66
94
|
mainProcessInstanceId: sandbox.status?.mainProcessInstanceId || undefined,
|
|
67
95
|
exitCode: sandbox.status?.exitCode,
|
|
96
|
+
createdFromWorkloadTemplate: sandbox.createdFromWorkloadTemplate
|
|
97
|
+
? {
|
|
98
|
+
name: sandbox.createdFromWorkloadTemplate.name,
|
|
99
|
+
resourceVersion: sandbox.createdFromWorkloadTemplate.resourceVersion,
|
|
100
|
+
}
|
|
101
|
+
: undefined,
|
|
102
|
+
serviceUrls,
|
|
68
103
|
};
|
|
69
104
|
}
|
|
105
|
+
function sandboxTemplate(template) {
|
|
106
|
+
if (!template)
|
|
107
|
+
throw new SdkError('invalid_config', 'sandbox template missing from gateway response');
|
|
108
|
+
return template;
|
|
109
|
+
}
|
|
70
110
|
function providerRef(provider) {
|
|
71
111
|
const meta = provider.metadata;
|
|
72
112
|
return {
|
|
@@ -128,6 +168,30 @@ function versionPin(value) {
|
|
|
128
168
|
return pin;
|
|
129
169
|
}
|
|
130
170
|
const FORWARD_CHUNK = 64 * 1024;
|
|
171
|
+
function workspaceName(options) {
|
|
172
|
+
const workspace = options?.workspace ?? 'default';
|
|
173
|
+
if (workspace.trim() === '')
|
|
174
|
+
throw new SdkError('invalid_config', 'workspace must be non-empty');
|
|
175
|
+
return workspace;
|
|
176
|
+
}
|
|
177
|
+
function workspaceScope(options) {
|
|
178
|
+
return { selection: { case: 'workspace', value: workspaceName(options) } };
|
|
179
|
+
}
|
|
180
|
+
function listWorkspaceScope(options) {
|
|
181
|
+
return options?.allWorkspaces ? { selection: { case: 'allWorkspaces', value: {} } } : workspaceScope(options);
|
|
182
|
+
}
|
|
183
|
+
function sandboxTarget(name, options) {
|
|
184
|
+
return { sandbox: name, workspaceScope: workspaceScope(options) };
|
|
185
|
+
}
|
|
186
|
+
function namedTarget(name, options) {
|
|
187
|
+
return { name, workspaceScope: workspaceScope(options) };
|
|
188
|
+
}
|
|
189
|
+
function requestCallOptions(options) {
|
|
190
|
+
if (!options)
|
|
191
|
+
return undefined;
|
|
192
|
+
const { workspace: _workspace, ...callOptions } = options;
|
|
193
|
+
return callOptions;
|
|
194
|
+
}
|
|
131
195
|
// Build CallOptions that bound one poll RPC by the remaining wall-clock budget
|
|
132
196
|
// and honor caller cancellation, so a stalled RPC cannot outlive the deadline.
|
|
133
197
|
function deadlineOptions(remainingMs, signal) {
|
|
@@ -136,12 +200,12 @@ function deadlineOptions(remainingMs, signal) {
|
|
|
136
200
|
signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
|
|
137
201
|
};
|
|
138
202
|
}
|
|
139
|
-
// Translate a poll failure at the wait boundary: caller cancellation and
|
|
140
|
-
// deadline
|
|
141
|
-
function mapWaitError(err, name, deadline, signal) {
|
|
203
|
+
// Translate a poll failure at the wait boundary: caller cancellation and the
|
|
204
|
+
// poll's deadline signal both become explicit SdkErrors; anything else propagates.
|
|
205
|
+
function mapWaitError(err, name, deadline, signal, pollSignal) {
|
|
142
206
|
if (signal?.aborted)
|
|
143
207
|
return new SdkError('connect', `wait for sandbox '${name}' aborted`);
|
|
144
|
-
if (Date.now() >= deadline)
|
|
208
|
+
if (pollSignal?.aborted || Date.now() >= deadline)
|
|
145
209
|
return new SdkError('connect', `timed out waiting for sandbox '${name}'`);
|
|
146
210
|
return err instanceof SdkError ? err : fromConnect(err);
|
|
147
211
|
}
|
|
@@ -245,6 +309,198 @@ export class Pushable {
|
|
|
245
309
|
}
|
|
246
310
|
}
|
|
247
311
|
}
|
|
312
|
+
// Single producer/consumer queue. Closing wakes both directions, including a
|
|
313
|
+
// producer blocked by backpressure. Successful completion drains queued values.
|
|
314
|
+
class ExecOutputQueue {
|
|
315
|
+
values = [];
|
|
316
|
+
ended = false;
|
|
317
|
+
error;
|
|
318
|
+
reader;
|
|
319
|
+
writer;
|
|
320
|
+
async push(value) {
|
|
321
|
+
// The terminal exit carries no output bytes and must never block cleanup
|
|
322
|
+
// after done has already settled and its cancellation listener is removed.
|
|
323
|
+
while (!('type' in value) && this.values.length >= 16 && !this.ended) {
|
|
324
|
+
await new Promise((resolve) => {
|
|
325
|
+
this.writer = resolve;
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
if (this.ended)
|
|
329
|
+
throw this.error ?? new SdkError('canceled', 'exec output closed');
|
|
330
|
+
this.values.push(value);
|
|
331
|
+
this.reader?.();
|
|
332
|
+
this.reader = undefined;
|
|
333
|
+
}
|
|
334
|
+
end(error, discard = false) {
|
|
335
|
+
if (discard)
|
|
336
|
+
this.values.length = 0;
|
|
337
|
+
if (!this.ended) {
|
|
338
|
+
this.ended = true;
|
|
339
|
+
this.error = error;
|
|
340
|
+
}
|
|
341
|
+
this.reader?.();
|
|
342
|
+
this.writer?.();
|
|
343
|
+
this.reader = undefined;
|
|
344
|
+
this.writer = undefined;
|
|
345
|
+
}
|
|
346
|
+
async *[Symbol.asyncIterator]() {
|
|
347
|
+
for (;;) {
|
|
348
|
+
const value = this.values.shift();
|
|
349
|
+
if (value !== undefined) {
|
|
350
|
+
this.writer?.();
|
|
351
|
+
this.writer = undefined;
|
|
352
|
+
yield value;
|
|
353
|
+
}
|
|
354
|
+
else if (this.ended) {
|
|
355
|
+
if (this.error !== undefined)
|
|
356
|
+
throw this.error;
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
await new Promise((resolve) => {
|
|
361
|
+
this.reader = resolve;
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const maxConsumedPageTokens = 10_000;
|
|
368
|
+
const maxConsumedPageTokenBytes = 1 << 20;
|
|
369
|
+
/** Lazy, single-pass iterator that fetches one RPC page per advance. */
|
|
370
|
+
export class Pager {
|
|
371
|
+
fetch;
|
|
372
|
+
maxConsumedTokens;
|
|
373
|
+
maxConsumedTokenBytes;
|
|
374
|
+
nextToken;
|
|
375
|
+
consumedTokens = new Set();
|
|
376
|
+
consumedTokenBytes = 0;
|
|
377
|
+
constructor(fetch, pageToken = '', maxConsumedTokens = maxConsumedPageTokens, maxConsumedTokenBytes = maxConsumedPageTokenBytes) {
|
|
378
|
+
this.fetch = fetch;
|
|
379
|
+
this.maxConsumedTokens = maxConsumedTokens;
|
|
380
|
+
this.maxConsumedTokenBytes = maxConsumedTokenBytes;
|
|
381
|
+
this.nextToken = pageToken;
|
|
382
|
+
}
|
|
383
|
+
validateCurrentTokenBudget(pageToken) {
|
|
384
|
+
if (pageToken === '')
|
|
385
|
+
return 0;
|
|
386
|
+
const tokenBytes = new TextEncoder().encode(pageToken).byteLength;
|
|
387
|
+
if (this.consumedTokens.size >= this.maxConsumedTokens ||
|
|
388
|
+
tokenBytes > this.maxConsumedTokenBytes - this.consumedTokenBytes) {
|
|
389
|
+
throw new Error('pager continuation token history limit exceeded');
|
|
390
|
+
}
|
|
391
|
+
return tokenBytes;
|
|
392
|
+
}
|
|
393
|
+
/** Fetch the next page, or return undefined after the final page. */
|
|
394
|
+
async nextPage() {
|
|
395
|
+
if (this.nextToken === undefined)
|
|
396
|
+
return undefined;
|
|
397
|
+
const pageToken = this.nextToken;
|
|
398
|
+
const tokenBytes = this.validateCurrentTokenBudget(pageToken);
|
|
399
|
+
const page = await this.fetch(pageToken);
|
|
400
|
+
if (pageToken !== '') {
|
|
401
|
+
this.consumedTokens.add(pageToken);
|
|
402
|
+
this.consumedTokenBytes += tokenBytes;
|
|
403
|
+
}
|
|
404
|
+
if (page.nextPageToken !== '' && this.consumedTokens.has(page.nextPageToken)) {
|
|
405
|
+
throw new Error('pager received a repeated continuation token');
|
|
406
|
+
}
|
|
407
|
+
this.nextToken = page.nextPageToken === '' ? undefined : page.nextPageToken;
|
|
408
|
+
return page;
|
|
409
|
+
}
|
|
410
|
+
/** Consume the pager and collect every remaining item. */
|
|
411
|
+
async all() {
|
|
412
|
+
const items = [];
|
|
413
|
+
for await (const page of this)
|
|
414
|
+
items.push(...page.items);
|
|
415
|
+
return items;
|
|
416
|
+
}
|
|
417
|
+
async *[Symbol.asyncIterator]() {
|
|
418
|
+
for (;;) {
|
|
419
|
+
const page = await this.nextPage();
|
|
420
|
+
if (page === undefined)
|
|
421
|
+
return;
|
|
422
|
+
yield page;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
// ---- sandbox template client ----------------------------------------------
|
|
427
|
+
// Reusable sandbox workload template lifecycle. Templates intentionally return
|
|
428
|
+
// generated proto messages because the resource owns portable workload fields
|
|
429
|
+
// plus driver-specific config that should not be lossy in the curated layer.
|
|
430
|
+
export class SandboxTemplateClient {
|
|
431
|
+
grpc;
|
|
432
|
+
raw;
|
|
433
|
+
transport;
|
|
434
|
+
constructor(transport, grpc = createClient(OpenShell, transport)) {
|
|
435
|
+
this.transport = transport;
|
|
436
|
+
this.grpc = grpc;
|
|
437
|
+
this.raw = this.grpc;
|
|
438
|
+
}
|
|
439
|
+
static async connect(options) {
|
|
440
|
+
return new SandboxTemplateClient(buildTransport(options));
|
|
441
|
+
}
|
|
442
|
+
async create(template, options) {
|
|
443
|
+
try {
|
|
444
|
+
const resp = await this.grpc.createSandboxTemplate({
|
|
445
|
+
workspaceScope: workspaceScope(options),
|
|
446
|
+
template,
|
|
447
|
+
});
|
|
448
|
+
return sandboxTemplate(resp.template);
|
|
449
|
+
}
|
|
450
|
+
catch (e) {
|
|
451
|
+
throw e instanceof SdkError ? e : fromConnect(e);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
async get(name, options) {
|
|
455
|
+
if (name.trim() === '')
|
|
456
|
+
throw new SdkError('invalid_config', 'template name is required');
|
|
457
|
+
try {
|
|
458
|
+
const resp = await this.grpc.getSandboxTemplate({
|
|
459
|
+
workspaceScope: workspaceScope(options),
|
|
460
|
+
name,
|
|
461
|
+
});
|
|
462
|
+
return sandboxTemplate(resp.template);
|
|
463
|
+
}
|
|
464
|
+
catch (e) {
|
|
465
|
+
throw e instanceof SdkError ? e : fromConnect(e);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
list(options) {
|
|
469
|
+
return new Pager(async (pageToken) => {
|
|
470
|
+
try {
|
|
471
|
+
const resp = await this.grpc.listSandboxTemplates({
|
|
472
|
+
pageSize: options?.pageSize ?? 0,
|
|
473
|
+
pageToken,
|
|
474
|
+
labelSelector: options?.labelSelector ?? '',
|
|
475
|
+
workspaceScope: listWorkspaceScope(options),
|
|
476
|
+
});
|
|
477
|
+
return { items: resp.templates, nextPageToken: resp.nextPageToken };
|
|
478
|
+
}
|
|
479
|
+
catch (e) {
|
|
480
|
+
throw fromConnect(e);
|
|
481
|
+
}
|
|
482
|
+
}, options?.pageToken ?? '');
|
|
483
|
+
}
|
|
484
|
+
/** List and collect every sandbox template in this scope. */
|
|
485
|
+
async listAll(options) {
|
|
486
|
+
return this.list(options).all();
|
|
487
|
+
}
|
|
488
|
+
async delete(name, options) {
|
|
489
|
+
if (name.trim() === '')
|
|
490
|
+
throw new SdkError('invalid_config', 'template name is required');
|
|
491
|
+
try {
|
|
492
|
+
const resp = await this.grpc.deleteSandboxTemplate({
|
|
493
|
+
workspaceScope: workspaceScope(options),
|
|
494
|
+
allowMissing: options?.allowMissing ?? false,
|
|
495
|
+
name,
|
|
496
|
+
});
|
|
497
|
+
return deletionResult(resp);
|
|
498
|
+
}
|
|
499
|
+
catch (e) {
|
|
500
|
+
throw fromConnect(e);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
248
504
|
// ---- sandbox client --------------------------------------------------------
|
|
249
505
|
// Sandbox lifecycle + exec. Usable standalone via `SandboxClient.connect()`,
|
|
250
506
|
// or reached as `client.sandbox` on an OpenShellClient, which shares one
|
|
@@ -292,42 +548,86 @@ export class SandboxClient {
|
|
|
292
548
|
if (spec.rawSpec)
|
|
293
549
|
Object.assign(specInit, spec.rawSpec);
|
|
294
550
|
const resp = await this.grpc.createSandbox({
|
|
551
|
+
workspaceScope: workspaceScope(spec),
|
|
295
552
|
name: spec.name ?? '',
|
|
296
553
|
labels: spec.labels ?? {},
|
|
297
554
|
spec: specInit,
|
|
555
|
+
serviceExposures: spec.serviceExposures?.map((exposure) => ({
|
|
556
|
+
service: exposure.service ?? '',
|
|
557
|
+
targetPort: exposure.targetPort,
|
|
558
|
+
})) ?? [],
|
|
298
559
|
});
|
|
299
|
-
return sandboxRef(resp.sandbox);
|
|
560
|
+
return sandboxRef(resp.sandbox, resp.serviceUrls);
|
|
300
561
|
}
|
|
301
562
|
catch (e) {
|
|
302
563
|
throw fromConnect(e);
|
|
303
564
|
}
|
|
304
565
|
}
|
|
305
|
-
async
|
|
566
|
+
async createFromTemplate(spec) {
|
|
567
|
+
if (spec.workloadTemplate.trim() === '')
|
|
568
|
+
throw new SdkError('invalid_config', 'workloadTemplate is required');
|
|
306
569
|
try {
|
|
307
|
-
const resp = await this.grpc.
|
|
308
|
-
|
|
570
|
+
const resp = await this.grpc.createSandbox({
|
|
571
|
+
workspaceScope: workspaceScope(spec),
|
|
572
|
+
name: spec.name ?? '',
|
|
573
|
+
labels: spec.labels ?? {},
|
|
574
|
+
spec: {
|
|
575
|
+
providers: spec.providers ?? [],
|
|
576
|
+
command: spec.command ?? [],
|
|
577
|
+
tty: spec.tty ?? false,
|
|
578
|
+
policy: spec.policy,
|
|
579
|
+
},
|
|
580
|
+
workloadTemplate: spec.workloadTemplate,
|
|
581
|
+
serviceExposures: spec.serviceExposures?.map((exposure) => ({
|
|
582
|
+
service: exposure.service ?? '',
|
|
583
|
+
targetPort: exposure.targetPort,
|
|
584
|
+
})) ?? [],
|
|
585
|
+
});
|
|
586
|
+
return sandboxRef(resp.sandbox, resp.serviceUrls);
|
|
309
587
|
}
|
|
310
588
|
catch (e) {
|
|
311
589
|
throw fromConnect(e);
|
|
312
590
|
}
|
|
313
591
|
}
|
|
314
|
-
async
|
|
592
|
+
async get(name, options) {
|
|
315
593
|
try {
|
|
316
|
-
const resp = await this.grpc.
|
|
317
|
-
|
|
318
|
-
offset: options?.offset ?? 0,
|
|
319
|
-
labelSelector: options?.labelSelector ?? '',
|
|
320
|
-
});
|
|
321
|
-
return resp.sandboxes.map((s) => sandboxRef(s));
|
|
594
|
+
const resp = await this.grpc.getSandbox({ ...namedTarget(name, options) }, requestCallOptions(options));
|
|
595
|
+
return sandboxRef(resp.sandbox);
|
|
322
596
|
}
|
|
323
597
|
catch (e) {
|
|
324
598
|
throw fromConnect(e);
|
|
325
599
|
}
|
|
326
600
|
}
|
|
327
|
-
|
|
601
|
+
list(options) {
|
|
602
|
+
return new Pager(async (pageToken) => {
|
|
603
|
+
try {
|
|
604
|
+
const resp = await this.grpc.listSandboxes({
|
|
605
|
+
pageSize: options?.pageSize ?? 0,
|
|
606
|
+
pageToken,
|
|
607
|
+
labelSelector: options?.labelSelector ?? '',
|
|
608
|
+
workspaceScope: listWorkspaceScope(options),
|
|
609
|
+
});
|
|
610
|
+
return {
|
|
611
|
+
items: resp.sandboxes.map((sandbox) => sandboxRef(sandbox)),
|
|
612
|
+
nextPageToken: resp.nextPageToken,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
catch (e) {
|
|
616
|
+
throw fromConnect(e);
|
|
617
|
+
}
|
|
618
|
+
}, options?.pageToken ?? '');
|
|
619
|
+
}
|
|
620
|
+
/** List and collect every sandbox in this scope. */
|
|
621
|
+
async listAll(options) {
|
|
622
|
+
return this.list(options).all();
|
|
623
|
+
}
|
|
624
|
+
async delete(name, options) {
|
|
328
625
|
try {
|
|
329
|
-
const resp = await this.grpc.deleteSandbox({
|
|
330
|
-
|
|
626
|
+
const resp = await this.grpc.deleteSandbox({
|
|
627
|
+
...namedTarget(name, options),
|
|
628
|
+
allowMissing: options?.allowMissing ?? false,
|
|
629
|
+
});
|
|
630
|
+
return deletionResult(resp);
|
|
331
631
|
}
|
|
332
632
|
catch (e) {
|
|
333
633
|
throw fromConnect(e);
|
|
@@ -346,14 +646,20 @@ export class SandboxClient {
|
|
|
346
646
|
if (Date.now() >= deadline)
|
|
347
647
|
throw new SdkError('connect', `timed out waiting for sandbox '${name}'`);
|
|
348
648
|
let ref;
|
|
649
|
+
const pollOptions = deadlineOptions(deadline - Date.now(), signal);
|
|
349
650
|
try {
|
|
350
|
-
ref = await this.get(name,
|
|
651
|
+
ref = await this.get(name, {
|
|
652
|
+
...pollOptions,
|
|
653
|
+
workspace: options?.workspace,
|
|
654
|
+
});
|
|
351
655
|
}
|
|
352
656
|
catch (e) {
|
|
353
|
-
throw mapWaitError(e, name, deadline, signal);
|
|
657
|
+
throw mapWaitError(e, name, deadline, signal, pollOptions.signal);
|
|
354
658
|
}
|
|
355
|
-
if (ref.phase === 'ready')
|
|
659
|
+
if (ref.phase === 'ready' || ref.phase === 'completed')
|
|
356
660
|
return ref;
|
|
661
|
+
if (ref.phase === 'stopped')
|
|
662
|
+
throw new SdkError('connect', `sandbox '${name}' stopped before becoming ready`);
|
|
357
663
|
if (ref.phase === 'error')
|
|
358
664
|
throw new SdkError('connect', `sandbox '${name}' entered error phase`);
|
|
359
665
|
if (Date.now() >= deadline)
|
|
@@ -362,8 +668,8 @@ export class SandboxClient {
|
|
|
362
668
|
delay = Math.min(delay * 2, 2000);
|
|
363
669
|
}
|
|
364
670
|
}
|
|
365
|
-
// Poll until the sandbox is gone
|
|
366
|
-
//
|
|
671
|
+
// Poll until the sandbox is gone, or its name resolves to a different ID when
|
|
672
|
+
// expectedSandboxId is supplied. Timeout and cancellation work as in waitReady.
|
|
367
673
|
async waitDeleted(name, timeoutSecs, options) {
|
|
368
674
|
const deadline = Date.now() + timeoutSecs * 1000;
|
|
369
675
|
const signal = options?.signal;
|
|
@@ -373,13 +679,16 @@ export class SandboxClient {
|
|
|
373
679
|
throw new SdkError('connect', `wait for sandbox '${name}' aborted`);
|
|
374
680
|
if (Date.now() >= deadline)
|
|
375
681
|
throw new SdkError('connect', `timed out waiting for sandbox '${name}' to delete`);
|
|
682
|
+
const pollOptions = deadlineOptions(deadline - Date.now(), signal);
|
|
376
683
|
try {
|
|
377
|
-
await this.get(name,
|
|
684
|
+
const ref = await this.get(name, { ...pollOptions, workspace: options?.workspace });
|
|
685
|
+
if (options?.expectedSandboxId !== undefined && ref.id !== options.expectedSandboxId)
|
|
686
|
+
return;
|
|
378
687
|
}
|
|
379
688
|
catch (e) {
|
|
380
689
|
if (e instanceof SdkError && e.code === 'not_found')
|
|
381
690
|
return;
|
|
382
|
-
throw mapWaitError(e, name, deadline, signal);
|
|
691
|
+
throw mapWaitError(e, name, deadline, signal, pollOptions.signal);
|
|
383
692
|
}
|
|
384
693
|
if (Date.now() >= deadline)
|
|
385
694
|
throw new SdkError('connect', `timed out waiting for sandbox '${name}' to delete`);
|
|
@@ -394,16 +703,20 @@ export class SandboxClient {
|
|
|
394
703
|
// path to reconstruct the buffered result.
|
|
395
704
|
async *execStream(name, command, options) {
|
|
396
705
|
try {
|
|
397
|
-
//
|
|
398
|
-
|
|
706
|
+
// Preserve the existing preflight so lookup failures surface before the stream starts.
|
|
707
|
+
await this.get(name, {
|
|
708
|
+
workspace: options?.workspace,
|
|
709
|
+
...(options?.signal ? { signal: options.signal } : {}),
|
|
710
|
+
});
|
|
399
711
|
const stream = this.grpc.execSandbox({
|
|
400
|
-
|
|
712
|
+
...sandboxTarget(name, options),
|
|
401
713
|
command,
|
|
402
714
|
workdir: options?.workdir ?? '',
|
|
403
715
|
environment: options?.environment ?? {},
|
|
404
|
-
|
|
716
|
+
executionTimeout: durationFromSeconds(options?.timeoutSecs ?? 0),
|
|
405
717
|
stdin: options?.stdin ? new Uint8Array(options.stdin) : new Uint8Array(),
|
|
406
718
|
tty: false,
|
|
719
|
+
noLoginShell: options?.noLoginShell ?? false,
|
|
407
720
|
}, { signal: options?.signal });
|
|
408
721
|
let sawExit = false;
|
|
409
722
|
for await (const event of stream) {
|
|
@@ -461,9 +774,8 @@ export class SandboxClient {
|
|
|
461
774
|
// `stdin`/`resize`. No terminal glue: raw mode, signal forwarding, and
|
|
462
775
|
// SIGWINCH stay with the caller.
|
|
463
776
|
async execInteractive(name, command, options) {
|
|
464
|
-
let sandboxId;
|
|
465
777
|
try {
|
|
466
|
-
|
|
778
|
+
await this.get(name, { workspace: options?.workspace, ...(options?.signal ? { signal: options.signal } : {}) });
|
|
467
779
|
}
|
|
468
780
|
catch (e) {
|
|
469
781
|
throw e instanceof SdkError ? e : fromConnect(e);
|
|
@@ -473,19 +785,33 @@ export class SandboxClient {
|
|
|
473
785
|
payload: {
|
|
474
786
|
case: 'start',
|
|
475
787
|
value: {
|
|
476
|
-
|
|
788
|
+
...sandboxTarget(name, options),
|
|
477
789
|
command,
|
|
478
790
|
workdir: options?.workdir ?? '',
|
|
479
791
|
environment: options?.environment ?? {},
|
|
480
|
-
|
|
792
|
+
executionTimeout: durationFromSeconds(options?.timeoutSecs ?? 0),
|
|
481
793
|
stdin: new Uint8Array(),
|
|
482
794
|
tty: options?.tty ?? true,
|
|
483
795
|
cols: options?.cols ?? 0,
|
|
484
796
|
rows: options?.rows ?? 0,
|
|
797
|
+
noLoginShell: options?.noLoginShell ?? false,
|
|
485
798
|
},
|
|
486
799
|
},
|
|
487
800
|
});
|
|
488
|
-
const
|
|
801
|
+
const controller = new AbortController();
|
|
802
|
+
const signal = options?.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
|
|
803
|
+
const grpc = this.grpc;
|
|
804
|
+
const queue = new ExecOutputQueue();
|
|
805
|
+
let inputClosed = false;
|
|
806
|
+
let exitCode;
|
|
807
|
+
const closeInput = () => {
|
|
808
|
+
inputClosed = true;
|
|
809
|
+
input.end();
|
|
810
|
+
};
|
|
811
|
+
const assertInputOpen = () => {
|
|
812
|
+
if (inputClosed || signal.aborted)
|
|
813
|
+
throw new SdkError('io', 'exec input is closed');
|
|
814
|
+
};
|
|
489
815
|
let resolveDone;
|
|
490
816
|
let rejectDone;
|
|
491
817
|
const done = new Promise((resolve, reject) => {
|
|
@@ -496,74 +822,118 @@ export class SandboxClient {
|
|
|
496
822
|
// keeps an unobserved rejection from surfacing as an unhandledRejection;
|
|
497
823
|
// real awaiters still receive it through their own handler.
|
|
498
824
|
void done.catch(() => { });
|
|
499
|
-
//
|
|
500
|
-
// when no exit was observed.
|
|
825
|
+
// The process exit and the terminal transport status are separate outcomes.
|
|
501
826
|
let settled = false;
|
|
502
827
|
const settleExit = (code) => {
|
|
503
828
|
if (settled)
|
|
504
829
|
return;
|
|
505
830
|
settled = true;
|
|
831
|
+
signal.removeEventListener('abort', onAbort);
|
|
506
832
|
resolveDone(code);
|
|
507
833
|
};
|
|
508
834
|
const settleError = (err) => {
|
|
509
835
|
if (settled)
|
|
510
836
|
return;
|
|
511
837
|
settled = true;
|
|
838
|
+
signal.removeEventListener('abort', onAbort);
|
|
512
839
|
rejectDone(err);
|
|
513
840
|
};
|
|
514
|
-
|
|
515
|
-
|
|
841
|
+
const onAbort = () => {
|
|
842
|
+
closeInput();
|
|
843
|
+
const error = new SdkError('canceled', 'exec cancelled');
|
|
844
|
+
queue.end(error, true);
|
|
845
|
+
settleError(error);
|
|
846
|
+
};
|
|
847
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
848
|
+
if (signal.aborted)
|
|
849
|
+
onAbort();
|
|
850
|
+
async function receive() {
|
|
516
851
|
try {
|
|
852
|
+
if (signal.aborted)
|
|
853
|
+
throw new SdkError('canceled', 'exec cancelled');
|
|
854
|
+
// Start and observe the transport immediately, independently of output
|
|
855
|
+
// consumption. Backpressure bounds the queue to 16 chunks of 64 KiB.
|
|
856
|
+
const stream = grpc.execSandboxInteractive(input, { signal });
|
|
517
857
|
for await (const event of stream) {
|
|
858
|
+
if (signal.aborted)
|
|
859
|
+
throw new SdkError('canceled', 'exec cancelled');
|
|
860
|
+
if (exitCode !== undefined) {
|
|
861
|
+
throw new SdkError('rpc', 'ExecSandboxInteractive received an event after exit');
|
|
862
|
+
}
|
|
518
863
|
switch (event.payload.case) {
|
|
519
864
|
case 'stdout':
|
|
520
|
-
yield {
|
|
521
|
-
stream: 'stdout',
|
|
522
|
-
data: Buffer.from(event.payload.value.data),
|
|
523
|
-
};
|
|
524
|
-
break;
|
|
525
865
|
case 'stderr':
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
866
|
+
for (let offset = 0; offset < event.payload.value.data.length; offset += 64 * 1024) {
|
|
867
|
+
await queue.push({
|
|
868
|
+
stream: event.payload.case,
|
|
869
|
+
data: Buffer.from(event.payload.value.data.subarray(offset, offset + 64 * 1024)),
|
|
870
|
+
});
|
|
871
|
+
}
|
|
530
872
|
break;
|
|
531
873
|
case 'exit':
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
// exit event abandons the generator at the yield, so anything
|
|
535
|
-
// after it would never run.
|
|
536
|
-
settleExit(event.payload.value.exitCode);
|
|
537
|
-
yield { type: 'exit', exitCode: event.payload.value.exitCode };
|
|
874
|
+
exitCode = event.payload.value.exitCode;
|
|
875
|
+
closeInput();
|
|
538
876
|
break;
|
|
877
|
+
default:
|
|
878
|
+
throw new SdkError('rpc', 'ExecSandboxInteractive received an empty or unknown event');
|
|
539
879
|
}
|
|
540
880
|
}
|
|
541
|
-
if (
|
|
881
|
+
if (exitCode === undefined) {
|
|
542
882
|
throw new SdkError('rpc', 'ExecSandboxInteractive stream ended without an exit event');
|
|
543
883
|
}
|
|
884
|
+
if (signal.aborted)
|
|
885
|
+
throw new SdkError('canceled', 'exec cancelled before completion');
|
|
886
|
+
// Delay the public exit event until trailers have been consumed. A
|
|
887
|
+
// caller can still break on exit without losing the terminal status.
|
|
888
|
+
settleExit(exitCode);
|
|
889
|
+
await queue.push({ type: 'exit', exitCode });
|
|
890
|
+
queue.end();
|
|
544
891
|
}
|
|
545
892
|
catch (e) {
|
|
546
893
|
const err = e instanceof SdkError ? e : fromConnect(e);
|
|
547
894
|
settleError(err);
|
|
548
|
-
|
|
895
|
+
queue.end(err);
|
|
896
|
+
}
|
|
897
|
+
finally {
|
|
898
|
+
closeInput();
|
|
899
|
+
controller.abort();
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
// receive catches transport failures even when nobody consumes output/done.
|
|
903
|
+
const receiving = receive();
|
|
904
|
+
async function* output() {
|
|
905
|
+
try {
|
|
906
|
+
yield* queue;
|
|
549
907
|
}
|
|
550
908
|
finally {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
909
|
+
const error = new SdkError('rpc', 'exec output abandoned before completion');
|
|
910
|
+
settleError(error);
|
|
911
|
+
queue.end(error, true);
|
|
912
|
+
closeInput();
|
|
913
|
+
controller.abort();
|
|
914
|
+
await receiving;
|
|
555
915
|
}
|
|
556
916
|
}
|
|
557
917
|
return {
|
|
558
918
|
output: output(),
|
|
559
919
|
write(data) {
|
|
920
|
+
assertInputOpen();
|
|
560
921
|
input.push({ payload: { case: 'stdin', value: new Uint8Array(data) } });
|
|
561
922
|
},
|
|
562
923
|
resize(cols, rows) {
|
|
924
|
+
assertInputOpen();
|
|
563
925
|
input.push({ payload: { case: 'resize', value: { cols, rows } } });
|
|
564
926
|
},
|
|
565
|
-
|
|
566
|
-
|
|
927
|
+
closeInput,
|
|
928
|
+
close: closeInput,
|
|
929
|
+
cancel() {
|
|
930
|
+
closeInput();
|
|
931
|
+
queue.end(new SdkError('canceled', 'exec cancelled'), true);
|
|
932
|
+
controller.abort();
|
|
933
|
+
settleError(new SdkError('canceled', 'exec cancelled'));
|
|
934
|
+
},
|
|
935
|
+
get exitCode() {
|
|
936
|
+
return exitCode;
|
|
567
937
|
},
|
|
568
938
|
done,
|
|
569
939
|
};
|
|
@@ -580,7 +950,7 @@ export class SandboxClient {
|
|
|
580
950
|
const localPort = opts.localPort ?? 0;
|
|
581
951
|
let sandboxId;
|
|
582
952
|
try {
|
|
583
|
-
const ref = await this.get(name, opts.signal ? { signal: opts.signal } :
|
|
953
|
+
const ref = await this.get(name, { workspace: opts.workspace, ...(opts.signal ? { signal: opts.signal } : {}) });
|
|
584
954
|
if (ref.phase !== 'ready') {
|
|
585
955
|
throw new SdkError('connect', `sandbox '${name}' is not ready (phase: ${ref.phase})`);
|
|
586
956
|
}
|
|
@@ -603,7 +973,7 @@ export class SandboxClient {
|
|
|
603
973
|
socket.on('error', () => { });
|
|
604
974
|
const controller = new AbortController();
|
|
605
975
|
controllers.add(controller);
|
|
606
|
-
const task = this.forwardConnection(socket, sandboxId, name, targetHost, targetPort, controller.signal)
|
|
976
|
+
const task = this.forwardConnection(socket, sandboxId, name, opts.workspace, targetHost, targetPort, controller.signal)
|
|
607
977
|
.catch((error) => {
|
|
608
978
|
if (!closing) {
|
|
609
979
|
try {
|
|
@@ -674,12 +1044,12 @@ export class SandboxClient {
|
|
|
674
1044
|
closed,
|
|
675
1045
|
};
|
|
676
1046
|
}
|
|
677
|
-
async forwardConnection(socket, sandboxId, name, targetHost, targetPort, signal) {
|
|
1047
|
+
async forwardConnection(socket, sandboxId, name, workspace, targetHost, targetPort, signal) {
|
|
678
1048
|
let token;
|
|
679
1049
|
const input = new Pushable();
|
|
680
1050
|
input.onDrain = () => socket.resume();
|
|
681
1051
|
try {
|
|
682
|
-
const session = await this.grpc.createSshSession({
|
|
1052
|
+
const session = await this.grpc.createSshSession({ ...sandboxTarget(name, { workspace }) }, { signal });
|
|
683
1053
|
// Defense-in-depth: the token feeds forwardTcp authorization, so hold it
|
|
684
1054
|
// to the same trust-boundary contract as createSshSession. A violation
|
|
685
1055
|
// tears down this one socket via the catch below.
|
|
@@ -689,7 +1059,7 @@ export class SandboxClient {
|
|
|
689
1059
|
payload: {
|
|
690
1060
|
case: 'init',
|
|
691
1061
|
value: {
|
|
692
|
-
|
|
1062
|
+
...sandboxTarget(name, { workspace }),
|
|
693
1063
|
serviceId: `service-forward:${name}:${targetHost}:${targetPort}`,
|
|
694
1064
|
target: {
|
|
695
1065
|
case: 'tcp',
|
|
@@ -743,7 +1113,7 @@ export class SandboxClient {
|
|
|
743
1113
|
input.end();
|
|
744
1114
|
if (token !== undefined) {
|
|
745
1115
|
try {
|
|
746
|
-
await this.grpc.revokeSshSession({ token }, { signal });
|
|
1116
|
+
await this.grpc.revokeSshSession({ token, allowMissing: true }, { signal });
|
|
747
1117
|
}
|
|
748
1118
|
catch {
|
|
749
1119
|
// Best-effort revoke; the token expires on its own regardless.
|
|
@@ -753,10 +1123,10 @@ export class SandboxClient {
|
|
|
753
1123
|
}
|
|
754
1124
|
// Mint a short-lived SSH session token for the sandbox — the input side of
|
|
755
1125
|
// ssh-config / ProxyCommand and forwardTcp authorization.
|
|
756
|
-
async createSshSession(name) {
|
|
1126
|
+
async createSshSession(name, options) {
|
|
757
1127
|
try {
|
|
758
|
-
const sandbox = await this.get(name);
|
|
759
|
-
const resp = await this.grpc.createSshSession({
|
|
1128
|
+
const sandbox = await this.get(name, options);
|
|
1129
|
+
const resp = await this.grpc.createSshSession({ ...sandboxTarget(name, options) });
|
|
760
1130
|
// Reject any response outside the proto trust-boundary contract before
|
|
761
1131
|
// handing these values to the caller (they feed OpenSSH ProxyCommand).
|
|
762
1132
|
validateSshResponse(resp, sandbox.id);
|
|
@@ -767,17 +1137,17 @@ export class SandboxClient {
|
|
|
767
1137
|
gatewayPort: resp.gatewayPort,
|
|
768
1138
|
gatewayScheme: resp.gatewayScheme,
|
|
769
1139
|
...(resp.hostKeyFingerprint ? { hostKeyFingerprint: resp.hostKeyFingerprint } : {}),
|
|
770
|
-
...(resp.
|
|
1140
|
+
...(timestampMillis(resp.expirationTime) ? { expiresAtMs: timestampMillis(resp.expirationTime) } : {}),
|
|
771
1141
|
};
|
|
772
1142
|
}
|
|
773
1143
|
catch (e) {
|
|
774
1144
|
throw e instanceof SdkError ? e : fromConnect(e);
|
|
775
1145
|
}
|
|
776
1146
|
}
|
|
777
|
-
async revokeSshSession(token) {
|
|
1147
|
+
async revokeSshSession(token, options) {
|
|
778
1148
|
try {
|
|
779
|
-
const resp = await this.grpc.revokeSshSession({ token });
|
|
780
|
-
return resp
|
|
1149
|
+
const resp = await this.grpc.revokeSshSession({ token, allowMissing: options?.allowMissing ?? false });
|
|
1150
|
+
return deletionResult(resp);
|
|
781
1151
|
}
|
|
782
1152
|
catch (e) {
|
|
783
1153
|
throw fromConnect(e);
|
|
@@ -786,8 +1156,8 @@ export class SandboxClient {
|
|
|
786
1156
|
async attachProvider(name, provider, options) {
|
|
787
1157
|
try {
|
|
788
1158
|
const resp = await this.grpc.attachSandboxProvider({
|
|
789
|
-
|
|
790
|
-
|
|
1159
|
+
...sandboxTarget(name, options),
|
|
1160
|
+
provider,
|
|
791
1161
|
expectedResourceVersion: versionPin(options?.expectedResourceVersion),
|
|
792
1162
|
});
|
|
793
1163
|
return { sandbox: sandboxRef(resp.sandbox), changed: resp.attached };
|
|
@@ -799,8 +1169,8 @@ export class SandboxClient {
|
|
|
799
1169
|
async detachProvider(name, provider, options) {
|
|
800
1170
|
try {
|
|
801
1171
|
const resp = await this.grpc.detachSandboxProvider({
|
|
802
|
-
|
|
803
|
-
|
|
1172
|
+
...sandboxTarget(name, options),
|
|
1173
|
+
provider,
|
|
804
1174
|
expectedResourceVersion: versionPin(options?.expectedResourceVersion),
|
|
805
1175
|
});
|
|
806
1176
|
return { sandbox: sandboxRef(resp.sandbox), changed: resp.detached };
|
|
@@ -809,19 +1179,32 @@ export class SandboxClient {
|
|
|
809
1179
|
throw fromConnect(e);
|
|
810
1180
|
}
|
|
811
1181
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
1182
|
+
listProviders(name, options) {
|
|
1183
|
+
return new Pager(async (pageToken) => {
|
|
1184
|
+
try {
|
|
1185
|
+
const resp = await this.grpc.listSandboxProviders({
|
|
1186
|
+
...sandboxTarget(name, options),
|
|
1187
|
+
pageSize: options?.pageSize ?? 0,
|
|
1188
|
+
pageToken,
|
|
1189
|
+
});
|
|
1190
|
+
return {
|
|
1191
|
+
items: resp.providers.map((provider) => providerRef(provider)),
|
|
1192
|
+
nextPageToken: resp.nextPageToken,
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
catch (e) {
|
|
1196
|
+
throw fromConnect(e);
|
|
1197
|
+
}
|
|
1198
|
+
}, options?.pageToken ?? '');
|
|
1199
|
+
}
|
|
1200
|
+
/** List and collect every provider attached to this sandbox. */
|
|
1201
|
+
async listAllProviders(name, options) {
|
|
1202
|
+
return this.listProviders(name, options).all();
|
|
820
1203
|
}
|
|
821
|
-
async getConfig(name,
|
|
1204
|
+
async getConfig(name, options) {
|
|
822
1205
|
try {
|
|
823
|
-
|
|
824
|
-
const resp = await this.grpc.getSandboxConfig({
|
|
1206
|
+
await this.get(name, options);
|
|
1207
|
+
const resp = await this.grpc.getSandboxConfig({ ...namedTarget(name, options) }, requestCallOptions(options));
|
|
825
1208
|
return sandboxConfig(resp);
|
|
826
1209
|
}
|
|
827
1210
|
catch (e) {
|
|
@@ -835,14 +1218,14 @@ export class SandboxClient {
|
|
|
835
1218
|
async setPolicy(name, policy, options) {
|
|
836
1219
|
try {
|
|
837
1220
|
const resp = await this.grpc.updateConfig({
|
|
838
|
-
name,
|
|
1221
|
+
...sandboxTarget(name, options),
|
|
839
1222
|
policy,
|
|
840
1223
|
global: false,
|
|
841
1224
|
expectedResourceVersion: versionPin(options?.expectedResourceVersion),
|
|
842
1225
|
});
|
|
843
1226
|
const result = updateConfigResult(resp);
|
|
844
1227
|
if (options?.wait)
|
|
845
|
-
await this.waitForPolicyHash(name, result.policyHash, options.waitTimeoutSecs);
|
|
1228
|
+
await this.waitForPolicyHash(name, result.policyHash, options.waitTimeoutSecs, options.workspace);
|
|
846
1229
|
return result;
|
|
847
1230
|
}
|
|
848
1231
|
catch (e) {
|
|
@@ -851,10 +1234,10 @@ export class SandboxClient {
|
|
|
851
1234
|
}
|
|
852
1235
|
// Upsert a single sandbox-scoped setting. Sandbox-scoped deletes are rejected
|
|
853
1236
|
// by the gateway, so there is no sandbox-scoped delete on this surface.
|
|
854
|
-
async setSetting(name, key, value) {
|
|
1237
|
+
async setSetting(name, key, value, options) {
|
|
855
1238
|
try {
|
|
856
1239
|
const resp = await this.grpc.updateConfig({
|
|
857
|
-
name,
|
|
1240
|
+
...sandboxTarget(name, options),
|
|
858
1241
|
settingKey: key,
|
|
859
1242
|
settingValue: value,
|
|
860
1243
|
global: false,
|
|
@@ -868,16 +1251,17 @@ export class SandboxClient {
|
|
|
868
1251
|
// Poll getConfig until the applied policy hash is observed. Each poll RPC is
|
|
869
1252
|
// bounded by the remaining deadline (deadlineOptions), so a stalled getConfig
|
|
870
1253
|
// cannot make the returned promise outlive timeoutSecs.
|
|
871
|
-
async waitForPolicyHash(name, policyHash, timeoutSecs = 60) {
|
|
1254
|
+
async waitForPolicyHash(name, policyHash, timeoutSecs = 60, workspace) {
|
|
872
1255
|
const deadline = Date.now() + timeoutSecs * 1000;
|
|
873
1256
|
let delay = 100;
|
|
874
1257
|
for (;;) {
|
|
875
1258
|
let config;
|
|
1259
|
+
const pollOptions = deadlineOptions(deadline - Date.now());
|
|
876
1260
|
try {
|
|
877
|
-
config = await this.getConfig(name,
|
|
1261
|
+
config = await this.getConfig(name, { ...pollOptions, workspace });
|
|
878
1262
|
}
|
|
879
1263
|
catch (e) {
|
|
880
|
-
if (Date.now() >= deadline) {
|
|
1264
|
+
if (pollOptions.signal?.aborted || Date.now() >= deadline) {
|
|
881
1265
|
throw new SdkError('connect', `timed out waiting for policy '${policyHash}' on sandbox '${name}'`);
|
|
882
1266
|
}
|
|
883
1267
|
throw e instanceof SdkError ? e : fromConnect(e);
|
|
@@ -896,6 +1280,8 @@ export class SandboxClient {
|
|
|
896
1280
|
export class OpenShellClient {
|
|
897
1281
|
/** Sandbox lifecycle + exec: create/get/list/delete, waitReady/waitDeleted, exec. */
|
|
898
1282
|
sandbox;
|
|
1283
|
+
/** Reusable sandbox workload template lifecycle. */
|
|
1284
|
+
sandboxTemplates;
|
|
899
1285
|
/**
|
|
900
1286
|
* Advanced escape hatch: a generated client for every gateway RPC, including
|
|
901
1287
|
* surface the curated sub-clients do not wrap yet (gateway config, provider
|
|
@@ -912,6 +1298,7 @@ export class OpenShellClient {
|
|
|
912
1298
|
this.grpc = createClient(OpenShell, transport);
|
|
913
1299
|
this.raw = this.grpc;
|
|
914
1300
|
this.sandbox = new SandboxClient(transport, this.grpc);
|
|
1301
|
+
this.sandboxTemplates = new SandboxTemplateClient(transport, this.grpc);
|
|
915
1302
|
}
|
|
916
1303
|
/**
|
|
917
1304
|
* Constructs a lazy Connect client. No network request is made until the
|