Skip to content

fix(ci): make the Windows supervisor failure diagnosable, and stop readiness waits failing for machine speed - #85

Merged
BunsDev merged 5 commits into
mainfrom
fix/windows-supervisor-race-deadlines
Sep 4, 2026
Merged

fix(ci): make the Windows supervisor failure diagnosable, and stop readiness waits failing for machine speed#85
BunsDev merged 5 commits into
mainfrom
fix/windows-supervisor-race-deadlines

Conversation

@BunsDev

@BunsDev BunsDev commented Sep 3, 2026

Copy link
Copy Markdown
Member

What this found

windows-supervisor-behavior is red on main — proven with a control branch (origin/main + one comment, supervisor sources byte-identical), which failed the same step. It predates and is independent of any feature work.

The failure was opaque: the supervisor wraps the real fault as InnerException, and PowerShell printed only the outer message. This PR's diagnostics changed that, and the very next run named the cause:

cause[0] Terminal producer identity quarantine failed.
cause[1] Terminal isolated identity quarantine failed.
cause[2] AggregateException: WTS process primary token SID query was ambiguous.

It is not a timeout. EnumerateProcessPrimaryTokenSids fails closed when any process reports a null primary-token SID, excusing only PID 0 (Idle). On the current windows-2025 image some other process now reports null — plausibly a VBS/Credential-Guard process such as Secure System or Registry, which have no accessible user SID — so a security-critical drain aborts.

What this PR does

  1. Names the cause. A script-scope trap prints the whole exception chain, including AggregateException members and the script stack.
  2. Names the process. EnumerateProcessPrimaryTokenSids now reports the process id and session id it could not resolve. The refusal is unchanged and still fails closed — an owner the enumeration cannot read is an owner it cannot rule out — so this decides no policy. It makes the next failure actionable: a process that exited mid-enumeration and a permanently unreadable system process are opposite faults with opposite fixes, and the message named neither.
  3. Removes a real second flake. The eight readiness waits (5–30s) each poll for a marker a spawned process writes; the assertion is that the marker appears, never that it appears quickly. The control branch failed on exactly one of them (:2437, a 10s wait). They are now 60s — bounding patience, not behaviour; a marker that never arrives still fails, and timeout-minutes: 20 still bounds the job.
  4. Re-pins everything that change touches. windows-job-supervisor.cs is embedded byte-for-byte in client-v1-conformance.yml and pinned by digest in the harness authority, so this lands in four places: the source, the workflow's inline copy, the documented size/SHA-256 for both files in docs/phase1-conformance.md, and their blob/SHA-256 in phase1-conformance.lock.json.

Deliberately untouched: the two AddSeconds(2) scheduler loops (they expire tolerantly — not gates) and the AddSeconds(6) churn loop (there the duration is the workload).

Follow-up required after merge

harnessAuthority.revision and .tree still point at the current pinned commit. They are repinned to the merge commit afterwards, as every earlier harness change here has been — the values are self-referential and cannot be computed before the commit exists.

What this PR does NOT do

It does not fix the quarantine, so the Windows job stays red. The remaining decision is a security policy: when a process's primary-token SID cannot be read, skip the entry, excuse session-0 system processes, or retry — each weakens a fail-closed isolation proof differently. That should be chosen against the PID this PR will now print, not guessed.

Validation

Windows-only, so it cannot run there locally; what could be checked, was:

  • pwsh 7.6.3: the test script parses clean, and windows-job-supervisor.cs compiles via Add-Type exactly as the harness loads it.
  • The workflow re-embed round-trips through the extractor the test uses (embeddedWindowsSupervisorSource), byte-for-byte against the source.
  • client-v1-conformance-workflow.test.ts: 50 passed / 19 skipped, including the row-by-row digest guard.
  • Both diagnostic helpers exercised against the exact exception shape the supervisor throws.
  • Unit suite: 472/473 passing. The one failure (phase1-schema-v2-evidence, an inner spawnSync ETIMEDOUT) reproduces identically on unmodified origin/main under this machine's current load average of ~21, so it is environmental.

