cd /news/ai-agents/show-hn-vitrin-os-an-agent-first-dis… · home topics ai-agents article
[ARTICLE · art-74350] src=github.com ↗ pub= topic=ai-agents verified=true sentiment=· neutral

Show HN: Vitrin OS – An agent-first display server with per-app isolation

Vitrin OS is an open-source, agent-first display server that provides per-app isolation for human and AI agents to concurrently observe and operate GUIs under granular, revocable, capability-scoped authorization. The system uses a small trusted core with a new wire protocol, confining legacy Wayland/X11 applications to unprivileged nested shims, enabling precise control such as allowing an agent to fill one form in one Firefox window for five minutes while unable to see other windows, with instant human override via mouse or Escape key.

read19 min views1 publishedJul 26, 2026
Show HN: Vitrin OS – An agent-first display server with per-app isolation
Image: source

Vitrin OS is an open-source, agent-first display server: a small trusted core (vitrind

) speaking a new capability-native wire protocol, with every legacy Wayland/X11 application confined to its own per-app nested shim — so that humans and AI agents can concurrently observe and operate GUIs under granular, revocable, capability-scoped authorization. Every principal has identity; every action carries a capability; every action is journaled; the trusted core stays small and legacy complexity is exiled to disposable, unprivileged shims.

The full vision, object model, and technical architecture live in docs/PRD.md. The wire protocol is specified in protocol/vitrin-v0.xml with prose in docs/protocol/.

One sentence, no jargon:

An agent is allowed to fill in one form, in one Firefox window, for the next five minutes. It cannot see the password manager open beside it. The moment you touch the mouse, you have control back. Hold Escape for a second and its authority is gone — mid-click, mid-keystroke, whatever it was doing.

Every clause there is a mechanism, not a policy setting:

The claim What makes it true
allowed to fill in one form
A grant: (principal × resource × verbs × constraints). Not a role, not a config flag — a row in the grant table the core checks on every single action.
for the next five minutes
Expiry is a constraint on the grant, enforced at the chokepoint. There is no path that skips it.
cannot see the password manager
The agent's Firefox is in a realm, talking to its own private nested shim. The other window is not "hidden" from it; it is not in its universe. Scoping is structural.
you touch the mouse, you have control
Physical input is origin-tagged at the core and preempts agent input by construction, not by a race between two clients.
hold Escape and its authority is gone
The dead-man switch: the core revokes every live grant, and the agent's next call fails revoked . Proven against a real app in test_real_deadman.py .

The prompt that asks you to approve any of this is drawn by the trusted core itself — the thing that owns the screen — so no client can paint a fake one over it. That property has its own gate (test_real_consent.py

): the prompt is shown to occlude the app's real pixels while the agent's own capture path sees the app unchanged.

Agents that drive desktops today work screenshot-by-screenshot: capture the screen, pick pixel coordinates, click, capture again. The loop is slow, token-hungry, and race-prone — and it runs with all-or-nothing authority. The isolation unit is a whole VM or desktop session, so a single prompt-injected agent's blast radius is everything on screen. There is no structural way to say: this agent may operate this one form in this one app, may not read the password-manager window beside it, and loses all input the instant a human touches the keyboard.

The underlying stack cannot express that sentence. X11 grants every client near-total authority over the session — that is its protocol model, not a bug. Wayland achieved isolation by removing cross-client capabilities rather than mediating them, and its wl_seat

singleton has no notion of N concurrent authenticated principals. AT-SPI2, the accessibility tree agents use to avoid pixels, is an unauthorized backdoor onto every application's widgets. The managed-cloud answers (AWS WorkSpaces for AI agents, Windows 365 for Agents) have the right instinct — identity per agent, audit, oversight — but at whole-VM granularity, locked to proprietary clouds.

Vitrin is designed from day zero around the missing primitives:

