@folsom/fuse 0.37.1 → 0.42.0
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/index.d.ts +868 -64
- package/dist/index.js +675 -57
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,7 +6,9 @@ var ErrorCode = {
|
|
|
6
6
|
InvalidArgument: "invalid_argument",
|
|
7
7
|
Unavailable: "unavailable",
|
|
8
8
|
Internal: "internal",
|
|
9
|
-
Unauthorized: "unauthorized"
|
|
9
|
+
Unauthorized: "unauthorized",
|
|
10
|
+
Forbidden: "forbidden",
|
|
11
|
+
Locked: "locked"
|
|
10
12
|
};
|
|
11
13
|
var STATUS_TEXT = {
|
|
12
14
|
400: "bad request",
|
|
@@ -14,6 +16,7 @@ var STATUS_TEXT = {
|
|
|
14
16
|
403: "forbidden",
|
|
15
17
|
404: "not found",
|
|
16
18
|
409: "conflict",
|
|
19
|
+
423: "locked",
|
|
17
20
|
500: "internal server error",
|
|
18
21
|
502: "bad gateway",
|
|
19
22
|
503: "service unavailable"
|
|
@@ -47,6 +50,44 @@ var FuseApiError = class _FuseApiError extends Error {
|
|
|
47
50
|
Object.setPrototypeOf(this, _FuseApiError.prototype);
|
|
48
51
|
}
|
|
49
52
|
};
|
|
53
|
+
var ForbiddenReason = {
|
|
54
|
+
MissingScope: "missing_scope",
|
|
55
|
+
EnvScope: "env_scope",
|
|
56
|
+
SelfApproval: "self_approval",
|
|
57
|
+
ActorRequired: "actor_required",
|
|
58
|
+
ActorMismatch: "actor_mismatch",
|
|
59
|
+
ActorNotAllowed: "actor_not_allowed",
|
|
60
|
+
MintEscalation: "mint_escalation",
|
|
61
|
+
/** A hitl deny gate (or the exec allowlist in deny mode) refused the
|
|
62
|
+
* action. The message is the gate's reason; details.gate names the gate. */
|
|
63
|
+
HitlDenied: "hitl_denied"
|
|
64
|
+
};
|
|
65
|
+
var ForbiddenError = class _ForbiddenError extends FuseApiError {
|
|
66
|
+
reason;
|
|
67
|
+
scope;
|
|
68
|
+
constructor(init) {
|
|
69
|
+
super(init);
|
|
70
|
+
this.name = "ForbiddenError";
|
|
71
|
+
this.reason = init.details?.reason ?? "";
|
|
72
|
+
this.scope = init.details?.scope;
|
|
73
|
+
Object.setPrototypeOf(this, _ForbiddenError.prototype);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
var LockedError = class _LockedError extends FuseApiError {
|
|
77
|
+
interventionId;
|
|
78
|
+
/** "takeover" or "stall". */
|
|
79
|
+
kind;
|
|
80
|
+
constructor(init) {
|
|
81
|
+
super(init);
|
|
82
|
+
this.name = "LockedError";
|
|
83
|
+
this.interventionId = init.details?.intervention_id ?? "";
|
|
84
|
+
this.kind = init.details?.kind ?? "";
|
|
85
|
+
Object.setPrototypeOf(this, _LockedError.prototype);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
function isLocked(err) {
|
|
89
|
+
return err instanceof LockedError;
|
|
90
|
+
}
|
|
50
91
|
var FuseError = class _FuseError extends Error {
|
|
51
92
|
constructor(message, options) {
|
|
52
93
|
super(message, options);
|
|
@@ -54,6 +95,39 @@ var FuseError = class _FuseError extends Error {
|
|
|
54
95
|
Object.setPrototypeOf(this, _FuseError.prototype);
|
|
55
96
|
}
|
|
56
97
|
};
|
|
98
|
+
var PendingInterventionError = class _PendingInterventionError extends Error {
|
|
99
|
+
intervention;
|
|
100
|
+
constructor(intervention) {
|
|
101
|
+
super(
|
|
102
|
+
`held by hitl gate ${intervention.gate ?? ""}: waiting on intervention ${intervention.id}`
|
|
103
|
+
);
|
|
104
|
+
this.name = "PendingInterventionError";
|
|
105
|
+
this.intervention = intervention;
|
|
106
|
+
Object.setPrototypeOf(this, _PendingInterventionError.prototype);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
var InterventionRejectedError = class _InterventionRejectedError extends Error {
|
|
110
|
+
intervention;
|
|
111
|
+
constructor(intervention) {
|
|
112
|
+
super(
|
|
113
|
+
`intervention ${intervention.id} ${intervention.state}: ${intervention.decision?.reason ?? ""}`
|
|
114
|
+
);
|
|
115
|
+
this.name = "InterventionRejectedError";
|
|
116
|
+
this.intervention = intervention;
|
|
117
|
+
Object.setPrototypeOf(this, _InterventionRejectedError.prototype);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
var HeldActionError = class _HeldActionError extends Error {
|
|
121
|
+
intervention;
|
|
122
|
+
constructor(intervention) {
|
|
123
|
+
super(
|
|
124
|
+
`approved action of intervention ${intervention.id} failed: ${intervention.result?.error ?? ""}`
|
|
125
|
+
);
|
|
126
|
+
this.name = "HeldActionError";
|
|
127
|
+
this.intervention = intervention;
|
|
128
|
+
Object.setPrototypeOf(this, _HeldActionError.prototype);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
57
131
|
function isFuseApiError(err) {
|
|
58
132
|
return err instanceof FuseApiError;
|
|
59
133
|
}
|
|
@@ -69,6 +143,12 @@ function isConflict(err) {
|
|
|
69
143
|
function isUnauthorized(err) {
|
|
70
144
|
return hasCode(err, ErrorCode.Unauthorized);
|
|
71
145
|
}
|
|
146
|
+
function isForbidden(err) {
|
|
147
|
+
return err instanceof ForbiddenError;
|
|
148
|
+
}
|
|
149
|
+
function isHitlDenied(err) {
|
|
150
|
+
return isForbidden(err) && err.reason === ForbiddenReason.HitlDenied;
|
|
151
|
+
}
|
|
72
152
|
function isInvalidArgument(err) {
|
|
73
153
|
return hasCode(err, ErrorCode.InvalidArgument);
|
|
74
154
|
}
|
|
@@ -81,14 +161,17 @@ function parseApiError(status, requestId, body) {
|
|
|
81
161
|
const env = JSON.parse(body);
|
|
82
162
|
const e = env?.error;
|
|
83
163
|
if (e && (e.code || e.message)) {
|
|
84
|
-
|
|
164
|
+
const init = {
|
|
85
165
|
status,
|
|
86
166
|
code: e.code,
|
|
87
167
|
message: e.message,
|
|
88
168
|
details: e.details,
|
|
89
169
|
requestId,
|
|
90
170
|
body
|
|
91
|
-
}
|
|
171
|
+
};
|
|
172
|
+
if (e.code === ErrorCode.Forbidden) return new ForbiddenError(init);
|
|
173
|
+
if (e.code === ErrorCode.Locked) return new LockedError(init);
|
|
174
|
+
return new FuseApiError(init);
|
|
92
175
|
}
|
|
93
176
|
} catch {
|
|
94
177
|
}
|
|
@@ -112,6 +195,8 @@ async function errorFromResponse(res) {
|
|
|
112
195
|
|
|
113
196
|
// src/transport.ts
|
|
114
197
|
var REQUEST_ID_HEADER2 = "X-Request-ID";
|
|
198
|
+
var ACTOR_HEADER = "Fuse-Actor";
|
|
199
|
+
var ACTOR_NAME_HEADER = "Fuse-Actor-Name";
|
|
115
200
|
function combineSignals(signals) {
|
|
116
201
|
const active = signals.filter((s) => s != null);
|
|
117
202
|
if (active.length === 0) return { cleanup: () => {
|
|
@@ -146,6 +231,10 @@ var Transport = class {
|
|
|
146
231
|
requestId;
|
|
147
232
|
timeoutMs;
|
|
148
233
|
defaultHeaders;
|
|
234
|
+
actor;
|
|
235
|
+
/** The client's hitl wait options. */
|
|
236
|
+
hitlNoWait;
|
|
237
|
+
onHeld;
|
|
149
238
|
constructor(cfg) {
|
|
150
239
|
if (!cfg.baseUrl) {
|
|
151
240
|
throw new FuseError("base url is required");
|
|
@@ -162,6 +251,9 @@ var Transport = class {
|
|
|
162
251
|
this.requestId = cfg.requestId;
|
|
163
252
|
this.timeoutMs = cfg.timeoutMs;
|
|
164
253
|
this.defaultHeaders = cfg.headers;
|
|
254
|
+
this.actor = cfg.actor;
|
|
255
|
+
this.hitlNoWait = cfg.hitlNoWait ?? false;
|
|
256
|
+
this.onHeld = cfg.onHeld;
|
|
165
257
|
const provided = cfg.fetch;
|
|
166
258
|
if (provided) {
|
|
167
259
|
this.fetchImpl = provided;
|
|
@@ -184,6 +276,53 @@ var Transport = class {
|
|
|
184
276
|
cleanup();
|
|
185
277
|
}
|
|
186
278
|
}
|
|
279
|
+
/**
|
|
280
|
+
* Perform a call a hitl gate may hold. A 202 answers with the held
|
|
281
|
+
* intervention; anything else 2xx with the action's own (possibly empty)
|
|
282
|
+
* body.
|
|
283
|
+
*/
|
|
284
|
+
async gated(method, path, spec = {}) {
|
|
285
|
+
const { signal: combined, cleanup } = this.withTimeout(spec.signal);
|
|
286
|
+
try {
|
|
287
|
+
const res = await this.execute(method, path, spec, combined);
|
|
288
|
+
if (!res.ok) throw await errorFromResponse(res);
|
|
289
|
+
if (res.status === 202) return { held: await this.decodeHeld(res) };
|
|
290
|
+
return { value: await this.decodeJson(res) };
|
|
291
|
+
} finally {
|
|
292
|
+
cleanup();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/** Like noContent for a call a hitl gate may hold: it resolves to the held
|
|
296
|
+
* intervention of a 202, or undefined when the action itself answered. */
|
|
297
|
+
async gatedNoContent(method, path, spec = {}) {
|
|
298
|
+
const { signal: combined, cleanup } = this.withTimeout(spec.signal);
|
|
299
|
+
try {
|
|
300
|
+
const res = await this.execute(method, path, spec, combined);
|
|
301
|
+
if (!res.ok) throw await errorFromResponse(res);
|
|
302
|
+
if (res.status === 202) return await this.decodeHeld(res);
|
|
303
|
+
await res.body?.cancel().catch(() => {
|
|
304
|
+
});
|
|
305
|
+
return void 0;
|
|
306
|
+
} finally {
|
|
307
|
+
cleanup();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/** Perform a request and return the response body as text. */
|
|
311
|
+
async text(method, path, spec = {}) {
|
|
312
|
+
const { signal: combined, cleanup } = this.withTimeout(spec.signal);
|
|
313
|
+
try {
|
|
314
|
+
const res = await this.execute(method, path, spec, combined);
|
|
315
|
+
if (!res.ok) throw await errorFromResponse(res);
|
|
316
|
+
return await res.text();
|
|
317
|
+
} finally {
|
|
318
|
+
cleanup();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async decodeHeld(res) {
|
|
322
|
+
const body = await this.decodeJson(res);
|
|
323
|
+
if (!body?.intervention?.id) throw new FuseError("decode held response");
|
|
324
|
+
return body.intervention;
|
|
325
|
+
}
|
|
187
326
|
/** Perform a request that returns no body (e.g. 204 actions). */
|
|
188
327
|
async noContent(method, path, spec = {}) {
|
|
189
328
|
const { signal: combined, cleanup } = this.withTimeout(spec.signal);
|
|
@@ -246,6 +385,10 @@ var Transport = class {
|
|
|
246
385
|
if (id) headers[REQUEST_ID_HEADER2] = id;
|
|
247
386
|
}
|
|
248
387
|
if (accept) headers["Accept"] = accept;
|
|
388
|
+
if (this.actor?.id) {
|
|
389
|
+
headers[ACTOR_HEADER] = this.actor.id;
|
|
390
|
+
if (this.actor.name) headers[ACTOR_NAME_HEADER] = this.actor.name;
|
|
391
|
+
}
|
|
249
392
|
return headers;
|
|
250
393
|
}
|
|
251
394
|
async decodeJson(res) {
|
|
@@ -266,20 +409,21 @@ var Transport = class {
|
|
|
266
409
|
}
|
|
267
410
|
};
|
|
268
411
|
|
|
269
|
-
// src/validate.ts
|
|
270
|
-
function requireArg(value, name) {
|
|
271
|
-
if (!value) {
|
|
272
|
-
throw new FuseError(`${name} is required`);
|
|
273
|
-
}
|
|
274
|
-
return value;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
412
|
// src/types.ts
|
|
413
|
+
var HitlScope = {
|
|
414
|
+
Read: "hitl:read",
|
|
415
|
+
Decide: "hitl:decide",
|
|
416
|
+
Secret: "hitl:secret",
|
|
417
|
+
Takeover: "hitl:takeover",
|
|
418
|
+
ActAs: "hitl:act-as"
|
|
419
|
+
};
|
|
278
420
|
var State = {
|
|
279
421
|
Provisioning: "provisioning",
|
|
280
422
|
Running: "running",
|
|
281
423
|
Draining: "draining",
|
|
282
424
|
Destroying: "destroying",
|
|
425
|
+
/** A destroy that ran out of retries. The environment may still exist on its host; destroy it again to retry. */
|
|
426
|
+
DestroyFailed: "destroy_failed",
|
|
283
427
|
Destroyed: "destroyed",
|
|
284
428
|
Failed: "failed"
|
|
285
429
|
};
|
|
@@ -296,6 +440,90 @@ var HealthState = {
|
|
|
296
440
|
* ended. */
|
|
297
441
|
Failing: "failing"
|
|
298
442
|
};
|
|
443
|
+
var AuditEvent = {
|
|
444
|
+
Raised: "raised",
|
|
445
|
+
Decided: "decided",
|
|
446
|
+
Expired: "expired",
|
|
447
|
+
Cancelled: "cancelled",
|
|
448
|
+
InviteMinted: "invite_minted",
|
|
449
|
+
InviteUsed: "invite_used",
|
|
450
|
+
GrantCreated: "grant_created",
|
|
451
|
+
GrantUsed: "grant_used",
|
|
452
|
+
SecretAnswered: "secret_answered",
|
|
453
|
+
TakeoverStarted: "takeover_started",
|
|
454
|
+
TakeoverEnded: "takeover_ended",
|
|
455
|
+
TakeoverSession: "takeover_session"
|
|
456
|
+
};
|
|
457
|
+
var HitlKind = {
|
|
458
|
+
Ask: "ask",
|
|
459
|
+
Exec: "exec",
|
|
460
|
+
Lifecycle: "lifecycle",
|
|
461
|
+
Computer: "computer",
|
|
462
|
+
Review: "review",
|
|
463
|
+
Secret: "secret",
|
|
464
|
+
Takeover: "takeover",
|
|
465
|
+
Stall: "stall",
|
|
466
|
+
Expiry: "expiry"
|
|
467
|
+
};
|
|
468
|
+
var GateMode = {
|
|
469
|
+
Ask: "ask",
|
|
470
|
+
Notify: "notify",
|
|
471
|
+
Deny: "deny"
|
|
472
|
+
};
|
|
473
|
+
var DeciderVerdict = {
|
|
474
|
+
Allow: "allow",
|
|
475
|
+
Deny: "deny",
|
|
476
|
+
Ask: "ask"
|
|
477
|
+
};
|
|
478
|
+
var ACTOR_SOURCE_DECIDER = "decider";
|
|
479
|
+
var ReviewFormat = {
|
|
480
|
+
Text: "text",
|
|
481
|
+
Diff: "diff"
|
|
482
|
+
};
|
|
483
|
+
var TakeoverVia = {
|
|
484
|
+
Terminal: "terminal",
|
|
485
|
+
Desktop: "desktop"
|
|
486
|
+
};
|
|
487
|
+
var LifecycleAction = {
|
|
488
|
+
Destroy: "destroy",
|
|
489
|
+
Fork: "fork",
|
|
490
|
+
Migrate: "migrate",
|
|
491
|
+
Drain: "drain",
|
|
492
|
+
Snapshot: "snapshot",
|
|
493
|
+
RotateToken: "rotate-token"
|
|
494
|
+
};
|
|
495
|
+
var ALLOWLIST_GATE = "exec.allowlist";
|
|
496
|
+
var InterventionState = {
|
|
497
|
+
Pending: "pending",
|
|
498
|
+
Approved: "approved",
|
|
499
|
+
Rejected: "rejected",
|
|
500
|
+
Expired: "expired",
|
|
501
|
+
Cancelled: "cancelled"
|
|
502
|
+
};
|
|
503
|
+
var InterventionOutcome = {
|
|
504
|
+
Approve: "approve",
|
|
505
|
+
Reject: "reject"
|
|
506
|
+
};
|
|
507
|
+
function isInterventionTerminal(i) {
|
|
508
|
+
return !!i && !!i.state && i.state !== InterventionState.Pending;
|
|
509
|
+
}
|
|
510
|
+
function isInterventionApproved(i) {
|
|
511
|
+
return i?.decision?.outcome === InterventionOutcome.Approve;
|
|
512
|
+
}
|
|
513
|
+
function isInterventionSettled(i) {
|
|
514
|
+
if (!i || !isInterventionTerminal(i)) return false;
|
|
515
|
+
if (i.decision?.decided_by?.source === ACTOR_SOURCE_DECIDER) return true;
|
|
516
|
+
const runs = i.mode === GateMode.Ask && (!!i.payload?.exec || !!i.payload?.lifecycle || !!i.payload?.computer);
|
|
517
|
+
return !runs || !isInterventionApproved(i) || !!i.result;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// src/validate.ts
|
|
521
|
+
function requireArg(value, name) {
|
|
522
|
+
if (!value) {
|
|
523
|
+
throw new FuseError(`${name} is required`);
|
|
524
|
+
}
|
|
525
|
+
return value;
|
|
526
|
+
}
|
|
299
527
|
|
|
300
528
|
// src/events.ts
|
|
301
529
|
var DATA_PREFIX = "data:";
|
|
@@ -365,8 +593,182 @@ function parseEventStream(res, signal) {
|
|
|
365
593
|
};
|
|
366
594
|
}
|
|
367
595
|
|
|
368
|
-
// src/
|
|
596
|
+
// src/interventions.ts
|
|
369
597
|
var MAX_PAGE_LIMIT = 200;
|
|
598
|
+
var WAIT_POLL = "30s";
|
|
599
|
+
var InterventionsService = class {
|
|
600
|
+
constructor(t) {
|
|
601
|
+
this.t = t;
|
|
602
|
+
}
|
|
603
|
+
t;
|
|
604
|
+
/** List every intervention matching options, oldest first, walking every
|
|
605
|
+
* page. */
|
|
606
|
+
async list(options = {}, opts = {}) {
|
|
607
|
+
const out = [];
|
|
608
|
+
let cursor = options.cursor;
|
|
609
|
+
for (; ; ) {
|
|
610
|
+
const page = await this.listPage(
|
|
611
|
+
{ ...options, limit: MAX_PAGE_LIMIT, cursor },
|
|
612
|
+
opts
|
|
613
|
+
);
|
|
614
|
+
out.push(...page.interventions);
|
|
615
|
+
if (!page.nextCursor) break;
|
|
616
|
+
cursor = page.nextCursor;
|
|
617
|
+
}
|
|
618
|
+
return out;
|
|
619
|
+
}
|
|
620
|
+
/** List one page of interventions matching options. */
|
|
621
|
+
async listPage(options = {}, opts = {}) {
|
|
622
|
+
const out = await this.t.json("GET", "/v1/interventions", {
|
|
623
|
+
query: {
|
|
624
|
+
env: options.envId,
|
|
625
|
+
state: options.state,
|
|
626
|
+
actor: options.actor,
|
|
627
|
+
limit: options.limit ? String(options.limit) : void 0,
|
|
628
|
+
cursor: options.cursor
|
|
629
|
+
},
|
|
630
|
+
signal: opts.signal
|
|
631
|
+
});
|
|
632
|
+
return { interventions: out.interventions ?? [], nextCursor: out.next_cursor };
|
|
633
|
+
}
|
|
634
|
+
/** List every hitl audit row matching options, oldest first, walking every
|
|
635
|
+
* page. */
|
|
636
|
+
async audit(options = {}, opts = {}) {
|
|
637
|
+
const out = [];
|
|
638
|
+
let cursor = options.cursor;
|
|
639
|
+
for (; ; ) {
|
|
640
|
+
const page = await this.auditPage(
|
|
641
|
+
{ ...options, limit: MAX_PAGE_LIMIT, cursor },
|
|
642
|
+
opts
|
|
643
|
+
);
|
|
644
|
+
out.push(...page.entries);
|
|
645
|
+
if (!page.nextCursor) break;
|
|
646
|
+
cursor = page.nextCursor;
|
|
647
|
+
}
|
|
648
|
+
return out;
|
|
649
|
+
}
|
|
650
|
+
/** List one page of the hitl audit log matching options. */
|
|
651
|
+
async auditPage(options = {}, opts = {}) {
|
|
652
|
+
const out = await this.t.json("GET", "/v1/hitl/audit", {
|
|
653
|
+
query: {
|
|
654
|
+
env: options.envId,
|
|
655
|
+
actor: options.actor,
|
|
656
|
+
since: options.since,
|
|
657
|
+
limit: options.limit ? String(options.limit) : void 0,
|
|
658
|
+
cursor: options.cursor
|
|
659
|
+
},
|
|
660
|
+
signal: opts.signal
|
|
661
|
+
});
|
|
662
|
+
return { entries: out.entries ?? [], nextCursor: out.next_cursor };
|
|
663
|
+
}
|
|
664
|
+
/** Fetch a takeover session's recording (an AuditEntry's recording_id):
|
|
665
|
+
* asciinema v2 cast text for a terminal session, json (an input-action log
|
|
666
|
+
* plus screenshots) for a desktop one. Needs the hitl:read scope. */
|
|
667
|
+
async getRecording(recordingId, opts = {}) {
|
|
668
|
+
requireArg(recordingId, "recording id");
|
|
669
|
+
return this.t.text("GET", `/v1/hitl/recordings/${encodeURIComponent(recordingId)}`, {
|
|
670
|
+
signal: opts.signal
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
/** Fetch one intervention without waiting. */
|
|
674
|
+
async get(id, opts = {}) {
|
|
675
|
+
return getIntervention(this.t, id, void 0, opts);
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Block until the intervention settles: resolved, and for an approved gated
|
|
679
|
+
* action, performed, with the outcome on result. Every intervention has a deadline,
|
|
680
|
+
* so this always returns eventually. A dropped connection or an
|
|
681
|
+
* orchestrator restart is survived by polling again; the server, not the
|
|
682
|
+
* connection, holds the state. Pass `opts.signal` to stop early.
|
|
683
|
+
*/
|
|
684
|
+
async wait(id, opts = {}) {
|
|
685
|
+
return waitIntervention(this.t, id, opts);
|
|
686
|
+
}
|
|
687
|
+
/** Resolve a pending intervention as approved. For a held exec or
|
|
688
|
+
* lifecycle action, the orchestrator then performs it (optionally with
|
|
689
|
+
* body.editedArgv, or granting repeats with body.grant); wait on the id for
|
|
690
|
+
* its result. Requires the master token. */
|
|
691
|
+
async approve(id, body = {}, opts = {}) {
|
|
692
|
+
return this.decide(id, "approve", body, opts);
|
|
693
|
+
}
|
|
694
|
+
/** Resolve a pending intervention as rejected. Requires the master token. */
|
|
695
|
+
async reject(id, body = {}, opts = {}) {
|
|
696
|
+
return this.decide(id, "reject", { reason: body.reason }, opts);
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Answer a pending secret request. The value is written into the guest at
|
|
700
|
+
* the declared target and dropped; fuse never stores, logs or returns it.
|
|
701
|
+
* Needs the hitl:secret scope.
|
|
702
|
+
*/
|
|
703
|
+
async answerSecret(id, value, opts = {}) {
|
|
704
|
+
requireArg(id, "intervention id");
|
|
705
|
+
return this.t.json(
|
|
706
|
+
"POST",
|
|
707
|
+
`/v1/interventions/${encodeURIComponent(id)}`,
|
|
708
|
+
{ query: { action: "secret" }, body: { value }, signal: opts.signal }
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
async decide(id, action, body, opts) {
|
|
712
|
+
requireArg(id, "intervention id");
|
|
713
|
+
return this.t.json(
|
|
714
|
+
"POST",
|
|
715
|
+
`/v1/interventions/${encodeURIComponent(id)}`,
|
|
716
|
+
{
|
|
717
|
+
query: { action },
|
|
718
|
+
body: {
|
|
719
|
+
answer: body.answer || void 0,
|
|
720
|
+
reason: body.reason || void 0,
|
|
721
|
+
edited_argv: body.editedArgv?.length ? body.editedArgv : void 0,
|
|
722
|
+
edited_text: body.editedText,
|
|
723
|
+
grant: body.grant || void 0,
|
|
724
|
+
extend_seconds: body.extendSeconds || void 0
|
|
725
|
+
},
|
|
726
|
+
signal: opts.signal
|
|
727
|
+
}
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
async function waitIntervention(t, id, opts = {}) {
|
|
732
|
+
for (; ; ) {
|
|
733
|
+
const i = await getIntervention(t, id, WAIT_POLL, opts);
|
|
734
|
+
if (isInterventionSettled(i)) return i;
|
|
735
|
+
opts.signal?.throwIfAborted();
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
function getIntervention(t, id, wait, opts) {
|
|
739
|
+
requireArg(id, "intervention id");
|
|
740
|
+
return t.json("GET", `/v1/interventions/${encodeURIComponent(id)}`, {
|
|
741
|
+
query: { wait },
|
|
742
|
+
signal: opts.signal
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// src/gates.ts
|
|
747
|
+
async function settleHeld(t, held, o, opts) {
|
|
748
|
+
const onHeld = o.onHeld ?? t.onHeld;
|
|
749
|
+
onHeld?.(held);
|
|
750
|
+
if (o.wait === false || t.hitlNoWait) throw new PendingInterventionError(held);
|
|
751
|
+
const i = await waitIntervention(t, held.id, opts);
|
|
752
|
+
if (!isInterventionApproved(i)) throw new InterventionRejectedError(i);
|
|
753
|
+
if (i.result?.error) throw new HeldActionError(i);
|
|
754
|
+
return i;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// src/takeover.ts
|
|
758
|
+
async function retryLocked(t, call, wait, opts) {
|
|
759
|
+
for (; ; ) {
|
|
760
|
+
try {
|
|
761
|
+
return await call();
|
|
762
|
+
} catch (err) {
|
|
763
|
+
if (wait === false || !isLocked(err) || !err.interventionId) throw err;
|
|
764
|
+
await waitIntervention(t, err.interventionId, opts);
|
|
765
|
+
opts.signal?.throwIfAborted();
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// src/environments.ts
|
|
771
|
+
var MAX_PAGE_LIMIT2 = 200;
|
|
370
772
|
var EnvironmentsService = class {
|
|
371
773
|
constructor(t) {
|
|
372
774
|
this.t = t;
|
|
@@ -380,7 +782,7 @@ var EnvironmentsService = class {
|
|
|
380
782
|
let cursor = options.cursor;
|
|
381
783
|
for (; ; ) {
|
|
382
784
|
const page = await this.listPage(
|
|
383
|
-
{ ...options, limit:
|
|
785
|
+
{ ...options, limit: MAX_PAGE_LIMIT2, cursor },
|
|
384
786
|
opts
|
|
385
787
|
);
|
|
386
788
|
out.push(...page.environments);
|
|
@@ -419,14 +821,18 @@ var EnvironmentsService = class {
|
|
|
419
821
|
signal: opts.signal
|
|
420
822
|
});
|
|
421
823
|
}
|
|
422
|
-
/** Fork an environment into a new one; returns the new environment.
|
|
824
|
+
/** Fork an environment into a new one; returns the new environment. When a
|
|
825
|
+
* hitl gate holds the fork, it waits for the decision and returns the
|
|
826
|
+
* environment the orchestrator then forked. */
|
|
423
827
|
async fork(vmId, body = {}, opts = {}) {
|
|
424
828
|
requireArg(vmId, "vm id");
|
|
425
|
-
|
|
829
|
+
const out = await this.t.gated(
|
|
426
830
|
"POST",
|
|
427
831
|
`/v1/environments/${encodeURIComponent(vmId)}`,
|
|
428
832
|
{ query: { action: "fork" }, body, signal: opts.signal }
|
|
429
833
|
);
|
|
834
|
+
if (!out.held) return out.value;
|
|
835
|
+
return this.heldEnvironment(out.held, opts);
|
|
430
836
|
}
|
|
431
837
|
/**
|
|
432
838
|
* Migrate an environment to another host; returns the new environment. The
|
|
@@ -434,11 +840,22 @@ var EnvironmentsService = class {
|
|
|
434
840
|
*/
|
|
435
841
|
async migrate(vmId, body = {}, opts = {}) {
|
|
436
842
|
requireArg(vmId, "vm id");
|
|
437
|
-
|
|
843
|
+
const out = await this.t.gated(
|
|
438
844
|
"POST",
|
|
439
845
|
`/v1/environments/${encodeURIComponent(vmId)}`,
|
|
440
846
|
{ query: { action: "migrate" }, body, signal: opts.signal }
|
|
441
847
|
);
|
|
848
|
+
if (!out.held) return out.value;
|
|
849
|
+
return this.heldEnvironment(out.held, opts);
|
|
850
|
+
}
|
|
851
|
+
/** heldEnvironment waits out a held fork or migrate and returns the
|
|
852
|
+
* environment it produced. */
|
|
853
|
+
async heldEnvironment(held, opts) {
|
|
854
|
+
const i = await settleHeld(this.t, held, {}, opts);
|
|
855
|
+
if (!i.result?.env_id) {
|
|
856
|
+
throw new FuseError(`intervention ${i.id} settled without an environment`);
|
|
857
|
+
}
|
|
858
|
+
return this.get(i.result.env_id, opts);
|
|
442
859
|
}
|
|
443
860
|
/**
|
|
444
861
|
* Run a command inside a running environment's guest and return its exit code
|
|
@@ -449,6 +866,16 @@ var EnvironmentsService = class {
|
|
|
449
866
|
*
|
|
450
867
|
* Exec requires the master token.
|
|
451
868
|
*
|
|
869
|
+
* When a hitl gate holds the exec, exec waits for the decision (bounded
|
|
870
|
+
* only by `opts.signal`); the orchestrator runs the approved (possibly
|
|
871
|
+
* edited) argv and exec returns its result. A rejection throws an
|
|
872
|
+
* InterventionRejectedError, a deny gate an error isHitlDenied matches.
|
|
873
|
+
* `wait: false` throws a PendingInterventionError at once instead.
|
|
874
|
+
*
|
|
875
|
+
* While a human holds a takeover (or an ask-mode stall is pending) the
|
|
876
|
+
* server answers 423; exec waits for that intervention to settle and runs
|
|
877
|
+
* the command then. `wait: false` throws a LockedError at once instead.
|
|
878
|
+
*
|
|
452
879
|
* @example
|
|
453
880
|
* const out = await client.environments.exec(id, { cmd: ["ls", "-l"] });
|
|
454
881
|
* if (out.exit_code !== 0) console.error(out.stderr);
|
|
@@ -463,11 +890,23 @@ var EnvironmentsService = class {
|
|
|
463
890
|
if (hasCmd && hasShell) {
|
|
464
891
|
throw new FuseError("cmd and shell are mutually exclusive");
|
|
465
892
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
893
|
+
const { wait, onHeld, ...wire } = body;
|
|
894
|
+
const out = await retryLocked(
|
|
895
|
+
this.t,
|
|
896
|
+
() => this.t.gated("POST", `/v1/environments/${encodeURIComponent(vmId)}`, {
|
|
897
|
+
query: { action: "exec" },
|
|
898
|
+
body: wire,
|
|
899
|
+
signal: opts.signal
|
|
900
|
+
}),
|
|
901
|
+
wait,
|
|
902
|
+
opts
|
|
470
903
|
);
|
|
904
|
+
if (!out.held) return out.value;
|
|
905
|
+
const i = await settleHeld(this.t, out.held, { wait, onHeld }, opts);
|
|
906
|
+
if (!i.result?.exec) {
|
|
907
|
+
throw new FuseError(`intervention ${i.id} settled without an exec result`);
|
|
908
|
+
}
|
|
909
|
+
return i.result.exec;
|
|
471
910
|
}
|
|
472
911
|
/**
|
|
473
912
|
* Relay one computer-use action to the environment's desktop and return the
|
|
@@ -475,6 +914,13 @@ var EnvironmentsService = class {
|
|
|
475
914
|
* booted from a desktop image; on any other image the server answers 503
|
|
476
915
|
* with a reason.
|
|
477
916
|
*
|
|
917
|
+
* Like exec, it waits out a takeover or a pending stall (a 423) and then
|
|
918
|
+
* acts; `opts.wait: false` throws a LockedError at once instead. When a
|
|
919
|
+
* computer gate holds the action (a 202), it waits for the decision and
|
|
920
|
+
* returns what the orchestrator got performing it (with the human's edited
|
|
921
|
+
* text, if any); a rejection throws an InterventionRejectedError, and
|
|
922
|
+
* `opts.wait: false` throws a PendingInterventionError at once instead.
|
|
923
|
+
*
|
|
478
924
|
* @example
|
|
479
925
|
* const res = await client.environments.computer(id, {
|
|
480
926
|
* action: "left_click",
|
|
@@ -484,11 +930,7 @@ var EnvironmentsService = class {
|
|
|
484
930
|
async computer(vmId, action, opts = {}) {
|
|
485
931
|
requireArg(vmId, "vm id");
|
|
486
932
|
requireArg(action?.action, "action");
|
|
487
|
-
return this.
|
|
488
|
-
"POST",
|
|
489
|
-
`/v1/environments/${encodeURIComponent(vmId)}/computer`,
|
|
490
|
-
{ body: action, signal: opts.signal }
|
|
491
|
-
);
|
|
933
|
+
return this.postComputer(vmId, action, opts);
|
|
492
934
|
}
|
|
493
935
|
/**
|
|
494
936
|
* Execute one computer tool_use against the environment and shape the
|
|
@@ -499,7 +941,7 @@ var EnvironmentsService = class {
|
|
|
499
941
|
* An action the environment refuses (a malformed coordinate, a display
|
|
500
942
|
* that is not up) resolves to `is_error` content rather than throwing: the
|
|
501
943
|
* agent loop should feed it to the model, which is the party that can
|
|
502
|
-
* correct it. A throw is reserved for failures the model cannot fix —
|
|
944
|
+
* correct it. So does a hitl deny gate or a human rejecting a held action. A throw is reserved for failures the model cannot fix —
|
|
503
945
|
* transport, auth, an unknown environment.
|
|
504
946
|
*
|
|
505
947
|
* @example
|
|
@@ -521,18 +963,22 @@ var EnvironmentsService = class {
|
|
|
521
963
|
}
|
|
522
964
|
let res;
|
|
523
965
|
try {
|
|
524
|
-
res = await this.
|
|
525
|
-
"POST",
|
|
526
|
-
`/v1/environments/${encodeURIComponent(vmId)}/computer`,
|
|
527
|
-
{ body: input, signal: opts.signal }
|
|
528
|
-
);
|
|
966
|
+
res = await this.postComputer(vmId, input, opts);
|
|
529
967
|
} catch (err) {
|
|
530
|
-
if (isFuseApiError(err) && (err.status === 400 || err.status === 409 || err.status === 503)) {
|
|
968
|
+
if (isFuseApiError(err) && (err.status === 400 || err.status === 409 || err.status === 503 || isHitlDenied(err))) {
|
|
531
969
|
return {
|
|
532
970
|
is_error: true,
|
|
533
971
|
content: [{ type: "text", text: err.message || "the computer action failed" }]
|
|
534
972
|
};
|
|
535
973
|
}
|
|
974
|
+
if (err instanceof InterventionRejectedError) {
|
|
975
|
+
return {
|
|
976
|
+
is_error: true,
|
|
977
|
+
content: [
|
|
978
|
+
{ type: "text", text: `a human rejected this action: ${err.message}` }
|
|
979
|
+
]
|
|
980
|
+
};
|
|
981
|
+
}
|
|
536
982
|
throw err;
|
|
537
983
|
}
|
|
538
984
|
const content = [];
|
|
@@ -550,6 +996,137 @@ var EnvironmentsService = class {
|
|
|
550
996
|
if (content.length === 0) content.push({ type: "text", text: "done" });
|
|
551
997
|
return { content };
|
|
552
998
|
}
|
|
999
|
+
async postComputer(vmId, body, opts) {
|
|
1000
|
+
const out = await retryLocked(
|
|
1001
|
+
this.t,
|
|
1002
|
+
() => this.t.gated(
|
|
1003
|
+
"POST",
|
|
1004
|
+
`/v1/environments/${encodeURIComponent(vmId)}/computer`,
|
|
1005
|
+
{ body, signal: opts.signal }
|
|
1006
|
+
),
|
|
1007
|
+
opts.wait,
|
|
1008
|
+
opts
|
|
1009
|
+
);
|
|
1010
|
+
if (!out.held) return out.value;
|
|
1011
|
+
const i = await settleHeld(
|
|
1012
|
+
this.t,
|
|
1013
|
+
out.held,
|
|
1014
|
+
{ wait: opts.wait, onHeld: opts.onHeld },
|
|
1015
|
+
opts
|
|
1016
|
+
);
|
|
1017
|
+
if (!i.result?.computer) {
|
|
1018
|
+
throw new FuseError(`intervention ${i.id} settled without a computer result`);
|
|
1019
|
+
}
|
|
1020
|
+
return i.result.computer;
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Ask a human for something the environment's hitl policy declares. By
|
|
1024
|
+
* default it waits (by long-polling) until the intervention resolves
|
|
1025
|
+
* (answered, rejected, expired or cancelled); read the result off the
|
|
1026
|
+
* returned intervention's decision. Pass `wait: false` to get the pending
|
|
1027
|
+
* handle instead. Pass `opts.signal` to stop waiting early.
|
|
1028
|
+
*
|
|
1029
|
+
* @example
|
|
1030
|
+
* const res = await client.environments.requestHuman(id, {
|
|
1031
|
+
* question: "deploy to prod?",
|
|
1032
|
+
* choices: ["yes", "no"],
|
|
1033
|
+
* });
|
|
1034
|
+
* if (isInterventionApproved(res)) console.log(res.decision?.answer);
|
|
1035
|
+
*/
|
|
1036
|
+
async requestHuman(vmId, req, opts = {}) {
|
|
1037
|
+
requireArg(vmId, "vm id");
|
|
1038
|
+
const pending = await this.t.json(
|
|
1039
|
+
"POST",
|
|
1040
|
+
`/v1/environments/${encodeURIComponent(vmId)}`,
|
|
1041
|
+
{
|
|
1042
|
+
query: { action: "request" },
|
|
1043
|
+
body: {
|
|
1044
|
+
kind: req.kind || "ask",
|
|
1045
|
+
question: req.question || void 0,
|
|
1046
|
+
choices: req.choices,
|
|
1047
|
+
target: req.target || void 0,
|
|
1048
|
+
message: req.message || void 0,
|
|
1049
|
+
via: req.via || void 0,
|
|
1050
|
+
reason: req.reason || void 0,
|
|
1051
|
+
path: req.path || void 0,
|
|
1052
|
+
content: req.content || void 0,
|
|
1053
|
+
format: req.format || void 0,
|
|
1054
|
+
timeout_seconds: req.timeoutSeconds || void 0
|
|
1055
|
+
},
|
|
1056
|
+
signal: opts.signal
|
|
1057
|
+
}
|
|
1058
|
+
);
|
|
1059
|
+
if (req.wait === false) return pending;
|
|
1060
|
+
return waitIntervention(this.t, pending.id, opts);
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Submit a guest file or a text/diff blob for a human to approve or reject
|
|
1064
|
+
* with comments, and wait for the verdict. Needs hitl.requests.review.
|
|
1065
|
+
* With `wait: false` it returns at once with the pending intervention and
|
|
1066
|
+
* approved false.
|
|
1067
|
+
*
|
|
1068
|
+
* @example
|
|
1069
|
+
* const r = await client.environments.requestReview(id, {
|
|
1070
|
+
* content: diff,
|
|
1071
|
+
* format: ReviewFormat.Diff,
|
|
1072
|
+
* });
|
|
1073
|
+
* if (!r.approved) console.log(r.comments);
|
|
1074
|
+
*/
|
|
1075
|
+
async requestReview(vmId, req, opts = {}) {
|
|
1076
|
+
const i = await this.requestHuman(
|
|
1077
|
+
vmId,
|
|
1078
|
+
{
|
|
1079
|
+
kind: HitlKind.Review,
|
|
1080
|
+
path: req.path,
|
|
1081
|
+
content: req.content,
|
|
1082
|
+
format: req.format,
|
|
1083
|
+
message: req.message,
|
|
1084
|
+
timeoutSeconds: req.timeoutSeconds,
|
|
1085
|
+
wait: req.wait
|
|
1086
|
+
},
|
|
1087
|
+
opts
|
|
1088
|
+
);
|
|
1089
|
+
return {
|
|
1090
|
+
approved: isInterventionApproved(i),
|
|
1091
|
+
comments: i.decision?.reason ?? "",
|
|
1092
|
+
intervention: i
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Take over the environment as a human: claims the agent's pending
|
|
1097
|
+
* takeover request if there is one, else opens a new one. While held, the
|
|
1098
|
+
* agent's exec and computer calls wait. Returns the held intervention.
|
|
1099
|
+
* Needs the hitl:takeover scope; a conflict when someone else holds it.
|
|
1100
|
+
*/
|
|
1101
|
+
async takeover(vmId, body = {}, opts = {}) {
|
|
1102
|
+
requireArg(vmId, "vm id");
|
|
1103
|
+
return this.t.json(
|
|
1104
|
+
"POST",
|
|
1105
|
+
`/v1/environments/${encodeURIComponent(vmId)}`,
|
|
1106
|
+
{
|
|
1107
|
+
query: { action: "takeover" },
|
|
1108
|
+
body: { via: body.via || void 0, reason: body.reason || void 0 },
|
|
1109
|
+
signal: opts.signal
|
|
1110
|
+
}
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* End a takeover and hand the environment back to the agent. The note is
|
|
1115
|
+
* the takeover's answer. Returns the approved intervention; a conflict when
|
|
1116
|
+
* nothing is held.
|
|
1117
|
+
*/
|
|
1118
|
+
async release(vmId, body = {}, opts = {}) {
|
|
1119
|
+
requireArg(vmId, "vm id");
|
|
1120
|
+
return this.t.json(
|
|
1121
|
+
"POST",
|
|
1122
|
+
`/v1/environments/${encodeURIComponent(vmId)}`,
|
|
1123
|
+
{
|
|
1124
|
+
query: { action: "release" },
|
|
1125
|
+
body: { note: body.note || void 0 },
|
|
1126
|
+
signal: opts.signal
|
|
1127
|
+
}
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
553
1130
|
/** Report whether the environment has a live display and at what geometry.
|
|
554
1131
|
* up is false with a reason on an image with no desktop. */
|
|
555
1132
|
async computerDisplay(vmId, opts = {}) {
|
|
@@ -563,31 +1140,42 @@ var EnvironmentsService = class {
|
|
|
563
1140
|
/**
|
|
564
1141
|
* Phase-1 only: quiesces the guest workload and moves the environment to
|
|
565
1142
|
* "draining" so you can inspect its output, but does not destroy it. Call
|
|
566
|
-
* destroy() afterward to remove it
|
|
567
|
-
* undestroyed keeps running (and billing)
|
|
1143
|
+
* destroy() afterward to remove it. A drained environment left
|
|
1144
|
+
* undestroyed keeps running (and billing) until the orchestrator's
|
|
1145
|
+
* draining timeout (5m30s by default) passes, and then the orchestrator
|
|
1146
|
+
* destroys it.
|
|
568
1147
|
*/
|
|
569
1148
|
async drain(vmId, opts = {}) {
|
|
570
1149
|
requireArg(vmId, "vm id");
|
|
571
|
-
|
|
1150
|
+
const out = await this.t.gated(
|
|
572
1151
|
"POST",
|
|
573
1152
|
`/v1/environments/${encodeURIComponent(vmId)}`,
|
|
574
1153
|
{ query: { action: "drain" }, signal: opts.signal }
|
|
575
1154
|
);
|
|
1155
|
+
if (!out.held) return out.value;
|
|
1156
|
+
await settleHeld(this.t, out.held, {}, opts);
|
|
1157
|
+
return this.get(vmId, opts);
|
|
576
1158
|
}
|
|
577
1159
|
/** Rotate the per-VM credentials and push them to the guest. */
|
|
578
1160
|
async rotateToken(vmId, opts = {}) {
|
|
579
1161
|
requireArg(vmId, "vm id");
|
|
580
|
-
await this.t.
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
1162
|
+
const held = await this.t.gatedNoContent(
|
|
1163
|
+
"POST",
|
|
1164
|
+
`/v1/environments/${encodeURIComponent(vmId)}`,
|
|
1165
|
+
{ query: { action: "rotate-token" }, signal: opts.signal }
|
|
1166
|
+
);
|
|
1167
|
+
if (held) await settleHeld(this.t, held, {}, opts);
|
|
584
1168
|
}
|
|
585
|
-
/** Forcefully destroy an environment. Idempotent.
|
|
1169
|
+
/** Forcefully destroy an environment. Idempotent. When a hitl gate holds
|
|
1170
|
+
* the destroy, it waits for the decision. */
|
|
586
1171
|
async destroy(vmId, opts = {}) {
|
|
587
1172
|
requireArg(vmId, "vm id");
|
|
588
|
-
await this.t.
|
|
589
|
-
|
|
590
|
-
|
|
1173
|
+
const held = await this.t.gatedNoContent(
|
|
1174
|
+
"DELETE",
|
|
1175
|
+
`/v1/environments/${encodeURIComponent(vmId)}`,
|
|
1176
|
+
{ signal: opts.signal }
|
|
1177
|
+
);
|
|
1178
|
+
if (held) await settleHeld(this.t, held, {}, opts);
|
|
591
1179
|
}
|
|
592
1180
|
/**
|
|
593
1181
|
* Open the SSE event stream for an environment. The returned promise rejects
|
|
@@ -608,7 +1196,7 @@ var EnvironmentsService = class {
|
|
|
608
1196
|
};
|
|
609
1197
|
|
|
610
1198
|
// src/snapshots.ts
|
|
611
|
-
var
|
|
1199
|
+
var MAX_PAGE_LIMIT3 = 200;
|
|
612
1200
|
var SnapshotsService = class {
|
|
613
1201
|
constructor(t) {
|
|
614
1202
|
this.t = t;
|
|
@@ -623,14 +1211,23 @@ var SnapshotsService = class {
|
|
|
623
1211
|
* be `"disk"` even for a `live: true` request that reached a host too old to
|
|
624
1212
|
* honour it. A caller that depends on the memory being there should check
|
|
625
1213
|
* it. `live: true` against a backend with no live-snapshot support throws a
|
|
626
|
-
* 501 `unimplemented`, not a conflict.
|
|
1214
|
+
* 501 `unimplemented`, not a conflict.
|
|
1215
|
+
*
|
|
1216
|
+
* When a hitl gate holds the snapshot, create waits for the decision and
|
|
1217
|
+
* returns the snapshot the orchestrator then took. */
|
|
627
1218
|
async create(vmId, body = {}, opts = {}) {
|
|
628
1219
|
requireArg(vmId, "vm id");
|
|
629
|
-
|
|
1220
|
+
const out = await this.t.gated(
|
|
630
1221
|
"POST",
|
|
631
1222
|
`/v1/environments/${encodeURIComponent(vmId)}/snapshots`,
|
|
632
1223
|
{ body, signal: opts.signal }
|
|
633
1224
|
);
|
|
1225
|
+
if (!out.held) return out.value;
|
|
1226
|
+
const i = await settleHeld(this.t, out.held, {}, opts);
|
|
1227
|
+
if (!i.result?.snapshot_id) {
|
|
1228
|
+
throw new FuseError(`intervention ${i.id} settled without a snapshot`);
|
|
1229
|
+
}
|
|
1230
|
+
return this.get(i.result.snapshot_id, opts);
|
|
634
1231
|
}
|
|
635
1232
|
/** List snapshots, optionally filtered. Transparently walks every result
|
|
636
1233
|
* page. For explicit single-page control (e.g. a cursor from a previous
|
|
@@ -640,7 +1237,7 @@ var SnapshotsService = class {
|
|
|
640
1237
|
let cursor = options.cursor;
|
|
641
1238
|
for (; ; ) {
|
|
642
1239
|
const page = await this.listPage(
|
|
643
|
-
{ ...options, limit:
|
|
1240
|
+
{ ...options, limit: MAX_PAGE_LIMIT3, cursor },
|
|
644
1241
|
opts
|
|
645
1242
|
);
|
|
646
1243
|
out.push(...page.snapshots);
|
|
@@ -718,7 +1315,7 @@ var SnapshotsService = class {
|
|
|
718
1315
|
};
|
|
719
1316
|
|
|
720
1317
|
// src/hosts.ts
|
|
721
|
-
var
|
|
1318
|
+
var MAX_PAGE_LIMIT4 = 200;
|
|
722
1319
|
var HostsService = class {
|
|
723
1320
|
constructor(t) {
|
|
724
1321
|
this.t = t;
|
|
@@ -735,7 +1332,7 @@ var HostsService = class {
|
|
|
735
1332
|
const out = [];
|
|
736
1333
|
let cursor;
|
|
737
1334
|
for (; ; ) {
|
|
738
|
-
const page = await this.listPage({ limit:
|
|
1335
|
+
const page = await this.listPage({ limit: MAX_PAGE_LIMIT4, cursor }, opts);
|
|
739
1336
|
out.push(...page.hosts);
|
|
740
1337
|
if (!page.nextCursor) break;
|
|
741
1338
|
cursor = page.nextCursor;
|
|
@@ -791,12 +1388,19 @@ var ApiKeysService = class {
|
|
|
791
1388
|
}
|
|
792
1389
|
t;
|
|
793
1390
|
/**
|
|
794
|
-
* Issue a new API key.
|
|
795
|
-
* unrecoverable afterward.
|
|
1391
|
+
* Issue a new API key. Pass a label, or options for a scoped key. The raw
|
|
1392
|
+
* secret is returned once in `key` and is unrecoverable afterward.
|
|
796
1393
|
*/
|
|
797
|
-
async create(
|
|
1394
|
+
async create(labelOrOptions, opts = {}) {
|
|
1395
|
+
const o = typeof labelOrOptions === "string" ? { label: labelOrOptions } : labelOrOptions ?? {};
|
|
798
1396
|
return this.t.json("POST", "/v1/api-keys", {
|
|
799
|
-
body:
|
|
1397
|
+
body: {
|
|
1398
|
+
label: o.label || void 0,
|
|
1399
|
+
scopes: o.scopes,
|
|
1400
|
+
env_id: o.envId || void 0,
|
|
1401
|
+
ttl_seconds: o.ttlSeconds || void 0,
|
|
1402
|
+
actor: o.actor
|
|
1403
|
+
},
|
|
800
1404
|
signal: opts.signal
|
|
801
1405
|
});
|
|
802
1406
|
}
|
|
@@ -817,15 +1421,18 @@ var ApiKeysService = class {
|
|
|
817
1421
|
};
|
|
818
1422
|
|
|
819
1423
|
// src/version.ts
|
|
820
|
-
var VERSION = "0.
|
|
1424
|
+
var VERSION = "0.42.0";
|
|
821
1425
|
|
|
822
1426
|
// src/client.ts
|
|
823
|
-
var FuseClient = class {
|
|
1427
|
+
var FuseClient = class _FuseClient {
|
|
824
1428
|
environments;
|
|
825
1429
|
snapshots;
|
|
826
1430
|
hosts;
|
|
827
1431
|
apiKeys;
|
|
1432
|
+
interventions;
|
|
1433
|
+
options;
|
|
828
1434
|
constructor(options) {
|
|
1435
|
+
this.options = options;
|
|
829
1436
|
const transport = new Transport({
|
|
830
1437
|
baseUrl: options.baseUrl,
|
|
831
1438
|
token: options.token,
|
|
@@ -833,15 +1440,26 @@ var FuseClient = class {
|
|
|
833
1440
|
userAgent: options.userAgent ?? `fuse-ts/${VERSION}`,
|
|
834
1441
|
requestId: options.requestId,
|
|
835
1442
|
timeoutMs: options.timeoutMs,
|
|
836
|
-
headers: options.headers
|
|
1443
|
+
headers: options.headers,
|
|
1444
|
+
actor: options.actor,
|
|
1445
|
+
hitlNoWait: options.hitlNoWait,
|
|
1446
|
+
onHeld: options.onHeld
|
|
837
1447
|
});
|
|
838
1448
|
this.environments = new EnvironmentsService(transport);
|
|
839
1449
|
this.snapshots = new SnapshotsService(transport);
|
|
840
1450
|
this.hosts = new HostsService(transport);
|
|
841
1451
|
this.apiKeys = new ApiKeysService(transport);
|
|
1452
|
+
this.interventions = new InterventionsService(transport);
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* withActor returns a copy of this client, with the same options otherwise,
|
|
1456
|
+
* that sends `actor` as the Fuse-Actor headers. This client is unchanged.
|
|
1457
|
+
*/
|
|
1458
|
+
withActor(actor) {
|
|
1459
|
+
return new _FuseClient({ ...this.options, actor });
|
|
842
1460
|
}
|
|
843
1461
|
};
|
|
844
1462
|
|
|
845
|
-
export { ApiKeysService, EnvironmentsService, ErrorCode, FuseApiError, FuseClient, FuseError, HealthState, HostsService, SnapshotsService, State, VERSION, isConflict, isFuseApiError, isInvalidArgument, isNotFound, isTerminalState, isUnauthorized, isUnavailable };
|
|
1463
|
+
export { ACTOR_SOURCE_DECIDER, ALLOWLIST_GATE, ApiKeysService, AuditEvent, DeciderVerdict, EnvironmentsService, ErrorCode, ForbiddenError, ForbiddenReason, FuseApiError, FuseClient, FuseError, GateMode, HealthState, HeldActionError, HitlKind, HitlScope, HostsService, InterventionOutcome, InterventionRejectedError, InterventionState, InterventionsService, LifecycleAction, LockedError, PendingInterventionError, ReviewFormat, SnapshotsService, State, TakeoverVia, VERSION, isConflict, isForbidden, isFuseApiError, isHitlDenied, isInterventionApproved, isInterventionSettled, isInterventionTerminal, isInvalidArgument, isLocked, isNotFound, isTerminalState, isUnauthorized, isUnavailable };
|
|
846
1464
|
//# sourceMappingURL=index.js.map
|
|
847
1465
|
//# sourceMappingURL=index.js.map
|