@molecule/api-code-sandbox-docker 1.1.0 → 1.1.2
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 +9 -3
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +107 -6
- package/package.json +5 -5
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-
|
|
6
|
+
Generated: 2026-09-18T06:33:40.632Z
|
|
7
7
|
-->
|
|
8
8
|
|
|
9
9
|
# @molecule/api-code-sandbox-docker
|
|
@@ -236,7 +236,7 @@ export function setupCodeSandboxDocker(): void {
|
|
|
236
236
|
Peer dependencies:
|
|
237
237
|
|
|
238
238
|
- `@molecule/api-bond` ^1.0.1
|
|
239
|
-
- `@molecule/api-code-sandbox` ^1.0
|
|
239
|
+
- `@molecule/api-code-sandbox` ^1.2.0
|
|
240
240
|
- `@molecule/api-i18n` ^1.0.1
|
|
241
241
|
|
|
242
242
|
### Runtime Dependencies
|
|
@@ -260,7 +260,13 @@ on the host. The daemon is reached over a unix socket by default
|
|
|
260
260
|
remote or rootless daemon can be selected with `config.host`/`config.port` or a
|
|
261
261
|
`DOCKER_HOST` (`tcp://host:port` or `unix:///path`). Only PLAIN (unencrypted)
|
|
262
262
|
TCP is supported — front a TLS-protected daemon (2376) with a local socket
|
|
263
|
-
proxy.
|
|
263
|
+
proxy. When the plain-TCP endpoint comes from the ambient `DOCKER_HOST` env
|
|
264
|
+
(not explicit config) and `NODE_ENV=production`, the provider REFUSES to
|
|
265
|
+
connect — exec payloads would cross the network unencrypted — unless
|
|
266
|
+
`MOL_DOCKER_ALLOW_PLAIN_TCP=1` is set, which is honored with a loud warning.
|
|
267
|
+
Caller-supplied container ids are shape-validated (`[A-Za-z0-9][A-Za-z0-9_.:-]*`,
|
|
268
|
+
so Docker hex ids, names, and fly-style `app:machine` ids pass) before they are
|
|
269
|
+
interpolated into Docker API paths. The provider never pulls images, so `create()` fails with a no-such-image
|
|
264
270
|
error if the base image (default `node:22-slim`, or `config.baseImage`) is
|
|
265
271
|
absent. The isolated sandbox network is auto-created on first use; it is NOT a
|
|
266
272
|
prerequisite.
|
package/dist/index.d.ts
CHANGED
|
@@ -17,7 +17,13 @@
|
|
|
17
17
|
* remote or rootless daemon can be selected with `config.host`/`config.port` or a
|
|
18
18
|
* `DOCKER_HOST` (`tcp://host:port` or `unix:///path`). Only PLAIN (unencrypted)
|
|
19
19
|
* TCP is supported — front a TLS-protected daemon (2376) with a local socket
|
|
20
|
-
* proxy.
|
|
20
|
+
* proxy. When the plain-TCP endpoint comes from the ambient `DOCKER_HOST` env
|
|
21
|
+
* (not explicit config) and `NODE_ENV=production`, the provider REFUSES to
|
|
22
|
+
* connect — exec payloads would cross the network unencrypted — unless
|
|
23
|
+
* `MOL_DOCKER_ALLOW_PLAIN_TCP=1` is set, which is honored with a loud warning.
|
|
24
|
+
* Caller-supplied container ids are shape-validated (`[A-Za-z0-9][A-Za-z0-9_.:-]*`,
|
|
25
|
+
* so Docker hex ids, names, and fly-style `app:machine` ids pass) before they are
|
|
26
|
+
* interpolated into Docker API paths. The provider never pulls images, so `create()` fails with a no-such-image
|
|
21
27
|
* error if the base image (default `node:22-slim`, or `config.baseImage`) is
|
|
22
28
|
* absent. The isolated sandbox network is auto-created on first use; it is NOT a
|
|
23
29
|
* prerequisite.
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgFG;AAEH,cAAc,oBAAoB,CAAA;AAClC,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAE1B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAA;AAMjE;;GAEG;AACH,eAAO,MAAM,QAAQ,EAAE,eAUrB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,13 @@
|
|
|
17
17
|
* remote or rootless daemon can be selected with `config.host`/`config.port` or a
|
|
18
18
|
* `DOCKER_HOST` (`tcp://host:port` or `unix:///path`). Only PLAIN (unencrypted)
|
|
19
19
|
* TCP is supported — front a TLS-protected daemon (2376) with a local socket
|
|
20
|
-
* proxy.
|
|
20
|
+
* proxy. When the plain-TCP endpoint comes from the ambient `DOCKER_HOST` env
|
|
21
|
+
* (not explicit config) and `NODE_ENV=production`, the provider REFUSES to
|
|
22
|
+
* connect — exec payloads would cross the network unencrypted — unless
|
|
23
|
+
* `MOL_DOCKER_ALLOW_PLAIN_TCP=1` is set, which is honored with a loud warning.
|
|
24
|
+
* Caller-supplied container ids are shape-validated (`[A-Za-z0-9][A-Za-z0-9_.:-]*`,
|
|
25
|
+
* so Docker hex ids, names, and fly-style `app:machine` ids pass) before they are
|
|
26
|
+
* interpolated into Docker API paths. The provider never pulls images, so `create()` fails with a no-such-image
|
|
21
27
|
* error if the base image (default `node:22-slim`, or `config.baseImage`) is
|
|
22
28
|
* absent. The isolated sandbox network is auto-created on first use; it is NOT a
|
|
23
29
|
* prerequisite.
|
package/dist/provider.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAQH,OAAO,KAAK,EAcV,eAAe,EAOhB,MAAM,4BAA4B,CAAA;AAoBnC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAQH,OAAO,KAAK,EAcV,eAAe,EAOhB,MAAM,4BAA4B,CAAA;AAoBnC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AA4K9C;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAQ9D;AAkCD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,kBAAkB,CAAC,CAAC,EACxC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,IAAI,EAAE;IACJ,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAA;IACjC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAA;IACrC,GAAG,CAAC,EAAE;QAAE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;KAAE,CAAA;CAC1D,GACA,OAAO,CAAC,CAAC,CAAC,CA2BZ;AA07CD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,MAAM,CAAC,EAAE,YAAY,GAAG,eAAe,CAErE"}
|
package/dist/provider.js
CHANGED
|
@@ -86,20 +86,32 @@ function parseDockerHost(value) {
|
|
|
86
86
|
* 2. `config.socketPath` → that unix socket;
|
|
87
87
|
* 3. `DOCKER_HOST` env (`tcp://` or `unix://`, the docker-client convention);
|
|
88
88
|
* 4. `DOCKER_SOCKET_PATH` env, else `/var/run/docker.sock` (the default transport).
|
|
89
|
+
*
|
|
90
|
+
* The result also reports whether the endpoint came from the ambient
|
|
91
|
+
* `DOCKER_HOST` env (vs. explicit code config) — the production plain-TCP
|
|
92
|
+
* guard treats those differently; see {@link DockerSandboxProvider.ensureEndpointSafety}.
|
|
93
|
+
*
|
|
89
94
|
* @param config - the provider configuration.
|
|
90
|
-
* @returns the resolved endpoint to spread into every Docker
|
|
95
|
+
* @returns the resolved endpoint plus its source, to spread into every Docker
|
|
96
|
+
* API request.
|
|
91
97
|
*/
|
|
92
98
|
function resolveDockerEndpoint(config) {
|
|
93
99
|
if (config.host !== undefined || config.port !== undefined) {
|
|
94
|
-
return {
|
|
100
|
+
return {
|
|
101
|
+
endpoint: { host: config.host ?? '127.0.0.1', port: config.port ?? DEFAULT_DOCKER_TCP_PORT },
|
|
102
|
+
fromDockerHostEnv: false,
|
|
103
|
+
};
|
|
95
104
|
}
|
|
96
105
|
if (config.socketPath !== undefined) {
|
|
97
|
-
return { socketPath: config.socketPath };
|
|
106
|
+
return { endpoint: { socketPath: config.socketPath }, fromDockerHostEnv: false };
|
|
98
107
|
}
|
|
99
108
|
const fromEnv = parseDockerHost(process.env.DOCKER_HOST);
|
|
100
109
|
if (fromEnv)
|
|
101
|
-
return fromEnv;
|
|
102
|
-
return {
|
|
110
|
+
return { endpoint: fromEnv, fromDockerHostEnv: true };
|
|
111
|
+
return {
|
|
112
|
+
endpoint: { socketPath: process.env.DOCKER_SOCKET_PATH ?? '/var/run/docker.sock' },
|
|
113
|
+
fromDockerHostEnv: false,
|
|
114
|
+
};
|
|
103
115
|
}
|
|
104
116
|
/**
|
|
105
117
|
* Parses Docker's multiplexed binary stream from a raw socket.
|
|
@@ -300,8 +312,16 @@ class DockerSandboxProvider {
|
|
|
300
312
|
* providers (and two tests) do not inherit each other's verdict.
|
|
301
313
|
*/
|
|
302
314
|
checkpoints = { supported: true };
|
|
315
|
+
/** Resolved daemon endpoint source: ambient `DOCKER_HOST` env vs explicit config. */
|
|
316
|
+
endpointFromDockerHostEnv;
|
|
317
|
+
/** {@link ensureEndpointSafety} runs once per provider instance (successful checks only). */
|
|
318
|
+
endpointSafetyChecked = false;
|
|
319
|
+
/** Whether the plain-TCP override warning has fired (once per provider). */
|
|
320
|
+
plainTcpOverrideWarned = false;
|
|
303
321
|
constructor(config = {}) {
|
|
304
|
-
|
|
322
|
+
const resolved = resolveDockerEndpoint(config);
|
|
323
|
+
this.endpoint = resolved.endpoint;
|
|
324
|
+
this.endpointFromDockerHostEnv = resolved.fromDockerHostEnv;
|
|
305
325
|
this.baseImage = config.baseImage ?? DEFAULT_IMAGE;
|
|
306
326
|
this.labelPrefix = config.labelPrefix ?? LABEL_PREFIX;
|
|
307
327
|
this.previewUrlTemplate = config.previewUrlTemplate ?? 'http://localhost:{port}';
|
|
@@ -569,6 +589,9 @@ class DockerSandboxProvider {
|
|
|
569
589
|
*/
|
|
570
590
|
async get(id) {
|
|
571
591
|
try {
|
|
592
|
+
// Validate before interpolation into `/containers/${id}/json` — an
|
|
593
|
+
// id carrying path structure could retarget the request.
|
|
594
|
+
this.assertContainerId(id);
|
|
572
595
|
const info = (await this.dockerApi(`/containers/${id}/json`));
|
|
573
596
|
const projectId = info.Config.Labels[`${this.labelPrefix}.projectId`] ?? '';
|
|
574
597
|
// Derive the real status from Docker's container state rather than
|
|
@@ -601,6 +624,8 @@ class DockerSandboxProvider {
|
|
|
601
624
|
* @param id - The Docker container ID to destroy.
|
|
602
625
|
*/
|
|
603
626
|
async destroy(id) {
|
|
627
|
+
// Validate before interpolation into `/containers/${id}...` paths.
|
|
628
|
+
this.assertContainerId(id);
|
|
604
629
|
// Read volume name from container labels before removing
|
|
605
630
|
let volumeName;
|
|
606
631
|
try {
|
|
@@ -641,6 +666,9 @@ class DockerSandboxProvider {
|
|
|
641
666
|
* @returns A `Sandbox` with lifecycle and file system methods bound to the container.
|
|
642
667
|
*/
|
|
643
668
|
buildSandbox(containerId, _projectId, initialStatus = 'stopped') {
|
|
669
|
+
// Every method on the returned Sandbox interpolates containerId into
|
|
670
|
+
// Docker API paths (start/stop/exec) — validate once, here.
|
|
671
|
+
this.assertContainerId(containerId);
|
|
644
672
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
645
673
|
const provider = this;
|
|
646
674
|
return {
|
|
@@ -1189,7 +1217,79 @@ class DockerSandboxProvider {
|
|
|
1189
1217
|
* `X-Registry-Auth`, which the daemon requires even for anonymous pulls.
|
|
1190
1218
|
* @returns The parsed JSON response, or raw text for non-JSON responses.
|
|
1191
1219
|
*/
|
|
1220
|
+
/**
|
|
1221
|
+
* Production guard for ambient plain-TCP daemon endpoints (DOCKER_HOST).
|
|
1222
|
+
*
|
|
1223
|
+
* A `tcp://` Docker endpoint speaks PLAIN HTTP: every API request —
|
|
1224
|
+
* including exec payloads (arbitrary commands + file contents) and image
|
|
1225
|
+
* operations — crosses the network unencrypted and unauthenticated.
|
|
1226
|
+
* Reaching such a daemon because an ambient `DOCKER_HOST` env happens to
|
|
1227
|
+
* be set is not a choice this library gets to make silently in production:
|
|
1228
|
+
* it refuses unless the operator explicitly accepts the risk via
|
|
1229
|
+
* `MOL_DOCKER_ALLOW_PLAIN_TCP=1` (which is honored with a loud warning).
|
|
1230
|
+
*
|
|
1231
|
+
* Explicit `config.host`/`config.port` is deliberately NOT guarded — that
|
|
1232
|
+
* is a code-level decision by the embedding application, and dev/CI setups
|
|
1233
|
+
* rely on it. Runs lazily (first API call) rather than in the constructor,
|
|
1234
|
+
* matching the provider's env-reads-at-call-time convention.
|
|
1235
|
+
*/
|
|
1236
|
+
ensureEndpointSafety() {
|
|
1237
|
+
if (this.endpointSafetyChecked)
|
|
1238
|
+
return;
|
|
1239
|
+
if (!this.endpointFromDockerHostEnv || !('host' in this.endpoint)) {
|
|
1240
|
+
this.endpointSafetyChecked = true;
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
1244
|
+
this.endpointSafetyChecked = true;
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
const override = process.env.MOL_DOCKER_ALLOW_PLAIN_TCP;
|
|
1248
|
+
if (override === '1' || override === 'true') {
|
|
1249
|
+
if (!this.plainTcpOverrideWarned) {
|
|
1250
|
+
this.plainTcpOverrideWarned = true;
|
|
1251
|
+
logger.warn('DOCKER_HOST selects a PLAIN-TCP Docker endpoint while NODE_ENV=production — ' +
|
|
1252
|
+
'container control traffic (incl. exec payloads) crosses the network unencrypted. ' +
|
|
1253
|
+
'Accepted only because MOL_DOCKER_ALLOW_PLAIN_TCP is set; prefer a unix socket ' +
|
|
1254
|
+
'or a TLS-terminating proxy.');
|
|
1255
|
+
}
|
|
1256
|
+
this.endpointSafetyChecked = true;
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
// Refuse WITHOUT memoizing: an earlier refusal can land inside a
|
|
1260
|
+
// best-effort try/catch (e.g. network ensure) and be swallowed — every
|
|
1261
|
+
// subsequent call must keep refusing until the configuration changes.
|
|
1262
|
+
throw new Error('DOCKER_HOST selects a plain-TCP (unencrypted, unauthenticated) Docker endpoint and ' +
|
|
1263
|
+
'NODE_ENV=production — refusing. Set MOL_DOCKER_ALLOW_PLAIN_TCP=1 to explicitly accept ' +
|
|
1264
|
+
'the risk, or point the provider at a unix socket / TLS-terminating proxy.');
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Shape a caller-supplied container id must have before it is interpolated
|
|
1268
|
+
* into a Docker API path. Docker's own ids are 64-char hex and container
|
|
1269
|
+
* names match `[a-zA-Z0-9][a-zA-Z0-9_.-]*`; the E2B-style provider uses
|
|
1270
|
+
* `app:machine` ids, so `:` is allowed too. Anything carrying URL/path
|
|
1271
|
+
* structure (`/`, `?`, `#`), percent-encoding, or whitespace is rejected —
|
|
1272
|
+
* interpolated raw, such a value could retarget the request path (e.g.
|
|
1273
|
+
* `../exec` or `?force=true` on a different resource).
|
|
1274
|
+
*/
|
|
1275
|
+
static CONTAINER_ID_SHAPE = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/;
|
|
1276
|
+
/**
|
|
1277
|
+
* Validates a container id before any Docker API path interpolation.
|
|
1278
|
+
* @param id - the caller-supplied container id.
|
|
1279
|
+
* @returns the validated id, unchanged.
|
|
1280
|
+
* @throws {Error} when the id does not match {@link DockerSandboxProvider.CONTAINER_ID_SHAPE}.
|
|
1281
|
+
*/
|
|
1282
|
+
assertContainerId(id) {
|
|
1283
|
+
if (!DockerSandboxProvider.CONTAINER_ID_SHAPE.test(id)) {
|
|
1284
|
+
throw new Error(`Invalid sandbox container id: ${JSON.stringify(id)}`);
|
|
1285
|
+
}
|
|
1286
|
+
return id;
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Performs a JSON Docker Engine API request against the resolved endpoint.
|
|
1290
|
+
*/
|
|
1192
1291
|
async dockerApi(path, method = 'GET', body, timeoutMs = 30_000, headers) {
|
|
1292
|
+
this.ensureEndpointSafety();
|
|
1193
1293
|
return new Promise((resolve, reject) => {
|
|
1194
1294
|
const opts = {
|
|
1195
1295
|
...this.endpoint,
|
|
@@ -1328,6 +1428,7 @@ class DockerSandboxProvider {
|
|
|
1328
1428
|
* @returns The raw response buffer.
|
|
1329
1429
|
*/
|
|
1330
1430
|
async dockerApiRaw(path, method = 'GET', body) {
|
|
1431
|
+
this.ensureEndpointSafety();
|
|
1331
1432
|
return new Promise((resolve, reject) => {
|
|
1332
1433
|
const opts = {
|
|
1333
1434
|
...this.endpoint,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@molecule/api-code-sandbox-docker",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Docker-based code sandbox container provider",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -29,11 +29,11 @@
|
|
|
29
29
|
],
|
|
30
30
|
"license": "Apache-2.0",
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@molecule/api-bond": "1.0.
|
|
33
|
-
"@molecule/api-code-sandbox": "1.2.
|
|
32
|
+
"@molecule/api-bond": "1.0.2",
|
|
33
|
+
"@molecule/api-code-sandbox": "1.2.1",
|
|
34
34
|
"@types/node": "26.1.2",
|
|
35
35
|
"typescript": "6.0.3",
|
|
36
|
-
"vitest": "4.1.
|
|
36
|
+
"vitest": "4.1.11"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@molecule/api-bond": "^1.0.1",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"url": "https://github.com/molecule-dev/molecule.git",
|
|
51
51
|
"directory": "packages/api/bonds/code-sandbox/docker"
|
|
52
52
|
},
|
|
53
|
-
"homepage": "https://
|
|
53
|
+
"homepage": "https://www.molecule.dev/packages/api-code-sandbox-docker",
|
|
54
54
|
"bugs": "https://github.com/molecule-dev/molecule/issues",
|
|
55
55
|
"publishConfig": {
|
|
56
56
|
"access": "public"
|