Principals. Every connection authenticates an identity (human or agent workload) at handshake; agent credentials areSPIFFE/OIDC-shaped.Grants. No ambient authority. A grant is (principal × resource × verbs × constraints — expiry, rate ceilings, focus conditions), sender-constrained to the connection, attenuable, and revocable — immediately and transitively.Consent. Rendered by the trusted core itself, which owns the screen and input, so prompts cannot be spoofed by any client.Realms and shims. Apps launch into realms; each legacy app gets its own private nested shim server, so its universe contains only itself — scoping is structural, not policy.Human override. Physical input preempts agent input by construction.

See docs/PRD.md §1 for the full problem statement and §6 for the pillar-by-pillar requirements.

Phase 1 is complete, and there is a 60-second recording of it working: the core-drawn consent card over a real Firefox, a human clicking Allow, the agent filling a record it was handed, and — in a second take — a physically held Escape revoking every grant mid-task so the run exits non-zero. Two takes rather than one, because a revoked run cannot also print

PASS

.All nine epics (E1–E9) have landed on main

, and every milestone's named acceptance gate is closed under the rule this project holds itself to — decision D12: a milestone closes only when a named integration test passes green against the shipped binaries with no mock on any seam it claims. Component tests built on vitrin-mock-shim are never that evidence, and are labelled as what they are.

Milestone Gate Proven by
M1.2 — buffer path

tests/integration/test_real_app.py

— real core + real shim + real weston-terminal

M1.3— observation#107test_real_capture_fidelity.py

— an agent observes a real app through the enforcement chokepointM1.4— actuation, consent, dead-man#108+#109+#138test_real_actuation.py

, test_real_deadman.py

, test_real_consent.py

M1.5— demo#110test_demo.py

— the demo agent is handed a task record it did not author, fills it into a real app and submits it, and the gate demands the confirmation carry that record's36-bit checksum (a positive content check, not a pixel diff) plus the app's own byte-exact report. The app,form-target

, is repo-authored — a real Wayland client, no mock, but less independent than the weston-terminal

it replaced; disclosed in and the D12 seam tableexamples/agent-demo/README.md

What exists today, on main

:

Protocol spec v0protocol/vitrin-v0.xml(11 interfaces, wire format, error taxonomy; the source of truth), its RELAX NG schemaprotocol/vitrin-v0.rng, and a prose page per interface underdocs/protocol/kept in lockstep with every landing PR.Codegen—plusvitrin-scanner

cargo xtask codegen

generate theRust crate (message types + codec; pure data, no I/O) and the C headervitrin-protocol

shim/include/vitrin-protocol.hfrom the IDL.(vitrind

, the trusted core) — a realcrates/vitrin-core

Smithaycompositor with both output backends (--nested

, a host Wayland client;--headless --size WxH

, GPU-less pixman software rendering for CI); the capability kernel and in-memory grant table (request → pending → consent → resolved, sender-constrained, rate-limited, revocable); the realm/spawn manager (fork/exec the shim with a private runtime dir and a scrubbed, allow-listed environment); the core-rendered consent prompt with an exclusive input grab; the hold-Esc dead-man revocation switch; thedmabufimport path (— zero-copy shim→core frames on a real GPU, with shm as the universal fallback and an explicitdmabuf.rs

buffer_done(import_failed)

event rather than a silent black frame, so CI stays GPU-free); and the flight-recorder log. See theArchitecture at a glancesection anddocs/ARCHITECTURE.mdfor the full crate↔PRD map.The Wayland shim() — ashim/

wlrootsheadless compositor, C +Meson, outside the Cargo workspace by design. It forwards every composited frame to the core over the inherited socketpair, replays origin-tagged input into the app through its ownwl_seat

, and runs real apps:weston-terminal

, aGTKentry probe, andFirefox ESR(pinned), climbed one rung at a time and proven in CI with no mock on the shim seam.The agent SDK() — a pure-Python (stdlib-only, D8) wire client: connect → handshake →sdk/python/

request_grant

observe()

actuate_pointer

/actuate_text

, typed grant-error exceptions, and capture ergonomics (.to_png()

).is the demo agent and doubles as the M1.5 integration test; run it withexamples/agent-demo/run_demo.py

