qubes-mcp
Autonomous AI agents inside a Qubes-isolated sandbox - tag-scoped Admin API access with dom0-mediated trust boundary.
Install / Use
claude mcp add alex-schose -- npx -y github:alex-schose/qubes-mcpIf the server publishes to npm under a different name, use that package instead — check the repo README.
MCP Server
Model Context Protocol server
Quality Score
Category
SecuritySupported Platforms
Skill content
View source on GitHubqubes_mcp
Autonomous AI workflows inside a Qubes-isolated sandbox. AI agents get real capabilities — provisioning qubes, building templates, running pentests, moving files between them — while the operator's actual system stays structurally invisible to the agent. Qubes provides kernel-level isolation; this project provides the capability surface AI agents need, mediated by dom0 wrappers so the trust boundary is enforced, not trusted.
Threat-model-driven implementation: human-designed boundaries, AI-assisted code. Review from Qubes engineers welcome and needed.
FastMCP server that exposes a tag-scoped Qubes Admin API sandbox to AI
assistants. An untrusted-AI principal runs inside a dedicated qube
(mcp-control) and can manage a subset of qubes carrying the ai-managed
tag — without dom0 access, without visibility into untagged qubes, and
without the ability to mutate tags.
Stages A through F3 and the Stage I Wave-1 sub-stages (I-0..I-5) are tested and working on Qubes R4.3-era systems — see the Status table below. Stages G–H are designed but deferred until Stage I completes.
Architecture
Every privileged action the AI takes is mediated by dom0. The MCP principal reaches dom0 only through qrexec; dom0 enforces the invariants and acts on the sandbox on its behalf. The AI never touches qubesd directly and never sees outside its tag scope.
── dom0 (TRUSTED) ────────────────────────────────────────────────
qrexec policy: policy/30-mcp-control.policy
qmcp.* wrappers: force-tag on create, cross-ref checks, opaque errors
operator sets the `ai-managed` tag here, by hand (qvm-tags)
▲
│ qrexec only — no dom0 shell; target=@adminvm routes to dom0
│
── mcp-control (UNTRUSTED — the AI / MCP principal) ───────────────
cannot reach a dom0 shell · cannot see untagged qubes · cannot set/remove tags
│
│ dom0 acts on its behalf, only on tagged qubes ↓
▼
── qubes tagged `ai-managed` (THE SANDBOX) ────────────────────────
ai-vm-1 ai-vm-2 ai-dvm …
network egress funnels through one qube — ai-net-router — whose
upstream only the operator sets in dom0 (Stage C)
untagged qubes = the operator's real system = invisible to the AI
Design highlights
- Tag-scoped trust boundary. AI sees and modifies only qubes carrying the
ai-managedtag. The qrexec policy hard-deniesadmin.vm.tag.{Set,Remove}for the MCP source qube; tagging happens only in two places: the operator's hand in dom0 (qvm-tags <vm> add|del ai-managed) and the create-time wrapperqmcp.SpawnAIManagedQube, which force-tags every qube it creates. - Dom0-mediated wrappers (
qmcp.*). State-changing calls route through small Python scripts in/etc/qubes-rpc/that enforce invariants in dom0 before touching qubesd: forced tagging on creation, cross-reference validation ontemplate/netvm/default_dispvm, opaque error responses. - Wrapped reads hide existence.
qmcp.GetPropertyAIManagedreturns the literal string"not found"indistinguishably whether the target qube doesn't exist or simply isn't tagged. The MCP-side helper normalises all qrexec failure modes (policy deny, no-such-VM, transport error) to the same opaque"not found or refused"so the lifecycle path doesn't leak either. - Multi-stage rollout, reversible at each step. See
CLAUDE.mdfor the full 8-stage design. Each stage has its owninstall-*.sh,uninstall-*.sh, andtest-*.pyindeploy/.
Reviewer asks
This is human-designed, AI-assisted code, and review from people who know the
Qubes Admin API and qrexec policy (R4.2+) is genuinely wanted. The detailed,
numbered questions — existence-oracle robustness at the qrexec layer, @tag:
matching on klass=DispVM, single-egress vs. cascade as a Qubes idiom,
event-stream payload minimisation, cap-as-contract disk budgeting,
security-tag inheritance on clone_vm / CreateDisposable (a created qube
must be stripped to its umbrella, not assumed clean), and more — are written
up in OPEN_QUESTIONS.md.
Where this has been discussed:
- qubes-devel design review — five Admin API / qrexec questions, answered point by point by the Qubes project lead: https://groups.google.com/g/qubes-devel/c/4NuSqL64DVE
- Qubes forum thread — original write-up and discussion: https://forum.qubes-os.org/t/41387
- Background — the threat-model case for moving MCP trust boundaries below the protocol: https://alexschose.com/writing/mcp-trust-boundaries-belong-below-the-protocol.html
Status
Current version: 0.9.0 (pre-1.0 — see CHANGELOG.md).
Development up to 0.9.0 was tracked as lettered stages; that vocabulary is kept
in deploy/ filenames and in the design document as the as-built record, and the
table below is still organised that way. Releases are versioned from 0.9.0 on.
0.9.0 is deliberately pre-1.0: the resource axis is complete and enforced, but least privilege is not yet operable end to end — a newly created qube is born untiered and so has no capability until an operator tiers it, which means an operator action sits between every create and its first use. 1.0.0 is reserved for the release that closes that gap.
Stages A through F3 land the binary trust boundary: a qube tagged
ai-managed is visible and acted on through the qmcp.* wrappers;
an untagged qube is invisible. The F band closes that surface with
disk-budget visibility (F3).
Stage I (graduated authority) is the current work line. It adds
graduated authority within ai-managed — resource tiers, an
action gate (per-call consent for destructive ops), per-trust-class
source qubes, a sign-only secrets vault, and persona presets — so a
hallucinating or prompt-injected agent cannot destroy real data
inside the boundary just because it has a qrexec channel.
Stage I lands as sub-stages I-0..I-11 in three waves; I-0 (cap-as-
gate), I-1 (read-surface scope redaction), I-2 (dom0 audit log),
I-3 (the tier taxonomy + resolution helper, landed behaviour-neutral),
I-4 (tiers on the policy-scoped surfaces), and I-5 (tiers on the wrapper
- exec surfaces, with the least-privilege flip available) are done — all listed below. Wave 1 (I-0..I-5) is complete, behaviour-neutral until the operator tiers the fleet and runs the flip; Wave 2 (I-6..I-8, the action gate) is next. Stage G0 (gateway input boundary) was pulled ahead of Wave 2 — a 2026-07-24 architecture review found reachable boundary breaks in the shipped tree, so the tier-independent hardening that closes them shipped now (see the G0 row). The rest of Stage G (mcp-control host hardening, G1/G2) and Stage H remain deferred until Stage I completes — both depend on a non-binary trust model (G1's lockdown is per-tier; H's remote reach needs Stage I's dom0 gate-lift).
| Stage | Capability | State |
|---|---|---|
| A | Tag-scoped lifecycle + spawn + wrapped property read/write + existence hiding | tested |
| B | Root command execution + inter-qube file transfer inside ai-managed qubes | tested |
| C | Single-egress network sandbox (ai-net-router chokepoint, operator-chosen upstream, tag-scoped firewall control) | tested |
| D | Clone (qmcp.CloneAIManagedQube) + DispVMTemplate/DispVM klass support in qmcp.SpawnAIManagedQube + dom0 lifecycle wrapper (qmcp.LifecycleAIManaged) covering klass=DispVM uniformly | tested |
| E1 | Device attach/detach (qmcp.AttachDeviceAIManaged / qmcp.DetachDeviceAIManaged) between ai-managed qubes, plus tag-scoped block/usb/mic enumeration | tested |
| E2 | Ephemeral DispVMs via qmcp.SpawnDisposableAIManaged (auto-cleanup on shutdown) + qubes_run_disposable one-shot | tested |
| F1 | Wrapped feature.Set (qmcp.SetFeatureAIManaged) — internal denied (operator-only), opaque cross-ref for audiovm/guivm, echoes post-set value; direct feature.Set stays denied | tested |
| F2 | Filtered event stream (qmcp.AIManagedEvents) — bounded-window batch (duration clamped [1, 120]s) of admin events whose subject is ai-managed; minimal {event, subject, subject_klass, ts} payload with whitelisted tag kwarg for tag-add/delete; ships with the opaque-cross-ref backport on SetPropertyAIManaged + SpawnAIManagedQube (closes reviewer ask #8) | tested |
| F3 | AI-scoped disk-budget visibility (qmcp.GetPoolStats) — sum of the persistent footprint of every ai-managed qube (each private, plus root for persistent-root klasses; COW root + ephemeral volatile excluded) + operator cap from /etc/qmcp/pool-cap (re-read per call); returns {used, cap, headroom}; pool topology and operator-side volumes intentionally absent. Cap is a contract operator → AI, not a sensor. (Accounting corrected 2026-06-12 — was every volume's provisioned size, which over-stated real usage ~8×.) | tested |
| I-0 | F3 cap promoted from advisory signal to a hard gate on every create path (qmcp.SpawnAIManagedQube / qmcp.CloneAIManagedQube / qmcp.SpawnDisposableAIManaged). Refuses with opaque "pool cap exceeded" before the Admin API call; measurement is byte-identical to F3 (shared qmcp_budget.py) so AI's (used, cap, headroom) predicts the gate. A per-qube ceiling /etc/qmcp/private-cap bounds any one qube's persistent private (a spawn may request a bigger private_size up to it). Because a volume can't exceed its size, Σ persistent ≤ cap is a hard ceiling on real usage. Cross-ref refusal still wins; caps fail closed. No new RPC, no policy change. First sub-stage of Stage I. | tested |
| I-1 | Read-surface name-leak fix (finding F-3): every VM-valued property read (netvm/template/default_dispvm/guivm/audiovm/management_dispvm) and the list template field is routed through a shared dom0 redactor (qmcp_scope.py) — a referenced qube's name survives only if it is itself ai-managed, else collapses to the opaque <out-of-scope> sentinel; tags reads are filtered to the qmcp vocabulary. The read-path sibling of the F2 write-path cross-ref opacity. Patches the two read wrappers; no policy change, no new RPC. | tested |
| I-2 | Hash-chained, AI-unreachable dom0 audit log of every state-changing qmcp.* call. A shared dom0 helper (qmcp_audit.py) appends one JSON line per call to /var/log/qmcp-audit.log (root:qubes 0660, O_APPEND + flock); each line carries the sha256 of the previous, so any edit/delete/reorder breaks the chain (verify() + a python3 qmcp_audit.py verify CLI re-check it). The 8 state-changing wrappers route their single emit() funnel through audit() and log a whitelisted summary (qube names / property + feature keys / action) — never a property/feature value. Best-effort (never blocks an op); AI-unreachable by construction (no service reads the log; no policy line exposes it). Foundational before the tier model. No new RPC, no policy change. | tested |
| I-3 | Tier taxonomy + dom0 tier-resolution helper — the keystone of the resource axis. Graduates the binary boundary into a cumulative ladder within ai-managed: ai-managed (read floor) < ai-exec (+commands) < ai-net (+firewall write) < ai-full (+lifecycle/property/clone/spawn/feature/attach/detach); ai-dump is an orthogonal copy-IN-only sink. A shared dom0 helper (qmcp_tier.py, sibling-loaded like qmcp_budget/qmcp_scope/qmcp_audit) exposes effective_capabilities(vm) → a frozenset of capability tokens, so the wrappers ask CAP_FULL in caps and stay decoupled from the taxonomy. Behaviour-neutral: ships inert (no wrapper sources it until I-5) in compat mode (untiered ai-managed = full = today's boundary). AI can neither mutate tags (keystone) nor read the tier tags (a tags read stays ["ai-managed"] — the authority topology is not an oracle). Two-phase migration: enforce in I-4/I-5, then flip /etc/qmcp/tier-default to ro for least privilege. No new RPC, no policy change. |
Truncated for display — read the full file on GitHub.
Related Skills
momen-cursurrules-prompt-file
40.6kCursor rules for building custom frontends with Momen.app as headless BaaS with GraphQL API, actionflows, AI agents, and Stripe integration.
pyspark-etl-best-practices-cursorrules-prompt-file
40.6kCursor rules for PySpark ETL development with code style, joins, window functions, map operations, and Iceberg patterns.
semiotic-react-dataviz-cursorrules-prompt-file
40.6kCursor rules for Semiotic data visualization library with 30+ chart types, MCP server, and AI-assisted chart generation.
Agent-Reach
72.4kGive your AI agent eyes to see the entire internet. Read & search Twitter, Reddit, YouTube, GitHub, Bilibili, XiaoHongShu — one CLI, zero API fees.
