@folsom/fuse 0.36.0 → 0.41.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 +873 -67
- package/dist/index.js +673 -57
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
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,15 +409,14 @@ 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",
|
|
@@ -296,6 +438,90 @@ var HealthState = {
|
|
|
296
438
|
* ended. */
|
|
297
439
|
Failing: "failing"
|
|
298
440
|
};
|
|
441
|
+
var AuditEvent = {
|
|
442
|
+
Raised: "raised",
|
|
443
|
+
Decided: "decided",
|
|
444
|
+
Expired: "expired",
|
|
445
|
+
Cancelled: "cancelled",
|
|
446
|
+
InviteMinted: "invite_minted",
|
|
447
|
+
InviteUsed: "invite_used",
|
|
448
|
+
GrantCreated: "grant_created",
|
|
449
|
+
GrantUsed: "grant_used",
|
|
450
|
+
SecretAnswered: "secret_answered",
|
|
451
|
+
TakeoverStarted: "takeover_started",
|
|
452
|
+
TakeoverEnded: "takeover_ended",
|
|
453
|
+
TakeoverSession: "takeover_session"
|
|
454
|
+
};
|
|
455
|
+
var HitlKind = {
|
|
456
|
+
Ask: "ask",
|
|
457
|
+
Exec: "exec",
|
|
458
|
+
Lifecycle: "lifecycle",
|
|
459
|
+
Computer: "computer",
|
|
460
|
+
Review: "review",
|
|
461
|
+
Secret: "secret",
|
|
462
|
+
Takeover: "takeover",
|
|
463
|
+
Stall: "stall",
|
|
464
|
+
Expiry: "expiry"
|
|
465
|
+
};
|
|
466
|
+
var GateMode = {
|
|
467
|
+
Ask: "ask",
|
|
468
|
+
Notify: "notify",
|
|
469
|
+
Deny: "deny"
|
|
470
|
+
};
|
|
471
|
+
var DeciderVerdict = {
|
|
472
|
+
Allow: "allow",
|
|
473
|
+
Deny: "deny",
|
|
474
|
+
Ask: "ask"
|
|
475
|
+
};
|
|
476
|
+
var ACTOR_SOURCE_DECIDER = "decider";
|
|
477
|
+
var ReviewFormat = {
|
|
478
|
+
Text: "text",
|
|
479
|
+
Diff: "diff"
|
|
480
|
+
};
|
|
481
|
+
var TakeoverVia = {
|
|
482
|
+
Terminal: "terminal",
|
|
483
|
+
Desktop: "desktop"
|
|
484
|
+
};
|
|
485
|
+
var LifecycleAction = {
|
|
486
|
+
Destroy: "destroy",
|
|
487
|
+
Fork: "fork",
|
|
488
|
+
Migrate: "migrate",
|
|
489
|
+
Drain: "drain",
|
|
490
|
+
Snapshot: "snapshot",
|
|
491
|
+
RotateToken: "rotate-token"
|
|
492
|
+
};
|
|
493
|
+
var ALLOWLIST_GATE = "exec.allowlist";
|
|
494
|
+
var InterventionState = {
|
|
495
|
+
Pending: "pending",
|
|
496
|
+
Approved: "approved",
|
|
497
|
+
Rejected: "rejected",
|
|
498
|
+
Expired: "expired",
|
|
499
|
+
Cancelled: "cancelled"
|
|
500
|
+
};
|
|
501
|
+
var InterventionOutcome = {
|
|
502
|
+
Approve: "approve",
|
|
503
|
+
Reject: "reject"
|
|
504
|
+
};
|
|
505
|
+
function isInterventionTerminal(i) {
|
|
506
|
+
return !!i && !!i.state && i.state !== InterventionState.Pending;
|
|
507
|
+
}
|
|
508
|
+
function isInterventionApproved(i) {
|
|
509
|
+
return i?.decision?.outcome === InterventionOutcome.Approve;
|
|
510
|
+
}
|
|
511
|
+
function isInterventionSettled(i) {
|
|
512
|
+
if (!i || !isInterventionTerminal(i)) return false;
|
|
513
|
+
if (i.decision?.decided_by?.source === ACTOR_SOURCE_DECIDER) return true;
|
|
514
|
+
const runs = i.mode === GateMode.Ask && (!!i.payload?.exec || !!i.payload?.lifecycle || !!i.payload?.computer);
|
|
515
|
+
return !runs || !isInterventionApproved(i) || !!i.result;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// src/validate.ts
|
|
519
|
+
function requireArg(value, name) {
|
|
520
|
+
if (!value) {
|
|
521
|
+
throw new FuseError(`${name} is required`);
|
|
522
|
+
}
|
|
523
|
+
return value;
|
|
524
|
+
}
|
|
299
525
|
|
|
300
526
|
// src/events.ts
|
|
301
527
|
var DATA_PREFIX = "data:";
|
|
@@ -365,8 +591,182 @@ function parseEventStream(res, signal) {
|
|
|
365
591
|
};
|
|
366
592
|
}
|
|
367
593
|
|
|
368
|
-
// src/
|
|
594
|
+
// src/interventions.ts
|
|
369
595
|
var MAX_PAGE_LIMIT = 200;
|
|
596
|
+
var WAIT_POLL = "30s";
|
|
597
|
+
var InterventionsService = class {
|
|
598
|
+
constructor(t) {
|
|
599
|
+
this.t = t;
|
|
600
|
+
}
|
|
601
|
+
t;
|
|
602
|
+
/** List every intervention matching options, oldest first, walking every
|
|
603
|
+
* page. */
|
|
604
|
+
async list(options = {}, opts = {}) {
|
|
605
|
+
const out = [];
|
|
606
|
+
let cursor = options.cursor;
|
|
607
|
+
for (; ; ) {
|
|
608
|
+
const page = await this.listPage(
|
|
609
|
+
{ ...options, limit: MAX_PAGE_LIMIT, cursor },
|
|
610
|
+
opts
|
|
611
|
+
);
|
|
612
|
+
out.push(...page.interventions);
|
|
613
|
+
if (!page.nextCursor) break;
|
|
614
|
+
cursor = page.nextCursor;
|
|
615
|
+
}
|
|
616
|
+
return out;
|
|
617
|
+
}
|
|
618
|
+
/** List one page of interventions matching options. */
|
|
619
|
+
async listPage(options = {}, opts = {}) {
|
|
620
|
+
const out = await this.t.json("GET", "/v1/interventions", {
|
|
621
|
+
query: {
|
|
622
|
+
env: options.envId,
|
|
623
|
+
state: options.state,
|
|
624
|
+
actor: options.actor,
|
|
625
|
+
limit: options.limit ? String(options.limit) : void 0,
|
|
626
|
+
cursor: options.cursor
|
|
627
|
+
},
|
|
628
|
+
signal: opts.signal
|
|
629
|
+
});
|
|
630
|
+
return { interventions: out.interventions ?? [], nextCursor: out.next_cursor };
|
|
631
|
+
}
|
|
632
|
+
/** List every hitl audit row matching options, oldest first, walking every
|
|
633
|
+
* page. */
|
|
634
|
+
async audit(options = {}, opts = {}) {
|
|
635
|
+
const out = [];
|
|
636
|
+
let cursor = options.cursor;
|
|
637
|
+
for (; ; ) {
|
|
638
|
+
const page = await this.auditPage(
|
|
639
|
+
{ ...options, limit: MAX_PAGE_LIMIT, cursor },
|
|
640
|
+
opts
|
|
641
|
+
);
|
|
642
|
+
out.push(...page.entries);
|
|
643
|
+
if (!page.nextCursor) break;
|
|
644
|
+
cursor = page.nextCursor;
|
|
645
|
+
}
|
|
646
|
+
return out;
|
|
647
|
+
}
|
|
648
|
+
/** List one page of the hitl audit log matching options. */
|
|
649
|
+
async auditPage(options = {}, opts = {}) {
|
|
650
|
+
const out = await this.t.json("GET", "/v1/hitl/audit", {
|
|
651
|
+
query: {
|
|
652
|
+
env: options.envId,
|
|
653
|
+
actor: options.actor,
|
|
654
|
+
since: options.since,
|
|
655
|
+
limit: options.limit ? String(options.limit) : void 0,
|
|
656
|
+
cursor: options.cursor
|
|
657
|
+
},
|
|
658
|
+
signal: opts.signal
|
|
659
|
+
});
|
|
660
|
+
return { entries: out.entries ?? [], nextCursor: out.next_cursor };
|
|
661
|
+
}
|
|
662
|
+
/** Fetch a takeover session's recording (an AuditEntry's recording_id):
|
|
663
|
+
* asciinema v2 cast text for a terminal session, json (an input-action log
|
|
664
|
+
* plus screenshots) for a desktop one. Needs the hitl:read scope. */
|
|
665
|
+
async getRecording(recordingId, opts = {}) {
|
|
666
|
+
requireArg(recordingId, "recording id");
|
|
667
|
+
return this.t.text("GET", `/v1/hitl/recordings/${encodeURIComponent(recordingId)}`, {
|
|
668
|
+
signal: opts.signal
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
/** Fetch one intervention without waiting. */
|
|
672
|
+
async get(id, opts = {}) {
|
|
673
|
+
return getIntervention(this.t, id, void 0, opts);
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Block until the intervention settles: resolved, and for an approved gated
|
|
677
|
+
* action, performed, with the outcome on result. Every intervention has a deadline,
|
|
678
|
+
* so this always returns eventually. A dropped connection or an
|
|
679
|
+
* orchestrator restart is survived by polling again; the server, not the
|
|
680
|
+
* connection, holds the state. Pass `opts.signal` to stop early.
|
|
681
|
+
*/
|
|
682
|
+
async wait(id, opts = {}) {
|
|
683
|
+
return waitIntervention(this.t, id, opts);
|
|
684
|
+
}
|
|
685
|
+
/** Resolve a pending intervention as approved. For a held exec or
|
|
686
|
+
* lifecycle action, the orchestrator then performs it (optionally with
|
|
687
|
+
* body.editedArgv, or granting repeats with body.grant); wait on the id for
|
|
688
|
+
* its result. Requires the master token. */
|
|
689
|
+
async approve(id, body = {}, opts = {}) {
|
|
690
|
+
return this.decide(id, "approve", body, opts);
|
|
691
|
+
}
|
|
692
|
+
/** Resolve a pending intervention as rejected. Requires the master token. */
|
|
693
|
+
async reject(id, body = {}, opts = {}) {
|
|
694
|
+
return this.decide(id, "reject", { reason: body.reason }, opts);
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Answer a pending secret request. The value is written into the guest at
|
|
698
|
+
* the declared target and dropped; fuse never stores, logs or returns it.
|
|
699
|
+
* Needs the hitl:secret scope.
|
|
700
|
+
*/
|
|
701
|
+
async answerSecret(id, value, opts = {}) {
|
|
702
|
+
requireArg(id, "intervention id");
|
|
703
|
+
return this.t.json(
|
|
704
|
+
"POST",
|
|
705
|
+
`/v1/interventions/${encodeURIComponent(id)}`,
|
|
706
|
+
{ query: { action: "secret" }, body: { value }, signal: opts.signal }
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
async decide(id, action, body, opts) {
|
|
710
|
+
requireArg(id, "intervention id");
|
|
711
|
+
return this.t.json(
|
|
712
|
+
"POST",
|
|
713
|
+
`/v1/interventions/${encodeURIComponent(id)}`,
|
|
714
|
+
{
|
|
715
|
+
query: { action },
|
|
716
|
+
body: {
|
|
717
|
+
answer: body.answer || void 0,
|
|
718
|
+
reason: body.reason || void 0,
|
|
719
|
+
edited_argv: body.editedArgv?.length ? body.editedArgv : void 0,
|
|
720
|
+
edited_text: body.editedText,
|
|
721
|
+
grant: body.grant || void 0,
|
|
722
|
+
extend_seconds: body.extendSeconds || void 0
|
|
723
|
+
},
|
|
724
|
+
signal: opts.signal
|
|
725
|
+
}
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
async function waitIntervention(t, id, opts = {}) {
|
|
730
|
+
for (; ; ) {
|
|
731
|
+
const i = await getIntervention(t, id, WAIT_POLL, opts);
|
|
732
|
+
if (isInterventionSettled(i)) return i;
|
|
733
|
+
opts.signal?.throwIfAborted();
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
function getIntervention(t, id, wait, opts) {
|
|
737
|
+
requireArg(id, "intervention id");
|
|
738
|
+
return t.json("GET", `/v1/interventions/${encodeURIComponent(id)}`, {
|
|
739
|
+
query: { wait },
|
|
740
|
+
signal: opts.signal
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// src/gates.ts
|
|
745
|
+
async function settleHeld(t, held, o, opts) {
|
|
746
|
+
const onHeld = o.onHeld ?? t.onHeld;
|
|
747
|
+
onHeld?.(held);
|
|
748
|
+
if (o.wait === false || t.hitlNoWait) throw new PendingInterventionError(held);
|
|
749
|
+
const i = await waitIntervention(t, held.id, opts);
|
|
750
|
+
if (!isInterventionApproved(i)) throw new InterventionRejectedError(i);
|
|
751
|
+
if (i.result?.error) throw new HeldActionError(i);
|
|
752
|
+
return i;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/takeover.ts
|
|
756
|
+
async function retryLocked(t, call, wait, opts) {
|
|
757
|
+
for (; ; ) {
|
|
758
|
+
try {
|
|
759
|
+
return await call();
|
|
760
|
+
} catch (err) {
|
|
761
|
+
if (wait === false || !isLocked(err) || !err.interventionId) throw err;
|
|
762
|
+
await waitIntervention(t, err.interventionId, opts);
|
|
763
|
+
opts.signal?.throwIfAborted();
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// src/environments.ts
|
|
769
|
+
var MAX_PAGE_LIMIT2 = 200;
|
|
370
770
|
var EnvironmentsService = class {
|
|
371
771
|
constructor(t) {
|
|
372
772
|
this.t = t;
|
|
@@ -380,7 +780,7 @@ var EnvironmentsService = class {
|
|
|
380
780
|
let cursor = options.cursor;
|
|
381
781
|
for (; ; ) {
|
|
382
782
|
const page = await this.listPage(
|
|
383
|
-
{ ...options, limit:
|
|
783
|
+
{ ...options, limit: MAX_PAGE_LIMIT2, cursor },
|
|
384
784
|
opts
|
|
385
785
|
);
|
|
386
786
|
out.push(...page.environments);
|
|
@@ -419,14 +819,18 @@ var EnvironmentsService = class {
|
|
|
419
819
|
signal: opts.signal
|
|
420
820
|
});
|
|
421
821
|
}
|
|
422
|
-
/** Fork an environment into a new one; returns the new environment.
|
|
822
|
+
/** Fork an environment into a new one; returns the new environment. When a
|
|
823
|
+
* hitl gate holds the fork, it waits for the decision and returns the
|
|
824
|
+
* environment the orchestrator then forked. */
|
|
423
825
|
async fork(vmId, body = {}, opts = {}) {
|
|
424
826
|
requireArg(vmId, "vm id");
|
|
425
|
-
|
|
827
|
+
const out = await this.t.gated(
|
|
426
828
|
"POST",
|
|
427
829
|
`/v1/environments/${encodeURIComponent(vmId)}`,
|
|
428
830
|
{ query: { action: "fork" }, body, signal: opts.signal }
|
|
429
831
|
);
|
|
832
|
+
if (!out.held) return out.value;
|
|
833
|
+
return this.heldEnvironment(out.held, opts);
|
|
430
834
|
}
|
|
431
835
|
/**
|
|
432
836
|
* Migrate an environment to another host; returns the new environment. The
|
|
@@ -434,11 +838,22 @@ var EnvironmentsService = class {
|
|
|
434
838
|
*/
|
|
435
839
|
async migrate(vmId, body = {}, opts = {}) {
|
|
436
840
|
requireArg(vmId, "vm id");
|
|
437
|
-
|
|
841
|
+
const out = await this.t.gated(
|
|
438
842
|
"POST",
|
|
439
843
|
`/v1/environments/${encodeURIComponent(vmId)}`,
|
|
440
844
|
{ query: { action: "migrate" }, body, signal: opts.signal }
|
|
441
845
|
);
|
|
846
|
+
if (!out.held) return out.value;
|
|
847
|
+
return this.heldEnvironment(out.held, opts);
|
|
848
|
+
}
|
|
849
|
+
/** heldEnvironment waits out a held fork or migrate and returns the
|
|
850
|
+
* environment it produced. */
|
|
851
|
+
async heldEnvironment(held, opts) {
|
|
852
|
+
const i = await settleHeld(this.t, held, {}, opts);
|
|
853
|
+
if (!i.result?.env_id) {
|
|
854
|
+
throw new FuseError(`intervention ${i.id} settled without an environment`);
|
|
855
|
+
}
|
|
856
|
+
return this.get(i.result.env_id, opts);
|
|
442
857
|
}
|
|
443
858
|
/**
|
|
444
859
|
* Run a command inside a running environment's guest and return its exit code
|
|
@@ -449,6 +864,16 @@ var EnvironmentsService = class {
|
|
|
449
864
|
*
|
|
450
865
|
* Exec requires the master token.
|
|
451
866
|
*
|
|
867
|
+
* When a hitl gate holds the exec, exec waits for the decision (bounded
|
|
868
|
+
* only by `opts.signal`); the orchestrator runs the approved (possibly
|
|
869
|
+
* edited) argv and exec returns its result. A rejection throws an
|
|
870
|
+
* InterventionRejectedError, a deny gate an error isHitlDenied matches.
|
|
871
|
+
* `wait: false` throws a PendingInterventionError at once instead.
|
|
872
|
+
*
|
|
873
|
+
* While a human holds a takeover (or an ask-mode stall is pending) the
|
|
874
|
+
* server answers 423; exec waits for that intervention to settle and runs
|
|
875
|
+
* the command then. `wait: false` throws a LockedError at once instead.
|
|
876
|
+
*
|
|
452
877
|
* @example
|
|
453
878
|
* const out = await client.environments.exec(id, { cmd: ["ls", "-l"] });
|
|
454
879
|
* if (out.exit_code !== 0) console.error(out.stderr);
|
|
@@ -463,11 +888,23 @@ var EnvironmentsService = class {
|
|
|
463
888
|
if (hasCmd && hasShell) {
|
|
464
889
|
throw new FuseError("cmd and shell are mutually exclusive");
|
|
465
890
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
891
|
+
const { wait, onHeld, ...wire } = body;
|
|
892
|
+
const out = await retryLocked(
|
|
893
|
+
this.t,
|
|
894
|
+
() => this.t.gated("POST", `/v1/environments/${encodeURIComponent(vmId)}`, {
|
|
895
|
+
query: { action: "exec" },
|
|
896
|
+
body: wire,
|
|
897
|
+
signal: opts.signal
|
|
898
|
+
}),
|
|
899
|
+
wait,
|
|
900
|
+
opts
|
|
470
901
|
);
|
|
902
|
+
if (!out.held) return out.value;
|
|
903
|
+
const i = await settleHeld(this.t, out.held, { wait, onHeld }, opts);
|
|
904
|
+
if (!i.result?.exec) {
|
|
905
|
+
throw new FuseError(`intervention ${i.id} settled without an exec result`);
|
|
906
|
+
}
|
|
907
|
+
return i.result.exec;
|
|
471
908
|
}
|
|
472
909
|
/**
|
|
473
910
|
* Relay one computer-use action to the environment's desktop and return the
|
|
@@ -475,6 +912,13 @@ var EnvironmentsService = class {
|
|
|
475
912
|
* booted from a desktop image; on any other image the server answers 503
|
|
476
913
|
* with a reason.
|
|
477
914
|
*
|
|
915
|
+
* Like exec, it waits out a takeover or a pending stall (a 423) and then
|
|
916
|
+
* acts; `opts.wait: false` throws a LockedError at once instead. When a
|
|
917
|
+
* computer gate holds the action (a 202), it waits for the decision and
|
|
918
|
+
* returns what the orchestrator got performing it (with the human's edited
|
|
919
|
+
* text, if any); a rejection throws an InterventionRejectedError, and
|
|
920
|
+
* `opts.wait: false` throws a PendingInterventionError at once instead.
|
|
921
|
+
*
|
|
478
922
|
* @example
|
|
479
923
|
* const res = await client.environments.computer(id, {
|
|
480
924
|
* action: "left_click",
|
|
@@ -484,11 +928,7 @@ var EnvironmentsService = class {
|
|
|
484
928
|
async computer(vmId, action, opts = {}) {
|
|
485
929
|
requireArg(vmId, "vm id");
|
|
486
930
|
requireArg(action?.action, "action");
|
|
487
|
-
return this.
|
|
488
|
-
"POST",
|
|
489
|
-
`/v1/environments/${encodeURIComponent(vmId)}/computer`,
|
|
490
|
-
{ body: action, signal: opts.signal }
|
|
491
|
-
);
|
|
931
|
+
return this.postComputer(vmId, action, opts);
|
|
492
932
|
}
|
|
493
933
|
/**
|
|
494
934
|
* Execute one computer tool_use against the environment and shape the
|
|
@@ -499,7 +939,7 @@ var EnvironmentsService = class {
|
|
|
499
939
|
* An action the environment refuses (a malformed coordinate, a display
|
|
500
940
|
* that is not up) resolves to `is_error` content rather than throwing: the
|
|
501
941
|
* 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 —
|
|
942
|
+
* 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
943
|
* transport, auth, an unknown environment.
|
|
504
944
|
*
|
|
505
945
|
* @example
|
|
@@ -521,18 +961,22 @@ var EnvironmentsService = class {
|
|
|
521
961
|
}
|
|
522
962
|
let res;
|
|
523
963
|
try {
|
|
524
|
-
res = await this.
|
|
525
|
-
"POST",
|
|
526
|
-
`/v1/environments/${encodeURIComponent(vmId)}/computer`,
|
|
527
|
-
{ body: input, signal: opts.signal }
|
|
528
|
-
);
|
|
964
|
+
res = await this.postComputer(vmId, input, opts);
|
|
529
965
|
} catch (err) {
|
|
530
|
-
if (isFuseApiError(err) && (err.status === 400 || err.status === 409 || err.status === 503)) {
|
|
966
|
+
if (isFuseApiError(err) && (err.status === 400 || err.status === 409 || err.status === 503 || isHitlDenied(err))) {
|
|
531
967
|
return {
|
|
532
968
|
is_error: true,
|
|
533
969
|
content: [{ type: "text", text: err.message || "the computer action failed" }]
|
|
534
970
|
};
|
|
535
971
|
}
|
|
972
|
+
if (err instanceof InterventionRejectedError) {
|
|
973
|
+
return {
|
|
974
|
+
is_error: true,
|
|
975
|
+
content: [
|
|
976
|
+
{ type: "text", text: `a human rejected this action: ${err.message}` }
|
|
977
|
+
]
|
|
978
|
+
};
|
|
979
|
+
}
|
|
536
980
|
throw err;
|
|
537
981
|
}
|
|
538
982
|
const content = [];
|
|
@@ -550,6 +994,137 @@ var EnvironmentsService = class {
|
|
|
550
994
|
if (content.length === 0) content.push({ type: "text", text: "done" });
|
|
551
995
|
return { content };
|
|
552
996
|
}
|
|
997
|
+
async postComputer(vmId, body, opts) {
|
|
998
|
+
const out = await retryLocked(
|
|
999
|
+
this.t,
|
|
1000
|
+
() => this.t.gated(
|
|
1001
|
+
"POST",
|
|
1002
|
+
`/v1/environments/${encodeURIComponent(vmId)}/computer`,
|
|
1003
|
+
{ body, signal: opts.signal }
|
|
1004
|
+
),
|
|
1005
|
+
opts.wait,
|
|
1006
|
+
opts
|
|
1007
|
+
);
|
|
1008
|
+
if (!out.held) return out.value;
|
|
1009
|
+
const i = await settleHeld(
|
|
1010
|
+
this.t,
|
|
1011
|
+
out.held,
|
|
1012
|
+
{ wait: opts.wait, onHeld: opts.onHeld },
|
|
1013
|
+
opts
|
|
1014
|
+
);
|
|
1015
|
+
if (!i.result?.computer) {
|
|
1016
|
+
throw new FuseError(`intervention ${i.id} settled without a computer result`);
|
|
1017
|
+
}
|
|
1018
|
+
return i.result.computer;
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Ask a human for something the environment's hitl policy declares. By
|
|
1022
|
+
* default it waits (by long-polling) until the intervention resolves
|
|
1023
|
+
* (answered, rejected, expired or cancelled); read the result off the
|
|
1024
|
+
* returned intervention's decision. Pass `wait: false` to get the pending
|
|
1025
|
+
* handle instead. Pass `opts.signal` to stop waiting early.
|
|
1026
|
+
*
|
|
1027
|
+
* @example
|
|
1028
|
+
* const res = await client.environments.requestHuman(id, {
|
|
1029
|
+
* question: "deploy to prod?",
|
|
1030
|
+
* choices: ["yes", "no"],
|
|
1031
|
+
* });
|
|
1032
|
+
* if (isInterventionApproved(res)) console.log(res.decision?.answer);
|
|
1033
|
+
*/
|
|
1034
|
+
async requestHuman(vmId, req, opts = {}) {
|
|
1035
|
+
requireArg(vmId, "vm id");
|
|
1036
|
+
const pending = await this.t.json(
|
|
1037
|
+
"POST",
|
|
1038
|
+
`/v1/environments/${encodeURIComponent(vmId)}`,
|
|
1039
|
+
{
|
|
1040
|
+
query: { action: "request" },
|
|
1041
|
+
body: {
|
|
1042
|
+
kind: req.kind || "ask",
|
|
1043
|
+
question: req.question || void 0,
|
|
1044
|
+
choices: req.choices,
|
|
1045
|
+
target: req.target || void 0,
|
|
1046
|
+
message: req.message || void 0,
|
|
1047
|
+
via: req.via || void 0,
|
|
1048
|
+
reason: req.reason || void 0,
|
|
1049
|
+
path: req.path || void 0,
|
|
1050
|
+
content: req.content || void 0,
|
|
1051
|
+
format: req.format || void 0,
|
|
1052
|
+
timeout_seconds: req.timeoutSeconds || void 0
|
|
1053
|
+
},
|
|
1054
|
+
signal: opts.signal
|
|
1055
|
+
}
|
|
1056
|
+
);
|
|
1057
|
+
if (req.wait === false) return pending;
|
|
1058
|
+
return waitIntervention(this.t, pending.id, opts);
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Submit a guest file or a text/diff blob for a human to approve or reject
|
|
1062
|
+
* with comments, and wait for the verdict. Needs hitl.requests.review.
|
|
1063
|
+
* With `wait: false` it returns at once with the pending intervention and
|
|
1064
|
+
* approved false.
|
|
1065
|
+
*
|
|
1066
|
+
* @example
|
|
1067
|
+
* const r = await client.environments.requestReview(id, {
|
|
1068
|
+
* content: diff,
|
|
1069
|
+
* format: ReviewFormat.Diff,
|
|
1070
|
+
* });
|
|
1071
|
+
* if (!r.approved) console.log(r.comments);
|
|
1072
|
+
*/
|
|
1073
|
+
async requestReview(vmId, req, opts = {}) {
|
|
1074
|
+
const i = await this.requestHuman(
|
|
1075
|
+
vmId,
|
|
1076
|
+
{
|
|
1077
|
+
kind: HitlKind.Review,
|
|
1078
|
+
path: req.path,
|
|
1079
|
+
content: req.content,
|
|
1080
|
+
format: req.format,
|
|
1081
|
+
message: req.message,
|
|
1082
|
+
timeoutSeconds: req.timeoutSeconds,
|
|
1083
|
+
wait: req.wait
|
|
1084
|
+
},
|
|
1085
|
+
opts
|
|
1086
|
+
);
|
|
1087
|
+
return {
|
|
1088
|
+
approved: isInterventionApproved(i),
|
|
1089
|
+
comments: i.decision?.reason ?? "",
|
|
1090
|
+
intervention: i
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Take over the environment as a human: claims the agent's pending
|
|
1095
|
+
* takeover request if there is one, else opens a new one. While held, the
|
|
1096
|
+
* agent's exec and computer calls wait. Returns the held intervention.
|
|
1097
|
+
* Needs the hitl:takeover scope; a conflict when someone else holds it.
|
|
1098
|
+
*/
|
|
1099
|
+
async takeover(vmId, body = {}, opts = {}) {
|
|
1100
|
+
requireArg(vmId, "vm id");
|
|
1101
|
+
return this.t.json(
|
|
1102
|
+
"POST",
|
|
1103
|
+
`/v1/environments/${encodeURIComponent(vmId)}`,
|
|
1104
|
+
{
|
|
1105
|
+
query: { action: "takeover" },
|
|
1106
|
+
body: { via: body.via || void 0, reason: body.reason || void 0 },
|
|
1107
|
+
signal: opts.signal
|
|
1108
|
+
}
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* End a takeover and hand the environment back to the agent. The note is
|
|
1113
|
+
* the takeover's answer. Returns the approved intervention; a conflict when
|
|
1114
|
+
* nothing is held.
|
|
1115
|
+
*/
|
|
1116
|
+
async release(vmId, body = {}, opts = {}) {
|
|
1117
|
+
requireArg(vmId, "vm id");
|
|
1118
|
+
return this.t.json(
|
|
1119
|
+
"POST",
|
|
1120
|
+
`/v1/environments/${encodeURIComponent(vmId)}`,
|
|
1121
|
+
{
|
|
1122
|
+
query: { action: "release" },
|
|
1123
|
+
body: { note: body.note || void 0 },
|
|
1124
|
+
signal: opts.signal
|
|
1125
|
+
}
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
553
1128
|
/** Report whether the environment has a live display and at what geometry.
|
|
554
1129
|
* up is false with a reason on an image with no desktop. */
|
|
555
1130
|
async computerDisplay(vmId, opts = {}) {
|
|
@@ -563,31 +1138,42 @@ var EnvironmentsService = class {
|
|
|
563
1138
|
/**
|
|
564
1139
|
* Phase-1 only: quiesces the guest workload and moves the environment to
|
|
565
1140
|
* "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)
|
|
1141
|
+
* destroy() afterward to remove it. A drained environment left
|
|
1142
|
+
* undestroyed keeps running (and billing) until the orchestrator's
|
|
1143
|
+
* draining timeout (5m30s by default) passes, and then the orchestrator
|
|
1144
|
+
* destroys it.
|
|
568
1145
|
*/
|
|
569
1146
|
async drain(vmId, opts = {}) {
|
|
570
1147
|
requireArg(vmId, "vm id");
|
|
571
|
-
|
|
1148
|
+
const out = await this.t.gated(
|
|
572
1149
|
"POST",
|
|
573
1150
|
`/v1/environments/${encodeURIComponent(vmId)}`,
|
|
574
1151
|
{ query: { action: "drain" }, signal: opts.signal }
|
|
575
1152
|
);
|
|
1153
|
+
if (!out.held) return out.value;
|
|
1154
|
+
await settleHeld(this.t, out.held, {}, opts);
|
|
1155
|
+
return this.get(vmId, opts);
|
|
576
1156
|
}
|
|
577
1157
|
/** Rotate the per-VM credentials and push them to the guest. */
|
|
578
1158
|
async rotateToken(vmId, opts = {}) {
|
|
579
1159
|
requireArg(vmId, "vm id");
|
|
580
|
-
await this.t.
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
1160
|
+
const held = await this.t.gatedNoContent(
|
|
1161
|
+
"POST",
|
|
1162
|
+
`/v1/environments/${encodeURIComponent(vmId)}`,
|
|
1163
|
+
{ query: { action: "rotate-token" }, signal: opts.signal }
|
|
1164
|
+
);
|
|
1165
|
+
if (held) await settleHeld(this.t, held, {}, opts);
|
|
584
1166
|
}
|
|
585
|
-
/** Forcefully destroy an environment. Idempotent.
|
|
1167
|
+
/** Forcefully destroy an environment. Idempotent. When a hitl gate holds
|
|
1168
|
+
* the destroy, it waits for the decision. */
|
|
586
1169
|
async destroy(vmId, opts = {}) {
|
|
587
1170
|
requireArg(vmId, "vm id");
|
|
588
|
-
await this.t.
|
|
589
|
-
|
|
590
|
-
|
|
1171
|
+
const held = await this.t.gatedNoContent(
|
|
1172
|
+
"DELETE",
|
|
1173
|
+
`/v1/environments/${encodeURIComponent(vmId)}`,
|
|
1174
|
+
{ signal: opts.signal }
|
|
1175
|
+
);
|
|
1176
|
+
if (held) await settleHeld(this.t, held, {}, opts);
|
|
591
1177
|
}
|
|
592
1178
|
/**
|
|
593
1179
|
* Open the SSE event stream for an environment. The returned promise rejects
|
|
@@ -608,7 +1194,7 @@ var EnvironmentsService = class {
|
|
|
608
1194
|
};
|
|
609
1195
|
|
|
610
1196
|
// src/snapshots.ts
|
|
611
|
-
var
|
|
1197
|
+
var MAX_PAGE_LIMIT3 = 200;
|
|
612
1198
|
var SnapshotsService = class {
|
|
613
1199
|
constructor(t) {
|
|
614
1200
|
this.t = t;
|
|
@@ -623,14 +1209,23 @@ var SnapshotsService = class {
|
|
|
623
1209
|
* be `"disk"` even for a `live: true` request that reached a host too old to
|
|
624
1210
|
* honour it. A caller that depends on the memory being there should check
|
|
625
1211
|
* it. `live: true` against a backend with no live-snapshot support throws a
|
|
626
|
-
* 501 `unimplemented`, not a conflict.
|
|
1212
|
+
* 501 `unimplemented`, not a conflict.
|
|
1213
|
+
*
|
|
1214
|
+
* When a hitl gate holds the snapshot, create waits for the decision and
|
|
1215
|
+
* returns the snapshot the orchestrator then took. */
|
|
627
1216
|
async create(vmId, body = {}, opts = {}) {
|
|
628
1217
|
requireArg(vmId, "vm id");
|
|
629
|
-
|
|
1218
|
+
const out = await this.t.gated(
|
|
630
1219
|
"POST",
|
|
631
1220
|
`/v1/environments/${encodeURIComponent(vmId)}/snapshots`,
|
|
632
1221
|
{ body, signal: opts.signal }
|
|
633
1222
|
);
|
|
1223
|
+
if (!out.held) return out.value;
|
|
1224
|
+
const i = await settleHeld(this.t, out.held, {}, opts);
|
|
1225
|
+
if (!i.result?.snapshot_id) {
|
|
1226
|
+
throw new FuseError(`intervention ${i.id} settled without a snapshot`);
|
|
1227
|
+
}
|
|
1228
|
+
return this.get(i.result.snapshot_id, opts);
|
|
634
1229
|
}
|
|
635
1230
|
/** List snapshots, optionally filtered. Transparently walks every result
|
|
636
1231
|
* page. For explicit single-page control (e.g. a cursor from a previous
|
|
@@ -640,7 +1235,7 @@ var SnapshotsService = class {
|
|
|
640
1235
|
let cursor = options.cursor;
|
|
641
1236
|
for (; ; ) {
|
|
642
1237
|
const page = await this.listPage(
|
|
643
|
-
{ ...options, limit:
|
|
1238
|
+
{ ...options, limit: MAX_PAGE_LIMIT3, cursor },
|
|
644
1239
|
opts
|
|
645
1240
|
);
|
|
646
1241
|
out.push(...page.snapshots);
|
|
@@ -718,7 +1313,7 @@ var SnapshotsService = class {
|
|
|
718
1313
|
};
|
|
719
1314
|
|
|
720
1315
|
// src/hosts.ts
|
|
721
|
-
var
|
|
1316
|
+
var MAX_PAGE_LIMIT4 = 200;
|
|
722
1317
|
var HostsService = class {
|
|
723
1318
|
constructor(t) {
|
|
724
1319
|
this.t = t;
|
|
@@ -735,7 +1330,7 @@ var HostsService = class {
|
|
|
735
1330
|
const out = [];
|
|
736
1331
|
let cursor;
|
|
737
1332
|
for (; ; ) {
|
|
738
|
-
const page = await this.listPage({ limit:
|
|
1333
|
+
const page = await this.listPage({ limit: MAX_PAGE_LIMIT4, cursor }, opts);
|
|
739
1334
|
out.push(...page.hosts);
|
|
740
1335
|
if (!page.nextCursor) break;
|
|
741
1336
|
cursor = page.nextCursor;
|
|
@@ -791,12 +1386,19 @@ var ApiKeysService = class {
|
|
|
791
1386
|
}
|
|
792
1387
|
t;
|
|
793
1388
|
/**
|
|
794
|
-
* Issue a new API key.
|
|
795
|
-
* unrecoverable afterward.
|
|
1389
|
+
* Issue a new API key. Pass a label, or options for a scoped key. The raw
|
|
1390
|
+
* secret is returned once in `key` and is unrecoverable afterward.
|
|
796
1391
|
*/
|
|
797
|
-
async create(
|
|
1392
|
+
async create(labelOrOptions, opts = {}) {
|
|
1393
|
+
const o = typeof labelOrOptions === "string" ? { label: labelOrOptions } : labelOrOptions ?? {};
|
|
798
1394
|
return this.t.json("POST", "/v1/api-keys", {
|
|
799
|
-
body:
|
|
1395
|
+
body: {
|
|
1396
|
+
label: o.label || void 0,
|
|
1397
|
+
scopes: o.scopes,
|
|
1398
|
+
env_id: o.envId || void 0,
|
|
1399
|
+
ttl_seconds: o.ttlSeconds || void 0,
|
|
1400
|
+
actor: o.actor
|
|
1401
|
+
},
|
|
800
1402
|
signal: opts.signal
|
|
801
1403
|
});
|
|
802
1404
|
}
|
|
@@ -817,15 +1419,18 @@ var ApiKeysService = class {
|
|
|
817
1419
|
};
|
|
818
1420
|
|
|
819
1421
|
// src/version.ts
|
|
820
|
-
var VERSION = "0.
|
|
1422
|
+
var VERSION = "0.41.0";
|
|
821
1423
|
|
|
822
1424
|
// src/client.ts
|
|
823
|
-
var FuseClient = class {
|
|
1425
|
+
var FuseClient = class _FuseClient {
|
|
824
1426
|
environments;
|
|
825
1427
|
snapshots;
|
|
826
1428
|
hosts;
|
|
827
1429
|
apiKeys;
|
|
1430
|
+
interventions;
|
|
1431
|
+
options;
|
|
828
1432
|
constructor(options) {
|
|
1433
|
+
this.options = options;
|
|
829
1434
|
const transport = new Transport({
|
|
830
1435
|
baseUrl: options.baseUrl,
|
|
831
1436
|
token: options.token,
|
|
@@ -833,15 +1438,26 @@ var FuseClient = class {
|
|
|
833
1438
|
userAgent: options.userAgent ?? `fuse-ts/${VERSION}`,
|
|
834
1439
|
requestId: options.requestId,
|
|
835
1440
|
timeoutMs: options.timeoutMs,
|
|
836
|
-
headers: options.headers
|
|
1441
|
+
headers: options.headers,
|
|
1442
|
+
actor: options.actor,
|
|
1443
|
+
hitlNoWait: options.hitlNoWait,
|
|
1444
|
+
onHeld: options.onHeld
|
|
837
1445
|
});
|
|
838
1446
|
this.environments = new EnvironmentsService(transport);
|
|
839
1447
|
this.snapshots = new SnapshotsService(transport);
|
|
840
1448
|
this.hosts = new HostsService(transport);
|
|
841
1449
|
this.apiKeys = new ApiKeysService(transport);
|
|
1450
|
+
this.interventions = new InterventionsService(transport);
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1453
|
+
* withActor returns a copy of this client, with the same options otherwise,
|
|
1454
|
+
* that sends `actor` as the Fuse-Actor headers. This client is unchanged.
|
|
1455
|
+
*/
|
|
1456
|
+
withActor(actor) {
|
|
1457
|
+
return new _FuseClient({ ...this.options, actor });
|
|
842
1458
|
}
|
|
843
1459
|
};
|
|
844
1460
|
|
|
845
|
-
export { ApiKeysService, EnvironmentsService, ErrorCode, FuseApiError, FuseClient, FuseError, HealthState, HostsService, SnapshotsService, State, VERSION, isConflict, isFuseApiError, isInvalidArgument, isNotFound, isTerminalState, isUnauthorized, isUnavailable };
|
|
1461
|
+
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
1462
|
//# sourceMappingURL=index.js.map
|
|
847
1463
|
//# sourceMappingURL=index.js.map
|