- Rust 76.2%
- TypeScript 16.4%
- QML 3.5%
- CSS 3.2%
- Shell 0.5%
- Other 0.2%
|
|
||
|---|---|---|
| .cargo | ||
| .forgejo/workflows | ||
| agent | ||
| ci | ||
| docs | ||
| packaging | ||
| server | ||
| shared | ||
| xtask | ||
| .gitignore | ||
| .node-version | ||
| Cargo.lock | ||
| Cargo.toml | ||
| deny.toml | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| rust-toolchain.toml | ||
EvilDesk
EvilDesk is a private, Tailscale-only hub for monitoring and controlling personal Linux devices. It is designed around a lightweight installed agent, an always-on VPS server, direct device-to-device clipboard and file transfer, a responsive iPhone/desktop PWA, and a Noctalia desktop plugin.
The project now has the private hub-connectivity and lightweight Linux telemetry foundations in
addition to the reproducible build baseline. The VPS is enrolled in the private tailnet as
evilvps; the hub exposes its owner surface through Tailscale Serve, accepts agent mTLS only
on the configured Tailscale address, and ships durable enrollment/replay/device-admin and
telemetry-history boundaries. The agent reads Linux interfaces directly, shares one bounded
snapshot cache with the session/CLI/stdio bridge, sends availability-aware compressed deltas,
and records explicit gaps instead of inventing zero values. Transfers, alerts, the remaining
workstation-action UX, and the full PWA acceptance gates remain later phases.
- Agent-side code will live in
agent/. - VPS/server code will live in
server/. - Shared protocols and libraries will live in
shared/. - The React/Vite shell lives in
server/web/; its approved visual direction isdocs/design/ui-direction.md. - Canonical protobuf and OpenAPI sources live in
shared/proto/andserver/openapi/; generated Rust/TypeScript outputs are produced by pinned, realprost-build/vendored-protoc andopenapi-typescripttoolchains withcargo xtask generate, then checked withcargo xtask generate --check. - The hub source build compiles the React production bundle and embeds its assets in the Rust binary. The resulting hub has no Node runtime dependency; source builds require the frozen pnpm dependencies to be installed first.
- SQLite metadata storage uses the immutable migrations in
server/migrations/with WAL, foreign keys, integrity checks, and a preflight that refuses databases carrying a foreign SQLite application ID. - Typed hub, agent, and session startup configuration lives in
evildesk-config. Safe defaults can be overridden by documentedEVILDESK_*deployment variables; validated runtime reloads atomically replace policy values while keeping listener, storage, and identity paths fixed. - The agent telemetry collectors use
/proc,/sys, rtnetlink, UPower/logind when available, and bounded service-manager adapters. Fast probes are isolated by timeout; SMART/NVMe and configured service checks stay on the slow schedule.EVILDESK_AGENT_LOCAL_SOCKET_PATHandEVILDESK_AGENT_MONITORED_SERVICESconfigure the shared local API and service probes. The six-hour local replay buffer is bounded across agent boots, andcargo xtask test perfreports process-specific outbound envelope bytes for the telemetry budget. For a target that is not yet enrolled,evildesk-agent --resource-gateruns only the collector/cache runtime, opens a local Unix socket, reads no identity material, and makes no network connection so its CPU/RSS/process-write/context-switch measurements are clearly separate from the full installed agent budget. - The E3 resource and phase gates are closed under documented owner-approved evidence boundaries: the CachyOS collector runtime and installed VPS agent remain within the measured CPU/RSS limits, while unavailable Gentoo AC/battery samples and prohibited host suspend/reboot observations are retained as explicit follow-up evidence rather than silently treated as passing measurements.
- Telemetry history stores raw availability-aware samples for seven days, five-minute summaries
for 90 days, hourly summaries for one year, and explicit outage gaps.
evildeskctl telemetryand the long-lived typedevildeskctl bridge --stdioconsume the same agent cache asevildesk-session; the bridge prefers the authenticated per-user socket and retains a bounded compatibility fallback to the authenticated agent socket while the session package is installed. - The initial Noctalia plugin bundle lives in
agent/noctalia/evildesk/. It starts only the package-owned typed bridge, keeps cached telemetry/presence in memory, and renders explicit stale/unavailable metric and alert states. Additional device lanes require the authenticated session provider. Its exchange rail keeps target selection and explicit clipboard/ file handoff in the desktop shell while the existing/shelfPWA owns payload gestures and the transfer ledger; payload bytes never enter Noctalia settings or the bridge. Its alert review, local refresh, and risk-labeled workstation controls hand off to exact private PWA action routes; the web app owns the final one-tap, explicit, or typed-device-name confirmation, and Noctalia never executes a workstation action. Optional new-alert toasts use Noctalia's existing service and are disabled by default. A separate opt-in setting can show Noctalia's own notification history/actions without creating another notification server or sending notification bodies through EvilDesk. The existingvps-monitorplugin remains untouched until an owner-visible parity decision in E7. - The shared crypto crate now defines explicit Ed25519 application certificates with owner/device bindings, capability claims, bounded lifetimes, stable certificate and revocation IDs, strict parsing, and zeroized private key material. Production CA keys are loaded explicitly; only isolated test PKI generation is available as an opt-in feature.
- SQLite identity migrations now persist owner-linked devices, capabilities, certificates, one-use expiring enrollment records, and append-only revocations. New repository operations enforce owner binding and preserve certificate/revocation history on deletion attempts.
- The agent protobuf envelope is size-bounded and BLAKE3-sealed before transport. Shared validation enforces canonical UUIDv7/message shape, negotiated major compatibility, contiguous device sequences, stable error codes, and forward-compatible optional Hello fields.
- The hub browser boundary accepts identity only from the trusted Tailscale Serve adapter,
normalizes and allowlists owner logins, and exchanges them for expiring opaque
__Host-sessions. Mutations require the session CSRF token and exact same-origin check; session cookies are Secure, HttpOnly, and SameSite=Strict. - Audit events are stored in an append-only SQLite table with database-enforced mutation and deletion rejection. Event metadata uses a closed, bounded, redaction-safe type set rather than arbitrary model serialization.
- The hub now binds owner HTTP/WebSocket traffic to loopback and agent gRPC traffic to an explicit Tailscale address. The stream uses transport mTLS plus EvilDesk application certificates, bounded heartbeats, reconnect jitter, durable acknowledgments, duplicate digest handling, and conflict rejection.
- Enrollment is one-use and expiring:
evildeskctl enrollpersists a root-only device key, displays a short-lived pairing code, and the authenticated owner API can approve, cancel, issue the device certificate, revoke, or explicitly purge device history. - Owner device administration and the bounded event stream are available under
/api/v1; sessions require Tailscale Serve identity, secure cookies, CSRF, and exact origin checks. - The responsive owner shell now uses a session-first query/event client, a persistent multi-device presence strip, five-destination mobile navigation, safe loading/offline/error states, explicit clipboard/file entry, and an owner-scoped Shelf ledger. Shared or targeted transfer metadata, progress, expiry, preview guidance, copy/download, retry/cancel/delete, and VPS quota are visible without placing payload bytes in the metadata request.
- Alerts, Activity, and Settings now have decision-led responsive surfaces: alert acknowledge/ snooze/reopen lifecycle, metadata-only activity filters, and preview controls for device cadence, notification privacy, retention, updates, backups, and the fixed dark appearance. Fixture changes are labeled as tab-local until their durable backend phases land.
- Device routes now expose the owner-scoped availability-aware telemetry snapshot and bounded 5-minute/hourly history APIs. Detail views acquire a short live-sampling lease, label stale or missing readings instead of inventing values, keep display-powered and active-use totals separate, lazy-load the uPlot history view, retain an accessible table fallback, and export only nonsensitive metric history as CSV.
- Device detail views also expose bounded, read-only service and process inspections. Refreshing asks the authenticated agent for a capped snapshot; filters, stable PID/start-time identity, virtualized rows, and permission/unavailable states stay visible without background process polling. Action controls remain inert until the later confirmed-action phase.
- The owner web surface is installable as a dark standalone PWA with maskable/Apple touch icons, safe-area and dynamic-viewport styling, and an update prompt. Its service worker caches only the static shell; authenticated API, event, upload, and payload responses are explicitly denied from Cache Storage.
- iPhone interaction keeps clipboard reads and file-picker launches behind an explicit tap, recomposes the exchange sheet above the home indicator and virtual keyboard, preserves touch chart/table fallback and interrupted-network states, and keeps downloads and clipboard writes owner-initiated. The Shelf keeps countdown, quota, staging, and unavailable-payload guidance visible; live endpoint and device acceptance remain explicit later gates.
- Push setup is explicit in Settings: fixture mode never requests permission, real subscriptions are deduplicated and rotated only when hub configuration exists, and the service worker builds bounded health-summary notifications with alert-focus and badge-clear behavior. Payload bodies and subscription tokens never enter local UI metadata or the shell cache.
- E8.1 adds an owner-scoped notification privacy policy: device relay and each discovered app are independently opt-in, new apps default disabled, title/body/icon/urgency/actions are separate choices, and body redaction keeps only app/device/timestamp metadata. Policy rows store no notification content; the API exposes the 24-hour body and 30-day metadata retention contract.
- E8.2 adds the bounded relay record pipeline: authenticated notification source sessions, capability-negotiated source/target transport, stable source-tuple and delivery-ID dedupe, offline target queues, receipt and dismissal/action return states, and separate 24-hour body / 30-day metadata cleanup. Browser events, push paths, and operational logs contain metadata only; action arguments never leave the source session. A missing local Linux notification adapter is reported as unavailable rather than treated as a successful action, while Noctalia continues to use its existing NotificationService/IPC surface.
- E8.3 defines the typed workstation-action contract: the closed lock/suspend/reboot/shutdown, service/process, refresh, and ring operation matrix; typed service names and PID/start-time identities; five-minute maximum expiry windows; owner-enrolled-device targeting; one-tap, explicit, and typed-target confirmation tiers; bounded result codes; and capability names advertised through the authenticated agent Hello. The wire contract carries no shell command, environment, path, or typed device-name proof. E8.4–E8.5 provide the privileged and durable execution boundaries; E8.6 adds the reachability-aware Wake-on-LAN path, while confirmation UX and the remaining phase gates stay in E8.7–E8.8.
- E8.4 now provides the narrow privileged-helper boundary: root-only startup, package-owned
root socket or explicit root stdio, peer UID/GID checks, bounded NDJSON frames and connections,
explicit systemd/OpenRC selection, a package service allowlist, PID/start-time verification,
absolute
loginctl/systemctl/rc-service/killplans, cleared child environments, null standard streams, and ten-second kill-on-timeout execution. Lock/ring/refresh remain outside the root helper. - E8.5 now connects the authenticated action stream to a durable agent ledger and hub-side action repository. UUIDv7 action IDs are idempotent, ledger writes precede side effects, and replayed or restarted actions do not run twice. Lock, ring, telemetry refresh, privileged suspend/reboot/shutdown, allowlisted service restart, and PID/start-time-bound stop/kill paths report bounded result states, enforce cancellation through the helper boundary, and publish metadata-only browser events plus redaction-safe audit transitions. Offline authorized actions remain queued until the target reconnects; expired and canceled actions are terminal without exposing command lines, environments, or sensitive arguments.
- E8.6 adds bounded Wake-on-LAN for an offline enrolled target: the hub selects an authenticated online peer advertising the WOL capability, persists the executor separately from the target, and reports a peer acknowledgment without claiming the target woke until it reconnects. Agents validate lowercase MAC addresses and live broadcast interfaces, send only the fixed magic packet, rate-limit packets locally, and fail closed when no suitable LAN peer or interface is available.
- The E9 remote-session control plane now has expiring, owner-scoped session grants, target-bound
agent offers, a bounded transient browser signaling WebSocket, explicit control leases, replay-
checked pointer/scroll/touch/key metadata, and a per-user Unix session bridge. Linux portal
consent and monitor discovery stay local to the session process; portal tokens, PipeWire node
IDs, and media frames never enter hub storage, replay, browser events, or React state. The web
route uses the Frappé monitor-deck brief, feature-detects WebRTC, attaches live media only to a
video element, and keeps a sanitized fixture preview for manual review. The target now owns a
bounded Rust WebRTC SDP/ICE peer with an H.264 track, a PipeWire/GStreamer sample worker, and a
lease-bound input DataChannel; the browser signaling channel retries transient WebSocket loss with
bounded exponential backoff and exposes a reconnecting state. The browser now samples bounded
inbound video statistics and can request bounded H.264 bitrate, frame-rate, and resolution
profiles; the target reconfigures its PipeWire/GStreamer capture without allowing codec changes
or unbounded values. Live portal consent/revocation, ydotool execution, real iPhone Safari,
packet-loss behavior, and phase-E9 hardware evidence remain explicitly open. The target-side
control boundary now
has a fixed absolute ydotool plan, allowlisted Linux keycodes, replay/rate/lease checks, expiry
reaping, and stop-on-rejection behavior. Control leases renew only through the same
authenticated lease identity; the browser renews at a bounded cadence and releases control on
window blur or hidden-page transitions. The local session exposes bounded indicator identity and
monitor state, emits a viewing transition when a control lease expires, releases the helper before
safety-sensitive lock/suspend/reboot/shutdown actions, and synchronizes Stop across hub, agent,
and target state. The lazy remote route promotes a connected WebRTC peer to viewing, surfaces
adaptive quality, and requests the selected monitor orientation in fullscreen; fixture mode stays
a neutral no-frame preview. Persistent desktop rendering, ydotool/uinput execution, and live
safety evidence remain unclaimed until exercised on the enrolled device. The owner-run E9
acceptance matrix is documented in
docs/operations/remote-view.md; the session watchdog also fails closed when a selected monitor disappears. The remaining live closeout rows are consolidated indocs/operations/final-acceptance.md. - Alert policy E5.1 now has fixed, validated rule IDs for offline, gaps, resource pressure,
thermal, battery, and service failures; owner-scoped SQLite persistence uses optimistic
revisions, per-device overrides, and an authenticated
/api/v1/alert-ruleseditor. E5.2 now evaluates normalized telemetry on ingest with minimum duration, hysteresis, cooldown, stable incident identities, late-sample protection, and durable metric/threshold/action evidence. E5.3 rebuilds fourteen-day five-minute median/MAD baselines by UTC hour-of-week and persisted AC/battery mode, requires 70% readiness coverage, and returns explainable robust scores. E5.4 adds durable acknowledge, snooze, maintenance, resolve, and reopen transitions with owner-scoped audit targets, expiry-aware badge counts, an authenticated alert API, and browser event invalidation across open surfaces. E5.5 adds one deduplicated delivery ID across durable Web Push, browser-event, native-Linux, and Noctalia-ready adapters, bounded retry/cooldown accounting, owner-scoped subscription rotation/revocation, and redaction-safe health summaries. Web Push sending is enabled only when both VAPID hub variables are configured; native-Linux delivery remains an explicit bridge boundary until that adapter is installed. - Transfer E6.1 now has a durable owner-scoped metadata contract for files and explicit clipboard items, shared or targeted visibility, compare-and-swap state versions, resumable attempt/chunk records, 24-hour expiry, and transactional 5 GB per-item/20 GB staged quota reservations.
- Payload crypto E6.2 now gives each object a zeroized random key, authenticated canonical metadata, XChaCha20-Poly1305 chunk framing, a wrapped-key envelope, atomic partial-file handling, and corruption-safe deletion. SQLite still stores metadata only; native transfer E6.3 now adds signed expiring tickets, Tailscale-only endpoint hints, bounded QUIC frames, streaming sinks, cancellation, and throughput accounting. Browser transport E6.4 now adds a bounded WebRTC boundary with explicit fallback; durable staging and cleanup remain E6.5-E6.9 surfaces.
- Browser transfer E6.4 now has a five-minute owner-scoped grant, bounded SDP/ICE WebSocket
signaling, TURN-less tailnet assumptions, WebRTC data-channel high/low-water backpressure,
verified-chunk resume control, Safari feature detection, and an explicit
stage_on_hubfallback. Signaling never carries payload bytes. - VPS staging E6.5 now accepts only owner-authenticated opaque ciphertext ranges with a 4 MiB body ceiling, atomic resume manifests, bounded range downloads, an explicit offline queue, 512 MiB free-disk reserve, 5 GiB per-object/20 GiB metadata reservations, expiry cleanup, crash recovery, private filesystem permissions, and no public payload URLs. SQLite retains transfer metadata and quota state only; staged files remain outside the web cache.
- Clipboard E6.6 now has explicit MIME-aware Linux session reads/writes for text, sanitized HTML, and bounded PNG data, shared source/target/digest/preview metadata, a recent-content loop guard, and browser clipboard helpers that require a foreground gesture for every read or write. No clipboard watcher or automatic synchronization path exists; iOS/WebKit background access is rejected.
- Transfer E6.7 now persists verified chunk maps and retry attempts, computes bounded ordered BLAKE3 final digests, makes completion idempotent, rejects conflicting resumed proofs, and lets transfer expiry win over late chunk, retry, and completion callbacks.
- Transfer E6.9 now expires active metadata before quota decisions, purges expired and completed rows with cascading attempt/chunk/clipboard cleanup, removes finalized and partial staging artifacts, redacts clipboard payloads and previews from debug output, and keeps key artifacts outside the metadata/cache cleanup boundary. Encrypted metadata backups remain the later E10 backup subsystem and are not claimed by this phase.
- Transfer E6.10 now streams caller-owned byte slices without a payload-sized convenience copy, bounds competing native transfers with a per-device limiter, exposes wire-byte throughput stats, and cancels blocked reads promptly. Live direct/relay throughput and installed-agent resource samples remain external E6.11 acceptance evidence.
- The fast web check includes axe accessibility checks, keyboard-focus smoke coverage,
reduced-motion token assertions, per-lane render-count checks, coalesced event invalidation
under flood, and route-split production bundles for the device/history/shared-surface paths.
cargo xtask test webadds the production-preview Playwright gate for desktop, iPhone portrait and landscape Chromium viewports, PWA metadata, sanitized remote-view boundaries, and a real browser axe scan. Its remote-browser prerequisite and Safari acceptance boundary are documented indocs/operations/web-quality.md. cargo xtask packagebuilds deterministic Debian, Arch, and Gentoo artifacts with the package-owned agent/session/helper/CLI binaries, hardened systemd/OpenRC definitions, standard paths, Noctalia assets, polkit/sysusers/tmpfiles metadata, uninstall retention hooks, and a multi-artifact manifest.cargo xtask verify-packageinspects any generated artifact without installing it into the development host; package operations are documented indocs/operations/packaging.md.- The repository-side E10.12 gates (
generate --check, integration, security, full check, and fresh package verification) pass on the selected remote Gentoo builder. No package was installed and no service or host was restarted; live endpoint, Safari, lifecycle, and disaster acceptance remain tracked indocs/operations/final-acceptance.md. - The E10.8 hardening matrix documents the split agent/helper runtime parents, root-owned secret
modes, systemd capability/syscall/path limits, OpenRC no-new-privileges/IAB/rlimit/health
controls, and the hardening controls that OpenRC does not provide:
docs/operations/hardening.md. cargo xtask repository keygen|publish|verify|rollbackprovides the signed package-repository boundary: explicit stable/candidate/nightly channels, monotonic signed indexes, immutable package/history paths, atomic current-pointer publication, and explicit historical rollback. There is no unsignedlatestURL, and the private repository key stays on the release workstation.- The shared update boundary now defines deterministic release manifests with BLAKE3 artifact digests, Ed25519 signatures, stable key IDs, key-ring rotation, and protocol/platform constraints. Update clients must verify the signed manifest and exact artifact bytes before any inactive-slot write; no daemon generates or stores an offline signing key.
- The update slot boundary stages only into the inactive package-owned slot, uses an exclusive lock and atomic fsync/rename markers, blocks active transfer/remote/shutdown/package work, requires local and hub health proof before promotion, and can recover or roll back to the previous ready slot after a torn update.
evildeskctl update status|stage|promote|rollback|recoverexposes the bounded local operator boundary; staging requires explicit release public keys, a signed manifest, exact artifact path, platform, and protocol inputs. The agent repairs an existing slot marker before opening its hub stream and leaves an unrecoverable state for operator diagnosis.- The hub owns an atomic maintenance/update-state file and exposes a bounded
/api/v1/healthreadiness summary with database-integrity, maintenance, and staged-update status. A supervisor can take the hub out of ready state without changing enrollment, audit, or payload metadata. - evildeskctl doctor provides bounded human-readable or --json read-only diagnostics for validated configuration, filesystem reserve, SQLite integrity, device certificate authority/ lifetime/key matching, private-key permissions, update-slot consistency, and the local telemetry socket. First-run absence is reported as a warning; corrupt or unsafe state includes a corrective action and exits nonzero. The operator runbook is docs/operations/doctor.md.
- E10.4 adds an encrypted metadata-backup boundary: SQLite snapshots are sanitized with
notification/clipboard/replay/browser-event/pending-enrollment and staged-payload exclusions,
authenticated with XChaCha20-Poly1305 and BLAKE3 checksums, retained by the 7-daily/4-weekly/
12-month policy, and staged as opaque targeted transfers for an enrolled destination. The
explicit
evildeskctl recovery export --output PATHcommand generates a root-readable recovery key file without printing the key; the hub never handles that key during delivery. - E10.5 adds
evildesk-recovery restore: it requires a mode-0600 owner-held recovery key, refuses to overwrite an existing database, decrypts and validates the snapshot, runs current SQLite migrations in an isolated path, reports restored device reconnect eligibility, and keeps revoked devices non-reconnectable. - E10.10 records the deterministic upgrade compatibility matrix: protocol minor negotiation in
both directions, interrupted migration resume, revoked-release rejection, strict configuration
downgrade handling, and the backup-required boundary for rollback after a schema change. See
docs/operations/updating.md; the live replacement/disaster drill remains E10.11. - The complete implementation plan is
docs/plan.md. - The accepted security baseline is
docs/threat-model.md, with decisions recorded indocs/adr/. - Development history and deliberate deviations are recorded in
docs/LOOPS.mdanddocs/DEVIATIONS.md.
The running product will consist of signed, package-owned programs with systemd services on CachyOS/Debian and OpenRC services on Gentoo. It will not rely on loose monitoring scripts, cron jobs, or independently managed SSH tunnels.
Development uses the pinned Rust and Node toolchains. Install web dependencies once, then run the repository gate:
corepack pnpm install --frozen-lockfile
cargo xtask check
Forgejo runs the same gate with isolated Cargo/pnpm caches, cargo xtask test security, a
CycloneDX 1.6 Cargo/npm SBOM at target/sbom/evildesk.cdx.json, and an explicitly unsigned
test package. The CI checks are repository tooling only; no package is signed or published
by this foundation stage.
The workspace decision and dependency policy are recorded in docs/adr/0001-rust-workspace.md.