Skip to content

feat(windows): add brokered AppContainer sandbox support - #2961

Merged
liugddx merged 100 commits into
apache:mainfrom
liugddx:feat/windows-sandbox-w0-spike
Aug 17, 2026
Merged

feat(windows): add brokered AppContainer sandbox support#2961
liugddx merged 100 commits into
apache:mainfrom
liugddx:feat/windows-sandbox-w0-spike

Conversation

@liugddx

@liugddx liugddx commented Aug 13, 2026

Copy link
Copy Markdown
Member

Summary

  • add a packaged Windows x64 AppContainer sandbox backend for restricted managed execution
  • compile PermissionProfile roots, network policy, and environment into a closed launch manifest
  • authorize one-shot broker requests with a kernel-reported client PID, single-use nonce, and digest of the complete launch policy
  • launch the target in an AppContainer with no network capabilities and atomic kill-on-close Job membership
  • grant only requested filesystem roots and restore AppContainer ACEs through a crash-recoverable ledger
  • register the backend only when the packaged native resource exists; otherwise managed execution fails closed
  • connect the backend to the existing Runtime Host filesystem-worker and Agent execution path
  • replace the W0 proposal with matching English and Chinese RFCs for the selected implementation

Security Evidence

The blocking Windows 2025 job verifies:

  • closed protocol validation and bounded broker framing
  • pipe access restricted to SYSTEM and the current user, remote-client rejection, and kernel PID binding
  • nonce replay, unapproved digest, response-identity, and manifest-cleanup rejection paths
  • recomputation of the digest from executable, arguments, cwd, roots, network policy, and sorted environment
  • AppContainer token creation with atomic PROC_THREAD_ATTRIBUTE_JOB_LIST membership
  • allowed-root read/write, denial of a user-readable sibling file, and denial of a live loopback endpoint
  • AppContainer ACE removal after normal exit, stale-ledger recovery, and junction/reparse-point rejection
  • bounded fail-closed behavior of the rejected restricted-token and non-atomic candidates

Product Integration

The Windows release pipeline builds maka-windows-sandbox.exe with locked Cargo dependencies, packages it under resources/windows-sandbox, and requires that resource during packaged-app verification. Runtime capability detection registers the backend only when that exact packaged resource exists. There is no environment-variable executable override and no unsandboxed retry.

Scope Boundary

This PR provides a real native sandbox for the packaged Windows x64 managed-execution path and is ready for implementation review once all checks pass. It does not by itself declare the entire Windows platform fully supported. Authenticode signing, the complete adversarial matrix, cancellation/parent-death/concurrency release evidence, and independent security review remain tracked by #2142.

