AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
MCP verified MIT Self-run

Qubes Mcp

mcp-alex-schose-qubes-mcp · by alex-schose

Autonomous AI agents inside a Qubes-isolated sandbox - tag-scoped Admin API access with dom0-mediated trust boundary.

No reviews yet
0 installs
35 views
0.0% view→install

Install

$ agentstack add mcp-alex-schose-qubes-mcp

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/mcp-alex-schose-qubes-mcp)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
3mo ago

Declared compatibility

Claude CodeClaude DesktopCursorWindsurf

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Qubes Mcp? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

qubes_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-managed tag. The qrexec policy hard-denies admin.vm.tag.{Set,Remove} for the MCP source qube; tagging happens only in two places: the operator's hand in dom0 (qvm-tags add|del ai-managed) and the create-time wrapper qmcp.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 on template/netvm/default_dispvm, opaque error responses.

  • Wrapped reads hide existence. qmcp.GetPropertyAIManaged returns 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.md for the

full 8-stage design. Each stage has its own install-*.sh, uninstall-*.sh, and test-*.py in deploy/.

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 [OPENQUESTIONS.md](OPENQUESTIONS.md).

Where this has been discussed:

  • qubes-devel design review — five Admin API / qrexec questions, answered

point by point by the Qubes project lead:

  • Qubes forum thread — original write-up and discussion:
  • Background — the threat-model case for moving MCP trust boundaries below

the protocol:

