@molecule/api-code-sandbox-flyio 1.2.7 → 1.2.9
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 +1120 -1029
- package/dist/api.d.ts +6 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +5 -0
- package/dist/index.d.ts +20 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -6
- package/dist/provider.d.ts +17 -0
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +185 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
|
|
|
3
3
|
Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
|
|
4
4
|
Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
|
|
5
5
|
To change this document, edit the module-level JSDoc in src/index.ts.
|
|
6
|
-
Generated: 2026-10-
|
|
6
|
+
Generated: 2026-10-16T16:08:45.819Z
|
|
7
7
|
-->
|
|
8
8
|
|
|
9
9
|
# @molecule/api-code-sandbox-flyio
|
|
@@ -82,6 +82,67 @@ npm install @molecule/api-code-sandbox-flyio @aws-sdk/client-s3 @aws-sdk/s3-requ
|
|
|
82
82
|
|
|
83
83
|
### Interfaces
|
|
84
84
|
|
|
85
|
+
#### `FlyRequestOptions`
|
|
86
|
+
|
|
87
|
+
Options for a single Fly API request.
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
interface FlyRequestOptions {
|
|
91
|
+
/** HTTP method. Defaults to `GET`. */
|
|
92
|
+
method?: string
|
|
93
|
+
/** JSON request body. Omitted entirely when undefined. */
|
|
94
|
+
body?: unknown
|
|
95
|
+
/** Per-request timeout in ms. Defaults to the client's configured timeout. */
|
|
96
|
+
timeoutMs?: number
|
|
97
|
+
/** Max attempts for a retryable failure. Defaults to 4. Pass 1 to disable retries. */
|
|
98
|
+
attempts?: number
|
|
99
|
+
/**
|
|
100
|
+
* Treat these HTTP statuses as a successful `null` result instead of throwing.
|
|
101
|
+
* Used for idempotent deletes and existence checks (`[404]`).
|
|
102
|
+
*/
|
|
103
|
+
nullOn?: number[]
|
|
104
|
+
/**
|
|
105
|
+
* Extra HTTP statuses to treat as transient (retry) beyond the default
|
|
106
|
+
* `429`/`5xx`. The exec endpoint passes `[404]`: a machine-not-found for a
|
|
107
|
+
* Machine we just created and are actively driving is Fly API inconsistency
|
|
108
|
+
* under exec-burst load, not a real absence, so it is worth repeating.
|
|
109
|
+
*/
|
|
110
|
+
retryStatuses?: number[]
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### `FlyApiClientOptions`
|
|
115
|
+
|
|
116
|
+
Constructor options for {@link FlyApiClient}.
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
interface FlyApiClientOptions {
|
|
120
|
+
/** Bearer token. Resolved lazily so env/secrets can land after construction. */
|
|
121
|
+
token: () => string | undefined
|
|
122
|
+
/** Base URL including `/v1`. */
|
|
123
|
+
baseUrl: string
|
|
124
|
+
/** Default per-request timeout (ms). */
|
|
125
|
+
timeoutMs?: number
|
|
126
|
+
/** Injectable fetch, for tests. Defaults to the global `fetch`. */
|
|
127
|
+
fetchImpl?: typeof fetch
|
|
128
|
+
/** Injectable sleep, for tests. Defaults to `setTimeout`. */
|
|
129
|
+
sleep?: (ms: number) => Promise<void>
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
#### `EgressProbeTarget`
|
|
134
|
+
|
|
135
|
+
One literal host:port the probe attempts a raw TCP connection to.
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
interface EgressProbeTarget {
|
|
139
|
+
/** A literal IPv4 or IPv6 address. Never a hostname — see {@link parseEgressProbeTargets}. */
|
|
140
|
+
host: string
|
|
141
|
+
/** TCP port. */
|
|
142
|
+
port: number
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
85
146
|
#### `EgressProbeContext`
|
|
86
147
|
|
|
87
148
|
Context {@link verdictForProbeExit} renders its operator-facing messages from.
|
|
@@ -115,72 +176,106 @@ interface EgressProbeContext {
|
|
|
115
176
|
}
|
|
116
177
|
```
|
|
117
178
|
|
|
118
|
-
#### `
|
|
179
|
+
#### `StoredObject`
|
|
119
180
|
|
|
120
|
-
One
|
|
181
|
+
One object as this bond needs to see it.
|
|
121
182
|
|
|
122
183
|
```typescript
|
|
123
|
-
interface
|
|
124
|
-
/**
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
|
|
184
|
+
interface StoredObject {
|
|
185
|
+
/** Full key, including the configured prefix. */
|
|
186
|
+
key: string
|
|
187
|
+
/** Size in bytes. */
|
|
188
|
+
size: number
|
|
189
|
+
/** Last-modified timestamp, ISO 8601, or `null` when the store did not report one. */
|
|
190
|
+
lastModified: string | null
|
|
128
191
|
}
|
|
129
192
|
```
|
|
130
193
|
|
|
131
|
-
#### `
|
|
132
|
-
|
|
133
|
-
Constructor options for {@link FlyApiClient}.
|
|
134
|
-
|
|
135
|
-
```typescript
|
|
136
|
-
interface FlyApiClientOptions {
|
|
137
|
-
/** Bearer token. Resolved lazily so env/secrets can land after construction. */
|
|
138
|
-
token: () => string | undefined
|
|
139
|
-
/** Base URL including `/v1`. */
|
|
140
|
-
baseUrl: string
|
|
141
|
-
/** Default per-request timeout (ms). */
|
|
142
|
-
timeoutMs?: number
|
|
143
|
-
/** Injectable fetch, for tests. Defaults to the global `fetch`. */
|
|
144
|
-
fetchImpl?: typeof fetch
|
|
145
|
-
/** Injectable sleep, for tests. Defaults to `setTimeout`. */
|
|
146
|
-
sleep?: (ms: number) => Promise<void>
|
|
147
|
-
}
|
|
148
|
-
```
|
|
194
|
+
#### `ObjectStore`
|
|
149
195
|
|
|
150
|
-
|
|
196
|
+
The object-store operations the template capability needs.
|
|
151
197
|
|
|
152
|
-
|
|
198
|
+
Declared as an interface so `templates.ts` stays transport-free and its tests
|
|
199
|
+
can drive a fake store, the same way `exec.ts` takes an injected `RawExec`.
|
|
153
200
|
|
|
154
201
|
```typescript
|
|
155
|
-
interface
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
202
|
+
interface ObjectStore {
|
|
203
|
+
/** Human-readable identification of the store, for error messages. */
|
|
204
|
+
readonly describe: string
|
|
205
|
+
/** Bucket holding the templates. Used to render the opaque `SandboxTemplate.ref`. */
|
|
206
|
+
readonly bucket: string
|
|
207
|
+
/** Key prefix every template object lives under, with no trailing slash. */
|
|
208
|
+
readonly prefix: string
|
|
209
|
+
/**
|
|
210
|
+
* Every object under a prefix, following pagination to the end.
|
|
211
|
+
* THROWS on failure — a caller that deletes must never read a failed query as
|
|
212
|
+
* "nothing is there".
|
|
213
|
+
*/
|
|
214
|
+
list(prefix: string): Promise<StoredObject[]>
|
|
215
|
+
/** One object's metadata, or `null` when it does not exist. Throws on any other failure. */
|
|
216
|
+
head(key: string): Promise<StoredObject | null>
|
|
217
|
+
/** An object's body as text, or `null` when it does not exist. Throws on any other failure. */
|
|
218
|
+
getText(key: string): Promise<string | null>
|
|
219
|
+
/** Write a small text object. */
|
|
220
|
+
putText(key: string, body: string, contentType: string): Promise<void>
|
|
221
|
+
/** Delete objects. Deleting a key that is not there is a success. */
|
|
222
|
+
remove(keys: string[]): Promise<void>
|
|
223
|
+
/** A presigned URL a sandbox can `PUT` an archive to. */
|
|
224
|
+
presignPut(key: string, expiresInSeconds: number): Promise<string>
|
|
225
|
+
/** A presigned URL a sandbox can `GET` an archive from. */
|
|
226
|
+
presignGet(key: string, expiresInSeconds: number): Promise<string>
|
|
159
227
|
}
|
|
160
228
|
```
|
|
161
229
|
|
|
162
|
-
#### `
|
|
230
|
+
#### `TemplateManifest`
|
|
163
231
|
|
|
164
|
-
|
|
232
|
+
The control-plane-written record of a template. Its existence defines the template's existence.
|
|
165
233
|
|
|
166
234
|
```typescript
|
|
167
|
-
interface
|
|
168
|
-
/**
|
|
169
|
-
|
|
170
|
-
|
|
235
|
+
interface TemplateManifest {
|
|
236
|
+
/** Schema version of this record. */
|
|
237
|
+
schema: number
|
|
238
|
+
/** The caller's template id. */
|
|
239
|
+
id: string
|
|
240
|
+
/**
|
|
241
|
+
* Absolute paths captured, exactly as the caller named them.
|
|
242
|
+
*
|
|
243
|
+
* This is the security-relevant field: on restore these become `tar`'s member
|
|
244
|
+
* selectors, so the extraction surface is control-plane policy rather than
|
|
245
|
+
* anything the captured sandbox could influence.
|
|
246
|
+
*/
|
|
247
|
+
capturePaths: string[]
|
|
248
|
+
/** When the capture completed, ISO 8601. */
|
|
249
|
+
createdAt: string
|
|
250
|
+
/** Archive size in bytes as observed in the store at capture time. */
|
|
251
|
+
sizeBytes: number
|
|
252
|
+
/** Free-form label recorded at capture time. */
|
|
253
|
+
label?: string
|
|
171
254
|
}
|
|
172
255
|
```
|
|
173
256
|
|
|
174
|
-
#### `
|
|
257
|
+
#### `TemplateContext`
|
|
175
258
|
|
|
176
|
-
|
|
259
|
+
What the template capability needs from the provider.
|
|
177
260
|
|
|
178
261
|
```typescript
|
|
179
|
-
interface
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
262
|
+
interface TemplateContext {
|
|
263
|
+
/** The configured object store, or `null` when the operator has not configured one. */
|
|
264
|
+
store: ObjectStore | null
|
|
265
|
+
/** Runs a shell command inside a Machine and returns its result. */
|
|
266
|
+
exec(app: string, machineId: string, command: string, timeoutMs: number): Promise<ExecResult>
|
|
267
|
+
/** Blocks until a Machine is running, so it can be exec'd into. */
|
|
268
|
+
ensureStarted(app: string, machineId: string): Promise<void>
|
|
269
|
+
/** Splits a composite sandbox id into its app and Machine id. */
|
|
270
|
+
parseSandboxId(id: string): { app: string; machineId: string }
|
|
271
|
+
/** Lifetime of a presigned capture/restore URL, in seconds. */
|
|
272
|
+
presignExpirySeconds: number
|
|
273
|
+
/** Wall-clock budget for a capture or restore transfer, in ms. */
|
|
274
|
+
transferTimeoutMs: number
|
|
275
|
+
/** Largest archive this provider will store, in bytes. */
|
|
276
|
+
maxArchiveBytes: number
|
|
277
|
+
warn?: (message: string, meta?: Record<string, unknown>) => void
|
|
278
|
+
debug?: (message: string, meta?: Record<string, unknown>) => void
|
|
184
279
|
}
|
|
185
280
|
```
|
|
186
281
|
|
|
@@ -351,223 +446,131 @@ interface FlyioConfig {
|
|
|
351
446
|
* anything publicly — see the `flycast.ts` module description.
|
|
352
447
|
*
|
|
353
448
|
* The declared PORT is load-bearing twice: it is unioned into
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
egressProbeTargets?: string[]
|
|
383
|
-
/** Per-connection timeout for the egress probe, in ms. Falls back to `SANDBOX_EGRESS_PROBE_TIMEOUT_MS`, then 3000. */
|
|
384
|
-
egressProbeTimeoutMs?: number
|
|
385
|
-
/**
|
|
386
|
-
* Image the throwaway `verifyEgress()` probe Machine runs. Must contain `node`
|
|
387
|
-
* and `sleep`. Defaults to the configured sandbox base image, so the probe
|
|
388
|
-
* observes egress from the same image real sandboxes run.
|
|
389
|
-
*/
|
|
390
|
-
egressProbeImage?: string
|
|
391
|
-
/**
|
|
392
|
-
* Bucket holding sandbox templates. Falls back to `SANDBOX_TEMPLATE_BUCKET`,
|
|
393
|
-
* then `BUCKET_NAME` (which `fly storage create` sets).
|
|
394
|
-
*
|
|
395
|
-
* Templates are the warm-start capability: Fly cannot commit a running Machine
|
|
396
|
-
* to an image, so a template is a tar archive in S3-compatible object storage.
|
|
397
|
-
* Without a bucket (and both credentials) the template methods throw an
|
|
398
|
-
* actionable error naming these settings, and `SandboxConfig.templateId` fails
|
|
399
|
-
* rather than silently booting the base image.
|
|
400
|
-
*/
|
|
401
|
-
templateBucket?: string
|
|
402
|
-
/**
|
|
403
|
-
* S3 endpoint for {@link FlyioConfig.templateBucket}, e.g. Tigris's
|
|
404
|
-
* `https://t3.storage.dev`. Falls back to `SANDBOX_TEMPLATE_ENDPOINT`, then
|
|
405
|
-
* `AWS_ENDPOINT_URL_S3`. Omit for AWS S3 itself.
|
|
406
|
-
*/
|
|
407
|
-
templateEndpoint?: string
|
|
408
|
-
/**
|
|
409
|
-
* Region used to sign template-store requests. Falls back to
|
|
410
|
-
* `SANDBOX_TEMPLATE_REGION`, then `AWS_REGION`, then `auto` — which Tigris and
|
|
411
|
-
* most S3-compatible stores accept, and which the AWS SDK still requires in
|
|
412
|
-
* order to build a signature.
|
|
413
|
-
*/
|
|
414
|
-
templateRegion?: string
|
|
415
|
-
/** Access key for the template store. Falls back to `SANDBOX_TEMPLATE_ACCESS_KEY_ID`, then `AWS_ACCESS_KEY_ID`. */
|
|
416
|
-
templateAccessKeyId?: string
|
|
417
|
-
/** Secret key for the template store. Falls back to `SANDBOX_TEMPLATE_SECRET_ACCESS_KEY`, then `AWS_SECRET_ACCESS_KEY`. */
|
|
418
|
-
templateSecretAccessKey?: string
|
|
419
|
-
/** Session token for temporary template-store credentials. Falls back to `SANDBOX_TEMPLATE_SESSION_TOKEN`, then `AWS_SESSION_TOKEN`. */
|
|
420
|
-
templateSessionToken?: string
|
|
421
|
-
/**
|
|
422
|
-
* Key prefix every template object lives under. Falls back to
|
|
423
|
-
* `SANDBOX_TEMPLATE_PREFIX`, then `molecule-sandbox-templates`. Give the
|
|
424
|
-
* templates their own prefix (or their own bucket): `removeTemplate` deletes
|
|
425
|
-
* every key under a template's prefix.
|
|
426
|
-
*/
|
|
427
|
-
templatePrefix?: string
|
|
428
|
-
/**
|
|
429
|
-
* Address the template bucket path-style (`https://endpoint/bucket/key`)
|
|
430
|
-
* instead of virtual-host style. Falls back to
|
|
431
|
-
* `SANDBOX_TEMPLATE_FORCE_PATH_STYLE=true`. Needed by stores that do not serve
|
|
432
|
-
* `<bucket>.<endpoint>`; Tigris and AWS S3 do not need it.
|
|
433
|
-
*/
|
|
434
|
-
templateForcePathStyle?: boolean
|
|
449
|
+
// … truncated — the full declaration is in node_modules/@molecule/api-code-sandbox-flyio/dist/types.d.ts
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
#### `ProcessEnv`
|
|
453
|
+
|
|
454
|
+
Environment variables the Fly.io sandbox provider reads. Every one is
|
|
455
|
+
overridden by the matching {@link FlyioConfig} field.
|
|
456
|
+
|
|
457
|
+
```typescript
|
|
458
|
+
interface ProcessEnv {
|
|
459
|
+
/** Fly API token. Also accepted as `FLY_ACCESS_TOKEN`. */
|
|
460
|
+
FLY_API_TOKEN?: string
|
|
461
|
+
/** Fly API token (flyctl's variable name). Used when `FLY_API_TOKEN` is unset. */
|
|
462
|
+
FLY_ACCESS_TOKEN?: string
|
|
463
|
+
/** Machines API base URL. `/v1` is appended when the value carries no path. */
|
|
464
|
+
FLY_API_HOSTNAME?: string
|
|
465
|
+
/** Fly organization slug owning the sandbox apps (default `personal`). */
|
|
466
|
+
FLY_ORG_SLUG?: string
|
|
467
|
+
/** Shared Fly app name, used only when `FLY_SANDBOX_APP_PER_PROJECT=false`. */
|
|
468
|
+
FLY_SANDBOX_APP?: string
|
|
469
|
+
/** Per-project Fly app name prefix (default `mol-sandbox`). */
|
|
470
|
+
FLY_SANDBOX_APP_PREFIX?: string
|
|
471
|
+
/** `false` disables per-project apps (refused in production). */
|
|
472
|
+
FLY_SANDBOX_APP_PER_PROJECT?: string
|
|
473
|
+
/** Fly region for Machines and volumes (default `iad`). */
|
|
474
|
+
FLY_REGION?: string
|
|
475
|
+
/** OCI image for sandbox Machines. */
|
|
476
|
+
FLY_SANDBOX_IMAGE?: string
|
|
435
477
|
/**
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
* S3 "checks the expiration date and time of a signed URL at the time of the
|
|
440
|
-
* HTTP request", so a download already in progress is not cut off.
|
|
478
|
+
* Comma-separated `protocol:port` pairs a sandbox may open outbound
|
|
479
|
+
* connections on (e.g. `tcp:3128,udp:53`). Set it to make this provider apply
|
|
480
|
+
* a Fly network policy to every sandbox app; unset means no policy.
|
|
441
481
|
*/
|
|
442
|
-
|
|
482
|
+
FLY_SANDBOX_EGRESS_ALLOWED_PORTS?: string
|
|
443
483
|
/**
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
484
|
+
* Comma-separated `<fly-app>:<port>` pairs every sandbox must reach across its
|
|
485
|
+
* per-project 6PN, e.g. `molecule-pg-tenant:5432,molecule-api:3129`. Each one
|
|
486
|
+
* gets a Flycast private address allocated into the project's network at app
|
|
487
|
+
* creation and released on destroy. Overridden by `config.privateServices`.
|
|
447
488
|
*/
|
|
448
|
-
|
|
489
|
+
FLY_SANDBOX_PRIVATE_SERVICES?: string
|
|
449
490
|
/**
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
* uploads with exactly one presigned `PUT`. The capture refuses before
|
|
453
|
-
* spending the bandwidth.
|
|
491
|
+
* Comma-separated literal `ip:port` targets the egress probe attempts. Shared
|
|
492
|
+
* with the Docker bond so a provider swap keeps the same configuration.
|
|
454
493
|
*/
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
/**
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
|
|
476
|
-
/**
|
|
477
|
-
|
|
478
|
-
/**
|
|
479
|
-
|
|
494
|
+
SANDBOX_EGRESS_PROBE_TARGETS?: string
|
|
495
|
+
/** Per-connection timeout for the egress probe, in ms (default 3000). */
|
|
496
|
+
SANDBOX_EGRESS_PROBE_TIMEOUT_MS?: string
|
|
497
|
+
/** Bucket holding sandbox templates. Overridden by `config.templateBucket`. */
|
|
498
|
+
SANDBOX_TEMPLATE_BUCKET?: string
|
|
499
|
+
/** Bucket name as exported by `fly storage create`. Used when `SANDBOX_TEMPLATE_BUCKET` is unset. */
|
|
500
|
+
BUCKET_NAME?: string
|
|
501
|
+
/** S3 endpoint for the template bucket. Overridden by `config.templateEndpoint`. */
|
|
502
|
+
SANDBOX_TEMPLATE_ENDPOINT?: string
|
|
503
|
+
/** S3 endpoint as exported by `fly storage create`. Used when `SANDBOX_TEMPLATE_ENDPOINT` is unset. */
|
|
504
|
+
AWS_ENDPOINT_URL_S3?: string
|
|
505
|
+
/** Signing region for the template store (default `auto`). Overridden by `config.templateRegion`. */
|
|
506
|
+
SANDBOX_TEMPLATE_REGION?: string
|
|
507
|
+
/** Signing region, standard AWS variable. Used when `SANDBOX_TEMPLATE_REGION` is unset. */
|
|
508
|
+
AWS_REGION?: string
|
|
509
|
+
/** Access key for the template store. Overridden by `config.templateAccessKeyId`. */
|
|
510
|
+
SANDBOX_TEMPLATE_ACCESS_KEY_ID?: string
|
|
511
|
+
/** Access key, standard AWS variable (also what `fly storage create` sets). */
|
|
512
|
+
AWS_ACCESS_KEY_ID?: string
|
|
513
|
+
/** Secret key for the template store. Overridden by `config.templateSecretAccessKey`. */
|
|
514
|
+
SANDBOX_TEMPLATE_SECRET_ACCESS_KEY?: string
|
|
515
|
+
/** Secret key, standard AWS variable (also what `fly storage create` sets). */
|
|
516
|
+
AWS_SECRET_ACCESS_KEY?: string
|
|
517
|
+
/** Session token for the template store. Overridden by `config.templateSessionToken`. */
|
|
518
|
+
SANDBOX_TEMPLATE_SESSION_TOKEN?: string
|
|
519
|
+
/** Session token, standard AWS variable. */
|
|
520
|
+
AWS_SESSION_TOKEN?: string
|
|
521
|
+
/** Key prefix for template objects (default `molecule-sandbox-templates`). */
|
|
522
|
+
SANDBOX_TEMPLATE_PREFIX?: string
|
|
523
|
+
/** `true` addresses the template bucket path-style rather than virtual-host style. */
|
|
524
|
+
SANDBOX_TEMPLATE_FORCE_PATH_STYLE?: string
|
|
480
525
|
}
|
|
481
526
|
```
|
|
482
527
|
|
|
483
|
-
#### `
|
|
528
|
+
#### `FlyPrivateService`
|
|
484
529
|
|
|
485
|
-
|
|
530
|
+
One app made reachable from every sandbox's per-project 6PN through a Flycast
|
|
531
|
+
private address.
|
|
532
|
+
|
|
533
|
+
See the `flycast.ts` module description for why this exists at all: apps on
|
|
534
|
+
separate 6PNs "can never communicate unless explicitly configured to do so",
|
|
535
|
+
and this declaration IS that explicit configuration.
|
|
486
536
|
|
|
487
537
|
```typescript
|
|
488
|
-
interface
|
|
489
|
-
id: string
|
|
490
|
-
name?: string
|
|
491
|
-
/** Optional so a response that omits it degrades to `created` rather than crashing the mapper. */
|
|
492
|
-
state?: FlyMachineState
|
|
493
|
-
region?: string
|
|
494
|
-
private_ip?: string
|
|
495
|
-
config?: FlyMachineConfig
|
|
496
|
-
/** RFC 3339 creation time, as Fly reports it. */
|
|
497
|
-
created_at?: string
|
|
538
|
+
interface FlyPrivateService {
|
|
498
539
|
/**
|
|
499
|
-
*
|
|
500
|
-
*
|
|
501
|
-
*
|
|
540
|
+
* The Fly app to allocate a Flycast address ON. The sandbox reaches it at
|
|
541
|
+
* `<app>.flycast`. It must already exist, must carry a services block, and
|
|
542
|
+
* must be in the same organization.
|
|
502
543
|
*/
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
Machine configuration (`fly.MachineConfig`) — the subset this provider writes.
|
|
510
|
-
|
|
511
|
-
```typescript
|
|
512
|
-
interface FlyMachineConfig {
|
|
513
|
-
image: string
|
|
514
|
-
env?: Record<string, string>
|
|
515
|
-
metadata?: Record<string, string>
|
|
516
|
-
guest?: { cpus?: number; cpu_kind?: string; memory_mb?: number }
|
|
517
|
-
mounts?: Array<{ volume?: string; name?: string; path: string }>
|
|
518
|
-
services?: FlyMachineService[]
|
|
519
|
-
restart?: { policy?: 'no' | 'always' | 'on-failure' | 'spot-price'; max_retries?: number }
|
|
520
|
-
auto_destroy?: boolean
|
|
521
|
-
init?: { exec?: string[]; entrypoint?: string[]; cmd?: string[]; tty?: boolean }
|
|
522
|
-
}
|
|
523
|
-
```
|
|
524
|
-
|
|
525
|
-
#### `FlyMachineEvent`
|
|
526
|
-
|
|
527
|
-
One entry of {@link FlyMachine.events}.
|
|
528
|
-
|
|
529
|
-
```typescript
|
|
530
|
-
interface FlyMachineEvent {
|
|
531
|
-
/** `start`, `launch`, `exit`, … */
|
|
532
|
-
type?: string
|
|
533
|
-
status?: string
|
|
534
|
-
/** Epoch milliseconds. */
|
|
535
|
-
timestamp?: number
|
|
544
|
+
app: string
|
|
545
|
+
/**
|
|
546
|
+
* The TCP port a sandbox dials it at. Unioned into the egress network policy,
|
|
547
|
+
* and checked against every `.flycast` URL in a sandbox's environment.
|
|
548
|
+
*/
|
|
549
|
+
port: number
|
|
536
550
|
}
|
|
537
551
|
```
|
|
538
552
|
|
|
539
|
-
#### `
|
|
540
|
-
|
|
541
|
-
A Fly Proxy service attached to a Machine (`fly.MachineService`).
|
|
542
|
-
|
|
543
|
-
```typescript
|
|
544
|
-
interface FlyMachineService {
|
|
545
|
-
protocol: string
|
|
546
|
-
internal_port: number
|
|
547
|
-
ports: Array<{ port: number; handlers?: string[]; force_https?: boolean }>
|
|
548
|
-
autostart?: boolean
|
|
549
|
-
autostop?: 'off' | 'stop' | 'suspend'
|
|
550
|
-
min_machines_running?: number
|
|
551
|
-
}
|
|
552
|
-
```
|
|
553
|
+
#### `FlyIpAssignment`
|
|
553
554
|
|
|
554
|
-
|
|
555
|
+
An IP assignment, as returned by `POST`/`GET /v1/apps/{app}/ip_assignments`
|
|
556
|
+
(`IPAssignment` in https://docs.machines.dev/openapi.json).
|
|
555
557
|
|
|
556
|
-
|
|
557
|
-
`
|
|
558
|
-
|
|
559
|
-
|
|
558
|
+
Note what is NOT here: the assignment's **network**. Fly's own schema is
|
|
559
|
+
`{created_at, ip, region, service_name, shared}`, so a listing cannot tell you
|
|
560
|
+
which 6PN a private address serves — which is why this provider records the
|
|
561
|
+
addresses it allocated in the sandbox Machine's metadata instead of
|
|
562
|
+
rediscovering them.
|
|
560
563
|
|
|
561
564
|
```typescript
|
|
562
|
-
interface
|
|
563
|
-
/**
|
|
564
|
-
|
|
565
|
-
/**
|
|
566
|
-
|
|
567
|
-
/**
|
|
568
|
-
|
|
569
|
-
/**
|
|
570
|
-
|
|
565
|
+
interface FlyIpAssignment {
|
|
566
|
+
/** The allocated address. Required for a later `DELETE .../ip_assignments/{ip}`. */
|
|
567
|
+
ip?: string
|
|
568
|
+
/** Region the address was allocated in, when Fly reports one. */
|
|
569
|
+
region?: string
|
|
570
|
+
/** Service the address is bound to, when the assignment names one. */
|
|
571
|
+
service_name?: string
|
|
572
|
+
/** Whether the address is shared (Anycast v4) rather than dedicated. */
|
|
573
|
+
shared?: boolean
|
|
571
574
|
}
|
|
572
575
|
```
|
|
573
576
|
|
|
@@ -636,266 +639,169 @@ interface FlyNetworkPolicySelector {
|
|
|
636
639
|
}
|
|
637
640
|
```
|
|
638
641
|
|
|
639
|
-
#### `
|
|
642
|
+
#### `FlyNetworkPolicy`
|
|
640
643
|
|
|
641
|
-
A
|
|
644
|
+
A Fly network policy, as accepted by
|
|
645
|
+
`POST /v1/apps/{app}/network_policies`. The endpoint is documented in Fly's
|
|
646
|
+
guide and announcement but is NOT in its OpenAPI specification, so only the
|
|
647
|
+
fields those two documents show are modelled.
|
|
642
648
|
|
|
643
649
|
```typescript
|
|
644
|
-
interface
|
|
645
|
-
|
|
650
|
+
interface FlyNetworkPolicy {
|
|
651
|
+
/** Present to UPDATE an existing policy; omitted to create one. */
|
|
652
|
+
id?: string
|
|
653
|
+
/** Policy name, unique per app in practice. */
|
|
654
|
+
name: string
|
|
655
|
+
/** Machines the policy applies to. */
|
|
656
|
+
selector: FlyNetworkPolicySelector
|
|
657
|
+
/** The allow rules. */
|
|
658
|
+
rules: FlyNetworkPolicyRule[]
|
|
646
659
|
}
|
|
647
660
|
```
|
|
648
661
|
|
|
649
|
-
#### `
|
|
650
|
-
|
|
651
|
-
One app made reachable from every sandbox's per-project 6PN through a Flycast
|
|
652
|
-
private address.
|
|
662
|
+
#### `FlyMachine`
|
|
653
663
|
|
|
654
|
-
|
|
655
|
-
separate 6PNs "can never communicate unless explicitly configured to do so",
|
|
656
|
-
and this declaration IS that explicit configuration.
|
|
664
|
+
A Fly Machine, as returned by the Machines API (fields this provider reads).
|
|
657
665
|
|
|
658
666
|
```typescript
|
|
659
|
-
interface
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
667
|
+
interface FlyMachine {
|
|
668
|
+
id: string
|
|
669
|
+
name?: string
|
|
670
|
+
/** Optional so a response that omits it degrades to `created` rather than crashing the mapper. */
|
|
671
|
+
state?: FlyMachineState
|
|
672
|
+
region?: string
|
|
673
|
+
private_ip?: string
|
|
674
|
+
config?: FlyMachineConfig
|
|
675
|
+
/** RFC 3339 creation time, as Fly reports it. */
|
|
676
|
+
created_at?: string
|
|
666
677
|
/**
|
|
667
|
-
*
|
|
668
|
-
*
|
|
678
|
+
* Lifecycle events, newest first. The ONLY place Fly records when a Machine
|
|
679
|
+
* was first started — there is no `started_at` field — which is what
|
|
680
|
+
* distinguishes a stopped Machine from one that was created and never ran.
|
|
669
681
|
*/
|
|
670
|
-
|
|
682
|
+
events?: FlyMachineEvent[]
|
|
671
683
|
}
|
|
672
684
|
```
|
|
673
685
|
|
|
674
|
-
#### `
|
|
686
|
+
#### `FlyMachineEvent`
|
|
675
687
|
|
|
676
|
-
|
|
688
|
+
One entry of {@link FlyMachine.events}.
|
|
677
689
|
|
|
678
690
|
```typescript
|
|
679
|
-
interface
|
|
680
|
-
/**
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
timeoutMs?: number
|
|
686
|
-
/** Max attempts for a retryable failure. Defaults to 4. Pass 1 to disable retries. */
|
|
687
|
-
attempts?: number
|
|
688
|
-
/**
|
|
689
|
-
* Treat these HTTP statuses as a successful `null` result instead of throwing.
|
|
690
|
-
* Used for idempotent deletes and existence checks (`[404]`).
|
|
691
|
-
*/
|
|
692
|
-
nullOn?: number[]
|
|
693
|
-
/**
|
|
694
|
-
* Extra HTTP statuses to treat as transient (retry) beyond the default
|
|
695
|
-
* `429`/`5xx`. The exec endpoint passes `[404]`: a machine-not-found for a
|
|
696
|
-
* Machine we just created and are actively driving is Fly API inconsistency
|
|
697
|
-
* under exec-burst load, not a real absence, so it is worth repeating.
|
|
698
|
-
*/
|
|
699
|
-
retryStatuses?: number[]
|
|
691
|
+
interface FlyMachineEvent {
|
|
692
|
+
/** `start`, `launch`, `exit`, … */
|
|
693
|
+
type?: string
|
|
694
|
+
status?: string
|
|
695
|
+
/** Epoch milliseconds. */
|
|
696
|
+
timestamp?: number
|
|
700
697
|
}
|
|
701
698
|
```
|
|
702
699
|
|
|
703
|
-
#### `
|
|
700
|
+
#### `FlyApp`
|
|
704
701
|
|
|
705
|
-
|
|
702
|
+
An app as returned by `GET /v1/apps?org_slug=…` (the fields this provider reads).
|
|
706
703
|
|
|
707
704
|
```typescript
|
|
708
|
-
interface
|
|
709
|
-
id
|
|
705
|
+
interface FlyApp {
|
|
706
|
+
id?: string
|
|
710
707
|
name: string
|
|
711
|
-
|
|
712
|
-
size_gb?: number
|
|
713
|
-
region?: string
|
|
714
|
-
attached_machine_id?: string
|
|
708
|
+
machine_count?: number
|
|
715
709
|
}
|
|
716
710
|
```
|
|
717
711
|
|
|
718
|
-
#### `
|
|
719
|
-
|
|
720
|
-
The object-store operations the template capability needs.
|
|
712
|
+
#### `FlyAppList`
|
|
721
713
|
|
|
722
|
-
|
|
723
|
-
can drive a fake store, the same way `exec.ts` takes an injected `RawExec`.
|
|
714
|
+
Response body of `GET /v1/apps?org_slug=…`.
|
|
724
715
|
|
|
725
716
|
```typescript
|
|
726
|
-
interface
|
|
727
|
-
/**
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
readonly bucket: string
|
|
731
|
-
/** Key prefix every template object lives under, with no trailing slash. */
|
|
732
|
-
readonly prefix: string
|
|
733
|
-
/**
|
|
734
|
-
* Every object under a prefix, following pagination to the end.
|
|
735
|
-
* THROWS on failure — a caller that deletes must never read a failed query as
|
|
736
|
-
* "nothing is there".
|
|
737
|
-
*/
|
|
738
|
-
list(prefix: string): Promise<StoredObject[]>
|
|
739
|
-
/** One object's metadata, or `null` when it does not exist. Throws on any other failure. */
|
|
740
|
-
head(key: string): Promise<StoredObject | null>
|
|
741
|
-
/** An object's body as text, or `null` when it does not exist. Throws on any other failure. */
|
|
742
|
-
getText(key: string): Promise<string | null>
|
|
743
|
-
/** Write a small text object. */
|
|
744
|
-
putText(key: string, body: string, contentType: string): Promise<void>
|
|
745
|
-
/** Delete objects. Deleting a key that is not there is a success. */
|
|
746
|
-
remove(keys: string[]): Promise<void>
|
|
747
|
-
/** A presigned URL a sandbox can `PUT` an archive to. */
|
|
748
|
-
presignPut(key: string, expiresInSeconds: number): Promise<string>
|
|
749
|
-
/** A presigned URL a sandbox can `GET` an archive from. */
|
|
750
|
-
presignGet(key: string, expiresInSeconds: number): Promise<string>
|
|
717
|
+
interface FlyAppList {
|
|
718
|
+
/** Total apps in the org, which the returned page may not cover. */
|
|
719
|
+
total_apps?: number
|
|
720
|
+
apps?: FlyApp[]
|
|
751
721
|
}
|
|
752
722
|
```
|
|
753
723
|
|
|
754
|
-
#### `
|
|
724
|
+
#### `FlyOrgMachine`
|
|
755
725
|
|
|
756
|
-
|
|
757
|
-
overridden by the matching {@link FlyioConfig} field.
|
|
726
|
+
A Machine as returned by the org-wide list endpoint (`GET /orgs/{org}/machines`).
|
|
758
727
|
|
|
759
728
|
```typescript
|
|
760
|
-
interface
|
|
761
|
-
|
|
762
|
-
FLY_API_TOKEN?: string
|
|
763
|
-
/** Fly API token (flyctl's variable name). Used when `FLY_API_TOKEN` is unset. */
|
|
764
|
-
FLY_ACCESS_TOKEN?: string
|
|
765
|
-
/** Machines API base URL. `/v1` is appended when the value carries no path. */
|
|
766
|
-
FLY_API_HOSTNAME?: string
|
|
767
|
-
/** Fly organization slug owning the sandbox apps (default `personal`). */
|
|
768
|
-
FLY_ORG_SLUG?: string
|
|
769
|
-
/** Shared Fly app name, used only when `FLY_SANDBOX_APP_PER_PROJECT=false`. */
|
|
770
|
-
FLY_SANDBOX_APP?: string
|
|
771
|
-
/** Per-project Fly app name prefix (default `mol-sandbox`). */
|
|
772
|
-
FLY_SANDBOX_APP_PREFIX?: string
|
|
773
|
-
/** `false` disables per-project apps (refused in production). */
|
|
774
|
-
FLY_SANDBOX_APP_PER_PROJECT?: string
|
|
775
|
-
/** Fly region for Machines and volumes (default `iad`). */
|
|
776
|
-
FLY_REGION?: string
|
|
777
|
-
/** OCI image for sandbox Machines. */
|
|
778
|
-
FLY_SANDBOX_IMAGE?: string
|
|
779
|
-
/**
|
|
780
|
-
* Comma-separated `protocol:port` pairs a sandbox may open outbound
|
|
781
|
-
* connections on (e.g. `tcp:3128,udp:53`). Set it to make this provider apply
|
|
782
|
-
* a Fly network policy to every sandbox app; unset means no policy.
|
|
783
|
-
*/
|
|
784
|
-
FLY_SANDBOX_EGRESS_ALLOWED_PORTS?: string
|
|
785
|
-
/**
|
|
786
|
-
* Comma-separated `<fly-app>:<port>` pairs every sandbox must reach across its
|
|
787
|
-
* per-project 6PN, e.g. `molecule-pg-tenant:5432,molecule-api:3129`. Each one
|
|
788
|
-
* gets a Flycast private address allocated into the project's network at app
|
|
789
|
-
* creation and released on destroy. Overridden by `config.privateServices`.
|
|
790
|
-
*/
|
|
791
|
-
FLY_SANDBOX_PRIVATE_SERVICES?: string
|
|
792
|
-
/**
|
|
793
|
-
* Comma-separated literal `ip:port` targets the egress probe attempts. Shared
|
|
794
|
-
* with the Docker bond so a provider swap keeps the same configuration.
|
|
795
|
-
*/
|
|
796
|
-
SANDBOX_EGRESS_PROBE_TARGETS?: string
|
|
797
|
-
/** Per-connection timeout for the egress probe, in ms (default 3000). */
|
|
798
|
-
SANDBOX_EGRESS_PROBE_TIMEOUT_MS?: string
|
|
799
|
-
/** Bucket holding sandbox templates. Overridden by `config.templateBucket`. */
|
|
800
|
-
SANDBOX_TEMPLATE_BUCKET?: string
|
|
801
|
-
/** Bucket name as exported by `fly storage create`. Used when `SANDBOX_TEMPLATE_BUCKET` is unset. */
|
|
802
|
-
BUCKET_NAME?: string
|
|
803
|
-
/** S3 endpoint for the template bucket. Overridden by `config.templateEndpoint`. */
|
|
804
|
-
SANDBOX_TEMPLATE_ENDPOINT?: string
|
|
805
|
-
/** S3 endpoint as exported by `fly storage create`. Used when `SANDBOX_TEMPLATE_ENDPOINT` is unset. */
|
|
806
|
-
AWS_ENDPOINT_URL_S3?: string
|
|
807
|
-
/** Signing region for the template store (default `auto`). Overridden by `config.templateRegion`. */
|
|
808
|
-
SANDBOX_TEMPLATE_REGION?: string
|
|
809
|
-
/** Signing region, standard AWS variable. Used when `SANDBOX_TEMPLATE_REGION` is unset. */
|
|
810
|
-
AWS_REGION?: string
|
|
811
|
-
/** Access key for the template store. Overridden by `config.templateAccessKeyId`. */
|
|
812
|
-
SANDBOX_TEMPLATE_ACCESS_KEY_ID?: string
|
|
813
|
-
/** Access key, standard AWS variable (also what `fly storage create` sets). */
|
|
814
|
-
AWS_ACCESS_KEY_ID?: string
|
|
815
|
-
/** Secret key for the template store. Overridden by `config.templateSecretAccessKey`. */
|
|
816
|
-
SANDBOX_TEMPLATE_SECRET_ACCESS_KEY?: string
|
|
817
|
-
/** Secret key, standard AWS variable (also what `fly storage create` sets). */
|
|
818
|
-
AWS_SECRET_ACCESS_KEY?: string
|
|
819
|
-
/** Session token for the template store. Overridden by `config.templateSessionToken`. */
|
|
820
|
-
SANDBOX_TEMPLATE_SESSION_TOKEN?: string
|
|
821
|
-
/** Session token, standard AWS variable. */
|
|
822
|
-
AWS_SESSION_TOKEN?: string
|
|
823
|
-
/** Key prefix for template objects (default `molecule-sandbox-templates`). */
|
|
824
|
-
SANDBOX_TEMPLATE_PREFIX?: string
|
|
825
|
-
/** `true` addresses the template bucket path-style rather than virtual-host style. */
|
|
826
|
-
SANDBOX_TEMPLATE_FORCE_PATH_STYLE?: string
|
|
729
|
+
interface FlyOrgMachine extends FlyMachine {
|
|
730
|
+
app_name: string
|
|
827
731
|
}
|
|
828
732
|
```
|
|
829
733
|
|
|
830
|
-
#### `
|
|
734
|
+
#### `FlyMachineConfig`
|
|
831
735
|
|
|
832
|
-
|
|
736
|
+
Machine configuration (`fly.MachineConfig`) — the subset this provider writes.
|
|
833
737
|
|
|
834
738
|
```typescript
|
|
835
|
-
interface
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
739
|
+
interface FlyMachineConfig {
|
|
740
|
+
image: string
|
|
741
|
+
env?: Record<string, string>
|
|
742
|
+
metadata?: Record<string, string>
|
|
743
|
+
guest?: { cpus?: number; cpu_kind?: string; memory_mb?: number }
|
|
744
|
+
mounts?: Array<{ volume?: string; name?: string; path: string }>
|
|
745
|
+
services?: FlyMachineService[]
|
|
746
|
+
restart?: { policy?: 'no' | 'always' | 'on-failure' | 'spot-price'; max_retries?: number }
|
|
747
|
+
auto_destroy?: boolean
|
|
748
|
+
init?: { exec?: string[]; entrypoint?: string[]; cmd?: string[]; tty?: boolean }
|
|
842
749
|
}
|
|
843
750
|
```
|
|
844
751
|
|
|
845
|
-
#### `
|
|
752
|
+
#### `FlyMachineService`
|
|
846
753
|
|
|
847
|
-
|
|
754
|
+
A Fly Proxy service attached to a Machine (`fly.MachineService`).
|
|
848
755
|
|
|
849
756
|
```typescript
|
|
850
|
-
interface
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
/** Splits a composite sandbox id into its app and Machine id. */
|
|
858
|
-
parseSandboxId(id: string): { app: string; machineId: string }
|
|
859
|
-
/** Lifetime of a presigned capture/restore URL, in seconds. */
|
|
860
|
-
presignExpirySeconds: number
|
|
861
|
-
/** Wall-clock budget for a capture or restore transfer, in ms. */
|
|
862
|
-
transferTimeoutMs: number
|
|
863
|
-
/** Largest archive this provider will store, in bytes. */
|
|
864
|
-
maxArchiveBytes: number
|
|
865
|
-
warn?: (message: string, meta?: Record<string, unknown>) => void
|
|
866
|
-
debug?: (message: string, meta?: Record<string, unknown>) => void
|
|
757
|
+
interface FlyMachineService {
|
|
758
|
+
protocol: string
|
|
759
|
+
internal_port: number
|
|
760
|
+
ports: Array<{ port: number; handlers?: string[]; force_https?: boolean }>
|
|
761
|
+
autostart?: boolean
|
|
762
|
+
autostop?: 'off' | 'stop' | 'suspend'
|
|
763
|
+
min_machines_running?: number
|
|
867
764
|
}
|
|
868
765
|
```
|
|
869
766
|
|
|
870
|
-
#### `
|
|
767
|
+
#### `FlyVolume`
|
|
871
768
|
|
|
872
|
-
|
|
769
|
+
A Fly volume (fields this provider reads).
|
|
873
770
|
|
|
874
771
|
```typescript
|
|
875
|
-
interface
|
|
876
|
-
/** Schema version of this record. */
|
|
877
|
-
schema: number
|
|
878
|
-
/** The caller's template id. */
|
|
772
|
+
interface FlyVolume {
|
|
879
773
|
id: string
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
774
|
+
name: string
|
|
775
|
+
state?: string
|
|
776
|
+
size_gb?: number
|
|
777
|
+
region?: string
|
|
778
|
+
attached_machine_id?: string
|
|
779
|
+
}
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
#### `FlyExecResponse`
|
|
783
|
+
|
|
784
|
+
Response body of `POST /v1/apps/{app}/machines/{id}/exec` (`flydv1.ExecResponse`).
|
|
785
|
+
|
|
786
|
+
```typescript
|
|
787
|
+
interface FlyExecResponse {
|
|
788
|
+
stdout?: string
|
|
789
|
+
stderr?: string
|
|
790
|
+
exit_code?: number
|
|
791
|
+
exit_signal?: number
|
|
894
792
|
}
|
|
895
793
|
```
|
|
896
794
|
|
|
897
795
|
### Types
|
|
898
796
|
|
|
797
|
+
#### `RawExec`
|
|
798
|
+
|
|
799
|
+
Runs one exec call against a Machine. Injected so this module stays transport-free.
|
|
800
|
+
|
|
801
|
+
```typescript
|
|
802
|
+
type RawExec = (command: string[], timeoutSeconds: number) => Promise<FlyExecResponse>
|
|
803
|
+
```
|
|
804
|
+
|
|
899
805
|
#### `FlyMachineState`
|
|
900
806
|
|
|
901
807
|
A Fly Machine state, as documented at https://fly.io/docs/machines/machine-states/.
|
|
@@ -924,275 +830,314 @@ type FlyMachineState =
|
|
|
924
830
|
| (string & {})
|
|
925
831
|
```
|
|
926
832
|
|
|
927
|
-
#### `RawExec`
|
|
928
|
-
|
|
929
|
-
Runs one exec call against a Machine. Injected so this module stays transport-free.
|
|
930
|
-
|
|
931
|
-
```typescript
|
|
932
|
-
type RawExec = (command: string[], timeoutSeconds: number) => Promise<FlyExecResponse>
|
|
933
|
-
```
|
|
934
|
-
|
|
935
833
|
### Classes
|
|
936
834
|
|
|
937
|
-
#### `FlyApiClient`
|
|
938
|
-
|
|
939
|
-
Minimal Fly Machines API client: bearer auth, JSON bodies, per-request
|
|
940
|
-
timeout, bounded retries with backoff, and account-wide request pacing via
|
|
941
|
-
{@link paceFlyRequest}.
|
|
942
|
-
|
|
943
835
|
#### `FlyApiError`
|
|
944
836
|
|
|
945
837
|
An error from the Fly Machines API. Carries the HTTP status and the raw
|
|
946
838
|
response body so callers can branch on `404` (absent) or `409` (conflict)
|
|
947
839
|
without string-matching a message.
|
|
948
840
|
|
|
949
|
-
### Functions
|
|
950
|
-
|
|
951
|
-
#### `acquireRestoreLease(ctx, templateId, leaseId)`
|
|
952
|
-
|
|
953
|
-
Records that a restore of this template is in flight.
|
|
954
|
-
|
|
955
|
-
The lease lives in the store rather than in this process, so a second control
|
|
956
|
-
plane enforcing a retention budget can see it. A control plane that dies
|
|
957
|
-
mid-restore leaves the lease behind; it stops counting once it is older than
|
|
958
|
-
the transfer budget, because no live restore can outlast that budget.
|
|
959
|
-
|
|
960
|
-
```typescript
|
|
961
|
-
function acquireRestoreLease(
|
|
962
|
-
ctx: TemplateContext,
|
|
963
|
-
templateId: string,
|
|
964
|
-
leaseId: string,
|
|
965
|
-
): Promise<string>
|
|
966
|
-
```
|
|
967
|
-
|
|
968
|
-
- `ctx` — Template context.
|
|
969
|
-
- `templateId` — The caller's identifier.
|
|
970
|
-
- `leaseId` — Unique id for this restore.
|
|
971
|
-
|
|
972
|
-
**Returns:** The lease's key, for release.
|
|
973
|
-
|
|
974
|
-
#### `appNameForProject(prefix, projectId)`
|
|
975
|
-
|
|
976
|
-
Derives a Fly app name for a project.
|
|
977
|
-
|
|
978
|
-
Fly app names are globally unique DNS labels: lowercase alphanumerics and
|
|
979
|
-
hyphens, no leading/trailing hyphen, at most 63 characters (they become
|
|
980
|
-
`<app>.fly.dev`). Project ids are uuids, so `<prefix>-<uuid>` fits with room
|
|
981
|
-
to spare; longer ids are truncated rather than rejected, and the trailing
|
|
982
|
-
hyphen that truncation can leave is stripped.
|
|
983
|
-
|
|
984
|
-
```typescript
|
|
985
|
-
function appNameForProject(prefix: string, projectId: string): string
|
|
986
|
-
```
|
|
987
|
-
|
|
988
|
-
- `prefix` — Configured app-name prefix.
|
|
989
|
-
- `projectId` — The project id from `SandboxConfig`.
|
|
990
|
-
|
|
991
|
-
**Returns:** A syntactically valid Fly app name.
|
|
992
|
-
|
|
993
|
-
#### `archiveKey(store, templateId)`
|
|
994
|
-
|
|
995
|
-
Key of a template's archive object.
|
|
996
|
-
|
|
997
|
-
```typescript
|
|
998
|
-
function archiveKey(store: ObjectStore, templateId: string): string
|
|
999
|
-
```
|
|
1000
|
-
|
|
1001
|
-
#### `archiveMember(path)`
|
|
1002
|
-
|
|
1003
|
-
Converts an absolute capture path into the relative member name used in the
|
|
1004
|
-
archive, so an archive of `/workspace` holds `workspace/…` and extracts with
|
|
1005
|
-
`tar -C /`.
|
|
1006
|
-
|
|
1007
841
|
```typescript
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
842
|
+
class FlyApiError extends Error {
|
|
843
|
+
/** HTTP status code, or 0 for a transport-level failure (timeout, DNS, reset). */
|
|
844
|
+
readonly status: number
|
|
845
|
+
/** Raw response body text, truncated to a sane length for logging. */
|
|
846
|
+
readonly body: string
|
|
847
|
+
/** HTTP method of the failed request. */
|
|
848
|
+
readonly method: string
|
|
849
|
+
/** API path of the failed request (no base URL). */
|
|
850
|
+
readonly path: string
|
|
1012
851
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
852
|
+
/**
|
|
853
|
+
* Creates a Fly API error.
|
|
854
|
+
* @param method - HTTP method of the failed request.
|
|
855
|
+
* @param path - API path of the failed request.
|
|
856
|
+
* @param status - HTTP status code, or 0 for a transport failure.
|
|
857
|
+
* @param body - Raw response body text.
|
|
858
|
+
* @param options - Standard error options (used to carry a `cause`).
|
|
859
|
+
*/
|
|
860
|
+
constructor(
|
|
861
|
+
method: string,
|
|
862
|
+
path: string,
|
|
863
|
+
status: number,
|
|
864
|
+
body: string,
|
|
865
|
+
options?: { cause?: unknown },
|
|
866
|
+
) {
|
|
867
|
+
super(
|
|
868
|
+
t(
|
|
869
|
+
'codeSandbox.flyio.error.apiError',
|
|
870
|
+
{ method, path, status: String(status), error: body },
|
|
871
|
+
{ defaultValue: `Fly API ${method} ${path}: ${status} ${body}` },
|
|
872
|
+
),
|
|
873
|
+
options,
|
|
874
|
+
)
|
|
875
|
+
this.name = 'FlyApiError'
|
|
876
|
+
this.status = status
|
|
877
|
+
this.body = body
|
|
878
|
+
this.method = method
|
|
879
|
+
this.path = path
|
|
880
|
+
}
|
|
881
|
+
}
|
|
1022
882
|
```
|
|
1023
883
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
#### `assertPrivateRoutesForEnv(env, services)`
|
|
1027
|
-
|
|
1028
|
-
Refuses to boot a sandbox that is told to dial a Flycast host this provider
|
|
1029
|
-
did not allocate a route for.
|
|
1030
|
-
|
|
1031
|
-
This is the check that turns the product-breaking failure into a startup
|
|
1032
|
-
error. The control plane bakes `DATABASE_URL` (and the proxy environment) into
|
|
1033
|
-
every sandbox; on Fly those must name `<app>.flycast`, and that name resolves
|
|
1034
|
-
ONLY because this provider allocated a private address into the project's
|
|
1035
|
-
network for that app. Get the two out of step — a `SANDBOX_DB_HOST` pointing
|
|
1036
|
-
at an app nobody declared, a port that does not match the one the policy
|
|
1037
|
-
allows — and the sandbox boots healthy, the scaffolded app cannot connect, and
|
|
1038
|
-
the only symptom is a connection timeout inside someone else's project.
|
|
884
|
+
#### `FlyApiClient`
|
|
1039
885
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
in the configuration — never something this provider arranges on its own.
|
|
886
|
+
Minimal Fly Machines API client: bearer auth, JSON bodies, per-request
|
|
887
|
+
timeout, bounded retries with backoff, and account-wide request pacing via
|
|
888
|
+
{@link paceFlyRequest}.
|
|
1044
889
|
|
|
1045
890
|
```typescript
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
-
|
|
1054
|
-
|
|
1055
|
-
|
|
891
|
+
class FlyApiClient {
|
|
892
|
+
private readonly token: () => string | undefined
|
|
893
|
+
private readonly baseUrl: string
|
|
894
|
+
private readonly timeoutMs: number
|
|
895
|
+
private readonly fetchImpl: typeof fetch
|
|
896
|
+
/**
|
|
897
|
+
* Injectable delay, shared with the provider: a retry that a CALLER drives
|
|
898
|
+
* (the provider's exec-precondition backoff) waits on the same injected clock
|
|
899
|
+
* the client's own retries use, so tests collapse every wait in one place.
|
|
900
|
+
*/
|
|
901
|
+
readonly sleep: (ms: number) => Promise<void>
|
|
1056
902
|
|
|
1057
|
-
|
|
903
|
+
/**
|
|
904
|
+
* Creates a Fly Machines API client.
|
|
905
|
+
* @param options - Token resolver, base URL, and injectable fetch/sleep.
|
|
906
|
+
*/
|
|
907
|
+
constructor(options: FlyApiClientOptions) {
|
|
908
|
+
this.token = options.token
|
|
909
|
+
this.baseUrl = options.baseUrl
|
|
910
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS
|
|
911
|
+
this.fetchImpl = options.fetchImpl ?? globalThis.fetch
|
|
912
|
+
this.sleep =
|
|
913
|
+
options.sleep ?? ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)))
|
|
914
|
+
}
|
|
1058
915
|
|
|
1059
|
-
|
|
1060
|
-
|
|
916
|
+
/**
|
|
917
|
+
* Issues a request against the Machines API and parses the JSON response.
|
|
918
|
+
*
|
|
919
|
+
* @template T - Expected shape of the parsed response body.
|
|
920
|
+
* @param path - API path relative to the base URL, e.g. `/apps/foo/machines`.
|
|
921
|
+
* @param options - Method, body, timeout, attempts, and statuses to null on.
|
|
922
|
+
* @returns The parsed body, or `null` for an empty body or a status listed in
|
|
923
|
+
* `options.nullOn`.
|
|
924
|
+
* @throws {FlyApiError} On a non-2xx status that is not listed in `nullOn`,
|
|
925
|
+
* or when every retryable attempt is exhausted.
|
|
926
|
+
*/
|
|
927
|
+
async request<T>(path: string, options: FlyRequestOptions = {}): Promise<T | null> {
|
|
928
|
+
const method = options.method ?? 'GET'
|
|
929
|
+
const attempts = options.attempts ?? DEFAULT_ATTEMPTS
|
|
930
|
+
const token = this.token()
|
|
931
|
+
if (!token) {
|
|
932
|
+
throw new Error(
|
|
933
|
+
t('codeSandbox.flyio.error.noToken', undefined, {
|
|
934
|
+
defaultValue:
|
|
935
|
+
'No Fly API token — set FLY_API_TOKEN (or FLY_ACCESS_TOKEN), or pass config.apiToken.',
|
|
936
|
+
}),
|
|
937
|
+
)
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
let lastError: FlyApiError | undefined
|
|
941
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
942
|
+
// Pace EVERY network call (including retries) through the process-wide
|
|
943
|
+
// limiter so a boot's burst of exec calls never trips Fly's account 429.
|
|
944
|
+
await paceFlyRequest(this.sleep)
|
|
945
|
+
const controller = new AbortController()
|
|
946
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? this.timeoutMs)
|
|
947
|
+
// Initialized to 0 ("no answer received"), which is the value that
|
|
948
|
+
// stands when the request throws before a response arrives.
|
|
949
|
+
let status = 0
|
|
950
|
+
let retryAfter: string | null = null
|
|
951
|
+
try {
|
|
952
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
953
|
+
method,
|
|
954
|
+
headers: {
|
|
955
|
+
Authorization: `Bearer ${token}`,
|
|
956
|
+
...(options.body === undefined ? {} : { 'Content-Type': 'application/json' }),
|
|
957
|
+
},
|
|
958
|
+
...(options.body === undefined ? {} : { body: JSON.stringify(options.body) }),
|
|
959
|
+
signal: controller.signal,
|
|
960
|
+
})
|
|
961
|
+
status = response.status
|
|
962
|
+
retryAfter = response.headers?.get?.('retry-after') ?? null
|
|
963
|
+
const text = await response.text()
|
|
964
|
+
|
|
965
|
+
if (status >= 200 && status < 300) {
|
|
966
|
+
if (!text.trim()) return null
|
|
967
|
+
try {
|
|
968
|
+
return JSON.parse(text) as T
|
|
969
|
+
} catch (error) {
|
|
970
|
+
// A 2xx with a non-JSON body is a real protocol surprise (an
|
|
971
|
+
// intercepting proxy, an HTML error page behind a 200). Refusing it
|
|
972
|
+
// loudly beats handing the caller a silently-empty result.
|
|
973
|
+
throw new FlyApiError(method, path, status, text.slice(0, 500), { cause: error })
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
if (options.nullOn?.includes(status)) return null
|
|
977
|
+
lastError = new FlyApiError(method, path, status, text.slice(0, 500))
|
|
978
|
+
} catch (error) {
|
|
979
|
+
if (error instanceof FlyApiError) throw error
|
|
980
|
+
// Transport-level failure (abort/timeout, DNS, connection reset) — no
|
|
981
|
+
// status was ever received, so it is modelled as status 0 and retried.
|
|
982
|
+
lastError = new FlyApiError(
|
|
983
|
+
method,
|
|
984
|
+
path,
|
|
985
|
+
0,
|
|
986
|
+
error instanceof Error ? error.message : String(error),
|
|
987
|
+
{ cause: error },
|
|
988
|
+
)
|
|
989
|
+
} finally {
|
|
990
|
+
clearTimeout(timer)
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
const retryable =
|
|
994
|
+
isRetryableStatus(status) || (options.retryStatuses?.includes(status) ?? false)
|
|
995
|
+
if (!retryable || attempt === attempts) break
|
|
996
|
+
const delay = retryDelayMs(attempt, retryAfter)
|
|
997
|
+
logger.warn(`Fly API transient failure on ${method} ${path} — retrying`, {
|
|
998
|
+
attempt,
|
|
999
|
+
attempts,
|
|
1000
|
+
status,
|
|
1001
|
+
delay,
|
|
1002
|
+
})
|
|
1003
|
+
await this.sleep(delay)
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
throw lastError ?? new FlyApiError(method, path, 0, 'unknown failure')
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1061
1009
|
```
|
|
1062
1010
|
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
#### `buildCaptureCommand(paths, url, maxBytes)`
|
|
1066
|
-
|
|
1067
|
-
Builds the in-sandbox capture command: archive the named paths, refuse an
|
|
1068
|
-
oversized result, and upload it to a presigned URL.
|
|
1011
|
+
### Functions
|
|
1069
1012
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1013
|
+
#### `isRetryableStatus(status)`
|
|
1014
|
+
|
|
1015
|
+
Decides whether a failed attempt is worth repeating.
|
|
1016
|
+
|
|
1017
|
+
`429` (the documented per-action rate limit) and `5xx` are transient — the
|
|
1018
|
+
request was well-formed and the same call can succeed moments later. Every
|
|
1019
|
+
other `4xx` is a real answer (no such app, name taken, bad token) and
|
|
1020
|
+
retrying it only wastes the rate-limit budget that the retry exists to
|
|
1021
|
+
protect. Status `0` means the transport failed before any answer arrived.
|
|
1075
1022
|
|
|
1076
1023
|
```typescript
|
|
1077
|
-
function
|
|
1024
|
+
function isRetryableStatus(status: number): boolean
|
|
1078
1025
|
```
|
|
1079
1026
|
|
|
1080
|
-
- `
|
|
1081
|
-
- `url` — Presigned `PUT` URL for the archive object.
|
|
1082
|
-
- `maxBytes` — Largest archive that may be uploaded.
|
|
1027
|
+
- `status` — HTTP status, or 0 for a transport-level failure.
|
|
1083
1028
|
|
|
1084
|
-
**Returns:**
|
|
1029
|
+
**Returns:** `true` when the request should be retried.
|
|
1085
1030
|
|
|
1086
|
-
#### `
|
|
1031
|
+
#### `retryDelayMs(attempt, retryAfter)`
|
|
1087
1032
|
|
|
1088
|
-
|
|
1033
|
+
Resolves how long to wait before the next attempt.
|
|
1089
1034
|
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
{@link EGRESS_PROBE_EXIT_BLOCKED} if all were refused or timed out.
|
|
1035
|
+
Honors a `Retry-After` header when the server sent one (Fly returns it on
|
|
1036
|
+
429), clamped to {@link MAX_RETRY_DELAY_MS}; otherwise uses exponential
|
|
1037
|
+
backoff from a 500 ms base. Fly's documented budget is one request per second
|
|
1038
|
+
per action, so the first backoff step deliberately exceeds a second.
|
|
1095
1039
|
|
|
1096
1040
|
```typescript
|
|
1097
|
-
function
|
|
1041
|
+
function retryDelayMs(attempt: number, retryAfter?: string | null): number
|
|
1098
1042
|
```
|
|
1099
1043
|
|
|
1100
|
-
- `
|
|
1101
|
-
- `
|
|
1044
|
+
- `attempt` — The 1-based attempt number that just failed.
|
|
1045
|
+
- `retryAfter` — Raw `Retry-After` header value, if present.
|
|
1102
1046
|
|
|
1103
|
-
**Returns:**
|
|
1047
|
+
**Returns:** The delay in milliseconds before the next attempt.
|
|
1104
1048
|
|
|
1105
|
-
#### `
|
|
1049
|
+
#### `normalizeApiUrl(value)`
|
|
1106
1050
|
|
|
1107
|
-
|
|
1108
|
-
manifest's paths, and prove no setuid/setgid file survived.
|
|
1051
|
+
Normalizes a configured API base into a URL prefix ending in `/v1`.
|
|
1109
1052
|
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1053
|
+
`FLY_API_HOSTNAME` is documented as a bare host (`https://api.machines.dev`,
|
|
1054
|
+
or `http://_api.internal:4280` from inside a Fly private network), so a value
|
|
1055
|
+
with no path gets `/v1` appended. A value that already carries a path is used
|
|
1056
|
+
verbatim, which is how an operator points at a proxy or a pinned version.
|
|
1113
1057
|
|
|
1114
1058
|
```typescript
|
|
1115
|
-
function
|
|
1059
|
+
function normalizeApiUrl(value: string | undefined): string
|
|
1116
1060
|
```
|
|
1117
1061
|
|
|
1118
|
-
- `
|
|
1119
|
-
- `url` — Presigned `GET` URL for the archive object.
|
|
1062
|
+
- `value` — Configured base URL, or undefined.
|
|
1120
1063
|
|
|
1121
|
-
**Returns:**
|
|
1064
|
+
**Returns:** The base URL with no trailing slash.
|
|
1122
1065
|
|
|
1123
|
-
#### `
|
|
1066
|
+
#### `resetFlyPacerForTests()`
|
|
1124
1067
|
|
|
1125
|
-
|
|
1126
|
-
requested environment, then run the command.
|
|
1068
|
+
Test-only: reset the shared pacer so timing tests start from a clean slate.
|
|
1127
1069
|
|
|
1128
1070
|
```typescript
|
|
1129
|
-
function
|
|
1071
|
+
function resetFlyPacerForTests(): void
|
|
1130
1072
|
```
|
|
1131
1073
|
|
|
1132
|
-
|
|
1133
|
-
- `opts` — Optional cwd and environment.
|
|
1134
|
-
- `defaultCwd` — Directory used when `opts.cwd` is unset.
|
|
1135
|
-
|
|
1136
|
-
**Returns:** The script source, newline separated.
|
|
1137
|
-
|
|
1138
|
-
#### `commitTemplate(ctx, options)`
|
|
1074
|
+
#### `formatEgressProbeTarget(target)`
|
|
1139
1075
|
|
|
1140
|
-
|
|
1076
|
+
Renders a target the way it is written in configuration, so an operator can
|
|
1077
|
+
paste it straight back.
|
|
1141
1078
|
|
|
1142
1079
|
```typescript
|
|
1143
|
-
function
|
|
1144
|
-
ctx: TemplateContext,
|
|
1145
|
-
options: CommitTemplateOptions,
|
|
1146
|
-
): Promise<SandboxTemplate>
|
|
1080
|
+
function formatEgressProbeTarget(target: EgressProbeTarget): string
|
|
1147
1081
|
```
|
|
1148
1082
|
|
|
1149
|
-
- `
|
|
1150
|
-
- `options` — What to capture and what to call it.
|
|
1083
|
+
- `target` — The parsed target.
|
|
1151
1084
|
|
|
1152
|
-
**Returns:**
|
|
1085
|
+
**Returns:** `host:port`, with IPv6 hosts bracketed.
|
|
1153
1086
|
|
|
1154
|
-
#### `
|
|
1087
|
+
#### `parseEgressProbeTargets(raw)`
|
|
1155
1088
|
|
|
1156
|
-
|
|
1089
|
+
Parses probe targets from configuration.
|
|
1090
|
+
|
|
1091
|
+
Only LITERAL addresses are accepted, never hostnames — Fly's own
|
|
1092
|
+
troubleshooting guidance for testing a policy is to "use direct IP addresses
|
|
1093
|
+
(not hostnames) to test blocked traffic to avoid DNS masking", and a probe
|
|
1094
|
+
that failed at name resolution would report a blocked connection it never
|
|
1095
|
+
actually attempted.
|
|
1157
1096
|
|
|
1158
1097
|
```typescript
|
|
1159
|
-
function
|
|
1160
|
-
config?: FlyioConfig,
|
|
1161
|
-
client?: FlyApiClient,
|
|
1162
|
-
store?: ObjectStore | null,
|
|
1163
|
-
): SandboxProvider
|
|
1098
|
+
function parseEgressProbeTargets(raw: string | string[] | undefined): EgressProbeTarget[]
|
|
1164
1099
|
```
|
|
1165
1100
|
|
|
1166
|
-
- `
|
|
1167
|
-
- `client` — Injectable Machines API client, for tests.
|
|
1168
|
-
- `store` — Injectable template object store, for tests. Resolved from the template settings (or their env fallbacks) on first use when omitted.
|
|
1101
|
+
- `raw` — Comma-separated `ip:port` string, or an array of them. IPv6 literals must be bracketed (`[2606:4700:4700::1111]:443`).
|
|
1169
1102
|
|
|
1170
|
-
**Returns:**
|
|
1103
|
+
**Returns:** The valid targets, in order. Invalid entries are dropped rather than throwing: the caller turns an empty result into an `inconclusive` verdict, which is the honest outcome for "nothing to probe".
|
|
1171
1104
|
|
|
1172
|
-
#### `
|
|
1105
|
+
#### `parseEgressAllowedPorts(raw)`
|
|
1173
1106
|
|
|
1174
|
-
|
|
1175
|
-
|
|
1107
|
+
Parses an allowed-port list for the egress network policy.
|
|
1108
|
+
|
|
1109
|
+
An EMPTY list is deliberately not expressible. Fly documents the deny default
|
|
1110
|
+
as a consequence of at least one `allow` rule existing for a direction; what
|
|
1111
|
+
an allow rule with zero ports does is not documented anywhere, so a provider
|
|
1112
|
+
that sent one would be guessing at a security control. An operator who wants
|
|
1113
|
+
near-total denial allows a single port nothing in the sandbox uses.
|
|
1176
1114
|
|
|
1177
1115
|
```typescript
|
|
1178
|
-
function
|
|
1116
|
+
function parseEgressAllowedPorts(raw: string | undefined): FlyNetworkPolicyPort[] | undefined
|
|
1179
1117
|
```
|
|
1180
1118
|
|
|
1181
|
-
- `
|
|
1119
|
+
- `raw` — Comma-separated `protocol:port` pairs, e.g. `tcp:3128,udp:53`. A bare port is treated as TCP.
|
|
1182
1120
|
|
|
1183
|
-
**Returns:**
|
|
1121
|
+
**Returns:** The parsed ports, or `undefined` when the input is absent or empty (meaning "apply no policy"), which is NOT the same as "deny everything".
|
|
1184
1122
|
|
|
1185
|
-
#### `
|
|
1123
|
+
#### `buildEgressProbeCommand(targets, timeoutMs)`
|
|
1186
1124
|
|
|
1187
|
-
|
|
1125
|
+
Builds the shell command that observes egress from inside a Machine.
|
|
1126
|
+
|
|
1127
|
+
Raw `net.connect` on purpose: an HTTP client would honour proxy environment
|
|
1128
|
+
variables and report the PROXY's policy instead of the network's. Every target
|
|
1129
|
+
is attempted in parallel and the process exits
|
|
1130
|
+
{@link EGRESS_PROBE_EXIT_REACHED} if any connected,
|
|
1131
|
+
{@link EGRESS_PROBE_EXIT_BLOCKED} if all were refused or timed out.
|
|
1188
1132
|
|
|
1189
1133
|
```typescript
|
|
1190
|
-
function
|
|
1134
|
+
function buildEgressProbeCommand(targets: EgressProbeTarget[], timeoutMs: number): string
|
|
1191
1135
|
```
|
|
1192
1136
|
|
|
1193
|
-
- `
|
|
1137
|
+
- `targets` — Literal targets to attempt.
|
|
1138
|
+
- `timeoutMs` — Per-connection timeout.
|
|
1194
1139
|
|
|
1195
|
-
**Returns:**
|
|
1140
|
+
**Returns:** A `sh`-safe command string.
|
|
1196
1141
|
|
|
1197
1142
|
#### `describePublicReach(policyPorts)`
|
|
1198
1143
|
|
|
@@ -1216,42 +1161,58 @@ function describePublicReach(policyPorts: FlyNetworkPolicyPort[] | undefined): s
|
|
|
1216
1161
|
|
|
1217
1162
|
**Returns:** A sentence naming the public reach, or `''` when there is nothing to say (no policy, or nothing but UDP/53 — DNS is a documented residual of its own and not a TCP channel).
|
|
1218
1163
|
|
|
1219
|
-
#### `
|
|
1164
|
+
#### `unexpectedPolicyPorts(applied, intended)`
|
|
1220
1165
|
|
|
1221
|
-
|
|
1166
|
+
Finds ports the policy Fly holds allows that this provider never configured.
|
|
1167
|
+
|
|
1168
|
+
The raw-connect probe can only speak for the targets it was handed, so a
|
|
1169
|
+
policy widened outside this provider — edited by hand to unblock something, or
|
|
1170
|
+
left behind by an older build — is invisible to it: the probe's own port stays
|
|
1171
|
+
denied and the verdict stays `filtered` while a port nothing here asked for is
|
|
1172
|
+
open to the whole internet. Comparing what Fly holds against what this
|
|
1173
|
+
provider intended is the one check that can catch that, and it is why the
|
|
1174
|
+
verdict reads the policy back rather than trusting the write.
|
|
1222
1175
|
|
|
1223
1176
|
```typescript
|
|
1224
|
-
function
|
|
1177
|
+
function unexpectedPolicyPorts(
|
|
1178
|
+
applied: FlyNetworkPolicyPort[] | undefined,
|
|
1179
|
+
intended: FlyNetworkPolicyPort[] | undefined,
|
|
1180
|
+
): FlyNetworkPolicyPort[]
|
|
1225
1181
|
```
|
|
1226
1182
|
|
|
1227
|
-
- `
|
|
1183
|
+
- `applied` — Ports in the policy Fly actually holds, or `undefined` when the readback found nothing (in which case there is nothing to compare).
|
|
1184
|
+
- `intended` — Ports this provider configured, or `undefined` when it applies no policy.
|
|
1228
1185
|
|
|
1229
|
-
**Returns:**
|
|
1186
|
+
**Returns:** The applied ports with no counterpart in `intended`, in order.
|
|
1230
1187
|
|
|
1231
|
-
#### `
|
|
1188
|
+
#### `verdictForProbeExit(exitCode, context)`
|
|
1232
1189
|
|
|
1233
|
-
|
|
1234
|
-
based on the caller's time budget.
|
|
1190
|
+
Maps a probe's exit status onto an {@link EgressVerdict}.
|
|
1235
1191
|
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1192
|
+
The mapping is the whole security contract of this file, so it is total and
|
|
1193
|
+
has exactly one path to each state:
|
|
1194
|
+
|
|
1195
|
+
| Exit | Verdict | Meaning |
|
|
1196
|
+
| --------------------------------- | -------------- | ---------------------------------------- |
|
|
1197
|
+
| {@link EGRESS_PROBE_EXIT_REACHED} | `open` | A raw socket reached a public IP. |
|
|
1198
|
+
| {@link EGRESS_PROBE_EXIT_BLOCKED} | `filtered` | Every attempt was refused or timed out. |
|
|
1199
|
+
| anything else | `inconclusive` | The probe did not complete its attempts. |
|
|
1200
|
+
|
|
1201
|
+
Every other outcome — a missing interpreter (127), a killed process, Fly
|
|
1202
|
+
returning no status at all (-1), an API failure before the probe ever ran — is
|
|
1203
|
+
`inconclusive`. Collapsing any of those into `filtered` is precisely the "I
|
|
1204
|
+
could not look" ⇒ "I looked and it is safe" conflation the capability exists
|
|
1205
|
+
to prevent, and the caller refuses to boot production on anything but
|
|
1206
|
+
`filtered`.
|
|
1239
1207
|
|
|
1240
1208
|
```typescript
|
|
1241
|
-
function
|
|
1242
|
-
rawExec: RawExec,
|
|
1243
|
-
command: string,
|
|
1244
|
-
opts: ExecOptions | undefined,
|
|
1245
|
-
defaultCwd: string,
|
|
1246
|
-
): Promise<ExecResult>
|
|
1209
|
+
function verdictForProbeExit(exitCode: number, context: EgressProbeContext): EgressVerdict
|
|
1247
1210
|
```
|
|
1248
1211
|
|
|
1249
|
-
- `
|
|
1250
|
-
- `
|
|
1251
|
-
- `opts` — Core exec options (cwd, env, timeout in ms).
|
|
1252
|
-
- `defaultCwd` — Working directory used when `opts.cwd` is unset.
|
|
1212
|
+
- `exitCode` — The probe process's exit status.
|
|
1213
|
+
- `context` — App, targets and the policy this provider applied.
|
|
1253
1214
|
|
|
1254
|
-
**Returns:** The
|
|
1215
|
+
**Returns:** The verdict.
|
|
1255
1216
|
|
|
1256
1217
|
#### `extractPolicyId(response, name)`
|
|
1257
1218
|
|
|
@@ -1298,7 +1259,63 @@ function extractPolicyPorts(response: unknown, name: string): FlyNetworkPolicyPo
|
|
|
1298
1259
|
- `response` — The raw parsed LIST response.
|
|
1299
1260
|
- `name` — The policy name to match.
|
|
1300
1261
|
|
|
1301
|
-
**Returns:** The allowed egress ports, or `undefined` when no policy of that name is present or the shape is unrecognized. `undefined` means "nothing to compare", never "nothing is allowed".
|
|
1262
|
+
**Returns:** The allowed egress ports, or `undefined` when no policy of that name is present or the shape is unrecognized. `undefined` means "nothing to compare", never "nothing is allowed".
|
|
1263
|
+
|
|
1264
|
+
#### `toExecResult(response)`
|
|
1265
|
+
|
|
1266
|
+
Normalizes a Fly exec response into the core `ExecResult`.
|
|
1267
|
+
|
|
1268
|
+
Fly reports `exit_signal` separately from `exit_code`; a signalled process has
|
|
1269
|
+
no meaningful exit code, so it is rendered the way a POSIX shell does, as
|
|
1270
|
+
`128 + signal`.
|
|
1271
|
+
|
|
1272
|
+
```typescript
|
|
1273
|
+
function toExecResult(response: FlyExecResponse): ExecResult
|
|
1274
|
+
```
|
|
1275
|
+
|
|
1276
|
+
- `response` — The raw Fly exec response.
|
|
1277
|
+
|
|
1278
|
+
**Returns:** The normalized result.
|
|
1279
|
+
|
|
1280
|
+
#### `buildScript(command, opts, defaultCwd)`
|
|
1281
|
+
|
|
1282
|
+
Builds the shell script body for a command: change directory, export the
|
|
1283
|
+
requested environment, then run the command.
|
|
1284
|
+
|
|
1285
|
+
```typescript
|
|
1286
|
+
function buildScript(command: string, opts: ExecOptions | undefined, defaultCwd: string): string
|
|
1287
|
+
```
|
|
1288
|
+
|
|
1289
|
+
- `command` — The shell command to run.
|
|
1290
|
+
- `opts` — Optional cwd and environment.
|
|
1291
|
+
- `defaultCwd` — Directory used when `opts.cwd` is unset.
|
|
1292
|
+
|
|
1293
|
+
**Returns:** The script source, newline separated.
|
|
1294
|
+
|
|
1295
|
+
#### `execCommand(rawExec, command, opts, defaultCwd)`
|
|
1296
|
+
|
|
1297
|
+
Executes a command on a Machine, choosing the direct or detached strategy
|
|
1298
|
+
based on the caller's time budget.
|
|
1299
|
+
|
|
1300
|
+
A command too large to pass inline (over {@link FLY_EXEC_MAX_ARG_BYTES}) is
|
|
1301
|
+
spilled to a file via chunked writes and run from there, so there is no size
|
|
1302
|
+
ceiling the caller must respect.
|
|
1303
|
+
|
|
1304
|
+
```typescript
|
|
1305
|
+
function execCommand(
|
|
1306
|
+
rawExec: RawExec,
|
|
1307
|
+
command: string,
|
|
1308
|
+
opts: ExecOptions | undefined,
|
|
1309
|
+
defaultCwd: string,
|
|
1310
|
+
): Promise<ExecResult>
|
|
1311
|
+
```
|
|
1312
|
+
|
|
1313
|
+
- `rawExec` — Transport callback issuing one Fly exec call.
|
|
1314
|
+
- `command` — The shell command to run.
|
|
1315
|
+
- `opts` — Core exec options (cwd, env, timeout in ms).
|
|
1316
|
+
- `defaultCwd` — Working directory used when `opts.cwd` is unset.
|
|
1317
|
+
|
|
1318
|
+
**Returns:** The command's stdout, stderr and exit code.
|
|
1302
1319
|
|
|
1303
1320
|
#### `flycastHost(app)`
|
|
1304
1321
|
|
|
@@ -1312,115 +1329,161 @@ function flycastHost(app: string): string
|
|
|
1312
1329
|
|
|
1313
1330
|
**Returns:** `<app>.flycast`.
|
|
1314
1331
|
|
|
1315
|
-
#### `
|
|
1332
|
+
#### `parsePrivateServices(raw)`
|
|
1316
1333
|
|
|
1317
|
-
|
|
1318
|
-
|
|
1334
|
+
Parses the declared cross-network services.
|
|
1335
|
+
|
|
1336
|
+
Each entry is `<app>:<port>` — the Fly app to allocate a Flycast address on,
|
|
1337
|
+
and the TCP port a sandbox dials it at. The port is not cosmetic: it is what
|
|
1338
|
+
{@link mergeEgressPorts} adds to the Fly network policy, and what
|
|
1339
|
+
{@link assertPrivateRoutesForEnv} checks the injected connection URLs against.
|
|
1319
1340
|
|
|
1320
1341
|
```typescript
|
|
1321
|
-
function
|
|
1342
|
+
function parsePrivateServices(raw: string | undefined): FlyPrivateService[] | undefined
|
|
1322
1343
|
```
|
|
1323
1344
|
|
|
1324
|
-
- `
|
|
1345
|
+
- `raw` — Comma-separated `app:port` pairs, e.g. `molecule-pg-tenant:5432,molecule-api:3129`.
|
|
1325
1346
|
|
|
1326
|
-
**Returns:** `
|
|
1347
|
+
**Returns:** The declared services, deduplicated, in order — or `undefined` when the input is absent or empty, meaning "allocate nothing", which is correct for a control plane that shares one 6PN with its sandboxes.
|
|
1327
1348
|
|
|
1328
|
-
#### `
|
|
1349
|
+
#### `mergeEgressPorts(ports, services)`
|
|
1329
1350
|
|
|
1330
|
-
|
|
1351
|
+
Adds every declared private-service port to the egress network policy.
|
|
1352
|
+
|
|
1353
|
+
A Fly network policy is deny-by-default once any rule exists for a direction,
|
|
1354
|
+
so a sandbox told to dial `molecule-pg-tenant.flycast:5432` under a policy
|
|
1355
|
+
allowing only `tcp:3128` would fail to connect with no diagnostic beyond a
|
|
1356
|
+
timeout. Deriving the port from the declaration — rather than asking the
|
|
1357
|
+
operator to keep two lists in step — is what makes that class of outage
|
|
1358
|
+
impossible: FLY-OPERATOR-SETUP.md § 9 can keep saying "never widen this list",
|
|
1359
|
+
because the operator never has to.
|
|
1360
|
+
|
|
1361
|
+
It is deliberately a UNION and never a replacement, so an operator's list is
|
|
1362
|
+
still exactly what they wrote plus the ports their own declarations require,
|
|
1363
|
+
and the applied policy is logged.
|
|
1364
|
+
|
|
1365
|
+
The addition is load-bearing — VERIFIED 2026-08-16, not inferred. Fly states
|
|
1366
|
+
"Network policies only apply to traffic directly to and from Machines. They do
|
|
1367
|
+
not affect traffic routed through the Fly Proxy"
|
|
1368
|
+
(https://fly.io/docs/machines/guides-examples/network-policies/), and Flycast
|
|
1369
|
+
traffic IS routed through Fly Proxy, so it was an open question whether these
|
|
1370
|
+
ports did anything. Measured on a throwaway app with a Flycast address into
|
|
1371
|
+
its own 6PN: under a policy allowing only `udp:53`, `molecule-pg-tenant.flycast`
|
|
1372
|
+
and `molecule-api.flycast` both RESOLVED and every TCP connect to them was
|
|
1373
|
+
dropped; re-applying the policy with `tcp:443` opened 443 and nothing else.
|
|
1374
|
+
Fly's sentence is about INGRESS — a Machine's egress TOWARD a Flycast address
|
|
1375
|
+
|
|
1376
|
+
_(description truncated — read the full text in `node_modules/@molecule/api-code-sandbox-flyio/dist/flycast.d.ts`.)_
|
|
1331
1377
|
|
|
1332
1378
|
```typescript
|
|
1333
|
-
function
|
|
1379
|
+
function mergeEgressPorts(
|
|
1380
|
+
ports: FlyNetworkPolicyPort[] | undefined,
|
|
1381
|
+
services: FlyPrivateService[] | undefined,
|
|
1382
|
+
): FlyNetworkPolicyPort[] | undefined
|
|
1334
1383
|
```
|
|
1335
1384
|
|
|
1336
|
-
- `
|
|
1337
|
-
- `
|
|
1338
|
-
|
|
1339
|
-
**Returns:** The template, or `null` when no template has that id.
|
|
1385
|
+
- `ports` — The operator's configured ports, or `undefined` when no policy is being applied at all.
|
|
1386
|
+
- `services` — The declared private services, or `undefined`.
|
|
1340
1387
|
|
|
1341
|
-
|
|
1388
|
+
**Returns:** The union, deduplicated — or `undefined` when `ports` is `undefined`. "No policy" is never turned INTO a policy here: applying one that Fly then uses to drop everything else is not something to infer from an unrelated setting.
|
|
1342
1389
|
|
|
1343
|
-
|
|
1390
|
+
#### `encodePrivateRoutes(routes)`
|
|
1344
1391
|
|
|
1345
|
-
|
|
1346
|
-
core states for this field: an unreadable answer resolves to in-use, never to
|
|
1347
|
-
free.
|
|
1392
|
+
Encodes allocated addresses for storage in Machine metadata.
|
|
1348
1393
|
|
|
1349
1394
|
```typescript
|
|
1350
|
-
function
|
|
1395
|
+
function encodePrivateRoutes(routes: Record<string, string>): string
|
|
1351
1396
|
```
|
|
1352
1397
|
|
|
1353
|
-
- `
|
|
1354
|
-
- `ttlMs` — How long a lease can possibly correspond to a live restore.
|
|
1355
|
-
- `now` — Current time in ms since the epoch.
|
|
1398
|
+
- `routes` — Target app name → allocated private address.
|
|
1356
1399
|
|
|
1357
|
-
**Returns:** `
|
|
1400
|
+
**Returns:** `app=address` pairs joined by commas. An IPv6 literal contains neither `,` nor `=`, so the encoding is unambiguous.
|
|
1358
1401
|
|
|
1359
|
-
#### `
|
|
1402
|
+
#### `decodePrivateRoutes(raw)`
|
|
1360
1403
|
|
|
1361
|
-
|
|
1362
|
-
having been stopped on purpose. {@link mapMachineState} flattens both to
|
|
1363
|
-
`stopped` because the core union has no failure status, so this is the only
|
|
1364
|
-
way to tell them apart.
|
|
1404
|
+
Decodes the addresses recorded by {@link encodePrivateRoutes}.
|
|
1365
1405
|
|
|
1366
1406
|
```typescript
|
|
1367
|
-
function
|
|
1407
|
+
function decodePrivateRoutes(raw: string | undefined): Record<string, string>
|
|
1368
1408
|
```
|
|
1369
1409
|
|
|
1370
|
-
- `
|
|
1410
|
+
- `raw` — The metadata value, or `undefined` when the Machine carries none.
|
|
1371
1411
|
|
|
1372
|
-
**Returns:**
|
|
1412
|
+
**Returns:** Target app name → allocated private address. Unparseable entries are skipped: this value only drives cleanup and self-healing, and a malformed entry must not stop either.
|
|
1373
1413
|
|
|
1374
|
-
#### `
|
|
1414
|
+
#### `assertPrivateRoutesForEnv(env, services)`
|
|
1375
1415
|
|
|
1376
|
-
|
|
1416
|
+
Refuses to boot a sandbox that is told to dial a Flycast host this provider
|
|
1417
|
+
did not allocate a route for.
|
|
1377
1418
|
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1419
|
+
This is the check that turns the product-breaking failure into a startup
|
|
1420
|
+
error. The control plane bakes `DATABASE_URL` (and the proxy environment) into
|
|
1421
|
+
every sandbox; on Fly those must name `<app>.flycast`, and that name resolves
|
|
1422
|
+
ONLY because this provider allocated a private address into the project's
|
|
1423
|
+
network for that app. Get the two out of step — a `SANDBOX_DB_HOST` pointing
|
|
1424
|
+
at an app nobody declared, a port that does not match the one the policy
|
|
1425
|
+
allows — and the sandbox boots healthy, the scaffolded app cannot connect, and
|
|
1426
|
+
the only symptom is a connection timeout inside someone else's project.
|
|
1427
|
+
|
|
1428
|
+
It is also the guard on the control-plane cluster: reaching
|
|
1429
|
+
`molecule-pg-control.flycast` from a sandbox would require an operator to have
|
|
1430
|
+
declared it as a private service, which is an explicit act with its own name
|
|
1431
|
+
in the configuration — never something this provider arranges on its own.
|
|
1383
1432
|
|
|
1384
1433
|
```typescript
|
|
1385
|
-
function
|
|
1434
|
+
function assertPrivateRoutesForEnv(
|
|
1435
|
+
env: Record<string, string> | undefined,
|
|
1436
|
+
services: FlyPrivateService[] | undefined,
|
|
1437
|
+
): void
|
|
1386
1438
|
```
|
|
1387
1439
|
|
|
1388
|
-
- `
|
|
1389
|
-
|
|
1390
|
-
**Returns:** `true` when the request should be retried.
|
|
1440
|
+
- `env` — The environment the caller is baking into the sandbox.
|
|
1441
|
+
- `services` — The declared private services, or `undefined`.
|
|
1391
1442
|
|
|
1392
|
-
#### `
|
|
1443
|
+
#### `toSandboxId(app, machineId)`
|
|
1393
1444
|
|
|
1394
|
-
|
|
1445
|
+
Builds the opaque sandbox id addressing one Machine inside one app.
|
|
1395
1446
|
|
|
1396
1447
|
```typescript
|
|
1397
|
-
function
|
|
1448
|
+
function toSandboxId(app: string, machineId: string): string
|
|
1398
1449
|
```
|
|
1399
1450
|
|
|
1400
|
-
|
|
1451
|
+
- `app` — Fly app name.
|
|
1452
|
+
- `machineId` — Fly Machine id.
|
|
1401
1453
|
|
|
1402
|
-
|
|
1454
|
+
**Returns:** The composite sandbox id.
|
|
1455
|
+
|
|
1456
|
+
#### `parseSandboxId(id)`
|
|
1457
|
+
|
|
1458
|
+
Splits a composite sandbox id back into its app and Machine id.
|
|
1403
1459
|
|
|
1404
1460
|
```typescript
|
|
1405
|
-
function
|
|
1406
|
-
ctx: TemplateContext,
|
|
1407
|
-
options?: ListTemplatesOptions,
|
|
1408
|
-
): Promise<SandboxTemplate[]>
|
|
1461
|
+
function parseSandboxId(id: string): { app: string; machineId: string }
|
|
1409
1462
|
```
|
|
1410
1463
|
|
|
1411
|
-
- `
|
|
1412
|
-
- `options` — Narrowing by id prefix.
|
|
1464
|
+
- `id` — A sandbox id previously produced by {@link toSandboxId}.
|
|
1413
1465
|
|
|
1414
|
-
**Returns:**
|
|
1466
|
+
**Returns:** The app name and Machine id.
|
|
1415
1467
|
|
|
1416
|
-
#### `
|
|
1468
|
+
#### `appNameForProject(prefix, projectId)`
|
|
1417
1469
|
|
|
1418
|
-
|
|
1470
|
+
Derives a Fly app name for a project.
|
|
1471
|
+
|
|
1472
|
+
Fly app names are globally unique DNS labels: lowercase alphanumerics and
|
|
1473
|
+
hyphens, no leading/trailing hyphen, at most 63 characters (they become
|
|
1474
|
+
`<app>.fly.dev`). Project ids are uuids, so `<prefix>-<uuid>` fits with room
|
|
1475
|
+
to spare; longer ids are truncated rather than rejected, and the trailing
|
|
1476
|
+
hyphen that truncation can leave is stripped.
|
|
1419
1477
|
|
|
1420
1478
|
```typescript
|
|
1421
|
-
function
|
|
1479
|
+
function appNameForProject(prefix: string, projectId: string): string
|
|
1422
1480
|
```
|
|
1423
1481
|
|
|
1482
|
+
- `prefix` — Configured app-name prefix.
|
|
1483
|
+
- `projectId` — The project id from `SandboxConfig`.
|
|
1484
|
+
|
|
1485
|
+
**Returns:** A syntactically valid Fly app name.
|
|
1486
|
+
|
|
1424
1487
|
#### `mapMachineState(state)`
|
|
1425
1488
|
|
|
1426
1489
|
Maps a Fly Machine state onto the core `Sandbox['status']` union.
|
|
@@ -1449,197 +1512,163 @@ function mapMachineState(state: FlyMachineState): 'creating' | 'stopped' | 'runn
|
|
|
1449
1512
|
|
|
1450
1513
|
**Returns:** The core sandbox status.
|
|
1451
1514
|
|
|
1452
|
-
#### `
|
|
1453
|
-
|
|
1454
|
-
Adds every declared private-service port to the egress network policy.
|
|
1455
|
-
|
|
1456
|
-
A Fly network policy is deny-by-default once any rule exists for a direction,
|
|
1457
|
-
so a sandbox told to dial `molecule-pg-tenant.flycast:5432` under a policy
|
|
1458
|
-
allowing only `tcp:3128` would fail to connect with no diagnostic beyond a
|
|
1459
|
-
timeout. Deriving the port from the declaration — rather than asking the
|
|
1460
|
-
operator to keep two lists in step — is what makes that class of outage
|
|
1461
|
-
impossible: FLY-OPERATOR-SETUP.md § 9 can keep saying "never widen this list",
|
|
1462
|
-
because the operator never has to.
|
|
1463
|
-
|
|
1464
|
-
It is deliberately a UNION and never a replacement, so an operator's list is
|
|
1465
|
-
still exactly what they wrote plus the ports their own declarations require,
|
|
1466
|
-
and the applied policy is logged.
|
|
1467
|
-
|
|
1468
|
-
The addition is load-bearing — VERIFIED 2026-08-16, not inferred. Fly states
|
|
1469
|
-
"Network policies only apply to traffic directly to and from Machines. They do
|
|
1470
|
-
not affect traffic routed through the Fly Proxy"
|
|
1471
|
-
(https://fly.io/docs/machines/guides-examples/network-policies/), and Flycast
|
|
1472
|
-
traffic IS routed through Fly Proxy, so it was an open question whether these
|
|
1473
|
-
ports did anything. Measured on a throwaway app with a Flycast address into
|
|
1474
|
-
its own 6PN: under a policy allowing only `udp:53`, `molecule-pg-tenant.flycast`
|
|
1475
|
-
and `molecule-api.flycast` both RESOLVED and every TCP connect to them was
|
|
1476
|
-
dropped; re-applying the policy with `tcp:443` opened 443 and nothing else.
|
|
1477
|
-
Fly's sentence is about INGRESS — a Machine's egress TOWARD a Flycast address
|
|
1478
|
-
is filtered like any other. Drop a declared service's port and every database
|
|
1479
|
-
connection in the fleet goes with it.
|
|
1515
|
+
#### `isFailedState(state)`
|
|
1480
1516
|
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
layer; it is named in `verifyEgress()`'s verdict and in
|
|
1486
|
-
`docs/sandbox-egress-enforcement.md`.
|
|
1517
|
+
Reports whether a Fly Machine state means the Machine errored, rather than
|
|
1518
|
+
having been stopped on purpose. {@link mapMachineState} flattens both to
|
|
1519
|
+
`stopped` because the core union has no failure status, so this is the only
|
|
1520
|
+
way to tell them apart.
|
|
1487
1521
|
|
|
1488
1522
|
```typescript
|
|
1489
|
-
function
|
|
1490
|
-
ports: FlyNetworkPolicyPort[] | undefined,
|
|
1491
|
-
services: FlyPrivateService[] | undefined,
|
|
1492
|
-
): FlyNetworkPolicyPort[] | undefined
|
|
1523
|
+
function isFailedState(state: FlyMachineState): boolean
|
|
1493
1524
|
```
|
|
1494
1525
|
|
|
1495
|
-
- `
|
|
1496
|
-
- `services` — The declared private services, or `undefined`.
|
|
1526
|
+
- `state` — Raw Fly Machine state string.
|
|
1497
1527
|
|
|
1498
|
-
**Returns:**
|
|
1528
|
+
**Returns:** `true` for `failed` and `launch_failed`.
|
|
1499
1529
|
|
|
1500
|
-
#### `
|
|
1530
|
+
#### `isTransientExecPrecondition(error)`
|
|
1501
1531
|
|
|
1502
|
-
|
|
1532
|
+
Whether a failed exec is Fly's TRANSIENT precondition — the request reached
|
|
1533
|
+
the Machine's exec agent and the connection was dropped before the command
|
|
1534
|
+
ran (`412 failed_precondition: exec request failed: EOF`) — rather than the
|
|
1535
|
+
Machine being in a state an exec cannot run in.
|
|
1503
1536
|
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1537
|
+
The distinction decides what retrying can achieve. A dropped connection is
|
|
1538
|
+
worth repeating: the Machine is `started` and its agent recovers in seconds.
|
|
1539
|
+
`failed_precondition: machine not running` is NOT — the Machine really is not
|
|
1540
|
+
running, and that is what {@link FlyioSandboxProvider.startMachine} waits out
|
|
1541
|
+
before the first exec. Repeating the exec there would only burn the caller's
|
|
1542
|
+
budget against a condition the wait endpoint already answers.
|
|
1508
1543
|
|
|
1509
1544
|
```typescript
|
|
1510
|
-
function
|
|
1545
|
+
function isTransientExecPrecondition(error: unknown): boolean
|
|
1511
1546
|
```
|
|
1512
1547
|
|
|
1513
|
-
- `
|
|
1514
|
-
|
|
1515
|
-
**Returns:** The base URL with no trailing slash.
|
|
1548
|
+
- `error` — The failure an exec attempt raised.
|
|
1516
1549
|
|
|
1517
|
-
|
|
1550
|
+
**Returns:** `true` when repeating the exec is the right recovery.
|
|
1518
1551
|
|
|
1519
|
-
|
|
1552
|
+
#### `createProvider(config, client, store)`
|
|
1520
1553
|
|
|
1521
|
-
|
|
1522
|
-
as a consequence of at least one `allow` rule existing for a direction; what
|
|
1523
|
-
an allow rule with zero ports does is not documented anywhere, so a provider
|
|
1524
|
-
that sent one would be guessing at a security control. An operator who wants
|
|
1525
|
-
near-total denial allows a single port nothing in the sandbox uses.
|
|
1554
|
+
Creates a Fly.io Machines sandbox provider.
|
|
1526
1555
|
|
|
1527
1556
|
```typescript
|
|
1528
|
-
function
|
|
1557
|
+
function createProvider(
|
|
1558
|
+
config?: FlyioConfig,
|
|
1559
|
+
client?: FlyApiClient,
|
|
1560
|
+
store?: ObjectStore | null,
|
|
1561
|
+
): SandboxProvider
|
|
1529
1562
|
```
|
|
1530
1563
|
|
|
1531
|
-
- `
|
|
1564
|
+
- `config` — Optional Fly configuration: API token/URL, org slug, app naming and isolation mode, region, base image, guest sizing, preview service, and the preview URL template. Every field has an env fallback.
|
|
1565
|
+
- `client` — Injectable Machines API client, for tests.
|
|
1566
|
+
- `store` — Injectable template object store, for tests. Resolved from the template settings (or their env fallbacks) on first use when omitted.
|
|
1532
1567
|
|
|
1533
|
-
**Returns:**
|
|
1568
|
+
**Returns:** A `SandboxProvider` backed by Fly Machines.
|
|
1534
1569
|
|
|
1535
|
-
#### `
|
|
1570
|
+
#### `resolveTemplateStorage(config)`
|
|
1536
1571
|
|
|
1537
|
-
|
|
1572
|
+
Resolves template-storage settings from config and the environment.
|
|
1538
1573
|
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1574
|
+
Returns `null` when the store is not configured at all, which the template
|
|
1575
|
+
methods turn into an actionable error naming the settings. Partial
|
|
1576
|
+
configuration is treated as unconfigured for the same reason: half a
|
|
1577
|
+
connection cannot be used, and reporting it as a connection failure would
|
|
1578
|
+
point an operator at the network instead of at their settings.
|
|
1544
1579
|
|
|
1545
1580
|
```typescript
|
|
1546
|
-
function
|
|
1581
|
+
function resolveTemplateStorage(config: FlyioConfig): ResolvedStorage | null
|
|
1547
1582
|
```
|
|
1548
1583
|
|
|
1549
|
-
- `
|
|
1550
|
-
|
|
1551
|
-
**Returns:** The valid targets, in order. Invalid entries are dropped rather than throwing: the caller turns an empty result into an `inconclusive` verdict, which is the honest outcome for "nothing to probe".
|
|
1584
|
+
- `config` — Fly provider configuration.
|
|
1552
1585
|
|
|
1553
|
-
|
|
1586
|
+
**Returns:** The resolved settings, or `null` when storage is not configured.
|
|
1554
1587
|
|
|
1555
|
-
|
|
1588
|
+
#### `createTemplateStore(config)`
|
|
1556
1589
|
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
{@link mergeEgressPorts} adds to the Fly network policy, and what
|
|
1560
|
-
{@link assertPrivateRoutesForEnv} checks the injected connection URLs against.
|
|
1590
|
+
Creates the object store used for templates, or `null` when it is not
|
|
1591
|
+
configured.
|
|
1561
1592
|
|
|
1562
1593
|
```typescript
|
|
1563
|
-
function
|
|
1594
|
+
function createTemplateStore(config: FlyioConfig): ObjectStore | null
|
|
1564
1595
|
```
|
|
1565
1596
|
|
|
1566
|
-
- `
|
|
1597
|
+
- `config` — Fly provider configuration; every field has an env fallback.
|
|
1567
1598
|
|
|
1568
|
-
**Returns:**
|
|
1599
|
+
**Returns:** An {@link ObjectStore}, or `null` when no bucket/credentials are set.
|
|
1569
1600
|
|
|
1570
|
-
#### `
|
|
1601
|
+
#### `assertTemplateId(templateId)`
|
|
1571
1602
|
|
|
1572
|
-
|
|
1603
|
+
Validate a caller-supplied template id.
|
|
1573
1604
|
|
|
1574
1605
|
```typescript
|
|
1575
|
-
function
|
|
1606
|
+
function assertTemplateId(templateId: string): void
|
|
1576
1607
|
```
|
|
1577
1608
|
|
|
1578
|
-
- `
|
|
1609
|
+
- `templateId` — The caller's identifier.
|
|
1579
1610
|
|
|
1580
|
-
|
|
1611
|
+
#### `assertCapturePath(path)`
|
|
1581
1612
|
|
|
1582
|
-
|
|
1613
|
+
Validate a path that will be interpolated into a `sh -c` command AND used as a
|
|
1614
|
+
`tar` member selector.
|
|
1583
1615
|
|
|
1584
|
-
|
|
1616
|
+
```typescript
|
|
1617
|
+
function assertCapturePath(path: string): void
|
|
1618
|
+
```
|
|
1585
1619
|
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1620
|
+
- `path` — Candidate absolute path inside the sandbox.
|
|
1621
|
+
|
|
1622
|
+
#### `archiveMember(path)`
|
|
1623
|
+
|
|
1624
|
+
Converts an absolute capture path into the relative member name used in the
|
|
1625
|
+
archive, so an archive of `/workspace` holds `workspace/…` and extracts with
|
|
1626
|
+
`tar -C /`.
|
|
1589
1627
|
|
|
1590
1628
|
```typescript
|
|
1591
|
-
function
|
|
1592
|
-
ctx: TemplateContext,
|
|
1593
|
-
templateId: string,
|
|
1594
|
-
): Promise<{ template: SandboxTemplate; manifest: TemplateManifest } | null>
|
|
1629
|
+
function archiveMember(path: string): string
|
|
1595
1630
|
```
|
|
1596
1631
|
|
|
1597
|
-
- `
|
|
1598
|
-
- `templateId` — The caller's identifier.
|
|
1599
|
-
|
|
1600
|
-
**Returns:** The template and its manifest, or `null` when no usable template exists.
|
|
1632
|
+
- `path` — Absolute capture path.
|
|
1601
1633
|
|
|
1602
|
-
|
|
1634
|
+
**Returns:** The path with its leading and trailing slashes removed.
|
|
1603
1635
|
|
|
1604
|
-
|
|
1636
|
+
#### `manifestKey(store, templateId)`
|
|
1605
1637
|
|
|
1606
|
-
|
|
1607
|
-
an eviction until it ages out. Failing the boot over it would trade a
|
|
1608
|
-
completed sandbox for a bookkeeping error.
|
|
1638
|
+
Key of a template's manifest object.
|
|
1609
1639
|
|
|
1610
1640
|
```typescript
|
|
1611
|
-
function
|
|
1641
|
+
function manifestKey(store: ObjectStore, templateId: string): string
|
|
1612
1642
|
```
|
|
1613
1643
|
|
|
1614
|
-
|
|
1615
|
-
- `key` — The lease key returned by {@link acquireRestoreLease}.
|
|
1616
|
-
|
|
1617
|
-
#### `removeTemplate(ctx, templateId)`
|
|
1644
|
+
#### `archiveKey(store, templateId)`
|
|
1618
1645
|
|
|
1619
|
-
|
|
1646
|
+
Key of a template's archive object.
|
|
1620
1647
|
|
|
1621
1648
|
```typescript
|
|
1622
|
-
function
|
|
1649
|
+
function archiveKey(store: ObjectStore, templateId: string): string
|
|
1623
1650
|
```
|
|
1624
1651
|
|
|
1625
|
-
|
|
1626
|
-
- `templateId` — The caller's identifier.
|
|
1652
|
+
#### `leaseKey(store, templateId, leaseId)`
|
|
1627
1653
|
|
|
1628
|
-
|
|
1654
|
+
Key of one in-flight restore lease.
|
|
1629
1655
|
|
|
1630
|
-
|
|
1656
|
+
```typescript
|
|
1657
|
+
function leaseKey(store: ObjectStore, templateId: string, leaseId: string): string
|
|
1658
|
+
```
|
|
1631
1659
|
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1660
|
+
#### `templateRef(store, templateId)`
|
|
1661
|
+
|
|
1662
|
+
The provider-native reference for a template. OPAQUE to callers.
|
|
1635
1663
|
|
|
1636
1664
|
```typescript
|
|
1637
|
-
function
|
|
1665
|
+
function templateRef(store: ObjectStore, templateId: string): string
|
|
1638
1666
|
```
|
|
1639
1667
|
|
|
1640
|
-
- `
|
|
1668
|
+
- `store` — The configured object store.
|
|
1669
|
+
- `templateId` — The caller's identifier.
|
|
1641
1670
|
|
|
1642
|
-
**Returns:**
|
|
1671
|
+
**Returns:** An `s3://bucket/key` reference to the archive.
|
|
1643
1672
|
|
|
1644
1673
|
#### `requireStore(ctx)`
|
|
1645
1674
|
|
|
@@ -1658,236 +1687,227 @@ function requireStore(ctx: TemplateContext): ObjectStore
|
|
|
1658
1687
|
|
|
1659
1688
|
**Returns:** The configured store.
|
|
1660
1689
|
|
|
1661
|
-
#### `
|
|
1690
|
+
#### `buildCaptureCommand(paths, url, maxBytes)`
|
|
1662
1691
|
|
|
1663
|
-
|
|
1692
|
+
Builds the in-sandbox capture command: archive the named paths, refuse an
|
|
1693
|
+
oversized result, and upload it to a presigned URL.
|
|
1694
|
+
|
|
1695
|
+
The archive is written to a file BEFORE the upload rather than piped into
|
|
1696
|
+
`curl`, for two reasons that both bite: a pipeline reports only the last
|
|
1697
|
+
command's status, so a failed `tar` would upload a truncated archive and
|
|
1698
|
+
report success; and an upload from a pipe has no `Content-Length`, so `curl`
|
|
1699
|
+
sends `Transfer-Encoding: chunked`, which S3 rejects on a presigned `PUT`.
|
|
1664
1700
|
|
|
1665
1701
|
```typescript
|
|
1666
|
-
function
|
|
1702
|
+
function buildCaptureCommand(paths: string[], url: string, maxBytes: number): string
|
|
1667
1703
|
```
|
|
1668
1704
|
|
|
1669
|
-
|
|
1705
|
+
- `paths` — Absolute capture paths, already validated.
|
|
1706
|
+
- `url` — Presigned `PUT` URL for the archive object.
|
|
1707
|
+
- `maxBytes` — Largest archive that may be uploaded.
|
|
1670
1708
|
|
|
1671
|
-
|
|
1709
|
+
**Returns:** A `sh` script.
|
|
1672
1710
|
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1711
|
+
#### `buildRestoreCommand(paths, url)`
|
|
1712
|
+
|
|
1713
|
+
Builds the in-sandbox restore command: download the archive, extract ONLY the
|
|
1714
|
+
manifest's paths, and prove no setuid/setgid file survived.
|
|
1715
|
+
|
|
1716
|
+
See the module docs for why each flag is here. The download is to a file
|
|
1717
|
+
rather than piped into `tar` so a failed download cannot be reported as a
|
|
1718
|
+
successful extraction of a truncated stream.
|
|
1678
1719
|
|
|
1679
1720
|
```typescript
|
|
1680
|
-
function
|
|
1721
|
+
function buildRestoreCommand(paths: string[], url: string): string
|
|
1681
1722
|
```
|
|
1682
1723
|
|
|
1683
|
-
- `
|
|
1724
|
+
- `paths` — Absolute capture paths from the CONTROL-PLANE manifest.
|
|
1725
|
+
- `url` — Presigned `GET` URL for the archive object.
|
|
1684
1726
|
|
|
1685
|
-
**Returns:**
|
|
1727
|
+
**Returns:** A `sh` script.
|
|
1686
1728
|
|
|
1687
|
-
#### `
|
|
1729
|
+
#### `hasLiveLease(leases, ttlMs, now)`
|
|
1688
1730
|
|
|
1689
|
-
|
|
1731
|
+
Decides whether any lease means a restore is still in flight.
|
|
1690
1732
|
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
per action, so the first backoff step deliberately exceeds a second.
|
|
1733
|
+
A lease with no readable timestamp counts as LIVE. That is the whole rule the
|
|
1734
|
+
core states for this field: an unreadable answer resolves to in-use, never to
|
|
1735
|
+
free.
|
|
1695
1736
|
|
|
1696
1737
|
```typescript
|
|
1697
|
-
function
|
|
1738
|
+
function hasLiveLease(leases: StoredObject[], ttlMs: number, now: number): boolean
|
|
1698
1739
|
```
|
|
1699
1740
|
|
|
1700
|
-
- `
|
|
1701
|
-
- `
|
|
1741
|
+
- `leases` — Lease objects for one template.
|
|
1742
|
+
- `ttlMs` — How long a lease can possibly correspond to a live restore.
|
|
1743
|
+
- `now` — Current time in ms since the epoch.
|
|
1702
1744
|
|
|
1703
|
-
**Returns:**
|
|
1745
|
+
**Returns:** `true` when at least one lease is live.
|
|
1704
1746
|
|
|
1705
|
-
#### `
|
|
1747
|
+
#### `readTemplate(ctx, templateId)`
|
|
1706
1748
|
|
|
1707
|
-
|
|
1749
|
+
Read one template, and the manifest behind it, by the caller's identifier.
|
|
1708
1750
|
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1751
|
+
Separate from {@link getTemplate} because the restore path needs the
|
|
1752
|
+
manifest's `capturePaths` — the extraction surface — and the public shape does
|
|
1753
|
+
not carry them.
|
|
1712
1754
|
|
|
1713
1755
|
```typescript
|
|
1714
|
-
function
|
|
1756
|
+
function readTemplate(
|
|
1757
|
+
ctx: TemplateContext,
|
|
1758
|
+
templateId: string,
|
|
1759
|
+
): Promise<{ template: SandboxTemplate; manifest: TemplateManifest } | null>
|
|
1715
1760
|
```
|
|
1716
1761
|
|
|
1717
|
-
- `
|
|
1762
|
+
- `ctx` — Template context.
|
|
1763
|
+
- `templateId` — The caller's identifier.
|
|
1718
1764
|
|
|
1719
|
-
**Returns:**
|
|
1765
|
+
**Returns:** The template and its manifest, or `null` when no usable template exists.
|
|
1720
1766
|
|
|
1721
|
-
#### `
|
|
1767
|
+
#### `getTemplate(ctx, templateId)`
|
|
1722
1768
|
|
|
1723
|
-
|
|
1769
|
+
Read one template by the caller's identifier.
|
|
1724
1770
|
|
|
1725
1771
|
```typescript
|
|
1726
|
-
function
|
|
1772
|
+
function getTemplate(ctx: TemplateContext, templateId: string): Promise<SandboxTemplate | null>
|
|
1727
1773
|
```
|
|
1728
1774
|
|
|
1729
|
-
- `
|
|
1775
|
+
- `ctx` — Template context.
|
|
1730
1776
|
- `templateId` — The caller's identifier.
|
|
1731
1777
|
|
|
1732
|
-
**Returns:**
|
|
1733
|
-
|
|
1734
|
-
#### `toExecResult(response)`
|
|
1778
|
+
**Returns:** The template, or `null` when no template has that id.
|
|
1735
1779
|
|
|
1736
|
-
|
|
1780
|
+
#### `listTemplates(ctx, options)`
|
|
1737
1781
|
|
|
1738
|
-
|
|
1739
|
-
no meaningful exit code, so it is rendered the way a POSIX shell does, as
|
|
1740
|
-
`128 + signal`.
|
|
1782
|
+
Enumerate templates so the caller can apply its retention policy.
|
|
1741
1783
|
|
|
1742
1784
|
```typescript
|
|
1743
|
-
function
|
|
1785
|
+
function listTemplates(
|
|
1786
|
+
ctx: TemplateContext,
|
|
1787
|
+
options?: ListTemplatesOptions,
|
|
1788
|
+
): Promise<SandboxTemplate[]>
|
|
1744
1789
|
```
|
|
1745
1790
|
|
|
1746
|
-
- `
|
|
1791
|
+
- `ctx` — Template context.
|
|
1792
|
+
- `options` — Narrowing by id prefix.
|
|
1747
1793
|
|
|
1748
|
-
**Returns:**
|
|
1794
|
+
**Returns:** Every matching template.
|
|
1749
1795
|
|
|
1750
|
-
#### `
|
|
1796
|
+
#### `commitTemplate(ctx, options)`
|
|
1751
1797
|
|
|
1752
|
-
|
|
1798
|
+
Capture a sandbox's filesystem into a reusable template.
|
|
1753
1799
|
|
|
1754
1800
|
```typescript
|
|
1755
|
-
function
|
|
1801
|
+
function commitTemplate(
|
|
1802
|
+
ctx: TemplateContext,
|
|
1803
|
+
options: CommitTemplateOptions,
|
|
1804
|
+
): Promise<SandboxTemplate>
|
|
1756
1805
|
```
|
|
1757
1806
|
|
|
1758
|
-
- `
|
|
1759
|
-
- `
|
|
1760
|
-
|
|
1761
|
-
**Returns:** The composite sandbox id.
|
|
1807
|
+
- `ctx` — Template context.
|
|
1808
|
+
- `options` — What to capture and what to call it.
|
|
1762
1809
|
|
|
1763
|
-
|
|
1810
|
+
**Returns:** The template as it now exists.
|
|
1764
1811
|
|
|
1765
|
-
|
|
1812
|
+
#### `removeTemplate(ctx, templateId)`
|
|
1766
1813
|
|
|
1767
|
-
|
|
1768
|
-
policy widened outside this provider — edited by hand to unblock something, or
|
|
1769
|
-
left behind by an older build — is invisible to it: the probe's own port stays
|
|
1770
|
-
denied and the verdict stays `filtered` while a port nothing here asked for is
|
|
1771
|
-
open to the whole internet. Comparing what Fly holds against what this
|
|
1772
|
-
provider intended is the one check that can catch that, and it is why the
|
|
1773
|
-
verdict reads the policy back rather than trusting the write.
|
|
1814
|
+
Delete a template, refusing while a restore is still reading it.
|
|
1774
1815
|
|
|
1775
1816
|
```typescript
|
|
1776
|
-
function
|
|
1777
|
-
applied: FlyNetworkPolicyPort[] | undefined,
|
|
1778
|
-
intended: FlyNetworkPolicyPort[] | undefined,
|
|
1779
|
-
): FlyNetworkPolicyPort[]
|
|
1817
|
+
function removeTemplate(ctx: TemplateContext, templateId: string): Promise<void>
|
|
1780
1818
|
```
|
|
1781
1819
|
|
|
1782
|
-
- `
|
|
1783
|
-
- `
|
|
1784
|
-
|
|
1785
|
-
**Returns:** The applied ports with no counterpart in `intended`, in order.
|
|
1786
|
-
|
|
1787
|
-
#### `verdictForProbeExit(exitCode, context)`
|
|
1788
|
-
|
|
1789
|
-
Maps a probe's exit status onto an {@link EgressVerdict}.
|
|
1790
|
-
|
|
1791
|
-
The mapping is the whole security contract of this file, so it is total and
|
|
1792
|
-
has exactly one path to each state:
|
|
1820
|
+
- `ctx` — Template context.
|
|
1821
|
+
- `templateId` — The caller's identifier.
|
|
1793
1822
|
|
|
1794
|
-
|
|
1795
|
-
| --------------------------------- | -------------- | ---------------------------------------- |
|
|
1796
|
-
| {@link EGRESS_PROBE_EXIT_REACHED} | `open` | A raw socket reached a public IP. |
|
|
1797
|
-
| {@link EGRESS_PROBE_EXIT_BLOCKED} | `filtered` | Every attempt was refused or timed out. |
|
|
1798
|
-
| anything else | `inconclusive` | The probe did not complete its attempts. |
|
|
1823
|
+
#### `acquireRestoreLease(ctx, templateId, leaseId)`
|
|
1799
1824
|
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1825
|
+
Records that a restore of this template is in flight.
|
|
1826
|
+
|
|
1827
|
+
The lease lives in the store rather than in this process, so a second control
|
|
1828
|
+
plane enforcing a retention budget can see it. A control plane that dies
|
|
1829
|
+
mid-restore leaves the lease behind; it stops counting once it is older than
|
|
1830
|
+
the transfer budget, because no live restore can outlast that budget.
|
|
1806
1831
|
|
|
1807
1832
|
```typescript
|
|
1808
|
-
function
|
|
1833
|
+
function acquireRestoreLease(
|
|
1834
|
+
ctx: TemplateContext,
|
|
1835
|
+
templateId: string,
|
|
1836
|
+
leaseId: string,
|
|
1837
|
+
): Promise<string>
|
|
1809
1838
|
```
|
|
1810
1839
|
|
|
1811
|
-
- `
|
|
1812
|
-
- `
|
|
1840
|
+
- `ctx` — Template context.
|
|
1841
|
+
- `templateId` — The caller's identifier.
|
|
1842
|
+
- `leaseId` — Unique id for this restore.
|
|
1813
1843
|
|
|
1814
|
-
**Returns:** The
|
|
1844
|
+
**Returns:** The lease's key, for release.
|
|
1815
1845
|
|
|
1816
|
-
|
|
1846
|
+
#### `releaseRestoreLease(ctx, key)`
|
|
1817
1847
|
|
|
1818
|
-
|
|
1848
|
+
Releases a restore lease.
|
|
1819
1849
|
|
|
1820
|
-
|
|
1850
|
+
Best-effort: the restore has already finished, and a stale lease only delays
|
|
1851
|
+
an eviction until it ages out. Failing the boot over it would trade a
|
|
1852
|
+
completed sandbox for a bookkeeping error.
|
|
1821
1853
|
|
|
1822
1854
|
```typescript
|
|
1823
|
-
|
|
1855
|
+
function releaseRestoreLease(ctx: TemplateContext, key: string): Promise<void>
|
|
1824
1856
|
```
|
|
1825
1857
|
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
Default probe targets: one IPv4 and one IPv6 literal, both anycast resolvers
|
|
1829
|
-
that answer on 443 from anywhere. IPv6 is included because Fly Machines have
|
|
1830
|
-
native public IPv6 egress and "often egress over IPv6 when the destination
|
|
1831
|
-
has an AAAA record" (https://fly.io/docs/networking/egress-ips/) — probing
|
|
1832
|
-
only IPv4 could miss an open v6 path entirely.
|
|
1858
|
+
- `ctx` — Template context.
|
|
1859
|
+
- `key` — The lease key returned by {@link acquireRestoreLease}.
|
|
1833
1860
|
|
|
1834
|
-
|
|
1835
|
-
const DEFAULT_EGRESS_PROBE_TARGETS: '1.1.1.1:443,[2606:4700:4700::1111]:443'
|
|
1836
|
-
```
|
|
1861
|
+
#### `shellQuote(value)`
|
|
1837
1862
|
|
|
1838
|
-
|
|
1863
|
+
Wraps a string in single quotes, escaping any single quotes it contains.
|
|
1839
1864
|
|
|
1840
|
-
|
|
1865
|
+
Every command this provider runs goes through `sh -c`, so an unquoted path or
|
|
1866
|
+
value is a command-injection vector. Double quotes are NOT sufficient — `$()`,
|
|
1867
|
+
backticks and `!` still expand inside them.
|
|
1841
1868
|
|
|
1842
1869
|
```typescript
|
|
1843
|
-
|
|
1870
|
+
function shellQuote(value: string): string
|
|
1844
1871
|
```
|
|
1845
1872
|
|
|
1846
|
-
|
|
1873
|
+
- `value` — The raw string to embed in a shell command.
|
|
1847
1874
|
|
|
1848
|
-
|
|
1849
|
-
is the value flyctl passes for `fly ips allocate-v6 --private`; the field has
|
|
1850
|
-
no enum in Fly's OpenAPI specification, so it is configurable rather than
|
|
1851
|
-
hardcoded.
|
|
1875
|
+
**Returns:** A single-quoted, shell-safe token.
|
|
1852
1876
|
|
|
1853
|
-
|
|
1854
|
-
const DEFAULT_PRIVATE_IP_TYPE: 'private_v6'
|
|
1855
|
-
```
|
|
1877
|
+
#### `renderEnvExports(env)`
|
|
1856
1878
|
|
|
1857
|
-
|
|
1879
|
+
Renders `ExecOptions.env` as `export` statements to prepend to a script.
|
|
1858
1880
|
|
|
1859
|
-
|
|
1881
|
+
Fly's exec endpoint takes no environment map (only `command`, `stdin`,
|
|
1882
|
+
`container` and `timeout`), so per-command environment has to be set inside
|
|
1883
|
+
the shell script itself.
|
|
1860
1884
|
|
|
1861
1885
|
```typescript
|
|
1862
|
-
|
|
1886
|
+
function renderEnvExports(env: Record<string, string> | undefined): string[]
|
|
1863
1887
|
```
|
|
1864
1888
|
|
|
1865
|
-
|
|
1889
|
+
- `env` — Environment variables to set for the command.
|
|
1866
1890
|
|
|
1867
|
-
|
|
1868
|
-
accept `auto`; the AWS SDK requires _some_ region to build a signature, so this
|
|
1869
|
-
is not optional even against a store that ignores it.
|
|
1891
|
+
**Returns:** One `export NAME='value'` line per variable, in insertion order.
|
|
1870
1892
|
|
|
1871
|
-
|
|
1872
|
-
const DEFAULT_TEMPLATE_REGION: 'auto'
|
|
1873
|
-
```
|
|
1893
|
+
### Constants
|
|
1874
1894
|
|
|
1875
|
-
#### `
|
|
1895
|
+
#### `DEFAULT_EGRESS_POLICY_NAME`
|
|
1876
1896
|
|
|
1877
|
-
Default
|
|
1897
|
+
Default name of the egress policy this provider owns on each sandbox app.
|
|
1878
1898
|
|
|
1879
1899
|
```typescript
|
|
1880
|
-
const
|
|
1900
|
+
const DEFAULT_EGRESS_POLICY_NAME: 'molecule-sandbox-egress'
|
|
1881
1901
|
```
|
|
1882
1902
|
|
|
1883
|
-
#### `
|
|
1903
|
+
#### `EGRESS_PROBE_EXIT_REACHED`
|
|
1884
1904
|
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1905
|
+
Probe exit status meaning at least one raw connection to a public IP
|
|
1906
|
+
SUCCEEDED. Deliberately not 1, so an interpreter error (which exits 1) can
|
|
1907
|
+
never be mistaken for a completed observation.
|
|
1888
1908
|
|
|
1889
1909
|
```typescript
|
|
1890
|
-
const
|
|
1910
|
+
const EGRESS_PROBE_EXIT_REACHED: 9
|
|
1891
1911
|
```
|
|
1892
1912
|
|
|
1893
1913
|
#### `EGRESS_PROBE_EXIT_BLOCKED`
|
|
@@ -1898,38 +1918,46 @@ Probe exit status meaning every raw connection attempt was refused or timed out.
|
|
|
1898
1918
|
const EGRESS_PROBE_EXIT_BLOCKED: 0
|
|
1899
1919
|
```
|
|
1900
1920
|
|
|
1901
|
-
#### `
|
|
1921
|
+
#### `DEFAULT_EGRESS_PROBE_TARGETS`
|
|
1902
1922
|
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1923
|
+
Default probe targets: one IPv4 and one IPv6 literal, both anycast resolvers
|
|
1924
|
+
that answer on 443 from anywhere. IPv6 is included because Fly Machines have
|
|
1925
|
+
native public IPv6 egress and "often egress over IPv6 when the destination
|
|
1926
|
+
has an AAAA record" (https://fly.io/docs/networking/egress-ips/) — probing
|
|
1927
|
+
only IPv4 could miss an open v6 path entirely.
|
|
1906
1928
|
|
|
1907
1929
|
```typescript
|
|
1908
|
-
const
|
|
1930
|
+
const DEFAULT_EGRESS_PROBE_TARGETS: '1.1.1.1:443,[2606:4700:4700::1111]:443'
|
|
1909
1931
|
```
|
|
1910
1932
|
|
|
1911
|
-
#### `
|
|
1933
|
+
#### `FLY_EXEC_MAX_TIMEOUT_SECONDS`
|
|
1912
1934
|
|
|
1913
|
-
|
|
1935
|
+
Fly's hard ceiling on the exec endpoint's `timeout` field, in seconds.
|
|
1936
|
+
Verified against Fly staff guidance (see the module docs); a larger value is
|
|
1937
|
+
rejected by the API rather than clamped.
|
|
1914
1938
|
|
|
1915
1939
|
```typescript
|
|
1916
|
-
const
|
|
1940
|
+
const FLY_EXEC_MAX_TIMEOUT_SECONDS: 60
|
|
1917
1941
|
```
|
|
1918
1942
|
|
|
1919
|
-
#### `
|
|
1943
|
+
#### `DIRECT_EXEC_BUDGET_SECONDS`
|
|
1920
1944
|
|
|
1921
|
-
|
|
1945
|
+
Largest budget still run as a single direct exec, in seconds. Kept below
|
|
1946
|
+
{@link FLY_EXEC_MAX_TIMEOUT_SECONDS} so the API's own timeout is never the
|
|
1947
|
+
thing that fires first — the caller's timeout should be.
|
|
1922
1948
|
|
|
1923
1949
|
```typescript
|
|
1924
|
-
const
|
|
1950
|
+
const DIRECT_EXEC_BUDGET_SECONDS: 55
|
|
1925
1951
|
```
|
|
1926
1952
|
|
|
1927
|
-
#### `
|
|
1953
|
+
#### `MAX_CAPTURED_OUTPUT_BYTES`
|
|
1928
1954
|
|
|
1929
|
-
|
|
1955
|
+
Cap on captured stdout/stderr per detached command, in bytes. The exec
|
|
1956
|
+
response carries output inline as JSON, so an unbounded `cat` of a build log
|
|
1957
|
+
would try to materialize the whole thing in one response body.
|
|
1930
1958
|
|
|
1931
1959
|
```typescript
|
|
1932
|
-
const
|
|
1960
|
+
const MAX_CAPTURED_OUTPUT_BYTES: number
|
|
1933
1961
|
```
|
|
1934
1962
|
|
|
1935
1963
|
#### `FLY_EXEC_MAX_ARG_BYTES`
|
|
@@ -1946,14 +1974,15 @@ ceiling; callers that move bulk data (writeFile, importFiles) chunk beneath it.
|
|
|
1946
1974
|
const FLY_EXEC_MAX_ARG_BYTES: 15000
|
|
1947
1975
|
```
|
|
1948
1976
|
|
|
1949
|
-
#### `
|
|
1977
|
+
#### `INDETERMINATE_EXIT_CODE`
|
|
1950
1978
|
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1979
|
+
Exit code reported when a command's outcome is genuinely unknown — it was
|
|
1980
|
+
still running when the caller's budget expired, or Fly returned no status.
|
|
1981
|
+
Distinct from every real shell exit status (0-255), matching the Docker
|
|
1982
|
+
provider's convention so callers can tell "unfinished" from "failed".
|
|
1954
1983
|
|
|
1955
1984
|
```typescript
|
|
1956
|
-
const
|
|
1985
|
+
const INDETERMINATE_EXIT_CODE: -1
|
|
1957
1986
|
```
|
|
1958
1987
|
|
|
1959
1988
|
#### `FLYCAST_SUFFIX`
|
|
@@ -1966,48 +1995,52 @@ allocated to it, there will be an AAAA record at `my-app-name.flycast`"
|
|
|
1966
1995
|
const FLYCAST_SUFFIX: '.flycast'
|
|
1967
1996
|
```
|
|
1968
1997
|
|
|
1969
|
-
#### `
|
|
1998
|
+
#### `PRIVATE_ROUTES_METADATA_SUFFIX`
|
|
1970
1999
|
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
2000
|
+
Machine-metadata key suffix (after the configured prefix) recording the
|
|
2001
|
+
private addresses allocated for a project, so `destroy()` can release exactly
|
|
2002
|
+
the ones it created. See consequence 2 in the module description: a listing on
|
|
2003
|
+
the target app cannot tell you which network an address serves.
|
|
1975
2004
|
|
|
1976
2005
|
```typescript
|
|
1977
|
-
const
|
|
2006
|
+
const PRIVATE_ROUTES_METADATA_SUFFIX: 'privateRoutes'
|
|
1978
2007
|
```
|
|
1979
2008
|
|
|
1980
|
-
#### `
|
|
2009
|
+
#### `DEFAULT_PRIVATE_IP_TYPE`
|
|
1981
2010
|
|
|
1982
|
-
|
|
1983
|
-
the
|
|
1984
|
-
|
|
1985
|
-
|
|
2011
|
+
`type` sent to `POST /apps/{app}/ip_assignments` for a Flycast address. This
|
|
2012
|
+
is the value flyctl passes for `fly ips allocate-v6 --private`; the field has
|
|
2013
|
+
no enum in Fly's OpenAPI specification, so it is configurable rather than
|
|
2014
|
+
hardcoded.
|
|
1986
2015
|
|
|
1987
2016
|
```typescript
|
|
1988
|
-
const
|
|
2017
|
+
const DEFAULT_PRIVATE_IP_TYPE: 'private_v6'
|
|
1989
2018
|
```
|
|
1990
2019
|
|
|
1991
|
-
#### `
|
|
2020
|
+
#### `provider`
|
|
1992
2021
|
|
|
1993
|
-
|
|
1994
|
-
object up to 5 GB in size"
|
|
1995
|
-
(https://docs.aws.amazon.com/AmazonS3/latest/userguide/upload-objects.html).
|
|
1996
|
-
The sandbox uploads with one presigned PUT, so an archive above this cannot be
|
|
1997
|
-
stored and the capture refuses before spending the bandwidth.
|
|
2022
|
+
The provider implementation.
|
|
1998
2023
|
|
|
1999
2024
|
```typescript
|
|
2000
|
-
const
|
|
2025
|
+
const provider: SandboxProvider
|
|
2001
2026
|
```
|
|
2002
2027
|
|
|
2003
|
-
#### `
|
|
2028
|
+
#### `DEFAULT_TEMPLATE_PREFIX`
|
|
2004
2029
|
|
|
2005
|
-
|
|
2006
|
-
response carries output inline as JSON, so an unbounded `cat` of a build log
|
|
2007
|
-
would try to materialize the whole thing in one response body.
|
|
2030
|
+
Default key prefix for every object this bond writes.
|
|
2008
2031
|
|
|
2009
2032
|
```typescript
|
|
2010
|
-
const
|
|
2033
|
+
const DEFAULT_TEMPLATE_PREFIX: 'molecule-sandbox-templates'
|
|
2034
|
+
```
|
|
2035
|
+
|
|
2036
|
+
#### `DEFAULT_TEMPLATE_REGION`
|
|
2037
|
+
|
|
2038
|
+
Region sent when none is configured. Tigris (and most S3-compatible stores)
|
|
2039
|
+
accept `auto`; the AWS SDK requires _some_ region to build a signature, so this
|
|
2040
|
+
is not optional even against a store that ignores it.
|
|
2041
|
+
|
|
2042
|
+
```typescript
|
|
2043
|
+
const DEFAULT_TEMPLATE_REGION: 'auto'
|
|
2011
2044
|
```
|
|
2012
2045
|
|
|
2013
2046
|
#### `MAX_PRESIGN_EXPIRY_SECONDS`
|
|
@@ -2022,23 +2055,67 @@ refuses — so it is clamped here.
|
|
|
2022
2055
|
const MAX_PRESIGN_EXPIRY_SECONDS: number
|
|
2023
2056
|
```
|
|
2024
2057
|
|
|
2025
|
-
#### `
|
|
2058
|
+
#### `MAX_ARCHIVE_BYTES`
|
|
2026
2059
|
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2060
|
+
Single-`PUT` ceiling: "With a single `PUT` operation, you can upload a single
|
|
2061
|
+
object up to 5 GB in size"
|
|
2062
|
+
(https://docs.aws.amazon.com/AmazonS3/latest/userguide/upload-objects.html).
|
|
2063
|
+
The sandbox uploads with one presigned PUT, so an archive above this cannot be
|
|
2064
|
+
stored and the capture refuses before spending the bandwidth.
|
|
2031
2065
|
|
|
2032
2066
|
```typescript
|
|
2033
|
-
const
|
|
2067
|
+
const MAX_ARCHIVE_BYTES: number
|
|
2034
2068
|
```
|
|
2035
2069
|
|
|
2036
|
-
#### `
|
|
2070
|
+
#### `DEFAULT_TRANSFER_TIMEOUT_MS`
|
|
2037
2071
|
|
|
2038
|
-
|
|
2072
|
+
Default wall-clock budget for a capture or restore transfer, in ms.
|
|
2039
2073
|
|
|
2040
2074
|
```typescript
|
|
2041
|
-
const
|
|
2075
|
+
const DEFAULT_TRANSFER_TIMEOUT_MS: 900000
|
|
2076
|
+
```
|
|
2077
|
+
|
|
2078
|
+
#### `DEFAULT_PRESIGN_EXPIRY_SECONDS`
|
|
2079
|
+
|
|
2080
|
+
Default lifetime of a presigned capture/restore URL, in seconds.
|
|
2081
|
+
|
|
2082
|
+
```typescript
|
|
2083
|
+
const DEFAULT_PRESIGN_EXPIRY_SECONDS: 3600
|
|
2084
|
+
```
|
|
2085
|
+
|
|
2086
|
+
#### `LEASE_GRACE_MS`
|
|
2087
|
+
|
|
2088
|
+
Extra time a restore lease counts as live beyond the transfer budget, covering
|
|
2089
|
+
the Machine boot and the API round trips that bracket the transfer itself.
|
|
2090
|
+
A lease older than `transferTimeoutMs + this` cannot belong to a live restore,
|
|
2091
|
+
because the restore's own exec is bounded by that budget.
|
|
2092
|
+
|
|
2093
|
+
```typescript
|
|
2094
|
+
const LEASE_GRACE_MS: 120000
|
|
2095
|
+
```
|
|
2096
|
+
|
|
2097
|
+
#### `EXIT_ARCHIVE_TOO_LARGE`
|
|
2098
|
+
|
|
2099
|
+
Exit status the capture script uses when the archive is over the size ceiling.
|
|
2100
|
+
|
|
2101
|
+
```typescript
|
|
2102
|
+
const EXIT_ARCHIVE_TOO_LARGE: 90
|
|
2103
|
+
```
|
|
2104
|
+
|
|
2105
|
+
#### `EXIT_SETUID_SURVIVED`
|
|
2106
|
+
|
|
2107
|
+
Exit status the restore script uses when a setuid/setgid file survived the sweep.
|
|
2108
|
+
|
|
2109
|
+
```typescript
|
|
2110
|
+
const EXIT_SETUID_SURVIVED: 91
|
|
2111
|
+
```
|
|
2112
|
+
|
|
2113
|
+
#### `EXIT_TAR_FAILED`
|
|
2114
|
+
|
|
2115
|
+
Exit status the capture script uses when `tar` failed outright (as opposed to warning).
|
|
2116
|
+
|
|
2117
|
+
```typescript
|
|
2118
|
+
const EXIT_TAR_FAILED: 92
|
|
2042
2119
|
```
|
|
2043
2120
|
|
|
2044
2121
|
#### `TAR_CHANGED_MARKER`
|
|
@@ -2374,12 +2451,26 @@ Fly documents the Machines API at "1 request, per second, per action … with a
|
|
|
2374
2451
|
short-term burst limit up to 3 req/s, per action", scoped "per-action,
|
|
2375
2452
|
per-machine … That might be Machine ID or App ID, depending on the type of
|
|
2376
2453
|
request", with Get Machine at 5 req/s and app deletions capped at 100 per
|
|
2377
|
-
minute. `429` and `5xx` are retried with `Retry-After`-aware backoff
|
|
2378
|
-
`
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2454
|
+
minute. `429` and `5xx` are retried with `Retry-After`-aware backoff, plus the
|
|
2455
|
+
`404` an app-propagation lag answers the Machine create with; every other
|
|
2456
|
+
`4xx` is a real answer and is never retried. The consequence to plan for is
|
|
2457
|
+
that EVERY file operation is an exec API call, so one sandbox's reads and
|
|
2458
|
+
writes serialize at roughly one per second and a multi-megabyte `writeFile`
|
|
2459
|
+
(chunked at ~45 KB decoded) takes tens of seconds. An agent that edits files
|
|
2460
|
+
in a tight loop will feel this; batch where you can.
|
|
2461
|
+
|
|
2462
|
+
**A refused exec that dropped the connection is retried; a refused exec that
|
|
2463
|
+
names a Machine state is not.** Fly answers `412 failed_precondition: exec
|
|
2464
|
+
request failed: EOF` when the request reached a `started` Machine's exec agent
|
|
2465
|
+
and the connection died before the command ran — a Machine seconds past boot,
|
|
2466
|
+
or Fly's own exec burst. `exec` retries that one at the transport (up to four
|
|
2467
|
+
attempts, ~11 s of backoff, each preceded by a Machine-state read and a wait
|
|
2468
|
+
for `started` when it has not got there), because every caller's alternative
|
|
2469
|
+
is to throw away a whole boot or deploy and start over. The OTHER `412`,
|
|
2470
|
+
`failed_precondition: machine not running`, is a real state and is not
|
|
2471
|
+
repeated: `start()`/`wake()` wait it out before the first exec. A 412 that is
|
|
2472
|
+
not retried comes back with the Machine's current state appended to its
|
|
2473
|
+
message, and a 412 the retries could not clear carries the state it ended in.
|
|
2383
2474
|
|
|
2384
2475
|
**`create()` launches the Machine, and `start()`/`wake()` WAIT for it.** Unlike
|
|
2385
2476
|
Docker's create-then-start, the Machines API starts a Machine as part of
|