Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 26 additions & 19 deletions AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,32 @@ The Tauri v2 + React/TypeScript rewrite of the Omnideck desktop app, replacing t

**Current status: sequencing steps 2–4 done** (Tauri mechanics, read-only dashboard, and lifecycle actions — see `desktop_tauri_rewrite.md`'s Sequencing section). `src-tauri/` and `src/` build clean (`cargo build`, `cargo test`, `cargo clippy -- -D warnings`, `npm run build` all pass). `cli_bridge.rs` + `commands.rs` cover `list`/`status`/`logs`/`start`/`stop`/`restart`/`add`/`remove` against the real CLI JSON/NDJSON contract, with `cargo test` fixtures pinning the JSON shapes. The frontend has an app shell (Dashboard/Settings nav) on the ported SIGNAL tokens: Dashboard polls `list --json` with per-row Start/Stop/Restart/Logs/Remove, a New Deck form streaming `add --json` progress, a Remove confirmation dialog with the CLI's required explicit keep/delete + backup choices, and a blocking screen for CLI-missing/contract-mismatch. Every backend command was verified against the real CLI directly (not just typechecked) before being trusted. `update_instance`, instance detail drill-in (DESIGN.md #6), and Open UI instance webview tabs (DESIGN.md #7) are done — the "still to do" list below was stale about these. **Hardening-migration Phases 1–4 are done** (see `reference/desktop-hardening-migration-PLAN.md`): sidecar pinned+checksummed against real CLI `v0.10.0` (`vendor-manifest.json`, `fetch:sidecars`/`verify:sidecars`), `EXPECTED_JSON_CONTRACT` corrected to `2` with a `MINIMUM_CLI_VERSION` floor check, the dashboard capability replaced with an enumerated `dashboard-bridge` allowlist, `cli_bridge.rs` now bounds stdout/stderr and enforces per-operation timeouts via a unified `run_cli` helper with correct NDJSON line reassembly across chunk boundaries, `tauri-plugin-single-instance` is wired up, and the AppImage runtime fixes were confirmed still intact and still build clean. **Hardening-migration Phase 5 is also done**: `bootstrap.rs` drives the shared Podman runtime's
readiness via `cli_bridge::runtime_status`/`runtime_ensure` (correcting this doc's earlier claim that the
CLI had no equivalent — it does, as of `v0.10.0`) and owns the 4-command IPC surface
(`bootstrap`/`begin_setup`/`open_dashboard`/`run_action`) for the isolated `"onboarding"` window, created
hidden via `WebviewWindowBuilder` in `lib.rs`'s `setup()` hook and scoped to its own `onboarding-bridge`
capability (never `"main"`, which is the dashboard here — see the module's own doc comment and
`reference/desktop-hardening-migration-PLAN.md`'s "Decisions from review" for why the sibling's window
labels don't map 1:1). The onboarding UI itself (`public/onboarding/{index.html,setup.css,setup.js,
host-adapter.js}`) is vanilla JS/CSS, ported from the sibling's `web/` and adapted to this repo's two real
bootstrap phases (`software`/`environment`, matching the CLI's own `runtime ensure` stages exactly — no
`download`/`startup` phase here, since pulling an image and creating a Deck is the dashboard's separate,
already-built `add_instance` flow). No resume-record file — `bootstrap.rs`'s doc comment explains why one
isn't needed here. Test coverage ported alongside: `tests/policy.test.mjs` (security-posture assertions,
`node --test`, wired into `npm run test:policy` and the `verify` composite), `tests/manual/*.md`
(clean-first-run and recovery-lifecycle procedures), and `tests/hardware/validate-proof.mjs` (packaged-build
smoke check, `OMNIDECK_DESKTOP_SMOKE_FILE`-gated in `lib.rs`). Verified end-to-end this session: `npm run
verify` clean (fetch/verify sidecars, policy tests, typecheck, fmt, Rust tests, clippy), a real `npm run
dev:app` launch with both windows live and no errors, a real `npm run build:appimage` + `run:appimage`
launch with no crashes/coredumps, and a caught-and-fixed regression (`bootstrap()` was unconditionally
showing the onboarding window on every launch before the fix — `tests/policy.test.mjs` now guards this
shape). Still to do: onboarding visual polish/copy review, migration (legacy Electron data → CLI-managed
CLI had no equivalent — it does, as of `v0.10.0`) and owns the 3-command IPC surface
(`bootstrap`/`begin_setup`/`run_action`), folded into the dashboard's own `dashboard-bridge` capability
and called from the single `"main"` window. **This wasn't the original design** — onboarding first
shipped as a second, isolated `"onboarding"` window (own capability, hidden via `WebviewWindowBuilder`,
vanilla JS/CSS UI ported from the sibling's `web/`), on the theory that window-scoped capabilities are a
real security boundary worth having. That held up until real hardware testing found it caused a genuine
bug: creating two GTK/WebKit windows at startup (one hidden) failed EGL/GPU-driver init
(`EGL_BAD_PARAMETER`, blank white dashboard) on a real Intel Iris Xe/Mesa 26.1.4 combination, reproduced
independently in the sibling app's own build too — i.e. not something specific to this repo's port. Fixed
by removing the second window entirely: onboarding is now just another React screen
(`src/components/OnboardingView.tsx`, `src/hooks/useBootstrap.ts`) that `App.tsx` swaps in for the
dashboard until the shared runtime is ready, using the exact same `SetupState` push model
(`tauri::ipc::Channel`) as before. **Real, knowingly-accepted tradeoff**: the bootstrap commands are no
longer isolated behind a separate capability grant the way a second window enforced — see `bootstrap.rs`'s
module doc comment and `reference/desktop-hardening-migration-PLAN.md`'s "Decisions from review" for the
full history of both the original design and the reversal. Two real bootstrap phases
(`software`/`environment`, matching the CLI's own `runtime ensure` stages exactly — no `download`/`startup`
phase here, since pulling an image and creating a Deck is the dashboard's separate, already-built
`add_instance` flow). No resume-record file — `bootstrap.rs`'s doc comment explains why one isn't needed
here. Test coverage: `tests/policy.test.mjs` (security-posture assertions, `node --test`, wired into
`npm run test:policy` and the `verify` composite), `tests/manual/*.md` (clean-first-run and
recovery-lifecycle procedures), and `tests/hardware/validate-proof.mjs` (packaged-build smoke check,
`OMNIDECK_DESKTOP_SMOKE_FILE`-gated in `lib.rs`). Verified end-to-end: `npm run verify` clean (fetch/verify
sidecars, policy tests, typecheck, fmt, Rust tests, clippy), a real `npm run dev:app` launch and a real
`npm run build:appimage` + `run:appimage` launch with no crashes/coredumps/EGL errors, single window
confirmed. Still to do: onboarding visual polish/copy review, migration (legacy Electron data → CLI-managed
instance — untouched by this session, still the highest-risk remaining path per this doc's rules above),
and Phase 6/7 of the hardening plan (CI, release engineering — explicitly deferred in that doc until this
repo actually gets CI / cuts a first release, not preemptive work). Update this file as decisions firm up;
Expand Down
8 changes: 4 additions & 4 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Detailed setup, testing, and command reference. See [`README.md`](./README.md) f
npm run verify:sidecars # re-checksums without re-downloading (cheap, e.g. in CI)
```
`src-tauri/binaries/vendor-manifest.json` records the pinned tag/commit/checksums (committed); the fetched binaries themselves are gitignored. `OMNIDECK_CLI_ARCHIVE_DIR=/path/to/archives` points `fetch:sidecars` at pre-downloaded release archives for an offline/sandboxed build — the pinned hashes are still enforced either way.
- **Podman**, installed — needed for the dashboard to show real Deck data and for the onboarding flow's "already ready" path to actually be ready. If Podman genuinely isn't ready, the app's own onboarding window is what sets it up; you don't need to pre-provision it by hand to develop against this repo, just to see the dashboard's populated state instead of an empty list.
- **Podman**, installed — needed for the dashboard to show real Deck data and for the onboarding flow's "already ready" path to actually be ready. If Podman genuinely isn't ready, the app's own onboarding screen is what sets it up; you don't need to pre-provision it by hand to develop against this repo, just to see the dashboard's populated state instead of an empty list.

### Linux on an immutable/atomic distro (Fedora Silverblue, Bluefin, etc.)

Expand All @@ -39,7 +39,7 @@ npm run dev # frontend only, no Rust/webview (fast iteration on Re

`npm run dev:app` is the correct entrypoint on every platform — it's a thin wrapper (`scripts/dev.sh`) that only does the toolbox dance on Linux; macOS/Windows/non-atomic-Linux just get `npm run tauri dev` directly. Don't run `npm run tauri dev` by hand on an atomic-Linux dev box.

The dashboard window opens immediately showing a real "Checking your setup…" state, then either the populated Dashboard or a blocking screen if the CLI sidecar is missing/version-mismatched — see `AGENT.md`'s "instant open" rule if you're touching startup code. If the shared Podman runtime isn't ready yet, the onboarding window appears instead; see below for how to preview its screens without needing an actually-unprovisioned machine.
The app window opens immediately showing a real "Checking your setup…" state, then either the populated Dashboard or a blocking screen if the CLI sidecar is missing/version-mismatched — see `AGENT.md`'s "instant open" rule if you're touching startup code. If the shared Podman runtime isn't ready yet, the onboarding screen (`src/components/OnboardingView.tsx`) renders in place of the dashboard instead; see below for how to preview its screens without needing an actually-unprovisioned machine.

### Testing the onboarding flow

Expand All @@ -52,7 +52,7 @@ OMNIDECK_DEBUG_ONBOARDING_STAGE=ready npm run dev:app
OMNIDECK_DEBUG_ONBOARDING_STAGE=error npm run dev:app
```

Each value forces the onboarding window to open showing that exact screen — real render, real buttons, no real Podman calls made for the check itself. `welcome`'s "Set up Omnideck" button still calls the real `begin_setup`, though: since your Podman is presumably actually ready, that resolves to the real `ready` state almost instantly, which is a nice free integration check of the real `runtime ensure` idempotent-no-op path.
Each value forces `OnboardingView` to render showing that exact screen — real render, real buttons, no real Podman calls made for the check itself. `welcome`'s "Set up Omnideck" button still calls the real `begin_setup`, though: since your Podman is presumably actually ready, that resolves to the real `ready` state almost instantly, which is a nice free integration check of the real `runtime ensure` idempotent-no-op path.

This is `debug_forced_state()` in `src-tauri/src/bootstrap.rs`, `#[cfg(debug_assertions)]`-gated — the function and the env var read don't exist at all in a release build (`cargo build --release`/`tauri build`), so there's no flag to accidentally ship enabled.

Expand All @@ -69,7 +69,7 @@ If you need to test the *real* first-run install path (not just the screens), th
cargo clippy -- -D warnings
cargo fmt
```
- **Policy tests** (`npm run test:policy`, `tests/policy.test.mjs`): security-posture assertions on the capability files, the onboarding window's authorization checks, the CLI sidecar pin, and the process hardening (output bounds, timeouts) — so a future PR can't silently widen the attack surface without a test failing. Uses Node's built-in test runner, no extra dependency.
- **Policy tests** (`npm run test:policy`, `tests/policy.test.mjs`): security-posture assertions on the capability files, the bootstrap commands' `window.label() == "main"` authorization checks, the CLI sidecar pin, and the process hardening (output bounds, timeouts) — so a future PR can't silently widen the attack surface without a test failing. Uses Node's built-in test runner, no extra dependency.
- **Manual/integration verification against the real CLI** — the most reliable way to confirm a `cli_bridge.rs` change actually matches what the CLI emits, since fixture tests only catch drift from *known* shapes:
1. Run the CLI command by hand with the exact same args your Rust code constructs, e.g. `src-tauri/binaries/omnideck-<your-triple> add --name test --port 46177 --json`, and diff the output against what your Rust structs expect.
2. For anything destructive (`remove`, or actions against instances you care about), test against a disposable instance you create and remove yourself, or a low-stakes existing one — never a production Deck. `omnideck list --json` shows what's currently installed before you touch anything.
Expand Down
37 changes: 18 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,25 @@ A Tauri v2 + React/TypeScript desktop app that manages one or more local Omnidec
## Architecture

```
┌──────────────────────┐ ┌──────────────────────────┐
│ "main" (dashboard) │ │ "onboarding" (hidden │
│ │ │ until actually needed) │
│ React + TypeScript │ │ vanilla JS/HTML/CSS │
│ Deck list, start/ │ │ first-run/repair Podman │
│ stop/update/remove │ │ runtime setup │
└───────────┬───────────┘ └────────────┬──────────────┘
│ │
└───────────────┬─────────────────┘
src-tauri/src/cli_bridge.rs
(owns all `omnideck` CLI subprocess I/O —
spawn, JSON/NDJSON parsing, bounds, timeouts)
bundled `omnideck` CLI sidecar
(pinned by version + checksum, never PATH)
single "main" window
┌───────────────────────────────────────────────────────────┐
│ React + TypeScript │
│ OnboardingView Dashboard view │
│ first-run/repair ⇄ Deck list, start/ │
│ Podman runtime setup stop/update/remove │
│ (shown until ready) (shown once ready) │
└──────────────────────────────┬──────────────────────────────┘
src-tauri/src/cli_bridge.rs
(owns all `omnideck` CLI subprocess I/O —
spawn, JSON/NDJSON parsing, bounds, timeouts)
bundled `omnideck` CLI sidecar
(pinned by version + checksum, never PATH)
```

Two windows, two frontend stacks, deliberately — the dashboard is React with a broad command surface; onboarding is an isolated, minimally-privileged vanilla-JS window with its own Tauri capability, unreachable from the dashboard. See [`src-tauri/src/bootstrap.rs`](./src-tauri/src/bootstrap.rs)'s module doc comment for the full rationale, and [`reference/desktop-hardening-migration-PLAN.md`](./reference/desktop-hardening-migration-PLAN.md) for how this repo's security posture got here.
One window, one React app — onboarding and the dashboard are two screens `App.tsx` swaps between client-side based on whether the shared Podman runtime is ready, not two OS-level windows. That wasn't the original design (onboarding first shipped as a second, hidden, minimally-privileged window with its own Tauri capability); it was reverted after real hardware testing found that creating two GTK/WebKit windows at startup broke EGL/GPU-driver init on at least one real Intel/Mesa combination. See [`src-tauri/src/bootstrap.rs`](./src-tauri/src/bootstrap.rs)'s module doc comment for the full rationale — including the security-isolation tradeoff that reversal knowingly accepts — and [`reference/desktop-hardening-migration-PLAN.md`](./reference/desktop-hardening-migration-PLAN.md) for how this repo's security posture got here.

## Commands

Expand Down Expand Up @@ -65,7 +64,7 @@ npm run fetch:sidecars
npm run dev:app
```

The dashboard opens immediately. If you don't have a ready Podman runtime yet, an onboarding window will guide you through setting one up.
The app window opens immediately. If you don't have a ready Podman runtime yet, an onboarding screen guides you through setting one up before handing off to the dashboard.

## Documentation

Expand Down
21 changes: 10 additions & 11 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,16 @@ release goes out to actual users — see `RELEASING.md`.
Source tests (`tests/policy.test.mjs`) keep the following invariants
release-blocking:

- the dashboard window's capability is an enumerated allowlist
(`dashboard-bridge`), never `core:default`;
- the onboarding window's capability (`onboarding-bridge`) is scoped to
`"windows": ["onboarding"]` only — never `"main"`, which is the dashboard
here (this exact mistake was caught once already; see the policy test's
own comment);
- every onboarding command re-checks `window.label() == "onboarding"`
server-side, not just the capability grant;
- `bootstrap()` only reveals the onboarding window when setup is actually
needed — never unconditionally (this was a real, caught-and-fixed
regression);
- the dashboard's (single `"main"` window's) capability is an enumerated
allowlist (`dashboard-bridge`), never `core:default`;
- every bootstrap command (`bootstrap`/`begin_setup`/`run_action`)
re-checks `window.label() == "main"` server-side, not just the
capability grant;
- `bootstrap.rs` never manages window visibility — asserted by absence, so
a future onboarding tweak can't quietly reintroduce the second window
that caused a real EGL/GPU-driver startup bug on some hardware (see
`AGENT.md` and `bootstrap.rs`'s doc comment for the full story; onboarding
is now a plain React screen `App.tsx` swaps in, not a separate window);
- the CLI sidecar is pinned by checksum for all 6 target triples, and the
runtime version check is a floor (`>= v0.10.0`), not an exact match;
- sidecar process output is bounded and every operation has a timeout.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "omnideck-desktop",
"private": true,
"version": "0.5.0-alpha.2",
"version": "0.5.0-alpha.3",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
58 changes: 0 additions & 58 deletions public/onboarding/host-adapter.js

This file was deleted.

Loading
Loading