diff --git a/docs/architecture.md b/docs/architecture.md index fc0d098..56eeaa1 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -463,9 +463,11 @@ separate attach transport admits only a live-stream POST for an exact full container ID, repeats the complete socket/peer proof, bounds the upgrade and error response, and returns a context-bound duplex stream with explicit TTY framing metadata and independent stdin half-close. Parent cancellation closes -the upgraded connection and unblocks I/O. Container lifecycle, -multiplexed-output decoding, terminal behavior, and signal forwarding remain -outside that primitive. +the upgraded connection and unblocks I/O. Sibling proof-bound primitives decode +strict non-TTY multiplexed output and perform exact container inspection, wait, +TTY resize and start operations. They are not yet wired into an enabled +container lifecycle; creation, terminal event collection, signal handling and +end-to-end exit propagation remain. `internal/wslvolume` defines the WSL Docker-volume identity and bounded control lifecycle. A volume name starts with `cb--`; diff --git a/docs/roadmap-decisions.md b/docs/roadmap-decisions.md index 52f8dac..c0d79af 100644 --- a/docs/roadmap-decisions.md +++ b/docs/roadmap-decisions.md @@ -347,9 +347,10 @@ is not completion. implemented; ordinary runtime integration remains gated; - Docker Desktop WSL integration proof, proof-bound bounded control requests, the separately constrained attach transport, strict raw-stream decoder, - exact container wait and TTY-resize operations are implemented but not yet - wired into an enabled frontend; terminal event collection, signal and - end-to-end exit-code propagation remain; + exact container inspection, wait, TTY-resize and start operations are + implemented but not yet wired into an enabled frontend; container + creation, terminal event collection, signal and end-to-end exit-code + propagation remain; - namespace-prefixed/labeled WSL volume identity plus proof-bound exact inspect/create/remove and namespace discovery are implemented; tool-time creation and state/GC/backup/restore command integration remain; diff --git a/docs/roadmap-implementation-requirements.md b/docs/roadmap-implementation-requirements.md index e468557..123b1ed 100644 --- a/docs/roadmap-implementation-requirements.md +++ b/docs/roadmap-implementation-requirements.md @@ -79,7 +79,7 @@ The minimum delivery gate for a code change is: | Enterprise policy | **Foundation and signed registry shipped / image trust remains** | PRs #75 and #84 shipped the machine-owned constraint layer and authenticated registry; image trust remains | | Image trust | **Online/offline production and runtime authorization implemented / private-registry work remains** | Add an explicit private-registry credential bridge | | Plugin/provider architecture | **Intentionally deferred** | Reopen only after at least two real integrations cannot fit the declarative model | -| WSL2 | **Installer foundation implemented / runtime qualification remaining** | PRs #77 and #83 shipped the fail-closed host boundary and fixed native layout/state identity; explicit read-only/apply filesystem preparation and the fixed-path native install/config/shim lifecycle are available alongside namespace-prefixed/labeled volume identity with proof-bound exact inspect/create/remove/discovery, Docker Desktop integration proof, bounded control requests, constrained attach and exact context-bound wait transports, while command/frontend wiring, stream/terminal/signal semantics and real WSL qualification remain | +| WSL2 | **Installer foundation implemented / runtime qualification remaining** | PRs #77 and #83 shipped the fail-closed host boundary and fixed native layout/state identity; explicit read-only/apply filesystem preparation and the fixed-path native install/config/shim lifecycle are available alongside namespace-prefixed/labeled volume identity with proof-bound exact inspect/create/remove/discovery, Docker Desktop integration proof, bounded control requests, constrained attach, strict raw-stream decoding, exact container inspection, exact context-bound wait, proof-bound TTY-resize and exact container-start operations, while command/frontend wiring, container creation, terminal event collection, signal/exit semantics and real WSL qualification remain | | Per-project overlays | **Completed in PR #80** | Add-only digest-bound trust model shipped on the merged enterprise-policy foundation | | Release SBOM | **Conditionally deferred** | Trigger on shipped third-party/runtime dependencies or concrete compliance/consumer demand | | Snyk | **Conditionally deferred** | Trigger only for a real coverage gap plus owner/account/token and triage/outage policy | @@ -610,9 +610,10 @@ execution. Canonical project storage classification with its proof-consuming argument mapper, fail-closed Docker Desktop WSL integration proof and a proof-bound bounded Engine API control-request primitive, constrained attach transport, strict multiplexed-output decoder, exact context-bound -container-wait operation and proof-bound container-TTY resize operation are -implemented but not yet wired into an enabled frontend. Terminal event -collection, signal and end-to-end exit-code propagation remain. Runtime wiring, +container inspection, container-wait, container-TTY resize and container-start +operations are implemented but not yet wired into an enabled frontend. +Container creation, terminal event collection, signal and end-to-end exit-code +propagation remain. Runtime wiring, argument/process behavior and real WSL qualification remain. Implementation must define native config/shim location, Docker endpoint, diff --git a/docs/wsl.md b/docs/wsl.md index 6d314f9..6403324 100644 --- a/docs/wsl.md +++ b/docs/wsl.md @@ -237,10 +237,18 @@ accepts only an exact full container ID and fixes the request to `condition=not-running`. It repeats the socket/peer proof, uses the caller's context as the long-poll lifetime, bounds the response to 64 KiB, rejects an unsafe Engine error, and accepts only process exit codes from 0 through 255. +A separate inspect operation accepts only an exact full container ID, bounds +the response to 1 MiB, requires the returned ID to match exactly and rejects +missing lifecycle/terminal fields. It exposes only the immutable ID, running, +TTY, stdin and defensively copied label state needed by later lifecycle checks. +A separate start operation accepts only an exact full container ID, repeats +the socket/peer proof, fixes the request to `POST /containers/{id}/start`, and +accepts only HTTP 204. Docker's HTTP 304 "already started" response fails +closed instead of being treated as an idempotent success. The native package also has a strict decoder for non-TTY multiplexed output and a proof-bound resize operation for one exact full container ID with positive unsigned 16-bit terminal dimensions. These primitives do not implement -container creation/start, terminal event collection, signals, or end-to-end +container creation, terminal event collection, signals, or end-to-end exit-code propagation. Nothing is wired into tool execution yet; real WSL2 + Docker Desktop qualification remains mandatory before support. @@ -284,10 +292,11 @@ following: 2. wire the implemented project-root selector, project boundary and argument mapper into native tool execution, then complete stdin/TTY and signal semantics; -3. wire the implemented bounded Docker Desktop control-operation and attach - primitives, raw-stream decoder, wait and resize operations into container - lifecycle, then implement terminal event collection, signal and exit-code - propagation without accepting ambient endpoint overrides; +3. wire the implemented bounded Docker Desktop control-operation, attach, + inspect, wait, resize and exact container-start primitives plus the + raw-stream decoder into container lifecycle, then implement container + creation, terminal event collection, signal and exit-code propagation + without accepting ambient endpoint overrides; 4. Windows-filesystem and WSL-filesystem project tests plus mixed-invocation rejection; and 5. real WSL2 + Docker Desktop end-to-end qualification before any support claim. diff --git a/internal/wsldocker/start.go b/internal/wsldocker/start.go new file mode 100644 index 0000000..72f12f1 --- /dev/null +++ b/internal/wsldocker/start.go @@ -0,0 +1,26 @@ +package wsldocker + +import ( + "context" + "errors" + "fmt" + "net/http" +) + +func startContainer(ctx context.Context, containerID string, deps operationDependencies) error { + if ctx == nil { + return errors.New("Docker Desktop WSL container start requires a context") + } + if err := validateContainerID(containerID); err != nil { + return fmt.Errorf("Docker Desktop WSL container start: %w", err) + } + if deps.check == nil || deps.statSocket == nil || deps.perform == nil { + return errors.New("Docker Desktop WSL container start dependencies are incomplete") + } + _, err := execute(ctx, Request{ + Method: http.MethodPost, + Path: "/containers/" + containerID + "/start", + SuccessStatuses: []int{http.StatusNoContent}, + }, deps) + return err +} diff --git a/internal/wsldocker/start_linux.go b/internal/wsldocker/start_linux.go new file mode 100644 index 0000000..9e9938c --- /dev/null +++ b/internal/wsldocker/start_linux.go @@ -0,0 +1,17 @@ +//go:build linux + +package wsldocker + +import "context" + +// StartContainer starts one exact stopped container through the proof-bound +// Docker Desktop WSL socket. Already-running containers fail closed. +func StartContainer(ctx context.Context, containerID string) error { + return startContainer(ctx, containerID, operationDependencies{ + check: Check, + statSocket: statDockerSocket, + perform: func(ctx context.Context, socketPath string, request Request) (operationResult, error) { + return performDockerRequest(ctx, socketPath, request, operationTimeout, maxOperationOutput, 0) + }, + }) +} diff --git a/internal/wsldocker/start_other.go b/internal/wsldocker/start_other.go new file mode 100644 index 0000000..82e8753 --- /dev/null +++ b/internal/wsldocker/start_other.go @@ -0,0 +1,14 @@ +//go:build !linux + +package wsldocker + +import ( + "context" + "errors" +) + +// StartContainer is unavailable outside Linux because the fixed Unix socket +// and peer credentials are part of the Docker Desktop WSL trust boundary. +func StartContainer(context.Context, string) error { + return errors.New("Docker Desktop WSL container start requires Linux") +} diff --git a/internal/wsldocker/start_test.go b/internal/wsldocker/start_test.go new file mode 100644 index 0000000..833a771 --- /dev/null +++ b/internal/wsldocker/start_test.go @@ -0,0 +1,74 @@ +package wsldocker + +import ( + "context" + "errors" + "net/http" + "strings" + "testing" +) + +func TestStartContainerBindsExactRequestToProvenSocket(t *testing.T) { + socket := validSocketInfo() + statCalls := 0 + deps := validOperationDependencies(socket) + deps.statSocket = func(path string) (socketInfo, error) { + if path != DockerSocketPath { + t.Fatalf("stat path = %q", path) + } + statCalls++ + return socket, nil + } + deps.perform = func(_ context.Context, path string, request Request) (operationResult, error) { + if path != DockerSocketPath || request.Method != http.MethodPost || request.Path != "/containers/"+testContainerID+"/start" { + t.Fatalf("perform(%q, %+v)", path, request) + } + if len(request.Query) != 0 || len(request.Body) != 0 || len(request.SuccessStatuses) != 1 || request.SuccessStatuses[0] != http.StatusNoContent { + t.Fatalf("start request = %+v", request) + } + return operationResult{StatusCode: http.StatusNoContent, PeerUID: 0}, nil + } + if err := startContainer(context.Background(), testContainerID, deps); err != nil { + t.Fatal(err) + } + if statCalls != 2 { + t.Fatalf("stat calls = %d, want 2", statCalls) + } +} + +func TestStartContainerRejectsInvalidInputsBeforeProof(t *testing.T) { + for name, containerID := range map[string]string{ + "short": "abc", + "uppercase": strings.ToUpper(testContainerID), + "non hex": strings.Repeat("g", 64), + } { + t.Run(name, func(t *testing.T) { + deps := validOperationDependencies(validSocketInfo()) + deps.check = func(context.Context) (Result, error) { panic("proof reached for invalid container ID") } + if err := startContainer(context.Background(), containerID, deps); err == nil { + t.Fatal("startContainer() succeeded") + } + }) + } + + deps := validOperationDependencies(validSocketInfo()) + deps.check = func(context.Context) (Result, error) { panic("proof reached with nil context") } + if err := startContainer(nil, testContainerID, deps); err == nil { + t.Fatal("startContainer() accepted nil context") + } + if err := startContainer(context.Background(), testContainerID, operationDependencies{}); err == nil || !strings.Contains(err.Error(), "dependencies are incomplete") { + t.Fatalf("incomplete-dependencies error = %v", err) + } +} + +func TestStartContainerRejectsAlreadyRunning(t *testing.T) { + deps := validOperationDependencies(validSocketInfo()) + deps.perform = func(context.Context, string, Request) (operationResult, error) { + return operationResult{StatusCode: http.StatusNotModified, Raw: []byte(`{"message":"container is already running"}`), PeerUID: 0}, nil + } + err := startContainer(context.Background(), testContainerID, deps) + var apiErr *APIError + if !errors.As(err, &apiErr) || apiErr.StatusCode != http.StatusNotModified || apiErr.Message != "container is already running" { + t.Fatalf("startContainer() error = %#v", err) + } +} diff --git a/internal/wsldocker/wsldocker.go b/internal/wsldocker/wsldocker.go index c9ffbc3..93222c7 100644 --- a/internal/wsldocker/wsldocker.go +++ b/internal/wsldocker/wsldocker.go @@ -1,9 +1,8 @@ // Package wsldocker proves that a native WSL2 process is connected to Docker // Desktop's supported WSL integration rather than an in-distribution or remote // Docker Engine, and provides proof-bound bounded control requests plus -// separately constrained container-attach and wait transports plus an exact -// proof-bound TTY-resize operation. It does not enable the WSL frontend by -// itself. +// separately constrained container-attach, inspect, wait, TTY-resize and start +// transports. It does not enable the WSL frontend by itself. package wsldocker import (