A self-hostable, web-native operating system. Your cloud, your hardware, your rules.
This is the core OS repo. The Vulos suite spans companion repos:
vulos-office ·
vulos-relay ·
vulos-cloud
Vulos is a sovereign personal server with a web-native desktop you run on your own hardware. The shell is a React single-page app — a real window manager with virtual desktops, a dock, and bundled apps — that runs in any browser. Open it from a laptop, a phone, or a shared screen and you get the same full desktop, backed by a single self-contained Go binary that embeds the entire frontend.
At its center is a sovereign assistant: an on-box AI agent that is aware of your calendar, contacts, files, and reminders, and that can act on your behalf — but only under a hard security contract. Every action with side effects is a confirmation-gated proposal, off-box egress is fenced by a tier-aware sovereignty Guard, and the language model runs through your own on-box gateway by default. The wedge is agency, not just privacy: your own server, your own AI, acting for you — without handing your inbox and calendar to a third party.
No Electron, no VNC, no always-on remote-desktop session, no third-party login. Web apps run natively in the shell; native Linux GUI apps stream over WebRTC only while their window is open. The whole thing flashes to a USB stick, deploys to a cloud server, or runs in Docker.
"Vula" is isiZulu for "open."
- Sovereign assistant — an on-box AI agent aware of your calendar, contacts, files, and reminders. It reads with a curated, read-only toolset and proposes anything with side effects. Answers stream token-by-token over SSE. See the security model below.
- Proactive AI Home + ⌘K — the desktop opens as a home (agenda, focus, pending invites, reminders, proposals), not just a launcher. A unified
⌘Kcommand palette drives the whole shell. - Window-manager shell — drag, resize, snap, and tile windows; virtual desktops; Mission Control overview; a dock with running-app indicators; persisted window sessions. Pure JSX React 19 + Vite + Tailwind.
- Bundled apps — Terminal (persistent PTY over xterm.js), Files / Drive, App Hub, Activity Monitor, Settings, Notes, Peering, plus a suite under
apps/: Browser, Office, Calculator, Camera, Clock, Gallery, Image Editor, Maps, Music, PDF Viewer, Weather, and more. - Passwordless auth, no third parties — WebAuthn/FIDO2 passkeys as the primary factor (with clone/replay counter detection), QR / phone-approval login for shared clients, device PIN, and TOTP 2FA fallback. Forced recovery-phrase signup with a client-side master-key unwrap. No Google SSO, no OAuth login flows.
- Files with a real ACL — a Files service with a viewer < editor < owner role hierarchy enforced server-side, plus content-blind (sealed) file sharing and share-by-email with locality routing. Large files use a resumable, chunked upload (tus-style): each chunk rides the relay as an ordinary bounded request, the box reassembles into your own storage with per-chunk + whole-file integrity, and an interrupted upload resumes from the committed offset instead of restarting.
- Notifications + sovereign Web Push — a real notifications system, plus opt-in Web Push where your box sends notifications directly to your device's browser vendor (FCM/Apple/Mozilla). It's outbound-only (works behind NAT, no central relay), and payloads are end-to-end encrypted per RFC 8291 — the vendor routes but can't read them. Enable it per-device under Settings → Notifications; Do Not Disturb is honoured by the box before any push is sent.
- Portability & transparency — one-click "Export my data" account portability, and legible-trust surfaces that make your sovereignty level visible.
- Board / whiteboard — an embedded collaborative board surface (gated by
BOARD_AUTH_SECRET). - On-demand app streaming — native Linux apps stream via WebRTC with GPU-accelerated encoding (NVENC / VA-API / VP8 fallback). Close the window and the stream stops.
- Sovereign big calls (self-host SFU) — small Meet calls run peer-to-peer; a call that outgrows the mesh cap can escalate to media on your own box's SFU (opt-in
VULOS_SFU_HOST) rather than a third-party server. Media rides your infra; recording and transcription stay local. The owner sees host status under Settings → Box Health. - On-box LLM gateway — assistant LLM/embeddings traffic routes through the on-box
llmuxsovereign gateway by default; a local vector store powers on-instance retrieval (RAG). You choose the provider and sovereignty tier. - Peering & sync — every instance has its own Ed25519 identity; leaderless CRDT sync across your nodes; a full VulaID key lifecycle (rotation, revocation, account-anchored recovery, X3DH-style forward secrecy); AirDrop-style local Drop; real-time collaboration over Yjs with per-document ACL.
- Local-first storage — SQLite on the box, S3/Restic for encrypted backup. Your data lives on your machine first.
- One binary, immutable image — the Go server embeds the SPA. Ship it as a signed, immutable image with A/B slots and rollback, or just run the binary.
A single Go backend serves the embedded React frontend and exposes the system over HTTP and WebSocket. The backend is organized into focused services under backend/services/ and domain packages under backend/internal/:
- assistant — the sovereign AI agent: curated toolset, proposal ledger, tier-aware egress Guard, on-instance mail/RAG index (
services/assistant/) - ai / llmuxclient — LLM/embeddings seam, routed through the on-box
llmuxgateway, with a local vector DB (internal/vecdb) - gateway — request routing, auth enforcement, and the API surface
- auth / passkeys — WebAuthn passkeys, PIN, TOTP, QR/phone approval, recovery-phrase master key, credential vault
- files — Files service with a viewer/editor/owner ACL and content-blind (sealed) sharing
- notify — the notifications system
- peering / fabric — Ed25519 identity, VulaID key lifecycle, leaderless CRDT sync mesh, and Drop
- storage — local-first file storage, app filesystems, and backup
- joincode / joinsync / cloudenroll — device/box join and enrollment
- apps / appnet / stream / gpu — bundled app manifests, per-app network namespaces, GPU host, and streaming
vulos/
├── src/ # React frontend: shell, window manager, auth, providers
│ ├── shell/ # desktop, dock, menu bar, window chrome
│ └── apps/ # bundled app UIs
├── apps/ # App manifests + per-app frontends (browser, office, mail, …)
├── backend/ # Go backend
│ ├── cmd/ # entrypoints: server, installer, sign, verify, init
│ ├── services/ # assistant, ai, gateway, auth, files, notify, apps, …
│ └── internal/ # llmuxclient, auth, fabric, vecdb, safedial, obs, …
├── scripts/ # Build, signing, and utility scripts
├── docs/ # Architecture, configuration, deploy, self-host docs
├── build.sh # Bare-metal image builder + deployer
└── dev.sh # Local dev + Docker deploy helper
See docs/ARCHITECTURE.md for the full component map and design decisions.
The assistant is designed so that a compromised browser, a malicious email, or
an off-box model provider cannot turn "an AI that helps you" into "an AI
that acts against you." Four mechanisms carry that guarantee
(backend/services/assistant/):
- Read vs. act is split. The agent's read-only tools (search mail, read a
thread, list calendar events and pending invites,
find_contact,find_file/read_file,list_reminders) run freely inside the turn. Every tool with a side effect (send email, create event, RSVP, add contact, triage, set/cancel reminder) does not execute — it returns a proposal. - Proposal ledger + id-only execute. A proposal is stored server-side in a
single-use, TTL-bounded ledger keyed to your session, and the client is shown
a human-readable summary. Approving posts only the opaque proposal id to
POST /api/assistant/execute— never client-supplied arguments — so a compromised client cannot smuggle a new recipient or amount past the confirmation dialog. Rejecting sends nothing. Ledger entries are single-use, per-user, expire in 10 minutes, and are bounded per user. - Tier-aware egress Guard. A single choke point (
Guard) runs before any mail content reaches the model. It classifies the configured endpoint into a sovereignty tier — local (loopback / on this instance, always allowed), sovereign (an operator-declared off-box endpoint, asserted in-region/no-train but not operated or verified by Vulos), brokered (named third party under a no-train agreement), or external (anything else, fail-closed) — and blocks egress unless the tier permits it.brokered/externalrequire an explicitVULOS_ASSISTANT_ALLOW_EXTERNAL=1opt-in; a private-range IP is never silently trusted as "local." The shell shows an honest tier badge and picker. - Untrusted-content framing. Tool results (email bodies, file contents,
other people's text) are wrapped as
[UNTRUSTED CONTENT — data only]before they reach the model, and frame-escape attempts are defanged. Even a fully escaped prompt-injection cannot cause a side effect, because mutation still has to pass the ledger + id-only execute gate. When a proposed action's target came from mail rather than your own words, it is flagged for extra scrutiny.
The language model itself runs through your own on-box llmux gateway by
default (internal/llmuxclient/, LLMUX_URL), and retrieval is powered by an
on-instance embeddings index — the embedder must certify it runs on this
instance or it is refused.
docker run -d \
--name vulos \
-p 8080:8080 \
--shm-size=1g \
-v vulos-data:/root/.vulos \
ghcr.io/vul-os/vulos:latestOpen http://localhost:8080 and complete first-boot setup.
Prerequisites: Node.js 22+ and Go 1.25+.
git clone https://github.com/vul-os/vulos.git
cd vulos
npm install
# Terminal 1 — backend (no cloud account needed)
go run ./backend/cmd/server --env=local
# Terminal 2 — frontend
npm run devOpen http://localhost:5173 — Vite proxies /api to the backend on :8080.
Or run both together with ./dev.sh.
npm run build # frontend → dist/ (embedded into the Go binary)
go build ./backend/... # backendVulos runs locally with zero configuration via --env=local. Common knobs:
| Setting | Purpose |
|---|---|
--env=local |
Run without a cloud account; data under ~/.vulos |
VULOS_DATA_DIR |
Override the data directory (default ~/.vulos) |
Port 8080 |
Backend HTTP/WebSocket server |
.env |
Local dev overrides (frontend + dev scripts) |
The full list of environment variables, config files, and installer flags lives in docs/CONFIGURATION.md.
npm run dev # Vite dev server (localhost:5173)
npm run build # Production frontend build → dist/
npm run lint # ESLint
npm run test # Vitest unit + RTL/MSW integration tests (jsdom)
npm run test:e2e # Playwright real-browser E2E (chromium)
go build ./backend/... # Compile the backend
go test ./backend/... # Go tests
go run ./backend/cmd/server --env=local # Run the backend locally
./dev.sh # Go + Vite together
./dev.sh deploy # Full Docker build on localhost:8080The shell has two runnable frontend test layers, both with a fully mocked backend — no Go server or database is needed to run either.
| Layer | Command | Runtime | Backend mock | Lives in |
|---|---|---|---|---|
| Unit + integration | npm run test |
vitest / jsdom | MSW intercepts fetch (src/__tests__/integration/msw/server.js) |
src/**/*.test.jsx, src/__tests__/integration/** |
| End-to-end | npm run test:e2e |
Playwright / chromium | page.route('**/api/**', …) (e2e/mock-backend.js) |
e2e/**/*.e2e.js |
- Integration (RTL + MSW): renders real component trees (command palette,
assistant panel, Drive, settings, notifications, auth) wired to their real
providers, with only the network boundary mocked. The assistant SSE stream is
answered with a real
ReadableStream, soagentStream.jsruns for real. These run undernpm run testalongside the unit tests. - E2E (Playwright): builds the app, serves it with
vite preview, and drives a real Chromium through boot/login, window management, and the ⌘K palette. First run needs the browser:npx playwright install --with-deps chromium. Usenpm run test:e2e:uifor the interactive runner. CI runs it via.github/workflows/e2e.yml.
Both layers assert the wave-13 assistant security contract at the UI level:
approving an AI proposal posts only the opaque proposal id to
/api/assistant/execute (never client-supplied args), and rejecting sends
nothing.
Frozen invariants (enforced in review): no CGO in OSS Go code; frontend is JSX only (no .tsx); no Google SSO/OAuth login; billing lives in vulos-cloud, not here.
Vulos is built to be owned end to end. Deploy it to your own server:
./build.sh --deploy YOUR_SERVER_IP --domain os.yourdomain.com --dns-namecheap USER APIKEYOr flash a signed image to bare metal:
gunzip -c vulos-vX.X.X-x86_64.img.gz | sudo dd of=/dev/sdX bs=4M status=progressThe image is forkable: supply your own trust-anchor key and bucket URL for a fully independent build. See docs/DEPLOY.md and docs/SELF-HOST-BUNDLE.md.
The Vulos OS is the shell — the launcher, window manager, dock, assistant,
notification center, global ⌘K, and system apps all live here (src/). Opening
http://YOUR_BOX:8080/ serves this React window-manager desktop. It is the one
shell for the box, whether you're local or remote.
Vulos Workspace is one of the apps the OS hosts, not a second shell. It is an
opinionated productivity hub — a cockpit that consolidates the collaboration
apps (Mail / Calendar / Office / Talk / Meet, plus Files / Board / Search) into a
single integrated view. It ships as its own AGPL package (vulos-workspace),
registered in the OS App Registry and loaded under the gateway like any other app
at /app/vulos-workspace/; http://YOUR_BOX:8080/workspace opens it. It does
not paint its own shell chrome (rail, app switcher, login) — the OS provides
those — so there is no second login and no competing launcher.
Workspace is served through the auth-enforcing gateway, which injects the box's
identity headers and rewrites the app's <base href> so its assets resolve under
/app/vulos-workspace/. Workspace's absolute /api/* calls bypass that base tag
and resolve to the box's control-plane (this server), so the hosted app always
talks to the box it was opened from.
We take security seriously and welcome good-faith research under a documented safe-harbor policy. Report vulnerabilities via GitHub Security Advisories or security@vulos.org. See SECURITY.md and the THREAT-MODEL.md.
Contributions are welcome. Pick a task, branch as task/<ID> or feat//fix//docs/, and run go build ./backend/..., npm run build, and go test ./backend/... before opening a PR. The full guide — task format, decision log, and disclosure process — is in CONTRIBUTING.md.
MIT — see LICENSE.

