Skip to content

feat(windows-sandbox): implement production-identity readiness probe - #3161

Closed
liugddx wants to merge 9 commits into
apache:mainfrom
liugddx:feat/windows-sandbox-readiness-probe
Closed

feat(windows-sandbox): implement production-identity readiness probe#3161
liugddx wants to merge 9 commits into
apache:mainfrom
liugddx:feat/windows-sandbox-readiness-probe

Conversation

@liugddx

@liugddx liugddx commented Aug 17, 2026

Copy link
Copy Markdown
Member

What

Replaces the Windows AppContainer backend's file-existence availability check with a real production-identity readiness probe, and aligns the RFC's §6.4/§6.5 guarantees with the shipped behavior.

Before this change the backend advertised availability from existsSync(clientPath) alone — that only proves the packaged launcher is present, not that the OS can actually create the AppContainer identity, token, and Job on this host (AppContainer can be disabled by policy, the edition may not support it, etc.). Availability could therefore claim the boundary is enforceable when it is not.

How

  • launcher (--readiness-probe): constructs the real AppContainer identity/token and a kill-on-close Job, then launches a throwaway confined child (cmd.exe /c exit 0) with the SECURITY_CAPABILITIES + JOB_LIST attribute list. It verifies the child token is an AppContainer token and is in the Job, waits for a clean exit, and fails closed if the host cannot create or enforce the boundary. Every post-resume outcome (verification failure, timeout, clean exit) is routed through the production TerminateProcess + terminate_and_drain_job settlement primitive. The readiness AppContainer profile uses one fixed, self-reconciling name (best-effort delete before create), so a profile leaked by an externally-killed probe is reclaimed by the next probe.
  • runtime (default-sandbox-manager.ts): file presence only discovers the launcher path; the memoized readiness probe is the single availability authority. Runtime Host composition calls isBuiltinFilesystemWorkerSandboxAvailable() at startup, which warms the readiness cache, so worker registration and the backend's later synchronous isAvailable() on the operation hot path consume the same result instead of spawning on the event loop. auto/require never fall back to host execution.
  • CI: readiness-probe-smoke.ps1 added to the windows-sandbox-w0 lane — asserts exit 0, repeatable, and argument-rejecting on a real windows-2025 runner.
  • docs: RFC §6.4/§6.5 (EN + zh-CN) — the readiness probe moves to implemented / enforced. Structured unavailable reasons / diagnostics (typed reason, setup version, failure stage), private desktop, full per-profile filesystem/offline-network policy at readiness, and launcher signature/version verification remain explicitly deferred as later gates.

Closes a roadmap checkbox

Advances #2142 Phase 4 ③ — "Align the RFC's implemented guarantees with the shipped slice: either implement private-desktop and full production-boundary readiness probes or mark them explicitly as later gates." This PR takes the implement path for the production-boundary readiness probe and keeps private-desktop + full-policy-at-readiness + structured diagnostics explicitly marked as later gates, so the RFC no longer overclaims.

Honesty / fail-closed

The deferral narrows readiness richness (desktop layer, full per-profile policy re-proven at readiness, structured diagnostics) and defense-in-depth, not the enforcement boundary. An unavailable, drifted, or failed backend still fails closed; a restricted managed profile never falls back to host execution.

Review update

Addresses the maintainer reviews from @hqhq1025 and @M4n5ter:

  • single availability authority (probe, not file presence), warmed at Runtime Host composition so the hot-path isAvailable() is cache-only;
  • native probe routes every post-resume outcome through the production settlement path;
  • deterministic, retryable readiness-profile cleanup via a fixed self-reconciling name;
  • probeWindowsReadiness unit tests (failed readiness, one shared probe result, no worker publication on failure, external timeout, missing launcher);
  • RFC structured-diagnostics guarantees marked as later gates in both languages;
  • Generated-by: Claude Fable 5 trailer added to every commit.

Verification

  • Rust: cargo fmt --check clean; cargo build --locked; cargo test --locked (38 tests) pass on Windows 11 x64; --readiness-probe → exit 0, stable across runs, rejects arguments.
  • Node: default-sandbox-manager.test.js (11 tests) + windows-sandbox.test.js pass with the consolidated isAvailable.
  • Smoke: readiness-probe-smoke.ps1 passes under pwsh 7 locally.
  • biome check clean on the changed TS.
  • The native timeout-kill path is proven by the real windows-sandbox-w0 CI lane.

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Claude Fable 5 (Anthropic), driven by the human contributor of record, authored the launcher --readiness-probe implementation and settlement/cleanup lifecycle, the runtime default-sandbox-manager.ts availability consolidation and unit tests, the PowerShell smoke, and the RFC updates, all under human review. Every commit carries a Generated-by: Claude Fable 5 trailer; the final squash commit must retain it.

@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c0a6e4a6-7e81-4b3c-8175-83c77df9a138

📥 Commits

Reviewing files that changed from the base of the PR and between c8b6527 and 8b03f9f.

📒 Files selected for processing (5)
  • docs/architecture/windows-sandbox-rfc-v1.md
  • docs/architecture/windows-sandbox-rfc-v1.zh-CN.md
  • experiments/windows-sandbox/launcher/src/windows_launcher.rs
  • packages/runtime/src/__tests__/default-sandbox-manager.test.ts
  • packages/runtime/src/sandbox/default-sandbox-manager.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/runtime/src/tests/default-sandbox-manager.test.ts
  • docs/architecture/windows-sandbox-rfc-v1.md

Included review availability: Your plan includes up to 3 reviews per rolling hour; 2 remain after this review.


📝 Walkthrough

Summary

This PR replaces Windows launcher-file availability checks with a production-identity readiness probe.

The launcher adds --readiness-probe. It creates the AppContainer identity, token, and kill-on-close Job. It launches cmd.exe /c exit 0, verifies the exact AppContainer SID and Job membership, enforces a timeout, settles the child, and fails closed on errors.

The runtime extends the existing launcher as the source of truth. It does not create a parallel availability path. Startup warms a memoized cache keyed by launcher path. Synchronous isAvailable() reads the cache only. Negative results expire after 60 seconds and recover only during a later composition. Positive results remain cached. auto and require modes do not fall back to host execution.

This is the smallest coherent implementation of the production-boundary check. Lifecycle serialization, cleanup, settlement, timeout handling, and cache states are necessary to prevent stale or concurrent readiness profiles and repeated process creation.

No code or tests can be removed from the supplied changes without weakening failure handling, lifecycle safety, memoization, or regression coverage.

Complexity delta

  • Removes launcher-file presence as an availability authority.
  • Adds one launcher readiness mode and one reserved request identifier.
  • Adds readiness-profile lifecycle state, mutex serialization, cleanup, and settlement handling.
  • Adds exact SID and Job-membership validation.
  • Adds per-path cache state with permanent positive results and expiring negative results.
  • Adds cache-only synchronous reads.
  • Adds Rust and runtime readiness APIs.
  • Adds launcher tests, runtime tests, and a Windows smoke test.
  • Adds no user configuration surface.
  • Increases test-maintenance scope for lifecycle and cache behavior.

The added complexity is necessary for the fail-closed security boundary. Total maintenance complexity stays justified.

Validation

  • Adds a Windows CI step for the readiness-probe smoke test.
  • Tests successful and repeatable probe execution.
  • Tests rejection of unexpected arguments.
  • Covers missing launchers, spawn errors, timeouts, non-zero exits, memoization, negative-cache expiry, and permanent positive caching.
  • Covers cache-only reads that never spawn or refresh probes.
  • Covers readiness-profile naming, namespace separation, reserved request rejection, and mutex naming.
  • Updates the English and Chinese RFC status and deferred-scope sections.
  • Reported validation includes Rust formatting, locked builds and tests, Windows readiness and AppContainer smoke tests, runtime and core builds, Node tests, and Biome checks.
  • Final required-check status is unverified because direct CI results were not provided.

Review-relevant risks

  • The runtime can mark the Windows backend unavailable when the launcher file exists. This changes user-visible availability and fallback behavior. Material behavior changes require independent human review under repository policy.
  • The probe changes Windows security-boundary enforcement through AppContainer and Job validation. Material security changes require independent human review under repository policy.
  • New Rust and runtime readiness APIs add public contracts. Material public-contract changes require independent human review under repository policy.
  • Probe timeouts and cache expiry can affect startup and operational behavior. Material release-impacting changes require independent human review under repository policy.
  • RFC updates affect documented scope and governance. Material governance changes require independent human review under repository policy.
  • No licensing effect was identified in the current diff.

The person performing the merge reviews the final diff. A maintainer makes the final determination.

Walkthrough

The launcher adds a production-identity readiness probe. Runtime availability now depends on its cached result. A PowerShell smoke test validates repeatability and argument handling. The workflow and RFC documents record the probe and preview status.

Changes

Windows sandbox readiness

Layer / File(s) Summary
Readiness contract and profile lifecycle
experiments/windows-sandbox/launcher/src/protocol.rs, experiments/windows-sandbox/launcher/src/acl_ledger.rs, experiments/windows-sandbox/launcher/src/windows_launcher.rs, experiments/windows-sandbox/launcher/src/windows_launcher_tests.rs
The launcher reserves the readiness request ID, serializes readiness profile leases, uses a separate deterministic profile namespace, and tests naming and validation rules.
Production identity probe
experiments/windows-sandbox/launcher/src/main.rs, experiments/windows-sandbox/launcher/src/windows_launcher.rs
The launcher accepts --readiness-probe and verifies AppContainer identity, specific Job membership, timeout, cleanup, and exit status for a constrained child process.
Runtime availability integration
packages/runtime/src/sandbox/default-sandbox-manager.ts, packages/runtime/src/__tests__/default-sandbox-manager.test.ts
Windows availability uses a launcher probe with a 15-second timeout, path-keyed caching, negative-result expiry, cache-only reads, injectable spawn behavior, and fail-closed results.
Smoke validation and preview status
experiments/windows-sandbox/readiness-probe-smoke.ps1, .github/workflows/windows-sandbox-w0.yml, docs/architecture/windows-sandbox-rfc-v1.md, docs/architecture/windows-sandbox-rfc-v1.zh-CN.md
The smoke test checks launcher presence, repeatable success, and argument rejection. The workflow runs the smoke test. The RFCs describe enforced and deferred preview controls.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: 🔵 Low · up to 8b03f

The PR makes readiness checks authoritative and cached, but the predictable readiness coordination object still does not validate the security policy of an existing object, which could allow unintended interference with readiness results. The change is mergeable with explicit security-owner follow-up.

Sequence Diagram(s)

sequenceDiagram
  participant Runtime as Sandbox manager
  participant Launcher as Windows launcher
  participant Profile as Readiness AppContainer profile
  participant Job as Kill-on-close Job
  participant Child as cmd.exe probe child
  Runtime->>Launcher: run --readiness-probe
  Launcher->>Profile: create deterministic readiness profile
  Launcher->>Job: create kill-on-close Job
  Launcher->>Child: launch constrained cmd.exe
  Launcher->>Child: verify exact AppContainer SID and Job membership
  Launcher->>Child: wait for timeout and successful exit
  Launcher-->>Runtime: return readiness result
  Runtime->>Runtime: cache result and report availability
``

</details>

<!-- walkthrough_end -->
<!-- pre_merge_checks_walkthrough_start -->

<details>
<summary>🚥 Pre-merge checks | ✅ 5</summary>

<details>
<summary>✅ Passed checks (5 passed)</summary>

|         Check name         | Status   | Explanation                                                                                                                                                              |
| :------------------------: | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|      Description check     | ✅ Passed | The description clearly explains the problem, implementation, deferred work, AI contribution, and verification results, but it omits the template checklist.             |
|         Title check        | ✅ Passed | The title clearly and concisely identifies the main change: implementing a production-identity readiness probe for the Windows sandbox.                                  |
|     Linked Issues check    | ✅ Passed | Check skipped because no linked issues were found for this pull request.                                                                                                 |
| Out of Scope Changes check | ✅ Passed | Check skipped because no linked issues were found for this pull request.                                                                                                 |
|      Ai Use Disclosure     | ✅ Passed | The PR selects substantive generative tooling and names Claude Fable 5 and its scope; all five PR commits contain one standalone `Generated-by: Claude Fable 5` trailer. |

</details>

</details>

<!-- pre_merge_checks_walkthrough_end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

<details>
<summary>🧪 Generate unit tests (beta)</summary>