cargo xtask demo

(seeQuickstartbelow).CI.github/workflows/ci.yml: Rust fmt/clippy/tests (debug + release), IDL schema validation and generated-code drift checking, a Rust-free container build of the C shim, golden-frame pixel/SSIM checks, and a headless integration suite that drives the real core against real apps (weston-terminal

, GTK, Firefox ESR) with a hard 10-minute budget.

Phase 1 being complete is a statement about a defined slice, not about readiness for anything real. Read this list before the pitch above convinces you of more than it should.

No sandbox (decision D9, closes in Phase 2). No namespaces, noseccomp, noLandlock. A realm's app runs as the core's own uid with the core's full view of the filesystem and network, and the session D-Bus remains reachable. Environment hygiene confines the well-behaved; it does not contain the hostile. This is the big one — seeSecurity notes.The 24-hour fuzz soak has not been run.fuzz/

ships two cargo-fuzz targets (protocol decode,vitrin-ipc

framing) with a checked-in corpus CI replays on every PR plus a short per-PR burst, but the 24-hour clean run the plan asks for is still a manual, documented procedure rather than a scheduled job —fuzz/README.mdsays so in its own words. Nobody has run it end to end.wlcs conformance is advisory and mostly red:total=180 passed=3 failed=145 skipped=32

on the 2026-07-25 run. That number is expected and is not a bug count —wlcstests a general-purpose desktop compositor, and the shim deliberately serves a narrow surface (no touch, no fullxdg-shell

policy, no decoration protocols), so most failures are "no such global" rather than misbehaviour. It is still the honest number, it hasnot been re-measured since, andshim/wlcs/README.md

distinguishes the structural absences from the genuine ones. Never built by default; never gates a PR.One maintainer. Governance is a documented BDFL (plan §5); bus factor is tracked as a first-class project risk, not waved away.v0 protocol, and it will break. The IDL is frozen for Phase 1, not forever — Phase 2 adds semantic trees and epoch/CAS action semantics, and v0 clients should expect to move.

Nothing above is discovered-by-a-reader; each is a decision with a recorded rationale in docs/plan/20-decision-log.md.

From a clean clone to a running demo. Verified against the state of main

described above (headless venue; the nested venue additionally needs a Wayland session and Firefox ESR — see the note at the end).

The demo runs the real wlroots shim in both venues, so unlike a Rust-only build it needs the C shim built and a real Wayland client installed. cargo xtask demo

fails with the exact meson

command below if the shim is missing, rather than silently substituting anything.

git clone https://github.com/vitrin-os/vitrin-os.git
cd vitrin-os

sudo apt-get install -y libxkbcommon-dev libpixman-1-dev weston   # Debian/Ubuntu
bash shim/ci/install-deps.sh                                      # Meson + wlroots deps

cargo build --workspace

meson setup shim/build shim && meson compile -C shim/build

cargo xtask demo --headless

cargo xtask demo --headless --task name=Grace --task email=grace@example.net

Expect output ending in xtask demo: PASS

and a path to the run's flight recorder (flight.jsonl

) and captured frames. This exercises the real wire protocol, the real capability kernel, the real consent auto-approve path, the real wlroots shim, and a real app — vitrin-mock-shim

appears in no demo venue (#110 / #127); it survives only as a unit-test fixture for the component tests in tests/integration/

.

The same chain under the full integration suite, including the named milestone gates:

VITRIN_C_SHIM_BIN="$PWD/shim/build/vitrin-shim" bash tests/integration/run.sh

Nested mode (cargo xtask demo

, no --headless

) draws a real window on your own Wayland session and launches Firefox ESR in the realm — it needs a running compositor (GNOME, Hyprland, ...) and firefox-esr

(or VITRIN_DEMO_FIREFOX=/path/to/firefox

) on the machine you run it on; it is never a CI dependency (nested mode has no headless equivalent by design — plan risk R1).

Other useful commands, all covered in CI:

xmllint --noout --relaxng protocol/vitrin-v0.rng protocol/vitrin-v0.xml  # validate the IDL
cargo test --workspace && cargo test --workspace --release              # unit + integration tests
cargo xtask codegen --check                                              # generated-code drift check
php
flowchart TD
    HUMAN["human<br/>physical input"] --> CORE
    AGENT["agent principal<br/>SDK client"] -->|"capability-native wire protocol<br/>handshake · grants · observe · actuate"| CORE

    CORE["<b>vitrind — trusted core (Rust)</b><br/>capability kernel · grant store · compositor<br/>input router · consent surface · journal"]

    CORE <-->|"frames up (dmabuf fd)<br/>input down (origin-tagged)"| S1
    CORE <--> S2

    subgraph R1["realm"]
        S1["per-app Wayland shim"] --> A1["unmodified legacy app"]
    end
    subgraph R2["realm"]
        S2["per-app Wayland shim"] --> A2["unmodified legacy app"]
    end

The core is the entire trusted computing base: capability kernel and grant store, scene composition, input routing, consent surface, journals. Legacy apps never touch it directly — each is launched with WAYLAND_DISPLAY

pointing only at its own private shim, which is itself an unprivileged client of the core. Frame buffers move as dmabuf file descriptors over SCM_RIGHTS

(zero-copy, one extra IPC hop — the gamescope/Qubes precedent). Window management policy, decoration, and theming stay out of the core, permanently.

Vitrin's design claims are strong, and the Phase-1 MVP does not yet deliver all of them. The gaps below are deliberate, settled decisions with a scheduled closure, not oversights — and they are stated here, at the front of the security story, rather than buried in a module doc, because a half-believed confinement claim is worse than an honest gap.

Realm confinement today is environment-structural only. When the core launches a realm's app it forks a per-app shim, hands it one end of a socketpair as its identity (no credential, no handshake — holding the descriptor is being that realm's shim), gives it a private 0700

runtime directory, and builds its environment from nothing: only names the operator allow-listed in realm.toml

, plus a WAYLAND_DISPLAY

pointing at that realm's own private socket. DISPLAY

, the host WAYLAND_DISPLAY

, WAYLAND_SOCKET

, XAUTHORITY

and the host XDG_RUNTIME_DIR

cannot reach the app at all. No unrelated descriptor of the core's — the agent listener, the flight-recorder log, other realms' sockets, capture memfds — crosses the fork, and the child starts from a defined signal state rather than inheriting whatever the operator's shell happened to be ignoring. Both are enforced by the fork itself (a close_range

sweep and a disposition reset between fork

and execve

), not by every other module remembering to be careful.

That is the complete list of what confines a realm right now.

No sandbox (decision D9, closes in Phase 2). There areno namespaces, no seccomp filter, and no Landlock policy. The shim and its app run as the core's own uid with the core's full view of the filesystem and the network. An app that ignoresWAYLAND_DISPLAY

and connects directly to a path it already knows is not stopped by anything in the MVP. Real sandboxing arrives with the Phase-2 powerbox (E2.6/E2.7). Environment hygiene confines the well-behaved; it does not contain the hostile.The session The core advertises noD-Busis reachable (known hole, closes with P13 in Phase 2).DBUS_SESSION_BUS_ADDRESS

and pointsXDG_RUNTIME_DIR

at the realm's private directory, so a well-behaved client finds no bus. But advertisement is not reachability:/run/user/<uid>/bus

is still on the filesystem and still connectable by any process of that uid, and the abstract-socket namespace is still shared. In practice, running Firefox — the Phase-1 acceptance app — means allow-listingDBUS_SESSION_BUS_ADDRESS

explicitly, which turns the implicit hole into an audited one. This is a lateral-escape path of exactly the shapePRD§15 catalogues (D-Bus activation of a privileged helper);P13 closes it with a loopback-only network namespace plus an empty mount namespace, so that there is nothing to reach rather than nothing advertised.Same-uid separation is not attempted. The0700

runtime directory bounds otheruserson the machine, not other processes of this user. Note what the realm'sXDG_RUNTIME_DIR

