@molecule/api-code-sandbox-flyio 1.0.8 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/exec.d.ts CHANGED
@@ -41,6 +41,16 @@ export declare const DIRECT_EXEC_BUDGET_SECONDS = 55;
41
41
  * would try to materialize the whole thing in one response body.
42
42
  */
43
43
  export declare const MAX_CAPTURED_OUTPUT_BYTES: number;
44
+ /**
45
+ * Hard cap on the `sh -c <script>` argument SENT to Fly's exec endpoint. This is
46
+ * NOT the OS `MAX_ARG_STRLEN` (128 KB) — the Fly Machines exec API rejects a
47
+ * command well below that, and does so INVISIBLY: it answers `200` with
48
+ * `exit_code: 0` and an `Unhandled rejection: Rejection([PayloadTooLarge…])`
49
+ * body, so the command never runs yet reads as success. Measured against the
50
+ * live API: ~15.8 KB scripts run, ~23.7 KB are rejected. 15 KB is the safe
51
+ * ceiling; callers that move bulk data (writeFile, importFiles) chunk beneath it.
52
+ */
53
+ export declare const FLY_EXEC_MAX_ARG_BYTES = 15000;
44
54
  /**
45
55
  * Exit code reported when a command's outcome is genuinely unknown — it was
46
56
  * still running when the caller's budget expired, or Fly returned no status.
@@ -1 +1 @@
1
- {"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../src/exec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAA;AAGzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAKjD;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,KAAK,CAAA;AAE9C;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,KAAK,CAAA;AAW5C;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,QAAkB,CAAA;AAWxD;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,KAAK,CAAA;AAEzC,0FAA0F;AAC1F,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,cAAc,EAAE,MAAM,KAAK,OAAO,CAAC,eAAe,CAAC,CAAA;AAE7F;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,eAAe,GAAG,UAAU,CAYlE;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,WAAW,GAAG,SAAS,EAC7B,UAAU,EAAE,MAAM,GACjB,MAAM,CAGR;AAaD;;;;;;;;;;;GAWG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,WAAW,GAAG,SAAS,EAC7B,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,UAAU,CAAC,CAUrB"}
1
+ {"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../src/exec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAA;AAGzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAKjD;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,KAAK,CAAA;AAE9C;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,KAAK,CAAA;AAW5C;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,QAAkB,CAAA;AAQxD;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,QAAS,CAAA;AAY5C;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,KAAK,CAAA;AAEzC,0FAA0F;AAC1F,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,cAAc,EAAE,MAAM,KAAK,OAAO,CAAC,eAAe,CAAC,CAAA;AAE7F;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,eAAe,GAAG,UAAU,CAiBlE;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,WAAW,GAAG,SAAS,EAC7B,UAAU,EAAE,MAAM,GACjB,MAAM,CAGR;AAaD;;;;;;;;;;;GAWG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,WAAW,GAAG,SAAS,EAC7B,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,UAAU,CAAC,CAqBrB"}
package/dist/exec.js CHANGED
@@ -54,6 +54,22 @@ export const MAX_CAPTURED_OUTPUT_BYTES = 5 * 1024 * 1024;
54
54
  * argument, which Linux limits to MAX_ARG_STRLEN (128 KB).
55
55
  */
56
56
  const MAX_SCRIPT_BASE64_BYTES = 60_000;
57
+ /**
58
+ * Hard cap on the `sh -c <script>` argument SENT to Fly's exec endpoint. This is
59
+ * NOT the OS `MAX_ARG_STRLEN` (128 KB) — the Fly Machines exec API rejects a
60
+ * command well below that, and does so INVISIBLY: it answers `200` with
61
+ * `exit_code: 0` and an `Unhandled rejection: Rejection([PayloadTooLarge…])`
62
+ * body, so the command never runs yet reads as success. Measured against the
63
+ * live API: ~15.8 KB scripts run, ~23.7 KB are rejected. 15 KB is the safe
64
+ * ceiling; callers that move bulk data (writeFile, importFiles) chunk beneath it.
65
+ */
66
+ export const FLY_EXEC_MAX_ARG_BYTES = 15_000;
67
+ /**
68
+ * Marker Fly puts in an exec response body when it REJECTS the command instead
69
+ * of running it (most often {@link FLY_EXEC_MAX_ARG_BYTES} exceeded). Detected so
70
+ * a rejection becomes a loud failure instead of a silent `exit_code: 0`.
71
+ */
72
+ const FLY_EXEC_REJECTION = 'Rejection([';
57
73
  /** Directory inside the Machine holding detached-command bookkeeping files. */
58
74
  const WORK_DIR = '/tmp/.mol-exec';