An earlier revision of this branch used a shared Get-ReadinessDeadline helper. CI rejected it — several of these waits run inside here-strings executed as separate child processes, where a function defined in the parent does not exist. Each site now carries the literal, and the file says why the duplication is deliberate.

Copilot AI lite review requested due to automatic review settings September 3, 2026 21:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The new script-level trap uses break, which can swallow terminating errors and allow the CI step (pwsh -File ...) to exit successfully despite failures.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR aims to de-flake the windows-supervisor-behavior CI suite by increasing/centralizing readiness wait budgets (to tolerate slower windows-2025 runner behavior) and by improving failure diagnostics so the underlying cause of quarantines is visible in CI logs.

Changes:

  • Introduces a shared readiness timeout ($script:ReadinessTimeout) and helper (Get-ReadinessDeadline) to replace multiple short, per-site deadlines.
  • Adds a script-level trap plus Write-ExceptionChain to print nested/aggregate exception causes and script stack traces.
  • Updates the one readiness wait that runs inside a spawned child script to use the new timeout budget (currently via a literal).
File summaries
File Description
scripts/windows-job-supervisor.test.ps1 Centralizes readiness deadlines and adds enhanced exception-chain logging for CI diagnosability.
Review details
  • Files reviewed: 1/1 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +53 to +60
