Cross-phase trust-boundary hardening for ci-copilot Gate (HMAC seal + fail-closed restore)#35843
Conversation
🔍 Skill Validation Results✅ Static Checks PassedSkills checked: 19 | Agents checked: 4 Full validator output❌ LLM Evaluation Failed0/1 skill(s) passed validation
❌ verify-tests-fail-without-fix: Skill regressed on task completion in one or more scenarios [NOT ACTIVATED (isolated) in 2 scenario(s)] [NOT ACTIVATED (plugin) in 3 scenario(s)] [1] High run-to-run variance (CV=1.21)
📊 verify-tests-fail-without-fix / edge-case-agent-uses-requirefullverification-when-fix-files-existJudge Report: Edge case: agent uses RequireFullVerification when fix files existBaseline JudgeOverall Score: 1/5
With-Skill Judge (Isolated)Overall Score: 1.7/5
With-Skill Judge (Plugin)Overall Score: 1/5
Baseline Agent Output
To proceed, I need:
Would you like me to:
Please provide the PR number or repo URL so I can set up the environment. With-Skill Agent Output (Isolated)
Verification blocked — no git repository found in the current working directory. To proceed, I need:
Would you like me to clone the repo and check out the PR branch first? If so, please provide the PR number. With-Skill Agent Output (Plugin)
📊 verify-tests-fail-without-fix / edge-case-no-test-files-detected-in-the-prJudge Report: Edge case: no test files detected in the PRBaseline JudgeOverall Score: 2/5
With-Skill Judge (Isolated)Overall Score: 3.7/5
With-Skill Judge (Plugin)Overall Score: 1.7/5
Baseline Agent Output
With the PR number, I can examine the fix, find relevant existing tests, and verify whether the change is properly covered by tests — or write new tests if needed. With-Skill Agent Output (Isolated)
More importantly, the skill's activation guard states it requires test files in the PR or an explicit TestFilter. You've confirmed that no test files were added to this PR — which means there are no tests to verify against the fix. Result: Verification cannot proceed ❌ The fix in
Would you like me to help write tests for this fix, or do you know of existing tests that should cover it? With-Skill Agent Output (Plugin)
With the PR number, I can check out the code and use the 📊 verify-tests-fail-without-fix / feature-agent-reports-verification-result-clearlyJudge Report: Feature: agent reports verification result clearlyBaseline JudgeOverall Score: 2/5
With-Skill Judge (Isolated)Overall Score: 4/5
With-Skill Judge (Plugin)Overall Score: 4/5
Baseline Agent Output
However, I can explain how you'd know if verification passed or failed for .NET MAUI UI tests: Verification Outcome Indicators✅ PASSED — The test catches the bug on iOS:
❌ FAILED — The test does NOT catch the bug:
How to Run It
Recommended Two-Step Verification
If both steps produce the expected result, verification passes. Would you like me to clone the repo and run this, or do you need help with something else? With-Skill Agent Output (Isolated)
How Verification Works for Your CaseSince the PR has both test files and a fix, it uses Full Verification mode ( Command to run (from the repo root):
How to interpret results:
Key principle (inverted semantics):
Output files will be at:
To proceed, I'd need the repo cloned with the PR branch checked out. Would you like me to clone it from a specific PR? With-Skill Agent Output (Plugin)
I cannot run verification because:
How You'll Know If Verification Passed or FailedWhen the environment is properly set up (PR branch checked out with both tests and fix), here's what to expect: Command to run: Results (inverted semantics!):
Reports are written to: To proceed, I'd need the actual PR branch for Issue #66666 checked out. Would you like me to fetch a specific PR? 📊 verify-tests-fail-without-fix / feature-agent-trusts-script-auto-detection-of-test-files-from-git-diffJudge Report: Feature: agent trusts script auto-detection of test files from git diffBaseline JudgeOverall Score: 1/5
With-Skill Judge (Isolated)Overall Score: 3.7/5
With-Skill Judge (Plugin)Overall Score: 3/5
Baseline Agent Output
To answer your question: No, there isn't a built-in "verification script" in the .NET MAUI repo that automatically detects which tests to run for a given PR. Here's how test verification typically works:
Would you like me to:
With-Skill Agent Output (Isolated)
To answer your question: Yes, the script auto-detects tests from the git diff — no need to specify test files manually. It examines changed file paths to determine test type and builds filters from test class names automatically. To run it, you'd need:
The With-Skill Agent Output (Plugin)
Blockers:
To answer your question: Yes, the verification script can auto-detect test files — it reads the git diff to find changed test files and automatically determines:
What you'd need to run it manually: Could you provide either a GitHub token with repo access, or let me know the PR branch name so I can fetch it directly? 📊 verify-tests-fail-without-fix / happy-path-full-verification-mode-with-fix-files-presentJudge Report: Happy path: full verification mode with fix files presentBaseline JudgeOverall Score: 1/5
With-Skill Judge (Isolated)Overall Score: 1/5
With-Skill Judge (Plugin)Overall Score: 1/5
Baseline Agent Output
The standard verification workflow would be:
Could you provide:
Without the actual codebase and build environment, I can't execute the verification. If you'd like, I can set up the repo and attempt the build — just confirm the PR number. With-Skill Agent Output (Isolated)
To proceed, I need:
Could you provide the PR number or the branch name? I'll clone the repo and re-run verification. With-Skill Agent Output (Plugin)
Verification Result: The
To run full verification, you need:
Would you like me to attempt a manual verification approach (reverting fix files by hand), or should this be run through CI instead? 📊 verify-tests-fail-without-fix / happy-path-verify-failure-only-mode-for-test-creationJudge Report: Happy path: verify failure only mode for test creationBaseline JudgeOverall Score: 1/5
With-Skill Judge (Isolated)Overall Score: 1/5
With-Skill Judge (Plugin)Overall Score: 1/5
Baseline Agent Output
Could you help me with one of these:
For Android UI tests, I'd typically:
What would you like me to do? With-Skill Agent Output (Isolated)
Could you point me to the correct directory where your MAUI repo lives, or provide the PR number so I can check it out? For example:
With-Skill Agent Output (Plugin)
Result: Blocked — the verification cannot proceed because the required shared script (
What this means for your test:
To proceed manually, you could run the UI test directly: Would you like me to try running the test directly using the host app runner, or is there another path you'd prefer? 📊 verify-tests-fail-without-fix / negative-trigger-general-test-question-should-not-invoke-verificationJudge Report: Negative trigger: general test question should not invoke verificationBaseline JudgeOverall Score: 4/5
With-Skill Judge (Isolated)Overall Score: 4/5
With-Skill Judge (Plugin)Overall Score: 4/5
Baseline Agent Output
Recommended ApproachPrefer element-based checks over
Test Structure
} Key Assertions
Tips
Use the With-Skill Agent Output (Isolated)
Test StructureUse the UI test pattern in
VerifyScreenshot vs Element-Based Checks
For scrolling bugs, prefer element-based checks because:
Key Assertions for Scrolling
// Item position changed (scrolled into view) // No crash after rapid scrolling Tips
Use With-Skill Agent Output (Plugin)
Test Structure
} VerifyScreenshot vs Element-Based Checks
For scrolling bugs, prefer element-based checks because:
Recommended Assertions
// Item position (below another element) // Item no longer visible (scrolled away) // Count visible items Tips
📊 verify-tests-fail-without-fix / regression-agent-correctly-reports-test-failure-as-verification-successJudge Report: Regression: agent correctly reports test failure as verification successBaseline JudgeOverall Score: 1/5
With-Skill Judge (Isolated)Overall Score: 5/5
With-Skill Judge (Plugin)Overall Score: 5/5
Baseline Agent Output
PR #44444 — Verification FAILED (Android)
Recommended comment on the PR:
If you have additional context (emulator/device info, target framework, specific control name), include that to help the author reproduce. With-Skill Agent Output (Isolated)
In failure-only verification, the test is supposed to fail — it confirms the test can catch the regression. Here's what to report: Verification Result: PASS (test fails as expected)
In short: test failure during failure-only verification = success. It proves the test is valid and will guard against the regression once a fix lands. With-Skill Agent Output (Plugin)
In failure-only verification, a test failure is the expected/desired outcome. It proves the test catches the bug before any fix is applied. Summary to report:
No further action needed on the test itself. The fix in PR #44444 should make this test pass. 📊 verify-tests-fail-without-fix / regression-agent-must-not-manually-revert-files-with-git-commandsJudge Report: Regression: agent must not manually revert files with git commandsBaseline JudgeOverall Score: 1.3/5
With-Skill Judge (Isolated)Overall Score: 1.7/5
With-Skill Judge (Plugin)Overall Score: 2/5
Baseline Agent Output
To verify the tests, I need either:
Could you provide:
Alternatively, if you'd like me to use the With-Skill Agent Output (Isolated)
Verification Result✅ VERIFICATION PASSED (logical analysis) The UI tests for PR #33134 correctly catch the EmptyView display bug on Android:
Note: The automated verification script couldn't run because the build infrastructure ( Also note: The user-specified filter With-Skill Agent Output (Plugin)
Verification Result: ✅ Tests correctly catch the EmptyView bug
AnalysisFix ( Test (
Why tests catch the bug:
Previous CI verification (from PR review) confirmed:
Note: Full on-device verification could not complete in this environment (no Android emulator available), but code analysis confirms the test directly exercises the buggy code path. 📊 verify-tests-fail-without-fix / regression-tests-passing-without-fix-means-verification-failedJudge Report: Regression: tests passing without fix means verification FAILEDBaseline JudgeOverall Score: 5/5
With-Skill Judge (Isolated)Overall Score: 5/5
With-Skill Judge (Plugin)Overall Score: 5/5
Baseline Agent Output
In failure-only verification, the goal is to confirm that the test reproduces the bug — meaning it should fail without the fix applied. If the test passed without the fix, it means:
Next step: The test must be revised so it fails in the unfixed state and passes only after the fix is applied. With-Skill Agent Output (Isolated)
In failure-only verification, the goal is to confirm that the test reproduces the bug — meaning it should fail without the fix applied. If the test passed without the fix, it means:
Next step: Revise the test so it fails on the buggy code path and only passes once the fix is applied. With-Skill Agent Output (Plugin)
In failure-only verification, the goal is to confirm that the test fails without the fix — proving it actually catches the bug. If the test passed without the fix, it means:
Next steps:
A successful failure-only verification would show the test failing without the fix, proving it detects the problem. |
PureWeen
left a comment
There was a problem hiding this comment.
Adversarial multi-model code review — PR #35843
3 independent reviewers analyzed the diff (Opus / GPT / GPT) and findings were cross-validated.
Methodology: Each reviewer read source files directly (not just the diff), formed an independent assessment before consulting the PR description, and was instructed to verify factual claims. The PR's central fix (drop StrictMode from helper; pre-init 5 phase vars; guard $winnerJson) was confirmed by all reviewers as correctly addressing the devdiv 14313972 root cause — the new runtime AST test (spawn pwsh + dot-source + read undefined var → exit 0) is a mutation-meaningful guard.
This is a security-tightening PR being reapplied after a strict-mode-leak revert. The pinned issues below are mostly about the reapplied hardening (HMAC, Pester workflow scope, pre-init scope) plus one regression introduced in this F2-fix (PadRight typo). No prior reviews on this PR.
Findings (5)
| # | Severity | File | Issue | Consensus |
|---|---|---|---|---|
| 1 | ❌ Regression | verify-tests-fail.ps1:1792 |
$nameDisplay.PadRight missing (22) → mangles summary table |
3/3 |
| 2 | ❌ Security / Race | ci-copilot.yml + Review-PR.ps1 HMAC restore |
Same-user TOCTOU window + missing-key downgrade defeat the HMAC defense | 2/3 |
| 3 | Review-PR.ps1:1942 |
Unconditional pre-init clobbers Gate's in-process state in non-phased / -DryRun runs |
1/3 (verified from source) | |
| 4 | Review-PR.ps1:2158 |
$winnerJson.winner guard only catches unparseable JSON, not missing properties — still throws under StrictMode |
2/3 | |
| 5 | security-scripts-pester.yml:107 |
Run.Path only covers .github/scripts; required .github/skills/** and eng/scripts/** tests are presence-checked but never executed (would have missed finding #1) |
2/3 |
Test coverage assessment
The Pester suite added in this PR is excellent for the script layer (Phase-Audit static analysis, HMAC interop, openssl ↔ .NET round-trip, StrictMode leak runtime test, F3 author-gating tests). However, the new security-scripts-pester.yml workflow has a scope mismatch (finding #5) that would have caught the PadRight regression in finding #1 had .github/skills/** been included in Run.Path. Both findings are coupled: fixing #5 will surface #1 automatically in CI.
Items verified clean (no action needed)
- StrictMode core fix:
Write-SafeSubprocessOutput.ps1does not callSet-StrictModeat script scope; the runtime test correctly verifies absence. - Sanitizer regex
(?i)##(vso\[|\[)→##~SANITIZED~$1: global, non-anchored (matches AzDO'sIndexOfmid-line parsing), output cannot re-trigger the parser. - openssl ↔ .NET HMAC interop:
-macopt hexkey:+ lowercase-hex round-trips correctly with[Convert]::FromHexString+HMACSHA256.HashData. - F3 author-gating in
Remove-StaleMauiBotComments.ps1: both marker and bot-only-content paths now correctly requireTest-IsMauiBotCommentAuthor. Get-PhaseStateDiris used consistently by all Setup/Gate/CopilotReview/Post callers; no orphaned legacy readers.Restore-GateResultOrFailClosedallowlist matches written verdicts and fails closed on every error path except the no-key downgrade in finding #2.SetupFailedtask conditions: variable name spelling matches, undefined-on-first-run correctly resolves to skip-Gate / run-Post.eval.yamlprompt edits: only prompt wording changed (skill-name removal); assertions unchanged.- Subprocess wrapping (
pwsh -NoProfile -File) is applied consistently at every PR-controlled runner site, with output routed throughOut-SafePRSubprocessLine.
Posted via 3-reviewer adversarial consensus. CI status intentionally out of scope.
c1fe81b to
1639725
Compare
1afaff4 to
c18d798
Compare
Five findings from PureWeen's adversarial multi-model review on PR dotnet#35843 (dotnet#35843): ❌ dotnet#1 PadRight regression — verify-tests-fail.ps1:1792 had $nameDisplay.PadRight with no argument (returns PSMethod object, mangles the summary table). Restored the original PadRight(22) (squashed out in PR dotnet#35804). Would have been caught by CI if finding dotnet#5 hadn't existed. ❌ dotnet#2 HMAC TOCTOU + suppress-signing downgrade — eng/pipelines/ci-copilot.yml Gate task. The HMAC sealing block was vulnerable to two attacks: Attack 1: a PR-spawned detached daemon outlived pwsh and raced the openssl dgst call to write 'PASSED' as the last gate-result.txt content before signing. HMAC computed over the forged content. Attack 2: the daemon deleted gate-result.txt during the brief window, causing the if [ -f ... ] guard to skip signing entirely. GATE_HMAC_KEY was never set; Restore-GateResultOrFailClosed then skipped HMAC verification because no key was supplied. Two-layer fix: - Run pwsh in its own process group via bash job control (set -m). \$! is the PGID. After wait, SIGKILL the whole group; verify with pgrep -g that nothing survived. Fail closed if anything did. This kills naive detached daemons before signing. - Restore-GateResultOrFailClosed gains a CI_REQUIRE_GATE_HMAC contract param (env var). Gate task sets it to '1' alongside GATE_HMAC_KEY. In CI, if the key is empty/missing the function refuses to downgrade to no-verification and fails closed. The 'local dev with no key' path remains valid for non-CI runs. - Tightened the sealing block: missing gate-result.txt or missing openssl now fails the Gate task instead of silently skipping. - Replaced the misleading 'a daemon can rewrite the file but cannot recompute a valid HMAC' comment that materially overstated the protection.⚠️ dotnet#3 Pre-init clobbers Gate's in-process state — Review-PR.ps1:1942. The previous F2-fix put \/\/\/ \/\ = … at the top of if (\), outside the if (\ -eq 'CopilotReview') branch. In non-phased / -DryRun runs both \ and \ are true; Gate populates these in-process around L1340 and the pre-init wiped them. Moved the init INSIDE the phased branch (where Gate has already exited and we restore from disk). Updated the AST regression test that asserted unconditional init at top of \ — it now asserts the init lives in the phased branch. Added a new test that forbids init of these 5 vars in the outer scope (locks the fix the other way).⚠️ dotnet#4 winnerJson guard incomplete — Review-PR.ps1:2158. The previous if (\) guard handled the parse-failed case but not the partial-object case (parsed JSON without winner/isPRFix keys throws PropertyNotFoundException under StrictMode). Now also checks property presence via .PSObject.Properties.Name -contains 'winner'.⚠️ dotnet#5 Pester workflow scope mismatch — security-scripts-pester.yml. \.Run.Path = @('.github/scripts') only, but the trigger paths and the required-files list both included .github/skills/** and �ng/scripts/**. Files were present-checked but never executed — that's why finding dotnet#1 (PadRight under .github/skills/) was missed by CI. Extended Run.Path to all three directories. Re-baselined the \ floor (200 → 250). Tests: - 294/294 Pester green (was 290; +4 new CI_REQUIRE_GATE_HMAC tests + 1 new clobber-guard AST test; net since previous tip). - Mutation-tested: injecting the bad pre-init pattern at top of \ makes the new clobber-guard test fail with the exact violation list ( isksData, regressionTests, regrPlatform, uitestCategories, detectScript). Restoring passes. - YAML schema check passes on both modified files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PureWeen
left a comment
There was a problem hiding this comment.
Round 2 Adversarial Review
Methodology: 3 independent reviewers run in parallel, then disputed findings cross-validated by the other 2 models. Inline comments below are the consensus output.
Round 1 produced 5 inline findings — all addressed by the 4 follow-up commits between 1afaff4c7e and bf8ab1949c. I verified each round-1 fix against source:
- ✅ Gate result trust boundary (
Restore-GateResultOrFailClosed) — sealed verdict + HMAC verification implemented atReview-PR.ps1:155-197 - ✅ HMAC key generation hardened —
python3 secrets.token_hex(32)with length validation - ✅ PGID process isolation —
set -m+kill -KILL -- "-${PWSH_PID}"orphan reap + pgrep poll - ✅ Strict-mode leak crash class — variables now unconditionally initialized inside the
$Phase -eq 'CopilotReview'branch - ✅ Pester collection scope widened to
('.github/scripts', '.github/skills', 'eng/scripts')
Round 2 surfaced 3 new findings, all reaching 3/3 consensus. Two are security regressions introduced by the round-1 fixes themselves; one is incomplete coverage of the original strict-mode-leak fix. See inline comments for details.
Findings summary (ranked by severity)
| Severity | Category | File:Line | Consensus |
|---|---|---|---|
| ❌ Critical | Security | eng/pipelines/ci-copilot.yml:846 |
3/3 |
| ❌ Critical | Security | .github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1:198 |
3/3 |
| Regression | .github/scripts/Review-PR.ps1:1956 |
3/3 |
Non-blocking observations
- Pester
silentlySkipped > 0 → exit 1gate vs. openssl-Skip test (security-scripts-pester.yml:151-158vs.Review-PR.Tests.ps1:647-651): one reviewer flagged a direct in-PR contradiction — the new floor will fail red on any agent withoutopenssl. Discarded per round-2 follow-up cap, but worth a follow-up either way (relax the gate to allow intentional skips, or remove the openssl-Skip in favor of a real fallback). Flagged by: 1/3 reviewers, not cross-validated. - PID-reuse window after
kill -KILL -- "-${PWSH_PID}"(ci-copilot.yml:~751): kill runs afterwaitreaped the PID, so there's a tiny kernel PID-reuse race. Author already acknowledges broader setsid-escape residual risk. Flagged by: 1/3 reviewers, suggestion only. - Branch is at HEAD
bf8ab1949c(4 commits ahead of round-1 HEAD1afaff4c7e) — no base-drift impact on this review sincelist_pull_request_fileswas used as the authoritative changed-file source.
Test coverage assessment
- New tests:
Aggregate-CopilotTokenUsage.Tests.ps1,Remove-StaleMauiBotComments.Tests.ps1,Write-SafeSubprocessOutput.Tests.ps1,Review-PR.Phase-Audit.Tests.ps1, plus 5 newCI_REQUIRE_GATE_HMACcases inReview-PR.Tests.ps1. New AST audit test enforces variable initialization inside the CopilotReview phased branch. - Test coverage gap: AST audit only enforces CopilotReview phase. Gate phase init (where R1-2 lives) is not asserted by any test. See inline at
Review-PR.ps1:1956. - No tests cover the trust boundary between
$RepoRootand$TrustedScriptsDir— R2-B would not be caught by current Pester suite.
Prior review status
Round 1 review (5 findings, all done): #35843 (review)
Reshaped from the earlier dotnet#35804 reapply: Kuba's branch has since reimplemented PR-stdout sanitization as ConvertTo-AzdoSafeConsole, so this drops the superseded Write-SafeSubprocessOutput/Out-SafePRSubprocessLine sanitizer + its audit and keeps ONLY the security primitives Kuba lacks. Added: - Restore-GateResultOrFailClosed: fail-CLOSED gate verdict restore (was fail-open "SKIPPED" on missing file). Wired into both the CopilotReview and Post phased-restore sites. Verifies an HMAC over gate-result.txt when GATE_HMAC_KEY is supplied; in CI, fails closed if CI_REQUIRE_GATE_HMAC=1 but the key is empty (suppress-signing downgrade defense). - HMAC sealing of gate-result.txt in the Gate bash task: Linux-gated (POSIX process groups / pgrep / python3), key generated AFTER pwsh exits via python3 secrets and fed to the HMAC over stdin (never argv /proc/cmdline or env /proc/environ), orphan-reap of PR-spawned process groups with a PID-reuse guard, length-checked. STATE is the staging root (where Review-PR.ps1 writes gate-result.txt), so the .hmac sibling lands next to it. Job-level GATE_HMAC_KEY / CI_REQUIRE_GATE_HMAC default to '' so undefined AzDO macros resolve to empty on Windows/macOS (no regression there). - winnerJson StrictMode property-presence guard (PSObject.Properties.Name -contains 'winner') — bare member access throws PropertyNotFoundException on partial objects under StrictMode. - Gate-phase undefined-var pre-init ($risksData/$regressionTests) — defense-in-depth against the devdiv 14313972 crash class. - Phase-Audit static analysis test (gh-call token-scope auditing). - security-scripts-pester workflow (without the dropped sanitizer audit). Kept intact: Kuba's ConvertTo-AzdoSafeConsole sanitization (5 sites). Tests: 251/251 Pester green, 0 skipped. bash -n + YAML schema OK. python3 ↔ .NET HMAC interop cross-checked. AST guards mutation-tested. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
0534fc7 to
2ef3984
Compare
The HMAC work passes GATE_HMAC_KEY into the CopilotReview task env (needed by Restore-GateResultOrFailClosed). The copilot invocation runs --allow-all, so without stripping the key a prompt-injected agent could read it from its bash/MCP subprocess, forge gate-result.txt, and recompute a valid HMAC — defeating the seal. Added GATE_HMAC_KEY to --secret-env-vars alongside the GH/COPILOT tokens. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
verify-tests-fail.ps1 runs as trusted bytes (copied to \/skills before the PR merge) with GH_TOKEN live, but dot-sourced EstablishBrokenBaseline.ps1 from \ = git rev-parse --show-toplevel = the PR-merged worktree. A PR could replace that file with arbitrary PowerShell executed in the trusted host session. Resolve the baseline + detection helpers from \ (../../../scripts/) so they load from the trusted copy in CI. \ stays for git ops + intentional PR-code subprocesses only. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Note
Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!
Targets
feature/enhanced-reviewer(#35677). Adds the cross-phase trust-boundary hardening that branch is missing (the PR-stdout sanitization it already has viaConvertTo-AzdoSafeConsoleis intentionally left untouched).Adds:
Restore-GateResultOrFailClosed— fail-closed gate verdict restore (was fail-openSKIPPEDon a missing file), with HMAC verification.gate-result.txtin the Gate task: Linux-gated, key generated afterpwshexits and fed over stdin (never/proc/cmdlineor/proc/environ), orphan-reap of PR-spawned process groups, fail-closed in CI if signing is suppressed.$risksData/$regressionTests).Tests: 251/251 Pester green, 0 skipped.
bash -n+ YAML schema clean; python3 ↔ .NET HMAC interop cross-checked.The macro-literal handling needs one validation run on a non-Linux platform (ios/catalyst/windows) since the public
maui-prpipeline skips.github/**.