spec: Windows runtime - #107
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nJ3scWucs71FH2T7y5oBs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nJ3scWucs71FH2T7y5oBs
The web spec already owns the w prefix; win avoids future rule-name collisions between the two specs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nJ3scWucs71FH2T7y5oBs
- cite l[job.type] instead of the nonexistent l[job] - stop claiming Executed.exit_code() holds verbatim; note the pending l[rt.executed.exit-code] restatement instead - rename runtime.capability() to rt.capability() to match BSL naming - select artifacts by config media type alone, dropping the tamanu-namespaced annotation (only Windows on x64 is supported) - point backup.v1's open question at plan Q3 instead of a nonexistent plan section - drop implementation details (extended TCP table, CreateProcessAsUser) from rule text Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nJ3scWucs71FH2T7y5oBs
- renumber open questions to start at Q1 and update references - schedule the l[rt.executed.exit-code] restatement alongside i[shell.exit], noting it is a semantic change on Linux - rename runtime.capability() to rt.capability() - clarify spec restructuring is a prerequisite for merging the implementation - mark windows-backend-research.md as superseded by the new design docs - fix list formatting in the plan and rationale Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nJ3scWucs71FH2T7y5oBs
Profiles are properties of the software, not the deployment: declared in the artifact config blob (following Docker's StopSignal precedent), overridable per-deployment from BSL, with built-in profiles for adopted native services that have no artifact. Link the three rules that used the term undefined, and add the BSL override surface to the plan's spec-restructuring workstream. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nJ3scWucs71FH2T7y5oBs
… profile-based reload - ingress (Caddy) and the resolver (CoreDNS) become special services: runtime-installed native SCM services with built-in profiles, bound directly rather than through the workload relay model, exempt from the identity GC sweep, replacing the Linux proxy and resolver infrastructure container rules - v1 drops blue/green: deploys and health-driven replacement are stop-then-start of the single instance, with the supervisor-held listener bounding disruption; recorded in non-goals and rationale - reload is a process-profile property, not a node capability: SIGHUP maps to the profile's declared reload event, and signal:reload leaves the capability vocabulary - enumerate WFP allows for ingress route targets and resolver access Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nJ3scWucs71FH2T7y5oBs
…ent, profile-based reload - ingress (Caddy) and the resolver (CoreDNS) become special services: runtime-installed native SCM services with built-in profiles, holding their own binds, exempt from the identity GC sweep, replacing the Linux proxy and resolver infrastructure container rules - special services hold their own binds so no Seedling process sits in their data path; the recorded trade is stop-then-start binary upgrades, with config changes applying via graceful reload - workload deploys keep zero-downtime replacement, mediated by the supervisor relay: new generation alongside old, relay target switch, service-address bind never dropped - reload is a process-profile property, not a node capability: SIGHUP maps to the profile's declared reload event, and signal:reload leaves the capability vocabulary - enumerate WFP allows for ingress route targets and resolver access Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nJ3scWucs71FH2T7y5oBs
…text parity - extend the WFP default-deny to the bind/listen layers so Seedling prefix addresses cannot be squatted, and enumerate the bind allows per principal (supervisor, workload, special service) - authenticate the daemon-supervisor pipe: creation ACL on the supervisor's SID, and the daemon verifies the pipe server against the supervisor record's PID and start time before trusting it - rt.exec commands run under the workload's stripped token with the full rendered instance environment, matching Linux exec-inherits- container semantics Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nJ3scWucs71FH2T7y5oBs
…nal-ladder questions seedlingd runs under its own virtual service account with its SID on the standard volume ACLs, which also resolves the backup engine's volume access; it ships as a single self-contained binary with no installer or package-manager distribution; and the ladder-only rt.signal mapping moves from open question to settled. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nJ3scWucs71FH2T7y5oBs
Companion to threat-model.md covering what changes when workloads are native processes: the discretionary identity-based containment model, the defended boundaries (mount-graph WFP, address squatting, file ACLs, privilege creep, pipe impersonation, attach verification, identity residue), the accepted exposures (administrators, ambient read surface, Defender exclusions, shared workload/supervisor SID), and the governance ledger for deployment security reviews. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nJ3scWucs71FH2T7y5oBs
One file per spike (A-E) with the spec rules and open questions at stake, concrete experiments, exit criteria, and retreat positions; the main plan's spike section now links them instead of duplicating. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nJ3scWucs71FH2T7y5oBs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nJ3scWucs71FH2T7y5oBs
One binary per spike (A-E) in a new Windows-only crate: - spike-a-stop: Job Object + CTRL_BREAK delivery, exit-code synthesis, sc.exe dispatch-latency loop - spike-b-conpty: pseudoconsole spawn, output pump, resize; QUIC bridge and empty-stderr contract stubbed as the follow-up - spike-c-net: loopback aliases/skipassource, NRPT, extended-TCP-table bind probe; WFP install left as a named FWPM follow-up - spike-d-vhdx: read-only attach with before/after digest to prove the image is untouched - spike-e-identity: restricted-token spawn with privilege-count check, sc.exe virtual-account and ghost checks, icacls inheritance break Each binary is cfg-gated to a stub main off Windows so the workspace stays green on Linux; heavy control-plane steps shell out to sc.exe/netsh/PowerShell/icacls, and sequences too large to draft without a Windows target are marked as explicit follow-ups. Added a Windows CI job scoped to the crate (fmt/clippy/test) since the daemon crates are Linux-only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nJ3scWucs71FH2T7y5oBs
Fixes surfaced by the Windows CI job: BOOL lives in windows::core, not Foundation; SetConsoleCtrlHandler/GetExtendedTcpTable take plain bool; CreateProcessW/CreateProcessAsUserW lpcommandline is Option<PWSTR>; GetVirtualDiskPhysicalPath takes PWSTR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nJ3scWucs71FH2T7y5oBs
- InitializeProcThreadAttributeList takes Option-wrapped list and flags - CreateProcessW/CreateProcessAsUserW binherithandles is plain bool - pass the ConPTY output handle across the pump thread as isize (HANDLE is not Send) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nJ3scWucs71FH2T7y5oBs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nJ3scWucs71FH2T7y5oBs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nJ3scWucs71FH2T7y5oBs
Alternative to the process-native Windows runtime draft: run each workload in a process-isolated Windows container composed just-in-time from a build-matching Microsoft OS base and the workload's VHDX artifact, obtaining isolation, addressing, and mount-graph enforcement from the container boundary (HCS compute system + HNS compartment) rather than reconstructing them out of host primitives. - docs/spec/runtime-windows-containers.md: draft spec, `wc[...]` namespace. Replaces the process-native infrastructure (Job Objects, per-instance SCM services/virtual-account SIDs, loopback aliasing, WFP, supervisor relay) with JIT layer composition, per-compartment fabric endpoints, direct-dial mounts, and mandatory namespace-boundary enforcement. The supervisor keeps exit-policy, log capture, and the stop ladder but carries no data plane. - docs/plans/windows-runtime-containers.md: companion plan — shared and divergent workstreams, open questions, spikes, rollout, cost ledger. - Registered runtime-windows-containers in .config/tracey/config.styx. No application is special-cased: a database is an ordinary workload whose base-image family is an artifact concern; migrating existing field installs is an operations concern, not runtime behaviour. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D5LfrU3ZM4FJ5fMF91wBKh
Code Coverage OverviewLanguages: TypeScript, Rust TypeScript / code-coverage/vitestThe overall coverage in commit 02cab4f in the Rust / code-coverage/rustThe overall coverage in commit 02cab4f in the Show a code coverage summary of the most impacted files.
Updated |
Rewrite the spec to describe only what the Windows container runtime does, delegating unchanged behaviour to the core specs: - Drop every comparison to the sibling Windows spec and every reference to the plan document; state rules positively rather than as absences, non-goals, or future/"v1" framing; remove the threat-model/"honesty" and backup material that belong elsewhere. - One runtime-chosen base pulled once per host build, no mirror and no per-artifact base; composition happens at image-preparation time, with a scratch layer stacked per instance start. - Lean pod: owns its container's lifetime and restart policy, holds events in memory and hands them to seedlingd on reconnect; a gone pod is reconciled like any observed exit. Logging and durable records live in seedlingd. - Workloads bind all interfaces inside their compartment (no injected bind address); service-address and mount reachability follow the portable dataplane rules realised at the compartment boundary. - Infrastructure services (ingress, resolver) run as runtime-managed containers binding the host's configured ports; no hardcoded ports. - Drop per-instance host accounts and the volume ACL dance; container mappings are the isolation. Capabilities lists only Windows-declared values. Bring the plan into line with these decisions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D5LfrU3ZM4FJ5fMF91wBKh
…s on containerd Spec: - Artifacts are ordinary OCI images built without a base image, carrying only the workload's own layers; composition stacks the runtime's base beneath them into a runnable chain. Replaces the filesystem-blob framing, gaining layer dedup, incremental pulls, and standard registry tooling. Drop the bespoke verify rule (standard OCI pull) and the root-directory indirection. - Infrastructure services are torn down when no workload requires them, so an idle host runs no infrastructure containers. Plan: - Settle the implementation on containerd + the runhcs shim + the Windows snapshotter, chosen on the "seedlingd restarts without losing workloads" axis: the shim is the per-instance supervisor and survives daemon restarts by construction. containerd is a seedling-managed infra dependency (lowest in the dependency graph), started on demand and stopped when the world is empty, so idle steady-state is one daemon. Restack via a synthesised merged image descriptor; networking stays seedling-owned over HNS. - Firm up Spike A as the decider (Windows shim-reconnect across a containerd restart) with the hand-rolled Compute*-API fallback priced in the same spike. Record the base-less artifact format and its two producer paths. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D5LfrU3ZM4FJ5fMF91wBKh
Thread the settled implementation into the spec, following the Linux runtime's precedent of naming its engine: - wc[engine]: each instance runs as a process-isolated container through containerd and its runhcs shim; containerd is a runtime-managed infrastructure dependency, started ahead of what needs it and stopped when no workload remains, so an idle host runs only seedlingd. - wc[shim]: the runhcs shim supervises the container and survives containerd and seedlingd restarts; a restarting containerd re-attaches to its shims. - wc[reconnect]: on restart seedlingd reconnects to containerd and reconciles the state and exits it reports (replaces the hand-rolled pod event/identity rules, which containerd now owns). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D5LfrU3ZM4FJ5fMF91wBKh
Rename the rule namespace from wc[...] to wcr[...] across the spec and plan, including all cross-reference anchors, and fix the plan's stale wcr[pod] reference (the rule is wcr[shim]). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D5LfrU3ZM4FJ5fMF91wBKh
Tracey requires rule ids to have at least one dot. Rename the ten single-segment ids: platform->platform.floor, base->base.image, compose->compose.chain, engine->engine.lifecycle, container->container.model, shim->shim.ownership, reconnect->daemon.reconnect, volume->volume.model, capability->capability.map, artifact->artifact.format; update anchors and the plan's references accordingly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D5LfrU3ZM4FJ5fMF91wBKh
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 15 changed files in this pull request and generated no new comments.
Suppressed comments (3)
crates/spikes/src/bin/spike_host_fs.rs:167
ctr run --mounthere uses Linux bind-propagation flags (options=rbind:rw). For Windows process-isolated containers those options aren’t supported, so the mount is likely to fail (breaking Q1’s container-side access experiments). Use a Windows-compatible mount option set (e.g. justrw) or omitoptionsentirely.
let mount = format!(
"type=bind,src={},dst={},options=rbind:rw",
dir.display(),
GUEST_MOUNT
);
crates/spikes/src/bin/spike_host_fs.rs:285
try_external_opsrenames the segment and then immediately tries to delete the renamed path. If the delete succeeds, the later check that readsrenamedto see whether appends followed the rename becomes impossible and can produce a misleading “did not follow” conclusion. Don’t delete the renamed file before the append-following check; handle delete as a separate sub-experiment if needed.
fn try_external_ops(path: &Path, renamed: &Path) -> (bool, bool) {
let rename_ok = fs::rename(path, renamed).is_ok();
// Delete whichever name currently exists.
let target = if rename_ok { renamed } else { path };
let delete_ok = fs::remove_file(target).is_ok();
(rename_ok, delete_ok)
crates/spikes/src/bin/spike_containerd.rs:131
- The harness step numbering doesn’t match the plan it claims to mirror: the plan’s experiment 2 includes recording the PID/shim, but the harness prints that as step 3, shifting subsequent headings by +1 (plan 3→harness 4, etc.). This makes it harder to paste harness output back into
docs/plans/windows-spike-containerd.mdverbatim. Consider folding the PID/shim recording under step 2 and renumbering later steps to match the plan.
step(2, "Start a long-lived process-isolated container");
cleanup();
let (pull_ok, pull) = sh("ctr", &["image", "pull", &image])?;
if !pull_ok {
observe(pull);
observe(
"pull failed; a base matching the host build must be available (wcr[base.image])",
);
return Ok(());
}
let (run_ok, run_out) = sh(
"ctr",
&["run", "-d", &image, CONTAINER_ID, "ping", "-t", "127.0.0.1"],
)?;
observe(run_out);
if !run_ok {
observe("container failed to start; check the base tag matches the host build");
return Ok(());
}
step(3, "Record the workload PID and its shim");
let (_, tasks) = sh("ctr", &["task", "ls"])?;
observe(&tasks);
A threat model written against an unbuilt runtime describes intentions rather than boundaries, and several of the boundaries here depend on spike answers that have not come back. Deferring it is right; losing the reasoning that accumulated while the specs were written is not. Record the deferral, the trigger for writing it, the confinement claims that need examining plainly — process isolation is not a security boundary, volume contents are not confidential from the host, host permissions cannot separate instances, the base image sits under every workload — what changed from the deleted process-native model, and the four questions it cannot be written without. One of those was a gap rather than an unknown: whether a container-admin workload can alter its own compartment policy decides whether wcr[net.dataplane]'s central claim holds, and S3 was not asking. It is now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CgdfXBAnSEj5s24gbJyXHi
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated no new comments.
Suppressed comments (2)
crates/spikes/src/bin/spike_containerd.rs:151
- After
sc stop containerd, the harness immediately declares containerd "down" and checks the workload PID.sc stopcan return while the service is still STOP_PENDING, so this can produce a false PASS without ever observing a full daemon stop/restart. Poll until the service is actually STOPPED (and confirmctrcannot connect) before evaluating the survival property.
let (_, stop) = sh("sc", &["stop", SERVICE])?;
observe(stop);
observe(
"containerd is down; ctr can no longer reach it, but the container must keep running",
);
crates/spikes/src/bin/spike_host_fs.rs:167
ctr --mountusesoptions=rbind:rw, butrbindis a Linux-specific recursive bind option and is not supported for Windows containers. This can make the container-side ACL experiments fail for reasons unrelated to the ACL (mount parse failure / unsupported option). Use a plain read-write bind mount option instead.
let mount = format!(
"type=bind,src={},dst={},options=rbind:rw",
dir.display(),
GUEST_MOUNT
);
…lings The event sender is global and the test binary runs tests in parallel, so the drain loop sees FaultCleared events from any test that clears a fault — and eleven of them do. The loop's Ok(_) arm skips other event kinds but not other tests' FaultCleared, so the first one to arrive was asserted against this test's id. Two unrelated uuids, intermittently. Guard the arm on the id so the loop skips what its comment always said it skipped. Observed failing on CI; passes on other branches with identical code, which is the flake. Also run the Windows spike tests with --nocapture: some of the S6 tests answer open spike questions by recording rather than asserting, and captured output threw the answer away. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CgdfXBAnSEj5s24gbJyXHi
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 17 changed files in this pull request and generated no new comments.
Suppressed comments (3)
crates/spikes/src/bin/spike_host_fs.rs:215
- In the Q1 negative-control setup, the
icaclsresult is also ignored. If that command fails, the “CONTROL FAILED” outcome can be a false positive (directory never actually confined), and the spike won’t surface the real root cause. Check the exit status and fail early on non-zero.
let (_, out) = sh(
"icacls",
&[
&path,
"/inheritance:r",
crates/spikes/src/bin/spike_host_fs.rs:152
q1_applyignores theicaclsexit status for both the DACL write and the readback. Ificaclsfails (non-elevated run, bad args, etc), the spike can proceed and interpret default/inherited ACLs as evidence, which makes experiment 3+ results unreliable. Capture and check the success flag, and fail the step if it’s false.
This issue also appears on line 211 of the same file.
let (_, out) = sh(
"icacls",
&[
&path,
"/inheritance:r",
crates/spikes/src/bin/spike_host_fs.rs:346
try_external_opsattempts rename and then delete. When both succeed,fs::read_to_string(&renamed)will fail andfollowedbecomesfalse, which is then interpreted as “appends did not follow the rename”. That’s a false conclusion because the file may simply have been unlinked. Avoid inferring “not followed” in the delete-ok case.
if rename_ok {
let followed = fs::read_to_string(&renamed)
.map(|s| s.contains("\"seq\":2"))
.unwrap_or(false);
record("appends followed the file to its new name", followed);
Three assertions held on a Server 2022 runner, so the premises behind wlog[retain.rollover], wlog[read.follow] and wcr[identity.workload] are confirmed rather than assumed. The recorded question came back negative — icacls refused a literal container SID — but that cannot be attributed. icacls was chosen over the ACL API to avoid binding code that could not be compiled at the time, and the cost is that a refusal could equally be the host rejecting an unresolvable principal, icacls refusing what the API would accept, the Containers feature being absent on the runner, or wrong SID constants. Note the limitation and point the next attempt at the API on a host with containers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CgdfXBAnSEj5s24gbJyXHi
|
Carrying over the "What Windows inherits" section from the restart-accounting plan, which #134 implements and deletes. Nothing in-tree would otherwise hold this, and it is guidance for this PR rather than that one. Restart accounting landed in #134: seedling records every container restart per instance with its exit status and a The spec rules are deliberately platform-neutral, so this runtime inherits them:
What changes for
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 17 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
crates/spikes/src/bin/spike_host_fs.rs:335
- This "name reusable" probe uses
open_writer()which setscreate(true)and can succeed by opening an existing file, so it doesn’t actually prove the original name is free for a new file. That can mis-record reuse semantics and make the spike’s conclusion unreliable.
let name_reusable = open_writer(&seg, FILE_SHARE_READ | FILE_SHARE_WRITE).is_ok();
record(
"original name reusable while the handle is open",
name_reusable,
);
crates/spikes/src/bin/spike_host_fs.rs:346
- When
try_external_opssucceeds in both renaming and deleting,renamedno longer exists, but the code still runsread_to_string(&renamed)and recordsfollowed = false. That mixes up “writer didn’t follow rename” with “file was deleted”, so the recorded observation is ambiguous.
if rename_ok {
let followed = fs::read_to_string(&renamed)
.map(|s| s.contains("\"seq\":2"))
.unwrap_or(false);
record("appends followed the file to its new name", followed);
| - name: cargo fmt | ||
| run: cargo fmt -p seedling-spikes --check |
wcr[shim.ownership] claimed the runhcs shim restarts the workload per policy. It does not — containerd has no restart policy and restart is a client concern — and with restart accounting landed, the portable spec now places crash-loop detection in the reconciler outright. Drop the clause and say where restart actually lives. Three Windows-specific consequences come with it. Restarts are recorded at the point the reconciler actions them, so the counter inference the Linux runtime needs is not needed here — and the recorded cause is recovery versus deliberate, not who acted, because seedlingd performs both kinds and an actor-shaped field would leave the crash-loop rate counting nothing. A terminated container's exit status must be read before its record is reaped, or the record is a tally with nothing to diagnose from. And a workload that exits while seedlingd is down stays down until it returns, which is a stated property of owning restart in the control plane rather than something to be found later. r[autonomous.restart.record] and r[autonomous.restart.rate] land with #134; the references resolve once both are on main. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CgdfXBAnSEj5s24gbJyXHi
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 17 changed files in this pull request and generated no new comments.
Suppressed comments (1)
crates/spikes/src/bin/spike_host_fs.rs:346
- The Q2 “original name reusable while the handle is open” probe is currently misleading:
open_writer()usescreate(true)so it can succeed even when neither rename nor delete worked (the name was never freed). Also, if both rename and delete succeed, the subsequentread_to_string(renamed)will fail and incorrectly report that appends didn’t follow the rename.
let name_reusable = open_writer(&seg, FILE_SHARE_READ | FILE_SHARE_WRITE).is_ok();
record(
"original name reusable while the handle is open",
name_reusable,
);
Split into #135 against main. It is unrelated to the Windows runtime and fixes a flake that can hit any branch, so it should not wait on this one. The --nocapture change that shared its commit stays: it belongs here, since it is what makes the S6 spike tests' recorded observations reach the log. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CgdfXBAnSEj5s24gbJyXHi
| let mount = format!( | ||
| "type=bind,src={},dst={},options=rbind:rw", | ||
| dir.display(), | ||
| GUEST_MOUNT | ||
| ); |
Main bumped the workspace to 0.8.0; seedling-spikes exists only on this branch, so its lockfile entry stayed at 0.7.2 and failed CI's --locked builds. Same shape as the previous merge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CgdfXBAnSEj5s24gbJyXHi
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated no new comments.
Suppressed comments (2)
crates/spikes/src/bin/spike_host_fs.rs:167
- The mount spec uses
options=rbind:rw, butrbindis a Linux-only recursive bind option and is not meaningful/supported for Windows container mounts. This can causectr runto fail on Windows, preventing the Q1 container-side access checks from running.
let mount = format!(
"type=bind,src={},dst={},options=rbind:rw",
dir.display(),
GUEST_MOUNT
);
crates/spikes/src/bin/spike_host_fs.rs:145
- In
q1_apply, theicaclscall’s success flag is discarded. Ificaclsfails (not elevated, missing feature, etc.), the harness can incorrectly proceed and potentially treat an error message as a persisted ACE, producing misleading spike results. Capture theokboolean and fail fast when it’s false (same for the subsequenticaclsread-back).
let (_, out) = sh(
"icacls",
&[
&path,
"/inheritance:r",
Establishes the Seedling Windows runtime — a second runtime implementation targeting Windows Server hosts, speaking the same operator interface and language specs so the CLI, web UI, and protocol crates carry over. Conforms to the portable runtime spec (reconciliation, generations, lifecycle, barriers, history, faults, scheduling) and defines the Windows infrastructure those semantics run on. Rule IDs use the
wcr[...]namespace, and the log store's usewlog[...].Design
Each workload runs as a process-isolated Windows container under containerd + the runhcs shim. The artifact is a base-less OCI image (
FROM scratch) completed at prepare time by stacking a single runtime-pulled OS base beneath its layers. Networking is per-instance HNS compartments with the mount graph enforced at the compartment boundary. containerd is a seedling-managed infrastructure dependency, started on demand and stopped when the host is idle — so an idle host runs only seedlingd.The design rests on the property that seedlingd can restart without losing workloads: the runhcs shim owns the container and outlives containerd, and a restarting containerd re-attaches to it. That's what lets containerd be a restartable managed dependency rather than an always-on peer.
Decisions settled in review:
FROM scratchartifact — MCR now ships distributable base layers, so "buildFROM mcrand omit the base on push" doesn't hold; the base-less path also drops any host-build↔base-tag coupling.Labels(au.bes.seedling.stop-method,au.bes.seedling.reload-event).wcr[shim.ownership]does not claim it does. Restarts are recorded at the point the reconciler actions them, their cause is recovery-versus-deliberate rather than which component acted, a terminated container's exit status is read before its record is reaped, and the daemon-down gap is stated as a property rather than left to be found.Contents
Specs
docs/spec/runtime-windows-containers.md— the runtime spec, 35wcr[...]rules: platform floor, base image and composition, containers, restarts, networking, infrastructure services, identity, volumes, logs, shutdown and signals, capabilities, actions and shells, artifacts.docs/spec/runtime-windows-logs.md— the log store, 20wlog[...]rules. Separate because Windows has no journald and the semantics are precise enough to be lost inside the runtime spec; the runtime spec cites one rule, so the log engine can be replaced without touching it.Plans
docs/plans/windows-runtime-containers.md— implementation approach (containerd versus hand-rolling the compute plane on the Compute* APIs), the base-less producer decision and the two alternatives it rules out, open questions Q1–Q4, spikes S1–S6, rollout, cost ledger.docs/plans/windows-log-store.md— thebinary-v2logging driver and whynpipeandfileeach fail on daemon-independence or retention, the tree layout, numbered segments, the rotation split, and what is deferred.docs/plans/windows-threat-model.md— why the threat model is deferred until the spikes answer and a workload runs, and what it must cover when written.Spikes
docs/plans/windows-spike-containerd.md+spike-containerd— S1, the decider: does a container survive a full containerd restart. Drives the sequence viactr/scand checks workload liveness directly by PID, independent of containerd.docs/plans/windows-spike-host-filesystem.md+spike-host-fs— S6: whether a host ACL can name a container account's SID, and what a second process can do to a file a writer holds open. The container-free half runs as tests in the Windows CI job; three assert properties the design rests on, and they pass.Infrastructure
.github/workflows/rust.yml— Windows CI job, not in the required set since it builds throwaway harnesses..config/tracey/config.styx— both Windows specs registered. Coverage is deliberately red: the specs describe an unbuilt runtime, so partial coverage is the accurate report and converges as implementation lands. The spikes crate is excluded from the impl globs so its mentions of rule ids do not read as coverage.History
A process-native design (Job Objects, per-instance SCM services + virtual-account SIDs, loopback aliasing + WFP, a supervisor byte-relay, VHDX artifacts) was explored first and then removed as superseded — its analysis and spikes live in the history of the closed #106. What transferred: the Windows Server 2019+/x64 floor, the ConPTY empty-stderr caveat, and the habit of stating a weakened property plainly rather than eliding it.
Supersedes #105 and #106 (both closed).
🤖 Generated with Claude Code
https://claude.ai/code/session_01D5LfrU3ZM4FJ5fMF91wBKh