@nifrajs/agent 3.1.0 → 3.2.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.
@@ -0,0 +1,621 @@
1
+ /**
2
+ * Provider-neutral deployment lifecycle contracts.
3
+ *
4
+ * A deployment adapter is an execution seam, not a sandbox claim. The host owns activation,
5
+ * authority, cancellation, and cleanup. Public reference adapters may be local, CI, or replay, but
6
+ * only a capability report declaring real OS isolation can admit a hostile-code plan.
7
+ */
8
+
9
+ const TOKEN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/
10
+ const MAX_LIMIT = 10_000_000_000
11
+
12
+ export type DeploymentRuntime = "local" | "ci" | "replay" | "worker" | "unknown"
13
+ export type DeploymentNetwork = "none" | "outbound" | "inbound" | "unrestricted"
14
+ export type DeploymentFilesystem = "none" | "workspace" | "host"
15
+ export type DeploymentProcess = "none" | "child"
16
+ export type DeploymentSecrets = "none" | "caller"
17
+ export type DeploymentCancellation = "none" | "cooperative" | "forced"
18
+ export type HostileCodeIsolation = "none" | "os"
19
+
20
+ export interface DeploymentCapabilities {
21
+ readonly runtime: DeploymentRuntime
22
+ readonly network: DeploymentNetwork
23
+ readonly filesystem: DeploymentFilesystem
24
+ readonly process: DeploymentProcess
25
+ readonly secrets: DeploymentSecrets
26
+ readonly workspace: {
27
+ readonly mode: "none" | "scoped"
28
+ readonly maxBytes: number
29
+ }
30
+ readonly cancellation: DeploymentCancellation
31
+ readonly hostileCodeIsolation: HostileCodeIsolation
32
+ }
33
+
34
+ export interface DeploymentCapabilityReport {
35
+ readonly schemaVersion: 1
36
+ readonly adapterId: string
37
+ readonly capabilities: DeploymentCapabilities
38
+ readonly limitations: readonly string[]
39
+ }
40
+
41
+ export interface DeploymentAuthority {
42
+ readonly workspaceMaxBytes: number
43
+ readonly deadlineAt?: number
44
+ readonly cancellation: DeploymentCancellation
45
+ readonly hostileCodeIsolation: HostileCodeIsolation
46
+ }
47
+
48
+ export interface DeploymentActivationOptions {
49
+ /** Host-owned approval obtained from a separately reviewed/certified isolation decision. */
50
+ readonly approved?: boolean
51
+ }
52
+
53
+ export interface AgentDeploymentPlan {
54
+ readonly deploymentId: string
55
+ readonly hostileCode?: boolean
56
+ readonly workspaceMaxBytes?: number
57
+ readonly deadlineAt?: number
58
+ }
59
+
60
+ export interface DeploymentPrepareRequest {
61
+ readonly plan: AgentDeploymentPlan
62
+ readonly authority: DeploymentAuthority
63
+ readonly signal: AbortSignal
64
+ }
65
+
66
+ export interface DeploymentPrepareResult {
67
+ readonly deploymentId: string
68
+ readonly state: "prepared"
69
+ readonly preparedRef: string
70
+ }
71
+
72
+ export interface DeploymentStartRequest {
73
+ readonly deploymentId: string
74
+ readonly preparedRef: string
75
+ readonly authority: DeploymentAuthority
76
+ readonly signal: AbortSignal
77
+ }
78
+
79
+ export interface DeploymentStartResult {
80
+ readonly deploymentId: string
81
+ readonly state: "running"
82
+ readonly handleRef: string
83
+ }
84
+
85
+ export interface DeploymentInspectRequest {
86
+ readonly deploymentId: string
87
+ readonly handleRef?: string
88
+ readonly signal: AbortSignal
89
+ }
90
+
91
+ export type DeploymentState = "prepared" | "running" | "cancelled" | "disposed"
92
+
93
+ export interface DeploymentInspection {
94
+ readonly deploymentId: string
95
+ readonly state: DeploymentState
96
+ readonly handleRef?: string
97
+ }
98
+
99
+ export interface DeploymentCancelRequest {
100
+ readonly deploymentId: string
101
+ readonly preparedRef?: string
102
+ readonly handleRef?: string
103
+ readonly signal: AbortSignal
104
+ }
105
+
106
+ export interface DeploymentCancelResult {
107
+ readonly deploymentId: string
108
+ readonly state: "cancelled"
109
+ }
110
+
111
+ export interface DeploymentDisposeRequest {
112
+ readonly deploymentId: string
113
+ readonly preparedRef?: string
114
+ readonly handleRef?: string
115
+ readonly signal: AbortSignal
116
+ }
117
+
118
+ export interface DeploymentDisposeResult {
119
+ readonly deploymentId: string
120
+ readonly state: "disposed"
121
+ }
122
+
123
+ export interface AgentDeploymentAdapter {
124
+ readonly id: string
125
+ capabilityReport(): DeploymentCapabilityReport | PromiseLike<DeploymentCapabilityReport>
126
+ prepare(
127
+ request: DeploymentPrepareRequest,
128
+ ): DeploymentPrepareResult | PromiseLike<DeploymentPrepareResult>
129
+ start(request: DeploymentStartRequest): DeploymentStartResult | PromiseLike<DeploymentStartResult>
130
+ inspect(
131
+ request: DeploymentInspectRequest,
132
+ ): DeploymentInspection | PromiseLike<DeploymentInspection>
133
+ cancel(
134
+ request: DeploymentCancelRequest,
135
+ ): DeploymentCancelResult | PromiseLike<DeploymentCancelResult>
136
+ dispose(
137
+ request: DeploymentDisposeRequest,
138
+ ): DeploymentDisposeResult | PromiseLike<DeploymentDisposeResult>
139
+ }
140
+
141
+ export type DeploymentEvidence = {
142
+ readonly kind: "prepared" | "started" | "inspected" | "cancelled" | "disposed" | "failed"
143
+ readonly deploymentId: string
144
+ readonly state: DeploymentState | "new"
145
+ readonly code?: DeploymentErrorCode
146
+ }
147
+
148
+ export type DeploymentErrorCode =
149
+ | "invalid_capabilities"
150
+ | "capability_denied"
151
+ | "hostile_code_requires_isolation"
152
+ | "authority_expanded"
153
+ | "invalid_plan"
154
+ | "invalid_transition"
155
+ | "malformed_callback"
156
+ | "adapter_error"
157
+ | "cancelled"
158
+
159
+ export class DeploymentError extends Error {
160
+ constructor(readonly code: DeploymentErrorCode) {
161
+ super(`deployment ${code}`)
162
+ this.name = "DeploymentError"
163
+ }
164
+ }
165
+
166
+ function isRecord(value: unknown): value is Record<string, unknown> {
167
+ return typeof value === "object" && value !== null && !Array.isArray(value)
168
+ }
169
+
170
+ function boundedToken(
171
+ value: unknown,
172
+ _label: string,
173
+ code: DeploymentErrorCode = "invalid_plan",
174
+ ): string {
175
+ if (typeof value !== "string" || !TOKEN.test(value)) throw new DeploymentError(code)
176
+ return value
177
+ }
178
+
179
+ function boundedLimit(
180
+ value: unknown,
181
+ _label: string,
182
+ code: DeploymentErrorCode = "invalid_plan",
183
+ ): number {
184
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0 || value > MAX_LIMIT)
185
+ throw new DeploymentError(code)
186
+ return value
187
+ }
188
+
189
+ function exactKeys(
190
+ value: Record<string, unknown>,
191
+ keys: ReadonlySet<string>,
192
+ code: DeploymentErrorCode,
193
+ ): void {
194
+ for (const key of Object.keys(value)) if (!keys.has(key)) throw new DeploymentError(code)
195
+ }
196
+
197
+ const RUNTIMES: ReadonlySet<string> = new Set(["local", "ci", "replay", "worker", "unknown"])
198
+ const NETWORKS: ReadonlySet<string> = new Set(["none", "outbound", "inbound", "unrestricted"])
199
+ const FILESYSTEMS: ReadonlySet<string> = new Set(["none", "workspace", "host"])
200
+ const PROCESSES: ReadonlySet<string> = new Set(["none", "child"])
201
+ const SECRETS: ReadonlySet<string> = new Set(["none", "caller"])
202
+ const CANCELLATIONS: ReadonlySet<string> = new Set(["none", "cooperative", "forced"])
203
+ const ISOLATIONS: ReadonlySet<string> = new Set(["none", "os"])
204
+
205
+ function member<T extends string>(value: unknown, values: ReadonlySet<string>): T {
206
+ if (typeof value !== "string" || !values.has(value))
207
+ throw new DeploymentError("invalid_capabilities")
208
+ return value as T
209
+ }
210
+
211
+ /** Parse an untrusted capability report before the host admits an adapter. */
212
+ export function parseDeploymentCapabilityReport(value: unknown): DeploymentCapabilityReport {
213
+ if (!isRecord(value)) throw new DeploymentError("invalid_capabilities")
214
+ exactKeys(
215
+ value,
216
+ new Set(["schemaVersion", "adapterId", "capabilities", "limitations"]),
217
+ "invalid_capabilities",
218
+ )
219
+ if (value.schemaVersion !== 1) throw new DeploymentError("invalid_capabilities")
220
+ const adapterId = boundedToken(value.adapterId, "adapterId", "invalid_capabilities")
221
+ if (!isRecord(value.capabilities)) throw new DeploymentError("invalid_capabilities")
222
+ const capabilities = value.capabilities
223
+ exactKeys(
224
+ capabilities,
225
+ new Set([
226
+ "runtime",
227
+ "network",
228
+ "filesystem",
229
+ "process",
230
+ "secrets",
231
+ "workspace",
232
+ "cancellation",
233
+ "hostileCodeIsolation",
234
+ ]),
235
+ "invalid_capabilities",
236
+ )
237
+ if (!isRecord(capabilities.workspace)) throw new DeploymentError("invalid_capabilities")
238
+ exactKeys(capabilities.workspace, new Set(["mode", "maxBytes"]), "invalid_capabilities")
239
+ const workspaceMode = member<"none" | "scoped">(
240
+ capabilities.workspace.mode,
241
+ new Set(["none", "scoped"]),
242
+ )
243
+ const maxBytes = boundedLimit(
244
+ capabilities.workspace.maxBytes,
245
+ "workspace.maxBytes",
246
+ "invalid_capabilities",
247
+ )
248
+ if (workspaceMode === "none" && maxBytes !== 0) throw new DeploymentError("invalid_capabilities")
249
+ const limitations = value.limitations
250
+ if (
251
+ !Array.isArray(limitations) ||
252
+ limitations.length > 32 ||
253
+ limitations.some((item) => typeof item !== "string" || !TOKEN.test(item))
254
+ )
255
+ throw new DeploymentError("invalid_capabilities")
256
+ return Object.freeze({
257
+ schemaVersion: 1,
258
+ adapterId,
259
+ capabilities: Object.freeze({
260
+ runtime: member<DeploymentRuntime>(capabilities.runtime, RUNTIMES),
261
+ network: member<DeploymentNetwork>(capabilities.network, NETWORKS),
262
+ filesystem: member<DeploymentFilesystem>(capabilities.filesystem, FILESYSTEMS),
263
+ process: member<DeploymentProcess>(capabilities.process, PROCESSES),
264
+ secrets: member<DeploymentSecrets>(capabilities.secrets, SECRETS),
265
+ workspace: Object.freeze({ mode: workspaceMode, maxBytes }),
266
+ cancellation: member<DeploymentCancellation>(capabilities.cancellation, CANCELLATIONS),
267
+ hostileCodeIsolation: member<HostileCodeIsolation>(
268
+ capabilities.hostileCodeIsolation,
269
+ ISOLATIONS,
270
+ ),
271
+ }),
272
+ limitations: Object.freeze([...limitations]),
273
+ })
274
+ }
275
+
276
+ export function parseDeploymentPlan(value: unknown): AgentDeploymentPlan {
277
+ if (!isRecord(value)) throw new DeploymentError("invalid_plan")
278
+ exactKeys(
279
+ value,
280
+ new Set(["deploymentId", "hostileCode", "workspaceMaxBytes", "deadlineAt"]),
281
+ "invalid_plan",
282
+ )
283
+ const deploymentId = boundedToken(value.deploymentId, "deploymentId")
284
+ if (value.hostileCode !== undefined && typeof value.hostileCode !== "boolean")
285
+ throw new DeploymentError("invalid_plan")
286
+ const workspaceMaxBytes =
287
+ value.workspaceMaxBytes === undefined
288
+ ? undefined
289
+ : boundedLimit(value.workspaceMaxBytes, "workspaceMaxBytes", "invalid_plan")
290
+ const rawDeadlineAt = value.deadlineAt
291
+ const deadlineAt =
292
+ rawDeadlineAt === undefined
293
+ ? undefined
294
+ : typeof rawDeadlineAt === "number"
295
+ ? rawDeadlineAt
296
+ : null
297
+ if (
298
+ deadlineAt === null ||
299
+ (deadlineAt !== undefined && (!Number.isSafeInteger(deadlineAt) || deadlineAt <= 0))
300
+ )
301
+ throw new DeploymentError("invalid_plan")
302
+ return Object.freeze({
303
+ deploymentId,
304
+ ...(value.hostileCode === undefined ? {} : { hostileCode: value.hostileCode }),
305
+ ...(workspaceMaxBytes === undefined ? {} : { workspaceMaxBytes }),
306
+ ...(deadlineAt === undefined ? {} : { deadlineAt }),
307
+ })
308
+ }
309
+
310
+ function parseRef(value: unknown): string {
311
+ if (typeof value !== "string" || !TOKEN.test(value))
312
+ throw new DeploymentError("malformed_callback")
313
+ return value
314
+ }
315
+
316
+ function parseCallbackState(value: unknown, expected: DeploymentState): boolean {
317
+ return isRecord(value) && value.state === expected
318
+ }
319
+
320
+ function parsePrepare(value: unknown, deploymentId: string): DeploymentPrepareResult {
321
+ if (
322
+ !isRecord(value) ||
323
+ value.deploymentId !== deploymentId ||
324
+ !parseCallbackState(value, "prepared")
325
+ )
326
+ throw new DeploymentError("malformed_callback")
327
+ return Object.freeze({
328
+ deploymentId,
329
+ state: "prepared",
330
+ preparedRef: parseRef(value.preparedRef),
331
+ })
332
+ }
333
+
334
+ function parseStart(value: unknown, deploymentId: string): DeploymentStartResult {
335
+ if (
336
+ !isRecord(value) ||
337
+ value.deploymentId !== deploymentId ||
338
+ !parseCallbackState(value, "running")
339
+ )
340
+ throw new DeploymentError("malformed_callback")
341
+ return Object.freeze({ deploymentId, state: "running", handleRef: parseRef(value.handleRef) })
342
+ }
343
+
344
+ function parseInspection(value: unknown, deploymentId: string): DeploymentInspection {
345
+ if (!isRecord(value) || value.deploymentId !== deploymentId || typeof value.state !== "string")
346
+ throw new DeploymentError("malformed_callback")
347
+ if (!["prepared", "running", "cancelled", "disposed"].includes(value.state))
348
+ throw new DeploymentError("malformed_callback")
349
+ const handleRef = value.handleRef === undefined ? undefined : parseRef(value.handleRef)
350
+ return Object.freeze({
351
+ deploymentId,
352
+ state: value.state as DeploymentState,
353
+ ...(handleRef === undefined ? {} : { handleRef }),
354
+ })
355
+ }
356
+
357
+ function parseTerminal<State extends "cancelled" | "disposed">(
358
+ value: unknown,
359
+ deploymentId: string,
360
+ state: State,
361
+ ): { deploymentId: string; state: State } {
362
+ if (!isRecord(value) || value.deploymentId !== deploymentId || !parseCallbackState(value, state))
363
+ throw new DeploymentError("malformed_callback")
364
+ return Object.freeze({ deploymentId, state })
365
+ }
366
+
367
+ function validateAuthority(authority: DeploymentAuthority): DeploymentAuthority {
368
+ const workspaceMaxBytes = boundedLimit(
369
+ authority.workspaceMaxBytes,
370
+ "authority.workspaceMaxBytes",
371
+ "authority_expanded",
372
+ )
373
+ if (
374
+ authority.deadlineAt !== undefined &&
375
+ (!Number.isSafeInteger(authority.deadlineAt) || authority.deadlineAt <= 0)
376
+ )
377
+ throw new DeploymentError("authority_expanded")
378
+ if (!CANCELLATIONS.has(authority.cancellation) || !ISOLATIONS.has(authority.hostileCodeIsolation))
379
+ throw new DeploymentError("authority_expanded")
380
+ return Object.freeze({ ...authority, workspaceMaxBytes })
381
+ }
382
+
383
+ /** Prove a child deployment authority is a subset of its parent's authority. */
384
+ export function assertDeploymentAuthorityMonotonic(
385
+ parent: DeploymentAuthority,
386
+ child: DeploymentAuthority,
387
+ ): void {
388
+ const p = validateAuthority(parent)
389
+ const c = validateAuthority(child)
390
+ if (c.workspaceMaxBytes > p.workspaceMaxBytes) throw new DeploymentError("authority_expanded")
391
+ if (p.deadlineAt !== undefined && (c.deadlineAt === undefined || c.deadlineAt > p.deadlineAt))
392
+ throw new DeploymentError("authority_expanded")
393
+ if (p.cancellation === "none" && c.cancellation !== "none")
394
+ throw new DeploymentError("authority_expanded")
395
+ if (p.cancellation === "cooperative" && c.cancellation === "forced")
396
+ throw new DeploymentError("authority_expanded")
397
+ if (p.hostileCodeIsolation === "none" && c.hostileCodeIsolation !== "none")
398
+ throw new DeploymentError("authority_expanded")
399
+ }
400
+
401
+ function assertCapabilityPlan(
402
+ report: DeploymentCapabilityReport,
403
+ plan: AgentDeploymentPlan,
404
+ authority: DeploymentAuthority,
405
+ approved: boolean,
406
+ ): void {
407
+ const capabilities = report.capabilities
408
+ if (plan.hostileCode === true && capabilities.hostileCodeIsolation !== "os")
409
+ throw new DeploymentError("hostile_code_requires_isolation")
410
+ if (plan.hostileCode === true && authority.hostileCodeIsolation !== "os")
411
+ throw new DeploymentError("hostile_code_requires_isolation")
412
+ if (plan.hostileCode === true && !approved)
413
+ throw new DeploymentError("hostile_code_requires_isolation")
414
+ if (
415
+ plan.workspaceMaxBytes !== undefined &&
416
+ (capabilities.workspace.mode !== "scoped" ||
417
+ plan.workspaceMaxBytes > capabilities.workspace.maxBytes)
418
+ )
419
+ throw new DeploymentError("capability_denied")
420
+ if (plan.workspaceMaxBytes !== undefined && plan.workspaceMaxBytes > authority.workspaceMaxBytes)
421
+ throw new DeploymentError("authority_expanded")
422
+ if (authority.cancellation !== "none" && capabilities.cancellation === "none")
423
+ throw new DeploymentError("capability_denied")
424
+ if (
425
+ plan.deadlineAt !== undefined &&
426
+ authority.deadlineAt !== undefined &&
427
+ plan.deadlineAt > authority.deadlineAt
428
+ )
429
+ throw new DeploymentError("authority_expanded")
430
+ }
431
+
432
+ export function createDeploymentAuthority(input: {
433
+ readonly workspaceMaxBytes: number
434
+ readonly deadlineAt?: number
435
+ readonly cancellation?: DeploymentCancellation
436
+ readonly hostileCodeIsolation?: HostileCodeIsolation
437
+ }): DeploymentAuthority {
438
+ return validateAuthority({
439
+ workspaceMaxBytes: input.workspaceMaxBytes,
440
+ ...(input.deadlineAt === undefined ? {} : { deadlineAt: input.deadlineAt }),
441
+ cancellation: input.cancellation ?? "cooperative",
442
+ hostileCodeIsolation: input.hostileCodeIsolation ?? "none",
443
+ })
444
+ }
445
+
446
+ /** Host-owned deterministic lifecycle wrapper for an adapter. */
447
+ export class AgentDeployment {
448
+ readonly id: string
449
+ private deploymentId: string | undefined
450
+ private state: "new" | DeploymentState = "new"
451
+ private preparedRef: string | undefined
452
+ private handleRef: string | undefined
453
+ private report: DeploymentCapabilityReport | undefined
454
+ private readonly evidence: DeploymentEvidence[] = []
455
+
456
+ constructor(
457
+ private readonly adapter: AgentDeploymentAdapter,
458
+ authority: DeploymentAuthority,
459
+ private readonly signal: AbortSignal = new AbortController().signal,
460
+ options: DeploymentActivationOptions = {},
461
+ ) {
462
+ this.id = boundedToken(adapter.id, "adapter.id")
463
+ this.authority = validateAuthority(authority)
464
+ this.approved = options.approved === true
465
+ }
466
+
467
+ private readonly authority: DeploymentAuthority
468
+ private readonly approved: boolean
469
+
470
+ get lifecycleState(): "new" | DeploymentState {
471
+ return this.state
472
+ }
473
+
474
+ get evidenceRecords(): readonly DeploymentEvidence[] {
475
+ return Object.freeze([...this.evidence])
476
+ }
477
+
478
+ async capabilityReport(): Promise<DeploymentCapabilityReport> {
479
+ if (this.report !== undefined) return this.report
480
+ try {
481
+ this.report = parseDeploymentCapabilityReport(await this.adapter.capabilityReport())
482
+ if (this.report.adapterId !== this.id) throw new DeploymentError("invalid_capabilities")
483
+ return this.report
484
+ } catch (error) {
485
+ throw asDeploymentError(error, "invalid_capabilities")
486
+ }
487
+ }
488
+
489
+ async prepare(planInput: AgentDeploymentPlan | unknown): Promise<DeploymentPrepareResult> {
490
+ if (this.state !== "new") return this.fail("invalid_transition")
491
+ const plan = parseDeploymentPlan(planInput)
492
+ this.deploymentId = plan.deploymentId
493
+ if (this.signal.aborted) return this.fail("cancelled")
494
+ const report = await this.capabilityReport()
495
+ assertCapabilityPlan(report, plan, this.authority, this.approved)
496
+ try {
497
+ const result = parsePrepare(
498
+ await this.adapter.prepare({ plan, authority: this.authority, signal: this.signal }),
499
+ plan.deploymentId,
500
+ )
501
+ this.preparedRef = result.preparedRef
502
+ this.state = "prepared"
503
+ this.evidence.push({ kind: "prepared", deploymentId: plan.deploymentId, state: "prepared" })
504
+ return result
505
+ } catch (error) {
506
+ return this.fail(asDeploymentError(error, "adapter_error").code)
507
+ }
508
+ }
509
+
510
+ async start(): Promise<DeploymentStartResult> {
511
+ if (
512
+ this.state !== "prepared" ||
513
+ this.preparedRef === undefined ||
514
+ this.deploymentId === undefined
515
+ )
516
+ return this.fail("invalid_transition")
517
+ if (this.signal.aborted) return this.fail("cancelled")
518
+ try {
519
+ const result = parseStart(
520
+ await this.adapter.start({
521
+ deploymentId: this.deploymentId,
522
+ preparedRef: this.preparedRef,
523
+ authority: this.authority,
524
+ signal: this.signal,
525
+ }),
526
+ this.deploymentId,
527
+ )
528
+ this.handleRef = result.handleRef
529
+ this.state = "running"
530
+ this.evidence.push({ kind: "started", deploymentId: this.deploymentId, state: "running" })
531
+ return result
532
+ } catch (error) {
533
+ return this.fail(asDeploymentError(error, "adapter_error").code)
534
+ }
535
+ }
536
+
537
+ async inspect(): Promise<DeploymentInspection> {
538
+ if (this.state === "new" || this.state === "disposed" || this.deploymentId === undefined)
539
+ return this.fail("invalid_transition")
540
+ try {
541
+ const result = parseInspection(
542
+ await this.adapter.inspect({
543
+ deploymentId: this.deploymentId,
544
+ ...(this.handleRef === undefined ? {} : { handleRef: this.handleRef }),
545
+ signal: this.signal,
546
+ }),
547
+ this.deploymentId,
548
+ )
549
+ this.evidence.push({
550
+ kind: "inspected",
551
+ deploymentId: this.deploymentId,
552
+ state: result.state,
553
+ })
554
+ return result
555
+ } catch (error) {
556
+ return this.fail(asDeploymentError(error, "adapter_error").code)
557
+ }
558
+ }
559
+
560
+ async cancel(): Promise<DeploymentCancelResult> {
561
+ if ((this.state !== "prepared" && this.state !== "running") || this.deploymentId === undefined)
562
+ return this.fail("invalid_transition")
563
+ try {
564
+ const result = parseTerminal(
565
+ await this.adapter.cancel({
566
+ deploymentId: this.deploymentId,
567
+ ...(this.preparedRef === undefined ? {} : { preparedRef: this.preparedRef }),
568
+ ...(this.handleRef === undefined ? {} : { handleRef: this.handleRef }),
569
+ signal: this.signal,
570
+ }),
571
+ this.deploymentId,
572
+ "cancelled",
573
+ )
574
+ this.state = "cancelled"
575
+ this.evidence.push({ kind: "cancelled", deploymentId: this.deploymentId, state: "cancelled" })
576
+ return result
577
+ } catch (error) {
578
+ return this.fail(asDeploymentError(error, "adapter_error").code)
579
+ }
580
+ }
581
+
582
+ async dispose(): Promise<DeploymentDisposeResult> {
583
+ if (this.state === "disposed")
584
+ return { deploymentId: this.deploymentId ?? this.id, state: "disposed" }
585
+ try {
586
+ const result = parseTerminal(
587
+ await this.adapter.dispose({
588
+ deploymentId: this.deploymentId ?? this.id,
589
+ ...(this.preparedRef === undefined ? {} : { preparedRef: this.preparedRef }),
590
+ ...(this.handleRef === undefined ? {} : { handleRef: this.handleRef }),
591
+ signal: this.signal,
592
+ }),
593
+ this.deploymentId ?? this.id,
594
+ "disposed",
595
+ )
596
+ this.state = "disposed"
597
+ this.evidence.push({
598
+ kind: "disposed",
599
+ deploymentId: this.deploymentId ?? this.id,
600
+ state: "disposed",
601
+ })
602
+ return result
603
+ } catch (error) {
604
+ return this.fail(asDeploymentError(error, "adapter_error").code)
605
+ }
606
+ }
607
+
608
+ private fail(code: DeploymentErrorCode): never {
609
+ this.evidence.push({
610
+ kind: "failed",
611
+ deploymentId: this.deploymentId ?? this.id,
612
+ state: this.state,
613
+ code,
614
+ })
615
+ throw new DeploymentError(code)
616
+ }
617
+ }
618
+
619
+ function asDeploymentError(error: unknown, fallback: DeploymentErrorCode): DeploymentError {
620
+ return error instanceof DeploymentError ? error : new DeploymentError(fallback)
621
+ }