Verification

  • node --test experiments/windows-sandbox/*.test.mjs
  • cargo fmt --manifest-path experiments/windows-sandbox/launcher/Cargo.toml -- --check
  • cargo check --manifest-path experiments/windows-sandbox/launcher/Cargo.toml --locked
  • PowerShell parser validation for all sandbox smoke scripts
  • Biome checks for changed JavaScript and TypeScript
  • GitHub workspace, Runtime Host, Desktop E2E, Storybook, Windows baseline/recovery, Windows packaging, and blocking sandbox jobs

Related to #2142. Supersedes the implementation choice proposed in #2940 with the AppContainer result proven by Windows CI.

中文说明

概要

  • 为 Windows x64 restricted managed execution 增加随安装包发布的 AppContainer 沙箱后端
  • PermissionProfile 的目录、网络策略和环境编译为封闭 launch manifest
  • 使用内核返回的 client PID、一次性 nonce 和完整 launch policy 摘要授权 one-shot broker 请求
  • 在不授予网络 capability 的 AppContainer 中启动目标,并在创建时原子加入 kill-on-close Job
  • 只给请求目录临时授予 ACL,并通过可恢复 ledger 清理 AppContainer ACE
  • 只有打包 native resource 存在时才注册后端;不可用时 managed execution 保持 fail closed
  • 接入既有 Runtime Host filesystem worker 与 Agent 执行链路
  • 用中英文 RFC 记录 Windows 实验证明后最终选定的 AppContainer 方案

安全证据

阻塞式 Windows 2025 job 验证:

  • 封闭协议和有长度上限的 broker frame
  • pipe 仅允许 SYSTEM 与当前用户、拒绝远程 client、从内核绑定 PID
  • nonce 重放、未批准摘要、response identity 和 manifest 清理失败路径
  • 从 executable、arguments、cwd、roots、network 和排序环境重算 launch digest
  • AppContainer token 与 PROC_THREAD_ATTRIBUTE_JOB_LIST 原子 Job membership
  • 允许目录读写、相邻用户可读文件拒绝、真实 loopback 网络拒绝
  • 正常退出 ACL 清理、遗留 ledger recovery、junction/reparse-point 拒绝
  • 被否决 restricted-token 与非原子候选的有界 fail-closed 行为

产品接入

Windows 发布流水线使用锁定 Cargo 依赖构建 maka-windows-sandbox.exe,将它打包到 resources/windows-sandbox,并在安装包验证时强制检查该资源。Runtime 只从这个打包路径做 capability detection,不接受环境变量替换任意 executable,也不会 unsandboxed retry。

范围边界

本 PR 为 Windows x64 安装包的 managed execution 提供真实原生沙箱;全部检查通过后即可进行实现评审。它不等于整个 Windows 平台已经正式支持。Authenticode 签名、完整对抗矩阵、cancel/parent-death/concurrency 发布证据和独立安全审查继续由 #2142 跟踪。

验证

  • node --test experiments/windows-sandbox/*.test.mjs
  • cargo fmt --manifest-path experiments/windows-sandbox/launcher/Cargo.toml -- --check
  • cargo check --manifest-path experiments/windows-sandbox/launcher/Cargo.toml --locked
  • 全部沙箱 PowerShell smoke script 的 parser 检查
  • 变更 JavaScript/TypeScript 的 Biome 检查
  • GitHub workspace、Runtime Host、Desktop E2E、Storybook、Windows baseline/recovery、Windows packaging 和阻塞式 sandbox jobs

关联 #2142。Windows CI 的 AppContainer 结果取代 #2940 最初提出的实现选择。

@liugddx
liugddx requested review from Astro-Han and a lite review from Copilot August 14, 2026 00:23

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 establishes an initial “W0 evidence” slice for the proposed Windows sandbox work: it documents the Phase 4 design baseline (RFC v1), adds an experiments-based closed v1 launcher request contract plus an untrusted probe, and introduces a non-blocking Windows 2025 CI lane to run the evidence tooling.

Changes:

  • Update runtime sandbox documentation to point Windows enforcement tracking to #2142 and the new Windows sandbox RFC.
  • Add experiments/windows-sandbox protocol + probe tooling (Node tests) and a Windows-only Rust launcher prototype.
  • Add a non-blocking windows-2025 GitHub Actions workflow to run the protocol/probe evidence steps when the experiments change.

Reviewed changes

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

Show a summary per file
File Description
packages/runtime/src/sandbox/README.md Updates sandbox docs to reference Windows sandbox RFC and current Linux backend behavior/tests.
experiments/windows-sandbox/README.md Documents the W0 spike purpose, how to run the checks, and current candidate behavior.
experiments/windows-sandbox/protocol.test.mjs Adds Node tests for the closed v1 launcher request contract parser.
experiments/windows-sandbox/protocol.mjs Implements the v1 launcher request parser with unknown-key rejection and path/env validation.
experiments/windows-sandbox/probe.test.mjs Adds Node tests for the probe’s pass/fail behavior and JSON reporting.
experiments/windows-sandbox/probe.mjs Implements the untrusted probe (fs, network connect, env, descendant launch) and report output.
experiments/windows-sandbox/launcher/src/windows_launcher.rs Adds a Windows launcher prototype using restricted tokens + Job Object + bounded wait.
experiments/windows-sandbox/launcher/src/protocol.rs Defines/deserializes the Rust-side request shape and performs request validation.
experiments/windows-sandbox/launcher/src/main.rs Adds the Windows-only launcher entrypoint and request loading/dispatch.
experiments/windows-sandbox/launcher/Cargo.toml Introduces the Rust launcher crate and dependencies (serde, windows-sys).
experiments/windows-sandbox/launcher/Cargo.lock Locks Rust dependencies for reproducible CI builds.
experiments/windows-sandbox/launcher/.gitignore Ignores Cargo build output.
experiments/windows-sandbox/launcher-smoke.ps1 Adds a CI smoke script that runs the launcher and validates restricted-token + Job membership.
docs/architecture/windows-sandbox-rfc-v1.zh-CN.md Adds the Chinese RFC v1 describing the proposed Windows sandbox architecture and evidence gates.
docs/architecture/windows-sandbox-rfc-v1.md Adds the English RFC v1 describing the proposed Windows sandbox architecture and evidence gates.
.github/workflows/windows-sandbox-w0.yml Adds a non-blocking Windows 2025 workflow to run protocol tests, build the launcher, and run the smoke probe.
Suppressed comments (1)

experiments/windows-sandbox/probe.test.mjs:34

  • spawnSync env values are stringified; setting MAKA_W0_TEST_SECRET: undefined can result in the child seeing the variable as present (e.g. value "undefined"), making the environment-absence assertion flaky/incorrect. Build an env object and delete the key instead of assigning undefined.
        cwd: process.cwd(),
        encoding: 'utf8',
        env: { ...process.env, MAKA_W0_TEST_SECRET: undefined },
      },

💡 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/probe.mjs Outdated
Comment thread experiments/windows-sandbox/probe.test.mjs Outdated
Comment thread experiments/windows-sandbox/launcher/src/protocol.rs
@liugddx
liugddx force-pushed the feat/windows-sandbox-w0-spike branch from 6f44c32 to e4c1c10 Compare August 14, 2026 01:01
@liugddx
liugddx marked this pull request as ready for review August 14, 2026 01:06
@liugddx
liugddx force-pushed the feat/windows-sandbox-w0-spike branch from 74b6529 to f58a15f Compare August 14, 2026 04:02
@liugddx
liugddx force-pushed the feat/windows-sandbox-w0-spike branch from f58a15f to 1cabbaa Compare August 14, 2026 04:03
@liugddx
liugddx marked this pull request as draft August 14, 2026 06:04
@liugddx
liugddx marked this pull request as ready for review August 14, 2026 06:30
@github-actions
github-actions Bot requested a lite review from Copilot August 17, 2026 02:56

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@liugddx

liugddx commented Aug 17, 2026

Copy link
Copy Markdown
Member Author

@Astro-Han Thank you — accepting all four preview-scope points as merge gates for #2961. Each is now addressed on head 7de7b5c55 with the narrow fail-closed shape you suggested, none by growing a new abstraction. Implementing your fourth point immediately caught a real product defect (details in 4 below), which I think settles whether these gates belonged in this PR.

  1. Hard links (c1978b67c): ACL admission now fails closed on any multi-link file. collect_roots queries the kernel link count (GetFileInformationByHandle / nNumberOfLinks) for every file root, and the recursive-tree scan that already rejected reparse points now also rejects multi-link files anywhere inside a recursively granted tree — an (OI)(CI) grant propagates onto existing children, so an in-tree alias of an out-of-tree object would otherwise carry the grant past the root. Exact directory roots stay out of scope (their grant never leaves the directory object), mirroring the existing reparse rule. Handle/file-ID keyed admission remains roadmap(windows): make Windows a supported platform #2142.

  2. Cross-session ownership (c1978b67c): the mutex and per-request leases moved from Local\ to the machine-wide namespace, since the ledger directory, the icacls grants and the AppContainer profiles are all cross-session state. The objects are user-SID-scoped names carrying an explicit SYSTEM+owner-only DACL (the same SDDL the broker pipe already uses), so a concurrent console/RDP session of the same user sees a live lease as live, another principal can neither open nor observe the lock, and a squatted name fails the launch closed instead of sharing an arbitration object with an untrusted owner.

  3. Grep contract (98bef67cd): the preview no longer exposes Grep inside the Windows sandbox. The literal-only in-process fallback is deleted; a sandboxed worker fails closed with grep_unavailable, pointing callers at Glob and Read. The signal is a broker-injected manifest environment marker, so an unsandboxed Windows worker keeps the full ripgrep behavior and no other platform changes. The contract-preserving SearchEngine remains roadmap(windows): make Windows a supported platform #2142.

  4. Packaged evidence (263e73f1a, 7de7b5c55): your suspicion was correct in the strongest sense — the packaged Electron worker path had never actually executed inside the AppContainer. Electron's run-as-node entry opens the NUL device to backfill standard handles, the container denies that open, and startup aborts (FATAL node_main.cc "Unable to open nul device") — a failure the copied-node.exe evidence could never observe, because plain node performs no such backfill. Fixed by passing --no-stdio-init (the broker always relays three valid standard handles, so the backfill is unnecessary). The release E2E now executes the packaged artifacts themselves: the packaged broker enforcing the AppContainer, the packaged Electron executable as the worker runtime (ELECTRON_RUN_AS_NODE, the production launch shape) and the packaged resources\workers\filesystem-worker.js bundle. Only the driver still imports repository-built client code, because the packaged copy lives inside app.asar. The spec assertions pin the packaged program/bundle paths and the not-widened runtime root.

Verification on the final head: cargo tests including the new admission/lock coverage (multi-link file root, in-tree out-of-tree alias, machine-wide lock naming), the local smoke suite including 10-way concurrent ACL recovery under the machine-wide locks, the Windows workspace suites, a live brokered-Electron AppContainer run of write/read/glob/grep-fail-closed/denied-write, and CI is green across all 16 checks — including release-windows-check, whose E2E now runs the packaged Electron worker end to end.

Agreed on the process gate: independent human security review remains required before merge, and this CI evidence does not substitute for it.

中文版

四个 preview 范围内的问题全部按你建议的窄范围 fail-closed 方案在 head 7de7b5c55 落地:

  1. 硬链接c1978b67c):准入对文件 root 及递归授权树内所有文件查内核链接数,多链接对象直接 fail closed;exact 目录 root 与既有 reparse 规则对齐不扫描。file-ID 化准入留 roadmap(windows): make Windows a supported platform #2142
  2. 跨会话归属c1978b67c):mutex/lease 迁移到机器级命名空间,名字按用户 SID 区分并带 SYSTEM+属主-only 显式 DACL(复用 broker pipe 的 SDDL);同用户跨会话能正确看到活跃 lease,他人抢名则启动 fail closed。
  3. Grep 契约98bef67cd):preview 在 Windows 沙箱内不再暴露 Grep——literal-only fallback 已删除,沙箱内 worker 返回 grep_unavailable 并提示改用 Glob+Read;非沙箱 Windows worker 保留完整 ripgrep 行为。统一 SearchEngine 留 roadmap(windows): make Windows a supported platform #2142
  4. 打包证据263e73f1a7de7b5c55):你的判断完全正确——打包 Electron worker 此前从未真正在 AppContainer 里跑通过:Electron run-as-node 入口启动时要打开 NUL 设备回填标准句柄,容器拒绝后直接 FATAL 中止,而复制 node.exe 的旧证据链观察不到这一点。已修(--no-stdio-init,broker 本就中继三个有效句柄),release E2E 现在实跑打包 broker + 打包 Electron 可执行文件 + 打包 worker bundle。

最终 head 上 16 项 CI 全绿(含跑真打包 worker E2E 的 release-windows-check)。同意流程门禁:合并前仍需独立人工安全审查,CI 证据不能替代。

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

Approved for the narrowed packaged Windows filesystem-worker preview. The current head runs the packaged broker, Electron executable, and worker end to end, and the earlier runtime-root, per-launch identity, ledger-mode, hard-link, cross-session lock, Grep-contract, and packaging findings are closed.

The two inline P2 notes are deliberately narrow: close the already-agreed exact-write and Job-settlement invariants without expanding this PR into a generalized Windows architecture rewrite. Please rerun the packaged Electron E2E on the final head. Independent human security review remains required before merge under the repository policy; this approval and green CI do not replace that gate.

AI-assisted review: Codex inspected the current PR head, full diff, CI and merge state, prior review ledger, author replies, native ACL/Job lifecycle, Runtime/Core contracts, and packaged evidence, with independent adversarial reviewer passes. The human contributor remains responsible for validating these findings and for the final review and merge decision.

中文版

整体批准当前收窄后的 Windows filesystem-worker preview。两条 inline P2 仅要求用最小方案关闭此前已经接受的 exact-write 与 Job-settlement 不变量,不要求扩展为通用 Windows 架构重构。最终 head 请重跑 packaged Electron E2E;合并前仍需仓库政策要求的独立人工安全审查。

Comment thread packages/runtime/src/filesystem-worker/client.ts
Comment thread experiments/windows-sandbox/launcher/src/acl_ledger.rs Outdated
liugddx and others added 2 commits August 17, 2026 13:59
…unproven

settle_job collapsed every failure into one error string, so ACL and
ledger cleanup ran identically whether the Job was proven empty or
termination, waiting and Job accounting themselves failed. In the latter
case processes may still hold the launch identity, and normal cleanup
would erase the only recovery record while the boundary was still live.

Launch failures now carry a settled/unsettled outcome. Only a
proven-empty Job (directly, or after a successful terminate-and-drain)
enters normal cleanup. An unsettled launch keeps its grants in place,
quarantines its ledger as a preserved recovery record, and leaves the
AppContainer profile undeleted; the kill-on-close Job handle remains the
kernel backstop either way. Recovery never interprets quarantined
ledgers and every launch uses a fresh identity, so no later child can
inherit the preserved authority.

Failure-injection tests cover both outcomes: an unsettled launch
preserves grants and quarantines the ledger; a settled failure still
releases grants and ledger normally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… shapes closed

A write whose target does not exist yet — and directory-entry mutations
such as ApplyPatch create/delete — reached the Windows compiler as a
runtime-writable parent root, which the launcher could only represent as
recursive Modify on the existing parent tree: a kernel boundary broader
than the approved exact operation. The worker itself still wrote only
the requested target, which is why the packaged E2E kept passing.

The compiler now refuses these shapes instead of widening them: the
preview fails closed before any process launches, per the agreed W0/W1
parent-entry gate. Precise parent-entry authority remains follow-up
work. The packaged E2E and the Windows smoke update a pre-seeded exact
file and assert that a missing-target write fails closed without
producing a file; verify-windows-sandbox-e2e.mjs joins the
release-windows-check path filter so E2E semantic changes rerun the
packaged evidence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions
github-actions Bot requested a lite review from Copilot August 17, 2026 06:00

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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

liugddx commented Aug 17, 2026

Copy link
Copy Markdown
Member Author

@Astro-Han Both inline P2 gates are closed on the final head 781d7e0f2, and the packaged Electron E2E has been rerun there as requested:

  1. Exact writes stay exact (24a099caf): the Windows compiler refuses any policy carrying a runtime-writable parent root instead of widening it to recursive Modify — a missing-target Write and ApplyPatch create/delete now fail closed at the transform stage before any process launches. The packaged E2E and the Windows smoke update a pre-seeded exact file and assert the missing-target shape fails closed without producing a file.

  2. Job settlement gates cleanup (77415ed47): settle_job propagates a settled-versus-unsettled outcome. Only a Job proven empty (directly or after a successful terminate-and-drain) enters normal ACL/ledger cleanup; an unsettled launch preserves its grants, quarantines its ledger as the recovery record, and keeps the AppContainer profile, with the kill-on-close Job handle as the kernel backstop. Failure-injection tests pin both outcomes.

CI on the final head: 15/16 green including release-windows-check (packaged installer + packaged Electron worker E2E, 13m). The one red check, request-review, is the repository's Copilot-request automation failing with a 403 from its configured token — unrelated to this PR's content and reproducible on rerun; flagging it for maintainers.

The independent human security review gate remains acknowledged as required by repository policy.

中文版

两条 inline P2 已在最终 head 781d7e0f2 关闭,打包 Electron E2E 也按要求在最终 head 重跑通过:

  1. exact 写保持 exact24a099caf):Windows 编译器对带 runtime-writable 父根的策略直接拒绝,缺失目标 Write / ApplyPatch create/delete 在 transform 阶段 fail closed;打包 E2E 与冒烟改为更新预置文件并断言缺失目标写 fail closed。
  2. Job settlement 门控清理77415ed47):只有证明清空的 Job 才进入正常 ACL/ledger 清理;unsettled 的 launch 保留 grants、隔离 ledger、保留 AppContainer profile,kill-on-close Job 句柄仍是内核兜底;注入式测试锁定双向行为。

最终 head CI 15/16 绿(含 13 分钟的打包验证)。唯一红项 request-review 是仓库 Copilot 自动化的 token 403,与本 PR 内容无关,已标记请维护者处理。仓库政策要求的独立人工安审仍然有效。

@liugddx
liugddx merged commit d3a21f9 into apache:main Aug 17, 2026
15 of 17 checks passed
liugddx added a commit to liugddx/maka-agent that referenced this pull request Aug 18, 2026
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 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>
M4n5ter pushed a commit to yunaremaia/maka that referenced this pull request Aug 26, 2026
… worker

The filesystem worker appended the pattern as a bare positional, so a
pattern starting with '-' was parsed by ripgrep as flags. A leading flag
exits 1, which maps to an empty match set - reporting strings that exist
as absent, with no diagnostic.

Adds the same '--' separator the host-local workspace executor already
pins (apache#2961), plus a regression test asserting the separator and a
successful dash-prefixed search.

Fixes apache#3733
M4n5ter pushed a commit to yunaremaia/maka that referenced this pull request Aug 26, 2026
… worker

The filesystem worker appended the pattern as a bare positional, so a
pattern starting with '-' was parsed by ripgrep as flags. A leading flag
exits 1, which maps to an empty match set - reporting strings that exist
as absent, with no diagnostic.

Adds the same '--' separator the host-local workspace executor already
pins (apache#2961), plus a regression test asserting the separator and a
successful dash-prefixed search.

Fixes apache#3733
M4n5ter pushed a commit to yunaremaia/maka that referenced this pull request Aug 26, 2026
… worker

The filesystem worker appended the pattern as a bare positional, so a
pattern starting with '-' was parsed by ripgrep as flags. A leading flag
exits 1, which maps to an empty match set - reporting strings that exist
as absent, with no diagnostic.

Adds the same '--' separator the host-local workspace executor already
pins (apache#2961), plus a regression test asserting the separator and a
successful dash-prefixed search.

Fixes apache#3733
M4n5ter pushed a commit to yunaremaia/maka that referenced this pull request Aug 26, 2026
… worker

The filesystem worker appended the pattern as a bare positional, so a
pattern starting with '-' was parsed by ripgrep as flags. A leading flag
exits 1, which maps to an empty match set - reporting strings that exist
as absent, with no diagnostic.

Adds the same '--' separator the host-local workspace executor already
pins (apache#2961), plus a regression test asserting the separator and a
successful dash-prefixed search.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants