Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 signal operations. They are not yet wired into an enabled
container lifecycle; creation/start, terminal event collection, host-signal
interception/forwarding 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-<wsl-namespace>-`;
Expand Down
7 changes: 4 additions & 3 deletions docs/roadmap-decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 signal operations are
implemented but not yet wired into an enabled frontend; container
creation/start, terminal event collection, host-signal interception and
forwarding policy, 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;
Expand Down
11 changes: 6 additions & 5 deletions docs/roadmap-implementation-requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 container-signal operations, while command/frontend wiring, container creation/start, terminal event collection, host-signal interception/forwarding policy, 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 |
Expand Down Expand Up @@ -610,10 +610,11 @@ 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,
argument/process behavior and real WSL qualification remain.
container inspection, container-wait, container-TTY resize and container-signal
operations are implemented but not yet wired into an enabled frontend.
Container creation/start, terminal event collection, host-signal interception
and forwarding policy, 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,
Expand Down
9 changes: 9 additions & 0 deletions docs/wsl-process-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,15 @@ child process; they do not require Docker.

## Signals

The unexposed native Engine API path has a proof-bound container-signal
operation. It accepts one exact full container ID and one explicit numeric Linux
signal in the `1..64` domain, always supplies Docker's `signal` query parameter
and accepts only HTTP 204. It never relies on the endpoint's default `SIGKILL`.

This primitive does not choose which host signals to intercept, install signal
handlers or define cleanup ordering. The enabled frontend must make those
policies explicit and qualify them end to end before invoking the operation.

The tool-run path installs no signal handler and creates no new process group.
On native Linux/WSL, the `cb` process and its `docker` child therefore retain
the operating system's default process-group relationship. ContainerBin does
Expand Down
21 changes: 15 additions & 6 deletions docs/wsl.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,12 +237,20 @@ 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 proof-bound signal operation accepts only an exact full container ID
and an explicit numeric Linux signal in the `1..64` domain, always supplies the
Engine `signal` query and accepts only HTTP 204.
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
exit-code propagation. Nothing is wired into tool execution yet; real WSL2 +
Docker Desktop qualification remains mandatory before support.
container creation/start, terminal event collection, host-signal interception
or forwarding policy, 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

Expand Down Expand Up @@ -285,9 +293,10 @@ following:
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;
primitives, raw-stream decoder, inspect, wait, resize and signal operations
into container lifecycle, then implement container creation/start, terminal
event collection, host-signal interception and forwarding policy, 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.
Expand Down
36 changes: 36 additions & 0 deletions internal/wsldocker/signal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package wsldocker

import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
)

const maxLinuxSignal = 64

func signalContainer(ctx context.Context, containerID string, signal int, deps operationDependencies) error {
if ctx == nil {
return errors.New("Docker Desktop WSL container signal requires a context")
}
if err := validateContainerID(containerID); err != nil {
return fmt.Errorf("Docker Desktop WSL container signal: %w", err)
}
if signal < 1 || signal > maxLinuxSignal {
return fmt.Errorf("Docker Desktop WSL container signal must be between 1 and %d", maxLinuxSignal)
}
if deps.check == nil || deps.statSocket == nil || deps.perform == nil {
return errors.New("Docker Desktop WSL container signal dependencies are incomplete")
}
_, err := execute(ctx, Request{
Method: http.MethodPost,
Path: "/containers/" + containerID + "/kill",
Query: url.Values{"signal": {strconv.Itoa(signal)}},
SuccessStatuses: []int{
http.StatusNoContent,
},
}, deps)
return err
}
17 changes: 17 additions & 0 deletions internal/wsldocker/signal_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//go:build linux

package wsldocker

import "context"

// SignalContainer sends one explicit Linux signal number to one exact running
// container. Signal numbers outside the Linux 1..64 domain fail closed.
func SignalContainer(ctx context.Context, containerID string, signal int) error {
return signalContainer(ctx, containerID, signal, operationDependencies{
check: Check,
statSocket: statDockerSocket,
perform: func(ctx context.Context, socketPath string, request Request) (operationResult, error) {
return performDockerRequest(ctx, socketPath, request, operationTimeout, maxOperationOutput, 0)
},
})
}
14 changes: 14 additions & 0 deletions internal/wsldocker/signal_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//go:build !linux

package wsldocker

import (
"context"
"errors"
)

// SignalContainer is unavailable outside Linux because the fixed Unix socket
// and peer credentials are part of the Docker Desktop WSL trust boundary.
func SignalContainer(context.Context, string, int) error {
return errors.New("Docker Desktop WSL container signal requires Linux")
}
70 changes: 70 additions & 0 deletions internal/wsldocker/signal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package wsldocker

import (
"context"
"net/http"
"strconv"
"strings"
"testing"
)

func TestSignalContainerBindsExactSignalToProvenSocket(t *testing.T) {
for _, signal := range []int{1, 2, maxLinuxSignal} {
t.Run(strconv.Itoa(signal), func(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+"/kill" {
t.Fatalf("perform(%q, %+v)", path, request)
}
if request.Query.Encode() != "signal="+strconv.Itoa(signal) || len(request.Body) != 0 || len(request.SuccessStatuses) != 1 || request.SuccessStatuses[0] != http.StatusNoContent {
t.Fatalf("signal request = %+v", request)
}
return operationResult{StatusCode: http.StatusNoContent, PeerUID: 0}, nil
}
if err := signalContainer(context.Background(), testContainerID, signal, deps); err != nil {
t.Fatal(err)
}
if statCalls != 2 {
t.Fatalf("stat calls = %d", statCalls)
}
})
}
}

func TestSignalContainerRejectsInvalidInputsBeforeProof(t *testing.T) {
tests := map[string]struct {
ctx context.Context
containerID string
signal int
}{
"nil context": {containerID: testContainerID, signal: 2},
"short ID": {ctx: context.Background(), containerID: "abc", signal: 2},
"uppercase ID": {ctx: context.Background(), containerID: strings.ToUpper(testContainerID), signal: 2},
"non-hex ID": {ctx: context.Background(), containerID: strings.Repeat("g", 64), signal: 2},
"zero signal": {ctx: context.Background(), containerID: testContainerID},
"negative": {ctx: context.Background(), containerID: testContainerID, signal: -1},
"too large": {ctx: context.Background(), containerID: testContainerID, signal: maxLinuxSignal + 1},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
deps := validOperationDependencies(validSocketInfo())
deps.check = func(context.Context) (Result, error) { panic("proof reached for invalid signal") }
if err := signalContainer(test.ctx, test.containerID, test.signal, deps); err == nil {
t.Fatal("signalContainer() succeeded")
}
})
}

if err := signalContainer(context.Background(), testContainerID, 2, operationDependencies{}); err == nil || !strings.Contains(err.Error(), "dependencies are incomplete") {
t.Fatalf("incomplete-dependencies error = %v", err)
}
}
5 changes: 2 additions & 3 deletions internal/wsldocker/wsldocker.go
Original file line number Diff line number Diff line change
@@ -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
// signal transports. It does not enable the WSL frontend by itself.
package wsldocker

import (
Expand Down
Loading