You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Running a config whose image wasn't already cached failed, and told you to run wxc-exec.exe --setup-wslc --image <name> and try again. The run now pulls the image and carries on, so a config naming a fresh image works on the first invocation.
Pull failures split by whether a retry could succeed. A reference the registry refuses and a registry the host is barred from are Rejected; no network, DNS failure, or a registry that is down is Host and retryable.
--setup-wslc stays. It is no longer a prerequisite, but it still moves the download off the critical path and is how you populate a cache for a host that cannot reach a registry.
A pull only happens where it is allowed. A config that denies egress is refused rather than pulled. An administrative policy can restrict which registries a pull may reach, and an unreadable policy refuses the pull rather than ignoring it. A pull that stops making progress is abandoned at a deadline instead of holding the host indefinitely, and a pull that resolves a tag records the digest it produced.
A reference naming a digest always goes to the registry, and records none. The digest a reference carries is not the digest the local store reports, and nothing exposes a mapping between the two, so matching such a reference against the cache could serve content nobody asked for, and reporting the store's digest against it would name content the caller did not pin. An image supplied as a tar records none either, since nothing was resolved.
On the state-aware path a pull no longer occupies the daemon's single lifecycle worker: deciding what to do with the image stays on the worker, and a pull parks the request until it lands. Measured against a pull that never responds, with the daemon identity pinned so both requests provably share one worker, a cached provision takes 0.1s whether or not the stalled pull is running.
🔗 References
No existing issue. Supersedes the two-call pattern introduced by #165.
WSLC E2E on a WSL2 host: 116/116 (35 one-shot + 81 state-aware)
Cold cache pulls then executes in one invocation; a warm cache reuses without pulling; an unreachable registry fails in ~5s with offline guidance; a nonexistent image is rejected without it
Live-checked on a cold cache that a denied egress posture refuses the pull, that a registry outside the allowlist is refused, and that the deadline abandons a pull that never responds
Node state-aware SDK suite passes against the in-process library. The Rust and .NET SDK suites pass but stop at the request boundary, so they show this breaks no contract rather than exercising the pull
Commits 1 and 2 claim no behavior change; that was checked by normalizing both original function bodies and the merged one and diffing — after substituting the config accessors and the log prefix, the remainder is identical.
Cargo.lock gains one line. winreg was already a workspace dependency and is now used to read the registry policy, so no new external dependency enters the graph.
The later commits address review findings. A provision whose client timed out mid-pull left a container nobody could reach, holding the daemon's idle watchdog above zero forever — and the client's own timeout is what hid it, because the abandoned reader kept the connection open and the daemon never learned its reply had not arrived. The client now closes that connection, and a sandbox that still cannot be released after repeated attempts stops counting against the watchdog so the daemon can shut down, deleting the container when it does. The offline advice pointed at --setup-wslc, which shares the same pull and fails identically on that host. The cache-miss path had no committed test, since the E2E preflight pre-pulls every image.
The reason will be displayed to describe this comment to others. Learn more.
Summary
Requesting changes for three verified blockers in the automatic image-pull path:
Registry pulling is an unbounded synchronous SDK call on the daemon's single lifecycle worker.
The new abandoned-provision cleanup observes the internal worker reply channel, which remains open during real named-pipe client timeouts/disconnects.
A request declaring network.egress.default: deny can now cause a configuration-directed host-network registry fetch before container enforcement exists.
Verification receipts: reviewed head d1688d35cdbc; all three anchors are added lines in the 19-file PR patch. The head is behind the current base (merge-base 86fb3d2ab, current base snapshot 3b0ac9c25), but the reviewed head and findings are unchanged.
The move/deduplication itself looked sound; these requests are about the new pull and ownership semantics.
The reason will be displayed to describe this comment to others. Learn more.
High (security/policy) — Settle the deny-policy contract before locking this behavior in.
Attribution:introduced_by_change — this new fixture intentionally demonstrates that a request declaring deny egress may still trigger a host-side pull on cache miss.
The pull happens before the container exists, using the host/session network and a destination selected by wslc.image. The validator still accepts egress.default: deny as the isolated WSLC posture, so documenting this exception does not provide enforcement or explicit caller authorization.
Fix: Either reject cache-miss pulls under isolated posture and require a prewarmed cache/imageTarPath, or add an explicit policy field authorizing host-side registry access and include it in validation, documentation, and policy identity.
The reason will be displayed to describe this comment to others. Learn more.
Taken the fail-closed option. resolve_image now takes a RegistryAccess that each caller derives from the posture it already computes (policy::network_is_isolated one-shot, NetworkMode on the daemon), so a cache miss under a deny posture is rejected, naming the three ways forward: warm the cache, set imageTarPath, or allow egress. --setup-wslc is deliberately unaffected.
Fixture split accordingly: wslc_cold_cache_pull.json declares allow and covers the pull, and a new wslc_cold_cache_denied_egress.json asserts the refusal and that the workload never runs. Verified live.
One thing to weigh: a config that omits network is isolated by default, so this refuses those too — including wslc_python_hello.json in the getting-started guide, which passes E2E only because the suite pre-pulls.
A wslc.allowImagePull opt-in is the proper fix, but stateAwareWslc is pinned at 0.9.0-alpha, so it needs a version promotion rather than an added field — its own PR. Happy either way: land this now, or hold for the opt-in.
The reason will be displayed to describe this comment to others. Learn more.
Supplementary Medium and Low findings
These are additional non-blocking findings from the adversarial review. The existing requested-changes verdict remains based on the three High findings.
Findings outside the diff / claim locations
Medium (documentation drift) — Canonical policy documentation still describes the deny posture as isolated without the provision-time pull exception.docs/wsl/wslc-state-aware.md:119-139 — Attribution:newly_exposed_by_change. The PR changes provision so a cache miss contacts the registry before container enforcement, but only updates the test-prerequisite line in this lifecycle guide; the policy matrix remains unchanged. docs/schema.md is also outside the PR patch and has no caveat near wslc.image. Add the host-side pull exception to both authoritative locations.
Low (proportionality) — Roughly half the changed LOC is an optional pure relocation.commit 84fe92c94851 message — Attribution:introduced_by_change. The move is verified as effectively verbatim and is not a correctness concern, but it substantially increases review volume for the feature. No fix is required; consider separating such moves in future changes.
Attribution notes
docs/schema.md and the policy-matrix lines are not modified by the PR; they are retained only because the new behavior makes their existing wording newly incomplete.
The stale tests/scripts/README.md wording is byte-identical and too minor to post as a separate finding.
The reason will be displayed to describe this comment to others. Learn more.
Copilot review overview
🟡 Changes recommended
The registry-policy override bypasses administrator policy, and additional timeout, digest-reporting, and specification discrepancies remain unresolved.
The reason will be displayed to describe this comment to others. Learn more.
Re-review after e9972527da9c
The update addresses or makes moot 12 of the 15 original findings. The pure-relocation proportionality note required no code change. I am keeping changes requested for two blockers: the pull deadline is not a hard wall-clock deadline, and the new test override bypasses the administrative registry policy it is meant to enforce.
Original finding resolution
#
Status
Verification
1
Partially addressed — still blocking
Added a 540s progress-callback deadline, but WSLC invokes the callback only for Docker response-body progress chunks; a silent DNS/connect/header stall produces no callback and can still hold the single worker indefinitely.
2
Addressed
Cleanup moved to response delivery; client timeout cancels the blocked pipe read; delivery and vanished-client tests pass.
3
Addressed
Cache misses under deny posture now fail closed; dedicated E2E fixture asserts refusal.
4
Addressed
Undelivered cleanup retries, then retires the entry from the live count while preserving shutdown cleanup.
5
Partially addressed — replacement has a new blocker
Registry allowlist and digest audit were added, but the production environment override can replace HKLM policy.
6
Addressed
Unknown HRESULT remediation is now conditional rather than diagnosed as network failure.
7
Addressed
Untagged references normalize to :latest; unit and live evidence cover reuse.
8
Addressed
E2E now distinguishes pull from cache hit and asserts no second pull.
9
Addressed
Pure action-selection and name-matching functions have unit coverage.
10
Addressed
E2E asserts image-not-found-specific guidance; classification unit tests remain.
11
Moot after redesign
Deny posture no longer pulls, so the isolation documentation is accurate again.
12
Addressed
SDK initialization moved to sdk_init, breaking the module cycle.
13
Addressed
SDK text is control-character-sanitized and character-boundary capped.
14
Addressed
Cold-cache purge refuses a reparse-point root.
15
Acknowledged
Optional relocation remains non-blocking and needs no code change.
The current LXC CI failure is unrelated to this PR: the version-comparison fixture now differs only in $schema; the latest main LXC workflow subsequently passed.
Short client timeouts leave no pull deadline headroom
src/backends/wslc/common/src/image.rs:64
For every supported client timeout of 60 seconds or less, subtracting PULL_HEADROOM yields zero and this fallback returns the entire client timeout. The daemon's pull deadline then fires at the same instant as the client's response deadline, leaving no time to deliver the pull failure and defeating this function's invariant. Reserve a positive fraction of short timeouts as headroom as well, and update the short-deadline tests accordingly.
Synchronous pulls can exceed the documented timeout indefinitely
src/backends/wslc/common/src/image.rs:172
The timeout is checked only from pull_progress, while this SDK call blocks synchronously. As the new start_pull documentation notes, a transfer stalled before its next progress callback cannot be stopped this way. The one-shot runner and --setup-wslc both call pull_image directly, so they can exceed the documented 540-second bound indefinitely. These paths need an external deadline with session-lifetime handling, not only a callback deadline.
Offline warm-cache runs incorrectly require a registry pull
tests/scripts/run_wslc_all_tests.ps1:405
This always purges a dedicated store and then requires a successful registry pull. Consequently the documented offline warm-cache invocation (-SkipSetup, or a host with only the default cache populated) now fails here despite the script prerequisites saying registry access or cached images is sufficient. Gate the cold-cache block behind an explicit network-enabled option/probe, or update the runner's prerequisites so registry access is mandatory.
Pure code move, no behavior change. Collects the WSLc image-handling code
into `wslc_common::image` so the one-shot runner and the state-aware daemon
read it from one place.
Moved verbatim:
wsl_container_runner.rs -> image.rs TarFormat, detect_tar_format,
import_image_from_tar, and the tar
detection tests
container_steps.rs -> image.rs resolve_image
The only edits to moved lines are the path qualifiers that had to drop when
associated functions became free functions (`Self::detect_tar_format` ->
`detect_tar_format`, `WSLContainerRunner::import_image_from_tar` ->
`import_image_from_tar`), plus one rustfmt reflow that fits `let
first_component = entry_path` on a single line at the shallower indent.
`setup_pull_image` stays in `wsl_container_runner.rs` for now: it reaches the
private `init_and_load_sdk`, and moving it would widen that visibility for no
present gain. It moves in the commit that extracts the pull primitive.
Review with `--color-moved=zebra --color-moved-ws=allow-indentation-change`;
git will not detect this as a rename because the functions come from the
middle of two files that both survive.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e1e6a680-2715-49df-ac0e-9df5352325b6
The one-shot runner and the state-aware daemon each carried their own copy
of resolve_image. The two bodies were the same cache-lookup, tar-import and
rejection logic, down to the error string, so a fix to one silently left the
other behind. This deletes the one-shot copy and points both callers at the
shared function.
Reconciling the two cost three things:
- The one-shot copy read `image`, `image_tar_path` and `storage_path` off
`self.config`. The shared function already took all three as arguments,
so the one-shot call site now passes them explicitly and the function
needs no receiver.
- The prefix each copy logged under, `[WSLC]` against `[WSLC][daemon]`, is
now an argument. Both callers pass what they printed before, so the
output is unchanged. Note this tag was already unreliable: the daemon's
own tar-import lines print `[WSLC]`, as do the container-settings lines
in `create_daemon_container`. Worth settling separately.
- The one-shot copy branched with if/else-if/else and a trailing `Ok(())`;
the shared one returns early. Kept the early returns, which leaves the
rejection as the function's tail expression, where the pull will go.
Also restores the comment about `info.name` being a fixed-size,
possibly-unterminated C buffer, which explains the manual NUL scan and had
survived only in the one-shot copy.
No behavior change. Verified by normalizing both original bodies and the
merged one and diffing: after substituting the config accessors and the log
prefix, the remainder is identical.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e1e6a680-2715-49df-ac0e-9df5352325b6
Running a config whose image was not already cached failed, and told the
operator to run `wxc-exec.exe --setup-wslc --image <name>` and try again.
Two commands to do one thing, and the first one is only discoverable by
hitting the error. The run now pulls the image and carries on, so a config
referencing a fresh image works on the first invocation.
Pull failures split by whether a retry could succeed. A reference the
registry refuses (WSLC_E_IMAGE_NOT_FOUND) and a registry the host is barred
from (WSLC_E_REGISTRY_BLOCKED_BY_POLICY) are Rejected: the same input will
fail again. Everything else — no network, DNS failure, a registry that is
down — is Host, and retryable. The header declares both codes with
MAKE_HRESULT, which bindgen cannot evaluate, so the facade derives them from
the generated WSLC_E_BASE.
The retryable message keeps the cache-warming guidance the old rejection
carried, including the storagePath override, and adds imageTarPath. Losing
the network should not also lose the instructions for working without it.
`--setup-wslc` stays. It is no longer a prerequisite, but it still moves the
download off the critical path, and it is how you populate a cache for a
host that cannot reach a registry. `setup_pull_image` moves to image.rs and
now shares `pull_image`, so WslcPullSessionImage has one call site.
Bring-up reaching the network is new and deliberate. A config setting
`network.egress.default` to `deny` constrains the container once it is
running; it does not constrain the pull that precedes it. Verified on a live
host: the pull completes and the container is still sealed. Documented in
the getting-started guide, since someone reviewing sandbox posture will ask.
Progress reporting is not wired up — `progressCallback` stays null, so a
large pull is silent until it finishes. A private registry still cannot be
reached: `registryAuth` is null, and such a pull surfaces as
WSLC_E_IMAGE_NOT_FOUND with the SDK's own docker-login wording.
Validated on a WSL2 host: cold cache pulls then executes in one invocation,
a warm cache reuses without pulling, an unreachable registry fails in ~5s
with the offline guidance, and a nonexistent image is rejected without it.
Full WSLC E2E 111/111.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e1e6a680-2715-49df-ac0e-9df5352325b6
Three findings from the adversarial review of this branch.
A provision that pulls a cold image can outlast the client's deadline. The
worker still created the container and filed it under a sandbox_id nobody
would ever receive, so it could not be deprovisioned, and its entry held the
idle watchdog's container count above zero — the daemon could never idle out
and the WSL utility VM leaked for the life of the process. The Provision
dispatch arm now releases a container whose reply channel has closed, which
is what the Exec arm already does for a disconnected client.
The unreachable-registry message told the operator to run `--setup-wslc`.
That shares `pull_image`, so on the host that just failed it is the same
fetch failing again. It now leads with "Restore network access and retry",
offers imageTarPath first, and qualifies the cache-warming command as
something to run from a connected machine. Both rejection arms also name a
way forward instead of stopping at the diagnosis; the PowerShell spelling of
the command is dropped, since repeating a long image reference four times
made the message harder to read, not more useful.
The cache-miss path had no committed test: the E2E preflight pre-pulls every
image, so nothing reached the branch this branch rewrote. Two configs pin it
against a storage path the runner purges first — one asserts pull-then-run
and reuse on a second pass, one asserts an unresolvable reference is
rejected.
Also folds `StorageArg` back into a plain function now that only one command
spelling is rendered.
WSLC E2E 114/114 on a WSL2 host (was 111; the three new cases are the
cold-cache ones). The daemon fix carries an #[ignore]d regression test —
it needs a live host, since a provision cannot succeed without one.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e1e6a680-2715-49df-ac0e-9df5352325b6
Addresses the PR review. Three behaviour changes and the tests that were
asserting less than they claimed.
A cache miss no longer pulls when the request declares no egress. The fetch
runs on the host's network before the container exists, so the declared
posture cannot constrain it; a config that asks for isolation now gets a
rejection naming the warm-cache, imageTarPath and allow-egress routes instead
of a silent fetch. `--setup-wslc` is unaffected: an operator warming a cache
is not a sandbox with a policy.
The orphan cleanup moved to where the failure is observable. Checking
`reply.is_closed()` on the worker never fired: `SessionHandle::provision`
holds the receiver and the control-server task stays parked on it, so a pipe
client timing out leaves it open and the failure only surfaces at the later
`write_frame`. Cleanup now lives there, with bounded retry, because a
transient delete leaves a container nobody else can name.
An untagged reference re-pulled on every run. The store spells out
`busybox:latest` while the request said `busybox`, and the comparison was
literal, so the cache never matched. Matching now accounts for the implicit
tag, leaving explicit tags, digests, and `host:port/` prefixes alone.
The catch-all failure arm no longer asserts a network cause -- a full disk
reaches it too -- and registry-supplied text is stripped of control
characters and capped before it reaches a log, a message, or the pipe.
The new E2E cases were checking too little: both cold-cache runs asserted
only the workload marker, so a needless re-pull passed, and the unresolvable
case asserted `could not be pulled`, which every arm says. They now assert
the pull and cache-hit signals, and the wording unique to the non-retryable
arm. The store they purge is checked for a reparse point first.
`resolve_image`'s branch selection and the name matching are now pure
functions with unit coverage, so cache-hit, tar, pull and refuse are testable
without a live host.
WSLC E2E 115/115 on a WSL2 host. 275 unit tests. Verified live: a deny
posture refuses rather than pulls, and an untagged reference now hits the
cache on its second run.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e1e6a680-2715-49df-ac0e-9df5352325b6
Bound the runtime pull, gated which registries it may reach, and broke the
cycle between image handling and the runner:
- A pull now runs under a 540s deadline, overridable with
MXC_WSLC_PULL_TIMEOUT_SECS. The SDK progress callback returns E_ABORT
past the deadline, which aborts the pull rather than only abandoning
the wait.
- An administrative allowlist (SOFTWARE\Policies\Mxc,
WslcAllowedImageRegistries) decides which registries a runtime pull may
reach, and the resolved digest is recorded on every resolution. Pinning
a digest from cache is not implementable: a reference carries the
manifest digest while the store reports the config digest, and the SDK
exposes no mapping between them, so digest references always go to the
registry.
- SDK bootstrap moved into sdk_init, so image no longer calls back into
wsl_container_runner.
Closed the orphaned-sandbox path a client timeout leaves behind:
- On timeout the client cancels its blocked read instead of abandoning
the reader thread. The reader held the pipe open, so the daemon's write
succeeded and it never learned the reply was undelivered.
- A sandbox whose release fails terminally is retired from the live
count, so the idle watchdog can shut the daemon down. Its handle stays
in the map and the container is still deleted at shutdown.
- Extracted deliver_provision_reply so the undelivered path is covered
without a live session. The previous test passed whenever provisioning
failed for unrelated reasons, without entering the cleanup branch.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e1e6a680-2715-49df-ac0e-9df5352325b6
Administrative registry policy can no longer be relaxed by user state:
- Removed the environment override that replaced the allowlist before HKLM
was read, so any standard user could widen an administrator's
restriction. Tests now redirect the policy read to a per-test HKCU key,
paired with an owner PID, and that redirect is compiled out of release
builds - the same shape the telemetry policy uses.
- The read path itself had no coverage: every earlier test either built a
policy value directly or went through the override. All four documented
states now drive the real registry, and each was checked by reverting
the behaviour and confirming the test fails.
- An allowlist an administrator deliberately emptied now reports as a
policy that was read and permits nothing, rather than one that could not
be read and should be corrected.
- Added the administrator-facing reference the value was missing; it lives
under the same key as the telemetry policy and needs the same
documentation.
The pull budget is bounded by the deadline its caller is waiting on:
- Raising MXC_WSLC_PULL_TIMEOUT_SECS past the client's response deadline
produced exactly the abandoned container the budget exists to prevent -
the error message advising the operator to raise it made that reachable
by following instructions. The effective budget is now capped under the
caller's deadline, and a value too large for the clock falls back to the
default rather than panicking.
- The pull runs on a thread of its own, so a registry that stops
responding cannot sit on the daemon's single lifecycle worker for the
whole budget. The SDK reports progress only as response chunks arrive,
so a transfer stalled before its next chunk cannot be cancelled from
outside; teardown therefore waits for an abandoned pull before releasing
the session it is still using.
Two fixes to what a run reports and what a test proves:
- The digest recorded after a pull was matched by repository, so a store
holding several tags of one image could attribute the wrong digest to
the run. It now matches the exact tag, and a reference pinning a digest
reports nothing rather than the store's unrelated one.
- The timeout test slept instead of blocking on I/O, so it never reached
the cancellation path it was meant to cover. It now blocks a real pipe
read and asserts the server's reply cannot land, which is what lets the
daemon release an undelivered sandbox.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e1e6a680-2715-49df-ac0e-9df5352325b6
A pull no longer occupies the daemon's single lifecycle worker.
Bounding the pull was not enough: the worker still waited on it, so a
registry that stopped responding delayed start, exec, stop, deprovision
and teardown for every other sandbox until the budget ran out. Provision
now splits at the network boundary. Deciding what to do with the image
touches only the local store and stays on the worker; when that decision
is a pull, the request parks and the worker returns to its queue. The
pull reports back as another command, and a timer posts the same command
if the budget expires first, so whichever lands first answers the caller
and the other finds nothing to answer.
Measured against a pull that never responds, with the daemon identity
pinned so both requests provably share one worker: a cached provision
takes 0.1s idle and 0.1s while the pull is stuck, where it previously
took 4.6s and 10.1s.
Two consequences worth naming. A parked provision has no container yet,
so it now counts toward the idle watchdog - otherwise the daemon could
retire itself out from under the client still waiting. And shutdown
answers a parked provision rather than dropping its reply, which would
otherwise reach the client as a bare "worker gone".
The pull still cannot be stopped from outside. The SDK reports progress
only as response chunks arrive and exposes no cancellation handle, so a
transfer stalled before its next chunk ends when the SDK's own network
timeouts fire. What changed is that nothing waits on it.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e1e6a680-2715-49df-ac0e-9df5352325b6
Enforce setup pull deadline via off-thread execution
src/backends/wslc/common/src/image.rs:993
--setup-wslc still calls the synchronous pull directly. Its deadline is checked only from pull_progress, so the no-progress stall described above start_pull never invokes the callback and can block this command indefinitely; the advertised 540-second bound is not enforced here. Run setup through the off-thread deadline path too, while preserving the session handle if the timed-out SDK call remains in flight.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Needs-AttentionRequires attention or a decision from the MXC maintainers.Needs-Author-FeedbackWaiting for additional information or action from the issue or pull-request author.
4 participants
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
📖 Description
Running a config whose image wasn't already cached failed, and told you to run
wxc-exec.exe --setup-wslc --image <name>and try again. The run now pulls the image and carries on, so a config naming a fresh image works on the first invocation.Pull failures split by whether a retry could succeed. A reference the registry refuses and a registry the host is barred from are
Rejected; no network, DNS failure, or a registry that is down isHostand retryable.--setup-wslcstays. It is no longer a prerequisite, but it still moves the download off the critical path and is how you populate a cache for a host that cannot reach a registry.A pull only happens where it is allowed. A config that denies egress is refused rather than pulled. An administrative policy can restrict which registries a pull may reach, and an unreadable policy refuses the pull rather than ignoring it. A pull that stops making progress is abandoned at a deadline instead of holding the host indefinitely, and a pull that resolves a tag records the digest it produced.
A reference naming a digest always goes to the registry, and records none. The digest a reference carries is not the digest the local store reports, and nothing exposes a mapping between the two, so matching such a reference against the cache could serve content nobody asked for, and reporting the store's digest against it would name content the caller did not pin. An image supplied as a tar records none either, since nothing was resolved.
On the state-aware path a pull no longer occupies the daemon's single lifecycle worker: deciding what to do with the image stays on the worker, and a pull parks the request until it lands. Measured against a pull that never responds, with the daemon identity pinned so both requests provably share one worker, a cached provision takes 0.1s whether or not the stalled pull is running.
🔗 References
No existing issue. Supersedes the two-call pattern introduced by #165.
🔍 Validation
cargo fmt --all --check,cargo clippy --workspace --all-targets --features wslc -- -D warningswslc_common, 44 in the daemonCommits 1 and 2 claim no behavior change; that was checked by normalizing both original function bodies and the merged one and diffing — after substituting the config accessors and the log prefix, the remainder is identical.
Cargo.lockgains one line.winregwas already a workspace dependency and is now used to read the registry policy, so no new external dependency enters the graph.The later commits address review findings. A provision whose client timed out mid-pull left a container nobody could reach, holding the daemon's idle watchdog above zero forever — and the client's own timeout is what hid it, because the abandoned reader kept the connection open and the daemon never learned its reply had not arrived. The client now closes that connection, and a sandbox that still cannot be released after repeated attempts stops counting against the watchdog so the daemon can shut down, deleting the container when it does. The offline advice pointed at
--setup-wslc, which shares the same pull and fails identically on that host. The cache-miss path had no committed test, since the E2E preflight pre-pulls every image.✅ Checklist
Cargo.lock, thedependency-feed-checkcheck passes (see docs/pull-requests.md)📋 Issue Type
Microsoft Reviewers: Open in CodeFlow