Status

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. Stages G and H are deferred until Stage I completes — both depend on a non-binary trust model (G's mcp-control lockdown is per-tier; H's remote reach needs Stage I's dom0 gate-lift) and are re-scoped once Stage I lands.

| 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 ` 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 (qmcpaudit.py) appends one JSON line per call to /var/log/qmcp-audit.log (root:qubes 0660, OAPPEND + flock); each line carries the sha256 of the previous, so any edit/delete/reorder breaks the chain (verify() + a python3 qmcpaudit.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) /tmp/install-a.sh less /tmp/install-a.sh # review before executing bash /tmp/install-a.sh mcp-control ~user/qubesmcp


The two positional arguments are the source qube and the path to the repo
inside it. Defaults: `mcp-control` and `/home/user/qubes_mcp`. Pass them
explicitly if you cloned to a different location.

The script clones `debian-13` → `ai-debian-13` (if needed), tags it
`ai-managed`, and installs the policy + qmcp scripts.

### Step 3 — Verify Stage A (from mcp-control)

cd ~/qubes_mcp .venv/bin/python deploy/test-stage-a.py


(All test scripts work from any cwd — they self-locate the package.)

Expect five PASS markers: existence-leak hidden; SetProperty cross-ref
opaque byte-identical; Spawn template cross-ref opaque byte-identical;
policy refusal on untagged; remove confirmation. The opaque-cross-ref
assertions land in the Stage A wrappers that `install-stage-a.sh`
ships today (they were backported in the Stage F2 bundle — see
reviewer ask #8), so a fresh install passes 5/5. If you're upgrading
an older deployment, expect the SetProperty and Spawn cross-ref
markers to FAIL until you ship Step 10 (which replaces the older
wrappers with the opaque-collapse versions).

### Step 4 — (Optional) Deploy Stage B for command exec + file transfer

From dom0:

qvm-run --pass-io mcp-control 'cat ~/qubesmcp/deploy/install-stage-b.sh' > /tmp/install-b.sh bash /tmp/install-b.sh mcp-control ~user/qubesmcp


Stage B briefly starts the `ai-debian-13` template, installs the two
template-side services into `/etc/qubes-rpc/`, and shuts the template back
down to commit changes.

Then from mcp-control:

.venv/bin/python deploy/test-stage-b.py


Four more PASS markers.

### Step 5 — (Optional) Deploy Stage C for the single-egress network sandbox

Stage C creates one ai-managed network qube — `ai-net-router` — that all
AI qubes route through by default. The operator chooses ai-net-router's
upstream in dom0 (`sys-firewall` for clearnet, `sys-whonix` for Tor, a
VPN qube, or `""` for offline); AI cannot change this. AI can still read
and set firewall rules on `ai-net-router` and on its own qubes.

From dom0:

qvm-run --pass-io mcp-control 'cat ~/qubesmcp/deploy/install-stage-c.sh' > /tmp/install-c.sh EGRESSUPSTREAM=sys-firewall bash /tmp/install-c.sh mcp-control ~user/qubes_mcp


Configurable via env vars (with defaults):

- `EGRESS_UPSTREAM=sys-firewall` — ai-net-router's netvm (any existing qube, or `""`).
- `EGRESS_TEMPLATE=fedora-43-xfce` — the AppVM template for ai-net-router.
- `EGRESS_LABEL=red` — Qubes colour.
- `EGRESS_MEMORY=500` — RAM in MiB.

Switch the upstream any time:

qvm-prefs ai-net-router netvm


Then from mcp-control:

.venv/bin/python deploy/test-stage-c.py


Eight PASS markers — egress visibility, default-netvm application,
explicit-null preservation, egress-qube lock, AI-qube netvm retarget,
firewall rules round-trip, untagged-target refusal, and former-ai-sys
invisibility.

### Step 6 — (Optional) Deploy Stage D for cloning + DispVM klass support

Stage D adds three things: `qmcp.CloneAIManagedQube` (clone an ai-managed
qube into a new ai-managed qube), the `DispVMTemplate` and `DispVM`
klasses in `qmcp.SpawnAIManagedQube`, and `qmcp.LifecycleAIManaged` (a
dom0 wrapper that replaces the Stage A `admin.vm.*` tag-scoped lifecycle
allow lines because qrexec's `@tag:` selector doesn't reach klass=DispVM
targets — see reviewer ask #6). No new dom0 provisioning — only the
policy + RPC scripts change.

From dom0:

qvm-run --pass-io mcp-control 'cat ~/qubesmcp/deploy/install-stage-d.sh' > /tmp/install-d.sh bash /tmp/install-d.sh mcp-control ~user/qubesmcp


Then from mcp-control:

.venv/bin/python deploy/test-stage-d.py


Six PASS markers — clone of ai-managed succeeds, clone of untagged
refuses opaquely, DispVMTemplate spawn sets `template_for_dispvms`,
DispVM spawn inherits template + ai-managed tag, DispVM from a plain
TemplateVM is refused by the `template_for_dispvms` cross-ref, and
end-to-end usability (start ai-dvm + run `whoami` as root inside via
`qmcp.RunInAIManaged` + clean shutdown — proves the
ai-debian-13 → DVMT → DispVM service-inheritance chain).

### Step 7 — (Optional) Deploy Stage E1 for device attach between ai-managed qubes

Stage E1 adds two dom0 wrappers (`qmcp.AttachDeviceAIManaged`,
`qmcp.DetachDeviceAIManaged`) that attach virtual block/USB/mic devices
between ai-managed qubes. Both backend and frontend must be ai-managed;
the wrapper collapses missing/untagged on either side to opaque
`"not found"`. Read-only enumeration (`admin.vm.device.{class}.{List,
Available}`) is tag-scoped at the policy layer — same shape as Stage C
firewall reads. No new qube provisioning.

In practice, **block** is the useful case (e.g. shared scratch volume
between two ai-managed AppVMs). **USB** requires `sys-usb` to be
ai-managed and **mic** requires the audio backend to be ai-managed —
both operator opt-ins. Default install leaves these dormant; the
wrappers are ready when the operator chooses to tag those backends.

From dom0:

qvm-run --pass-io mcp-control 'cat ~/qubesmcp/deploy/install-stage-e1.sh' > /tmp/install-e1.sh bash /tmp/install-e1.sh mcp-control ~user/qubesmcp


Then from mcp-control:

.venv/bin/python deploy/test-stage-e1.py


Six PASS markers (hard): tag-scoped list on ai-managed backend
succeeds; list on untagged refuses opaquely; attach refuses when
either endpoint is untagged; same for detach. Plus a SOFT block of
informational checks for a real loop-device round-trip (template-
dependent — qubes-core-agent's block enumerator may or may not
auto-expose `/dev/loop*` on a given Debian build, so those are
reported but not counted toward the pass total).

### Step 8 — (Optional) Deploy Stage E2 for ephemeral DispVMs

Stage E2 adds `qmcp.SpawnDisposableAIManaged` — a dom0 wrapper around
`admin.vm.CreateDispo

…

## Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [alex-schose](https://github.com/alex-schose)
- **Source:** [alex-schose/qubes-mcp](https://github.com/alex-schose/qubes-mcp)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.