59
75
  /**
@@ -73,16 +89,21 @@ export const INDETERMINATE_EXIT_CODE = -1;
73
89
  * @returns The normalized result.
74
90
  */
75
91
  export function toExecResult(response) {
92
+ const stdout = response.stdout ?? '';
93
+ const stderr = response.stderr ?? '';
94
+ // Fly answers a REJECTED command (over-limit payload, bad method) with a 200,
95
+ // `exit_code: 0`, and a rejection body — the command never ran. Trusting that
96
+ // 0 silently dropped every oversized write. Surface it as a failure so
97
+ // exit-code checks catch it instead of reporting phantom success.
98
+ if (stderr.includes(FLY_EXEC_REJECTION) || stdout.includes(FLY_EXEC_REJECTION)) {
99
+ return { stdout, stderr, exitCode: INDETERMINATE_EXIT_CODE };
100
+ }
76
101
  const exitCode = typeof response.exit_code === 'number'
77
102
  ? response.exit_code
78
103
  : typeof response.exit_signal === 'number' && response.exit_signal > 0
79
104
  ? 128 + response.exit_signal
80
105
  : INDETERMINATE_EXIT_CODE;
81
- return {
82
- stdout: response.stdout ?? '',
83
- stderr: response.stderr ?? '',
84
- exitCode,
85
- };
106
+ return { stdout, stderr, exitCode };
86
107
  }
87
108
  /**
88
109
  * Builds the shell script body for a command: change directory, export the
@@ -122,6 +143,14 @@ function newRunId() {
122
143
  export async function execCommand(rawExec, command, opts, defaultCwd) {
123
144
  const budgetMs = opts?.timeout ?? DEFAULT_TIMEOUT_MS;
124
145
  const script = buildScript(command, opts, defaultCwd);
146
+ // A direct command travels as the `sh -c <script>` argument. Fly rejects one
147
+ // over FLY_EXEC_MAX_ARG_BYTES with a silent `exit_code: 0` (see the constant),
148
+ // so refuse it here with a message that names the real limit rather than
149
+ // letting the caller's data vanish. Bulk callers chunk beneath this.
150
+ if (script.length > FLY_EXEC_MAX_ARG_BYTES) {
151
+ throw new Error(`Fly exec command is ${script.length} bytes, over the ${FLY_EXEC_MAX_ARG_BYTES}-byte ` +
152
+ `limit the API silently rejects — chunk the payload smaller.`);
153
+ }
125
154
  if (budgetMs <= DIRECT_EXEC_BUDGET_SECONDS * 1000) {
126
155
  const seconds = Math.max(1, Math.min(Math.ceil(budgetMs / 1000), FLY_EXEC_MAX_TIMEOUT_SECONDS));
127
156
  return toExecResult(await rawExec(['sh', '-c', script], seconds));
@@ -1 +1 @@
1
- {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,KAAK,EAUV,eAAe,EAEhB,MAAM,4BAA4B,CAAA;AAGnC,OAAO,EAAE,YAAY,EAAgC,MAAM,UAAU,CAAA;AAsBrE,OAAO,EAAmD,KAAK,WAAW,EAAE,MAAM,cAAc,CAAA;AAEhG,OAAO,KAAK,EAEV,WAAW,EAUZ,MAAM,YAAY,CAAA;AA6yDnB;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,MAAM,CAAC,EAAE,WAAW,EACpB,MAAM,CAAC,EAAE,YAAY,EACrB,KAAK,CAAC,EAAE,WAAW,GAAG,IAAI,GACzB,eAAe,CAEjB"}
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,KAAK,EAUV,eAAe,EAEhB,MAAM,4BAA4B,CAAA;AAGnC,OAAO,EAAE,YAAY,EAAgC,MAAM,UAAU,CAAA;AAsBrE,OAAO,EAAmD,KAAK,WAAW,EAAE,MAAM,cAAc,CAAA;AAEhG,OAAO,KAAK,EAEV,WAAW,EAUZ,MAAM,YAAY,CAAA;AAmzDnB;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,MAAM,CAAC,EAAE,WAAW,EACpB,MAAM,CAAC,EAAE,YAAY,EACrB,KAAK,CAAC,EAAE,WAAW,GAAG,IAAI,GACzB,eAAe,CAEjB"}
package/dist/provider.js CHANGED
@@ -42,11 +42,13 @@ const LIST_PAGE_SIZE = 200;
42
42
  /** Hard cap on pages fetched while listing, so a bad cursor cannot loop forever. */
43
43
  const LIST_MAX_PAGES = 50;
