Resolve the deferred docker system events subscription question. docker/system/events is now a v1 Subscription operation using the same StreamingHandler pattern already wired for logs, exec, and image/pull. The internal ownership-store subscription for stale-entry cleanup on destroy events is a follow-up refinement. Scrub hedging language from ADR-060 and docker specs: - Remove 'marginal gain' / 'future feature is additive' framing - Replace 'no reaper' / 'not promptly cleaned up' with clean statement that events subscription provides the prompt-cleanup path - Remove stale-entry policy from ADR-060's two-way door classification
12 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-07-08 |
alknet-docker
The docker operations crate for the ALPN-as-service architecture: a
thin, single-host bollard wrapper that exposes docker container and
image operations as call-protocol operations on the shared
alknet/call ALPN, plus (behind a tty feature) a DockerTtyBackend
implementing alknet-tty's TtyBackend trait for interactive terminal
sessions into containers.
What
alknet-docker does two things:
-
Call operations. A set of
OperationSpec-registered operations (docker/container/*,docker/image/*,docker/system/events) on the sharedalknet/callALPN, mapping bollard's docker API to the call protocol'sQuery/Mutation/Subscriptiondispatch paths. Lifecycle (create/start/stop/remove/list/inspect) isQuery/Mutation; logs, non-interactive exec, image pull, and system events areSubscription(streaming viaStreamingHandler, ADR-049). The operations declareAccessControlagainst the ADR-050 container-as-resource model. Decided in ADR-058. -
DockerTtyBackend(behind thettyfeature). Animpl TtyBackend(ADR-053) wrappingbollard::attach_container()andbollard::exec::start_execwithtty: true, for interactive terminal sessions into containers overalknet/tty. This is theTtyBackendrow the alknet-tty spec left open ("future, out of scope here"). Decided in ADR-061.
The two use cases the crate serves (per the user's brief and the system docs):
- Disposable dev containers (the common case by volume) — a
coordinator spawns a container for an implementation agent or an
isolated env. alknet-docker's
createrecords ownership (OwnershipStore::record); the coordinator owns the container for its lifetime;removerevokes. Interactive sessions into the container go throughDockerTtyBackendonalknet/tty. - Long-running hosted services (the production server case) —
rarely-changing services (reverse-proxy, postgres, redis, gitea on
dev1, per
/workspace/system/dev1/docker.md) created by an operator viadocker compose, not via alknet. alknet-docker's operations (start/stop/inspect/logs) manage them; the operator role reaches them via the static-resource fallback (ADR-060 §3).
Documents
| Document | Status | Description |
|---|---|---|
| overview.md | draft | Crate purpose, two-role design (call ops + DockerTtyBackend), dependencies, ALPN, label namespace, feature gates, assembly-layer wiring |
| docker-operations.md | draft | The operation surface: lifecycle (Query/Mutation), logs/exec/pull (Subscription via StreamingHandler), access control, label namespace, teardown coupling |
| docker-tty-backend.md | draft | DockerTtyBackend (impl TtyBackend): attach vs exec mode, TtyHandle field mapping, TtyControl → bollard resize/signal, exit_code Drop-kill (ADR-056) |
Applicable ADRs
| ADR | Title | Relevance |
|---|---|---|
| 058 | alknet-docker Registers on alknet/call |
Docker ops are call-protocol operations, not a separate ALPN; raw-carriage dissolved by alknet-tty extraction |
| 059 | bollard 0.21 Dependency and Feature Selection | Version pin (0.21, verified current); features http+pipe+time, no ssl/ssh/websocket/buildkit |
| 060 | Container Resource Model and Label Namespace | ADR-050 application: alknet.managed/alknet.owner labels; list owned_only flag; hosted-services static-resource fallback; handler-driven revoke + autonomous-death tolerance |
| 061 | DockerTtyBackend in alknet-docker | Backend in alknet-docker behind tty feature; attach/exec mode split; POC drive_attach_raw as reference |
| 003 | Crate Decomposition | alknet-docker depends on alknet-core + alknet-call (ops) and alknet-tty (tty feature); no handler-depends-on-handler violation |
| 012 | Call Protocol Stream Model | The wire format docker ops use (EventEnvelope, no carriage field) |
| 017 | Call Protocol Client and Adapter Contract | from_call re-export — the proxy pattern's mechanism for hub→worker docker management |
| 023 | Operation Error Schemas | Docker ops declare domain error codes (CONTAINER_NOT_FOUND, IMAGE_NOT_FOUND, etc.) |
| 024 | Operation Registry Layering | Docker ops register in the curated Layer 0 at startup |
| 029 | Peer-Graph Routing Model | Head→worker docker management via PeerRef / invoke_peer |
| 032 | Forwarded-For Identity | End-user identity as metadata when a coordinator proxies docker ops |
| 049 | Streaming Handler for Subscriptions | StreamingHandler for logs/exec/pull; exit code on final call.responded |
| 050 | Dynamic Resource Ownership | Containers as AccessControl resources; the model ADR-060 applies |
| 052 | alknet-tty Wire Format | The alknet/tty wire format DockerTtyBackend sessions use |
| 053 | TtyBackend Trait and TtyHandle | The trait DockerTtyBackend implements |
| 055 | Exit Code on a Control Chunk | The exit-chunk ordering DockerTtyBackend's exit_code feeds into |
| 056 | Backend Cleanup on Session Cancel | DockerTtyBackend's exit_code future Drop kills the container/exec |
| 014 | Secret Material Flow and Capability Injection | Capabilities is for secret material only — the Docker handle is not secret (ADR-062) |
| 062 | Docker Client and OwnershipStore Injection via Closure Capture | Closure capture at registration; Capabilities for secrets only; matches from_openapi pattern |
| 063 | Exit Code on a Terminal call.responded for Non-Interactive Exec |
{ "exitCode": N, "terminal": true } on final call.responded; call.completed stays empty |
Relevant Open Questions
| OQ | Title | Status | Relevance |
|---|---|---|---|
| OQ-048 | Network and volume operation surface | deferred(scope) | Network/volume CRUD deferred; v1 is containers + images |
| OQ-049 | Image build (buildkit) scope | deferred(scope) | buildkit feature deferred; v1 has image/pull + image/list + image/inspect |
| OQ-050 | Docker system events subscription | resolved | docker/system/events included in v1 as a Subscription operation; internal ownership-store subscription for cleanup is a follow-up refinement |
| OQ-051 | Container create options surface | deferred(scope) | Full CreateContainerOptions (mounts, port bindings, networks) surface deferred to v1 implementation |
Key Design Principles
-
Single-host, bollard-specific. alknet-docker talks to one local docker daemon over
/var/run/docker.sock. The fleet case (multiple daemons on different machines) is a call-protocol concern — aCallClientper remote daemon, each running alknet-docker locally — not a bollard-feature concern. Nossl/sshfeatures, no remote daemon over TLS. See overview.md and ADR-058. -
Call operations on
alknet/call, not a separate ALPN. Docker ops are ordinary call-protocol operations withOperationSpecs,AccessControl, and service discovery. They inheritfrom_callre-export and peer routing. The one operation that didn't fit (interactive attach) moved toalknet/ttyviaDockerTtyBackend. Nocarriagefield oncall.requested, no parallel dispatch surface. See ADR-058. -
Containers are runtime-spawned resources (ADR-050).
createrecords ownership;removerevokes;exec/stop/inspectcheck ownership viaOperationSpec.resource_id_path. The hosted-services case (operator-managed, pre-existing containers) works through the static-resource fallback. Two labels (alknet.managed,alknet.owner) mark alknet-spawned containers for thelistfilter and the cross-check. See docker-operations.md and ADR-060. -
Interactive terminal is a tty concern, not a docker concern.
DockerTtyBackendimplementsalknet-tty'sTtyBackendtrait, behind attyfeature. The wire format, the session lifecycle, and the exit-chunk ordering live in alknet-tty; the backend produces bollard-backed handles. This is the same inversion asLocalTtyBackend(local process) andSshTtyBackend(SSH). See docker-tty-backend.md and ADR-061. -
bollard 0.21, verified current. The POC used a local 0.21 checkout; the crate depends on published 0.21 from crates.io (verified as latest). Features:
http+pipe(default, local daemon) +time(log timestamps). Nowebsocket(reliable attach only), nossl/ssh(fleet is call-protocol), nobuildkit(deferred). See ADR-059. -
Mechanical mapping, no feasibility risk. The POC validated the hard parts (raw carriage attach, logs subscription, exec with exit code). The remaining lifecycle operations (create/start/stop/remove/ list/inspect) are mechanical bollard wrapping —
Query/Mutationwith singlecall.respondedresponses, "the boring case" (POC §"What the POC Does NOT Validate" #4). See docker-operations.md.
References
docs/research/alknet-docker/poc-summary.md— the POC that validated the hard parts (interactive attach, logs subscription, exec with exit code) and surfaced the open unknowns this spec set resolves/workspace/alknet-docker-poc/— the POC source (src/ops.rsDockerOps,src/raw.rschunk codec,src/frame.rsEventEnvelope mirror,tests/integration.rs6 tests against a live daemon)/workspace/bollard/— bollard 0.21.0 source (the local checkout the POC used; identical API surface to the published 0.21)/workspace/@alkdev/dispatch/— the dispatch POC (bollard 0.18,dispatch.managed=truelabels, SSH-tunnel fleet model — the prior art this crate generalizes and the friction it removes)/workspace/system/dev1/docker.md— the production hosted-services use case (reverse-proxy, postgres, redis, gitea on dev1)/workspace/@alkdev/reverse-proxy/deploy/docker-compose.yml— the reverse-proxy's docker setup (operator-created, not alknet-spawned)docs/architecture/crates/tty/— the alknet-tty spec (DockerTtyBackendis the rowtty-backend.mdleft open)