@emulates/docker 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,447 @@
1
- # Temporary Holding Version
1
+ # @emulates/docker
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > Part of [Emulates](https://github.com/crvouga/emulators): high-fidelity, in-process emulators for APIs and databases.
4
+
5
+ Work-in-progress Docker Engine API 1.52 emulator. It implements GET/HEAD `/_ping`,
6
+ GET `/version`, `/info`, `/containers/json`, and `/containers/{id}/json`, plus
7
+ the shared Emulates runtime controls. POST `/containers/create` persists a stopped
8
+ container. Start, wait, stop, kill, and removal use explicit simulated completion.
9
+ The Node entry supports non-TTY attach and scripted duplex streams. The contract is
10
+ pinned in [API_EVIDENCE.md](API_EVIDENCE.md); [SUPPORT.md](SUPPORT.md) lists operations.
11
+
12
+ ## Install
13
+
14
+ ```sh
15
+ bun add @emulates/docker
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ```ts
21
+ import { createRuntime } from "@emulates/docker"
22
+
23
+ const docker = createRuntime({ seed: 42 })
24
+ const response = await docker.fetch(new Request("http://docker.mock/_ping"))
25
+ console.log(await response.text()) // OK
26
+ ```
27
+
28
+ Use `DockerAPI` for the provider-only Fetch surface, or `createRuntime` for health,
29
+ admin controls, namespaces, faults, metrics, and the request journal. Neither entry
30
+ imports a Node server. The Node entry provides `createServer()` with an ephemeral
31
+ loopback port by default; close it with `await server.close()` after a test.
32
+
33
+ ```sh
34
+ emulates-docker serve --port 8826
35
+ ```
36
+
37
+ HTTP clients can target `http://127.0.0.1:8826`. A Docker client using `DOCKER_HOST`
38
+ can select `tcp://127.0.0.1:8826`. Unversioned and `/v1.52` provider routes work.
39
+ Versions above 1.52 or below the simulated Engine minimum 1.44 receive provider
40
+ 400 errors (plain text below 1.24, JSON otherwise). Versions 1.44–1.51 receive an
41
+ explicit emulator-only 501: their wire formats are not implemented. The emulator reports
42
+ a synthetic Engine 29.1.0 identity; this is not a required local Docker version.
43
+ Selected API 1.52 scenarios passed against Engine 29.8.0; full client and Engine
44
+ compatibility are not claimed. See [verification boundaries](#verification-boundaries).
45
+
46
+ ## Node HTTP example
47
+
48
+ Run this with Node against the built package. The server uses an ephemeral
49
+ loopback port and synthetic state; no Docker installation is needed.
50
+
51
+ ```ts
52
+ import { createServer } from "@emulates/docker/server"
53
+
54
+ const server = await createServer({ seed: 42 })
55
+ try {
56
+ const response = await fetch(`${server.url}/v1.52/version`)
57
+ if (!response.ok) throw new Error(`Docker mock returned ${response.status}`)
58
+ console.log(await response.json())
59
+ } finally {
60
+ await server.close()
61
+ }
62
+ ```
63
+
64
+ For Unix sockets, pass an absolute, absent `socketPath` in your test directory
65
+ and use Node HTTP requests as described below. The CLI uses the shared TCP
66
+ adapter; use `createServer` for Docker's attach and Unix-socket transport.
67
+
68
+ ## Synthetic observations
69
+
70
+ `POST /__admin/docker/seed` atomically adds images and containers in the selected
71
+ namespace. It never downloads an image or starts a process. For example:
72
+
73
+ ```ts
74
+ import { createRuntime } from "@emulates/docker"
75
+
76
+ const docker = createRuntime({ seed: 42 })
77
+ await docker.fetch(new Request("http://docker.mock/__admin/docker/seed", {
78
+ method: "POST",
79
+ headers: { "content-type": "application/json" },
80
+ body: JSON.stringify({
81
+ images: [{ id: `sha256:${"a".repeat(64)}`, tags: ["synthetic:latest"] }],
82
+ containers: [{
83
+ id: "b".repeat(64), name: "worker", image: "synthetic:latest",
84
+ status: "running", labels: { suite: "example" }, cmd: ["synthetic-worker"],
85
+ }],
86
+ daemon: { rootless: true },
87
+ }),
88
+ }))
89
+ ```
90
+
91
+ Image IDs are immutable SHA-256 strings; container IDs are separate 64-character
92
+ lowercase hexadecimal strings. Container `image` resolves an existing image ID
93
+ or exact tag. Duplicate IDs, names and image tags conflict; unknown images return
94
+ 404. Invalid seed fields return 400 and roll back the entire seed. Container
95
+ fields also accept `exitCode`, `entrypoint`, `env`, `workingDir`, `user`,
96
+ `hostConfig`, `networkSettings`, `sizeRw`, and `sizeRootFs`. Status is one of
97
+ `created`, `running`, `paused`, `restarting`, `removing`, `exited`, or `dead`.
98
+ Timestamps use the emulator clock; reported PIDs are zero, never host processes.
99
+
100
+ `GET /__admin/docker/daemon` reports `{ available, rootless, simulated: true }`.
101
+ `POST` to the same route changes either boolean. `available: false` drops provider
102
+ requests while shared health/admin routes remain accessible and container state
103
+ is retained. Rootless, security, OS, build, size, and resource metadata are
104
+ synthetic observations, not host attestation or enforcement.
105
+
106
+ List defaults to running containers, including paused/restarting ones. `all`,
107
+ positive `limit`, or a `status` filter includes stopped containers. `limit` selects
108
+ the newest results; `size` includes the seeded byte counts. Like the pinned
109
+ Engine's boolean parser, empty/0/no/false/none (case-insensitive) mean false and
110
+ other values mean true. Inspect resolves full IDs, unique prefixes, and names.
111
+
112
+ `filters` accepts JSON string arrays or boolean-key sets for `id`, `name`,
113
+ `status`, `label`, and `exited`. Categories combine with AND; values within a
114
+ category combine with OR, except labels which all must match. Name matching
115
+ supports literals, dots, anchors, and at most one `.*`; other regex constructs
116
+ and other filter categories return explicit emulator-only 501 errors. Malformed
117
+ filter shapes, statuses and exit codes return 400. Seeded state and daemon
118
+ settings participate in shared reset and Timeline checkpoints.
119
+
120
+ ## Container creation
121
+
122
+ Seed images before calling `POST /containers/create`. Image seeds may include
123
+ `platform` (default `linux/amd64`) and `config` containing supported image defaults.
124
+ Creation resolves image IDs or exact tags, adding `:latest` to an untagged reference.
125
+ It never pulls or executes an image. Missing images and requested platform
126
+ mismatches return 404; an implicit host-platform mismatch produces a warning.
127
+
128
+ The supported body fields are `Image`, `Cmd`, `Entrypoint`, `Env`, `Labels`,
129
+ `WorkingDir`, `User`, `Hostname`, `Domainname`, `AttachStdin`, `AttachStdout`,
130
+ `AttachStderr`, `OpenStdin`, `StdinOnce`, `Tty`, `NetworkDisabled`, `StopSignal`,
131
+ `StopTimeout`, `HostConfig`, and `NetworkingConfig`. Unsupported fields return
132
+ explicit emulator-only 501 errors. Bad field types, relative working directories,
133
+ invalid stop signals, invalid names, and missing commands return 400.
134
+ Image defaults supply commands/entrypoints, environment, labels and selected
135
+ strings. Request environment keys and labels take precedence. `Entrypoint: [""]`
136
+ clears the image entrypoint; provide a replacement command when doing so.
137
+
138
+ Use `?name=...` for a stable name. Conflicting names return 409, including concurrent
139
+ creation requests. Omitted names use `emulators_<id-prefix>`. Responses contain
140
+ `Id` and `Warnings`; IDs are deterministic synthetic 64-character hex strings,
141
+ immutable within stored records and restored with the shared ID sequence by
142
+ Timeline checkout. New records inspect as `created` with `Running: false`.
143
+
144
+ Supported `HostConfig` metadata includes `NetworkMode`, `IpcMode`, `PidMode`,
145
+ `CgroupnsMode`, `Runtime`, `AutoRemove`, `ReadonlyRootfs`, `Privileged`, `Init`,
146
+ `Memory`, `MemorySwap`, `NanoCpus`, `CpuShares`, `PidsLimit`, `Binds`, `CapDrop`,
147
+ `CapAdd`, `SecurityOpt`, `Dns`, `ExtraHosts`, `Mounts`, `Tmpfs`, `PortBindings`,
148
+ `RestartPolicy`, and `LogConfig`. `NetworkingConfig.EndpointsConfig` is retained
149
+ under inspected `NetworkSettings.Networks`. These are declared configuration
150
+ observations, not enforced resources, mounts, security controls or network setup.
151
+ Nested host metadata is retained as supplied; full daemon-specific resource and
152
+ network validation is not modelled. No host isolation claim follows from it.
153
+
154
+ ## Start, wait and scripted completion
155
+
156
+ `POST /containers/<id>/start` marks a created/exited container running (204),
157
+ returns 304 when already running/restarting, and returns 409 for paused, dead,
158
+ or removing records. No image is executed. The clock supplies `StartedAt`;
159
+ restart clears the exit code but retains the prior `FinishedAt` until completion.
160
+ Checkpoint, checkpoint-dir and detachKeys options return explicit emulator-only 501.
161
+ As in the pinned Engine route, bodies longer than seven bytes and chunked bodies
162
+ return 400; use an empty body.
163
+
164
+ `POST /containers/<id>/wait?condition=...` accepts `not-running` (also the default
165
+ for omitted/empty values), `next-exit`, or `removed`. Headers arrive immediately;
166
+ the JSON body `{ "StatusCode": 7 }` arrives only when the condition holds.
167
+ Not-running returns immediately for stopped/created containers. Next-exit waits
168
+ for a future exit even if already stopped. Removed remains pending after an
169
+ ordinary exit. Invalid conditions return 400 and unknown containers return 404
170
+ before opening the stream.
171
+
172
+ Use `POST /__admin/docker/containers/<id>/complete` with `{ "exitCode": 7 }` to
173
+ explicitly finish a running synthetic container. Completion persists `exited`,
174
+ `FinishedAt`, and the supplied integer code, wakes relevant waiters, and captures
175
+ a shared Timeline checkpoint. If `HostConfig.AutoRemove` is true, completion also
176
+ removes the synthetic record and satisfies removed waits. No host resource is
177
+ touched. Repeated completion while stopped returns 409. These controls operate on
178
+ the selected namespace's main branch; provider operations can use shared branches.
179
+
180
+ `GET /__admin/docker/waits` reports the namespace's pending wait and termination-reply handles.
181
+ Aborting the request or canceling its response body releases its wait handle.
182
+ Reset cancels waits in the reset namespace; `runtime.close()`, `DockerAPI.close()`
183
+ and `createServer().close()` cancel owned waits. Canceled bodies reject rather
184
+ than fabricate an exit code. Wait handles are transient and never serialized.
185
+ Checkout and snapshot restore cancel handles owned by the restored instance before
186
+ replacing its state; subsequent completion cannot satisfy an old wait.
187
+ Generated parity excludes blocking waits; deterministic Fetch and real HTTP tests
188
+ cover their completion/cancellation behavior. The real Engine oracle separately
189
+ checks selected wait and termination outcomes; cancellation coverage is synthetic.
190
+
191
+ ## Stop, signals and removal
192
+
193
+ `POST /containers/<id>/stop` records an accepted stop request without changing
194
+ `Running`. The HTTP request remains pending until the explicit completion control
195
+ finishes execution, then returns 204. Already-stopped containers return 304.
196
+ `signal` selects the requested signal (default Config.StopSignal or TERM); `t`
197
+ selects the timeout (default Config.StopTimeout or 10 seconds; negative means no
198
+ escalation timeout). The emulator records these parameters for scenario inspection.
199
+ It does not schedule real timers or automatically declare exit when a timeout
200
+ expires: script graceful completion or forced completion, including the exit code,
201
+ through `/__admin/docker/containers/<id>/complete`. This permits controlled delayed
202
+ termination without fabricating an early Docker success response.
203
+
204
+ `POST /containers/<id>/kill` defaults to SIGKILL and keeps its 204 reply pending
205
+ until completion; explicit KILL/9 behaves identically. Other supported Linux
206
+ signal names/numbers acknowledge delivery with 204 while preserving execution
207
+ state. The fixture decides the subsequent process response. No host signal is
208
+ sent. Killing a stopped container returns 409. Invalid kill signals return 400;
209
+ pinned Engine stop signal errors and malformed `t` return 500. Timeouts outside
210
+ JavaScript's safe integer range return explicit emulator-only 501.
211
+
212
+ `DELETE /containers/<id>` removes a stopped record and releases its name (204).
213
+ Running or paused records require `force=true`; otherwise removal returns 409.
214
+ Forced removal records intent and waits for explicit completion, then removes the
215
+ record and wakes both exit and removed waiters. A second removal while forced
216
+ removal is pending returns 409. Seed status `removing` to model an existing removal
217
+ conflict. Missing records return 404. `v` is accepted but no host volumes exist;
218
+ `link=true` is outside this subset and returns emulator-only 501.
219
+
220
+ `GET /__admin/docker/containers/<id>/termination` returns the last accepted request
221
+ (operation, numeric signal, optional timeout and request time), `removalPending`,
222
+ and `simulated: true`. These diagnostic fields are not Docker wire fields.
223
+ Canceling a stop/kill/removal reply releases only the reply handle. Accepted intent
224
+ and running state survive socket loss; explicit completion still applies, including
225
+ pending forced removal. Reset clears the namespace and cancels its pending replies.
226
+ Closing a runtime/server cancels replies without claiming exit. Accepted-mutation
227
+ history survives response loss through shared Timeline checkpoints, as described
228
+ under failure scenarios below.
229
+
230
+ ## Controls
231
+
232
+ - `GET /__admin/health` reports readiness and service identity.
233
+ - Select independent state with `x-emulates-namespace` or `/__admin/ns/<name>/…`.
234
+ - `POST /__admin/reset` resets the selected namespace's records and Timeline;
235
+ `?all=1` resets all namespaces. It preserves clock, fault configuration and journal.
236
+ - `POST /__admin/clock` accepts shared `set`, `advance`, and `freeze` controls.
237
+ - `POST /__admin/faults` configures faults by operation, path, or method.
238
+ `DELETE /__admin/faults` clears them. Docker fault presets are described below.
239
+ - `GET /__admin/requests` exposes metadata, never request bodies or query values;
240
+ `DELETE /__admin/requests` clears the selected journal.
241
+ - `POST /__admin/checkpoints`, `POST /__admin/branches/<name>`, and
242
+ `POST /__admin/branches/<name>/checkout` use the shared Timeline coordinator.
243
+ There is no Docker-local history manager. Pass `{ "checkpoint": "cp_…" }` to checkout.
244
+
245
+ The shared `adminKey` option gates admin routes. Docker credential-based namespace
246
+ selection, webhooks and provider credentials are not configured.
247
+
248
+ ## API
249
+
250
+ The portable `@emulates/docker` entry exports:
251
+
252
+ - `DockerAPI`: provider Fetch handler with `fetch`, `reset`, and `close`.
253
+ - `DOCKER_NAMESPACE`: default storage namespace (`docker`).
254
+ - `createRuntime`: provider plus standard Emulates controls and Timeline.
255
+ - `document`: annotated OpenAPI contract.
256
+ - `operationIds`: all inventoried operation IDs, including unsupported routes.
257
+ - `supportedOperationIds`: currently implemented operation IDs.
258
+
259
+ The Node-only `@emulates/docker/server` entry exports:
260
+
261
+ - `createServer`: HTTP server with `runtime`, `url`, `port`, `host`, `server`,
262
+ `close`, `attachments()`, and optional Unix `socketPath`.
263
+ - `DEFAULT_PORT`: CLI default port, 8826 (programmatic default is ephemeral).
264
+ - `serveTarget`: shared CLI server configuration.
265
+
266
+ Type exports include `DockerAPIOptions`, `DockerRuntime`, `DockerRuntimeOptions`,
267
+ `OperationId`, `SupportedOperationId`, and the Node entry's `DockerServer` and
268
+ `DockerServerOptions`, `AttachStreamOptions`, and `DockerAttachment`. The executable `emulates-docker` provides `serve`.
269
+
270
+ ## Deliberately not modelled
271
+
272
+ Fetch attach and unsupported options return Emulates-specific 501 JSON errors;
273
+ unknown routes return 404. The emulator never starts real containers or executes
274
+ commands. The Node server supports the documented non-TTY attach handshake and
275
+ scripted duplex streams; a Fetch response cannot represent that upgrade.
276
+
277
+ Image builds/pulls, registry access, exec, TTY streams, log replay, real networks,
278
+ mounts, volumes, resource enforcement, peer credentials and host isolation are
279
+ outside this package. Reported rootless/security settings and HostConfig fields
280
+ are synthetic metadata. Logical restart controls do not reproduce real daemon
281
+ restart or live-restore. No Initiative orchestration or recovery policy is verified.
282
+
283
+ ## Failure scenarios and logical restart
284
+
285
+ For each operation `create`, `start`, `stop`, `kill`, and `remove`, install a
286
+ one-use preset through `POST /__admin/faults` with
287
+ `{"preset":"docker_<operation>_pre_failure"}` or
288
+ `{"preset":"docker_<operation>_accepted_drop"}`. The first returns 503 before
289
+ mutation. The second lets validation and mutation run, captures the accepted
290
+ state in shared Timeline, then drops the reply. Failed validation and unchanged
291
+ operations do not become accepted mutations. Inspect state after a lost reply;
292
+ acceptance alone does not establish container exit or removal.
293
+
294
+ The shared journal records lost replies with status 0, `accepted: true`, the
295
+ acceptance `checkpoint`, container ID, and fault ID. Ordinary rejected operations
296
+ do not gain an acceptance checkpoint. Pending termination intent is checkpointed
297
+ before waiting for completion, including when the caller later disconnects.
298
+
299
+ `POST /__admin/docker/restart` requires an explicit `containers` choice:
300
+ `"preserve"` retains execution state and accepted intent; `"terminate"` completes
301
+ running containers with `exitCode` (default 137), applying pending removal and
302
+ AutoRemove. Both cancel existing wait/reply handles, set daemon availability to
303
+ true, and checkpoint the selected namespace's main branch. Invalid input changes
304
+ nothing. Daemon availability changes also checkpoint independently of execution.
305
+ These are synthetic scenario controls, not claims about a real daemon's restart
306
+ policy or live-restore configuration. No daemon or host process is restarted.
307
+
308
+ ## Node Unix-socket transport
309
+
310
+ The programmatic Node entry supports `createServer({ socketPath })` on Unix.
311
+ Supply an absolute, absent path (at most 103 UTF-8 bytes) in a test-owned directory.
312
+ It refuses existing files, symlinks and sockets, including stale sockets; it never
313
+ unlinks them to make room. Do not use a host Engine path. The operating system and
314
+ Node remove the bound socket when `await server.close()` completes. Close also
315
+ cancels runtime waits and destroys owned connections; repeated close calls share
316
+ one shutdown operation. Tests must close their server in a finally block.
317
+
318
+ Use Node HTTP `request({ socketPath, path: "/_ping" })` for Unix requests. The
319
+ returned `socketPath` identifies the endpoint; `url` is the synthetic HTTP origin
320
+ `http://docker.mock` and `port` is 0. For TCP, omit `socketPath` and use `host`/`port`
321
+ as before. Combining Unix and TCP options rejects. Retained sequential HTTP/1.1
322
+ requests work on both transports. Each connection permits one active request;
323
+ pipelining another request before its response finishes closes that connection
324
+ before dispatching the extra request. A deliberate drop closes the connection; the
325
+ transport does not reconnect clients.
326
+
327
+ `createServer` bounds bodies to `maxBodyBytes` (default 1 MiB, including chunked
328
+ input), with a `bodyTimeoutMs` receive deadline (default 30 seconds). Rejections
329
+ return transport-specific 413 or 408 and close the connection without invoking a
330
+ provider operation. Response waits have no artificial execution deadline. Header
331
+ size is limited to 16 KiB, header/request receive time to 30 seconds, idle
332
+ keep-alive to 5 seconds, and simultaneous connections to `maxConnections` (default
333
+ 128). These limits are Emulates controls, not Docker Engine parity claims.
334
+ The existing CLI/shared fleet target still uses the shared TCP adapter; Unix
335
+ sockets and these Docker transport limits currently require `createServer`.
336
+ No peer credentials, procfs provenance, host isolation or real Engine access is
337
+ claimed. Attach framing and lifetime controls are described below.
338
+
339
+ ## Node attach handshake
340
+
341
+ Send POST `/v1.52/containers/<id>/attach?stream=1&stdout=1&stderr=1` with
342
+ `Connection: Upgrade` and `Upgrade: tcp` to `createServer`. Unversioned attach
343
+ uses the same pinned contract. The response is `101 UPGRADED` with
344
+ `Content-Type: application/vnd.docker.multiplexed-stream`, `Connection: Upgrade`,
345
+ and `Upgrade: tcp`. Read through the first CRLFCRLF and retain all following
346
+ bytes. This duplex upgrade cannot be represented by a Fetch Response; ordinary
347
+ Fetch and non-upgrade requests continue to return an explicit 501.
348
+
349
+ `stream=true`, `logs=false`, and stdin/stdout/stderr selections are accepted for
350
+ handshake negotiation. TTY, replay, detachKeys, unknown query modes and non-TCP
351
+ upgrades are unsupported. Missing containers and paused/restarting conflicts use
352
+ the pinned pre-upgrade plain-text error envelope with raw-stream content type.
353
+ Shared namespaces, branches, faults, daemon availability and request logging apply.
354
+ Attach admission is journaled as 101 and does not itself checkpoint a mutation.
355
+
356
+ For protocol fixtures, `attachHandshake: { chunkBytes, initialStreamBytes }`
357
+ splits header writes into 1–4096-byte chunks and appends at most 1 MiB of synthetic
358
+ already-framed bytes to the last fragment. This does not promise OS packet
359
+ boundaries. The bytes are caller-owned wire fixtures, not generated container
360
+ output. Use the live attachment controls below for framing, channel routing and scripted
361
+ stdin. No command interprets the input, and payload bytes never enter the journal. Connection loss does not stop the container.
362
+ Owned sockets close on server shutdown. The emulator never connects to a real Engine;
363
+ the separately invoked oracle performs the authorized provider comparison.
364
+
365
+ ## Scripted attach streams
366
+
367
+ `server.attachments()` returns current Node-owned handles. Each exposes an `id`,
368
+ `containerId`, public `namespace` and `branch`, plus `closed`, `stdinClosed` and
369
+ `queuedBytes`. Retain a handle only for that attachment; it cannot address a
370
+ restored container or a new execution after its lifetime ends.
371
+
372
+ ```ts
373
+ import type { DockerServer } from "@emulates/docker/server"
374
+
375
+ // Call after a client attaches to a running container on this server.
376
+ export async function writeAttachedOutput(server: DockerServer) {
377
+ const [attachment] = server.attachments()
378
+ if (attachment) {
379
+ await attachment.write("stdout", new TextEncoder().encode("synthetic output"))
380
+ const rawInput = attachment.takeStdin()
381
+ await attachment.end()
382
+ return rawInput
383
+ }
384
+ }
385
+ ```
386
+
387
+ `write("stdout" | "stderr", bytes)` emits the selected channel as an eight-byte
388
+ Docker header (channel 1/2, three reserved zero bytes, uint32 big-endian length)
389
+ followed by the exact payload. Empty frames work; concurrent writes are serialized
390
+ without frame interleaving. Unselected output is ignored. Await each write for
391
+ backpressure. `attachStreams.frameChunkBytes` splits frames for protocol tests
392
+ (default 64 KiB); OS packet boundaries remain uncontrolled.
393
+
394
+ Incoming stdin is raw, including bytes read ahead with the HTTP upgrade. Input
395
+ requires `stdin=1` and container `OpenStdin=true`; otherwise it is ignored.
396
+ `takeStdin()` drains the bounded synthetic input buffer. No program is executed.
397
+ With `StdinOnce=true`, input EOF closes modeled container stdin and checkpoints
398
+ that state without declaring exit; output may continue until explicit completion.
399
+ Without StdinOnce, effective stdin EOF ends that attachment and leaves container
400
+ input reusable. Starting a new execution reopens modeled input. Transport loss
401
+ alone never invokes container completion.
402
+
403
+ `end()` scripts output EOF after queued frames. For non-TTY StdinOnce containers,
404
+ it retains the connection until explicit process completion, matching the pinned
405
+ handler's wait. `cancel()` immediately disconnects just this attachment. Process
406
+ completion ends its attachments after queued output. Reset, snapshot restore,
407
+ checkout, synthetic restart and shutdown immediately invalidate the affected
408
+ physical instance's handles; other namespaces/branches remain isolated. Queued
409
+ stale writes reject. Socket handles and buffered payloads are never snapshotted.
410
+
411
+ `attachStreams.maxQueuedBytes` and `maxStdinBytes` default to 1 MiB. Queued output
412
+ includes frame headers; exceeding the output limit rejects the write. Exceeding
413
+ unread stdin capacity cancels that attachment. Stream limits must be positive
414
+ integers no greater than 16 MiB. These bounds are emulator resource controls.
415
+
416
+ ## Verification boundaries
417
+
418
+ `bun run build && bun test` exercises the modeled contract without an Engine or
419
+ credentials. Generated OpenAPI self-parity asserts that all eight eligible
420
+ operations are planned and exercised: list, create, inspect, start, info, version,
421
+ GET ping and HEAD ping. Generated walks include error paths; an exercised count
422
+ alone does not establish successful creation or state transitions. A seeded
423
+ nonempty-list comparison additionally rejects a deliberately wrong, schema-valid
424
+ execution state. The independent wire scenarios cover successful mutations.
425
+
426
+ `test/node-consumer.mjs` runs under native Node against the built server entry,
427
+ once over loopback TCP and once over a test-owned Unix socket. It uses Node HTTP
428
+ requests on a retained connection and raw attach bytes. It verifies lost create
429
+ and start responses, lookup by name, duplicate-name conflict, framed binary
430
+ stdout/stderr, raw stdin, continued execution after attachment loss, and lost
431
+ stop/kill/remove responses followed by completion and re-inspection. Setup and
432
+ completion use public emulator admin controls; attachment handles only script output
433
+ and observe synthetic stdin. Public request metadata confirms each dropped mutation
434
+ was accepted and checkpointed. No client assertion imports provider handlers or
435
+ internal state. The fixture never automatically retries a mutation.
436
+
437
+ Wait/stop/kill/remove have deterministic lifecycle and wire tests rather than
438
+ unbounded generated walks. Attach uses dedicated raw-wire tests because Fetch
439
+ cannot represent duplex HTTP upgrade. The declared Docker consumer is raw-socket,
440
+ so no SDK compatibility is claimed or substituted for wire coverage. Self-parity
441
+ and synthetic consumer tests are separate from real Engine differential evidence
442
+ recorded by the oracle, and do not verify Initiative recovery policies or host enforcement.
443
+
444
+ The opt-in [real Engine oracle](ORACLE.md) requires an explicitly authorized
445
+ current Engine endpoint and immutable image. The recorded Engine 29.8.0 run
446
+ passed 29 selected API 1.52 comparisons; see the evidence record for scope and
447
+ limitations. Engine identity fields and full API 1.56 compatibility are not claimed.
package/SUPPORT.md ADDED
@@ -0,0 +1,23 @@
1
+ # Docker Engine (Emulates) — operation support
2
+
3
+ Generated from `openapi.yaml`; do not edit by hand.
4
+
5
+ - operations in spec: **13**
6
+ - supported by the emulator: **12**
7
+ - parity enabled: **8**
8
+
9
+ | operationId | route | emulator | parity | notes |
10
+ | --- | --- | --- | --- | --- |
11
+ | `ContainerList` | `GET /containers/json` | ✅ supported | ✅ | |
12
+ | `ContainerCreate` | `POST /containers/create` | ✅ supported | ⚠️ unsafe (opt-in) | |
13
+ | `ContainerInspect` | `GET /containers/{id}/json` | ✅ supported | ✅ | |
14
+ | `ContainerStart` | `POST /containers/{id}/start` | ✅ supported | ⚠️ unsafe (opt-in) | |
15
+ | `ContainerStop` | `POST /containers/{id}/stop` | ✅ supported | ❌ disabled | Explicit completion controls and pending responses require deterministic termination tests, not unbounded generated walks. |
16
+ | `ContainerKill` | `POST /containers/{id}/kill` | ✅ supported | ❌ disabled | Explicit completion controls and pending responses require deterministic termination tests, not unbounded generated walks. |
17
+ | `ContainerAttach` | `POST /containers/{id}/attach` | ❌ unsupported | — | Unsupported through Fetch. The Node server supports non-TTY streaming attach, verified by raw-wire tests and selected real Engine comparisons. |
18
+ | `ContainerWait` | `POST /containers/{id}/wait` | ✅ supported | ❌ disabled | Requires deterministic completion and cancellation; verified by lifecycle tests instead of unbounded generated waits. |
19
+ | `ContainerDelete` | `DELETE /containers/{id}` | ✅ supported | ❌ disabled | Explicit completion controls and pending responses require deterministic termination tests, not unbounded generated walks. |
20
+ | `SystemInfo` | `GET /info` | ✅ supported | ✅ | |
21
+ | `SystemVersion` | `GET /version` | ✅ supported | ✅ | |
22
+ | `SystemPing` | `GET /_ping` | ✅ supported | ✅ | |
23
+ | `SystemPingHead` | `HEAD /_ping` | ✅ supported | ✅ | |