From 7fcc31c49e589fa1f0029368da09210970805200 Mon Sep 17 00:00:00 2001 From: AviBackToBlack <54722547+AviBackToBlack@users.noreply.github.com> Date: Thu, 1 Oct 2026 11:38:12 +0100 Subject: [PATCH 1/2] Add proof-bound WSL container start --- docs/roadmap-decisions.md | 5 +- docs/roadmap-implementation-requirements.md | 11 +-- docs/wsl.md | 20 +++--- internal/wsldocker/start.go | 26 ++++++++ internal/wsldocker/start_linux.go | 17 +++++ internal/wsldocker/start_other.go | 14 ++++ internal/wsldocker/start_test.go | 74 +++++++++++++++++++++ internal/wsldocker/wsldocker.go | 4 +- 8 files changed, 154 insertions(+), 17 deletions(-) create mode 100644 internal/wsldocker/start.go create mode 100644 internal/wsldocker/start_linux.go create mode 100644 internal/wsldocker/start_other.go create mode 100644 internal/wsldocker/start_test.go diff --git a/docs/roadmap-decisions.md b/docs/roadmap-decisions.md index 91adb50..e847563 100644 --- a/docs/roadmap-decisions.md +++ b/docs/roadmap-decisions.md @@ -344,8 +344,9 @@ is not completion. preparation plus the fixed-path native install/config lifecycle are implemented; ordinary runtime integration remains gated; - Docker Desktop WSL integration proof, proof-bound bounded control requests, - and the separately constrained proof-bound attach transport are implemented - but not yet wired into an enabled frontend; multiplexed output, terminal, + the separately constrained proof-bound attach transport and exact + container-start operation are implemented but not yet wired into an + enabled frontend; multiplexed output, container creation, terminal, resize, signal and exit-code semantics remain; - namespace-prefixed/labeled WSL volume identity plus proof-bound exact inspect/create/remove and namespace discovery are implemented; tool-time diff --git a/docs/roadmap-implementation-requirements.md b/docs/roadmap-implementation-requirements.md index abf9b5d..f87797c 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 production and runtime authorization implemented / offline and private-registry work remains** | Add fully pinned offline inputs and 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, exact context-bound wait and exact container-start transports, while command/frontend wiring, stream/terminal/signal 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 | @@ -607,10 +607,11 @@ registry-derived management/tool-shim reconciliation without enabling tool 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 and exact context-bound container-wait operation are implemented but -not yet wired into an enabled frontend. Multiplexed-output decoding, -terminal/resize, signal and end-to-end exit-code propagation remain. Runtime -wiring, argument/process behavior and real WSL qualification remain. +transport, exact context-bound container-wait operation and exact +container-start operation are implemented but not yet wired into an enabled +frontend. Multiplexed-output decoding, container creation, terminal/resize, +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, project identity, named-volume behavior, file permissions, case sensitivity, diff --git a/docs/wsl.md b/docs/wsl.md index 015fee1..edf6056 100644 --- a/docs/wsl.md +++ b/docs/wsl.md @@ -237,10 +237,14 @@ 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. -These primitives do not decode multiplexed output or implement container -creation/start, terminal behavior, resize, signals, or end-to-end exit-code -propagation. Nothing is wired into tool execution yet; real WSL2 + Docker -Desktop qualification remains mandatory before support. +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. These primitives do +not decode multiplexed output or implement container creation, terminal +behavior, resize, signals, or end-to-end exit-code propagation. Nothing is +wired into tool execution yet; real WSL2 + Docker Desktop qualification +remains mandatory before support. ## Native WSL volume identity and control lifecycle @@ -282,10 +286,10 @@ 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 and wait primitive into container lifecycle, - then implement terminal/resize, signal and exit-code propagation without - accepting ambient +3. wire the implemented bounded Docker Desktop control-operation, attach, + wait and exact container-start primitives plus the raw-stream decoder into + container lifecycle, then implement container creation, terminal/resize, + signal and exit-code propagation without accepting ambient endpoint overrides; 4. Windows-filesystem and WSL-filesystem project tests plus mixed-invocation rejection; and 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 0dd5a97..3be304f 100644 --- a/internal/wsldocker/wsldocker.go +++ b/internal/wsldocker/wsldocker.go @@ -1,8 +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. It does not -// enable the WSL frontend by itself. +// separately constrained container-attach, wait and start transports. It does +// not enable the WSL frontend by itself. package wsldocker import ( From fb28b9b532f6a3e16f8e7e6457e9fce3bb3f756b Mon Sep 17 00:00:00 2001 From: AviBackToBlack <54722547+AviBackToBlack@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:05:07 +0100 Subject: [PATCH 2/2] Document WSL wait capability consistently --- docs/roadmap-decisions.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/roadmap-decisions.md b/docs/roadmap-decisions.md index e847563..6077e3b 100644 --- a/docs/roadmap-decisions.md +++ b/docs/roadmap-decisions.md @@ -344,10 +344,10 @@ is not completion. preparation plus the fixed-path native install/config lifecycle are implemented; ordinary runtime integration remains gated; - Docker Desktop WSL integration proof, proof-bound bounded control requests, - the separately constrained proof-bound attach transport and exact - container-start operation are implemented but not yet wired into an - enabled frontend; multiplexed output, container creation, terminal, - resize, signal and exit-code semantics remain; + the separately constrained proof-bound attach transport, exact + context-bound wait and exact container-start operations are implemented + but not yet wired into an enabled frontend; multiplexed output, container + creation, terminal, resize, signal and exit-code semantics 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;