44
44
  /**
45
- * Chunk size for base64-encoded `writeFile` payloads. `sh -c` receives the whole
46
- * command as ONE argument, which Linux caps at MAX_ARG_STRLEN (128 KB), so a
47
- * file over ~45 KB has to be written across several appends.
45
+ * Chunk size for base64-encoded `writeFile` payloads. The binding limit is NOT
46
+ * the OS `MAX_ARG_STRLEN` (128 KB) but Fly's exec API, which silently rejects a
47
+ * command over ~15 KB (`FLY_EXEC_MAX_ARG_BYTES`) with a phantom `exit_code: 0` —
48
+ * so this stays well under it, leaving room for the `printf … | base64 -d`
49
+ * wrapper. A file over ~9 KB is written across several appends.
48
50
  */
49
- const WRITE_CHUNK_BASE64 = 60_000;
51
+ const WRITE_CHUNK_BASE64 = 12_000;
50
52
  /**
51
53
  * Budget for the file-operation execs (`cat`, `ls`, `rm`, chunked writes), in ms.
52
54
  *
@@ -81,15 +83,13 @@ const DEFAULT_PROBE_TIMEOUT_MS = 3000;
81
83
  */
82
84
  const IMPORT_EXTRACT_TIMEOUT_MS = 55_000;
83
85
  /**
84
- * Base64 chunk size for {@link Sandbox.importFiles}'s temp-file write. Larger
85
- * than `WRITE_CHUNK_BASE64` (which is tuned for small single files) so a
86
- * multi-megabyte scaffold moves in fewer exec calls — each exec spends Fly's
87
- * per-action rate-limit budget, and a burst of small chunks trips 429s (and, on
88
- * a busy API, transient 404s). Kept well under the 128 KB `MAX_ARG_STRLEN`
89
- * ceiling once the `printf … | base64 -d` wrapper and `buildScript` prefix are
90
- * added.
86
+ * Base64 chunk size for {@link Sandbox.importFiles}'s temp-file write. Bounded by
87
+ * the same Fly exec payload limit as `WRITE_CHUNK_BASE64` (~15 KB, silently
88
+ * enforced) — a larger chunk is rejected with a phantom `exit_code: 0` and the
89
+ * bytes vanish. A multi-megabyte scaffold therefore moves in many small appends;
90
+ * the gzip pass before transfer is what keeps that count reasonable.
91
91
  */
92
- const IMPORT_WRITE_CHUNK_BASE64 = 100_000;
92
+ const IMPORT_WRITE_CHUNK_BASE64 = 12_000;
93
93
  /** Extra seconds allowed for the probe exec beyond its connection budget. */
94
94
  const PROBE_EXEC_HEADROOM_SECONDS = 10;
95
95
  /**
@@ -209,10 +209,16 @@ class FlyioSandboxProvider {
209
209
  * Builds the Fly Proxy service exposing a sandbox's preview port, or `undefined`
210
210
  * when public exposure is disabled.
211
211
  *
212
- * `autostop: 'suspend'` is the scale-to-zero behaviour this bond exists for:
213
- * Fly Proxy suspends the Machine when it goes idle and resumes it from its
214
- * memory snapshot on the next request, so an idle sandbox bills only for its
215
- * rootfs and volume.
212
+ * `autostart: true` wakes an idle (stopped or suspended) Machine on the next
213
+ * preview request. `autostop` defaults to `'off'` — NOT `'suspend'` — on
214
+ * purpose: Fly's autostop timer counts the PROXY service's idle time, and a
215
+ * sandbox spends its first minutes being provisioned over the exec API with no
216
+ * proxy traffic, so `'suspend'` cold-starts the Machine mid-scaffold (execs are
217
+ * not proxy traffic) and the scaffold's own temp state is lost. Idle-sleep is
218
+ * instead driven by the consumer's own per-tier hibernation loop, which calls
219
+ * `sleep()`/`hibernate()` — the same provider-agnostic mechanism the Docker
220
+ * bond relies on. An operator who wants Fly-native scale-to-zero can still opt
221
+ * in with `config.autostop: 'suspend'`.
216
222
  * @returns The service definition, or `undefined`.
217
223
  */
218
224
  previewService() {
@@ -226,7 +232,7 @@ class FlyioSandboxProvider {
226
232
  { port: 443, handlers: ['tls', 'http'] },
227
233
  ],
228
234
  autostart: true,
229
- autostop: this.config.autostop ?? 'suspend',
235
+ autostop: this.config.autostop ?? 'off',
230
236
  min_machines_running: 0,
231
237
  };
232
238
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-code-sandbox-flyio",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "Fly.io Machines (Firecracker microVM) code sandbox provider",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",