feat: support filesystem allowWrite with Cloud Hypervisor - #7669
Conversation
Wires the write-policy planner (#7660) into the host mount-tree enforcement layer (#7661) so `filesystem.allowWrite` is actually enforced by the Cloud Hypervisor microVM runtime. Both foundations landed deliberately inert; this is the runtime integration. `filesystem-write-enforcement.ts` is the only place the two meet. It plans the policy in a dedicated startup stage before the boot loop, so an invalid allowlist aborts before virtiofsd or the guest launches, and translates each planner disposition into the merged mount-tree API: - policy absent: no enforcement argument at all, byte-identical legacy behaviour and original export objects; - unrestricted/fully writable: guest `rw`, no plan; - fully read-only: guest `ro`, staged host root `ro`, zero-overlay plan; - selectively writable: guest `rw`, staged host root `ro`, one overlay per allowed path. A zero-overlay read-only export still gets a plan rather than falling back to the legacy single bind plus remount, so a narrowed export always uses the recursively-verified staged tree. No `internalTags` are passed: Cloud Hypervisor has no analogue of Docker's always-writable log and session-state binds, and exempting `tmp-gh-aw` would defeat the narrowing a policy like `allowWrite: ["/tmp/gh-aw/agent"]` expresses. Because the host tree is the boundary, a selective export is never mounted read-only guest-side. `validateCloudHypervisorExports()` accepts a read-only `workspace` only when a plan for that tag exists, so a read-only workspace nothing enforces is still rejected. Unknown plan tags stay fail-closed and planner validation is not duplicated. Removes only the Cloud Hypervisor rejection from `filesystem-policy.ts`; sbx and Docker-in-Docker still fail closed. Adds live-KVM smoke cases proving an allowed directory and file write persists to the host while sibling, parent, create, truncate, rename, and delete outside the allowlist fail, plus empty-allowlist narrowing and a fail-closed abort. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
✅ Coverage Check PassedOverall Coverage
📁 Per-file Coverage Changes (7 files)
✨ New Files (1 files)
Coverage comparison generated by |
There was a problem hiding this comment.
Pull request overview
Integrates filesystem.allowWrite with the Cloud Hypervisor runtime using host-side virtio-fs mount-tree enforcement.
Changes:
- Translates write-policy plans into guest mount modes and host enforcement plans.
- Threads enforcement through runtime startup, manager, and virtiofsd.
- Adds unit/live-KVM coverage and updates configuration documentation.
Show a summary per file
| File | Description |
|---|---|
src/filesystem-policy.ts |
Enables Cloud Hypervisor policy support. |
src/filesystem-policy.test.ts |
Tests runtime compatibility. |
src/cloud-hypervisor/vm-config-builder.ts |
Encodes policy-derived guest mount modes. |
src/cloud-hypervisor/vm-config-builder.test.ts |
Tests read-only workspace encoding. |
src/cloud-hypervisor/manager.ts |
Passes enforcement to virtiofsd. |
src/cloud-hypervisor/manager.test.ts |
Tests manager enforcement handling. |
src/cloud-hypervisor/manager-types.ts |
Adds enforcement to guest configuration. |
src/cloud-hypervisor/filesystem-write-enforcement.ts |
Implements planner-to-mount translation. |
src/cloud-hypervisor/filesystem-write-enforcement.test.ts |
Covers translation and failure behavior. |
src/cloud-hypervisor/exports.ts |
Allows plan-gated read-only workspaces. |
src/cloud-hypervisor/exports.test.ts |
Tests workspace validation. |
src/cloud-hypervisor-runtime-backend.ts |
Plans policy before VM boot attempts. |
src/cloud-hypervisor-runtime-backend.test.ts |
Tests runtime integration. |
src/awf-config-schema.json |
Documents supported runtimes. |
scripts/ci/cloud-hypervisor-live-smoke.sh |
Adds live enforcement cases. |
scripts/ci/cloud-hypervisor-ci-scripts.test.ts |
Verifies smoke-test assertions. |
docs/cloud-hypervisor-foundation.md |
Documents runtime architecture and behavior. |
docs/awf-config.schema.json |
Updates published schema documentation. |
docs/awf-config-spec.md |
Documents Cloud Hypervisor policy semantics. |
.github/workflows/test-cloud-hypervisor.yml |
Expands workflow path triggers. |
Review details
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- Files reviewed: 20/20 changed files
- Comments generated: 1
- Review effort level: Balanced
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
|
✅ Copilot review passed with no inline comments. @lpcox Add the |
|
✨ The prophecy is fulfilled... Smoke Codex has completed its mystical journey. The stars align. 🌟 Warning Firewall blocked 3 domainsThe following domains were blocked by the firewall during workflow execution:
To allow these domains, add them to the network:
allowed:
- defaults
- "msfeed2.pkgs.visualstudio.com"
- "msfeed25.pkgs.visualstudio.com"
- "registry.npmjs.org"See Network Configuration for more information.
|
|
✅ Smoke Copilot BYOK AOAI (Entra) completed. Copilot AOAI BYOK (Entra) mode operational. 🔓
|
|
📰 VERDICT: Smoke Copilot has concluded. All systems operational. This is a developing story. 🎤
|
|
Chroot tests passed! Smoke Chroot - All security and functionality tests succeeded.
|
|
✅ Smoke Claude passed
|
|
✅ Smoke Copilot BYOK completed. Copilot BYOK mode operational. 🔓
|
|
✅ Build Test Suite completed successfully!
|
|
📡 Smoke OTel Tracing completed. All tracing scenarios validated. ✅
|
|
🛡️ Smoke Copilot Network Isolation confirmed the egress allowlist is enforced. ✅ Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
To allow these domains, add them to the network:
allowed:
- defaults
- "example.com"See Network Configuration for more information.
|
|
❌ Contribution Check failed. Please review the logs for details.
|
|
📰 VERDICT: Smoke Docker Sbx has concluded. All systems operational. This is a developing story. 🎤
|
|
❌ Smoke Gemini reports failed. Facets need polishing...
|
|
🚀 Security Guard has started processing this pull request |
|
🔌 Smoke Services — All services reachable! ✅
|
|
✅ Smoke Copilot BYOK AOAI (api-key) completed. Copilot AOAI BYOK (api-key) mode operational. 🔓
|
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
🔌 Smoke Services — All services reachable! ✅
|
|
📰 VERDICT: Smoke Docker Sbx has concluded. All systems operational. This is a developing story. 🎤
|
|
🚀 Security Guard has started processing this pull request |
|
❌ Smoke Gemini reports failed. Facets need polishing...
|
|
✅ Smoke Copilot BYOK completed. Copilot BYOK mode operational. 🔓
|
|
❌ Contribution Check was cancelled. Please review the logs for details.
|
Smoke Test: Claude Engine Validation
Overall result: PASS
|
|
Smoke Test: Copilot Engine
Overall: PASS cc @lpcox
|
|
EGRESS_RESULT allow=pass deny=pass ✅ Allowed domain (github.com) reachable: Overall status: PASS Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
To allow these domains, add them to the network:
allowed:
- defaults
- "example.com"See Network Configuration for more information.
|
|
🚀 Security Guard has started processing this pull request |
|
Smoke Test: Docker Sbx — @lpcox
Overall: PASS
|
|
Smoke Test: Copilot BYOK (Direct) Mode ✅
Status: PASS
|
|
Smoke Test: Services Connectivity
Overall: FAIL —
|
|
@lpcox
|
Smoke Test
Warning Firewall blocked 3 domainsThe following domains were blocked by the firewall during workflow execution:
To allow these domains, add them to the network:
allowed:
- defaults
- "msfeed2.pkgs.visualstudio.com"
- "msfeed25.pkgs.visualstudio.com"
- "registry.npmjs.org"See Network Configuration for more information.
|
📡 OTel Tracing Smoke Test Results
Overall: All 5 scenarios passed. No regressions detected in OTEL tracing integration.
|
Chroot Version Comparison
Node.js version differs between host and chroot environment; Python and Go match. Since not all versions matched, the
|
|
@lpcox
|
🏗️ Build Test Suite Results
Overall: 8/8 ecosystems passed — PASS Notes:
.NET dotnet-run entry was included as a build/run check since
|
Wires
filesystem.allowWriteinto the Cloud Hypervisor microVM runtime. Both foundations are already onmainand were deliberately left inert — the write-policy planner (#7660,6aab9a1) and host mount-tree enforcement (#7661,62de637). This is the runtime integration that joins them.Branched from current
main; not stacked. Does not touchgithub/gh-aw— compiler rollout is a later independent PR once firewall support is released.Behavioral mapping
src/cloud-hypervisor/filesystem-write-enforcement.tsis the only place the planner and the mount tree meet. The translation is total; there is no fallback path.undefined)start()receives no enforcement argument at all, and the original export objects are returned by identity, so behaviour is byte-identical to a run without a policyhostRootMode: 'rw')rwhostRootMode: 'ro', no overlays)rorohostRootMode: 'ro', overlays)rwroA zero-overlay read-only export still gets a plan rather than falling back to the legacy single
mount --bind+remount,ro. The staged tree is the only variant that recursively remounts carried-in submounts read-only and verifies against/proc/self/mountinfo, so a policy-narrowed export always uses the stronger path.No
internalTagsare passed to the planner. Cloud Hypervisor has no analogue of the Docker runtime's always-writable agent-log and session-state binds — every export it publishes is host-visible workspace or runner state. Marking one internal (tmp-gh-awin particular) would defeat the narrowing that the motivatingallowWrite: ["/tmp/gh-aw/agent"]exists to express. An empty allowlist therefore narrows every writable export,/tmp/gh-awincluded.Correctness / security
MNT_READONLYfrom the parent viad_automount/finish_automount(), so a guest-siderowould also block the writable nodes.validateCloudHypervisorExports()accepts a read-onlyworkspaceexport only when a mount plan for theworkspacetag actually exists. A read-only workspace that nothing enforces is still rejected (allowReadOnlyWorkspaceis opt-in and plan-gated).filesystem-write-policystartup stage before the boot loop, so an invalid allowlist aborts before virtiofsd or the guest is launched and before any retry can re-attempt it. Dependency injection and retry behaviour are otherwise untouched.assertPlansMatchExports(). No broad catch or fallback anywhere.src/filesystem-policy.ts. sbx and Docker-in-Docker still fail closed.One consequence worth flagging, documented in
docs/cloud-hypervisor-foundation.md: the guestHOMEis/workspace/.awf-home, inside the workspace export, so a policy that narrows/workspacemakes the agent home read-only. That is the policy working as specified — add the home path toallowWriteif the workload needs it.Files
New
src/cloud-hypervisor/filesystem-write-enforcement.ts— planner → mount-tree translationsrc/cloud-hypervisor/filesystem-write-enforcement.test.tsRuntime
src/cloud-hypervisor-runtime-backend.ts—filesystem-write-policystage,mountEnforcementthreaded throughcreateManagersrc/cloud-hypervisor/manager.ts,manager-types.ts— carries enforcement toVirtiofsdManager.start()src/cloud-hypervisor/exports.ts— plan-gatedallowReadOnlyWorkspacevalidation optionsrc/cloud-hypervisor/vm-config-builder.ts— guest cmdline mode encoding under a plansrc/filesystem-policy.ts— Cloud Hypervisor rejection removedDocs/spec
docs/cloud-hypervisor-foundation.md— de-inerted, new "Runtime integration" section with the mapping tabledocs/awf-config-spec.md§4.1,src/awf-config-schema.json,docs/awf-config.schema.json(regenerated vianpm run generate:schema)Tests/CI
scripts/ci/cloud-hypervisor-live-smoke.sh,scripts/ci/cloud-hypervisor-ci-scripts.test.ts.github/workflows/test-cloud-hypervisor.yml— addedsrc/filesystem-policy.tstopaths:Review fixes (
0fbc6b3)CRITICAL —
filesystem.allowWriteleaked into compose generation.writeConfigs()→generateDockerCompose()→buildAgentVolumes()called Docker'sapplyFilesystemWritePolicy()unconditionally. Compose still builds an agent service object for microVM runtimes (so infra containers can wiredepends_onedges) even though it is omitted from the emitted file, so a Cloud Hypervisor policy was evaluated against compose bind mounts the agent never uses. Guest paths like/workspace/allowed— and the motivating/tmp/gh-aw/agent— are not backed by any host bind mount, so they threwfilesystem.allowWrite path is not an existing path within a writable host mountduringwriteConfigs(), long before the Cloud Hypervisor planner ran.New
resolveComposeFilesystemAllowWrite()returns the policy only whenruntimeUsesComposeAgent()is true. Both consumers route through it: theapplyFilesystemWritePolicy()call involume-builder.tsand thedropUnbackedHostHomeOverlays()toggle inoptional-services.ts. Docker/gVisor behaviour unchanged; sbx still fails closed. Every otherfilesystemAllowWritereader was audited — the rest are the type declaration, config mapping, the compatibility gate, and the CH planner itself, all correct.HIGH — the live smoke truncate probe killed the guest shell. The guest runs BusyBox ash, where a redirection failure on a POSIX special builtin is fatal.
! : > /workspace/input.txtexited the shell before the rename and delete probes ran, while host post-checks still passed vacuously — a write never attempted also never changes anything. Reproduced inbusybox:latest: the old chain exits 1 and never prints the rename/delete markers, whileprintf ''continues correctly. Every write-denial probe is now a regular builtin or external command, subshell-wrapped so a fatal error stays contained, and followed by a sentinel; a newassert_sentinelshelper requires all of them after each case.Regression coverage added: top-level
generateDockerComposetests proving a Cloud Hypervisor config with/workspace/allowedneither throws nor rewrites any emitted compose volume, while Docker/gVisor still narrow mounts and still fail closed on an unbacked path (verified to fail without the fix); plus script-contract tests banning special-builtin write probes and requiring each sentinel to be both emitted and asserted.Tests
Unit/focused coverage: undefined policy (identity-preserving, no
mountEnforcementkey,call.length === 6withcall[5]undefined), empty allowlist narrowing all rw exports including/tmp/gh-aw, the/tmp/gh-aw/agentmotivating case, wholly-allowed export, selective directory and selective file overlays, invalid/missing/escaping/relative paths failing closed beforecreateManager,hostRootMode/guestMountModetranslation, deterministic plan tags and ordering, read-only workspace refused without enforcement, and theawf.virtiofs=workspace:…:rocmdline. Compatibility tests updated: Cloud Hypervisor accepted (including[]), sbx rejected, DinD rejected (including combined with cloud-hypervisor).Teardown/no-residue is covered by the existing
mount-tree/virtiofsdsuites from #7661 plusassert_no_residueafter every live case (netns, veth/TAP, cgroup, process, and/run/awf-cloud-hypervisorrun-directory residue) — not duplicated here.Live-KVM cases added to the existing preview harness (no new framework):
allow-write— selective policy; asserts the allowed directory/file writes and a nested create persist to the host,/workspace/allowedappears as its own virtio-fs submount, and sibling write, root-level create,mkdir, truncate ofinput.txt, rename, and delete outside the allowlist all fail; verified host-side afterward.allow-write-none— empty allowlist; guest mount isro, reads still work, writes fail.allow-write-invalid— unmatched allowlist entry aborts with afilesystem.allowWriteerror and no residue.Validation
tsc --noEmit+npm run type-checknpx jest(full suite)npx eslinton changed filesmarkdownlint-cli2on changed docsbash -n+shellcheck --severity=erroron the smoke scriptnpm run generate:schemaCI-only: the three live-KVM cases require
/dev/kvmon a GitHub-hosted Ubuntu x86_64 runner and cannot execute on this macOS host. Everything locally runnable was validated — script syntax, shellcheck, and the assertion-contract tests incloud-hypervisor-ci-scripts.test.ts. The live job needs aworkflow_dispatchrun or thecloud-hypervisor-kvmlabel on this PR.