@nifrajs/agent 3.1.0 → 3.3.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/README.md +36 -0
- package/dist/deployment.d.ts +157 -0
- package/dist/deployment.d.ts.map +1 -0
- package/dist/deployment.js +374 -0
- package/dist/deployment.js.map +1 -0
- package/dist/events.d.ts +41 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +75 -0
- package/dist/events.js.map +1 -1
- package/dist/execution-policy.d.ts.map +1 -1
- package/dist/execution-policy.js +11 -7
- package/dist/execution-policy.js.map +1 -1
- package/dist/gateway.d.ts +153 -0
- package/dist/gateway.d.ts.map +1 -0
- package/dist/gateway.js +409 -0
- package/dist/gateway.js.map +1 -0
- package/dist/index.d.ts +86 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +163 -1
- package/dist/index.js.map +1 -1
- package/dist/mount.d.ts +59 -0
- package/dist/mount.d.ts.map +1 -0
- package/dist/mount.js +247 -0
- package/dist/mount.js.map +1 -0
- package/dist/registry.d.ts +112 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +326 -0
- package/dist/registry.js.map +1 -0
- package/package.json +12 -2
- package/src/deployment.ts +621 -0
- package/src/events.ts +125 -0
- package/src/execution-policy.ts +9 -5
- package/src/gateway.ts +607 -0
- package/src/index.ts +303 -1
- package/src/mount.ts +344 -0
- package/src/registry.ts +444 -0
|
@@ -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
|
+
}
|