therefore is and is not: its value,$XDG_RUNTIME_DIR/vitrin-0/<realm>

, sits one level below the directory holding the core's own agent socket and this run's flight-recorder log, so it names the control plane as much as it hides it. Redirecting it means a well-behaved client finds its own realm's socket instead of the host session's — it does not mean the app cannot reach the rest, because under D9 it runs as the core's uid and can derive those paths with or without a variable pointing at them.

The spawn path and every decision above are documented in full in crates/vitrin-core/src/spawn.rs;

realm.toml

's own security rules are in .

examples/realm.toml

Path What it is
docs/PRD.md

docs/ARCHITECTURE.md

protocol/vitrin-v0.xml

protocol/vitrin-v0.rng

docs/protocol/

docs/plan/

docs/demo/

crates/vitrin-core/

vitrind

— the trusted core (compositor, capability kernel, grant store, realms, consent)crates/vitrin-protocol/

crates/vitrin-scanner/

crates/vitrin-ipc/

SCM_RIGHTS

, SO_PEERCRED

, backpressure policycrates/vitrin-mock-shim/

crates/vitrin-golden/

crates/xtask/

cargo xtask codegen [--check]

/ bless

/ demo [--headless]

shim/

sdk/python/

vitrin_os

package, D8)examples/agent-demo/run_demo.py

cargo xtask demo

tests/integration/

vitrind

binary + real shim + real apps over a real socket.github/workflows/ci.yml

The toolchain is pinned to Rust 1.94.1 in rust-toolchain.toml; rustup installs it automatically on the first cargo

invocation. The pin is exact rather than stable

because cargo xtask codegen --check

compares generated output byte-for-byte and that output depends on rustfmt's exact formatting decisions (the codegen shells out to rustfmt

; the pinned toolchain's default profile ships it). The crates' MSRV is 1.87. CI builds with RUSTFLAGS="-D warnings"

.

xmllint --noout --relaxng protocol/vitrin-v0.rng protocol/vitrin-v0.xml

cargo test --workspace
cargo test --workspace --release

cargo xtask codegen --check

cargo xtask codegen

cc -std=c11 -Wall -Werror -I shim/include -c shim/tests/test_header_compiles.c -o /dev/null
cc -std=c11 -Wall -Werror -I shim/include shim/tests/test_golden_frames.c -o /tmp/golden && /tmp/golden

The wire format (little-endian frames, 8-byte header, at most one fd per message), object-id rules, ordering guarantee, error taxonomy, and versioning rules are defined in 00-conventions.md. There are two connection classes sharing one wire format: agent principals on the core's listening socket, and per-app shims on a core-inherited socketpair. A grant petition co-mints the grant, a consent observer, and the facets (view, pointer, text) in one request; facets are born inert and confer nothing until the grant resolves. Where prose and IDL disagree, the IDL's <description> text wins.

Interface Purpose
vitrin_handshake

vitrin_principal

vitrin_realm

realm-0

)vitrin_grant

vitrin_consent

vitrin_view

vitrin_actuator_pointer

vitrin_actuator_text

vitrin_shim_session

vitrin_shim_surface

vitrin_shim_seat

Condensed from docs/PRD.md §8:

Phase 0 — Spec & manifesto. Vision, object model, wire-protocol draft (the PRD in this repo).Phase 1 — MVP slice*(complete). Trusted core (headless + nested), one wlroots Wayland shim, Firefox in a realm, and an agent that captures the realm and injects scoped input — gated by a single grant with consent rendered by the core.Phase 2 — Semantic + epochs(next)*.AccessKit/AT-SPI2 bridge, versioned and diffable semantic trees, epoch/CAS action semantics, VLM fallback for treeless surfaces, native semantic demo app, filesystem powerbox v0, network authority v0 (per-realm loopback-only netns, egress as a grant).Phase 3 — Network + X11 + fleet.QUICnetwork sessions, per-app X11 shim with embedded WM, multi-realm headless fleet mode, journal replay tooling, synthetic-path FUSE layer, credential wallet v0, mission-control shell v0.Phase 4 — Horizon. Session mode on bare DRM/KMS, Flutter/iced/egui semantic backends, capability-remoting protocol hardened for third-party clients, EUDI/OID4VC conformance — entered only when adoption justifies the support burden.

