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

@liugddxliugddx 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 CopilotAugust 14, 2026 00:23

CopilotAI 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
FileDescription
packages/runtime/src/sandbox/README.mdUpdates sandbox docs to reference Windows sandbox RFC and current Linux backend behavior/tests.
experiments/windows-sandbox/README.mdDocuments the W0 spike purpose, how to run the checks, and current candidate behavior.
experiments/windows-sandbox/protocol.test.mjsAdds Node tests for the closed v1 launcher request contract parser.
experiments/windows-sandbox/protocol.mjsImplements the v1 launcher request parser with unknown-key rejection and path/env validation.
experiments/windows-sandbox/probe.test.mjsAdds Node tests for the probe’s pass/fail behavior and JSON reporting.
experiments/windows-sandbox/probe.mjsImplements the untrusted probe (fs, network connect, env, descendant launch) and report output.
experiments/windows-sandbox/launcher/src/windows_launcher.rsAdds a Windows launcher prototype using restricted tokens + Job Object + bounded wait.
experiments/windows-sandbox/launcher/src/protocol.rsDefines/deserializes the Rust-side request shape and performs request validation.
experiments/windows-sandbox/launcher/src/main.rsAdds the Windows-only launcher entrypoint and request loading/dispatch.
experiments/windows-sandbox/launcher/Cargo.tomlIntroduces the Rust launcher crate and dependencies (serde, windows-sys).
experiments/windows-sandbox/launcher/Cargo.lockLocks Rust dependencies for reproducible CI builds.
experiments/windows-sandbox/launcher/.gitignoreIgnores Cargo build output.
experiments/windows-sandbox/launcher-smoke.ps1Adds a CI smoke script that runs the launcher and validates restricted-token + Job membership.
docs/architecture/windows-sandbox-rfc-v1.zh-CN.mdAdds the Chinese RFC v1 describing the proposed Windows sandbox architecture and evidence gates.
docs/architecture/windows-sandbox-rfc-v1.mdAdds the English RFC v1 describing the proposed Windows sandbox architecture and evidence gates.
.github/workflows/windows-sandbox-w0.ymlAdds 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 threadexperiments/windows-sandbox/probe.mjs Outdated
Comment threadexperiments/windows-sandbox/probe.test.mjs Outdated
Comment threadexperiments/windows-sandbox/launcher/src/protocol.rs
@liugddx
liugddxforce-pushed the feat/windows-sandbox-w0-spike branch from 6f44c32 to e4c1c10CompareAugust 14, 2026 01:01
@liugddx
liugddx marked this pull request as ready for review August 14, 2026 01:06
@liugddx
liugddxforce-pushed the feat/windows-sandbox-w0-spike branch from 74b6529 to f58a15fCompareAugust 14, 2026 04:02
@liugddx
liugddxforce-pushed the feat/windows-sandbox-w0-spike branch from f58a15f to 1cabbaaCompareAugust 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-actionsBot requested a lite review from CopilotAugust 17, 2026 02:56

CopilotAI 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

Copy link
Copy Markdown
MemberAuthor

@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-HanAstro-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 threadpackages/runtime/src/filesystem-worker/client.ts
Comment threadexperiments/windows-sandbox/launcher/src/acl_ledger.rs Outdated
liugddxand 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-actionsBot requested a lite review from CopilotAugust 17, 2026 06:00

CopilotAI 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

Copy link
Copy Markdown
MemberAuthor

@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:mainAug 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
@qodo-code-reviewqodo-code-reviewBot mentioned this pull request Aug 18, 2026
3 tasks
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.
Fixesapache#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.
Fixesapache#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.
Fixesapache#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.
Fixesapache#3733
Sign up for freeto 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

@liugddx@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat(windows): add brokered AppContainer sandbox support by liugddx · Pull Request #2961 · apache/maka · GitHub
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

@liugddxliugddx 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 CopilotAugust 14, 2026 00:23

