qubes-mcp design review — five Admin API / qrexec questions

25 views
Skip to first unread message

Alex Schose

unread,
May 31, 2026, 6:08:10 AM (5 days ago) May 31
to qubes-devel
Hi all,

I've built a FastMCP server that exposes a tag-scoped Qubes Admin API
to AI agents via dom0-mediated wrappers — the qrexec policy + the
qmcp.* RPC scripts are the load-bearing pieces. Wanted to put the
design questions in front of the developer community before claiming
the design is idiom-correct.

Repo: https://github.com/alex-schose/qubes-mcp  (MIT, stages A through
F2 tested on Qubes R4.3-era; eleven dom0 qmcp.* RPC services)

Writeup with the broader threat-model case:
https://alexschose.com/writing/mcp-trust-boundaries-belong-below-the-protocol.html

Five questions, all reproduced from the README's reviewer-asks list:

1. Wrapped-reads existence-hiding. qmcp.GetPropertyAIManaged returns
the literal string "not found" indistinguishably whether the target
qube doesn't exist or simply isn't tagged ai-managed. Is uniform "not
found" from a dom0 qmcp wrapper robust against qrexec-layer leaks
(timing, error chains, side effects), or are there paths I'm missing?

2. qubes.Filecopy @tag:ai-managed -> @tag:ai-managed allow. Stage B
adds a policy line bypassing the default ask dialog for inter-qube
file transfer between ai-managed qubes. Are there assumptions in
qubes.Filecopy's implementation that depend on the operator dialog
being present?

3. target=@adminvm clause on tag-scoped admin allows. Without it,
qrexec attempts to start the target VM during read-only operations.
This is subtle, easy to miss, and not surfaced in current Qubes docs.
Worth a docs PR? Happy to write it.

4. Single-egress chokepoint vs cascade as Qubes idiom. The original
Stage C design was a cascade (ai-sys-firewall <- ai-sys-tor /
ai-sys-vpn) with multiple ai-managed network qubes. The implemented
design is one egress qube with operator-chosen upstream
(sys-firewall / sys-whonix / a VPN qube / null) and the
provides_network invariant on it. Documented Qubes patterns lean on
cascades; is the single-egress chokepoint an established pattern I
missed, or a reinvention?

5. @tag: matching on klass=DispVM targets on R4.3. Stage D testing
surfaced this: qrexec policy refuses
"admin.vm.Remove * mcp-control @tag:ai-managed allow target=@adminvm"
with "Request refused" when the target is a klass=DispVM, despite the
DispVM carrying the ai-managed tag (verified via Admin API from dom0).
Same rule works for klass=AppVM and klass=TemplateVM. Workaround
deployed: lifecycle (Start/Shutdown/Kill/Pause/Unpause/Remove) routes
through a dom0 wrapper (qmcp.LifecycleAIManaged) doing the
ai-managed check in dom0 with qubesadmin authority. Is the underlying
@tag:-on-DispVM behaviour intentional, a bug, or a configuration step
I'm missing?

Even short corrections would help. Thanks.

Regards,
Alex Schose
alexs...@atomicmail.io
PGP: F4F9 735B E899 60F4 70E6  ADBE 5312 A67E E8CE 816B

Marek Marczykowski-Górecki

unread,
Jun 1, 2026, 10:12:15 AM (4 days ago) Jun 1
to Alex Schose, qubes-devel
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

On Sun, May 31, 2026 at 02:56:37AM -0700, Alex Schose wrote:
> Hi all,

Hi,

> I've built a FastMCP server that exposes a tag-scoped Qubes Admin API
> to AI agents via dom0-mediated wrappers — the qrexec policy + the
> qmcp.* RPC scripts are the load-bearing pieces. Wanted to put the
> design questions in front of the developer community before claiming
> the design is idiom-correct.

Interesting idea, but honestly, with the current state of LLM, I would
be _very_ careful with giving them access to modify system state without
checking what it actually does. Mistakes like deleting important data
due to some hallucination happen quite often...
And this doesn't even tackle the prompt injection risk, which is yet
another story.

You described this risk in your article, and IIUC the idea is to simply
don't give access to sensitive qubes. That's fair. But surely people
will want to do use it to do something with their actual qubes, not just
empty mockups. Separation into "read-only" access and some additionally
protected write access would be useful.
Maybe have user confirm any (potentially) destructive action? Or maybe
make the LLM write an ansible playbook that user can review before
executing? This is hard problem, because such verification usually
requires substantial knowledge and time (if user would know how to do
that themselves, maybe they wouldn't ask LLM for it?) ...

> Repo: https://github.com/alex-schose/qubes-mcp (MIT, stages A through
> F2 tested on Qubes R4.3-era; eleven dom0 qmcp.* RPC services)
>
> Writeup with the broader threat-model case:
> https://alexschose.com/writing/mcp-trust-boundaries-belong-below-the-protocol.html

Regardless of the above remark, it is possible to scope some Admin API
access based on tags, without needing to introduce extra wrappers in
dom0. For example, to allow listing only qubes with ai-managed tag, you
can add rule like:

# allow calling admin.vm.List at all
admin.vm.List * mcp-control @adminvm allow target=dom0
# limit its scope to just ai-managed qubes
admin.vm.List * mcp-control @tag:ai-managed allow target=dom0

And for calls that are about specific qube, the first line wouldn't be
needed, for example:

admin.vm.Start * mcp-control @tag:ai-managed allow target=dom0