Work is tracked as GitHub issues in vitrin-os/vitrin-os. Phase 1 is split into nine epics, each carrying exactly one track:*

label (track:protocol

, track:rust-core

, track:c-shim

, track:sdk

, track:ci-docs

), sequenced by milestones M1.1

M1.5

.

Branches:p<phase>.<epic>.<task>-slug

, e.g.p1.1.1-protocol-idl

.Commits:Conventional Commits,type(scope): summary

, where scope is the track (protocol

,rust-core

,c-shim

,sdk

,ci-docs

) orroot

. Reference the tracking issue in the footer (Closes #10

/Refs #10

).Protocol changes are paired edits, never one alone: changeprotocol/vitrin-v0.xml

(andprotocol/vitrin-v0.rng

only if the dialect itself changes) together with the matchingdocs/protocol/NN-*.md

page, then validate withxmllint --noout --relaxng protocol/vitrin-v0.rng protocol/vitrin-v0.xml

. After IDL edits, runcargo xtask codegen

and commit the regenerated code in the same change.Language: English only — code, docs, commits, issues, PRs.

Split per decisions D-005 and D-016 (docs/plan/20-decision-log.md

). NOTICE is the normative path→license map; the

SPDX-License-Identifier

header on a file is authoritative for that file.Apache-2.0() — the protocol (LICENSE

,protocol/vitrin-v0.xml

), the bindings generated from it (protocol/vitrin-v0.rng

crates/vitrin-protocol

,shim/include/vitrin-protocol.h

), the code generator that emits them and its driver (crates/vitrin-scanner

,crates/xtask

), the conformance and fuzz instruments, theclient SDKs(), the integration harness (sdk/python/

) and the shippedtests/integration/

.examples/

MPL-2.0(LICENSE-MPL-2.0

) — the reference implementation:crates/vitrin-core

(the trusted core),crates/vitrin-ipc

, and the per-app Wayland shim under.shim/

CC-BY-4.0() — spec prose (LICENSE-CC-BY-4.0

,docs/PRD.md

,docs/protocol/

).docs/plan/

GPL-3.0-only(LICENSE-GPL-3.0-only

) — one carve-out,, the advisoryshim/wlcs/

WLCSconformance module, which links GPL-3.0 headers from Canonical's wlcs. It is never built by default, never installed, and never linked intovitrin-shim

.

What that means for you, plainly: you never have to touch an MPL-2.0 file to write a client, build an alternate compositor or shim, or ship an integration — the protocol, the generated bindings, the codegen and the SDKs are all Apache-2.0, patent grant included. The copyleft binds one group only: people who modify the trusted core itself, whose changes to a capability kernel should come back. MPL's copyleft is per-file, so applications running under Vitrin are unaffected — running an app inside a shim does not make it derivative of anything here.

No CLA. Contributions are taken under the Developer Certificate of Origin (a Signed-off-by:

line), not a Contributor License Agreement — decision D-012. Nobody is asked to assign copyright: contributors keep theirs, and the project never acquires the unilateral power over their code that a CLA would hand it. That is deliberate — it is what stops the split above from being merely this year's mood.

No patents. Vitrin OS files none and intends to file none (D-015). The design is protected by publishing it — a timestamped spec is itself the prior art — and by the Apache-2.0 §3 and MPL-2.0 §2.1(b) patent grants that ship with the code. Both are in force today. A third leg, joining the Open Invention Network's royalty-free cross-licence, is decided but not yet done. None of this is a patent wall, and none of it is a freedom-to-operate opinion.

── more in #ai-agents 4 stories · sorted by recency
── more on @vitrin os 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/show-hn-vitrin-os-an…] indexed:0 read:19min 2026-07-26 ·