CopilotAI 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
FileDescription
packages/runtime/src/sandbox/README.mdUpdates sandbox docs to reference Windows sandbox RFC and current Linux backend behavior/tests.
experiments/windows-sandbox/README.mdDocuments the W0 spike purpose, how to run the checks, and current candidate behavior.
experiments/windows-sandbox/protocol.test.mjsAdds Node tests for the closed v1 launcher request contract parser.
experiments/windows-sandbox/protocol.mjsImplements the v1 launcher request parser with unknown-key rejection and path/env validation.
experiments/windows-sandbox/probe.test.mjsAdds Node tests for the probe’s pass/fail behavior and JSON reporting.
experiments/windows-sandbox/probe.mjsImplements the untrusted probe (fs, network connect, env, descendant launch) and report output.
experiments/windows-sandbox/launcher/src/windows_launcher.rsAdds a Windows launcher prototype using restricted tokens + Job Object + bounded wait.
experiments/windows-sandbox/launcher/src/protocol.rsDefines/deserializes the Rust-side request shape and performs request validation.
experiments/windows-sandbox/launcher/src/main.rsAdds the Windows-only launcher entrypoint and request loading/dispatch.
experiments/windows-sandbox/launcher/Cargo.tomlIntroduces the Rust launcher crate and dependencies (serde, windows-sys).
experiments/windows-sandbox/launcher/Cargo.lockLocks Rust dependencies for reproducible CI builds.
experiments/windows-sandbox/launcher/.gitignoreIgnores Cargo build output.
experiments/windows-sandbox/launcher-smoke.ps1Adds a CI smoke script that runs the launcher and validates restricted-token + Job membership.
docs/architecture/windows-sandbox-rfc-v1.zh-CN.mdAdds the Chinese RFC v1 describing the proposed Windows sandbox architecture and evidence gates.
docs/architecture/windows-sandbox-rfc-v1.mdAdds the English RFC v1 describing the proposed Windows sandbox architecture and evidence gates.
.github/workflows/windows-sandbox-w0.ymlAdds 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 threadexperiments/windows-sandbox/probe.mjs Outdated
Comment threadexperiments/windows-sandbox/probe.test.mjs Outdated
Comment threadexperiments/windows-sandbox/launcher/src/protocol.rs
@liugddx
liugddxforce-pushed the feat/windows-sandbox-w0-spike branch from 6f44c32 to e4c1c10CompareAugust 14, 2026 01:01
@liugddx
liugddx marked this pull request as ready for review August 14, 2026 01:06
@liugddx
liugddxforce-pushed the feat/windows-sandbox-w0-spike branch from 74b6529 to f58a15fCompareAugust 14, 2026 04:02
@liugddx
liugddxforce-pushed the feat/windows-sandbox-w0-spike branch from f58a15f to 1cabbaaCompareAugust 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-actionsBot requested a lite review from CopilotAugust 17, 2026 02:56

CopilotAI 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

Copy link
Copy Markdown
MemberAuthor

@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-HanAstro-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 threadpackages/runtime/src/filesystem-worker/client.ts
Comment threadexperiments/windows-sandbox/launcher/src/acl_ledger.rs Outdated
liugddxand 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-actionsBot requested a lite review from CopilotAugust 17, 2026 06:00

CopilotAI 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

Copy link
Copy Markdown
MemberAuthor

@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:mainAug 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
@qodo-code-reviewqodo-code-reviewBot mentioned this pull request Aug 18, 2026
3 tasks
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.
Fixesapache#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.
Fixesapache#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.
Fixesapache#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.
Fixesapache#3733
Sign up for freeto 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

@liugddx@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(windows): add brokered AppContainer sandbox support by liugddx · Pull Request #2961 · apache/maka · GitHub
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

@liugddxliugddx 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 CopilotAugust 14, 2026 00:23

CopilotAI 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
FileDescription
packages/runtime/src/sandbox/README.mdUpdates sandbox docs to reference Windows sandbox RFC and current Linux backend behavior/tests.
experiments/windows-sandbox/README.mdDocuments the W0 spike purpose, how to run the checks, and current candidate behavior.
experiments/windows-sandbox/protocol.test.mjsAdds Node tests for the closed v1 launcher request contract parser.
experiments/windows-sandbox/protocol.mjsImplements the v1 launcher request parser with unknown-key rejection and path/env validation.
experiments/windows-sandbox/probe.test.mjsAdds Node tests for the probe’s pass/fail behavior and JSON reporting.
experiments/windows-sandbox/probe.mjsImplements the untrusted probe (fs, network connect, env, descendant launch) and report output.
experiments/windows-sandbox/launcher/src/windows_launcher.rsAdds a Windows launcher prototype using restricted tokens + Job Object + bounded wait.
experiments/windows-sandbox/launcher/src/protocol.rsDefines/deserializes the Rust-side request shape and performs request validation.
experiments/windows-sandbox/launcher/src/main.rsAdds the Windows-only launcher entrypoint and request loading/dispatch.
experiments/windows-sandbox/launcher/Cargo.tomlIntroduces the Rust launcher crate and dependencies (serde, windows-sys).
experiments/windows-sandbox/launcher/Cargo.lockLocks Rust dependencies for reproducible CI builds.
experiments/windows-sandbox/launcher/.gitignoreIgnores Cargo build output.
experiments/windows-sandbox/launcher-smoke.ps1Adds a CI smoke script that runs the launcher and validates restricted-token + Job membership.
docs/architecture/windows-sandbox-rfc-v1.zh-CN.mdAdds the Chinese RFC v1 describing the proposed Windows sandbox architecture and evidence gates.
docs/architecture/windows-sandbox-rfc-v1.mdAdds the English RFC v1 describing the proposed Windows sandbox architecture and evidence gates.
.github/workflows/windows-sandbox-w0.ymlAdds 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 threadexperiments/windows-sandbox/probe.mjs Outdated
Comment threadexperiments/windows-sandbox/probe.test.mjs Outdated
Comment threadexperiments/windows-sandbox/launcher/src/protocol.rs
@liugddx
liugddxforce-pushed the feat/windows-sandbox-w0-spike branch from 6f44c32 to e4c1c10CompareAugust 14, 2026 01:01
@liugddx
liugddx marked this pull request as ready for review August 14, 2026 01:06
@liugddx
liugddxforce-pushed the feat/windows-sandbox-w0-spike branch from 74b6529 to f58a15fCompareAugust 14, 2026 04:02
@liugddx
liugddxforce-pushed the feat/windows-sandbox-w0-spike branch from f58a15f to 1cabbaaCompareAugust 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-actionsBot requested a lite review from CopilotAugust 17, 2026 02:56

CopilotAI 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

Copy link
Copy Markdown
MemberAuthor

@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-HanAstro-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 threadpackages/runtime/src/filesystem-worker/client.ts
Comment threadexperiments/windows-sandbox/launcher/src/acl_ledger.rs Outdated
liugddxand 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-actionsBot requested a lite review from CopilotAugust 17, 2026 06:00

CopilotAI 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

Copy link
Copy Markdown
MemberAuthor

@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:mainAug 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
@qodo-code-reviewqodo-code-reviewBot mentioned this pull request Aug 18, 2026
3 tasks
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.
Fixesapache#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.
Fixesapache#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.
Fixesapache#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.
Fixesapache#3733
Sign up for freeto 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

@liugddx@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(windows): add brokered AppContainer sandbox support by liugddx · Pull Request #2961 · apache/maka · GitHub
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

@liugddxliugddx 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 CopilotAugust 14, 2026 00:23

CopilotAI 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
FileDescription
packages/runtime/src/sandbox/README.mdUpdates sandbox docs to reference Windows sandbox RFC and current Linux backend behavior/tests.
experiments/windows-sandbox/README.mdDocuments the W0 spike purpose, how to run the checks, and current candidate behavior.
experiments/windows-sandbox/protocol.test.mjsAdds Node tests for the closed v1 launcher request contract parser.
experiments/windows-sandbox/protocol.mjsImplements the v1 launcher request parser with unknown-key rejection and path/env validation.
experiments/windows-sandbox/probe.test.mjsAdds Node tests for the probe’s pass/fail behavior and JSON reporting.
experiments/windows-sandbox/probe.mjsImplements the untrusted probe (fs, network connect, env, descendant launch) and report output.
experiments/windows-sandbox/launcher/src/windows_launcher.rsAdds a Windows launcher prototype using restricted tokens + Job Object + bounded wait.
experiments/windows-sandbox/launcher/src/protocol.rsDefines/deserializes the Rust-side request shape and performs request validation.
experiments/windows-sandbox/launcher/src/main.rsAdds the Windows-only launcher entrypoint and request loading/dispatch.
experiments/windows-sandbox/launcher/Cargo.tomlIntroduces the Rust launcher crate and dependencies (serde, windows-sys).
experiments/windows-sandbox/launcher/Cargo.lockLocks Rust dependencies for reproducible CI builds.
experiments/windows-sandbox/launcher/.gitignoreIgnores Cargo build output.
experiments/windows-sandbox/launcher-smoke.ps1Adds a CI smoke script that runs the launcher and validates restricted-token + Job membership.
docs/architecture/windows-sandbox-rfc-v1.zh-CN.mdAdds the Chinese RFC v1 describing the proposed Windows sandbox architecture and evidence gates.
docs/architecture/windows-sandbox-rfc-v1.mdAdds the English RFC v1 describing the proposed Windows sandbox architecture and evidence gates.
.github/workflows/windows-sandbox-w0.ymlAdds 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 threadexperiments/windows-sandbox/probe.mjs Outdated
Comment threadexperiments/windows-sandbox/probe.test.mjs Outdated
Comment threadexperiments/windows-sandbox/launcher/src/protocol.rs
@liugddx
liugddxforce-pushed the feat/windows-sandbox-w0-spike branch from 6f44c32 to e4c1c10CompareAugust 14, 2026 01:01
@liugddx
liugddx marked this pull request as ready for review August 14, 2026 01:06
@liugddx
liugddxforce-pushed the feat/windows-sandbox-w0-spike branch from 74b6529 to f58a15fCompareAugust 14, 2026 04:02
@liugddx
liugddxforce-pushed the feat/windows-sandbox-w0-spike branch from f58a15f to 1cabbaaCompareAugust 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-actionsBot requested a lite review from CopilotAugust 17, 2026 02:56