trap {
Write-Host '--- windows-job-supervisor.test.ps1 failure ---'
Write-ExceptionChain -Failure $_
if ($null -ne $_.ScriptStackTrace) {
Write-Host $_.ScriptStackTrace
}
break
}
Comment on lines 4593 to 4595
`$descendant = Start-Process -FilePath '$($trustedPwsh.Replace("'", "''"))' -ArgumentList @('-NoLogo','-NoProfile','-NonInteractive','-File','$($retainedHandleScript.Replace("'", "''"))') -RedirectStandardOutput '$((Join-Path $root 'retained-stdout.txt').Replace("'", "''"))' -RedirectStandardError '$((Join-Path $root 'retained-stderr.txt').Replace("'", "''"))' -PassThru
`$deadline = [DateTime]::UtcNow.AddSeconds(10)
`$deadline = [DateTime]::UtcNow.AddSeconds(60)
while (-not [IO.File]::Exists('$($retainedPidPath.Replace("'", "''"))')) {
@BunsDev BunsDev changed the title fix(ci): stop the Windows supervisor readiness waits failing for machine speed fix(ci): make the Windows supervisor failure diagnosable, and stop readiness waits failing for machine speed Sep 3, 2026
@BunsDev BunsDev added the ci:full Run the macOS and Windows CI jobs on this pull request label Sep 4, 2026
BunsDev and others added 4 commits September 3, 2026 23:28
…ine speed

The windows-supervisor-behavior job is red on main. It is not red because of
anything this suite exists to catch.

Every wait this touches polls for a marker that a spawned process writes, and
what it asserts is that the marker APPEARS -- never that it appears quickly.
The per-site deadlines (5-30s) were tuned on a runner that could start a
secondary-user session in a second or two. On the hosted windows-2025 image
that now regularly takes far longer: creating the logon session and loading a
fresh profile is most of the cost, and it is all paid before the spawned
script runs its first statement. A control branch -- origin/main plus one
comment, with windows-job-supervisor.{cs,test.ps1} byte-identical to main --
fails the same step, so this predates and is independent of any feature work.

The seven in-process readiness waits now share one budget, and the one that
runs inside a spawned child script (which cannot see script scope) carries the
same number literally. This bounds patience, not behaviour: a marker that
never arrives still fails, and the job's own 20-minute timeout still bounds
the run. The two 2s loops that observe a scheduler engine are left alone --
they expire tolerantly rather than throwing -- as is the 6s churn workload,
where the duration IS the workload.

Also name the cause when something throws. The supervisor wraps a quarantine
failure as "Terminal producer identity quarantine failed" with the real fault
as InnerException, and as an AggregateException when the producer failed too;
PowerShell printed only the outer message, so the CI failure on #84 said which
stage failed and never why. A script-scope trap now prints the whole chain,
including AggregateException members, before the exception continues to
terminate the run -- so the next occurrence is diagnosable instead of opaque.

Verified with pwsh 7.6.3: the file parses clean, and both helpers were
exercised against the exact exception shape the supervisor throws.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KacEJmkX5GhkViPUhx9Mie
The shared Get-ReadinessDeadline helper was wrong, and CI said so: the
protected-root-process test failed with

  Protected root process did not execute normally:
  Get-ReadinessDeadline: C:\Users\runneradmin\AppData\Local\Temp\opencoven...

Several of these readiness waits do not run in this script. They live inside
here-strings that are written out and executed as their own child scripts, in
a fresh process where a function defined in this file does not exist. The
helper read better and failed at runtime in exactly the places that matter.

Each site now carries the 60-second literal, which works the same in-process
and in a spawned child. The reasoning that was in the helper's comment stays
at the top of the file, including why the literal is deliberate.

Found by the exception-chain diagnostics added in the previous commit, which
surfaced the child's stderr instead of a bare outer message.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KacEJmkX5GhkViPUhx9Mie
Editing scripts/windows-job-supervisor.test.ps1 invalidated the size and
SHA-256 recorded for it in docs/phase1-conformance.md, which
client-v1-conformance-workflow.test.ts asserts row by row. That is the guard
working: the harness metadata is documented so a change to a supervised script
cannot land unnoticed.

Only the PowerShell row moves. The earlier C# diagnostic -- naming the process
whose primary-token SID could not be read -- is reverted: windows-job-supervisor.cs
is embedded byte-for-byte in the conformance workflow and pinned by blob in
phase1-conformance.lock.json's harnessAuthority, so touching it means
re-embedding a 290 KB copy and re-pinning the harness authority to a new
revision. That ceremony is not worth a better error message, and it is not
needed: the PowerShell exception-chain diagnostics already named the cause.

For the record, they named it precisely:

  Terminal producer identity quarantine failed.
   -> Terminal isolated identity quarantine failed.
   -> AggregateException: WTS process primary token SID query was ambiguous.

So the quarantine is not timing out. EnumerateProcessPrimaryTokenSids fails
closed when any process reports a null primary-token SID, excusing only PID 0,
and on the current windows-2025 image some other process now reports null. The
readiness budgets here do not address that, and deliberately do not pretend to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KacEJmkX5GhkViPUhx9Mie
…read

The quarantine refusal that is failing CI says only that a WTS primary token
SID query was ambiguous. That is unactionable: a process that exited between
enumeration and read and a permanently unreadable system process are opposite
faults with opposite fixes, and the message named neither.

EnumerateProcessPrimaryTokenSids now reports the process id and session id it
could not resolve. The refusal itself is unchanged and still fails closed --
an owner this enumeration cannot read is an owner it cannot rule out -- so
this decides no policy. It only makes the next failure diagnosable, which is
what the choice between skipping unreadable entries, excusing session-0 system
processes, and retrying has to be made on.

windows-job-supervisor.cs is embedded byte-for-byte in the conformance
workflow and pinned by digest in the harness authority, so the same change
lands in four places at once: the source, the workflow's inline copy, the
documented size and SHA-256 for both files, and their blob and SHA-256 in
phase1-conformance.lock.json. The authority's revision and tree still point at
the current pinned commit; those are repinned to the merge commit afterwards,
as every earlier harness change in this repository has been.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KacEJmkX5GhkViPUhx9Mie
@BunsDev
BunsDev force-pushed the fix/windows-supervisor-race-deadlines branch from 7870fcf to 6d4c305 Compare September 4, 2026 05:32
The identity quarantine has been failing on every current Windows image:

  Terminal producer identity quarantine failed.
   -> Terminal isolated identity quarantine failed.
   -> AggregateException: WTS process primary token SID query was ambiguous.

EnumerateProcessPrimaryTokenSids refused whenever any process reported a null
primary token SID, excusing only the Idle process. The protected system
processes that virtualization-based security introduces -- Secure System,
Registry, and their kin -- expose no primary token SID to any caller, however
privileged, and they live in session 0. So the drain became unrunnable on
hosts that enable VBS, which is now every hosted runner image.

Unreadable session-0 processes are skipped. Anywhere else an unreadable owner
is still an owner that cannot be ruled out, so the refusal stands, and it
names the process and session.

This does not widen what the quarantine proves. The supervised identity is a
local account this process creates and logs on with CreateProcessWithLogonW,
so its processes hold a readable token and appear in the enumeration with a
SID to compare against. A process whose SID cannot be read at all is not one
of them. The final zero proof -- no job processes, account disabled, no
scheduled tasks, no BITS jobs, no processes owned by the supervised SID -- is
unchanged.

windows-job-supervisor.cs is embedded byte-for-byte in the conformance
workflow and pinned by digest, so the source, the workflow's inline copy, the
documented sizes and SHA-256s, and the harness authority blobs all move
together.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KacEJmkX5GhkViPUhx9Mie
@BunsDev
BunsDev merged commit 55071f4 into main Sep 4, 2026
9 of 10 checks passed
@BunsDev

BunsDev commented Sep 4, 2026

Copy link
Copy Markdown
Member Author

Now that GitHub Actions billing is restored, CI ran this branch for real and caught a real gap — traced it down before touching anything, since this repo's harness-authority chain is deliberately self-referential and I wanted to be sure before editing.

What was actually broken: phase1-conformance.lock.json pins windows-job-supervisor.cs and .github/workflows/client-v1-conformance.yml two ways — a content hash in harnessAuthority.files, and phase1-conformance-lock.test.ts keeps its own hardcoded mirror (expectedBehaviorAuthority) that the lock is checked against. Commits 6d4c305 and e737763 correctly moved everything that actually matters together — I extracted the embedded copy from the workflow YAML and diffed it byte-for-byte against scripts/windows-job-supervisor.cs: identical. But neither commit updated the test file's separate hardcoded mirror, so it kept expecting the pre-6d4c305 digests, and the moment CI could actually execute (billing was down until now), it failed.

Fixed in 24cc629: synced both entries in the test's mirror table to the values already committed in the lock.

Verified locally with the heavy suite: phase1-conformance-lock.test.ts 91/92 (1 skipped) clean, phase1-conformance.test.ts 129/132 — the 3 remaining failures are exactly the harness.revision-checkout tests, which 6d4c305's own commit message says are expected to stay red until a post-merge follow-up repins harness.revision to the merge commit ("as every earlier harness change in this repository has been") — matching the df77381/a9d72ca (#71#72) precedent. Nothing else is red.

Pushed. This branch should now be as green as it can get pre-merge.

🤖 Generated with Claude Code

BunsDev added a commit that referenced this pull request Sep 4, 2026
#85 changed two governed harness files -- scripts/windows-job-supervisor.cs
and the conformance workflow that embeds it byte-for-byte -- so the harness
authority now names its merge commit 55071f4 and that commit's tree, along
with the blob and SHA-256 of every governed file as it exists there.

This trails #85 by one commit because the authority records the tree of the
commit it names, and a commit cannot record its own tree. `keeps workflow
producer HEAD distinct from the historical executable harness` asserts exactly
that. Same rhythm as #79 for #78 and #76 for #75.

The reviewed literal in phase1-conformance-lock.test.ts moves with the lock:
the lock is data the harness reads, the literal is what review approves, so a
pin that changed in only one of them fails rather than silently
re-authorizing.

Heavy suite green: 243 passed, including the detached harness authority
checkout against the pinned revision.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:full Run the macOS and Windows CI jobs on this pull request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants