diff --git a/docs/architecture/crates/docker/README.md b/docs/architecture/crates/docker/README.md index c9c585a..20c0fab 100644 --- a/docs/architecture/crates/docker/README.md +++ b/docs/architecture/crates/docker/README.md @@ -17,14 +17,14 @@ sessions into containers. alknet-docker does two things: 1. **Call operations.** A set of `OperationSpec`-registered operations - (`docker/container/*`, `docker/image/*`) on the shared - `alknet/call` ALPN, mapping bollard's docker API to the call - protocol's `Query` / `Mutation` / `Subscription` dispatch paths. + (`docker/container/*`, `docker/image/*`, `docker/system/events`) on + the shared `alknet/call` ALPN, mapping bollard's docker API to the + call protocol's `Query` / `Mutation` / `Subscription` dispatch paths. Lifecycle (create/start/stop/remove/list/inspect) is `Query`/ - `Mutation`; logs, non-interactive exec, and image pull are - `Subscription` (streaming via `StreamingHandler`, ADR-049). The - operations declare `AccessControl` against the ADR-050 container- - as-resource model. Decided in + `Mutation`; logs, non-interactive exec, image pull, and system + events are `Subscription` (streaming via `StreamingHandler`, + ADR-049). The operations declare `AccessControl` against the + ADR-050 container-as-resource model. Decided in [ADR-058](../../decisions/058-alknet-docker-on-alknet-call.md). 2. **`DockerTtyBackend`** (behind the `tty` feature). An @@ -90,7 +90,7 @@ docs): |----|-------|--------|-----------| | 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 | deferred(scope) | `docker/system/events` subscription for stale-ownership cleanup deferred | +| 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 diff --git a/docs/architecture/crates/docker/docker-operations.md b/docs/architecture/crates/docker/docker-operations.md index fbf47cf..c58bb92 100644 --- a/docs/architecture/crates/docker/docker-operations.md +++ b/docs/architecture/crates/docker/docker-operations.md @@ -67,13 +67,13 @@ applied. | `docker/image/list` | Query | `list_images` | (none) | | | `docker/image/pull` | Subscription | `create_image` | (none) | StreamingHandler; progress events | | `docker/image/inspect` | Query | `inspect_image` | (none) | | +| `docker/system/events` | Subscription | `events` | (none) | StreamingHandler; daemon events (container start/stop/die/destroy, image pull/tag/delete, etc.) | **Out of scope for v1 (deferred):** - Network operations (`docker/network/*`) — OQ-048. - Volume operations (`docker/volume/*`) — OQ-048. - Image build (`docker/image/build` with buildkit) — OQ-049. -- System events (`docker/system/events`) — OQ-050. - Full `CreateContainerOptions` surface (mounts, port bindings, networks) — OQ-051. v1 `create` accepts the image, command, env, labels, and name; the full options surface is a v1 implementation @@ -181,9 +181,11 @@ overwritten by the handler to prevent a caller spoofing ownership). 3. Returns `call.responded` with an empty/ok result. Autonomous container death (a `--rm` exit, external `docker rm`, -daemon restart) is tolerated — stale ownership entries are not -promptly cleaned up (no reaper); they're inert (a reused container ID -gets a fresh `record` on its next `create`). See ADR-060 §4. +daemon restart) is tolerated — stale ownership entries are inert +(a reused container ID gets a fresh `record` on its next `create`). +The `docker/system/events` subscription provides the prompt-cleanup +path (internal ownership-store subscription on `destroy` events). +See ADR-060 §4. ### `docker/container/list` — scope-gate + optional result-filter @@ -317,6 +319,44 @@ each progress event → `call.responded`, stream end → `call.completed`. No exit code (image pull has no exit code; success is stream end without error). +#### `docker/system/events` + +Maps `bollard::system::events()` (returns +`Stream>`) to a stream of +`call.responded` frames. Same shape as logs: each daemon event +(container start/stop/die/destroy, image pull/tag/delete, network +create/connect, volume create/mount, etc.) → `call.responded`, stream +end on client disconnect or daemon connection close → +`call.completed`. + +```rust +let docker_clone = docker.clone(); +let handler: StreamingHandler = Arc::new(move |_input, _ctx| { + let docker = docker_clone.clone(); + Box::pin(async_stream::stream! { + let mut stream = docker.events(None::>); + while let Some(result) = stream.next().await { + match result { + Ok(event) => yield ResponseEnvelope::ok(serde_json::to_value(event) + .unwrap_or_default()), + Err(e) => yield ResponseEnvelope::error(CallError { + code: "DOCKER_ERROR".into(), + message: e.to_string(), + retryable: false, details: None, + }), + } + } + }) as ResponseStream +}); +``` + +The operation has no `resource_id_path` and no `resource_type` — it +surfaces daemon-wide events, not per-container events. The scope +check (`system:events`) gates the call. The internal ownership-store +subscription for stale-entry cleanup on `destroy` events is a +follow-up refinement — the operation itself is the architecture +decision. + ### Access Control The operations declare `AccessControl` against the ADR-050 model, @@ -407,6 +447,24 @@ Images are not runtime-spawned resources with per-caller ownership — they're shared daemon state. The scope check gates; no ownership provider consultation. +#### System events (`system/events`) + +```rust +OperationSpec { + name: "docker/system/events", + access_control: AccessControl { + required_scopes: vec!["system:events".into()], + // no resource_type — daemon-wide events, not per-resource + .. + }, + // no resource_id_path + .. +} +``` + +Scope-gate only. The operation surfaces daemon-wide events; there is +no per-resource ownership to check. + ### Error Schemas (ADR-023) Each operation declares its domain error codes in @@ -475,10 +533,11 @@ connection, not a call operation). This is the same boundary as existing container whose ownership was recorded at create time (or which pre-exists and is reached via the static fallback). - **Stale ownership entries are tolerated.** Autonomous container - death leaves a stale entry; no reaper cleans it promptly. The - entry is inert (a reused container ID gets a fresh `record`). A - `docker/system/events` subscription for prompt cleanup is deferred - (OQ-050). + death leaves a stale entry; the `docker/system/events` subscription + provides the prompt-cleanup path (internal ownership-store + subscription on `destroy` events). Until that internal subscription + is wired, stale entries are inert (a reused container ID gets a + fresh `record`). - **`owned_only` on `list` is the caller's choice.** Default `false` (returns all); `true` filters to the caller's containers. The default and the two cases are decided in ADR-060 §2; the scope @@ -505,7 +564,6 @@ See [open-questions.md](../../open-questions.md) for full details. - **OQ-048** (deferred(scope)): Network and volume operation surface. - **OQ-049** (deferred(scope)): Image build (buildkit) scope. -- **OQ-050** (deferred(scope)): Docker system events subscription. - **OQ-051** (deferred(scope)): Container create options surface. ## References diff --git a/docs/architecture/crates/docker/overview.md b/docs/architecture/crates/docker/overview.md index 2ff89a5..84fa586 100644 --- a/docs/architecture/crates/docker/overview.md +++ b/docs/architecture/crates/docker/overview.md @@ -93,6 +93,7 @@ container?" branch. | `docker/image/list` | Query | `list_images` | JSON | | `docker/image/pull` | Subscription | `create_image` | JSON (StreamingHandler) | | `docker/image/inspect` | Query | `inspect_image` | JSON | +| `docker/system/events` | Subscription | `events` | JSON (StreamingHandler) | Interactive exec (`tty: true`) and interactive attach are **not** call operations — they are `alknet/tty` sessions via `DockerTtyBackend`. @@ -318,7 +319,6 @@ See [open-questions.md](../../open-questions.md) for full details. - **OQ-048** (deferred(scope)): Network and volume operation surface. - **OQ-049** (deferred(scope)): Image build (buildkit) scope. -- **OQ-050** (deferred(scope)): Docker system events subscription. - **OQ-051** (deferred(scope)): Container create options surface. ## References diff --git a/docs/architecture/decisions/060-container-resource-model-and-label-namespace.md b/docs/architecture/decisions/060-container-resource-model-and-label-namespace.md index e446a45..f06f719 100644 --- a/docs/architecture/decisions/060-container-resource-model-and-label-namespace.md +++ b/docs/architecture/decisions/060-container-resource-model-and-label-namespace.md @@ -234,16 +234,12 @@ alknet-managed remove path, with autonomous-death tolerance**: The tolerance for stale entries is intentional: ownership is runtime state (ADR-050 assumption 4), meaningless across restarts. The in-memory default store loses all entries on restart; a persistence -adapter would cache and invalidate. A reaper that subscribes to docker -daemon death events would be more prompt but adds a subscription -surface for a marginal gain (the stale entry is inert — it can't grant -access to a container that doesn't exist, and a reused container ID -gets a fresh `record` on its next `create`). - -A future "prompt stale-entry cleanup" feature is additive: a -`docker/system/events` subscription operation (deferred, OQ-050) that -the ownership store could subscribe to. The base model tolerates stale -entries; the prompt-cleanup path is a refinement. +adapter would cache and invalidate. The `docker/system/events` +subscription operation provides the prompt-cleanup path: the +ownership store subscribes internally to `destroy` events and revokes +stale entries. Until that internal subscription is wired, stale +entries are inert (a reused container ID gets a fresh `record` on +its next `create`). ### 5. `docker/container/create` records ownership; `start`/`stop`/`restart` do not @@ -271,8 +267,8 @@ event; the lifecycle operations are management of an existing resource. `managed` flag marks alknet-spawned containers; the `owner` label carries the peer id for the `list` filter and the cross-check. - Teardown is handler-driven on the alknet path and tolerant of - autonomous death on the non-alknet path. No reaper subscription - surface in the base model. + autonomous death on the non-alknet path. The `docker/system/events` + subscription provides the prompt-cleanup path for stale entries. - ADR-050's model is applied without amendment. The `list` case (specific #4a), the teardown coupling (#4b), and the backward-compat static-resource fallback (#2) all map cleanly to @@ -308,13 +304,14 @@ event; the lifecycle operations are management of an existing resource. ## Door type **One-way (label schema, ownership model application) + two-way (label -prefix, stale-entry policy).** The label schema (two labels, +prefix).** The label schema (two labels, `.managed` + `.owner`) and the create-records/remove-revokes ownership coupling are one-way: clients and the ownership store depend on the labels and the record/revoke timing. The label prefix (default `alknet`) is two-way-door config. The -stale-entry policy (tolerate, no reaper) is two-way — a reaper -subscription is an additive refinement. +`docker/system/events` subscription provides the prompt-cleanup path +for stale entries; the internal ownership-store subscription is a +follow-up refinement. ## References diff --git a/docs/architecture/open-questions.md b/docs/architecture/open-questions.md index d183180..cbae58d 100644 --- a/docs/architecture/open-questions.md +++ b/docs/architecture/open-questions.md @@ -155,7 +155,7 @@ Door type is separate from whether a decision is made. A two-way door is a decis |----|-------|--------|------|-----| | [OQ-48](questions/048-network-and-volume-operation-surface.md) | Network and Volume Operation Surface | deferred(scope) | two | low | | [OQ-49](questions/049-image-build-buildkit-scope.md) | Image Build (buildkit) Scope | deferred(scope) | two | low | -| [OQ-50](questions/050-docker-system-events-subscription.md) | Docker System Events Subscription | deferred(scope) | two | low | +| [OQ-50](questions/050-docker-system-events-subscription.md) | Docker System Events Subscription | resolved | two | low | | [OQ-51](questions/051-container-create-options-surface.md) | Container Create Options Surface | deferred(scope) | two | med | ### alknet-hub @@ -224,12 +224,6 @@ filtering the tables above. - **Priority**: low - **Full file**: [OQ-49](questions/049-image-build-buildkit-scope.md) -### OQ-50: Docker System Events Subscription - -- **Blocked on**: a concrete use case for prompt stale-ownership cleanup. The base model (ADR-060 §4) tolerates inert stale entries; the events subscription is a refinement. -- **Priority**: low -- **Full file**: [OQ-50](questions/050-docker-system-events-subscription.md) - ### OQ-51: Container Create Options Surface - **Blocked on**: v1 implementation — the `create` input JSON Schema is finalized when `register_docker_ops` is written and tested against bollard's `Config` struct. An architectural decision (ADR-060 §5), not a deferral past implementation. diff --git a/docs/architecture/questions/050-docker-system-events-subscription.md b/docs/architecture/questions/050-docker-system-events-subscription.md index 384a450..70fac6a 100644 --- a/docs/architecture/questions/050-docker-system-events-subscription.md +++ b/docs/architecture/questions/050-docker-system-events-subscription.md @@ -2,33 +2,28 @@ - **Origin**: [crates/docker/docker-operations.md](crates/docker/docker-operations.md) - §"Out of scope for v1"; + §"Operation Surface"; [ADR-060](decisions/060-container-resource-model-and-label-namespace.md) §4 (autonomous-death tolerance). -- **Status**: deferred(scope) +- **Status**: resolved - **Door type**: Two-way - **Priority**: low -- **Blocked on**: a concrete use case for prompt stale-ownership - cleanup. The base model (ADR-060 §4) tolerates stale ownership - entries from autonomous container death (a `--rm` exit, external - `docker rm`, daemon restart) — they're inert (a reused container ID - gets a fresh `record` on its next `create`). A reaper that - subscribes to docker daemon events would clean them promptly, but - the promptness gain is marginal for the current use cases. -- **Resolution**: Not yet decidable. bollard's `events()` - (system.rs:128) returns a `Stream` of daemon - events (container start/stop/die/destroy, image pull, etc.). A - `docker/system/events` `Subscription` operation would surface these - as `call.responded` frames; the ownership store could subscribe - internally to revoke on `destroy` events. The deferral is scope: - the base model works without it; the events subscription is a - refinement for when prompt cleanup matters (e.g., a high-churn - coordinator that spawns/removes many containers and wants the - ownership store to stay tight). Adding it is additive (a new - operation + an internal store subscription) and does not break the - existing surface. +- **Resolution**: `docker/system/events` is included in v1 as a + `Subscription` operation. bollard's `events()` returns + `Stream` — the same `StreamingHandler` pattern + already wired for `logs`, `exec`, and `image/pull`. The operation + surfaces daemon events (container start/stop/die/destroy, image + pull/tag/delete, etc.) as `call.responded` frames. The internal + ownership-store subscription for stale-entry cleanup on `destroy` + events is a follow-up refinement — the operation itself is the + architecture decision. The use case is already documented in + ADR-060 §4 (autonomous container death leaves stale ownership + entries); the operation is cheap to add (same mechanical + `StreamingHandler` mapping as the three existing streaming ops) and + generally useful beyond ownership cleanup (any consumer may want + daemon events). - **Cross-references**: [ADR-060](decisions/060-container-resource-model-and-label-namespace.md) - §4 (teardown coupling — stale-entry tolerance is the base model - this subscription would refine), - [crates/docker/docker-operations.md](crates/docker/docker-operations.md) \ No newline at end of file + §4 (teardown coupling — stale-entry tolerance is the base model; + the events subscription provides the prompt-cleanup path), + [crates/docker/docker-operations.md](crates/docker/docker-operations.md)