CopilotAI 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

Copy link
Copy Markdown
MemberAuthor

@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-HanAstro-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 threadpackages/runtime/src/filesystem-worker/client.ts
Comment threadexperiments/windows-sandbox/launcher/src/acl_ledger.rs Outdated
liugddxand 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-actionsBot requested a lite review from CopilotAugust 17, 2026 06:00

CopilotAI 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

Copy link
Copy Markdown
MemberAuthor

@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:mainAug 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
@qodo-code-reviewqodo-code-reviewBot mentioned this pull request Aug 18, 2026
3 tasks
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.
Fixesapache#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.
Fixesapache#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.
Fixesapache#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.
Fixesapache#3733
Sign up for freeto 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

@liugddx@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat(windows): add brokered AppContainer sandbox support by liugddx · Pull Request #2961 · apache/maka · GitHub
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

@liugddxliugddx 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 CopilotAugust 14, 2026 00:23

CopilotAI 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
FileDescription
packages/runtime/src/sandbox/README.mdUpdates sandbox docs to reference Windows sandbox RFC and current Linux backend behavior/tests.
experiments/windows-sandbox/README.mdDocuments the W0 spike purpose, how to run the checks, and current candidate behavior.
experiments/windows-sandbox/protocol.test.mjsAdds Node tests for the closed v1 launcher request contract parser.
experiments/windows-sandbox/protocol.mjsImplements the v1 launcher request parser with unknown-key rejection and path/env validation.
experiments/windows-sandbox/probe.test.mjsAdds Node tests for the probe’s pass/fail behavior and JSON reporting.
experiments/windows-sandbox/probe.mjsImplements the untrusted probe (fs, network connect, env, descendant launch) and report output.
experiments/windows-sandbox/launcher/src/windows_launcher.rsAdds a Windows launcher prototype using restricted tokens + Job Object + bounded wait.
experiments/windows-sandbox/launcher/src/protocol.rsDefines/deserializes the Rust-side request shape and performs request validation.
experiments/windows-sandbox/launcher/src/main.rsAdds the Windows-only launcher entrypoint and request loading/dispatch.
experiments/windows-sandbox/launcher/Cargo.tomlIntroduces the Rust launcher crate and dependencies (serde, windows-sys).
experiments/windows-sandbox/launcher/Cargo.lockLocks Rust dependencies for reproducible CI builds.
experiments/windows-sandbox/launcher/.gitignoreIgnores Cargo build output.
experiments/windows-sandbox/launcher-smoke.ps1Adds a CI smoke script that runs the launcher and validates restricted-token + Job membership.
docs/architecture/windows-sandbox-rfc-v1.zh-CN.mdAdds the Chinese RFC v1 describing the proposed Windows sandbox architecture and evidence gates.
docs/architecture/windows-sandbox-rfc-v1.mdAdds the English RFC v1 describing the proposed Windows sandbox architecture and evidence gates.
.github/workflows/windows-sandbox-w0.ymlAdds 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 threadexperiments/windows-sandbox/probe.mjs Outdated
Comment threadexperiments/windows-sandbox/probe.test.mjs Outdated
Comment threadexperiments/windows-sandbox/launcher/src/protocol.rs
@liugddx
liugddxforce-pushed the feat/windows-sandbox-w0-spike branch from 6f44c32 to e4c1c10CompareAugust 14, 2026 01:01
@liugddx
liugddx marked this pull request as ready for review August 14, 2026 01:06
@liugddx
liugddxforce-pushed the feat/windows-sandbox-w0-spike branch from 74b6529 to f58a15fCompareAugust 14, 2026 04:02
@liugddx
liugddxforce-pushed the feat/windows-sandbox-w0-spike branch from f58a15f to 1cabbaaCompareAugust 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-actionsBot requested a lite review from CopilotAugust 17, 2026 02:56

CopilotAI 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

Copy link
Copy Markdown
MemberAuthor

@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-HanAstro-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 threadpackages/runtime/src/filesystem-worker/client.ts
Comment threadexperiments/windows-sandbox/launcher/src/acl_ledger.rs Outdated
liugddxand 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-actionsBot requested a lite review from CopilotAugust 17, 2026 06:00

CopilotAI 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

Copy link
Copy Markdown
MemberAuthor

@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:mainAug 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
@qodo-code-reviewqodo-code-reviewBot mentioned this pull request Aug 18, 2026
3 tasks
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.
Fixesapache#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.
Fixesapache#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.
Fixesapache#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.
Fixesapache#3733
Sign up for freeto 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

@liugddx@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(windows): add brokered AppContainer sandbox support by liugddx · Pull Request #2961 · apache/maka · GitHub
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

@liugddxliugddx 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 CopilotAugust 14, 2026 00:23

CopilotAI 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
FileDescription
packages/runtime/src/sandbox/README.mdUpdates sandbox docs to reference Windows sandbox RFC and current Linux backend behavior/tests.
experiments/windows-sandbox/README.mdDocuments the W0 spike purpose, how to run the checks, and current candidate behavior.
experiments/windows-sandbox/protocol.test.mjsAdds Node tests for the closed v1 launcher request contract parser.
experiments/windows-sandbox/protocol.mjsImplements the v1 launcher request parser with unknown-key rejection and path/env validation.
experiments/windows-sandbox/probe.test.mjsAdds Node tests for the probe’s pass/fail behavior and JSON reporting.
experiments/windows-sandbox/probe.mjsImplements the untrusted probe (fs, network connect, env, descendant launch) and report output.
experiments/windows-sandbox/launcher/src/windows_launcher.rsAdds a Windows launcher prototype using restricted tokens + Job Object + bounded wait.
experiments/windows-sandbox/launcher/src/protocol.rsDefines/deserializes the Rust-side request shape and performs request validation.
experiments/windows-sandbox/launcher/src/main.rsAdds the Windows-only launcher entrypoint and request loading/dispatch.
experiments/windows-sandbox/launcher/Cargo.tomlIntroduces the Rust launcher crate and dependencies (serde, windows-sys).
experiments/windows-sandbox/launcher/Cargo.lockLocks Rust dependencies for reproducible CI builds.
experiments/windows-sandbox/launcher/.gitignoreIgnores Cargo build output.
experiments/windows-sandbox/launcher-smoke.ps1Adds a CI smoke script that runs the launcher and validates restricted-token + Job membership.
docs/architecture/windows-sandbox-rfc-v1.zh-CN.mdAdds the Chinese RFC v1 describing the proposed Windows sandbox architecture and evidence gates.
docs/architecture/windows-sandbox-rfc-v1.mdAdds the English RFC v1 describing the proposed Windows sandbox architecture and evidence gates.
.github/workflows/windows-sandbox-w0.ymlAdds 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 threadexperiments/windows-sandbox/probe.mjs Outdated
Comment threadexperiments/windows-sandbox/probe.test.mjs Outdated
Comment threadexperiments/windows-sandbox/launcher/src/protocol.rs
@liugddx
liugddxforce-pushed the feat/windows-sandbox-w0-spike branch from 6f44c32 to e4c1c10CompareAugust 14, 2026 01:01
@liugddx
liugddx marked this pull request as ready for review August 14, 2026 01:06
@liugddx
liugddxforce-pushed the feat/windows-sandbox-w0-spike branch from 74b6529 to f58a15fCompareAugust 14, 2026 04:02
@liugddx
liugddxforce-pushed the feat/windows-sandbox-w0-spike branch from f58a15f to 1cabbaaCompareAugust 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-actionsBot requested a lite review from CopilotAugust 17, 2026 02:56

CopilotAI 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

Copy link
Copy Markdown
MemberAuthor

@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-HanAstro-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 threadpackages/runtime/src/filesystem-worker/client.ts
Comment threadexperiments/windows-sandbox/launcher/src/acl_ledger.rs Outdated
liugddxand 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-actionsBot requested a lite review from CopilotAugust 17, 2026 06:00

CopilotAI 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

Copy link
Copy Markdown
MemberAuthor

@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:mainAug 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
@qodo-code-reviewqodo-code-reviewBot mentioned this pull request Aug 18, 2026
3 tasks
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.
Fixesapache#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.
Fixesapache#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.
Fixesapache#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.
Fixesapache#3733
Sign up for freeto 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

@liugddx@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(windows): add brokered AppContainer sandbox support by liugddx · Pull Request #2961 · apache/maka · GitHub
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

@liugddxliugddx 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 CopilotAugust 14, 2026 00:23

CopilotAI 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
FileDescription
packages/runtime/src/sandbox/README.mdUpdates sandbox docs to reference Windows sandbox RFC and current Linux backend behavior/tests.
experiments/windows-sandbox/README.mdDocuments the W0 spike purpose, how to run the checks, and current candidate behavior.
experiments/windows-sandbox/protocol.test.mjsAdds Node tests for the closed v1 launcher request contract parser.
experiments/windows-sandbox/protocol.mjsImplements the v1 launcher request parser with unknown-key rejection and path/env validation.
experiments/windows-sandbox/probe.test.mjsAdds Node tests for the probe’s pass/fail behavior and JSON reporting.
experiments/windows-sandbox/probe.mjsImplements the untrusted probe (fs, network connect, env, descendant launch) and report output.
experiments/windows-sandbox/launcher/src/windows_launcher.rsAdds a Windows launcher prototype using restricted tokens + Job Object + bounded wait.
experiments/windows-sandbox/launcher/src/protocol.rsDefines/deserializes the Rust-side request shape and performs request validation.
experiments/windows-sandbox/launcher/src/main.rsAdds the Windows-only launcher entrypoint and request loading/dispatch.
experiments/windows-sandbox/launcher/Cargo.tomlIntroduces the Rust launcher crate and dependencies (serde, windows-sys).
experiments/windows-sandbox/launcher/Cargo.lockLocks Rust dependencies for reproducible CI builds.
experiments/windows-sandbox/launcher/.gitignoreIgnores Cargo build output.
experiments/windows-sandbox/launcher-smoke.ps1Adds a CI smoke script that runs the launcher and validates restricted-token + Job membership.
docs/architecture/windows-sandbox-rfc-v1.zh-CN.mdAdds the Chinese RFC v1 describing the proposed Windows sandbox architecture and evidence gates.
docs/architecture/windows-sandbox-rfc-v1.mdAdds the English RFC v1 describing the proposed Windows sandbox architecture and evidence gates.
.github/workflows/windows-sandbox-w0.ymlAdds 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 threadexperiments/windows-sandbox/probe.mjs Outdated
Comment threadexperiments/windows-sandbox/probe.test.mjs Outdated
Comment threadexperiments/windows-sandbox/launcher/src/protocol.rs
@liugddx
liugddxforce-pushed the feat/windows-sandbox-w0-spike branch from 6f44c32 to e4c1c10CompareAugust 14, 2026 01:01
@liugddx
liugddx marked this pull request as ready for review August 14, 2026 01:06
@liugddx
liugddxforce-pushed the feat/windows-sandbox-w0-spike branch from 74b6529 to f58a15fCompareAugust 14, 2026 04:02
@liugddx
liugddxforce-pushed the feat/windows-sandbox-w0-spike branch from f58a15f to 1cabbaaCompareAugust 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-actionsBot requested a lite review from CopilotAugust 17, 2026 02:56

CopilotAI 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

Copy link
Copy Markdown
MemberAuthor

@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-HanAstro-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 threadpackages/runtime/src/filesystem-worker/client.ts
Comment threadexperiments/windows-sandbox/launcher/src/acl_ledger.rs Outdated
liugddxand 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-actionsBot requested a lite review from CopilotAugust 17, 2026 06:00

CopilotAI 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

Copy link
Copy Markdown
MemberAuthor

@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:mainAug 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
@qodo-code-reviewqodo-code-reviewBot mentioned this pull request Aug 18, 2026
3 tasks
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.
Fixesapache#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.
Fixesapache#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.
Fixesapache#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.
Fixesapache#3733
Sign up for freeto 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

@liugddx@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat(windows): add brokered AppContainer sandbox support by liugddx · Pull Request #2961 · apache/maka · GitHub
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

@liugddxliugddx 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 CopilotAugust 14, 2026 00:23

CopilotAI 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
FileDescription
packages/runtime/src/sandbox/README.mdUpdates sandbox docs to reference Windows sandbox RFC and current Linux backend behavior/tests.
experiments/windows-sandbox/README.mdDocuments the W0 spike purpose, how to run the checks, and current candidate behavior.
experiments/windows-sandbox/protocol.test.mjsAdds Node tests for the closed v1 launcher request contract parser.
experiments/windows-sandbox/protocol.mjsImplements the v1 launcher request parser with unknown-key rejection and path/env validation.
experiments/windows-sandbox/probe.test.mjsAdds Node tests for the probe’s pass/fail behavior and JSON reporting.
experiments/windows-sandbox/probe.mjsImplements the untrusted probe (fs, network connect, env, descendant launch) and report output.
experiments/windows-sandbox/launcher/src/windows_launcher.rsAdds a Windows launcher prototype using restricted tokens + Job Object + bounded wait.
experiments/windows-sandbox/launcher/src/protocol.rsDefines/deserializes the Rust-side request shape and performs request validation.
experiments/windows-sandbox/launcher/src/main.rsAdds the Windows-only launcher entrypoint and request loading/dispatch.
experiments/windows-sandbox/launcher/Cargo.tomlIntroduces the Rust launcher crate and dependencies (serde, windows-sys).
experiments/windows-sandbox/launcher/Cargo.lockLocks Rust dependencies for reproducible CI builds.
experiments/windows-sandbox/launcher/.gitignoreIgnores Cargo build output.
experiments/windows-sandbox/launcher-smoke.ps1Adds a CI smoke script that runs the launcher and validates restricted-token + Job membership.
docs/architecture/windows-sandbox-rfc-v1.zh-CN.mdAdds the Chinese RFC v1 describing the proposed Windows sandbox architecture and evidence gates.
docs/architecture/windows-sandbox-rfc-v1.mdAdds the English RFC v1 describing the proposed Windows sandbox architecture and evidence gates.
.github/workflows/windows-sandbox-w0.ymlAdds 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 threadexperiments/windows-sandbox/probe.mjs Outdated
Comment threadexperiments/windows-sandbox/probe.test.mjs Outdated
Comment threadexperiments/windows-sandbox/launcher/src/protocol.rs
@liugddx
liugddxforce-pushed the feat/windows-sandbox-w0-spike branch from 6f44c32 to e4c1c10CompareAugust 14, 2026 01:01
@liugddx
liugddx marked this pull request as ready for review August 14, 2026 01:06
@liugddx
liugddxforce-pushed the feat/windows-sandbox-w0-spike branch from 74b6529 to f58a15fCompareAugust 14, 2026 04:02
@liugddx
liugddxforce-pushed the feat/windows-sandbox-w0-spike branch from f58a15f to 1cabbaaCompareAugust 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-actionsBot requested a lite review from CopilotAugust 17, 2026 02:56

CopilotAI 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

Copy link
Copy Markdown
MemberAuthor

@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-HanAstro-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 threadpackages/runtime/src/filesystem-worker/client.ts
Comment threadexperiments/windows-sandbox/launcher/src/acl_ledger.rs Outdated
liugddxand 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-actionsBot requested a lite review from CopilotAugust 17, 2026 06:00

CopilotAI 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

Copy link
Copy Markdown
MemberAuthor

@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:mainAug 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
@qodo-code-reviewqodo-code-reviewBot mentioned this pull request Aug 18, 2026
3 tasks
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.
Fixesapache#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.
Fixesapache#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.
Fixesapache#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.
Fixesapache#3733
Sign up for freeto 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

@liugddx@Astro-Han