Imported from Polycentric-Labs/voidseal (
SKILL.md). Install upstream withnpx skills add Polycentric-Labs/voidseal. Copyright stays with the author.
voidseal — risk-tiered sandbox VM provisioning
A Claude-facing skill + PowerShell engine that auto-provisions risk-tiered, sandboxed Hyper-V VMs so you can work on risky things — autonomous agent loops, untrusted plugins, eventually malware — without exposing the host. It runs the full lifecycle:
INIT -> PROVISIONED -> STAGED -> SEALED -> RUNNING -> CAPTURED -> EXTRACTED -> DESTROYED
Design north star (Anthropic containment P1): supervise capability, not behavior — assume the agent/code inside is a prompt-injectable insider; make the blast radius structurally small. Isolation strength is matched to the task's risk via the tier axis.
Status (v1): core engine + the Tier 0/1 Hyper-V provisioning paths built + tested. The whole module is mock-backed green (866 Pester tests passed, 0 failed, 2 skipped; plus 56 pytest tests), AND the Tier-0
firefoxdisk-mode round-trip is now LIVE-PROVEN on real Hyper-V (2026-06-25 — Milestone 3: provision → host-verified seal gate → disk-passing workload → host-read result → clean teardown, end to end; the first live run drove out 7 realfake≠realhost/Hyper-V gaps, all since fixed). The Tier-1ralphlive run is still pending (mock-green; not yet exercised on metal — and serial-mode seed delivery needs the same seal-survival treatment disk mode got). Tier 0's container substrate is not yet built (v1 provisions Tier 0 via the Hyper-V path; the Docker /docker sbx/ sandbox-runtime substrate is a planned future addition). Tier 2/3 are scaffolded code paths validated with benign inputs only — no untrusted plugin or malware is run until you explicitly green-light verified isolation. Credential-injecting host-Envoy is deferred to Phase-1B (v1 Tier-1/ralph egress now ships an in-guest iptables default-DROP + Squiddstdomainallowlist —EgressMode='InGuestSquid', ported from the separate builder profile's mechanism — as defense-in-depth, mock-shape-asserted only, NOT a boundary; the host-verified boundary remains Phase-6).
The tier model
| Tier | Substrate | Network / egress | Credentials | Extraction | Lifecycle | v1 status |
|---|---|---|---|---|---|---|
| 0 | Hyper-V path / lightweight guest today (container runtime — Docker / devcontainer / docker sbx / sandbox-runtime — is PLANNED, not yet built) |
host-proxy allowlist (default offline) | none | host reads result dir | --rm ephemeral (container tier, when built) |
mock-backed, and LIVE-PROVEN once (the firefox disk round-trip on real Hyper-V, 2026-06-25) — the proof runs the Hyper-V path, not a container |
| 1 | Hyper-V Gen2 VM | Internal switch (NIC kept); in-guest egress defense-in-depth ships — EgressMode='InGuestSquid': iptables default-DROP + Squid dstdomain allowlist, mock-shape-asserted only, NOT a boundary |
scoped, on-demand, default none | host reads result dir | snapshot-revert | provisioning/seal validated (mock-backed; live run = the live smoke test, operator-run, elevated); in-guest egress defense-in-depth ships (mock-shape-asserted), NOT yet live-exercised — the boundary is Phase-6 — ralph proof |
| 2 | Hyper-V VM, disposable | no NIC (structurally starved) | none (enforced) | cold output-VHDX → quarantine VM → CDR → inert promote | create → destroy | scaffold / benign dry-run only |
| 3 | Hyper-V Gen2, no NIC + sinkhole VM | structurally no egress | none (enforced) | same as Tier 2, mandatory | detonate → wipe | scaffold / benign dry-run only |
On the "container" substrate for Tier 0: the v1 engine provisions only through Hyper-V (
New-SandboxVMis Hyper-V-only). The container runtime listed for Tier 0 (Docker / devcontainer /docker sbx/ sandbox-runtime) is design-intent for a future addition — it is not wired in v1, so nothing here invokesdocker sbx. The Tier-0firefoxproof runs through the Hyper-V path / a lightweight guest, not a container. On "validated": the whole engine is mock-backed — all tests run against the fake Hyper-V backend (no real VM is created in CI). The first real, elevated end-to-end run is the live smoke test (operator-run, elevated) (see Elevation requirement).
The loader structurally enforces the high-tier guarantees: a Tier ≥ 2 profile that declares any credential, egress mode, or allowlist entry is refused at load time (fail-closed), and the runtime seal gate refuses to mark a Tier-3 VM sealed if it detects any live NIC, secret volume, or egress route.
How to invoke
The Claude-facing entry point is scripts/Invoke-Voidseal.ps1, which dot-sources the
whole engine. From an elevated PowerShell session at the skill root:
# Dot-source the orchestrator (pulls in the backend + loader + provisioner + sealer + runner).
. .\scripts\Invoke-Voidseal.ps1
# Tier-1 Ralph loop (net-restricted Claude Code agent loop):
$report = Invoke-Voidseal -Tier 1 -Profile ralph `
-Workload @{ ResultPath = 'C:\sandbox\ralph-workdir\out' } `
-ParentDiskPath 'C:\sandbox\golden\debian-12-cloud.vhdx'
# Tier-0 Firefox bookmark organizer (offline, on a profile COPY):
$report = Invoke-Voidseal -Tier 0 -Profile firefox `
-Workload @{ ResultPath = 'C:\sandbox\firefox-organizer-out\bookmarks.html' }
$report.States # the states traversed, in order (INIT..DESTROYED on success)
$report.SealVerdict # $true only if Assert-Sealed certified the VM
$report.ExtractedArtifact # host-side path(s) the one-way extractor wrote
$report.TeardownStatus # teardown always runs (no orphaned VM/disk/switch)
-Profile accepts a bare name (resolved against profiles/<name>.psd1 or
tier-profiles/tier<N>.psd1), a .psd1 path, or an already-normalized profile
hashtable. -Tier is cross-checked against the resolved profile's own Tier (a
mismatch is a caller error and throws). See the full parameter list in the
Invoke-Voidseal comment-based help (Get-Help Invoke-Voidseal -Full).
What the orchestrator does, state by state
- INIT — load + validate the tier/workload profile (fail-closed on a bad/unknown profile).
- PROVISIONED —
New-SandboxVMcreates the substrate (Gen2, Secure Boot template, COM1 serial, Internal switch) from the profile. Left powered off. - STAGED —
Import-SandboxAssetfor eachStageAssetsentry (one-way IN, read-only ISO, before the seal). The loader screens both theStageAssetsand theMountssource keys against the secret-shaped-path list at load time, so a source whose name looks like a credential is refused before anything is attached. That screen is a path-shape lint, not a content scan: it catches the common accident, not a caller who renamed the file. - SEALED —
Lock-Sandboxcuts the VM to the tier's isolation, thenAssert-Sealedis a HARD GATE. If it fails, the deploy aborts here — the workload never runs and the VM is torn down. - RUNNING —
Start-SandboxWorkloadboots the sealed VM and delivers the entrypoint over the COM1 named-pipe serial seam (PowerShell Direct is Windows-guest-only; the Debian guest is driven over serial). - CAPTURED — the run-result + its host-side capture artifact are recorded out-of-band (P8 — never trust the guest to self-report).
- EXTRACTED —
Export-SandboxArtifact, one-way OUT. Tier 0/1: a trusting host-read of the emitted result. Tier ≥ 2: routes to the quarantine sink, which THROWS (the cold-VHDX/CDR flow is post-v1) — a hostile-tier artifact is never trustingly copied to the host. - DESTROYED —
Remove-Sandboxstops + unregisters + deletes the created disks. Teardown always runs (in afinally), so a mid-flow failure leaves no orphan.
The two v1 workload profiles
| Profile | Tier | What it runs | Egress | Data |
|---|---|---|---|---|
profiles/ralph.psd1 |
1 | bash ralph_loop.sh (frankbria/ralph-claude-code, pinned by commit SHA — it's bash, has no tags). Drives the claude CLI ≥ 2.0.76 headless (claude -p … --output-format json --allowedTools … --resume; no --dangerously-skip-permissions). Bare Debian VM, no nested devcontainer; native bubblewrap for defense-in-depth. |
inherits tier1's EgressAllowlist (api.anthropic.com, github.com, npm, pypi …), with the Serial seed's in-guest iptables+Squid defense-in-depth control over it (ships in the seed, mock-shape-asserted only; not a boundary — the host-verified boundary is Phase-6) |
OAuth token via read-only file bind-mount — never -e, never embedded |
profiles/firefox.psd1 |
0 | organize_bookmarks.py — dedupe + frecency-rank + auto-folder a Firefox profile, emit an importable <!DOCTYPE NETSCAPE-Bookmark-file-1> HTML file. Operates on a COPY (places.sqlite closed-copy + bookmarkbackups/*.jsonlz4 via lz4.block), never mutates live, never reads logins.json/key4.db/cookies.sqlite. |
none (offline; the lone optional dead-link check escalates to Tier 1) | defaults to SYNTHETIC/sample data; real profile data needs explicit per-task authorization |
Load-time and runtime refusals (fail closed)
These are enforced in code (scripts/lib/ProfileLoader.ps1) and covered by must-pass tests:
-
Secret-shaped-path refusal (a lint, not a boundary) — the loader screens the source keys of both
MountsandStageAssetsagainst a filename-and-directory pattern list ($script:SecretLeafGlobs,$script:SecretDirSegmentsand$script:SecretDirFilePairsinscripts/lib/ProfileLoader.ps1are the single source of truth;tier-profiles/SCHEMA.mdmirrors them). Dotenv files, PEM and other private-key extensions, keystores, SSH and GPG key material,credentials*.json,.netrc,.git-credentials,.npmrc,.pypirc,*.tfvars, service-account keys and anything under a.secrets/,.ssh/or.gnupg/directory all match; trailing dots or spaces and NTFS alternate-data-stream suffixes are normalized away first. This matches names, not contents. It never opens the file, so it cannot tell a real credential from a lookalike, and it does not fire on a credential you renamed. It lowers the odds of an accident; it is not a control that stops a determined profile author. It is why the Ralph profile points at a copied.tokenfile rather than the live~/.claude/.credentials.json, which does match. -
Credential + network starvation at Tier ≥ 2 — a Tier ≥ 2 profile MUST set
Credentials='None',EgressMode='None', and an emptyEgressAllowlist, or it is refused at load time. -
Extraction by tier — Tier ≥ 2 MUST use the cold-VHDX quarantine extraction; the trusting host-read is structurally unreachable for a hostile tier.
-
The seal gate is mandatory —
Assert-Sealedis host-verified and fails closed; a VM that fails it never reaches the workload-run state. Even a (hypothetical future) non-throwing false verdict still blocks RUNNING (defense-in-depth, test-locked). -
One-way boundaries — assets flow IN before the seal (read-only ISO); results flow OUT after the run (host-read at Tier 0/1; quarantine/CDR sink at Tier ≥ 2). No live host-filesystem mount of a hostile guest.
-
Harness-only for the dangerous tiers in v1 — Tier 2/3 paths run only with benign placeholder inputs. No live malware or plugin detonation until you explicitly green-light verified isolation.
Elevation requirement
Live runs touch real Hyper-V and require an elevated PowerShell session whose user is in
the Hyper-V Administrators group (or an elevated admin). New-SandboxVM runs a
TestAvailable preflight and fails closed with an actionable message if Hyper-V is
unreachable or the session is not elevated — it never half-builds a VM. The test suite
mocks the backend, so tests run unprivileged; only the live end-to-end path needs
elevation (that is the live smoke test, operator-run, elevated).
Operator docs
docs/operator-runbook.md— provision/run/teardown walkthrough, the host-patch CVE floors (Hyper-V ≥ May-2026 CU; VirtualBox > 7.2.6; VMware 17.6.3), the elevation requirement, credential-handling reminders, and how to run the tests.docs/tier-reference.md— the full tier model + the P1–P10 containment rubric (the per-tier acceptance checklist).guest-images/debian-12-cloud.md— the Debian 12 cloud-init NoCloud recipe (Secure Boot template, COM1 serial-getty, theCIDATAseed ISO).tier-profiles/SCHEMA.md— the tier + workload profile schema (the build contract every profile targets).
Repo layout
voidseal/ # the repo root IS the skill
├── SKILL.md # this file — the Claude-facing entry point
├── README.md # project landing page + quick start
├── scripts/
│ ├── Invoke-Voidseal.ps1 # top-level orchestrator (the entry surface)
│ ├── Get-VoidsealGoldenImage.ps1 # fetch+verify+convert the pinned Debian golden .vhdx
│ ├── Test-VoidsealPrereqs.ps1 # read-only pass/fail prerequisite checker
│ └── lib/*.ps1 # the engine — HyperVBackend (real+fake), ProfileLoader,
│ # Provisioner, Sealer, Runner, Workload, SeedBuilder,
│ # BuilderVM, SensitivityGate, ReleaseGovernor
├── guest/*.py # in-guest runner + outbox/screener (run INSIDE the sealed VM)
├── host/read_outbox.py # host-side fail-closed outbox reader
├── tier-profiles/{tier0,tier1}.psd1 # isolation contracts (+ SCHEMA.md)
├── profiles/*.psd1 # workload profiles: ralph, firefox, builder, example-skeleton
├── guest-images/debian-12-cloud.md # cloud-init NoCloud recipe (a doc, not an image)
├── docs/ # runbooks, tier reference, threat model, authoring guide
├── tests/ # Pester + pytest (unit, e2e, invariant, profile)
├── .github/ # issue + PR templates, CI workflow
├── AGENTS.md, CONTRIBUTING.md # contributor + AI-agent guidance
├── SECURITY.md, CODE_OF_CONDUCT.md # scoped-claims disclosure + conduct
└── CHANGELOG.md, LICENSE # release notes + MIT
Tier 2/3 profile files are not shipped as defaults (the engine supports them, and the orchestrator tests synthesize an in-memory Tier-2 fixture to exercise the starvation + quarantine paths). Add a Tier-2/3 profile only alongside an explicit verified-isolation green-light.
Testing
From the skill root (no elevation needed — the backend is mocked):
Invoke-Pester -Path tests/
As of the last local run: 866 passed, 0 failed, 2 skipped (unit, e2e, invariant refusals, and the
shipped profiles), plus 56 pytest tests for the guest and host helpers. The profile-loader refusals
(secret-shaped Mounts and StageAssets sources, Tier >= 2 starvation, the pre-seal gate) and the
seal-gate abort are must-pass tests.