Oh, and in your article I see you had issue with disposables, likely
because they didn't have necessary tag added. I guess you will be
interested in `tag-created-with` feature - for example to automatically
add "ai-managed" tag to any qube created by mcp-control:

qvm-features mcp-control tag-created-with ai-managed

> Five questions, all reproduced from the README's reviewer-asks list:
>
> 1. Wrapped-reads existence-hiding. qmcp.GetPropertyAIManaged returns
> the literal string "not found" indistinguishably whether the target
> qube doesn't exist or simply isn't tagged ai-managed. Is uniform "not
> found" from a dom0 qmcp wrapper robust against qrexec-layer leaks
> (timing, error chains, side effects), or are there paths I'm missing?

I'm pretty sure you can't easily eliminate timing side channel.
Existence check is faster, than existence check + tag check. But I'm not
sure how much is that measurable in practice. Other than that, it should
be okay.

> 2. qubes.Filecopy @tag:ai-managed -> @tag:ai-managed allow. Stage B
> adds a policy line bypassing the default ask dialog for inter-qube
> file transfer between ai-managed qubes. Are there assumptions in
> qubes.Filecopy's implementation that depend on the operator dialog
> being present?

There is nothing requiring the GUI confirmation at the protocol level.
Note to use such allow rule, you need to use qvm-copy-to-vm, not just
qvm-copy - otherwise you cannot specify the target, and you'll get
interactive prompt (based on the default qrexec policy, unless you block
it).

But, is your intention really to allow any copy operation between any
AI-managed qubes? Didn't you want to allow copy only to/from
mcp-control?

> 3. target=@adminvm clause on tag-scoped admin allows. Without it,
> qrexec attempts to start the target VM during read-only operations.
> This is subtle, easy to miss, and not surfaced in current Qubes docs.
> Worth a docs PR? Happy to write it.

Sounds like a good idea, yes. Currently it's only mentioned in a comment
in the default policy file, very easy to miss.

> 4. Single-egress chokepoint vs cascade as Qubes idiom. The original
> Stage C design was a cascade (ai-sys-firewall <- ai-sys-tor /
> ai-sys-vpn) with multiple ai-managed network qubes. The implemented
> design is one egress qube with operator-chosen upstream
> (sys-firewall / sys-whonix / a VPN qube / null) and the
> provides_network invariant on it. Documented Qubes patterns lean on
> cascades; is the single-egress chokepoint an established pattern I
> missed, or a reinvention?

I'm not sure if I understand the question. Are you asking if a whole
separate ai-sys-firewall - ai-sys-tor etc chain is a good idea? If all
the network traffic should be configured uniformely (so, all over
clearnet, or all over VPN / Tor), I wouldn't bother with duplicating the
whole chain, and just create one egress qube attached to the chosen
point.

BTW, currently there is no way to limit value of property to be set via
admin.vm.property.Set on the qrexec level. So, with just qrexec policy,
you can't say "allow setting netvm only to value X, or none". You can do
that via a wrapper service, or an admin-permission event handler.

> 5. @tag: matching on klass=DispVM targets on R4.3. Stage D testing
> surfaced this: qrexec policy refuses
> "admin.vm.Remove * mcp-control @tag:ai-managed allow target=@adminvm"
> with "Request refused" when the target is a klass=DispVM, despite the
> DispVM carrying the ai-managed tag (verified via Admin API from dom0).
> Same rule works for klass=AppVM and klass=TemplateVM. Workaround
> deployed: lifecycle (Start/Shutdown/Kill/Pause/Unpause/Remove) routes
> through a dom0 wrapper (qmcp.LifecycleAIManaged) doing the
> ai-managed check in dom0 with qubesadmin authority. Is the underlying
> @tag:-on-DispVM behaviour intentional, a bug, or a configuration step
> I'm missing?

Does it still exist at the point you want to remove it? Disposables
usually have auto_cleanup property set, which means they get removed
automatically once stopped. And you cannot remove a running qube.

But, if you have a stopped disposable, with relevant tag added and it
still can't be removed based on the above rule, it sounds like a bug.
Check journalctl in dom0 for details about such denial.

- --
Best Regards,
Marek Marczykowski-Górecki
Invisible Things Lab
-----BEGIN PGP SIGNATURE-----

iQEzBAEBCAAdFiEEhrpukzGPukRmQqkK24/THMrX1ywFAmodkzgACgkQ24/THMrX
1ywkbgf9HdxKcjrCtDTIpmVwzPK9wEG3O5siPJmw0YD5hSJlcCuPlR9HGjNxo1C+
P6UYb++2Jjc6isWwhUMt7bQ8H9DOPBMxzDUag9LKpMhbIBhKFP97opFeyKsC/qcI
RYW9JEkdeXLXwYNslhfmGUE9qYSpYI7P2gRVGenSIcw5j7Z9CX3Nqk9W9L4u/Brz
rst9og6ODZSk+QDrEQqgLBx4H+MRmUfx4xiJX/dtWSdxhwBDwMa11Z4KdnaVc78c
Nv56ytWbEu+tyMOK/Zh4JqKgwKGtPTFpbwfJIFDG7TKEJEvM7Cb28lYONnootVLw
4Rr2rDpkFuCqPldrdTx+d0FGwyRqRQ==
=F0HH
-----END PGP SIGNATURE-----
Reply all
Reply to author
Forward
0 new messages