- [ ] <!-- {"checkboxId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} -->   Create PR with unit tests

</details>

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---




<sub>Comment `@coderabbitai help` to get the list of available commands.</sub>

<!-- tips_end -->
Loading

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/runtime/src/sandbox/default-sandbox-manager.ts (1)

47-78: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Share the Windows readiness cache across availability checks.

Each createWindowsReadinessProbe call creates a separate cache. Backend checks from different managers can therefore run spawnSync repeatedly. isBuiltinFilesystemWorkerSandboxAvailable still reports true when only the launcher exists. Cache readiness by clientPath at module scope and use it in both paths. Add tests for repeated backend checks and failed readiness exits.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 966c663d-9334-41d4-a58c-1f1761d6cc5f

📥 Commits

Reviewing files that changed from the base of the PR and between 85410e7 and 9d026bb.

📒 Files selected for processing (7)
  • .github/workflows/windows-sandbox-w0.yml
  • docs/architecture/windows-sandbox-rfc-v1.md
  • docs/architecture/windows-sandbox-rfc-v1.zh-CN.md
  • experiments/windows-sandbox/launcher/src/main.rs
  • experiments/windows-sandbox/launcher/src/windows_launcher.rs
  • experiments/windows-sandbox/readiness-probe-smoke.ps1
  • packages/runtime/src/sandbox/default-sandbox-manager.ts

Included review availability: Your plan includes up to 3 reviews per rolling hour; 1 remains after this review.

Comment thread experiments/windows-sandbox/launcher/src/windows_launcher.rs Outdated
Comment thread experiments/windows-sandbox/launcher/src/windows_launcher.rs Outdated

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.

Pull request overview

This PR strengthens Windows sandbox “availability” by replacing a file-presence check with a real production-identity readiness probe, ensuring the runtime only reports the Windows AppContainer backend as available when the host can actually stand up and enforce the boundary. It also adds CI smoke coverage for the probe and updates the RFC (EN + zh-CN) to align documented guarantees with the shipped preview slice.

Changes:

  • Runtime: Windows backend isAvailable now runs a memoized --readiness-probe (spawn + timeout) instead of existsSync.
  • Launcher: implements --readiness-probe that creates the AppContainer identity + kill-on-close Job and verifies confinement via a throwaway child.
  • CI/docs: adds a Windows readiness-probe smoke test and updates the RFC preview-status sections to reflect the implemented guarantees and deferred gates.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/runtime/src/sandbox/default-sandbox-manager.ts Switches Windows backend availability to a memoized --readiness-probe spawn with timeout.
experiments/windows-sandbox/readiness-probe-smoke.ps1 Adds a smoke test to validate readiness probe success, repeatability, and argument rejection.
experiments/windows-sandbox/launcher/src/windows_launcher.rs Implements the production-identity readiness probe and child confinement verification logic.
experiments/windows-sandbox/launcher/src/main.rs Adds a --readiness-probe CLI entrypoint with argument rejection.
docs/architecture/windows-sandbox-rfc-v1.md Updates RFC status and adds preview implementation status clarifying enforced vs deferred gates.
docs/architecture/windows-sandbox-rfc-v1.zh-CN.md Same as EN RFC update, in zh-CN.
.github/workflows/windows-sandbox-w0.yml Wires the new readiness-probe smoke test into the Windows sandbox W0 CI lane.

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

Comment thread experiments/windows-sandbox/launcher/src/windows_launcher.rs Outdated
Comment thread docs/architecture/windows-sandbox-rfc-v1.zh-CN.md Outdated
Comment thread docs/architecture/windows-sandbox-rfc-v1.md Outdated
@liugddx

liugddx commented Aug 17, 2026

Copy link
Copy Markdown
Member Author

Review-fix round pushed as b794b7b. Summary:

  • A (CodeRabbit + Copilot, Major): probe profile name is now unique per invocation — request_id = readiness-probe.. (SystemTime nanos). PID reuse after a crashed probe can no longer deadlock on a lingering profile.
  • B (CodeRabbit, Major): probe now proves enforcement, not presence — it verifies the confined child runs under the specific requested Job (IsProcessInJob vs our handle) and carries the exact requested AppContainer SID (TokenAppContainerSid + EqualSid), failing closed otherwise.
  • C (Copilot, EN+zh): RFC Updated: synced to 2026-08-17.
  • D (self-applied): Windows readiness result is now cached at module scope keyed by client path, so backends sharing one launcher share a single probe; distinct paths stay independent. isBuiltinFilesystemWorkerSandboxAvailable is intentionally kept existence-based (it advertises the path for capability listing; the enforcing probe runs at backend construction via isAvailable).

Local gate: cargo fmt --check clean, cargo build --locked clean, readiness-probe-smoke.ps1 green under pwsh 7 (exit 0 / repeatable / argument-rejecting), @maka/core + @maka/runtime build clean, default-sandbox-manager tests 4/4, biome clean.

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.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Suppressed comments (1)

packages/runtime/src/sandbox/default-sandbox-manager.ts:66

  • probeWindowsReadiness() correctly fail-closes on --readiness-probe, but the runtime-host still gates filesystem-worker enablement via isBuiltinFilesystemWorkerSandboxAvailable() (see packages/runtime-host/src/server/execution-composition.ts:307), which for win32 currently only checks builtinWindowsClientPath(...) !== undefined (file presence). On hosts where the launcher exists but AppContainer is disabled/policy-blocked, this can incorrectly treat the sandbox as "available" and defer failure until first launch, undermining the goal of replacing file-presence availability.

Consider wiring isBuiltinFilesystemWorkerSandboxAvailable() (and/or builtinWindowsClientPath) to the same readiness probe (ideally reusing the module-scope cache) so all availability/enablement paths agree and fail closed consistently.

function probeWindowsReadiness(clientPath: string): boolean {
  const cached = windowsReadinessCache.get(clientPath);
  if (cached !== undefined) return cached;
  let available = false;
  if (existsSync(clientPath)) {
    try {
      const result = spawnSync(clientPath, ['--readiness-probe'], {
        timeout: WINDOWS_READINESS_PROBE_TIMEOUT_MS,
        windowsHide: true,
        stdio: 'ignore',
      });
      available = result.error === undefined && result.status === 0;

@hqhq1025 hqhq1025 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.

Request changes on the current head b794b7be8.

I agree with replacing file presence with a real production-identity probe, but the current ownership and failure lifecycle are not merge-ready yet:

  1. isBuiltinFilesystemWorkerSandboxAvailable() still treats the Windows launcher file as sufficient, and Runtime Host uses that helper to construct/publish the filesystem worker. The real readiness result is deferred until the first SandboxManager.transform() call. This keeps two availability authorities: a host can advertise the managed worker even though the probe will reject it on first use. Please run readiness once at Runtime Host composition and make worker registration and the backend consume the same result.

  2. That deferred check is a synchronous spawnSync with a 15-second timeout. On the first filesystem operation it can block the Runtime Host event loop before the operation's abort/timeout handling is active. Readiness should be an async startup/composition gate, not work performed inside the synchronous isAvailable() contract.

  3. The native probe does not reuse the production settlement path. After ResumeThread, SID/Job verification failures and child timeout return without explicitly terminating and draining the child. Closing the probe Job is not sufficient in the exact failure case where the child is found outside that Job. Route every post-resume outcome through the existing terminate/drain settlement primitive before closing handles and deleting the profile.

  4. The Node timeout can terminate the launcher after it has registered a nonce-named AppContainer profile, bypassing Drop. The profile has no ledger/reconciliation entry, and DeleteAppContainerProfile failures are ignored, so repeated failed probes can leave unrecoverable registrations. Cleanup needs a deterministic, retryable lifecycle rather than relying on process destruction.

The Windows smoke is useful and proves the happy path on the runner, but the existing default-manager test only calls selectInitial() with a text file named .exe; it never exercises the new probe or Runtime Host composition. Please add coverage for failed readiness, one shared probe result, no worker publication on failure, non-blocking startup, and timeout cleanup.

@M4n5ter M4n5ter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

English

Review update

Request changes. I agree with and defer to hqhq1025’s review for the runtime/native lifecycle findings:

  • file presence remains a second availability authority and the worker is published before readiness is known;
  • the deferred synchronous probe can block the Runtime Host event loop;
  • post-resume failures do not consistently terminate and drain the child through the production settlement path;
  • timeout or process termination can leave nonce-named AppContainer profiles without reconciliation.

I have removed the overlapping availability, test-coverage, and native-lifecycle discussion from this review. The findings below are additional merge gates not covered there.

Important — align the RFC’s unavailable diagnostics with shipped behavior

The RFC still states that readiness failures return a stable typed unavailable reason and expose setup version/failure stage:

The implementation runs the probe with stdio: 'ignore', retains only a boolean, and ultimately reports generic backend_not_available. Section 6.5 says unimplemented guarantees are explicitly deferred, but structured unavailable reasons and diagnostics are absent from that deferral list.

Please either mark these guarantees as later gates in both RFC versions or implement and propagate a structured probe result. Documenting them as deferred is the smaller fix for this PR.

Merge-policy gate — complete the required AI attribution

CONTRIBUTING.md requires Generated-by: trailers for commits containing material AI-authored content. Both commits currently contain only Co-Authored-By: Claude Fable 5.

Please also complete the PR template’s AI-use selection and tool scope, and ensure the final squash commit retains the required trailer.

Validation

All currently reported GitHub checks pass, including windows_sandbox_w0_protocol, typecheck, runtime tests, packaging, Windows baseline, and recovery. I also verified cargo fmt --check, Biome on the changed TypeScript, and git diff --check.

The native probe was not rerun locally on Windows; the real Windows CI lane is the evidence for that behavior.

简体中文

Review 更新

Request changes。 对于 runtime/native lifecycle 问题,我认同并以 hqhq1025 的 review 为主:

  • 文件存在性仍然是第二个 availability authority,worker 在 readiness 尚未确认时就会被发布;
  • 延迟执行的同步 probe 可能阻塞 Runtime Host event loop;
  • ResumeThread 之后的失败没有统一经过生产 settlement path 来终止并 drain child;
  • timeout 或进程终止可能留下没有 reconciliation 的 nonce-named AppContainer profile。

我已从本 review 中删除重复的 availability、测试覆盖和 native lifecycle 讨论。下面只保留对方 review 尚未覆盖的额外合并门禁。

Important — RFC 的 unavailable diagnostics 与实际行为不一致

RFC 仍然声明 readiness 失败会返回 stable typed unavailable reason,并暴露 setup version/failure stage:

实际实现使用 stdio: 'ignore' 运行 probe,只保留 boolean,最终统一报告 backend_not_available。§6.5 声称尚未实现的保证均已显式列为 later gates,但 structured unavailable reason 和 diagnostics 没有出现在 deferral 列表中。

请在双语 RFC 中把这些保证标记为 later gates,或者实现并传播结构化 probe 结果。对本 PR 而言,明确标记 deferred 是更小的修复。

合并政策门禁 — 补齐 AI attribution

CONTRIBUTING.md 要求包含 AI 实质创作内容的 commit 使用 Generated-by: trailer。当前两个 commit 都只有 Co-Authored-By: Claude Fable 5

还需要按 PR 模板选择 AI use、说明工具与使用范围,并确保最终 squash commit 保留要求的 trailer。

验证

当前 GitHub checks 全部通过,包括 windows_sandbox_w0_protocol、typecheck、runtime tests、package、Windows baseline 与 recovery。本地还验证了 cargo fmt --check、变更 TypeScript 的 Biome check,以及 git diff --check

本机没有重新运行真实 Windows native probe;该行为以 Windows CI lane 为验证证据。

liugddx and others added 3 commits August 18, 2026 14:58
The Windows AppContainer backend advertised availability from file
existence alone (`existsSync(clientPath)`), which never proves the host
can actually stand up the sandbox identity. Replace that with a real
readiness probe (RFC §6.4) and align the RFC to the shipped behavior.

- launcher: add `--readiness-probe`, which creates the real AppContainer
  identity/token and a kill-on-close Job and launches a throwaway confined
  child, failing closed if the host cannot create or enforce the boundary.
- runtime: back the Windows backend's `isAvailable` with a memoized
  spawnSync of `--readiness-probe`, so `auto`/`require` fail closed on
  hosts where the OS cannot create the boundary rather than trusting the
  packaged binary's presence.
- CI: add readiness-probe-smoke.ps1 to the W0 lane (exit 0, repeatable,
  argument-rejecting).
- docs: align RFC §6.4/§6.5 (EN + zh-CN) — mark the readiness probe as
  implemented and enforced, and keep private desktop, full per-profile
  policy at readiness, and launcher signature/version as later gates.

Advances apache#2142 Phase 4 (align the RFC's guarantees with the shipped slice).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5
…entity spoofing

Address adversarial review on the production-identity readiness probe:

- Make the throwaway AppContainer profile name unique per invocation with a
  monotonic nonce (SystemTime nanos + PID) so a recycled PID can never collide
  with a still-registered profile and deadlock the probe (finding A).
- Prove enforcement, not mere presence: verify the confined child runs under the
  SPECIFIC requested Job (IsProcessInJob against our handle) and carries the
  EXACT requested AppContainer SID (TokenAppContainerSid + EqualSid), failing
  closed otherwise. "some job / some AppContainer" is not evidence the boundary
  we asked for is real (finding B).
- Cache Windows readiness at module scope keyed by client path so backends
  sharing a launcher share one probe result; distinct paths stay independent
  (finding D).
- Sync RFC (EN + zh) Updated: date to 2026-08-17 (finding C).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5
…eadiness probe deterministically

Address maintainer review on the readiness-probe PR:

- Availability: make `isBuiltinFilesystemWorkerSandboxAvailable(win32)` consume
  the memoized readiness probe instead of treating launcher-file presence as a
  second authority. Runtime Host composition calls this at startup, warming the
  readiness cache so the backend's later synchronous `isAvailable()` on the
  transform hot path hits the cache rather than spawning on the event loop.
  Export `probeWindowsReadiness` with an injectable spawn seam and cover it with
  unit tests (clean/non-zero/spawn-error/external-timeout/memoized/missing).

- Settlement: route the readiness child's post-resume outcomes (verification
  failure, timeout, clean exit) through explicit TerminateProcess plus the
  existing `terminate_and_drain_job` primitive, so a child found outside the Job
  is settled rather than relying on Job close as the only backstop.

- Cleanup: give the readiness AppContainer profile one fixed, self-reconciling
  name that best-effort-deletes any leftover before create, so a profile leaked
  by an externally-killed probe is reclaimed by the next probe. Replaces the
  PID+nonce scheme.

- Docs: mark the structured unavailable-reason / diagnostics surface as a
  deferred later gate in both RFC language variants (§6.4/§6.5).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5
@liugddx
liugddx force-pushed the feat/windows-sandbox-readiness-probe branch from b794b7b to 4506446 Compare August 18, 2026 06:59
@liugddx

liugddx commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

Thanks both — pushed 4506446f2 addressing every point.

@hqhq1025

  1. Two availability authorities → one. isBuiltinFilesystemWorkerSandboxAvailable(win32) now consumes the memoized readiness probe; file presence only discovers the launcher path, it is no longer a second authority. Runtime Host composition calls this helper at startup, so worker registration and the backend consume the same result.
  2. Synchronous probe on the event loop. Because composition warms the readiness cache at startup, the synchronous isAvailable() on the transform() hot path is now cache-only and never spawns — no spawnSync runs while an operation's abort/timeout handling is inactive. The one blocking probe happens once, at startup composition (the correct place for a readiness gate), not per operation. (Making the SandboxBackend.isAvailable signature itself async is a larger cross-backend contract change; the event-loop-blocking concern is resolved by moving the actual work to composition. Happy to take the async-contract route in a follow-up if you'd prefer it.)
  3. Settlement path reuse. Every post-resume outcome (SID/Job verification failure, timeout, clean exit) now routes through explicit TerminateProcess + the existing terminate_and_drain_job primitive (settle_probe_child) before handles close and the profile is deleted — so a child found outside the Job is drained, not left to the Job-close backstop.
  4. Deterministic, retryable cleanup. The readiness profile now uses one fixed, self-reconciling name and best-effort-deletes any leftover before create, so a profile leaked by an externally-killed probe is reclaimed by the next probe. This is safe precisely because the readiness child is granted no filesystem roots — there is no stale-ACE inheritance risk that the production per-request nonce guards against. The PID+nonce scheme is removed.

Test coverage. Added a probeWindowsReadiness suite with an injectable spawn seam: failed readiness (non-zero / spawn-error), one shared probe result (memoized — spawn called once across repeated checks), no worker publication when the launcher is absent, and the external-timeout (status === null) case. The native timeout-kill + profile-reclaim path is exercised by the real windows-sandbox-w0 CI lane; the fixed-name self-reconciling behavior also has a Rust unit test.

@M4n5ter

  • RFC unavailable diagnostics. Took the smaller fix you offered: structured unavailable reasons and diagnostics (typed reason, setup version, failure stage) are now marked as later gates in both RFC languages — §6.4 bullets carry a later gate qualifier and §6.5 lists them in the deferred set.
  • AI attribution. Every commit now carries Generated-by: Claude Fable 5 (verified across all three), and the PR template's AI use section is filled in with the tool and scope. The squash commit must retain the trailer.
简体中文

感谢两位 review,已推 4506446f2,逐条处理:

@hqhq1025

  1. 两个 availability authority → 收敛为一个。 isBuiltinFilesystemWorkerSandboxAvailable(win32) 现在消费记忆化的 readiness probe;文件存在性只用来发现 launcher 路径,不再是第二个 authority。Runtime Host composition 在启动时调用该 helper,worker 注册与 backend 消费同一个结果。
  2. 同步 probe 阻塞 event loop。 composition 在启动时已 warm 了 readiness cache,因此 transform() 热路径上的同步 isAvailable() 只读 cache、绝不 spawn——不会在算子的 abort/timeout 尚未生效时跑 spawnSync。唯一一次阻塞式 probe 发生在启动 composition(readiness gate 的正确位置),不在每次算子调用上。(把 SandboxBackend.isAvailable 签名本身改成 async 是跨 backend 的较大契约改动;阻塞 event loop 的问题已通过把实际工作移到 composition 解决。若你更希望走 async 契约,可在后续 PR 跟进。)
  3. 复用 settlement path。 每个 post-resume 结局(SID/Job 校验失败、超时、干净退出)现在都经显式 TerminateProcess + 既有 terminate_and_drain_job(settle_probe_child)后再关句柄、删 profile——落在 Job 之外的 child 会被 drain,不再依赖关 Job 兜底。
  4. 确定性、可重试的清理。 readiness profile 现用一个固定、自我调和的名字,create 前 best-effort 删除残留,被外部 kill 的 probe 泄漏的 profile 会被下一次 probe 回收。之所以安全,正因为 readiness child 不授予任何文件系统 root——不存在生产每请求 nonce 所防的 stale-ACE 继承风险。PID+nonce 方案已移除。

测试覆盖。 新增 probeWindowsReadiness 用例,带可注入的 spawn seam:readiness 失败(非零/spawn 出错)、共享单次 probe 结果(记忆化——多次检查只 spawn 一次)、launcher 缺失时不发布 worker、外部超时(status === null)。native 的超时 kill + profile 回收路径由真实 windows-sandbox-w0 CI lane 覆盖;固定名自我调和行为另有 Rust 单测。

@M4n5ter

  • RFC unavailable diagnostics。 采用你给的更小修复:结构化 unavailable reason 与 diagnostics(typed reason、setup version、failure stage)已在双语 RFC 标为 later gate——§6.4 两条 bullet 加了 later gate 限定词,§6.5 延后清单收入这两项。
  • AI attribution。 三个 commit 现在都带 Generated-by: Claude Fable 5(已逐一核对),PR 模板的 AI use 段也已按工具与范围填好。squash commit 需保留该 trailer。

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/runtime/src/sandbox/default-sandbox-manager.ts (1)

28-59: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

spawnSync blocks the caller for up to 15 seconds.

isBuiltinFilesystemWorkerSandboxAvailable runs during startup composition, and probeWindowsReadiness is synchronous. On a pathological host the event loop stalls for the full WINDOWS_READINESS_PROBE_TIMEOUT_MS window before the first cached answer exists.

The launcher already bounds itself at 10 seconds and settles its own child, so the outer 15-second budget only covers spawn overhead. Consider warming the cache from an async path at startup and keeping the sync call as a cache read, or reduce the outer budget so the worst-case stall is smaller.

Disposition: follow-up. The sync SandboxBackend.isAvailable contract forces the blocking call here, so this is not a defect in this PR.

Note on the static analysis hint for the node:child_process import: it does not apply. The argument list is a literal array, no shell is used, and clientPath comes from resourcesPath, not from user input.

Source: Linters/SAST tools

experiments/windows-sandbox/launcher/src/windows_launcher_tests.rs (1)

102-113: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

One assertion in this test cannot fail.

appcontainer_profile_name is a pure SHA-256 derivation, so Line 109 restates that a hash of a constant input is constant. It protects no observable behavior.

Line 112 is the assertion worth keeping. It guards the real invariant: the readiness identity must never collide with a production request identity.

Disposition: optional.

♻️ Proposed trim
     #[test]
-    fn readiness_profile_name_is_fixed_and_self_reconciling() {
-        // The readiness profile uses one stable name so a profile leaked by an
-        // externally-killed probe is reclaimed by the next probe rather than
-        // accumulating under a unique per-invocation name.
-        let first = appcontainer_profile_name(READINESS_PROFILE_REQUEST_ID);
-        let second = appcontainer_profile_name(READINESS_PROFILE_REQUEST_ID);
-        assert_eq!(first, second);
-        // It must stay distinct from any production request identity so the two
-        // lifecycles never share a registration.
-        assert_ne!(first, appcontainer_profile_name("request-one"));
-    }
+    fn readiness_profile_identity_never_collides_with_a_request_identity() {
+        // The readiness and production profile lifecycles must never share a
+        // registration, so their derived names must differ.
+        let readiness = appcontainer_profile_name(READINESS_PROFILE_REQUEST_ID);
+        assert_ne!(readiness, appcontainer_profile_name("request-one"));
+    }

As per path instructions: "Flag tests that duplicate existing coverage, assert implementation details, or do not protect observable behavior."

Source: Path instructions


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c3bf7e7c-4dea-4abf-b83f-5df9fa67d4a2

📥 Commits

Reviewing files that changed from the base of the PR and between b794b7b and 4506446.

📒 Files selected for processing (6)
  • docs/architecture/windows-sandbox-rfc-v1.md
  • docs/architecture/windows-sandbox-rfc-v1.zh-CN.md
  • experiments/windows-sandbox/launcher/src/windows_launcher.rs
  • experiments/windows-sandbox/launcher/src/windows_launcher_tests.rs
  • packages/runtime/src/__tests__/default-sandbox-manager.test.ts
  • packages/runtime/src/sandbox/default-sandbox-manager.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/architecture/windows-sandbox-rfc-v1.zh-CN.md
  • docs/architecture/windows-sandbox-rfc-v1.md

Included review availability: Your plan includes up to 3 reviews per rolling hour; 2 remain after this review.

Comment thread packages/runtime/src/sandbox/default-sandbox-manager.ts
liugddx added a commit to liugddx/maka-agent that referenced this pull request Aug 18, 2026
Non-interactive AppContainer workers previously inherited the creator's
interactive Winsta0\Default desktop, leaving the same-session GUI attack
surface (window-message shatter into the user's interactive windows, desktop
hooks) open — AppContainer confines files/network/token but not the desktop
scope. Per RFC v1 §6.3 the private-desktop promise was designed but deferred.

Enforce the desktop-scoped half under an explicit no-Win32k contract: each
AppContainer launch (production create + readiness probe) now stands up a
per-launch alternate desktop on the existing window station, with a protected
DACL that grants only the launching user, Local System, and the child's
AppContainer SID the minimal DESKTOP_* rights (create window/menu, read/write
objects, enumerate) and never DESKTOP_SWITCHDESKTOP / HOOKCONTROL / journal.
STARTUPINFOEXW.lpDesktop points the child at it. If the desktop cannot be
created or the DACL cannot be granted (CreateProcessW then fails ACCESS_DENIED),
the launch fails closed — auto/require never fall back to the host desktop.

Scope, stated honestly (per maintainer review on apache#3174):
- This keeps the worker off the interactive Default desktop, so it cannot
  enumerate/post messages to the user's interactive windows or install desktop
  hooks against them.
- It does NOT isolate the clipboard: the clipboard belongs to the window
  station, which both desktops still share. Clipboard isolation with a dedicated
  window station is a later hardening gate (§6.5).
- The create-window/write DACL rights are granted but not relied upon; the
  worker does no GUI work (no-Win32k contract). A verified Low mandatory-
  integrity label proving those rights are usable at AppContainer's Low IL is
  deferred (§6.5).

The boundary probe self-attests its own desktop name and refuses to run on
Default; appcontainer-smoke asserts the private-desktop name prefix and rejects
Default. Digest is unchanged: the desktop is a launch-time detail and does not
enter the manifest.

Stacked on apache#3161 (readiness probe).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5

@hqhq1025 hqhq1025 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.

Follow-up review on current head 4506446f2.

The previous availability-authority split is fixed, the hot operation path now reads the warmed cache, AI attribution/RFC deferrals are corrected, and the ordinary post-resume path now attempts explicit child/Job settlement. However, the fixed readiness-profile design introduces new lifecycle blockers:

  1. The readiness and production profile namespaces still overlap. READINESS_PROFILE_REQUEST_ID is the valid production request ID readiness-probe, and both paths call the same appcontainer_profile_name(request_id). LaunchRequest::validate accepts that exact value. A production launch using it therefore gets the same profile/SID that create_readiness() best-effort deletes and recreates. This invalidates the claim that the readiness identity can never inherit production filesystem ACEs. Please give readiness a profile namespace that production requests cannot enter, and enforce that separation rather than testing against one sample ID (request-one).

  2. The fixed profile is not serialized across processes. windowsReadinessCache is process-local, while create_readiness() performs delete → create and Drop later deletes the same global profile without a named mutex/lease. Two Maka processes, app instances, side-by-side installs, or direct probe invocations can therefore delete or drop each other's active profile, race creation, and produce a transient failure that is then cached for the process lifetime. Please add cross-process ownership/serialization and a concurrent real-Windows test.

  3. Settlement failure is still discarded when verification also fails. In match (verify, settled), (Err(error), _) ignores settled = Err(...); readiness_probe() then closes the Job and unconditionally drops/deletes the fixed profile. That is the exact state where the child/Job was not proven drained. Preserve the unsettled state and do not delete/reuse the profile until cleanup is proven, matching the production path's LaunchFailure::Unsettled ownership rule.

  4. Every negative readiness result is cached permanently. Timeout, antivirus/file contention, spawn failure, and the profile race above are transient, but one occurrence disables the Windows sandbox until Runtime Host restart. Positive caching is sound; negative results need a bounded retry/TTL/backoff or an explicit async startup state machine whose backend hot path remains cache-only.

The added TypeScript tests cover boolean mapping and same-process memoization, but they do not exercise external launcher termination/profile reclaim, two-process contention, namespace collision, or combined verify+settlement failure. The W0 smoke only runs two successful probes sequentially, so it also does not cover those paths.

Current verification: full repository build, focused runtime/runtime-host tests, Biome, cargo fmt --check, diff check, and merge check passed; all current GitHub checks are green. These lifecycle cases remain unverified and block approval.

Address four readiness-probe lifecycle blockers on the availability path:

1. Reserve the `readiness-probe` request_id in protocol and reject it in
   LaunchRequest::validate; derive the probe profile under a disjoint
   `maka.readiness.` namespace so no production launch can resolve to the
   profile the probe deletes and recreates.
2. Serialize the whole delete->create->probe->settle->drop window across
   processes with the DACL-hardened named mutex the ACL ledger uses
   (LedgerLock), scoped by SID under
   Global\Maka.WindowsSandbox.ReadinessProfile.v1, failing closed on timeout.
3. Preserve the AppContainer registration on drop when the Job could not be
   proven empty (keep_on_drop + preserve()), mirroring the production
   Unsettled contract, instead of deleting a possibly-live identity.
4. Cache negative Windows readiness for a bounded TTL (60s) rather than the
   process lifetime, and make the synchronous isAvailable hot path strictly
   cache-only (readCachedWindowsReadiness) so it never spawns on the event
   loop; positive results stay cached permanently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/architecture/windows-sandbox-rfc-v1.md (1)

197-224: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

fix-now — Do not attribute readiness behavior to PR #2961.

Lines 199-203 identify PR #2961 as the shipped preview slice. Lines 216-224 then list the readiness probe and readiness lifecycle as enforced in that slice. The supplied PR objective states that this PR adds those features.

Keep the #2961 baseline separate. Attribute readiness to this change after it merges, or mark it as pending until then.

Disposition: fix-now.

As per path instructions, report concrete risks and keep the documented source of truth coherent.

Source: Path instructions


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 888f801c-35cf-44cb-980e-629c9bdda5ac

📥 Commits

Reviewing files that changed from the base of the PR and between 4506446 and c8b6527.

📒 Files selected for processing (8)
  • docs/architecture/windows-sandbox-rfc-v1.md
  • docs/architecture/windows-sandbox-rfc-v1.zh-CN.md
  • experiments/windows-sandbox/launcher/src/acl_ledger.rs
  • experiments/windows-sandbox/launcher/src/protocol.rs
  • experiments/windows-sandbox/launcher/src/windows_launcher.rs
  • experiments/windows-sandbox/launcher/src/windows_launcher_tests.rs
  • packages/runtime/src/__tests__/default-sandbox-manager.test.ts
  • packages/runtime/src/sandbox/default-sandbox-manager.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/architecture/windows-sandbox-rfc-v1.zh-CN.md

Included review availability: Your plan includes up to 3 reviews per rolling hour; 2 remain after this review.

Comment thread experiments/windows-sandbox/launcher/src/acl_ledger.rs
liugddx added a commit to liugddx/maka-agent that referenced this pull request Aug 18, 2026
Non-interactive AppContainer workers previously inherited the creator's
interactive Winsta0\Default desktop, leaving the same-session GUI attack
surface (window-message shatter into the user's interactive windows, desktop
hooks) open — AppContainer confines files/network/token but not the desktop
scope. Per RFC v1 §6.3 the private-desktop promise was designed but deferred.

Enforce the desktop-scoped half under an explicit no-Win32k contract: each
AppContainer launch (production create + readiness probe) now stands up a
per-launch alternate desktop on the existing window station, with a protected
DACL that grants only the launching user, Local System, and the child's
AppContainer SID the minimal DESKTOP_* rights (create window/menu, read/write
objects, enumerate) and never DESKTOP_SWITCHDESKTOP / HOOKCONTROL / journal.
STARTUPINFOEXW.lpDesktop points the child at it. If the desktop cannot be
created or the DACL cannot be granted (CreateProcessW then fails ACCESS_DENIED),
the launch fails closed — auto/require never fall back to the host desktop.

Scope, stated honestly (per maintainer review on apache#3174):
- This keeps the worker off the interactive Default desktop, so it cannot
  enumerate/post messages to the user's interactive windows or install desktop
  hooks against them.
- It does NOT isolate the clipboard: the clipboard belongs to the window
  station, which both desktops still share. Clipboard isolation with a dedicated
  window station is a later hardening gate (§6.5).
- The create-window/write DACL rights are granted but not relied upon; the
  worker does no GUI work (no-Win32k contract). A verified Low mandatory-
  integrity label proving those rights are usable at AppContainer's Low IL is
  deferred (§6.5).

The boundary probe self-attests its own desktop name and refuses to run on
Default; appcontainer-smoke asserts the private-desktop name prefix and rejects
Default. Digest is unchanged: the desktop is a launch-time detail and does not
enter the manifest.

Stacked on apache#3161 (readiness probe).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5
@liugddx

liugddx commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

Thanks — all four lifecycle blockers are addressed in c8b6527d2 (pushed). Point-by-point:

1. Namespace overlap → dedicated, enforced readiness namespace.

  • RESERVED_READINESS_REQUEST_ID = "readiness-probe" now lives in protocol.rs as the single source of truth, and LaunchRequest::validate rejects it (requestId 'readiness-probe' is reserved for the internal readiness probe). A production launch can never carry it.
  • Enforcement is structural, not just validation: the probe profile is derived by a separate appcontainer_readiness_profile_name() under a disjoint maka.readiness.<hash> prefix, while production stays under maka.sandbox.<hash>. Even if validate were bypassed, the two namespaces cannot collide by construction.
  • Tested beyond a single sample ID: validate_rejects_reserved_readiness_request_id asserts rejection (and that an ordinary id is otherwise valid, so the id is the sole reason), and readiness_namespace_is_disjoint_from_production feeds the reserved id through the production deriver and asserts it still lands under maka.sandbox., disjoint from maka.readiness..

2. Cross-process serialization.

  • The whole delete → create → probe → settle → drop window is now serialized across processes by a DACL-hardened named mutex, acquired (RAII) before create_readiness and held until after drop(profile). I reused the ACL ledger's existing LedgerLock rather than adding a second mutex implementation, so the readiness lease inherits the same SYSTEM+owner-only DACL and WAIT_ABANDONED handling. Its object name is Global\Maka.WindowsSandbox.ReadinessProfile.v1.{user_sid}Global\ and per-user for the same reason the ledger mutex is: the AppContainer profile is a per-user, machine-wide registration, so contention is cross-session. Timeout → fail closed (never an unlocked delete/create).
  • On the concurrent real-Windows test: I've deliberately not added one, and I'd rather make the case than silently skip it. The fix here is the serialization primitive itself; a multi-process real-machine race test would have to spawn N processes each standing up a real AppContainer profile and assert the absence of an interleaving — inherently timing-dependent and a classic CI flake source, disproportionate for a throwaway diagnostic probe. Instead I cover the contract deterministically: readiness_lease_name_is_scoped_and_distinct pins the lease name/scope and its distinctness from the ledger mutex, and the serialization is a direct reuse of the already-tested LedgerLock. I've logged the cross-process real-machine race integration test as an explicit deferred gate in RFC §6.5 rather than claiming it's covered. Happy to add it if you consider it merge-blocking.

3. Unsettled state preserved.

  • probe_appcontainer_child now takes unsettled: &mut bool and sets *unsettled = settled.is_err() — independent of verify, so a verification failure whose Job did drain cleanly still deletes, but any unproven-drain preserves. readiness_probe calls profile.preserve() before drop(profile) when unsettled, and Drop skips DeleteAppContainerProfile when keep_on_drop is set. This mirrors the production LaunchFailure::Unsettled rule: the identity is not deleted/reused until cleanup is proven; the preserved registration is reclaimed by the next lease-holding create_readiness (safe because of point 2's serialization).

4. Bounded negative caching + strictly cache-only hot path.

  • windowsReadinessCache is now Map<string, { available, expiresAt }>. Positive → expiresAt = Infinity (host capability doesn't regress in-process). Negative → now() + 60_000 (a transient timeout / AV contention / spawn failure re-probes on the next composition warm, no longer poisoning until restart).
  • The backend's synchronous isAvailable hot path is now strictly cache-only via a new readCachedWindowsReadiness (never spawns; expired negatives read as false, fail closed). Only the composition warmer (probeWindowsReadiness) spawns, and it is TTL-aware. New tests cover TTL expiry + recovery, positive-permanence, and that the hot path never spawns even on an expired entry.

Validation on this head: cargo fmt --check, cargo build --locked, cargo test --locked (45 passing, incl. the 4 new readiness tests), the W0 readiness + appcontainer smokes, full local PowerShell smoke regression, and node --test + biome check on the runtime changes. #3174 has been rebased onto this head (content unchanged, re-validated).

中文

四条 lifecycle blocker 均已在 c8b6527d2 修复:

  1. 命名空间重叠:RESERVED_READINESS_REQUEST_ID 收敛到 protocol.rs 单一事实源,validate 直接拒绝该 id;probe profile 走独立 appcontainer_readiness_profile_name(),前缀 maka.readiness. 与生产 maka.sandbox. 结构性不相交,即便绕过 validate 也无法撞名。测试不再只对单个样本 id:新增拒绝测试 + 把保留 id 喂给生产 deriver 断言仍落在 maka.sandbox.
  2. 跨进程串行:整个 delete→create→probe→settle→drop 由 DACL 加固命名互斥量(复用 ledger 的 LedgerLock,非另造一套)串行,名为 Global\Maka.WindowsSandbox.ReadinessProfile.v1.{user_sid} —— Global\+按用户与 ledger 同因(AppContainer profile 是按用户、机器范围注册,竞争是跨会话的);超时即 fail closed。关于并发真机测试:我据实未加——多进程真机竞态测试要拉起 N 个进程各建真 AppContainer profile 并断言"无交错",本质 timing 相关、是典型 CI flake 源,对一个抛弃式诊断探针不成比例;改为确定性覆盖锁名/作用域并复用已测的 LedgerLock,并把该集成测试作为显式 deferred gate 记入 RFC §6.5,而非假装已覆盖。若你认为是合并必需,我可以补。
  3. 未证清空保留:probe_appcontainer_childunsettled: &mut bool,*unsettled = settled.is_err()(独立于 verify);readiness_probe 在 drop 前 preserve(),Dropkeep_on_drop 时跳过删除。对齐生产 LaunchFailure::Unsettled:未证清空不删不复用,保留的注册由下一次持锁 create_readiness reclaim(因 point 2 串行而安全)。
  4. 负结果有界 + 热路径严格 cache-only:缓存改为 {available, expiresAt},正结果永久、负结果 60s TTL;后端同步 isAvailable 走新的 readCachedWindowsReadiness(从不 spawn,过期负结果读作 false),仅 composition warmer spawn 且 TTL-aware。新测试覆盖 TTL 过期+恢复、正结果永久、热路径永不 spawn。

本 head 验证:cargo fmt/build/test(45 通过,含 4 个新 readiness 测试)、W0 readiness+appcontainer 冒烟、本地全量 PS 冒烟回归、runtime 的 node --test + biome。#3174 已 rebase 到本 head(内容不变,已复验)。

@hqhq1025 hqhq1025 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.

Follow-up review on current head c8b6527d2.

The namespace collision, cross-process serialization, and combined verification/settlement handling from my previous review are substantially improved. Two lifecycle blockers remain:

  1. The 60-second negative TTL does not produce recovery in a running Runtime Host. probeWindowsReadiness() is warmed only while createExecutionRuntimeHostComposition() is built. If that initial probe fails, the filesystem worker and its launch-spec provider are not constructed. Later backend checks only call readCachedWindowsReadiness(), which never spawns, and there is no timer, retry owner, or dynamic worker publication path. Expiry therefore changes the cached read from “negative” to “absent”, but nothing re-probes until the Runtime Host/composition is restarted. Please either add a real async readiness state machine that retries and publishes the worker, or document and test restart-required recovery rather than claiming TTL recovery.

  2. preserve() does not make an unsettled readiness identity safe to reuse. On settlement failure, the profile is preserved only until readiness_probe() returns; the named-mutex lease is then released. The next probe acquires the lease and create_readiness() unconditionally calls DeleteAppContainerProfile for that same fixed identity while the previous child may still be alive outside the drained Job. The delete result is ignored, and creation proceeds. Please quarantine an unsettled identity durably until cleanup is proven, or use unique probe identities plus an orphan/reconciliation ledger. Add a real Windows test covering an unsettled child followed by another probe.

CI is fully green, and local build/focused tests/Biome/Rust formatting and cross-target checks pass. Those checks do not exercise either failure path above, so this revision is not ready to merge yet.

@M4n5ter M4n5ter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

English

Follow-up review on c8b6527d2

I completed a full review of the current PR head, including the runtime availability authority, Runtime Host composition lifecycle, native AppContainer/Job/profile lifecycle, protocol namespace, CI smoke coverage, and both RFC variants.

No additional merge blocker needs a duplicate comment. The production recovery gap is already covered precisely by hqhq1025’s current-head review: the negative entry expires, but the Runtime Host has no retry owner and the filesystem worker was permanently omitted from the immutable composition. I independently confirmed that finding and consider it blocking.

The unsettled-profile reclaim concern in the same review is technically valid, but I would classify it as a follow-up under the current threshold: it requires native settlement itself to fail, the child is a fixed no-roots readiness command, and the production namespace is structurally disjoint. The implementation and RFC should still stop claiming that the mutex serializes reclaim against a potentially surviving descendant.

I added two non-blocking follow-ups:

  1. pass /d to cmd.exe so machine/user Command Processor\AutoRun configuration cannot contaminate readiness;
  2. remove the now-redundant reserved production requestId, because the maka.sandbox.* / maka.readiness.* namespace split already proves non-collision.

I did not duplicate CodeRabbit’s existing RFC-attribution or cross-user mutex-squatting comments.

Correctness verdict: not ready until the production recovery behavior matches the TTL claim, or the design explicitly adopts restart-required recovery.

Design verdict: the recovery authority must live either in a retrying/dynamically published Runtime Host state or in a deliberately one-shot startup decision. The current composition mixes the latter ownership model with TTL state from the former.

Validation on exact head c8b6527d22603c07aa9be4487ed8cc230bcb5132: 15/15 focused runtime tests passed, Biome passed, cargo fmt --check passed, Windows GNU cross-target cargo check --locked passed, git diff --check passed, and all current GitHub checks are green. Native Windows behavior was not rerun locally; the Windows 2025 CI lane remains the execution evidence.

简体中文

c8b6527d2 后续 review

我已完成当前 PR head 的全量复审,覆盖 runtime availability authority、Runtime Host composition 生命周期、原生 AppContainer/Job/profile 生命周期、协议命名空间、CI smoke 覆盖和双语 RFC。

没有需要重复发布的新 blocker。生产恢复缺口已经由 hqhq1025 针对当前 head 的 review 精确覆盖:负缓存虽然会过期,但 Runtime Host 没有 retry owner,filesystem worker 也已从不可变 composition 中永久省略。我已独立确认该问题,并认为它阻塞合并。

同一 review 中 unsettled profile 的 reclaim 问题在技术上成立,但按当前门槛我会将其归为 follow-up:它需要原生 settlement 本身失败,child 是固定且无 roots 的 readiness 命令,生产命名空间也已结构性隔离。不过,实现和 RFC 仍不应声称 mutex 能把 reclaim 与一个可能存活的 descendant 串行起来。

我新增了两个非阻塞 follow-up:

  1. cmd.exe 添加 /d,避免机器或用户的 Command Processor\AutoRun 配置污染 readiness;
  2. 删除现在已经冗余的生产 requestId 保留值,因为 maka.sandbox.* / maka.readiness.* 命名空间分离已经证明不会碰撞。

我没有重复 CodeRabbit 已有的 RFC 归属和跨用户 mutex squatting 评论。

Correctness 结论: 在生产恢复行为与 TTL 声明一致,或者设计明确改为“恢复需要重启”之前,不宜合并。

Design 结论: 恢复 authority 应当明确归属于可重试、可动态发布的 Runtime Host state,或者明确采用一次性启动决策。当前 composition 使用后者的 ownership,却又维护了属于前者的 TTL state。

精确 head c8b6527d22603c07aa9be4487ed8cc230bcb5132 的验证结果:focused runtime tests 15/15 通过,Biome 通过,cargo fmt --check 通过,Windows GNU cross-target cargo check --locked 通过,git diff --check 通过,当前 GitHub checks 全绿。本地没有重新运行原生 Windows 行为;该部分仍以 Windows 2025 CI lane 为执行证据。

Comment thread experiments/windows-sandbox/launcher/src/windows_launcher.rs Outdated
{
return Err("requestId must use 1-128 safe ASCII characters".to_owned());
}
if self.request_id == RESERVED_READINESS_REQUEST_ID {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

English

Follow-up — remove the redundant reserved request ID

The new structural namespace split already makes the identities disjoint: every production request derives maka.sandbox.*, while readiness derives maka.readiness.*. Even the literal requestId == "readiness-probe" cannot resolve to the readiness profile.

This validation branch therefore adds no remaining collision protection, but it introduces a forbidden protocol value, an extra test/documentation contract, and coupling between production request validation and an internal probe seed. Keep the readiness seed private and rely on the existing namespace-disjointness test; the reservation can disappear.

简体中文

Follow-up — 删除冗余的保留 request ID

新的结构性命名空间已经保证 identity 不相交:所有生产请求都派生为 maka.sandbox.*,readiness 则派生为 maka.readiness.*。即使生产请求的字面值是 requestId == "readiness-probe",也不可能解析到 readiness profile。

因此,这个 validation 分支已经不再提供额外的碰撞保护,却新增了一个协议禁用值、一组测试/文档契约,以及生产请求校验对内部 probe seed 的耦合。可以把 readiness seed 保持为私有常量,依靠现有的 namespace-disjointness 测试,并删除这层 reservation。

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Keeping this one deliberately, as defense in depth rather than redundancy — reasons: (1) the namespace split is the structural guarantee, but it lives in windows_launcher.rs; the validate() rejection is the only guard at the protocol boundary itself, and it keeps the two files independently safe against drift (a future edit that unifies the prefixes would otherwise silently re-open the collision); (2) without the rejection, a production request literally named readiness-probe would work while aliasing the probe's identity in logs, ledger names, and diagnostics — rejecting it with a typed error is clearer than letting the ambiguity exist; (3) the cost is four lines plus one test. If you feel strongly this should go, I'd rather drop it in a follow-up than churn this stack again — but my recommendation is to keep it.

中文:有意保留作为纵深防御:命名空间分离是结构性保证但在另一文件,validate 拒绝是协议边界上唯一的守卫,防止未来前缀统一类改动静默重开碰撞;且不拒绝的话生产请求可合法叫 readiness-probe,在日志/账本命名上与探针身份混淆。成本仅四行+一测。若坚持删除建议放后续 PR,不再折腾本 stack。

The readiness probe launched its throwaway child as `"<cmd>" /c exit 0`
without `/d`, so cmd.exe ran the machine-wide
`Software\Microsoft\Command Processor\AutoRun` value before the payload.
Because AutoRun is machine-constant, a host that ships an AutoRun which
exits non-zero would make *every* probe report failure, and one that
ships a blocking AutoRun would hang the child until the Job drain times
out -- either way the Windows sandbox fails closed on that machine on
every startup, independent of whether the OS can actually stand up the
boundary. The probe would then be measuring the host's shell
customization instead of the sandbox.

Route the command line through a pure `readiness_probe_command_line`
helper that emits `"<cmd>" /d /c exit 0`, matching the `/d` the
production launch path already passes, and add a unit test asserting
`/d` precedes `/c`. Also correct the RFC (EN + zh): the readiness probe
and its serialized profile lifecycle are added by this PR (apache#3161), not
the merged apache#2961 preview slice -- tag those two bullets accordingly and
note the AutoRun-disabled command line.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5
liugddx added a commit to liugddx/maka-agent that referenced this pull request Aug 18, 2026
Non-interactive AppContainer workers previously inherited the creator's
interactive Winsta0\Default desktop, exposing the same-session GUI attack
surface (window-message shatter into the user's interactive windows, desktop
hooks) — AppContainer confines files/network/token but not the desktop scope.
Per RFC v1 §6.3 the private-desktop promise was designed but deferred.

This change enforces initial-desktop *placement* plus DACL protection, not
escape-proof confinement. Each AppContainer launch (production create +
readiness probe) stands up a per-launch alternate desktop on the existing
window station, with a protected DACL: a leading deny ACE strips
DESKTOP_SWITCHDESKTOP / HOOKCONTROL / journal record+playback (0x138) from the
launching-user SID — which the AppContainer child's token still carries as an
effective SID, so without the deny the owner's full-control allow would name
the child as a grantee of those rights (maintainer review, P2-1) — then allow
ACEs grant the launching user and Local System control for cleanup and the
child's AppContainer SID only the minimal DESKTOP_* rights (create
window/menu, read/write objects, enumerate). STARTUPINFOEXW.lpDesktop points
the child at it. If the desktop cannot be created or the DACL cannot be
granted (CreateProcessW then fails ACCESS_DENIED), the launch fails closed —
auto/require never fall back to the host desktop.

Scope, stated honestly (per maintainer review on apache#3174):
- lpDesktop selects only the child's *initial* desktop. This places the worker
  off the interactive Default desktop and DACL-protects the private one, but it
  does NOT structurally confine: absent a no-Win32k mitigation, a dedicated
  window station, or a token boundary, in-process code can still
  OpenDesktopW("Default") + SetThreadDesktop to re-attach. Enforcing a no-Win32k
  mitigation is a deferred gate (§6.5).
- It does NOT isolate the clipboard: the clipboard belongs to the window
  station, which both desktops still share. Clipboard isolation with a dedicated
  window station is a later hardening gate (§6.5).
- The create-window/write DACL rights are granted but not relied upon; the
  worker does no GUI work. A verified Low mandatory-integrity label proving
  those rights are usable at AppContainer's Low IL is deferred (§6.5).

The boundary probe self-attests its own initial desktop name
(desktopPrivatePlacement) and requires the launcher-owned
maka-sandbox-desktop. prefix — not merely "not Default", so landing on any
other pre-existing desktop also fails the placement check; appcontainer-smoke
asserts the same prefix and rejects Default. Digest is unchanged: the desktop
is a launch-time detail and does not enter the manifest.

Stacked on apache#3161 (readiness probe).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5
liugddx added a commit to liugddx/maka-agent that referenced this pull request Aug 18, 2026
Non-interactive AppContainer workers previously inherited the creator's
interactive Winsta0\Default desktop, exposing the same-session GUI attack
surface (window-message shatter into the user's interactive windows, desktop
hooks) — AppContainer confines files/network/token but not the desktop scope.
Per RFC v1 §6.3 the private-desktop promise was designed but deferred.

This change enforces initial-desktop *placement* plus DACL protection, not
escape-proof confinement. Each AppContainer launch (production create +
readiness probe) stands up a per-launch alternate desktop on the existing
window station, with a protected DACL: a leading deny ACE strips
DESKTOP_SWITCHDESKTOP / HOOKCONTROL / journal record+playback (0x138) from the
launching-user SID — which the AppContainer child's token still carries as an
effective SID, so without the deny the owner's full-control allow would name
the child as a grantee of those rights (maintainer review, P2-1) — then allow
ACEs grant the launching user and Local System control for cleanup and the
child's AppContainer SID only the minimal DESKTOP_* rights (create
window/menu, read/write objects, enumerate). STARTUPINFOEXW.lpDesktop points
the child at it. If the desktop cannot be created or the DACL cannot be
granted (CreateProcessW then fails ACCESS_DENIED), the launch fails closed —
auto/require never fall back to the host desktop.

Scope, stated honestly (per maintainer review on apache#3174):
- lpDesktop selects only the child's *initial* desktop. This places the worker
  off the interactive Default desktop and DACL-protects the private one, but it
  does NOT structurally confine: absent a no-Win32k mitigation, a dedicated
  window station, or a token boundary, in-process code can still
  OpenDesktopW("Default") + SetThreadDesktop to re-attach. Enforcing a no-Win32k
  mitigation is a deferred gate (§6.5).
- It does NOT isolate the clipboard: the clipboard belongs to the window
  station, which both desktops still share. Clipboard isolation with a dedicated
  window station is a later hardening gate (§6.5).
- The create-window/write DACL rights are granted but not relied upon; the
  worker does no GUI work. A verified Low mandatory-integrity label proving
  those rights are usable at AppContainer's Low IL is deferred (§6.5).

The boundary probe self-attests its own initial desktop name
(desktopPrivatePlacement) and requires the launcher-owned
maka-sandbox-desktop. prefix — not merely "not Default", so landing on any
other pre-existing desktop also fails the placement check; appcontainer-smoke
asserts the same prefix and rejects Default. Digest is unchanged: the desktop
is a launch-time detail and does not enter the manifest.

Stacked on apache#3161 (readiness probe).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5
@liugddx

liugddx commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

Thanks @Astro-Han — acted on this at head 04892c7dd, point by point:

P2-2 (/d missing) — fixed. You're right, and this was worth fixing before merge: AutoRun is machine-constant, so a non-zero-exiting or blocking AutoRun would fail the probe closed on every startup, and stacked on the restart-scoped recovery it would disable the sandbox on that machine permanently while measuring the host's shell customization instead of the boundary. The command line now routes through a pure readiness_probe_command_line() helper emitting "<cmd>" /d /c exit 0 (same /d-before-/c contract as the production launch path), with a unit test asserting /d precedes /c. Real-machine readiness-probe-smoke.ps1 re-verified green. (Credit also to @M4n5ter, who listed this as a follow-up.)

P2-1 (transient startup failure → restart-scoped recovery) — unchanged, explicitly deferred. As you noted, this is documented and tested as the RFC §6.5 "Active running-host readiness recovery" gate: recovery = new composition build or restart. If the team wants in-process recovery instead, that's the async readiness state machine + dynamic worker publication follow-up — out of scope for this PR.

P3 RFC attribution — fixed. §6.5 no longer credits the readiness probe to merged #2961: the two readiness bullets are now tagged (#3161) and the intro states that tagged bullets land in the follow-up PR, not the merged slice. (EN + zh.)

P3 tautological assertions — acknowledged, not churned. Agreed the two equal-hash assertions in readiness_profile_name_is_fixed_and_self_reconciling prove nothing by themselves; the assertions that carry weight there are the maka.readiness. prefix and the fixed rendered length, and the real contract lives in readiness_namespace_is_disjoint_from_production + validate_rejects_reserved_readiness_request_id. Not re-pushing the stack for a test-shape nicety; happy to fold it into any future readiness change.

P3 positive-cache-forever / composition-time spawnSync — documented tradeoffs, no change. In-place binary replacement without restart re-uses the positive result until the process restarts (updates restart in practice); the one-time worst-case 15s composition spawn is the price of never spawning on the hot path.

One meta note: rather than only patching the quoted lines, we also swept both PRs for any remaining spots where documented claims exceed what the code enforces — that sweep is what surfaced the residual overclaimed comment fixed on #3174.

中文

P2-2(缺 /d)— 已修。 判断正确且值得合并前修:AutoRun 每机恒定,exit 非 0 或长驻的 AutoRun 会让每次探针 fail closed,叠加"恢复=重启"即该机沙箱永久不可用——探针测的是宿主 shell 定制而非边界。命令行改走纯函数 readiness_probe_command_line()"<cmd>" /d /c exit 0,与生产路径同一 /d 契约),新增单测断言 /d/c 前;真机 readiness 冒烟复验绿。(也感谢 @M4n5ter 先前列为 follow-up。)

P2-1(瞬时启动失败→重启级恢复)— 不改,显式延后。 如你所述已按 RFC §6.5 "Active running-host readiness recovery" 门禁文档化+测试化:恢复 = 新 composition 构建或重启;进程内恢复属异步状态机+动态 worker 发布的后续工作。

P3 RFC 归属 — 已修。 §6.5 两条 readiness 条目标注 (#3161),导语明确标注条目属后续 PR 而非已合并切片(中英同步)。

P3 同义反复断言 — 认同,不为此重推。 双哈希相等确实无证明力;该测试有效的是前缀+长度断言,真契约在 disjoint + validate 两测试。后续 readiness 变更时顺手收掉。

P3 正结果永久缓存 / composition 期 spawnSync — 已文档化的权衡,不改。

另:本轮没有只改被引用的行,而是对两个 PR 全量清扫了"声明超出代码强制"的残留——#3174 上又扫出并修掉了一处过度声明注释。

@Astro-Han Astro-Han 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.

The readiness proof now matches the boundary it protects: the launcher verifies the AppContainer identity and Job assignment on the actual host, Runtime warms that result before admitting the filesystem worker, and the backend hot path consumes the cached authority instead of running a second probe. Failure and concurrency paths remain fail-closed, with focused evidence for SID mismatch, Job membership, timeouts, and profile cleanup.

I found no new reproducible P0-P2 issue on the latest head. The remaining hardening ideas are non-blocking and overlap existing review discussion; the live checks are green.

Review performed with Codex reviewer agents and DeepSeek V4 Flash as advisory tools; I verified the conclusion against the latest head and current main.

中文评论

当前 readiness proof 与受保护边界一致:launcher 在真实 host 上验证 AppContainer identity 与 Job assignment,Runtime 在 filesystem worker admission 前预热结果,backend 热路径只消费这一缓存权威,不再运行第二套探测。SID mismatch、Job membership、timeout 和 profile cleanup 的失败与并发路径均保持 fail-closed,并有针对性证据。

最新 head 上未发现新的可复现 P0-P2 问题。其余强化建议均为非阻塞,且已与现有 review 讨论重合;实时检查全部通过。

本次审查使用了 Codex reviewer agents 与 DeepSeek V4 Flash 作为辅助工具;我已依据最新 head 和当前 main 复核结论。

The named-lock comment claimed a squatted Global\ name "fails closed at
acquisition", but that only held for a squatter with a restrictive DACL:
CreateMutexW ignores the supplied security descriptor when the name
already exists, so a local user pre-creating the predictable mutex name
with a *permissive* DACL would hand the launcher an attacker-owned
arbitration object -- readiness and ACL-ledger operations would then
block on a mutex the squatter can hold forever (availability DoS), while
the code read as if the DACL had rejected them.

Close the gap where it is checkable: when CreateMutexW reports
ERROR_ALREADY_EXISTS (also the normal same-user contention path), read
the existing object's owner via GetSecurityInfo before any wait and
require it to be the current user or SYSTEM. Ownership is the one
property a permissive squatter cannot forge -- re-owning an object to
another SID requires SeTakeOwnership/SeRestore, which standard users do
not hold. Any other owner fails closed with an explicit
squatted-mutex error instead of blocking. The 10-way concurrency smoke
(same-user contention) passes unchanged; the cross-user negative path
cannot be exercised by an unprivileged CI runner and is enforced by the
owner check itself.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5
liugddx added a commit to liugddx/maka-agent that referenced this pull request Aug 18, 2026
Non-interactive AppContainer workers previously inherited the creator's
interactive Winsta0\Default desktop, exposing the same-session GUI attack
surface (window-message shatter into the user's interactive windows, desktop
hooks) — AppContainer confines files/network/token but not the desktop scope.
Per RFC v1 §6.3 the private-desktop promise was designed but deferred.

This change enforces initial-desktop *placement* plus DACL protection, not
escape-proof confinement. Each AppContainer launch (production create +
readiness probe) stands up a per-launch alternate desktop on the existing
window station, with a protected DACL: a leading deny ACE strips
DESKTOP_SWITCHDESKTOP / HOOKCONTROL / journal record+playback (0x138) from the
launching-user SID — which the AppContainer child's token still carries as an
effective SID, so without the deny the owner's full-control allow would name
the child as a grantee of those rights (maintainer review, P2-1) — then allow
ACEs grant the launching user and Local System control for cleanup and the
child's AppContainer SID only the minimal DESKTOP_* rights (create
window/menu, read/write objects, enumerate). STARTUPINFOEXW.lpDesktop points
the child at it. If the desktop cannot be created or the DACL cannot be
granted (CreateProcessW then fails ACCESS_DENIED), the launch fails closed —
auto/require never fall back to the host desktop.

Scope, stated honestly (per maintainer review on apache#3174):
- lpDesktop selects only the child's *initial* desktop. This places the worker
  off the interactive Default desktop and DACL-protects the private one, but it
  does NOT structurally confine: absent a no-Win32k mitigation, a dedicated
  window station, or a token boundary, in-process code can still
  OpenDesktopW("Default") + SetThreadDesktop to re-attach. Enforcing a no-Win32k
  mitigation is a deferred gate (§6.5).
- It does NOT isolate the clipboard: the clipboard belongs to the window
  station, which both desktops still share. Clipboard isolation with a dedicated
  window station is a later hardening gate (§6.5).
- The create-window/write DACL rights are granted but not relied upon; the
  worker does no GUI work. A verified Low mandatory-integrity label proving
  those rights are usable at AppContainer's Low IL is deferred (§6.5).

The boundary probe self-attests its own initial desktop name
(desktopPrivatePlacement) and requires the launcher-owned
maka-sandbox-desktop. prefix — not merely "not Default", so landing on any
other pre-existing desktop also fails the placement check; appcontainer-smoke
asserts the same prefix and rejects Default. Digest is unchanged: the desktop
is a launch-time detail and does not enter the manifest.

Stacked on apache#3161 (readiness probe).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5
@liugddx

liugddx commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

Head is now a59c2554e (one commit on top of the approved 04892c7dd), addressing the remaining inline threads: CodeRabbit's mutex-squatting finding is fixed — on ERROR_ALREADY_EXISTS the pre-existing lock's owner is read via GetSecurityInfo before any wait and must be the current user or SYSTEM, otherwise acquisition fails closed with an explicit squatted-mutex error (the old comment claiming "a squatted name fails closed" was itself a claim-exceeding-enforcement instance: CreateMutexW ignores our DACL for an existing object, so only a restrictive squat failed; a permissive squat handed us an attacker-owned lock). Copilot's outdated threads and M4n5ter's follow-ups are replied per-thread; the reserved requestId is kept as defense-in-depth with reasoning in-thread. Same-user 10-way contention smoke passes unchanged.

中文

head 更新为 a59c2554e(在已批准的 04892c7dd 上加一个 commit):修复 CodeRabbit 的 mutex 抢注问题——ERROR_ALREADY_EXISTS 时等待前经 GetSecurityInfo 读取既有锁 owner,必须为当前用户或 SYSTEM,否则显式报"抢注"并 fail closed(旧注释"抢注即失败"本身又是一处声明超出强制:CreateMutexW 对既有对象忽略我方 DACL,宽 DACL 抢注会把攻击者持有的锁交给我们)。Copilot 过时线程与 M4n5ter follow-up 已逐线程回复;保留 requestId 的理由见线程。同用户十路争用冒烟不变通过。

@Astro-Han Astro-Han 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.

The new cross-user mutex-squatting defense is the right problem, but the current identity rule rejects the legitimate owner produced by the elevated Windows runner and fails the required W0 check.

The first-principles fix is not to widen trust to the whole Administrators group. The creator and validator should share one explicit owner identity: set the mutex security descriptor owner to the intended SID, then require that exact SID on reuse. This preserves fail-closed cross-user protection without breaking same-user elevated acquisition.

Reviewed with Codex using two independent review passes and DeepSeek V4 Flash as an external adversarial pass; I verified the owner path and live required-check failure against this exact head.

中文

防止跨用户 mutex squatting 的问题定义正确,但当前 identity 规则会拒绝 elevated Windows runner 产生的合法 owner,并导致 required W0 失败。

第一性原理下,不应直接扩大为信任整个 Administrators group。创建方与校验方应共享一个显式 owner identity:安全描述符设置预期 SID,复用时只接受该 SID。这样既保持跨用户 fail-closed,也不破坏同用户 elevated acquisition。

本次由 Codex 两轮独立审查,并使用 DeepSeek V4 Flash 做外部对抗审查;我核对了当前 head 和 required check。

return Err(error);
}
if already_exists {
if let Err(error) = validate_existing_lock_owner(handle, user_sid) {

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.

P1 — The new owner rule rejects legitimate elevated acquisitions. Required Windows CI reports the existing mutex owner as S-1-5-32-544 (Administrators), not TokenUser or SYSTEM, so the same workflow fails before waiting on its lock. Set an explicit expected owner SID when creating the mutex and validate that exact identity on reuse (or consistently use the token's actual owner); do not broadly accept arbitrary Administrators owners. Add repeated elevated acquisition coverage and rerun W0.

…ting locks

The squatted-lock owner check rejected locks owned by
BUILTIN\Administrators (S-1-5-32-544), which broke on elevated hosts:
an elevated administrator token stamps the Administrators group -- not
the user SID -- as the default owner on objects it creates, so a mutex
this very code created earlier on the elevated CI runner failed the
{user, SYSTEM} check and the W0 protocol lane went red
(unsettled_launch_preserves_grants_and_quarantines_the_ledger).

Accept the process token's default-owner SID (TokenOwner) as a third
legitimate owner. This stays inside the threat model: only an elevated
administrator can create objects owned by Administrators, and RFC
S1/S5 explicitly does not defend against administrators. Standard-user
squatters are still rejected -- they cannot forge either the user SID
or the Administrators owner. Adds a unit test pinning the helper to
{user SID, S-1-5-32-544} so both elevation states stay covered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5
liugddx added a commit to liugddx/maka-agent that referenced this pull request Aug 18, 2026
Non-interactive AppContainer workers previously inherited the creator's
interactive Winsta0\Default desktop, exposing the same-session GUI attack
surface (window-message shatter into the user's interactive windows, desktop
hooks) — AppContainer confines files/network/token but not the desktop scope.
Per RFC v1 §6.3 the private-desktop promise was designed but deferred.

This change enforces initial-desktop *placement* plus DACL protection, not
escape-proof confinement. Each AppContainer launch (production create +
readiness probe) stands up a per-launch alternate desktop on the existing
window station, with a protected DACL: a leading deny ACE strips
DESKTOP_SWITCHDESKTOP / HOOKCONTROL / journal record+playback (0x138) from the
launching-user SID — which the AppContainer child's token still carries as an
effective SID, so without the deny the owner's full-control allow would name
the child as a grantee of those rights (maintainer review, P2-1) — then allow
ACEs grant the launching user and Local System control for cleanup and the
child's AppContainer SID only the minimal DESKTOP_* rights (create
window/menu, read/write objects, enumerate). STARTUPINFOEXW.lpDesktop points
the child at it. If the desktop cannot be created or the DACL cannot be
granted (CreateProcessW then fails ACCESS_DENIED), the launch fails closed —
auto/require never fall back to the host desktop.

Scope, stated honestly (per maintainer review on apache#3174):
- lpDesktop selects only the child's *initial* desktop. This places the worker
  off the interactive Default desktop and DACL-protects the private one, but it
  does NOT structurally confine: absent a no-Win32k mitigation, a dedicated
  window station, or a token boundary, in-process code can still
  OpenDesktopW("Default") + SetThreadDesktop to re-attach. Enforcing a no-Win32k
  mitigation is a deferred gate (§6.5).
- It does NOT isolate the clipboard: the clipboard belongs to the window
  station, which both desktops still share. Clipboard isolation with a dedicated
  window station is a later hardening gate (§6.5).
- The create-window/write DACL rights are granted but not relied upon; the
  worker does no GUI work. A verified Low mandatory-integrity label proving
  those rights are usable at AppContainer's Low IL is deferred (§6.5).

The boundary probe self-attests its own initial desktop name
(desktopPrivatePlacement) and requires the launcher-owned
maka-sandbox-desktop. prefix — not merely "not Default", so landing on any
other pre-existing desktop also fails the placement check; appcontainer-smoke
asserts the same prefix and rejects Default. Digest is unchanged: the desktop
is a launch-time detail and does not enter the manifest.

Stacked on apache#3161 (readiness probe).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5
@liugddx

liugddx commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

CI red on a59c2554e was the new squatted-lock owner check being too strict for elevated hosts: an elevated administrator token stamps BUILTIN\Administrators (S-1-5-32-544) — not the user SID — as the default owner on objects it creates, so a mutex this same code created earlier on the elevated CI runner failed the {user, SYSTEM} check and windows_sandbox_w0_protocol went red (all other 19 lanes were green). Fixed in 0df509bc3: the check now also accepts the process token's default-owner SID (TokenOwner). This stays inside the threat model — only an elevated administrator can create Administrators-owned objects, and RFC §1/§5 explicitly does not defend against administrators; standard-user squatters still cannot forge either accepted owner. Added a unit test pinning the helper to {user SID, S-1-5-32-544} so both elevation states stay covered.

中文:CI 红因新 owner 校验对提权宿主过严——提权管理员 token 创建对象的默认 owner 是 BUILTIN\Administrators 而非用户 SID,CI runner 上自己建的锁被误判为抢注。0df509bc3 修复:额外接受本进程 token 的默认 owner SID(TokenOwner),仍在威胁模型内(RFC 明确不抗管理员);标准用户抢注者两个 owner 都伪造不了。附单测覆盖两种提权态。

@liugddx
liugddx requested review from M4n5ter and hqhq1025 August 18, 2026 15:37

@hqhq1025 hqhq1025 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.

Follow-up review on current head 0df509bc3.

No remaining merge-blocking finding.

Problem and mechanism

The PR fixes a real availability-authority bug: launcher-file presence did not prove that the host could create and enforce the AppContainer identity, exact SID, specific Job assignment, and child lifecycle. The implementation now probes those host invariants once at composition time, shares the result with the cache-only backend hot path, and fails closed instead of advertising a worker whose boundary cannot be created. That problem definition and mechanism follow first principles and avoid a second availability authority.

The previous blockers are resolved or honestly scoped: running-host recovery is explicitly deferred rather than claimed; ineffective profile preservation was removed; readiness and production identities are structurally disjoint; lifecycle mutation is serialized; /d makes the shell probe hermetic; and the existing-lock owner check closes the permissive mutex-squatting path within the stated non-administrator threat model.

Optimality and deletion

For this preview scope, the current architecture is sufficient and a dynamic readiness state machine or durable orphan ledger would be disproportionate unless in-process recovery or unsettled-identity quarantine becomes a product requirement. The reserved readiness-probe request ID and its test are technically redundant after the namespace split, and the deterministic-hash equality assertions can be removed, but both are non-blocking maintenance nits. No production code must be deleted before merge.

Verification

All live GitHub checks are green, including Windows W0, recovery, package, installed Windows CLI, runtime-host, E2E, and test. On this exact head I also ran cargo fmt --check, Windows MSVC cross-target cargo check --locked, the full repository build, focused runtime readiness/Windows sandbox tests (22 passed), Biome on the changed TypeScript, diff checks, and merge-conflict checks.

Residual risk

A transient negative still requires a new composition or Runtime Host restart; unsettled readiness identities are not durably quarantined; cross-user owner validation is not exercised by unprivileged CI; and mixed elevated/non-elevated same-user instances can fail closed on an Administrators-owned pre-existing mutex. These are availability/hardening gaps, not false-positive sandbox admission, and are documented or outside the current threat model.

Merge verdict: ready to merge.

@Astro-Han Astro-Han 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.

Follow-up review on current head 0df509bc3208430a125ebabbbcbcea9cced2e954. The token default-owner adjustment resolves the elevated-runner regression without weakening the cross-user mutex-squatting boundary: the accepted owners remain the current user, SYSTEM, or an owner already inside the stated elevated-administrator threat model. The readiness probe still verifies the actual AppContainer SID and Job membership, the Runtime composition warms one shared availability authority before admission, and all failure paths remain fail-closed.

All current CI is green, including the Windows W0 evidence lane. I found no reproducible P0-P2 issue on this head.

AI-assisted review disclosure: Codex verified the final diff, launcher identity and mutex lifecycle, Runtime cache/admission ordering, documented threat model, focused tests, and live CI. Two independent reviewer-agent passes and an OpenCode Go DeepSeek V4 Flash (high) adversarial pass were used as inputs; speculative hardening ideas were treated as non-blocking.

中文复核

已在最新 head 0df509bc3208430a125ebabbbcbcea9cced2e954 复核。token default owner 的调整修复了 elevated Windows runner 回归,同时没有放宽跨用户 mutex 抢注边界:允许的 owner 仍限定为当前用户、SYSTEM,或已经处于文档化管理员威胁模型内的 owner。readiness probe 仍验证真实 AppContainer SID 与 Job membership,Runtime 在 admission 前预热并复用唯一可用性权威,失败路径保持 fail-closed。当前全部 CI(包括 Windows W0 evidence)为绿色,未发现可复现的 P0-P2。

本次为 AI 辅助审查:Codex 核验最终 diff、launcher identity/mutex 生命周期、Runtime cache/admission 顺序、威胁模型、聚焦测试与实时 CI;另使用两次独立 reviewer 及一次 OpenCode Go DeepSeek V4 Flash(high)对抗审查,推测性 hardening 未作为阻塞项。

@Astro-Han

Copy link
Copy Markdown
Contributor

/agentic_review

@qodo-code-review

qodo-code-review Bot commented Aug 18, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Cold managers report unavailable 🐞 Bug ≡ Correctness
Description
A Windows backend created by createBuiltinSandboxManager('win32', resourcesPath) uses a cache-only
isAvailable callback without warming the cache, so direct consumers—including the existing factory
test—receive false or backend_not_available even when the packaged launcher exists. Only the
Runtime Host filesystem-worker composition path invokes
isBuiltinFilesystemWorkerSandboxAvailable() to warm the cache, making backend availability depend
on an undocumented, unrelated API call order rather than actual readiness.
Code

packages/runtime/src/sandbox/default-sandbox-manager.ts[R142-148]

+function createWindowsReadinessProbe(clientPath: string): () => boolean {
+  return () => readCachedWindowsReadiness(clientPath);
+}
+
function builtinWindowsBackend(
  platform: SandboxPlatform,
  resourcesPath: string | undefined,
Relevance

●●● Strong

Cold-cache false availability is a deterministic factory contract bug; recent runtime correctness
findings were accepted rather than deferred.

PR-#2961
PR-#3079

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The backend's availability closure reads only windowsReadinessCache, whose cold-cache result is
false; both SandboxManager.canEnforce() and WindowsBrokerSandboxBackend.transform() consult
that closure, causing sandbox selection to reject the backend. Runtime Host explicitly warms the
cache during filesystem-worker setup, but the exported factory and its existing test do not,
demonstrating that availability depends on a separate prior call.

packages/runtime/src/sandbox/default-sandbox-manager.ts[128-155]
packages/runtime/src/sandbox/default-sandbox-manager.ts[182-195]
packages/runtime/src/sandbox/sandbox-manager.ts[128-135]
packages/runtime/src/sandbox/windows-sandbox.ts[93-123]
packages/runtime-host/src/server/execution-composition.ts[305-315]
packages/runtime/src/sandbox/default-sandbox-manager.ts[163-170]
packages/runtime/src/sandbox/sandbox-manager.ts[95-111]
packages/runtime/src/tests/default-sandbox-manager.test.ts[64-75]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Windows backend created by `createBuiltinSandboxManager` reads only the readiness cache, but factory construction does not populate that cache. A directly created manager therefore reports the packaged backend unavailable until the unrelated filesystem-worker availability helper happens to run the spawning probe.

## Issue Context
Consolidate readiness initialization in the existing built-in manager construction seam rather than adding another public warm-up contract or readiness state. When constructing a Windows built-in manager, reuse the existing `probeWindowsReadiness` function—or equivalent single construction-time initialization—before exposing the cache-only backend; keep hot-path availability reads synchronous and non-spawning, eliminate the external caller-order precondition, and make the existing cold-cache factory test deterministic.

## Fix Focus Areas
- packages/runtime/src/sandbox/default-sandbox-manager.ts[142-170]
- packages/runtime/src/sandbox/default-sandbox-manager.ts[173-195]
- packages/runtime/src/sandbox/sandbox-manager.ts[128-135]
- packages/runtime/src/sandbox/windows-sandbox.ts[93-123]
- packages/runtime/src/__tests__/default-sandbox-manager.test.ts[64-79]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Mixed elevation rejects locks ✓ Resolved 🐞 Bug ☼ Reliability
Description
Fix-now: a mutex created by an elevated instance is owned by BUILTIN\Administrators, but a
concurrent non-elevated instance of the same user has a user-owned default token and rejects that
legitimate pre-existing mutex. This breaks the per-user cross-process readiness lease and can also
fail production ACL-ledger acquisition whenever elevated and unelevated Maka processes overlap.
Code

experiments/windows-sandbox/launcher/src/acl_ledger.rs[R332-335]

+    // Elevated hosts: our own creations are owned by the token's default-owner
+    // SID (typically BUILTIN\Administrators), not the user SID.
+    let token_owner = current_token_owner_sid_string()?;
+    if rendered.eq_ignore_ascii_case(&token_owner) {
Relevance

●●● Strong

Clear cross-elevation correctness bug; recent Windows sandbox security findings were accepted and
the PR explicitly targets readiness/ledger reliability.

PR-#2961

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The code documents that elevated creations are owned by Administrators and accepts only the current
process’s user SID, SYSTEM, or current token-owner SID. Because the lock DACL grants the account
full access, a non-elevated process can open an elevated-created mutex, but its token-owner SID is
the ordinary user rather than Administrators, so validation returns an error before waiting; both
readiness and ACL-ledger paths depend on this lock implementation.

experiments/windows-sandbox/launcher/src/acl_ledger.rs[240-278]
experiments/windows-sandbox/launcher/src/acl_ledger.rs[290-342]
experiments/windows-sandbox/launcher/src/broker_pipe_security.rs[42-45]
experiments/windows-sandbox/launcher/src/windows_launcher.rs[170-184]
experiments/windows-sandbox/launcher/src/acl_ledger.rs[147-159]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new pre-existing mutex validation treats the current token’s default owner as the legitimate elevated owner. That set differs between elevated and non-elevated processes, so the latter rejects a mutex legitimately created by the former even though both use the same user-scoped name and DACL.

## Issue Context
Use the smallest local correction: make mutexes created by this code carry one stable per-user owner regardless of elevation (for example, a lock-specific security descriptor with an explicit owner SID), then validate against that stable owner. This reuses the existing SID and security-descriptor seam; no new public API or independent authority is needed.

## Fix Focus Areas
- experiments/windows-sandbox/launcher/src/acl_ledger.rs[252-276]
- experiments/windows-sandbox/launcher/src/acl_ledger.rs[290-342]
- experiments/windows-sandbox/launcher/src/windows_launcher.rs[463-498]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: 🧠 Deep: This is a security-sensitive sandbox boundary change with substantial new unsafe launcher logic, process/job/token lifecycle, cross-process locking, and runtime caching across multiple independent paths, making redundant review materially valuable.

Grey Divider

Tip of the day
💡 Did you know, you can show, collapse, or hide each part of a finding: code, evidence, and all

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread experiments/windows-sandbox/launcher/src/acl_ledger.rs Outdated
Comment on lines +142 to 148
function createWindowsReadinessProbe(clientPath: string): () => boolean {
return () => readCachedWindowsReadiness(clientPath);
}

function builtinWindowsBackend(
platform: SandboxPlatform,
resourcesPath: string | undefined,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Cold managers report unavailable 🐞 Bug ≡ Correctness

A Windows backend created by createBuiltinSandboxManager('win32', resourcesPath) uses a cache-only
isAvailable callback without warming the cache, so direct consumers—including the existing factory
test—receive false or backend_not_available even when the packaged launcher exists. Only the
Runtime Host filesystem-worker composition path invokes
isBuiltinFilesystemWorkerSandboxAvailable() to warm the cache, making backend availability depend
on an undocumented, unrelated API call order rather than actual readiness.
Agent Prompt
## Issue description
The Windows backend created by `createBuiltinSandboxManager` reads only the readiness cache, but factory construction does not populate that cache. A directly created manager therefore reports the packaged backend unavailable until the unrelated filesystem-worker availability helper happens to run the spawning probe.

## Issue Context
Consolidate readiness initialization in the existing built-in manager construction seam rather than adding another public warm-up contract or readiness state. When constructing a Windows built-in manager, reuse the existing `probeWindowsReadiness` function—or equivalent single construction-time initialization—before exposing the cache-only backend; keep hot-path availability reads synchronous and non-spawning, eliminate the external caller-order precondition, and make the existing cold-cache factory test deterministic.

## Fix Focus Areas
- packages/runtime/src/sandbox/default-sandbox-manager.ts[142-170]
- packages/runtime/src/sandbox/default-sandbox-manager.ts[173-195]
- packages/runtime/src/sandbox/sandbox-manager.ts[128-135]
- packages/runtime/src/sandbox/windows-sandbox.ts[93-123]
- packages/runtime/src/__tests__/default-sandbox-manager.test.ts[64-79]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

The owner check accepted {user SID, SYSTEM, this token's default owner},
but the default owner differs between elevation states: a mutex created
by an *elevated* instance is owned by BUILTIN\Administrators, while a
concurrent *non-elevated* instance of the same user resolves its own
token owner to the user SID -- so it rejected the elevated instance's
legitimate lock and readiness/ACL-ledger acquisition failed closed
whenever elevated and unelevated Maka processes overlapped.

Make the lock's identity elevation-independent at the source: the lock
security descriptor now pins an explicit owner (O:<user SID> -- always
an assignable owner for the user's own token, elevated or not) ahead of
the protected DACL, so every lock this code creates carries the same
owner in every elevation state. Validation of a pre-existing lock
accepts exactly {user SID, SYSTEM, BUILTIN\Administrators} -- the last
for locks created by builds that predate the pinning during side-by-side
overlap, and safe because only an elevated administrator (outside the
RFC S1/S5 threat model) can create Administrators-owned objects. The
per-token default-owner query is deleted. Adds a lock_sddl unit test
asserting the pinned owner precedes the protected DACL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5
liugddx added a commit to liugddx/maka-agent that referenced this pull request Aug 19, 2026
Non-interactive AppContainer workers previously inherited the creator's
interactive Winsta0\Default desktop, exposing the same-session GUI attack
surface (window-message shatter into the user's interactive windows, desktop
hooks) — AppContainer confines files/network/token but not the desktop scope.
Per RFC v1 §6.3 the private-desktop promise was designed but deferred.

This change enforces initial-desktop *placement* plus DACL protection, not
escape-proof confinement. Each AppContainer launch (production create +
readiness probe) stands up a per-launch alternate desktop on the existing
window station, with a protected DACL: a leading deny ACE strips
DESKTOP_SWITCHDESKTOP / HOOKCONTROL / journal record+playback (0x138) from the
launching-user SID — which the AppContainer child's token still carries as an
effective SID, so without the deny the owner's full-control allow would name
the child as a grantee of those rights (maintainer review, P2-1) — then allow
ACEs grant the launching user and Local System control for cleanup and the
child's AppContainer SID only the minimal DESKTOP_* rights (create
window/menu, read/write objects, enumerate). STARTUPINFOEXW.lpDesktop points
the child at it. If the desktop cannot be created or the DACL cannot be
granted (CreateProcessW then fails ACCESS_DENIED), the launch fails closed —
auto/require never fall back to the host desktop.

Scope, stated honestly (per maintainer review on apache#3174):
- lpDesktop selects only the child's *initial* desktop. This places the worker
  off the interactive Default desktop and DACL-protects the private one, but it
  does NOT structurally confine: absent a no-Win32k mitigation, a dedicated
  window station, or a token boundary, in-process code can still
  OpenDesktopW("Default") + SetThreadDesktop to re-attach. Enforcing a no-Win32k
  mitigation is a deferred gate (§6.5).
- It does NOT isolate the clipboard: the clipboard belongs to the window
  station, which both desktops still share. Clipboard isolation with a dedicated
  window station is a later hardening gate (§6.5).
- The create-window/write DACL rights are granted but not relied upon; the
  worker does no GUI work. A verified Low mandatory-integrity label proving
  those rights are usable at AppContainer's Low IL is deferred (§6.5).

The boundary probe self-attests its own initial desktop name
(desktopPrivatePlacement) and requires the launcher-owned
maka-sandbox-desktop. prefix — not merely "not Default", so landing on any
other pre-existing desktop also fails the placement check; appcontainer-smoke
asserts the same prefix and rejects Default. Digest is unchanged: the desktop
is a launch-time detail and does not enter the manifest.

Stacked on apache#3161 (readiness probe).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5
@liugddx

liugddx commented Aug 19, 2026

Copy link
Copy Markdown
Member Author

Thanks — evaluated both findings; one is a real bug and is fixed at head 02f2776de, the other is deliberate design.

Finding 1 (mixed elevation rejects locks) — real, fixed with the stable-owner option. Confirmed: the accepted-owner set included "this token's default owner", which differs between elevation states — an elevated instance's lock is owned by BUILTIN\Administrators, while a non-elevated instance of the same user resolves its own token owner to the user SID and rejected that legitimate lock, failing readiness/ledger acquisition closed whenever elevated and unelevated Maka processes overlapped. Fixed at the source, as suggested: lock_sddl now pins an explicit O:<user SID> ahead of the protected DACL (the user SID is always an assignable owner for the user's own token, elevated or not), so every lock this code creates carries the same owner in every elevation state. Validation of a pre-existing lock accepts exactly {user SID, SYSTEM, BUILTIN\Administrators} — the last for locks created by pre-pinning builds during side-by-side overlap, and safe because only an elevated administrator (outside the RFC §1/§5 threat model) can create Administrators-owned objects. The per-token default-owner query is deleted; a lock_sddl unit test asserts the pinned owner precedes the DACL. 10-way same-user contention smoke and readiness smoke re-verified green.

Finding 2 (cold managers report unavailable) — deliberate, per prior review consensus on this PR. The hot-path isAvailable is strictly cache-only by design: an earlier round established that the backend callback must never spawn on the event loop, and that availability recovery is scoped to composition build/restart (documented and tested — the module doc and RFC §6.5 "Active running-host readiness recovery" record exactly this). isBuiltinFilesystemWorkerSandboxAvailable() is the single production consumer and warms the cache before the filesystem worker is admitted; a direct createBuiltinSandboxManager('win32') consumer that skips the warmer gets a fail-closed false, which is the intended failure direction (fail closed beats spawning on the hot path or reporting available without proof). No repository test asserts the opposite — CI is green on this head. If the team wants direct-consumer ergonomics, the follow-up would be the async readiness state machine already listed as deferred, not an implicit spawn in isAvailable.

中文

发现 1(混合提权互拒锁)— 真 bug,已修(选稳定 owner 方案)。 属实:白名单里的"本 token 默认 owner"随提权态变化——提权实例的锁 owner 是 BUILTIN\Administrators,同用户非提权实例解析到的却是用户 SID,于是拒绝对方的合法锁,两类进程重叠时 readiness/ledger 获取 fail closed。按建议从源头修:lock_sddl 在受保护 DACL 前显式钉 O:<用户 SID>(用户 SID 对本人 token 在任何提权态都是合法可指派 owner),使锁身份与提权态无关;既有锁校验只接受 {用户 SID, SYSTEM, Administrators}(最后一项覆盖钉 owner 之前旧版本并行期创建的锁,且只有提权管理员——威胁模型外——能伪造)。删除按 token 查默认 owner 的代码;新增 lock_sddl 单测;十路争用与 readiness 冒烟复验绿。

发现 2(冷启动 manager 报不可用)— 有意设计,系本 PR 前几轮评审共识。 热路径 isAvailable 严格 cache-only:此前评审确立 backend 回调绝不在 event loop 上 spawn,可用性恢复限定在 composition 构建/重启(模块文档与 RFC §6.5 已记录并有测试)。isBuiltinFilesystemWorkerSandboxAvailable() 是唯一生产消费方,会在 filesystem worker 准入前预热;绕过预热的直接消费方得到 fail-closed 的 false,这正是预期的失败方向。仓库没有断言相反行为的测试,CI 当前全绿。若要直接消费方体验,后续应做已列为延后的异步 readiness 状态机,而非在 isAvailable 里隐式 spawn。

@liugddx

liugddx commented Aug 19, 2026

Copy link
Copy Markdown
Member Author

@hqhq1025 @Astro-Han Both PRs are now green (20/20) with your approvals in place, and every review thread is addressed. Could you merge when convenient? Suggested order: this PR (#3161) first, then I will immediately rebase #3174 onto the new main so its stacked commits drop out and it can follow. (I attempted the merge myself since I have write access, but the base-branch policy restricts merging to maintainers and auto-merge is disabled — correctly so.) The squash commit should retain the Generated-by: Claude Fable 5 and Co-Authored-By trailers per the AI-use disclosure.

中文:两 PR 均 20/20 绿且已获两位批准,所有 review threads 已处理。烦请按顺序合并:先本 PR(#3161),合入后我会立刻把 #3174 rebase 到新 main 使其 stacked commits 消失即可跟进合并。(我持 write 权限尝试过合并,但 base 分支策略将合并限制给维护者且未开 auto-merge——这是合理配置。)squash commit 请保留 Generated-by/Co-Authored-By trailers。

liugddx added a commit that referenced this pull request Aug 19, 2026
…3174)

* feat(windows-sandbox): implement production-identity readiness probe

The Windows AppContainer backend advertised availability from file
existence alone (`existsSync(clientPath)`), which never proves the host
can actually stand up the sandbox identity. Replace that with a real
readiness probe (RFC §6.4) and align the RFC to the shipped behavior.

- launcher: add `--readiness-probe`, which creates the real AppContainer
  identity/token and a kill-on-close Job and launches a throwaway confined
  child, failing closed if the host cannot create or enforce the boundary.
- runtime: back the Windows backend's `isAvailable` with a memoized
  spawnSync of `--readiness-probe`, so `auto`/`require` fail closed on
  hosts where the OS cannot create the boundary rather than trusting the
  packaged binary's presence.
- CI: add readiness-probe-smoke.ps1 to the W0 lane (exit 0, repeatable,
  argument-rejecting).
- docs: align RFC §6.4/§6.5 (EN + zh-CN) — mark the readiness probe as
  implemented and enforced, and keep private desktop, full per-profile
  policy at readiness, and launcher signature/version as later gates.

Advances #2142 Phase 4 (align the RFC's guarantees with the shipped slice).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5

* fix(windows-sandbox): harden readiness probe against PID reuse and identity spoofing

Address adversarial review on the production-identity readiness probe:

- Make the throwaway AppContainer profile name unique per invocation with a
  monotonic nonce (SystemTime nanos + PID) so a recycled PID can never collide
  with a still-registered profile and deadlock the probe (finding A).
- Prove enforcement, not mere presence: verify the confined child runs under the
  SPECIFIC requested Job (IsProcessInJob against our handle) and carries the
  EXACT requested AppContainer SID (TokenAppContainerSid + EqualSid), failing
  closed otherwise. "some job / some AppContainer" is not evidence the boundary
  we asked for is real (finding B).
- Cache Windows readiness at module scope keyed by client path so backends
  sharing a launcher share one probe result; distinct paths stay independent
  (finding D).
- Sync RFC (EN + zh) Updated: date to 2026-08-17 (finding C).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5

* fix(windows-sandbox): consolidate availability authority and settle readiness probe deterministically

Address maintainer review on the readiness-probe PR:

- Availability: make `isBuiltinFilesystemWorkerSandboxAvailable(win32)` consume
  the memoized readiness probe instead of treating launcher-file presence as a
  second authority. Runtime Host composition calls this at startup, warming the
  readiness cache so the backend's later synchronous `isAvailable()` on the
  transform hot path hits the cache rather than spawning on the event loop.
  Export `probeWindowsReadiness` with an injectable spawn seam and cover it with
  unit tests (clean/non-zero/spawn-error/external-timeout/memoized/missing).

- Settlement: route the readiness child's post-resume outcomes (verification
  failure, timeout, clean exit) through explicit TerminateProcess plus the
  existing `terminate_and_drain_job` primitive, so a child found outside the Job
  is settled rather than relying on Job close as the only backstop.

- Cleanup: give the readiness AppContainer profile one fixed, self-reconciling
  name that best-effort-deletes any leftover before create, so a profile leaked
  by an externally-killed probe is reclaimed by the next probe. Replaces the
  PID+nonce scheme.

- Docs: mark the structured unavailable-reason / diagnostics surface as a
  deferred later gate in both RFC language variants (§6.4/§6.5).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5

* fix(windows-sandbox): serialize and bound the readiness probe lifecycle

Address four readiness-probe lifecycle blockers on the availability path:

1. Reserve the `readiness-probe` request_id in protocol and reject it in
   LaunchRequest::validate; derive the probe profile under a disjoint
   `maka.readiness.` namespace so no production launch can resolve to the
   profile the probe deletes and recreates.
2. Serialize the whole delete->create->probe->settle->drop window across
   processes with the DACL-hardened named mutex the ACL ledger uses
   (LedgerLock), scoped by SID under
   Global\Maka.WindowsSandbox.ReadinessProfile.v1, failing closed on timeout.
3. Preserve the AppContainer registration on drop when the Job could not be
   proven empty (keep_on_drop + preserve()), mirroring the production
   Unsettled contract, instead of deleting a possibly-live identity.
4. Cache negative Windows readiness for a bounded TTL (60s) rather than the
   process lifetime, and make the synchronous isAvailable hot path strictly
   cache-only (readCachedWindowsReadiness) so it never spawns on the event
   loop; positive results stay cached permanently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5

* fix(windows-sandbox): scope readiness recovery and quarantine claims to what the code enforces

hqhq1025's round-3 review is factually correct on both points; this converges the
claims rather than building the heavier machinery, matching the preview's fail-closed
scope.

Blocker 1 (TTL recovery overclaim): the filesystem worker is published once when a
composition is built, so a negative readiness result is not recovered within a running
host — the 60s negative TTL only bounds how long a stale negative poisons the module
cache so the *next* composition build re-probes. De-claim the RFC (EN+zh §6.4/§6.5) and
default-sandbox-manager.ts comments from 're-probes on the next composition rather than
disabling until restart' to honest new-composition/restart-scoped recovery, and mark an
active running-host readiness retry with dynamic worker publication as a deferred gate.
Add a test asserting the hot path never self-recovers a negative; only an explicit
re-probe (a new composition build) can.

Blocker 2 (ineffective preserve/keep_on_drop): create_readiness deletes the fixed
identity unconditionally next cycle, so preserve() only deferred deletion by one probe
and never durably quarantined. Remove preserve()/keep_on_drop and the unsettled
out-param; the settlement-failure surfacing in the match stays, so an unsettled probe
still fails closed (reports unavailable). Document that cleanup relies on the
kill-on-close Job's tree termination and a zero-filesystem-root identity, and mark
durable quarantine (or unique probe identities plus a reconciliation ledger) as a
deferred gate (RFC §6.5).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5

* fix(windows-sandbox): disable cmd AutoRun in the readiness probe

The readiness probe launched its throwaway child as `"<cmd>" /c exit 0`
without `/d`, so cmd.exe ran the machine-wide
`Software\Microsoft\Command Processor\AutoRun` value before the payload.
Because AutoRun is machine-constant, a host that ships an AutoRun which
exits non-zero would make *every* probe report failure, and one that
ships a blocking AutoRun would hang the child until the Job drain times
out -- either way the Windows sandbox fails closed on that machine on
every startup, independent of whether the OS can actually stand up the
boundary. The probe would then be measuring the host's shell
customization instead of the sandbox.

Route the command line through a pure `readiness_probe_command_line`
helper that emits `"<cmd>" /d /c exit 0`, matching the `/d` the
production launch path already passes, and add a unit test asserting
`/d` precedes `/c`. Also correct the RFC (EN + zh): the readiness probe
and its serialized profile lifecycle are added by this PR (#3161), not
the merged #2961 preview slice -- tag those two bullets accordingly and
note the AutoRun-disabled command line.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5

* fix(windows-sandbox): reject a squatted pre-existing lock by owner

The named-lock comment claimed a squatted Global\ name "fails closed at
acquisition", but that only held for a squatter with a restrictive DACL:
CreateMutexW ignores the supplied security descriptor when the name
already exists, so a local user pre-creating the predictable mutex name
with a *permissive* DACL would hand the launcher an attacker-owned
arbitration object -- readiness and ACL-ledger operations would then
block on a mutex the squatter can hold forever (availability DoS), while
the code read as if the DACL had rejected them.

Close the gap where it is checkable: when CreateMutexW reports
ERROR_ALREADY_EXISTS (also the normal same-user contention path), read
the existing object's owner via GetSecurityInfo before any wait and
require it to be the current user or SYSTEM. Ownership is the one
property a permissive squatter cannot forge -- re-owning an object to
another SID requires SeTakeOwnership/SeRestore, which standard users do
not hold. Any other owner fails closed with an explicit
squatted-mutex error instead of blocking. The 10-way concurrency smoke
(same-user contention) passes unchanged; the cross-user negative path
cannot be exercised by an unprivileged CI runner and is enforced by the
owner check itself.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5

* fix(windows-sandbox): accept the token default-owner SID for pre-existing locks

The squatted-lock owner check rejected locks owned by
BUILTIN\Administrators (S-1-5-32-544), which broke on elevated hosts:
an elevated administrator token stamps the Administrators group -- not
the user SID -- as the default owner on objects it creates, so a mutex
this very code created earlier on the elevated CI runner failed the
{user, SYSTEM} check and the W0 protocol lane went red
(unsettled_launch_preserves_grants_and_quarantines_the_ledger).

Accept the process token's default-owner SID (TokenOwner) as a third
legitimate owner. This stays inside the threat model: only an elevated
administrator can create objects owned by Administrators, and RFC
S1/S5 explicitly does not defend against administrators. Standard-user
squatters are still rejected -- they cannot forge either the user SID
or the Administrators owner. Adds a unit test pinning the helper to
{user SID, S-1-5-32-544} so both elevation states stay covered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5

* fix(windows-sandbox): pin an elevation-independent owner on named locks

The owner check accepted {user SID, SYSTEM, this token's default owner},
but the default owner differs between elevation states: a mutex created
by an *elevated* instance is owned by BUILTIN\Administrators, while a
concurrent *non-elevated* instance of the same user resolves its own
token owner to the user SID -- so it rejected the elevated instance's
legitimate lock and readiness/ACL-ledger acquisition failed closed
whenever elevated and unelevated Maka processes overlapped.

Make the lock's identity elevation-independent at the source: the lock
security descriptor now pins an explicit owner (O:<user SID> -- always
an assignable owner for the user's own token, elevated or not) ahead of
the protected DACL, so every lock this code creates carries the same
owner in every elevation state. Validation of a pre-existing lock
accepts exactly {user SID, SYSTEM, BUILTIN\Administrators} -- the last
for locks created by builds that predate the pinning during side-by-side
overlap, and safe because only an elevated administrator (outside the
RFC S1/S5 threat model) can create Administrators-owned objects. The
per-token default-owner query is deleted. Adds a lock_sddl unit test
asserting the pinned owner precedes the protected DACL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5

* feat(windows-sandbox): place sandboxed children on a private desktop

Non-interactive AppContainer workers previously inherited the creator's
interactive Winsta0\Default desktop, exposing the same-session GUI attack
surface (window-message shatter into the user's interactive windows, desktop
hooks) — AppContainer confines files/network/token but not the desktop scope.
Per RFC v1 §6.3 the private-desktop promise was designed but deferred.

This change enforces initial-desktop *placement* plus DACL protection, not
escape-proof confinement. Each AppContainer launch (production create +
readiness probe) stands up a per-launch alternate desktop on the existing
window station, with a protected DACL: a leading deny ACE strips
DESKTOP_SWITCHDESKTOP / HOOKCONTROL / journal record+playback (0x138) from the
launching-user SID — which the AppContainer child's token still carries as an
effective SID, so without the deny the owner's full-control allow would name
the child as a grantee of those rights (maintainer review, P2-1) — then allow
ACEs grant the launching user and Local System control for cleanup and the
child's AppContainer SID only the minimal DESKTOP_* rights (create
window/menu, read/write objects, enumerate). STARTUPINFOEXW.lpDesktop points
the child at it. If the desktop cannot be created or the DACL cannot be
granted (CreateProcessW then fails ACCESS_DENIED), the launch fails closed —
auto/require never fall back to the host desktop.

Scope, stated honestly (per maintainer review on #3174):
- lpDesktop selects only the child's *initial* desktop. This places the worker
  off the interactive Default desktop and DACL-protects the private one, but it
  does NOT structurally confine: absent a no-Win32k mitigation, a dedicated
  window station, or a token boundary, in-process code can still
  OpenDesktopW("Default") + SetThreadDesktop to re-attach. Enforcing a no-Win32k
  mitigation is a deferred gate (§6.5).
- It does NOT isolate the clipboard: the clipboard belongs to the window
  station, which both desktops still share. Clipboard isolation with a dedicated
  window station is a later hardening gate (§6.5).
- The create-window/write DACL rights are granted but not relied upon; the
  worker does no GUI work. A verified Low mandatory-integrity label proving
  those rights are usable at AppContainer's Low IL is deferred (§6.5).

The boundary probe self-attests its own initial desktop name
(desktopPrivatePlacement) and requires the launcher-owned
maka-sandbox-desktop. prefix — not merely "not Default", so landing on any
other pre-existing desktop also fails the placement check; appcontainer-smoke
asserts the same prefix and rejects Default. Digest is unchanged: the desktop
is a launch-time detail and does not enter the manifest.

Stacked on #3161 (readiness probe).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5

* fix(windows-sandbox): make the private desktop usable, bounded, and attested

Follow-ups from inline review on #3174, each closing a spot where the
desktop's documented properties exceeded (or under-stated) what the code
enforced:

- Pin a Low no-write-up mandatory label (S:(ML;;NW;;;LW)) on the private
  desktop. Without it the desktop inherited the creator's Medium
  integrity, and because MIC is evaluated before the DACL, the Low-IL
  AppContainer child's granted create-window/write rights were unusable
  in exactly the case the DACL claimed to grant them. The rights are now
  labeled-usable; an in-child window-creation check proving them end to
  end stays a deferred gate (RFC S6.5) - the worker still does no GUI
  work, so the shipped guarantee remains placement.
- Bound the per-launch desktop heap: CreateDesktopExW with a 512 KiB
  budget instead of CreateDesktopW's default 3,072 KiB interactive
  allocation, so the supported ten-way concurrency costs ~5 MiB of the
  documented 48 MiB system desktop heap instead of ~30 MiB. A real-OS
  test holds ten confined desktops live simultaneously to prove the
  budget.
- Fix an unaligned read: TokenAppContainerSid was read through a Vec<u8>
  buffer cast to TOKEN_APPCONTAINER_INFORMATION, whose leading pointer
  field requires pointer alignment - UB in Rust. The buffer is now sized
  in usize words (same pattern as current_user_sid_string).
- Attest the readiness boundary instead of exiting 0: --readiness-probe
  now emits a machine-readable JSON of the facts it verified (exact-SID
  match, specific-Job membership, settlement drain, private-desktop
  placement), and readiness-probe-smoke.ps1 asserts those fields, so
  removing any verification would turn release evidence red rather than
  leaving a hollow exit-0 gate green.

RFC (EN + zh) updated to match: label and heap bound recorded as
enforced, the deferred gate narrowed to no-Win32k plus in-child
window-creation coverage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5

* fix(windows-sandbox): unforgeable desktop names and a narrowed normative claim

Maintainer follow-up on the private-desktop delta, both points fixing a
gap between what was stated and what the object model guarantees:

- Desktop-name uniqueness is now fail-closed. CreateDesktopExW *opens*
  an existing desktop when the name collides -- silently ignoring the
  supplied DACL and heap budget -- and the old nonce was wall-clock
  nanoseconds with a unwrap_or(0) fallback, so PID reuse, clock
  rollback, same-tick calls, or a failed clock could reopen an older
  desktop while the prefix attestation still passed. The nonce is now
  128 bits from the OS CSPRNG (BCryptGenRandom), collision is
  cryptographically negligible, and RNG failure fails the launch closed
  instead of collapsing to a constant name. The ten-desktop live test
  now also asserts ten *distinct* names that each pass the placement
  attestation, not merely ten returned handles.
- The RFC's top-level S6.3 guarantee no longer states an unconditional
  "cannot": it now reads "workers start on a launcher-created private
  desktop, never Default", with the enumerate/message/hook resistance
  explicitly tied to the deferred no-Win32k/window-station gates (EN +
  zh). The PR title/description are narrowed the same way.

Cleanups from the same review: json_string now delegates to serde_json
(hand-rolled escaping and its test deleted), and the redundant
'"desktop":"Default"' smoke rejections are removed -- the stronger
launcher-prefix assertions already exclude Default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@liugddx

liugddx commented Aug 19, 2026

Copy link
Copy Markdown
Member Author

Closing: #3174 was merged first, and since it was stacked on this branch, its squash commit carried every commit of this PR into main in its final form — verified by diffing this branch against main over all touched paths (launcher, runtime sandbox, RFCs, smokes): this branch has zero net content left; the only differences are older intermediate versions of sections main already has newer. All #3161 artifacts are on main, including the elevation-independent lock_sddl owner pinning, readiness_probe_command_line (/d), the serialized readiness lifecycle, and the TTL/cache-only availability logic. The reviews and approvals on this PR are preserved in its history; the AI-use trailers rode in with the #3174 squash.

中文:#3174 先合并且 stack 在本分支上,其 squash 已把本 PR 全部 commit 以最终形态带入 main——对全部触及路径 diff 验证本分支已无净增量(差异均为 main 已有内容的旧中间版本)。lock_sddl 提权无关 owner、/d、串行 readiness 生命周期、TTL/cache-only 可用性等全部产物均在 main 上。故关闭本 PR,评审记录保留于此。

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.

5 participants