Skip to content

spec: Windows runtime - #107

Open
passcod wants to merge 52 commits into
mainfrom
claude/seedling-oci-vhdx-layering-7xh5f0
Open

spec: Windows runtime#107
passcod wants to merge 52 commits into
mainfrom
claude/seedling-oci-vhdx-layering-7xh5f0

Conversation

@passcod

@passcod passcod commented Jul 11, 2026

Copy link
Copy Markdown
Member

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 use wlog[...].

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:

  • Base-less FROM scratch artifact — MCR now ships distributable base layers, so "build FROM mcr and omit the base on push" doesn't hold; the base-less path also drops any host-build↔base-tag coupling.
  • Process profile carried in the OCI config's Labels (au.bes.seedling.stop-method, au.bes.seedling.reload-event).
  • Integrity via the standard OCI layer digest — no bespoke pre-attach verification.
  • The reconciler owns restart. containerd has no restart policy and the shim does not restart tasks, so 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.
  • Confinement is the mapping, not the permissions. A container's account is not a host principal, so a volume's host permissions cannot separate instances and a mapped volume is not confidential from the host. Both are stated in the spec rather than implied away.

Contents

Specs

  • docs/spec/runtime-windows-containers.md — the runtime spec, 35 wcr[...] 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, 20 wlog[...] 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 — the binary-v2 logging driver and why npipe and file each 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 via ctr/sc and 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

passcod and others added 22 commits July 11, 2026 00:31
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
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
@passcod
passcod changed the base branch from main to claude/pr-105-spec-tracey-config-70jsvh July 11, 2026 14:02
@github-code-quality

github-code-quality Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Overview

Languages: TypeScript, Rust

TypeScript / code-coverage/vitest

The overall coverage in commit 02cab4f in the claude/seedling-oci-... branch remains at 66%, unchanged from commit 6f27fa0 in the main branch.

Rust / code-coverage/rust

The overall coverage in commit 02cab4f in the claude/seedling-oci-... branch remains at 58%, unchanged from commit 6f27fa0 in the main branch.

Show a code coverage summary of the most impacted files.
File main 6f27fa0 claude/seedling-oci-... 02cab4f +/-
crates/core/src/oi/server.rs 60% 59% -1%
crates/spikes/s...e_containerd.rs 0% 0% 0%
crates/spikes/s...pike_host_fs.rs 0% 0% 0%
crates/spikes/src/lib.rs 0% 0% 0%

Updated August 01, 2026 22:59 UTC

Comment thread docs/spec/runtime-windows-containers.md Outdated
Comment thread docs/spec/runtime-windows-containers.md Outdated
Comment thread docs/spec/runtime-windows-containers.md Outdated
Comment thread docs/spec/runtime-windows-containers.md Outdated
Comment thread docs/spec/runtime-windows-containers.md Outdated
Comment thread docs/spec/runtime-windows-containers.md Outdated
Comment thread docs/spec/runtime-windows-containers.md Outdated
Comment thread docs/spec/runtime-windows-containers.md Outdated
Comment thread docs/spec/runtime-windows-containers.md Outdated
Comment thread docs/spec/runtime-windows-containers.md Outdated
claude added 5 commits July 12, 2026 00:49
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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 --mount here 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. just rw) or omit options entirely.
        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_ops renames the segment and then immediately tries to delete the renamed path. If the delete succeeds, the later check that reads renamed to 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.md verbatim. 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
Copilot AI review requested due to automatic review settings August 1, 2026 08:27

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 stop can 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 confirm ctr cannot 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 --mount uses options=rbind:rw, but rbind is 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
Copilot AI review requested due to automatic review settings August 1, 2026 08:31

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 icacls result 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_apply ignores the icacls exit status for both the DACL write and the readback. If icacls fails (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_ops attempts rename and then delete. When both succeed, fs::read_to_string(&renamed) will fail and followed becomes false, 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
Copilot AI review requested due to automatic review settings August 1, 2026 19:23

passcod commented Aug 1, 2026

Copy link
Copy Markdown
Member Author

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 cause of recovery or deliberate, and files crash_loop when the recovery rate reaches an operator-settable threshold within a window. systemd's start limit is demoted to a secondary trigger.

The spec rules are deliberately platform-neutral, so this runtime inherits them:

  • r[autonomous.restart.record] — record every restart, its exit status where the platform reports one, and its cause.
  • r[autonomous.restart.rate]crash_loop is a function of the recorded recovery-restart rate.
  • r[autonomous.restart.rate.settings] — the threshold and window are operator-settable.
  • r[gc.restarts] — records bounded per instance (default 50), dropped with their instance.
  • r[autonomous.restart.backoff] — now scoped to "where the platform's supervisor actions restarts", so it does not bind here.

What changes for wcr[shim.ownership]

It drops its restart clause. The reconciler owns restart, pacing, and the start limit, and records each attempt at the point it actions one — no counter inference needed, which is the awkward part of the Linux implementation and the part this runtime gets to skip.

Two Windows-specific requirements come with it:

  • The exit observation must be folded into history before the exited task is reaped. containerd requires deletion before the container ID is reusable, so the exit status is gone once the task is deleted — it has to be read and recorded first, or the records are a tally with no diagnostic value.
  • The daemon-down gap should be stated as a property rather than left to be discovered. A workload that crashes while seedlingd is down stays down until it returns, bounded by SCM restart. On Linux systemd covers that gap; here nothing does.

One thing to be careful of

cause is recovery | deliberate — why the restart happened, not who performed it. That distinction exists because of this runtime: an earlier revision of #134 had initiator: supervisor | runtime, which works on Linux only because systemd happens to action recovery there. Here seedlingd actions both kinds, so an actor-shaped field would mark every restart runtime, leave the rate counting nothing, and kill the primary crash-loop trigger — with no failed/start-limit-hit equivalent to fall back on.

So when this runtime records a restart, the question to answer is whether the workload exited unexpectedly (recovery) or whether seedlingd restarted it on purpose (deliberate), regardless of the fact that seedlingd performed both.


Generated by Claude Code

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 sets create(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_ops succeeds in both renaming and deleting, renamed no longer exists, but the code still runs read_to_string(&renamed) and records followed = 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);

Comment on lines +79 to +80
- 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
Copilot AI review requested due to automatic review settings August 1, 2026 19:27

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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() uses create(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 subsequent read_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
Copilot AI review requested due to automatic review settings August 1, 2026 21:54

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated 1 comment.

Comment on lines +163 to +167
let mount = format!(
"type=bind,src={},dst={},options=rbind:rw",
dir.display(),
GUEST_MOUNT
);
claude added 2 commits August 1, 2026 22:56
Brings in restart accounting (#134), so wcr[restart.ownership] and
wcr[restart.record]'s references to r[autonomous.restart.record] and
r[autonomous.restart.rate] resolve within the branch, and the fault-cleared
test fix (#135).
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
Copilot AI review requested due to automatic review settings August 1, 2026 22:57

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, but rbind is a Linux-only recursive bind option and is not meaningful/supported for Windows container mounts. This can cause ctr run to 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, the icacls call’s success flag is discarded. If icacls fails (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 the ok boolean and fail fast when it’s false (same for the subsequent icacls read-back).
        let (_, out) = sh(
            "icacls",
            &[
                &path,
                "/inheritance:r",

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants