@claxedo/sandbox-manager 0.9.0 → 0.10.1
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 +233 -26
- package/dist/checkpoint-manager.d.ts +29 -4
- package/dist/checkpoint-manager.mjs +133 -24
- package/dist/command.d.ts +9 -2
- package/dist/command.mjs +8 -8
- package/dist/contract.d.ts +687 -0
- package/dist/driver-billing.d.ts +29 -0
- package/dist/driver-catalog.d.ts +42 -7
- package/dist/driver-catalog.mjs +115 -161
- package/dist/drivers/boat-client.d.ts +95 -0
- package/dist/drivers/boat-named-snapshots.d.ts +31 -0
- package/dist/drivers/boat-template.d.ts +49 -0
- package/dist/drivers/boat-template.mjs +1196 -0
- package/dist/drivers/boat.d.ts +64 -0
- package/dist/drivers/boat.mjs +990 -0
- package/dist/drivers/cloudflare.d.ts +3 -3
- package/dist/drivers/cloudflare.mjs +208 -147
- package/dist/drivers/docker.d.ts +4 -3
- package/dist/drivers/docker.mjs +154 -126
- package/dist/drivers/local-brokering.d.ts +14 -0
- package/dist/drivers/local-brokering.mjs +1029 -0
- package/dist/drivers/modal.d.ts +2 -2
- package/dist/drivers/modal.mjs +121 -111
- package/dist/drivers/transient-error.d.ts +2 -0
- package/dist/drivers/vercel.d.ts +3 -2
- package/dist/drivers/vercel.mjs +151 -115
- package/dist/egress-policy.d.ts +134 -0
- package/dist/egress-warnings.d.ts +22 -0
- package/dist/garbage-collect.d.ts +14 -0
- package/dist/hosted-network-policy.d.ts +9 -2
- package/dist/hosted-network-policy.mjs +218 -1
- package/dist/image-name.d.ts +12 -2
- package/dist/image-name.mjs +14 -5
- package/dist/image.d.ts +1 -20
- package/dist/image.mjs +11 -88
- package/dist/index.d.ts +8 -578
- package/dist/index.mjs +1379 -320
- package/dist/lease-failure.d.ts +173 -0
- package/dist/lease-lifecycle.d.ts +65 -0
- package/dist/lease-retry-wait.d.ts +8 -0
- package/dist/lease-start.d.ts +21 -0
- package/dist/lease-types.d.ts +4 -3
- package/dist/manager.d.ts +2 -0
- package/dist/runtime-env.d.ts +4 -2
- package/dist/runtime-env.mjs +37 -2
- package/dist/runtime-snapshot.d.ts +49 -0
- package/dist/runtime-version.d.ts +9 -0
- package/dist/runtime-version.mjs +2 -0
- package/dist/service-url-exposure.d.ts +0 -1
- package/dist/service-url-exposure.mjs +0 -9
- package/dist/start-fresh.d.ts +17 -0
- package/dist/start-telemetry.d.ts +37 -0
- package/dist/stores/memory.d.ts +1 -1
- package/dist/stores/memory.mjs +63 -52
- package/package.json +26 -42
- package/dist/daytona-allow-list.d.ts +0 -16
- package/dist/daytona-allow-list.mjs +0 -11
- package/dist/drivers/box.d.ts +0 -52
- package/dist/drivers/box.mjs +0 -574
- package/dist/drivers/daytona.d.ts +0 -163
- package/dist/drivers/daytona.mjs +0 -683
- package/dist/drivers/exe.d.ts +0 -26
- package/dist/drivers/exe.mjs +0 -492
- package/dist/drivers/fetch-bridge.d.ts +0 -10
- package/dist/drivers/fetch-bridge.mjs +0 -126
- package/dist/lease-policy.d.ts +0 -52
- package/dist/lease-policy.mjs +0 -290
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
import { DEFAULT_WORKSPACE_RUNTIME_PORT } from "./constants";
|
|
2
|
+
import type { SandboxMachineClass, SandboxPhaseTiming, SandboxSecretBrokering, SandboxStartPhase } from "@claxedo/sandbox-contract";
|
|
3
|
+
export type { SandboxMachineClass, SandboxSecretBrokering } from "@claxedo/sandbox-contract";
|
|
4
|
+
import { type SandboxCheckpointCaptureInput, type SandboxCheckpointRestoreInput, type SandboxCheckpointResult, type SandboxStopInput } from "./checkpoint-manager";
|
|
5
|
+
import type { SandboxLifecycleEvent } from "./lease-lifecycle";
|
|
6
|
+
import type { SandboxBrokeredSecret, SandboxEgressControl, SandboxEgressUnenforcedEvent, SandboxNetworkPolicy } from "./egress-policy";
|
|
7
|
+
import type { SandboxLeaseFailure } from "./lease-failure";
|
|
8
|
+
export { DEFAULT_WORKSPACE_RUNTIME_PORT };
|
|
9
|
+
export type SandboxRegion = string;
|
|
10
|
+
export type SandboxCaptureScope = "none" | "same-resource" | "filesystem";
|
|
11
|
+
export type SandboxCaptureSourceBehavior = "not-applicable" | "preserved" | "stopped" | "deleted";
|
|
12
|
+
export type SandboxRestoreMountBehavior = "not-applicable" | "same-resource" | "new-resource";
|
|
13
|
+
export type SandboxPersistenceCapabilities = {
|
|
14
|
+
resume: "same-sandbox" | "replacement-restore";
|
|
15
|
+
capture: SandboxCaptureScope;
|
|
16
|
+
clone: boolean;
|
|
17
|
+
captureSource: SandboxCaptureSourceBehavior;
|
|
18
|
+
retention: "not-applicable" | "provider-managed" | "explicit";
|
|
19
|
+
restoreMount: SandboxRestoreMountBehavior;
|
|
20
|
+
};
|
|
21
|
+
export declare function validateSandboxPersistenceCapabilities(input: SandboxPersistenceCapabilities): {
|
|
22
|
+
valid: false;
|
|
23
|
+
reason: string;
|
|
24
|
+
} | {
|
|
25
|
+
reason?: undefined;
|
|
26
|
+
valid: true;
|
|
27
|
+
};
|
|
28
|
+
export type SandboxCheckpointReference = {
|
|
29
|
+
id: string;
|
|
30
|
+
providerReference: string;
|
|
31
|
+
sourceEpoch: number;
|
|
32
|
+
capturedAt: number;
|
|
33
|
+
metadata: {
|
|
34
|
+
scope: Exclude<SandboxCaptureScope, "none">;
|
|
35
|
+
sourceBehavior: Exclude<SandboxCaptureSourceBehavior, "not-applicable">;
|
|
36
|
+
restoreMount: Exclude<SandboxRestoreMountBehavior, "not-applicable">;
|
|
37
|
+
/** The runtime did not freeze before this capture: turns and writes in flight are as a crash left them. */
|
|
38
|
+
unfrozen?: true;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
type SandboxRestoreIdentity = {
|
|
42
|
+
checkpointId: string;
|
|
43
|
+
sourceEpoch: number;
|
|
44
|
+
};
|
|
45
|
+
export type SandboxRestoreStatus = (SandboxRestoreIdentity & {
|
|
46
|
+
state: "pending";
|
|
47
|
+
requestedAt: number;
|
|
48
|
+
}) | (SandboxRestoreIdentity & {
|
|
49
|
+
state: "restoring";
|
|
50
|
+
requestedAt: number;
|
|
51
|
+
startedAt: number;
|
|
52
|
+
}) | (SandboxRestoreIdentity & {
|
|
53
|
+
state: "ready";
|
|
54
|
+
requestedAt: number;
|
|
55
|
+
startedAt: number;
|
|
56
|
+
completedAt: number;
|
|
57
|
+
}) | (SandboxRestoreIdentity & {
|
|
58
|
+
state: "failed";
|
|
59
|
+
requestedAt: number;
|
|
60
|
+
startedAt?: number;
|
|
61
|
+
failedAt: number;
|
|
62
|
+
error: string;
|
|
63
|
+
});
|
|
64
|
+
export type SandboxDriverMetadata = {
|
|
65
|
+
/** Where the sandbox driver code can execute. This is not sandbox infrastructure. */
|
|
66
|
+
driverRunsIn: Array<"worker" | "node" | "local">;
|
|
67
|
+
/** What `SandboxManager.stop()` actually does to the driver-owned resource. */
|
|
68
|
+
hostStopBehavior: "not-supported" | "suspends-host" | "terminates-host";
|
|
69
|
+
/** Whether a stopped/stale lease can resume the same driver-owned resource. */
|
|
70
|
+
hostResumeBehavior: "same-host" | "replacement-host";
|
|
71
|
+
/**
|
|
72
|
+
* The host itself reads its runtime's idleness and stops once the idle
|
|
73
|
+
* window has passed (the Cloudflare sandbox Worker). The control plane's
|
|
74
|
+
* provisioner reads every serving runtime over the relay for liveness, and
|
|
75
|
+
* stops an idle one through the driver only where this is absent.
|
|
76
|
+
*/
|
|
77
|
+
hostWatchesIdle?: true;
|
|
78
|
+
/** How the control plane reaches the sandbox target returned by the driver. */
|
|
79
|
+
targetAccess: "relay" | "loopback";
|
|
80
|
+
/**
|
|
81
|
+
* How the driver can honor a `SandboxBrokeredSecret` — a credential the
|
|
82
|
+
* sandbox may USE for outbound requests but must NEVER be able to READ.
|
|
83
|
+
*
|
|
84
|
+
* - `"native"` — the provider brokers it on egress to the allowlisted hosts
|
|
85
|
+
* with no extra infrastructure of ours: Vercel firewall header
|
|
86
|
+
* transforms, Cloudflare outbound handlers reading
|
|
87
|
+
* the value from KV. The driver installs it during `ensureHost`.
|
|
88
|
+
* - `"none"` — no way to keep the value out of sandbox processes. The
|
|
89
|
+
* provider may still have an encrypted secret STORE (e.g. Modal secrets),
|
|
90
|
+
* but it is exposed as a readable env var, which cannot satisfy the
|
|
91
|
+
* never-readable contract.
|
|
92
|
+
*/
|
|
93
|
+
secretBrokering: SandboxSecretBrokering;
|
|
94
|
+
/**
|
|
95
|
+
* How the driver can enforce a RESTRICTED `SandboxNetworkPolicy` — i.e.
|
|
96
|
+
* whether the sandbox's outbound network can actually be contained.
|
|
97
|
+
*
|
|
98
|
+
* A `SandboxNetworkPolicy` states the same allowance in up to two encodings:
|
|
99
|
+
* `hosts` (names) and `cidrs` (addresses). A driver contains egress if it
|
|
100
|
+
* enforces AT LEAST ONE encoding the policy carries and blocks everything
|
|
101
|
+
* else; the encodings are alternatives, not additive requirements.
|
|
102
|
+
*
|
|
103
|
+
* - `"hosts-and-cidrs"` — the provider filters by name AND by address.
|
|
104
|
+
* - `"hosts"` — the provider filters by hostname only (Vercel firewall).
|
|
105
|
+
* - `"none"` — the driver cannot express an egress allowlist. This covers
|
|
106
|
+
* BOTH drivers that throw when handed a restricted policy (docker, boat,
|
|
107
|
+
* and modal for host policy) AND — more dangerously — drivers that
|
|
108
|
+
* silently ignore `net` and run wide open (cloudflare, the fetch bridge).
|
|
109
|
+
* Modal is `"none"` even though it can cut the network entirely: a total
|
|
110
|
+
* blackout is not an allowlist and cannot serve a workspace that has to
|
|
111
|
+
* clone a repo and reach a model provider.
|
|
112
|
+
*
|
|
113
|
+
* The manager reads this to decide what the driver is handed:
|
|
114
|
+
* `sandboxEgressDisposition` enforces where it can and WITHHOLDS (loudly)
|
|
115
|
+
* where it cannot. See `SandboxEgressDisposition` for the posture and
|
|
116
|
+
* `public-docs/sandbox-egress.md` for the operator-facing consequence.
|
|
117
|
+
*/
|
|
118
|
+
egressControl: SandboxEgressControl;
|
|
119
|
+
persistence: SandboxPersistenceCapabilities;
|
|
120
|
+
/** The machine classes the provider lets this driver choose at create; absent, the provider's one default. */
|
|
121
|
+
machineClasses?: readonly SandboxMachineClass[];
|
|
122
|
+
};
|
|
123
|
+
export type SandboxExposure = {
|
|
124
|
+
kind: "loopback";
|
|
125
|
+
} | {
|
|
126
|
+
kind: "relay";
|
|
127
|
+
} | {
|
|
128
|
+
kind: "private-network";
|
|
129
|
+
} | {
|
|
130
|
+
kind: "embedded";
|
|
131
|
+
};
|
|
132
|
+
export type SandboxLeaseStatus = "acquiring" | "ready" | "unavailable" | "stopped" | "destroyed";
|
|
133
|
+
export type SandboxLease = {
|
|
134
|
+
workspaceId: string;
|
|
135
|
+
homeRegion: SandboxRegion;
|
|
136
|
+
driver: string;
|
|
137
|
+
epoch: number;
|
|
138
|
+
routingId?: string;
|
|
139
|
+
status: SandboxLeaseStatus;
|
|
140
|
+
retryCount: number;
|
|
141
|
+
updatedAt: number;
|
|
142
|
+
createdAt: number;
|
|
143
|
+
sandboxId?: string;
|
|
144
|
+
url?: string;
|
|
145
|
+
hostId?: string;
|
|
146
|
+
driverResourceId?: string;
|
|
147
|
+
nextRetryAt?: number;
|
|
148
|
+
lastError?: string;
|
|
149
|
+
lastHeartbeatAt?: number;
|
|
150
|
+
lastActivityAt?: number;
|
|
151
|
+
/** When a start of this lease last failed, or its runtime last left a read unanswered. */
|
|
152
|
+
lastHealthFailureAt?: number;
|
|
153
|
+
labels?: Record<string, string>;
|
|
154
|
+
checkpoint?: SandboxCheckpointReference;
|
|
155
|
+
persistence?: SandboxPersistenceCapabilities;
|
|
156
|
+
restore?: SandboxRestoreStatus;
|
|
157
|
+
/** This epoch's start, recorded while it boots; a new epoch starts without one. */
|
|
158
|
+
start?: SandboxStartProgress;
|
|
159
|
+
/** The machine this epoch's sandbox runs on: the class its acquire named, else the one the host it resumes already has. */
|
|
160
|
+
machineClass?: SandboxMachineClass;
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* One lease epoch's start as far as it has been observed. Persisted because a
|
|
164
|
+
* start outlives the ensure call that began it: a driver that answers
|
|
165
|
+
* "provisioning" is polled again from another request, often another isolate.
|
|
166
|
+
*
|
|
167
|
+
* `startedAt` is this epoch's boot, the interval the epoch is metered by;
|
|
168
|
+
* `requestedAt`, `bootMode` and `failures` are the start the person is waiting
|
|
169
|
+
* on, which an epoch that retries a failed attempt or takes over a stale one
|
|
170
|
+
* inherits. A failure is recorded here as well as in the lease's `lastError`,
|
|
171
|
+
* which the next epoch's acquire and the target it then records clear.
|
|
172
|
+
*/
|
|
173
|
+
export type SandboxStartProgress = {
|
|
174
|
+
requestedAt: number;
|
|
175
|
+
startedAt: number;
|
|
176
|
+
bootMode: SandboxBootMode;
|
|
177
|
+
/** When the last phase the control plane itself observed ended; the next one is timed from here. */
|
|
178
|
+
markedAt: number;
|
|
179
|
+
phases: SandboxStartPhase[];
|
|
180
|
+
/** The epochs of this start that failed, in order, each with the reason the lease recorded. */
|
|
181
|
+
failures?: SandboxStartFailure[];
|
|
182
|
+
};
|
|
183
|
+
export type SandboxStartFailure = {
|
|
184
|
+
epoch: number;
|
|
185
|
+
failedAt: number;
|
|
186
|
+
error: string;
|
|
187
|
+
};
|
|
188
|
+
export type SandboxEpochStart = SandboxStartProgress & {
|
|
189
|
+
epoch: number;
|
|
190
|
+
};
|
|
191
|
+
export type SandboxStartPhaseEvent = SandboxPhaseTiming<SandboxStartPhase> & {
|
|
192
|
+
/** Since the start the person is waiting on began, across the epochs it took. */
|
|
193
|
+
sinceStartMs: number;
|
|
194
|
+
workspaceId: string;
|
|
195
|
+
epoch: number;
|
|
196
|
+
driver: string;
|
|
197
|
+
homeRegion: SandboxRegion;
|
|
198
|
+
bootMode: SandboxBootMode;
|
|
199
|
+
labels: Record<string, string>;
|
|
200
|
+
repoSizeBytes?: number;
|
|
201
|
+
};
|
|
202
|
+
export type SandboxLeaseAcquireInput = {
|
|
203
|
+
homeRegion: SandboxRegion;
|
|
204
|
+
driver: string;
|
|
205
|
+
staleAfterMs: number;
|
|
206
|
+
now?: number;
|
|
207
|
+
/** The placement's labels, recorded on a fresh lease before the driver is asked for a resource; a resumed lease keeps its own. */
|
|
208
|
+
labels?: Record<string, string>;
|
|
209
|
+
/** The machine the workspace names now; a held host is resumed onto it, and absent, keeps the machine it has. */
|
|
210
|
+
machineClass?: SandboxMachineClass;
|
|
211
|
+
/** The start the new epoch continues, recorded on it before any driver step, so a poll from another isolate times from the same request. */
|
|
212
|
+
start?: SandboxStartProgress;
|
|
213
|
+
};
|
|
214
|
+
export type SandboxLeaseAcquireResult = {
|
|
215
|
+
acquired: true;
|
|
216
|
+
lease: SandboxLease;
|
|
217
|
+
} | {
|
|
218
|
+
acquired: false;
|
|
219
|
+
lease: SandboxLease;
|
|
220
|
+
retryAfterMs: number;
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* The lease whose host, placement and size the next epoch keeps, so that
|
|
224
|
+
* epoch resumes it: one stopped, or one whose attempt failed on a host the
|
|
225
|
+
* driver had already reported. A destroyed lease's host is gone, and a lease
|
|
226
|
+
* that never got one is placed afresh.
|
|
227
|
+
*/
|
|
228
|
+
export declare function sandboxLeaseHeldHost(lease: SandboxLease | undefined): SandboxLease | undefined;
|
|
229
|
+
/**
|
|
230
|
+
* What may be said about a lease after it exists: lifecycle state, liveness,
|
|
231
|
+
* and the capture/restore record.
|
|
232
|
+
*
|
|
233
|
+
* Identity and labels are absent, and that absence is the whole ownership
|
|
234
|
+
* mechanism. `sandboxId`, `url`, `hostId`, `driverResourceId` and `labels`
|
|
235
|
+
* decide which provider resource every later checkpoint, snapshot, stop and
|
|
236
|
+
* destroy acts on; if a patch could carry them, anything that can reach a
|
|
237
|
+
* store — a status update, a heartbeat, a retry — could point one workspace's
|
|
238
|
+
* lease at another workspace's sandbox. They enter a lease only through
|
|
239
|
+
* `SandboxLeaseStore.recordTarget`, whose argument is a driver's resource report.
|
|
240
|
+
*/
|
|
241
|
+
export type SandboxLeasePatch = Partial<Pick<SandboxLease, "status" | "retryCount" | "lastHeartbeatAt" | "lastActivityAt" | "lastHealthFailureAt">> & {
|
|
242
|
+
/** `null` clears the stored value; `undefined` leaves it unchanged. */
|
|
243
|
+
nextRetryAt?: number | null;
|
|
244
|
+
/** `null` clears the stored value; `undefined` leaves it unchanged. */
|
|
245
|
+
lastError?: string | null;
|
|
246
|
+
/** `null` clears the stored value; `undefined` leaves it unchanged. */
|
|
247
|
+
checkpoint?: SandboxCheckpointReference | null;
|
|
248
|
+
/** `null` clears the stored value; `undefined` leaves it unchanged. */
|
|
249
|
+
restore?: SandboxRestoreStatus | null;
|
|
250
|
+
start?: SandboxStartProgress;
|
|
251
|
+
/**
|
|
252
|
+
* When the host this write takes the epoch off was last proven serving, for
|
|
253
|
+
* a write that learns of its end after the fact; the boot it records ends
|
|
254
|
+
* there rather than at the write. Not a lease field.
|
|
255
|
+
*/
|
|
256
|
+
endedAt?: number;
|
|
257
|
+
};
|
|
258
|
+
/**
|
|
259
|
+
* Canonical SandboxLeasePatch merge semantics shared by every lease store driver:
|
|
260
|
+
* absent/`undefined` keeps the current value, `null` clears it.
|
|
261
|
+
*/
|
|
262
|
+
export declare function applySandboxLeasePatch(current: SandboxLease, patch: SandboxLeasePatch, updatedAt: number): SandboxLease;
|
|
263
|
+
/**
|
|
264
|
+
* A provider resource may exist before it has a runtime URL. Persisting its
|
|
265
|
+
* identity while acquiring lets the next ensure continue that same resource.
|
|
266
|
+
*/
|
|
267
|
+
export type SandboxProvisionedTarget = {
|
|
268
|
+
sandboxId: string;
|
|
269
|
+
url?: string;
|
|
270
|
+
hostId: string;
|
|
271
|
+
driverResourceId?: string;
|
|
272
|
+
labels: Record<string, string>;
|
|
273
|
+
persistence?: SandboxPersistenceCapabilities;
|
|
274
|
+
};
|
|
275
|
+
/** The lease after its epoch failed: unavailable, a retry counted, and the reason on the lease and on the start it was on. */
|
|
276
|
+
export declare function applySandboxLeaseFailure(current: SandboxLease, error: string, nextRetryAt: number | undefined, failedAt: number): SandboxLease;
|
|
277
|
+
export declare function applySandboxProvisionedTarget(current: SandboxLease, target: SandboxProvisionedTarget, updatedAt: number): SandboxLease;
|
|
278
|
+
export type SandboxLeaseStore = {
|
|
279
|
+
acquire: (workspaceId: string, input: SandboxLeaseAcquireInput) => Promise<SandboxLeaseAcquireResult>;
|
|
280
|
+
/** The one writer of lease identity. Fenced on the epoch the driver was asked for. */
|
|
281
|
+
recordTarget: (workspaceId: string, expectedEpoch: number, target: SandboxProvisionedTarget) => Promise<SandboxLease | undefined>;
|
|
282
|
+
update: (workspaceId: string, expectedEpoch: number, patch: SandboxLeasePatch, expectedStatus?: SandboxLeaseStatus) => Promise<SandboxLease | undefined>;
|
|
283
|
+
recordFailure: (workspaceId: string, expectedEpoch: number, error: string, nextRetryAt?: number) => Promise<SandboxLease | undefined>;
|
|
284
|
+
release: (workspaceId: string) => Promise<void>;
|
|
285
|
+
get: (workspaceId: string) => Promise<SandboxLease | undefined>;
|
|
286
|
+
list: () => Promise<SandboxLease[]>;
|
|
287
|
+
};
|
|
288
|
+
export type SandboxBootSource = {
|
|
289
|
+
kind: "image";
|
|
290
|
+
image: string;
|
|
291
|
+
} | {
|
|
292
|
+
kind: "driver-snapshot";
|
|
293
|
+
snapshotId: string;
|
|
294
|
+
} | {
|
|
295
|
+
kind: "default";
|
|
296
|
+
};
|
|
297
|
+
export type SandboxSource = {
|
|
298
|
+
kind: "git";
|
|
299
|
+
repoUrl: string;
|
|
300
|
+
branch?: string;
|
|
301
|
+
} | {
|
|
302
|
+
kind: "empty";
|
|
303
|
+
};
|
|
304
|
+
export type SandboxDriverEnsureInput = {
|
|
305
|
+
/** Await before readiness polling so the manager can durably fence the resource identity. */
|
|
306
|
+
onResource?: (resource: Omit<SandboxProvisionedTarget, "url" | "persistence">) => Promise<void>;
|
|
307
|
+
/** Called by a driver whose provider machine, created or resumed, takes commands before the image is prepared on it. */
|
|
308
|
+
onMachineReady?: () => Promise<void>;
|
|
309
|
+
/** Called by a driver that sees its sandbox running from the image or snapshot before the runtime is up. */
|
|
310
|
+
onImageReady?: () => Promise<void>;
|
|
311
|
+
workspaceId: string;
|
|
312
|
+
hostId?: string;
|
|
313
|
+
homeRegion: SandboxRegion;
|
|
314
|
+
epoch: number;
|
|
315
|
+
labels: Record<string, string>;
|
|
316
|
+
bootSource?: SandboxBootSource;
|
|
317
|
+
workspaceRoot?: string;
|
|
318
|
+
runtimeCwd?: string;
|
|
319
|
+
workspaceRuntimePort?: number;
|
|
320
|
+
env?: Record<string, string>;
|
|
321
|
+
/** Brokered secrets: injected on egress, never readable inside the sandbox. */
|
|
322
|
+
secrets?: SandboxBrokeredSecret[];
|
|
323
|
+
source?: SandboxSource;
|
|
324
|
+
exposure?: SandboxExposure;
|
|
325
|
+
/** The machine to create the sandbox on; one the driver declared, or nothing for the provider's default. */
|
|
326
|
+
machineClass?: SandboxMachineClass;
|
|
327
|
+
/**
|
|
328
|
+
* Egress policy. Omitted means allow-all, which is only appropriate for a
|
|
329
|
+
* single-tenant deployment the operator already trusts.
|
|
330
|
+
*
|
|
331
|
+
* A driver can rely on this being ABSENT whenever it declares
|
|
332
|
+
* `egressControl: "none"`: the manager withholds the policy rather than hand
|
|
333
|
+
* it to a driver that would either throw on it or silently drop it. A driver
|
|
334
|
+
* that receives a restricted policy here is one that declared it can enforce
|
|
335
|
+
* it. @see SandboxEgressDisposition
|
|
336
|
+
*/
|
|
337
|
+
net?: SandboxNetworkPolicy;
|
|
338
|
+
/** Optional snapshot/image to boot from (user-supplied or pre-built). */
|
|
339
|
+
snapshot?: string;
|
|
340
|
+
};
|
|
341
|
+
export type SandboxTarget = {
|
|
342
|
+
workspaceId?: string;
|
|
343
|
+
sandboxId: string;
|
|
344
|
+
url: string;
|
|
345
|
+
hostId: string;
|
|
346
|
+
driverResourceId?: string;
|
|
347
|
+
labels?: Record<string, string>;
|
|
348
|
+
driver?: {
|
|
349
|
+
id: string;
|
|
350
|
+
resourceId: string;
|
|
351
|
+
metadata?: Record<string, string>;
|
|
352
|
+
};
|
|
353
|
+
};
|
|
354
|
+
export type SandboxResource = Omit<SandboxTarget, "url"> & {
|
|
355
|
+
url?: string;
|
|
356
|
+
};
|
|
357
|
+
/** What the provider says of a host: running or booting, held but not running, or no longer had. */
|
|
358
|
+
export type SandboxHostState = "serving" | "stopped" | "gone";
|
|
359
|
+
export type SandboxSnapshotResult = {
|
|
360
|
+
snapshotId: string;
|
|
361
|
+
};
|
|
362
|
+
export type SandboxCommandResult = {
|
|
363
|
+
stdout: string;
|
|
364
|
+
stderr: string;
|
|
365
|
+
exitCode: number;
|
|
366
|
+
};
|
|
367
|
+
/** The label a target records the image its runtime booted from, when the control plane chose that image. */
|
|
368
|
+
export declare const SANDBOX_IMAGE_LABEL = "image";
|
|
369
|
+
export type SandboxDriver = {
|
|
370
|
+
id: string;
|
|
371
|
+
metadata: SandboxDriverMetadata;
|
|
372
|
+
/** The image this driver's next boot runs, when the control plane chooses it. */
|
|
373
|
+
image?: string;
|
|
374
|
+
ensureHost: (input: SandboxDriverEnsureInput) => Promise<SandboxTarget | {
|
|
375
|
+
provisioning: true;
|
|
376
|
+
retryAfterMs: number;
|
|
377
|
+
}>;
|
|
378
|
+
resumeHost?: (input: {
|
|
379
|
+
lease: SandboxLease;
|
|
380
|
+
ensure: SandboxDriverEnsureInput;
|
|
381
|
+
}) => Promise<SandboxTarget | {
|
|
382
|
+
provisioning: true;
|
|
383
|
+
retryAfterMs: number;
|
|
384
|
+
}>;
|
|
385
|
+
list?: () => Promise<SandboxTarget[]>;
|
|
386
|
+
touch?: (target: SandboxTarget) => Promise<void>;
|
|
387
|
+
/** A host whose runtime never published a URL is suspended all the same: a boot that failed on it still holds it. */
|
|
388
|
+
suspend?: (target: SandboxResource) => Promise<void>;
|
|
389
|
+
/**
|
|
390
|
+
* Stops the host; given `epoch`, a host that a newer lease generation already
|
|
391
|
+
* took over keeps running. `checkpoint` names the snapshot the lease references.
|
|
392
|
+
*/
|
|
393
|
+
stop?: (target: SandboxResource & {
|
|
394
|
+
epoch?: number;
|
|
395
|
+
checkpoint?: string;
|
|
396
|
+
}) => Promise<void>;
|
|
397
|
+
destroy?: (target: SandboxResource) => Promise<void>;
|
|
398
|
+
/** `committed` names the snapshot the lease references now; a driver tracking uncommitted snapshots drops the rest. */
|
|
399
|
+
snapshot?: (target: SandboxTarget, committed?: string) => Promise<SandboxSnapshotResult>;
|
|
400
|
+
/** Deletes a snapshot `snapshot` returned; implemented by drivers whose snapshots outlive their sandbox. */
|
|
401
|
+
deleteSnapshot?: (target: SandboxResource, snapshotId: string) => Promise<void>;
|
|
402
|
+
inspect?: (target: SandboxTarget) => Promise<SandboxHostState>;
|
|
403
|
+
exec?: (target: SandboxTarget, command: string) => Promise<SandboxCommandResult>;
|
|
404
|
+
clone?: (target: SandboxTarget, input: {
|
|
405
|
+
name: string;
|
|
406
|
+
}) => Promise<SandboxTarget>;
|
|
407
|
+
};
|
|
408
|
+
export type SandboxManager = {
|
|
409
|
+
/** Takes or continues the workspace's lease and runs the driver for it until it answers. */
|
|
410
|
+
ensure: (workspaceId: string, input: SandboxManagerInput) => Promise<SandboxEnsureResult>;
|
|
411
|
+
/**
|
|
412
|
+
* The lease decision of `ensure` alone: `provisioning` names the epoch a
|
|
413
|
+
* caller that schedules its own driver work now holds and owes `provision`.
|
|
414
|
+
*/
|
|
415
|
+
acquire: (workspaceId: string, input: SandboxManagerInput) => Promise<SandboxEnsureResult>;
|
|
416
|
+
/**
|
|
417
|
+
* The driver work of `ensure` for the lease `acquire` handed out at `epoch`;
|
|
418
|
+
* another epoch answers `runtime_lease_changed`. A lease whose attempt failed
|
|
419
|
+
* is not driven again: once its retry is due, the next attempt takes a fresh
|
|
420
|
+
* epoch, which the answer names, keeping the host the failed one holds.
|
|
421
|
+
*/
|
|
422
|
+
provision: (workspaceId: string, epoch: number, input: SandboxManagerInput) => Promise<SandboxEnsureResult>;
|
|
423
|
+
register: (workspaceId: string, input: SandboxRuntimeSnapshotInput) => Promise<SandboxMutationResult>;
|
|
424
|
+
heartbeat: (workspaceId: string, input: SandboxRuntimeSnapshotInput) => Promise<SandboxMutationResult>;
|
|
425
|
+
target: (workspaceId: string) => Promise<SandboxTargetResult>;
|
|
426
|
+
touch: (workspaceId: string) => Promise<SandboxTouchResult>;
|
|
427
|
+
snapshot: (workspaceId: string, committed?: string) => Promise<SandboxSnapshotManagerResult>;
|
|
428
|
+
checkpoint: (workspaceId: string, input: SandboxCheckpointCaptureInput) => Promise<SandboxCheckpointResult>;
|
|
429
|
+
restore: (workspaceId: string, input: SandboxCheckpointRestoreInput) => Promise<SandboxCheckpointResult>;
|
|
430
|
+
stop: (workspaceId: string, input?: SandboxStopInput) => Promise<SandboxMutationResult>;
|
|
431
|
+
/**
|
|
432
|
+
* Ends the serving lease at `epoch` when the provider says its host is not
|
|
433
|
+
* running: `stopped` (held, not running) ends it stopped, so the next start
|
|
434
|
+
* resumes that host; `gone` ends it destroyed. Nothing while the host
|
|
435
|
+
* serves, the epoch moved on, or the driver cannot inspect its hosts. A
|
|
436
|
+
* host the provider says serves is the lease's heartbeat at that moment,
|
|
437
|
+
* whether or not its runtime answers: the host is billed until it stops,
|
|
438
|
+
* and its boot closes at the last such proof. Asked for a runtime that left
|
|
439
|
+
* a read unanswered, so a lease it does not end records that moment as its
|
|
440
|
+
* last health failure.
|
|
441
|
+
*/
|
|
442
|
+
endIfHostDown: (workspaceId: string, epoch: number) => Promise<Exclude<SandboxHostState, "serving"> | undefined>;
|
|
443
|
+
destroy: (workspaceId: string) => Promise<SandboxMutationResult>;
|
|
444
|
+
/**
|
|
445
|
+
* A person's Start fresh: drops the stopped workspace's checkpoint so its
|
|
446
|
+
* next start boots the image and clones the repository. Refused while the
|
|
447
|
+
* workspace runs or a start is in flight (`workspace_running`), and for a
|
|
448
|
+
* driver whose checkpoint is the host itself or that keeps none
|
|
449
|
+
* (`start_fresh_unsupported`).
|
|
450
|
+
*/
|
|
451
|
+
startFresh: (workspaceId: string) => Promise<SandboxMutationResult>;
|
|
452
|
+
release: (workspaceId: string) => Promise<{
|
|
453
|
+
released: boolean;
|
|
454
|
+
}>;
|
|
455
|
+
garbageCollect: () => Promise<SandboxGarbageCollectResult>;
|
|
456
|
+
list: () => Promise<SandboxLease[]>;
|
|
457
|
+
/** Phases the runtime timed inside its sandbox, recorded into that epoch's start once each. */
|
|
458
|
+
recordStartPhases: (workspaceId: string, input: SandboxStartPhasesReport) => Promise<void>;
|
|
459
|
+
/**
|
|
460
|
+
* Ends `phase` now, timed from the start's last observed phase. `notBefore`
|
|
461
|
+
* is when the evidence for it came into being; evidence older than the start
|
|
462
|
+
* belongs to an earlier one and records nothing.
|
|
463
|
+
*/
|
|
464
|
+
markStartPhase: (workspaceId: string, input: {
|
|
465
|
+
epoch: number;
|
|
466
|
+
phase: SandboxStartPhase;
|
|
467
|
+
notBefore: number;
|
|
468
|
+
}) => Promise<void>;
|
|
469
|
+
};
|
|
470
|
+
export type SandboxStartPhasesReport = {
|
|
471
|
+
epoch: number;
|
|
472
|
+
phases: readonly SandboxPhaseTiming<SandboxStartPhase>[];
|
|
473
|
+
repoSizeBytes?: number;
|
|
474
|
+
};
|
|
475
|
+
export type SandboxManagerInput = {
|
|
476
|
+
homeRegion: SandboxRegion;
|
|
477
|
+
hostId?: string;
|
|
478
|
+
labels?: Record<string, string>;
|
|
479
|
+
bootSource?: SandboxBootSource;
|
|
480
|
+
workspaceRoot?: string;
|
|
481
|
+
runtimeCwd?: string;
|
|
482
|
+
workspaceRuntimePort?: number;
|
|
483
|
+
env?: Record<string, string>;
|
|
484
|
+
/** Brokered secrets: injected on egress, never readable inside the sandbox. */
|
|
485
|
+
secrets?: SandboxBrokeredSecret[];
|
|
486
|
+
/**
|
|
487
|
+
* Env minted for the lease the driver boots, joined to `env`: a credential
|
|
488
|
+
* bound to one lease generation and host can only be minted once the start
|
|
489
|
+
* has settled which epoch it runs, and a failed attempt's retry takes a new
|
|
490
|
+
* one.
|
|
491
|
+
*/
|
|
492
|
+
leaseEnv?: (lease: {
|
|
493
|
+
epoch: number;
|
|
494
|
+
hostId: string;
|
|
495
|
+
}) => Promise<Record<string, string>>;
|
|
496
|
+
/** The machine the workspace chose; refused before a lease is taken when its driver declares no such class. */
|
|
497
|
+
machineClass?: SandboxMachineClass;
|
|
498
|
+
source?: SandboxSource;
|
|
499
|
+
exposure?: SandboxExposure;
|
|
500
|
+
/**
|
|
501
|
+
* @see SandboxDriverEnsureInput.net — enforced by a capable driver, withheld
|
|
502
|
+
* (with a warning) by one that declares `egressControl: "none"`.
|
|
503
|
+
*/
|
|
504
|
+
net?: SandboxNetworkPolicy;
|
|
505
|
+
snapshot?: string;
|
|
506
|
+
/**
|
|
507
|
+
* Called by the one ensure whose acquire created the workspace's first
|
|
508
|
+
* lease, before its driver is asked for anything; a concurrent start that
|
|
509
|
+
* lost the acquire is not called.
|
|
510
|
+
*/
|
|
511
|
+
onLeaseOpened?: () => void;
|
|
512
|
+
};
|
|
513
|
+
/**
|
|
514
|
+
* What a running sandbox may report about itself: liveness and activity.
|
|
515
|
+
*
|
|
516
|
+
* Identity — `sandboxId`, `url`, `hostId`, `driverResourceId` — is absent on
|
|
517
|
+
* purpose. `provision()` writes it from the driver's answer and nothing else
|
|
518
|
+
* does, because every later lifecycle call resolves its provider target from
|
|
519
|
+
* those fields: a snapshot able to restate them lets the reporting sandbox
|
|
520
|
+
* point this workspace's checkpoint, snapshot and destroy at a resource
|
|
521
|
+
* belonging to another workspace.
|
|
522
|
+
*
|
|
523
|
+
* `epoch` is required and never defaulted. Reading the lease's own epoch when
|
|
524
|
+
* the reporter omits one makes the fence agree with whatever it is handed.
|
|
525
|
+
*/
|
|
526
|
+
export type SandboxRuntimeSnapshotInput = {
|
|
527
|
+
epoch: number;
|
|
528
|
+
/** Whether the runtime is serving. How a reporter words that is its own. */
|
|
529
|
+
ok: boolean;
|
|
530
|
+
active?: boolean;
|
|
531
|
+
now?: number;
|
|
532
|
+
};
|
|
533
|
+
/**
|
|
534
|
+
* What a provisioning cycle is actually doing, for honest progress UI.
|
|
535
|
+
* `restore` = booting a replacement restored from the workspace's snapshot
|
|
536
|
+
* (files come back), `resume` = restarting the same paused resource,
|
|
537
|
+
* `cold-start` = a fresh sandbox with no prior state to bring back (first
|
|
538
|
+
* boot, or nothing snapshot-able survived).
|
|
539
|
+
*/
|
|
540
|
+
export type SandboxBootMode = "restore" | "resume" | "cold-start";
|
|
541
|
+
export type SandboxEnsureResult = ({
|
|
542
|
+
status: "ready";
|
|
543
|
+
} & SandboxTarget & {
|
|
544
|
+
epoch: number;
|
|
545
|
+
routingId?: string;
|
|
546
|
+
homeRegion: SandboxRegion;
|
|
547
|
+
/**
|
|
548
|
+
* Set when the driver call for this ensure failed on a lease that was
|
|
549
|
+
* already serving: the returned target predates this ensure's inputs.
|
|
550
|
+
* A caller that handed new brokered secrets must not record them as
|
|
551
|
+
* delivered — the provider edge still holds the previous set.
|
|
552
|
+
*/
|
|
553
|
+
stale?: true;
|
|
554
|
+
}) | {
|
|
555
|
+
status: "provisioning";
|
|
556
|
+
retryAfterMs: number;
|
|
557
|
+
epoch: number;
|
|
558
|
+
homeRegion: SandboxRegion;
|
|
559
|
+
bootMode?: SandboxBootMode;
|
|
560
|
+
} | {
|
|
561
|
+
status: "unavailable";
|
|
562
|
+
retryAfterMs?: number;
|
|
563
|
+
error?: string;
|
|
564
|
+
epoch?: number;
|
|
565
|
+
homeRegion: SandboxRegion;
|
|
566
|
+
/** The start's retry budget is spent: `retryAfterMs` is the cooldown every start waits out, which nothing drives. */
|
|
567
|
+
exhausted?: true;
|
|
568
|
+
};
|
|
569
|
+
export type SandboxTargetResult = ({
|
|
570
|
+
status: "ready";
|
|
571
|
+
} & SandboxTarget & {
|
|
572
|
+
epoch: number;
|
|
573
|
+
routingId?: string;
|
|
574
|
+
homeRegion: SandboxRegion;
|
|
575
|
+
/** The runtime booted from an image other than the driver's current one; its next ensure boots the current one. */
|
|
576
|
+
imageOutdated?: true;
|
|
577
|
+
/** The start that brought this epoch up, as observed; the run still delivering its settings is timed from it. */
|
|
578
|
+
start?: SandboxEpochStart;
|
|
579
|
+
}) | {
|
|
580
|
+
status: "unavailable";
|
|
581
|
+
reason: string;
|
|
582
|
+
/**
|
|
583
|
+
* The lease's own lifecycle word when a lease exists (`"acquiring"`,
|
|
584
|
+
* `"stopped"`, `"unavailable"`, `"destroyed"`); absent when none does.
|
|
585
|
+
* `target()` deliberately does not collapse these — a read path needs to
|
|
586
|
+
* tell "a start is already in flight" apart from "nothing is running".
|
|
587
|
+
*/
|
|
588
|
+
leaseStatus?: SandboxLeaseStatus;
|
|
589
|
+
/** Delay until the lease's own next scheduled retry, when it carries one. */
|
|
590
|
+
retryAfterMs?: number;
|
|
591
|
+
/** What the lease's last start or health report failed on, when it is unavailable for that reason. */
|
|
592
|
+
failure?: SandboxLeaseFailure;
|
|
593
|
+
/** Why a stopped lease was stopped, in a person's words, when neither a person nor idleness stopped it. */
|
|
594
|
+
stopReason?: string;
|
|
595
|
+
/** The start the lease is on while it is acquiring or waiting out a retry, as far as it has been observed. */
|
|
596
|
+
start?: SandboxEpochStart;
|
|
597
|
+
/** The epoch and host an acquiring lease is booting, once the driver has handed it a host. */
|
|
598
|
+
booting?: {
|
|
599
|
+
epoch: number;
|
|
600
|
+
hostId: string;
|
|
601
|
+
};
|
|
602
|
+
};
|
|
603
|
+
export type SandboxTouchResult = {
|
|
604
|
+
touched: boolean;
|
|
605
|
+
status: SandboxLeaseStatus | "missing";
|
|
606
|
+
};
|
|
607
|
+
/** `unsaved` is a stop that ended the machine without capturing it: the workspace's latest changes are lost, its last checkpoint kept. */
|
|
608
|
+
export type SandboxMutationResult = {
|
|
609
|
+
ok: true;
|
|
610
|
+
status: SandboxLeaseStatus;
|
|
611
|
+
unsaved?: true;
|
|
612
|
+
} | {
|
|
613
|
+
ok: false;
|
|
614
|
+
reason: string;
|
|
615
|
+
};
|
|
616
|
+
export type SandboxSnapshotManagerResult = ({
|
|
617
|
+
ok: true;
|
|
618
|
+
} & SandboxSnapshotResult) | {
|
|
619
|
+
ok: false;
|
|
620
|
+
reason: string;
|
|
621
|
+
};
|
|
622
|
+
export type SandboxGarbageCollectResult = {
|
|
623
|
+
destroyed: SandboxTarget[];
|
|
624
|
+
kept: SandboxTarget[];
|
|
625
|
+
skipped: Array<{
|
|
626
|
+
target: SandboxTarget;
|
|
627
|
+
reason: string;
|
|
628
|
+
}>;
|
|
629
|
+
failed: Array<{
|
|
630
|
+
target: SandboxTarget;
|
|
631
|
+
error: string;
|
|
632
|
+
}>;
|
|
633
|
+
/**
|
|
634
|
+
* Set when the driver cannot enumerate provider state, so the four arrays
|
|
635
|
+
* above mean "we could not look" rather than "nothing is orphaned". Absent on
|
|
636
|
+
* a real sweep. Callers must treat it as a FAILED sweep: four empty arrays
|
|
637
|
+
* reported as success is the exact defect finding B1 names.
|
|
638
|
+
*/
|
|
639
|
+
listingUnsupported?: true;
|
|
640
|
+
/** Driver id, so an ops report can name which driver could not look. */
|
|
641
|
+
driver?: string;
|
|
642
|
+
/** Provider accounts a sweep over several keys could not open, so nothing on them was examined. */
|
|
643
|
+
unreachable?: Array<{
|
|
644
|
+
driver: string;
|
|
645
|
+
error: string;
|
|
646
|
+
}>;
|
|
647
|
+
};
|
|
648
|
+
/**
|
|
649
|
+
* A driver's `list()` may throw this shape to say "my backing service cannot
|
|
650
|
+
* enumerate", as distinct from "listing failed this time". `garbageCollect()`
|
|
651
|
+
* converts it into `listingUnsupported` (a loud, non-fatal capability gap);
|
|
652
|
+
* every other error propagates as a real sweep failure.
|
|
653
|
+
*
|
|
654
|
+
* Structural rather than an `instanceof` check on purpose: drivers are published
|
|
655
|
+
* as separate entry points and bundlers duplicate class identities, so a
|
|
656
|
+
* nominal check would silently fall through to "transient failure" in exactly
|
|
657
|
+
* the deployment that needs the distinction.
|
|
658
|
+
*/
|
|
659
|
+
export type SandboxListingUnsupported = {
|
|
660
|
+
listingUnsupported: true;
|
|
661
|
+
};
|
|
662
|
+
export declare function isSandboxListingUnsupported(err: unknown): err is SandboxListingUnsupported;
|
|
663
|
+
export type SandboxManagerOptions = {
|
|
664
|
+
leaseStore: SandboxLeaseStore;
|
|
665
|
+
driver: SandboxDriver;
|
|
666
|
+
staleAfterMs?: number;
|
|
667
|
+
retryAfterMs?: number;
|
|
668
|
+
retryDelayMs?: (retryCount: number) => number;
|
|
669
|
+
maxRetryCount?: number;
|
|
670
|
+
retryCapCooldownMs?: number;
|
|
671
|
+
now?: () => number;
|
|
672
|
+
appLabel?: string;
|
|
673
|
+
/**
|
|
674
|
+
* Sink for the loud half of "enforce where we can, document where we can't".
|
|
675
|
+
* Called at composition time and again every time a restricted policy is
|
|
676
|
+
* withheld from a driver that cannot enforce it.
|
|
677
|
+
*
|
|
678
|
+
* Defaults to `console.warn(event.message)`. Pass your own to route the gap
|
|
679
|
+
* into a telemetry pipeline instead — but note the default is deliberately
|
|
680
|
+
* unconditional: an operator who never wires a sink still gets the warning.
|
|
681
|
+
* Pass `() => {}` only if you have another way to surface it.
|
|
682
|
+
*/
|
|
683
|
+
onEgressUnenforced?: (event: SandboxEgressUnenforcedEvent) => void;
|
|
684
|
+
/** Receives each start phase as it ends; a sink that throws is ignored. */
|
|
685
|
+
onStartPhase?: (event: SandboxStartPhaseEvent) => void;
|
|
686
|
+
onLifecycle?: (event: SandboxLifecycleEvent) => void;
|
|
687
|
+
};
|