scaffold: project structure, wire contract, roadmap and agent briefs

Lays out Velox Download Manager (IDM-class download manager for Ubuntu
26.04) as a monorepo ready for parallel lane development. No implementation
code by design.

- docs/: architecture, roadmap M0-M7, IDM-parity GUI spec, engine design,
  Firefox extension spec, risks/spikes, packaging
- contracts/: wire-contract skeleton (JSON Schema + fixture templates) —
  the single synchronization point between lanes
- docs/agents/: one brief per lane (PROTO, CORE, DAEMON, GUI, EXT, PKG/QA)
  with owned directories, build order and definition of done
- CLAUDE.md: rules of engagement — lane ownership, layering, non-negotiables
- CMake scaffolding with dev/tsan/release/ci presets

Two environment findings shape the design: Firefox here is the Mozilla snap
(native-messaging risk, so the extension carries a loopback-WebSocket
fallback), and Wayland forbids passive clipboard monitoring (so clipboard
capture is explicit-action-first).

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-09 18:21:11 +04:00
co-authored by Claude Opus 5
commit 8bb683b09d
90 changed files with 1814 additions and 0 deletions
+100
View File
@@ -0,0 +1,100 @@
# 01 — Architecture
## 1. Decision summary
| Concern | Decision | Why this and not the alternative |
|---|---|---|
| Engine language | **C++23** (`libveloxcore`) | You asked for speed and it is the right call here: segment scheduling, a 64 KiB8 MiB write path, and 32 concurrent sockets are exactly where a GC and a per-chunk allocation tax show up. Rust would be an equally good engine choice, but Qt's C++ API means C++ removes an entire FFI boundary from the GUI lane. |
| HTTP stack | **libcurl** (multi interface, `curl_multi_poll`) | Writing an HTTP/1.1+2 client on raw epoll costs weeks and buys nothing: curl already does connection reuse, HTTP/2 multiplexing, TLS, proxies, SOCKS5, Digest/NTLM auth, redirect and cookie semantics. One `curl_multi` handle per worker thread, N easy handles = N segments. |
| GUI toolkit | **Qt 6 Widgets** (not QML, not GTK4) | IDM's UI is a dense data grid, a toolbar of big icons, a tree, and ~12 tabbed dialogs. `QTreeView` + a custom `QAbstractItemModel` renders 100k rows without breaking a sweat; QML would need that grid hand-built. GTK4/libadwaita fights you the moment you want a non-GNOME look, and gtkmm's tree/column story is worse. Qt also gives you `QSystemTrayIcon`, drag-and-drop, a global clipboard API, and `QSS` theming to get the IDM look. LGPLv3 dynamic linking is fine for an open-source app. |
| Process model | **Daemon + thin clients** | Downloads must survive closing the window, and the extension must work when no window is open. One owner of state also kills every "GUI and extension disagree" bug class. |
| Wire protocol | **JSON-RPC 2.0** over Unix domain socket (+ loopback WebSocket for the extension fallback) | One protocol for GUI, CLI, and extension = one contract to keep in sync, and it is trivially mockable so three lanes can build in parallel. D-Bus is more idiomatic on Linux but is painful to speak from a WebExtension and adds a second schema. A D-Bus *shim* can be added in M5 for desktop integration only. |
| Persistence | **SQLite** (WAL) | Task list, segment bitmaps, categories, rules, queues, history. Crash-safe, zero admin, one file. |
| Extension | **MV3 WebExtension, TypeScript** | Firefox MV3 still allows blocking `webRequest`, which is what makes true "intercept before the browser downloads it" possible — the thing Chrome MV3 took away. |
| Build | **CMake ≥ 3.28 + Ninja + presets** | Presets mean every agent and CI run the identical configure line. |
## 2. Processes
### `veloxd` — the daemon
Owns everything: task list, scheduler, queues, disk, sockets, settings.
- Single instance, enforced by an abstract-namespace lock socket.
- Started by systemd **user** unit `velox.service`, socket-activated by `velox.socket`.
Also auto-started by the GUI or nmhost if not running (`systemctl --user start velox`).
- Listens on:
- `$XDG_RUNTIME_DIR/velox/velox.sock` (mode 0600) — GUI, CLI, nmhost. Peer credentials
checked via `SO_PEERCRED`; same-UID only. No token needed, the socket permission *is*
the authorization.
- `127.0.0.1:<ephemeral, recorded in $XDG_RUNTIME_DIR/velox/ws.port>` — loopback
WebSocket for the extension fallback transport. **Token-authenticated + pairing
prompt.** See `docs/05-extension-spec.md` §4.
- Threads: 1 RPC/event loop, 1 curl-multi transfer thread per ~8 active segments
(capped), 1 disk writer thread per active task, 1 timer thread for the scheduler.
Never block the RPC loop on disk or DNS.
### `velox-gui` — Qt 6 client
Stateless view. On start: connect → `session.hello``download.list` → subscribe to
`event.*`. On daemon restart: exponential-backoff reconnect with a banner, never lose the
window. Everything it displays comes from the daemon; it holds no download state of its own.
### `velox` — CLI
Same RPC, scriptable: `velox add <url> --dir ~/ISOs --segments 16`, `velox ls`,
`velox pause <id>`. Falls out nearly free once the generated client exists, and it is the
fastest way to test the daemon before the GUI is ready.
### `velox-nmhost` — native-messaging bridge
Deliberately trivial and boring: reads Firefox's 4-byte-length-prefixed JSON from stdin,
writes it to the Unix socket, pumps replies back. **No business logic — ever.** Under 300
lines. It exists only because Firefox's native messaging speaks stdio and the daemon
speaks sockets. If a feature needs logic, it belongs in the daemon.
## 3. The layering rule (enforced in review)
```
libveloxcore → knows nothing about RPC, JSON, SQL, or Qt.
Input: a DownloadSpec. Output: bytes on disk + callbacks.
veloxd → knows RPC, SQL, scheduling. Depends on core. No Qt.
velox-gui → knows Qt and the generated client. Zero engine code.
extension → knows the browser and the generated TS client. Zero download logic.
```
If the GUI ever needs to know what a "segment steal" is, the layering has been violated.
The daemon's job is to project the engine's state into the contract's `TaskDetail` type,
and that is the only shape the GUI ever sees.
## 4. How three lanes build at the same time without breaking each other
This is the part that makes parallel agents work, so it is a hard process, not a habit:
1. **`contracts/` is the single source of truth** and is owned by exactly one lane (PROTO).
JSON Schema for every type, method, and event, plus an OpenRPC document.
2. **Codegen, not hand-written types.** `contracts/codegen/gen_cpp.py` emits
`libveloxproto` (structs + (de)serialization); `gen_ts.py` emits `@velox/protocol`.
Generated files are committed so nobody is blocked on running the generator. Hand-editing
a generated file is a merge-blocking offence.
3. **Golden fixtures.** Every method has request/response JSON examples in
`contracts/fixtures/`. `tests/conformance/` replays them against *both* the C++ server
and the TS client. Green fixtures = the two lanes are compatible, without them ever
having run against each other.
4. **`tools/mockd`** — a TS mock daemon that serves the fixtures and fakes progress events.
The GUI and extension lanes develop against it from day one and never wait for `veloxd`.
5. **Protocol changes are a PR to `contracts/` only**, with a `VERSION` bump and updated
fixtures. Adding an optional field = minor. Removing/renaming/retyping = major, and
`session.hello` refuses a mismatched major with a clear error the GUI shows as
"Update Velox".
## 5. Data locations
| What | Path |
|---|---|
| Config | `~/.config/velox/settings.json` |
| Database | `~/.local/share/velox/velox.db` |
| Logs | `~/.local/state/velox/velox.log` (rotated, 5×2 MiB) |
| Runtime sockets | `$XDG_RUNTIME_DIR/velox/` |
| Temp/partial data | configurable; default `~/.local/share/velox/temp/` |
| Default download root | `~/Downloads/` with category subfolders |
Partial files: the target file is created **sparse and preallocated at final size** in the
destination directory as `<name>.veloxpart`, with `<name>.veloxpart.meta` beside it. On
completion the part file is renamed in place — no copy, no second full-size write, and a
half-finished download is never mistaken for a real file by other apps.
+141
View File
@@ -0,0 +1,141 @@
# 02 — Roadmap
Eight milestones. **M0 is the only serialized one** — after the contract freeze, four
lanes run in parallel and only re-synchronize at integration gates.
Durations are given in "lane-weeks" for a focused agent. Treat them as sequencing weight,
not a delivery promise.
---
## M0 — Foundations & contract freeze *(everything is blocked on this; keep it short)*
| # | Task | Lane |
|---|---|---|
| 0.1 | Toolchain bootstrap (`apt` list in README), `git init`, branch policy | PKG |
| 0.2 | CMake presets, top-level targets, clang-format/tidy, ASan/UBSan/TSan CI | PKG |
| 0.3 | **Write every schema in `contracts/schema/`** for the v1 method surface | PROTO |
| 0.4 | `gen_cpp.py` + `gen_ts.py`, generated code committed | PROTO |
| 0.5 | Golden fixtures for every method; `tests/conformance/` runner (C++ + TS) | PROTO |
| 0.6 | `tools/mockd` serving fixtures + synthetic progress events | PROTO |
| 0.7 | `tools/testserver` — a *hostile* HTTP server: no-Range, flaky, redirect chains, 401, slow-loris, changing ETag | QA |
| 0.8 | Decide product name + icon set licence; ADR for each M0 decision | PKG |
**Exit gate:** `tools/mockd` answers every fixture; the generated C++ and TS clients both
round-trip every fixture; CI is green on an empty repo. **Freeze `VERSION` at 1.0.0.**
---
## M1 — Four lanes in parallel *(the long stretch)*
### 1A · CORE — engine ⏱ 45
Probe → segmentation → dynamic stealing → sparse preallocation → ring-buffered `pwrite`
`.veloxpart.meta` resume → token-bucket limiter → retry policy.
**DoD:** downloads a 5 GB file at line rate; kill `-9` at 60 % and resume finishes with a
byte-identical SHA-256; every `tools/testserver` hostile mode handled; ASan/TSan clean;
`tools/bench` numbers recorded as the baseline.
### 1B · DAEMON — RPC + state ⏱ 34
Unix socket + WS listeners, JSON-RPC dispatcher, subscriptions and event batching, SQLite
schema + migrations, queues, scheduler, settings, pairing/token store, systemd user units.
**DoD:** passes the full conformance suite as a *server*; `velox` CLI can add/list/pause/
resume; survives daemon restart with all tasks intact.
### 1C · GUI — Qt shell against `mockd` ⏱ 45
Main window, model/delegate table, category tree, Add-URL + File-Info dialogs, progress
dialog with segment bars and speed graph, Options tabs wired to `settings.*`, tray, drop
target, QSS theming.
**DoD:** every screen in `docs/03-gui-spec.md` exists and is driven **entirely** by
`mockd`; 10 000 synthetic rows scroll at 60 fps; zero download logic in the GUI tree.
### 1D · EXT — extension against `mockd` ⏱ 34
Capture pipeline, both transports + pairing, context menus, popup with live progress,
options page, media detection.
**DoD:** intercepts a real download in real Firefox and hands it to `mockd`; **fail-open
verified by test** (daemon killed → Firefox downloads normally); `web-ext lint` clean.
> Lanes touch **only their own directories**. The sole shared surface is `contracts/`, and
> only PROTO writes there. See `CLAUDE.md`.
---
## M2 — First vertical slice ⏱ 12 *(all lanes, one week, together)*
Swap `mockd` for the real `veloxd`. Click a link in Firefox → extension captures →
File-Info dialog appears → download runs multi-segment → progress in GUI *and* popup →
file lands in the right category folder → checksum verified.
**Exit gate:** that flow works end-to-end on a clean Ubuntu 26.04 VM, from a `.deb`, with
**snap Firefox**. This is the moment the snap native-messaging risk is settled for real.
---
## M3 — IDM parity ⏱ 34 *(parallel again)*
Categories & automatic file distribution · rules engine (extension/MIME/host/size → folder)
· queues + scheduler UI · speed limiter · batch download (wildcards + clipboard blob) ·
Site Grabber wizard · Site Logins via Secret Service · proxy/SOCKS5 · duplicate handling ·
"Refresh download address" · post-download commands · virus-scan hook.
**Exit gate:** the IDM feature checklist in this doc's appendix is ticked or has a written
"won't do, because…".
---
## M4 — Media grabber ⏱ 23
HLS/DASH manifest parsing in the daemon, variant enumeration, segment-parallel fetch, mux
via ffmpeg, in-page video panel in the extension. DRM streams detected and clearly refused.
## M5 — Polish ⏱ 2
Theming light/dark, i18n + RTL, accessibility pass, notifications, sounds, global shortcut
via the portal, Wayland clipboard spike resolved, first-run wizard, tray/drop-target
behaviour, `--help` and man pages.
## M6 — Packaging & release ⏱ 12
`.deb` + PPA, Flatpak manifest, native-messaging manifests installed to all four locations,
AMO submission (signed XPI), autostart, upgrade/migration test from a previous DB version,
uninstall leaves no orphan sockets or manifests.
## M7 — Hardening ⏱ 2
libFuzzer on every parser (`Content-Disposition`, HLS, DASH, meta file, JSON-RPC), 72-hour
soak with 500 queued tasks, perf targets from `docs/04` §8 enforced in CI, threat-model
review of the WS transport, crash reporting (local only, opt-in, no telemetry).
---
## Dependency graph
```
M0 ──┬──► 1A CORE ──┐
├──► 1B DAEMON ─┼──► M2 ──► M3 ──► M4 ──► M5 ──► M6 ──► M7
├──► 1C GUI ────┤
└──► 1D EXT ────┘
(all four against mockd, no cross-lane blocking)
```
Critical path: **M0 → 1A/1B → M2**. GUI and EXT can absorb schedule slack because `mockd`
never blocks them. If you must cut scope, cut M4 (media) before anything else — it is the
largest chunk of work with the least effect on core parity.
---
## Appendix — IDM feature checklist
| IDM feature | Milestone | Notes |
|---|---|---|
| Multi-segment accelerated download | M1 | Dynamic stealing, not static split |
| Resume broken/interrupted downloads | M1 | With `If-Range` revalidation |
| Browser integration / auto-capture | M1D+M2 | Firefox first; Chrome later if wanted |
| Download categories + auto file distribution | M3 | Rules engine |
| Queues + scheduler | M3 | Per-queue time windows |
| Speed limiter | M1/M3 | Engine in M1, UI in M3 |
| Batch downloads / wildcards | M3 | |
| Site Grabber | M3 | Depth-limited crawler with filters |
| Video/media grabber | M4 | HLS/DASH; no DRM |
| Drag-and-drop drop target | M1C | |
| Clipboard monitoring | M5 | Wayland-limited; see risks |
| Proxy / SOCKS5 / site logins | M3 | Credentials in Secret Service |
| Checksum verification | M1 | MD5/SHA-256 |
| On-completion actions (open/shutdown) | M1C/M3 | Shutdown via logind, confirmed |
| ZIP preview | — | **Won't do.** Low value on Linux |
| Dial-up / VPN auto-redial | — | **Won't do.** Obsolete |
+152
View File
@@ -0,0 +1,152 @@
# 03 — GUI specification (IDM parity)
Owner: lane **GUI**. Qt 6 Widgets, C++23.
> **Legal note, stated once:** replicating IDM's *layout, workflow and feature set* is
> fine — UI ideas aren't protectable. Copying its *icons, artwork, sounds or exact
> logo/branding* is not. Ship an original icon set (a Papirus/Breeze-derived set under a
> compatible licence is the cheap route) laid out in the same positions. That gives users
> the muscle memory without shipping someone else's assets.
## 1. Main window
```
┌─ Velox Download Manager ─────────────────────────────────────────────── ─ □ ✕ ┐
│ Tasks File Downloads View Help │
├───────────────────────────────────────────────────────────────────────────────┤
│ [+] [▶] [⏸] [⏹] [🗑] [🗑✓] [🗓] [⚙] [🌐] │
│ Add URL Resume Pause Stop All Delete Del.Compl Scheduler Options Grabber│
├──────────────────┬────────────────────────────────────────────────────────────┤
│ ▼ All Downloads │ File Name │Q│ Size │ Status │Time Left│ Speed │Date│
│ Unfinished │ ubuntu.iso │1│ 5.8GB │ 47.2 % │ 00:03:11│ 28MB/s│... │
│ Finished │ report.pdf │ │ 2.1MB │Complete │ │ │... │
│ ▼ Categories │ track.flac │2│ 38MB │ Queued │ │ │... │
│ Compressed │ film.mkv │ │ 1.4GB │ Paused │ │ │... │
│ Documents │ │
│ Music │ │
│ Programs │ │
│ Video │ │
│ ▼ Queues │ │
│ Main Queue │ │
│ Sync Queue │ │
├──────────────────┴────────────────────────────────────────────────────────────┤
│ 4 downloads, 1 active ↓ 28.4 MB/s Limit: off Queue: running ● Connected│
└───────────────────────────────────────────────────────────────────────────────┘
```
**Columns** (reorderable, resizable, hideable, persisted): File Name · Q · Size · Status ·
Time Left · Transfer Rate · Last Try Date · Description. Sort on any column, ascending and
descending, sort state persisted.
**Implementation:** `QTreeView` + `DownloadTableModel : QAbstractItemModel` fed by
`event.task.progress` deltas. Never rebuild the model on an event — apply a row patch and
emit `dataChanged` for the touched columns only. Progress bar drawn by a `QStyledItemDelegate`
in the Status column. Coalesce progress events on a 250 ms timer; at 20 active downloads
you get 4 repaints/sec, not 400.
**Row context menu:** Open · Open With · Open Folder · Move/Rename · Redownload ·
Refresh Download Address · Resume · Pause · Delete · Add to Queue ▸ · Properties ·
Copy Download URL.
**Category tree:** counts per node, drag a row onto a category to re-file it (moves the
file on disk and updates the DB in one RPC).
## 2. Download File Info dialog (appears when a URL is added)
IDM's signature dialog. Populated from `download.probe`.
```
File Name: [ ubuntu-26.04-desktop-amd64.iso ]
Save As: [ /home/sami/Downloads/Programs/ ] [ Browse… ]
Category: [ Programs ▾ ] Size: 5.8 GB Resume capability: Yes
Description:[ ]
Connections:[ 8 ▾ ] Buffer: [ 4 MiB ▾ ] ☐ Remember for this file type
[ Download Now ] [ Download Later ] [ Add to Queue ▾ ] [ Cancel ]
```
"Download Later" = `startMode: "later"` (sits in the list as `PAUSED_MANUAL`).
Probe runs async: show the dialog immediately with a spinner in Size/Resume, fill in when
the reply lands, and never block the UI thread.
## 3. Download progress dialog
```
┌ ubuntu-26.04-desktop-amd64.iso ──────────────────────────────── ─ □ ✕ ┐
│ URL https://releases.ubuntu.com/26.04/ubuntu-26.04-…iso │
│ Status Receiving data… File size 5.80 GB │
│ Downloaded 2.74 GB (47.24 %) Transfer rate 28.41 MB/s │
│ Time left 00:03:11 Resume capability Yes │
├────────────────────────────────────────────────────────────────────────┤
│ 1 ████████████░░░░░ Receiving 3.9 MB/s │ 5 ██████████░░░░ 3.2 MB/s │
│ 2 ██████████░░░░░░░ Receiving 3.4 MB/s │ 6 ███████████░░░ 3.6 MB/s │
│ 3 █████████████░░░░ Receiving 4.1 MB/s │ 7 ████████░░░░░░ 2.9 MB/s │
│ 4 ███████████░░░░░░ Receiving 3.7 MB/s │ 8 ████████████░░ 3.5 MB/s │
├────────────────────────────────────────────────────────────────────────┤
│ [speed graph, 60 s rolling window, filled area, 1 Hz] │
├────────────────────────────────────────────────────────────────────────┤
│ ☐ Close dialog when done On completion: [ Do nothing ▾ ] │
│ [ Pause ] [ Cancel ] [ Hide ] │
└────────────────────────────────────────────────────────────────────────┘
```
Per-segment bars come from `TaskDetail.segments[]`. "On completion" offers: Do nothing /
Open file / Open folder / Exit Velox / Shut down (the last one via
`org.freedesktop.login1`, and it must confirm).
## 4. Options dialog — tabs
| Tab | Contents |
|---|---|
| **General** | Launch on login · minimize to tray · show floating drop target · confirm on exit · language · check for updates |
| **File Types** | Per-category extension lists (the auto-capture table the extension mirrors) · "Automatically start downloading these types" · MIME overrides |
| **Save To** | Default download directory · per-category folders · temp folder · **file-exists policy** (ask / rename / overwrite / resume) · "create subfolder per site" |
| **Connection** | Connection type preset · **max connections per download (132)** · **write buffer per connection** · global max concurrent downloads · timeout · retries · per-host connection overrides |
| **Downloads** | Speed limiter default · virus-scan command · post-download command hook · duplicate-URL policy · integrity check (MD5/SHA-256) |
| **Proxy** | System / manual HTTP / HTTPS / SOCKS5 / PAC · per-host bypass list |
| **Site Logins** | Host → username/password, stored in the **Secret Service** (gnome-keyring), never in SQLite |
| **Sounds** | Per-event sound toggles (download complete, queue complete, error) |
Everything on this dialog maps 1:1 onto `settings.get`/`settings.set` keys. The settings
key list lives in `contracts/schema/types/Settings.schema.json` — the GUI must not invent
a key that isn't in the schema.
## 5. Other windows
- **Scheduler** — per-queue: start time, stop time, days of week, one-time vs periodic,
"hang up/exit when done", max concurrent per queue.
- **Site Grabber wizard** — 4 steps (project template → start URL + depth + filters →
file-type filter → review found files, check what to download).
- **Batch download from clipboard** — parse a pasted blob of URLs, dedupe, assign category.
- **Batch download with wildcards** — `http://host/img{1..50}.jpg` expansion with preview.
- **Speed limiter** — off / limit to N KB/s, with a "apply to running downloads now" button.
- **Floating drop target** — frameless always-on-top `QWidget`, accepts dropped links,
right-click menu, position remembered. IDM's drop box, minus the branding.
- **Tray icon** — active count in tooltip, menu: Show · Add URL · Pause All · Resume All ·
Speed limiter ▸ · Quit (Quit asks whether to also stop the daemon).
## 6. Clipboard capture
`QClipboard::dataChanged` → if the text is a URL whose extension is in the monitored list,
show a toast: "Download this link? [Download] [Ignore]".
**Under Wayland this does not work the way it does on X11** — a Wayland client is not
notified of clipboard changes made by other applications. This is the #2 risk in
`docs/06-risks-and-spikes.md` and has a dedicated spike. The design must therefore treat
clipboard monitoring as *best-effort* and ship these as the real paths:
1. The extension's context menu ("Download with Velox") — covers the browser case, which
is the overwhelming majority.
2. A global shortcut (via `org.freedesktop.portal.GlobalShortcuts`) that reads the
clipboard *on demand* — an explicit user action, which the portal does allow.
3. "Add URL" dialog, pre-filled from the clipboard when it opens (also an explicit action).
## 7. Theming
`gui/resources/qss/idm-like.qss` plus a `dark.qss`. Follow the system light/dark preference
via `QStyleHints::colorScheme()`. Keep every colour in one variables block at the top of
the QSS; no hard-coded hex scattered through widget code.
## 8. Accessibility & i18n (M5, not optional)
Keyboard-reachable everything, `Qt::AccessibleName` on custom widgets, `tr()` from the
first commit, `.ts` files under `gui/i18n/`, RTL layout verified with Arabic.
+127
View File
@@ -0,0 +1,127 @@
# 04 — Download engine design (`libveloxcore`)
Owner: lane **CORE**. This is the part that has to be genuinely fast.
## 1. Task lifecycle
```
NEW → PROBING → QUEUED → CONNECTING → DOWNLOADING ⇄ PAUSED
│ │
│ └→ RETRY_WAIT → CONNECTING
ASSEMBLING → VERIFYING → COMPLETE
└→ FAILED | CANCELLED
```
`ASSEMBLING` is normally a no-op rename (see §4). It exists as a real state only for the
HLS/DASH path, which must mux.
## 2. Probe (`download.probe`)
Feeds IDM's "Download File Info" dialog, so it must be fast and never partially download.
1. `HEAD` with the browser's headers/cookies/referrer/UA verbatim.
2. If `HEAD` is refused (405/403 — common), fall back to `GET` with `Range: bytes=0-0` and
abort after headers.
3. Extract: final URL after redirects, `Content-Length`, `Content-Type`,
`Content-Disposition` filename (RFC 5987/6266, UTF-8 and legacy), `Accept-Ranges`,
`ETag`, `Last-Modified`.
4. **Resumability is proven, not assumed:** treat as resumable only if the range request
returned `206` *and* `Content-Range` matches. Servers lie about `Accept-Ranges`.
5. Filename resolution order: explicit user name → `Content-Disposition` → last path
segment (percent-decoded) → `Content-Type` extension → `download.bin`.
Then sanitize: strip `/` `\0`, control chars, trailing dots/spaces, cap at 200 bytes
*of UTF-8* (never split a codepoint), reject `.` and `..`.
## 3. Segmentation — the part IDM is actually famous for
Static N-way splitting is not what makes IDM feel fast; **dynamic segment stealing** is.
```
Initial: [====seg0====][====seg1====][====seg2====][====seg3====]
seg1 is on a fast mirror and finishes early:
[==seg0==....][ done ][==seg2==....][==seg3==....]
▲ largest remaining tail
Steal: seg1 restarts on the second half of seg2's remaining range:
[==seg0==....][ done ][=seg2=][==seg1'==][==seg3==....]
```
Rules:
- Default 8 segments, user-configurable 132, clamped per-host by settings (some hosts
ban >4 connections; keep a per-host override table).
- Never split below `min_segment_bytes` (default 1 MiB) — more segments than that is pure
overhead and gets you rate-limited.
- On finish, a worker steals the **second half of the largest remaining range**, atomically
under the task lock, and only if that half is ≥ `min_segment_bytes`.
- A segment that fails 3× with a connection error is not retried on the same host if a
mirror exists; the range is returned to the pool and re-split.
- Non-resumable servers → exactly 1 segment, and the UI must say so
(IDM's "Resume capability: No").
## 4. Disk I/O — the buffer setting you asked for
One file, opened once, `O_WRONLY`. Each segment `pwrite()`s at its own absolute offset, so
**there is no reassembly pass and no second write of the whole file.**
- `posix_fallocate()` the full size up front → contiguous extents, no ENOSPC surprise at
99 %, no fragmentation.
- Per-segment ring buffer, size = **`buffer_bytes`** (the user-visible "Buffer size"
setting). Default 4 MiB, range 64 KiB 64 MiB. Curl's write callback appends; the
buffer is flushed with a single `pwrite` when full or when the segment ends.
*This is the single biggest throughput knob and it is exposed in the UI: Options →
Downloads → "Write buffer per connection".*
- Global cap `max_total_buffer_bytes` (default 256 MiB) so 32 segments × 64 MiB can't OOM
the box. The per-segment value is silently reduced to fit and the effective value is
reported back to the UI.
- `posix_fadvise(POSIX_FADV_DONTNEED)` on written ranges — do not let a 40 GB ISO evict
the user's entire page cache.
- `fdatasync()` on a timer (default 5 s) and on pause, **not** per write.
- `io_uring` is a **post-1.0 optimization**, gated behind a benchmark in `tools/bench/`
that must show ≥10 % improvement on NVMe. Do not start there.
## 5. Resume metadata — `<name>.veloxpart.meta`
Written next to the part file so a download survives a daemon crash, a reboot, *and* a
database loss. Little-endian, versioned, `fdatasync`'d on every segment-boundary update:
```
magic "VDMP" | u16 version | u16 flags
u64 total_size | u64 downloaded
url_set (original + effective + mirrors, length-prefixed UTF-8)
etag | last_modified | content_type
u32 segment_count
per segment: u64 start, u64 end, u64 completed
sha256_partial_state (optional, for streaming hash)
crc32 of the whole record
```
On resume, revalidate with `If-Range: <etag or last-modified>`. If the server answers
`200` instead of `206`, the file changed underneath us: surface it as
"File on server has changed — restart download?" rather than silently corrupting the file.
This is the single most common way download managers produce broken files. Do not get it wrong.
## 6. Rate limiting
Hierarchical token buckets: global → per-queue → per-task. Refill on a 100 ms tick;
throttle by delaying curl reads (`CURLOPT_MAX_RECV_SPEED_LARGE` as a coarse floor, plus
our own pause/unpause via `curl_easy_pause` for precision). "Speed Limiter" in the UI
toggles between Full speed / a saved limit, exactly as IDM does.
## 7. Failure policy
| Case | Behaviour |
|---|---|
| 5xx / timeout / reset | Exponential backoff 1 s→2→4→8→…→cap 60 s, jitter ±20 %, `max_retries` default 10 |
| 401/407 | Emit `event.auth.required`, pause task, GUI shows credential dialog, store in secret service (never in SQLite) |
| 403 after redirect | Retry once with the original referrer; many CDNs require it |
| 416 | Metadata is stale → re-probe, re-split |
| Disk full | Pause all, one notification, do not spin |
| Server drops range support mid-download | Demote to 1 segment, keep what's on disk, continue |
## 8. Performance targets (M7 gate, `tools/bench/`)
- Saturate a 1 Gbit link with ≤ 8 % of one core.
- ≤ 60 MB RSS with 20 active downloads at default buffers.
- 10 000-row task list: RPC `download.list` under 50 ms, GUI scroll at 60 fps.
- No allocation in the curl write callback hot path (ring buffer is preallocated).
+160
View File
@@ -0,0 +1,160 @@
# 05 — Firefox extension specification
Owner: lane **EXT**. TypeScript, MV3, `browser.*` promise APIs, built with esbuild,
packaged with `web-ext`.
## 1. Why Firefox MV3 is actually good news here
Chrome's MV3 removed blocking `webRequest`, which is why IDM-style "grab the download
before the browser starts it" is hard there. **Firefox kept blocking `webRequest` in
MV3.** That means we can inspect response headers and cancel the browser's own download,
which is exactly the interception model IDM uses. Build for Firefox first and do not
compromise the design to stay Chrome-portable.
## 2. Capture pipeline
```
onBeforeSendHeaders ──► stash request headers by requestId (ring buffer, 5 min TTL)
onHeadersReceived ───► shouldCapture(details, headers, settings)?
│ │
│ yes│
│ ▼
│ cookies.getAll(url) ──► transport.send("capture.offer", {...})
│ │
│ daemon replies {action:"take", taskId}
│ ▼
└──────────────► return {cancel: true} ← browser never starts the download
```
`shouldCapture` returns true when **any** of:
- `Content-Disposition: attachment` present, or
- the file extension is in the user's monitored list (Options → File Types, mirrored from
the daemon so the two never disagree), or
- `Content-Type` is in the monitored MIME list and not `text/html`, or
- `Content-Length` > `minSizeBytes` (default 1 MiB) **and** the type is not renderable.
And **none** of:
- the tab is a `blob:`/`data:` URL we generated,
- the host is on the user's exclusion list,
- the response is a navigation to an HTML page,
- the user held the bypass modifier (default: Alt) on the click.
**Fail-open, always.** If the daemon is unreachable or the RPC times out (750 ms budget),
`return {}` and let Firefox download it normally. A download manager that eats downloads
when its daemon is down is worse than no download manager. This rule is non-negotiable and
has a dedicated conformance test.
Belt-and-braces second path: `browser.downloads.onCreated` → if it slipped past the
header hook, `downloads.cancel(id)` + `downloads.erase(id)` and offer to the daemon. Some
downloads (form POSTs, service-worker-generated blobs) only surface here.
## 3. Other surfaces
| Surface | Behaviour |
|---|---|
| Context menu (link) | "Download with Velox" |
| Context menu (image/video/audio) | "Download with Velox" |
| Context menu (page/selection) | "Download all links with Velox…" → opens the batch dialog with the harvested list |
| Toolbar popup | Active downloads with live progress (via `event.task.progress` relayed over the transport), pause/resume, "Add URL", speed indicator, daemon status dot |
| Media panel | Floating in-page button on a tab where a video/HLS/DASH stream was detected — "Download this video ▾" listing quality variants |
| Options page | Transport & pairing · monitored types · min size · exclusion list · default category/folder · bypass modifier · enable/disable capture |
| Keyboard | Configurable command to grab the current tab's URL |
**Media detection:** `webRequest` sniffing for `.m3u8`, `.mpd`, `Content-Type:
application/vnd.apple.mpegurl` / `dash+xml`, plus a content script observing
`MediaSource.addSourceBuffer` and `<video>` `src` changes. The extension sends the manifest
URL + headers to the daemon; the **daemon** parses the manifest and enumerates variants
(`media.listVariants`). The extension never parses HLS — keep the logic in one language.
> DRM-protected streams (Widevine/EME) are explicitly out of scope. Detect them and grey
> out the button with "Protected content" rather than failing mysteriously.
## 4. Transports — the reason this design has two
**Evidence from this machine:** Firefox here is the **Mozilla snap** (`snap list firefox`
`154.0`, `mozilla**`). Snap-confined Firefox has a long history of trouble executing
native-messaging host binaries that live outside the snap's world. Meanwhile
`snap connections firefox` shows `network` and `network-bind` **connected**.
So the extension implements a `Transport` interface with two implementations and picks at
runtime:
```ts
interface Transport {
connect(): Promise<void>
call<M extends Method>(m: M, p: Params<M>): Promise<Result<M>>
on(event: string, cb: (p: unknown) => void): void
readonly state: "connected" | "connecting" | "disconnected"
}
```
### A. `NativeTransport` (preferred when it works)
`browser.runtime.connectNative("com.velox.host")``velox-nmhost` → Unix socket.
Manifest installed to **all** of these, because the right one depends on how Firefox was
installed:
- `~/.mozilla/native-messaging-hosts/com.velox.host.json` (deb/tarball)
- `~/snap/firefox/common/.mozilla/native-messaging-hosts/com.velox.host.json` (snap)
- `/usr/lib/mozilla/native-messaging-hosts/com.velox.host.json` (system-wide)
- `~/.var/app/org.mozilla.firefox/.mozilla/native-messaging-hosts/` (flatpak)
### B. `WebSocketTransport` (fallback, guaranteed to work under snap confinement)
`ws://127.0.0.1:<port>` where the port is discovered by trying a small fixed range and
verifying a `session.hello` handshake — the extension cannot read
`$XDG_RUNTIME_DIR/velox/ws.port`, so the daemon binds the first free port in
`5200052016` and the extension probes them.
**Pairing (first run only):** the extension connects and calls `session.pair`. The daemon
pops a GUI/desktop-notification dialog: *"Firefox is requesting to connect to Velox.
Pairing code: **4821**. [Allow] [Deny]"*. The user clicks Allow (or types the code in the
extension options if the GUI isn't running). The daemon returns a 256-bit token; the
extension stores it in `browser.storage.local` and sends it on every subsequent connect.
**Security requirements (non-negotiable, conformance-tested):**
- Bind `127.0.0.1` only — never `0.0.0.0`.
- Verify `Origin: moz-extension://…` on the WS upgrade, and require the token.
- Rate-limit failed auth (5/min, then a 60 s lockout) so the token can't be brute-forced
by another local process.
- Token is per-install, revocable from Options → "Unpair", and stored in the daemon's DB
as a hash, not plaintext.
- **Never** expose a method that can write to an arbitrary path without a task the user
approved. The extension can request a download; it cannot ask the daemon to write to
`~/.bashrc`.
## 5. Startup UX when the daemon is missing
Popup shows a red dot and: *"Velox isn't running. [Start it] [Install]"*. `[Start it]`
tries `session.hello` again after asking the native host to spawn the daemon; if the
native transport is unavailable, link to install instructions. Capture stays fail-open
throughout — Firefox keeps downloading normally.
## 6. Build & test
```
extension/
├── manifest.json # MV3, permissions listed and justified in a comment
├── src/background/index.ts # event page entry
│ ├── capture/{headers,rules,downloads-api,media}.ts
│ ├── transport/{index,native,websocket,discovery}.ts
│ ├── context-menus.ts badge.ts state.ts
├── src/content/{media-observer,link-harvest,video-panel}.ts
├── src/popup/ src/options/ # plain TS + minimal CSS; no framework needed
└── src/shared/protocol/ # GENERATED from contracts/ — never hand-edit
```
- Unit: **vitest** with `webextension-polyfill` mocked; every `shouldCapture` decision gets
a table-driven test (this is where the bugs will be).
- Integration: against `tools/mockd` over WebSocket.
- E2E: **Playwright** with a real Firefox and a real `veloxd`, asserting a real file lands
on disk with the right bytes. Lives in `tests/e2e/`.
- Lint: ESLint + `web-ext lint` (AMO rules) in CI from day one — finding out at submission
time that a permission is disallowed costs a week.
## 7. Permissions (keep this list short; AMO reviews it)
`webRequest`, `webRequestBlocking`, `downloads`, `cookies`, `contextMenus`, `storage`,
`notifications`, `nativeMessaging`, `<all_urls>`.
`<all_urls>` is unavoidable for a download manager but is the main review-friction item:
document *why* in the AMO submission notes and in the source, and make the exclusion list
prominent in Options.
+111
View File
@@ -0,0 +1,111 @@
# 06 — Risks and spikes
Each spike is a **timeboxed M0/M1 investigation with a written answer in `docs/adr/`**.
Do not let any of these be discovered in M6.
---
## R1 — Snap-packaged Firefox blocks native messaging ⚠ HIGH
**Evidence gathered on this machine (2026-09-09):**
```
$ snap list firefox → firefox 154.0 mozilla**
$ snap connections firefox → home connected
network connected
network-bind connected
personal-files (dot-mozilla-firefox)
```
Ubuntu ships Firefox as a strictly-confined snap. Executing a native-messaging host binary
that lives outside the snap's confinement has a long history of breaking, and even when the
manifest is found, the host runs under the snap's constraints.
**Why it's already handled:** `network` and `network-bind` are connected, so a loopback
WebSocket to `127.0.0.1` is available regardless. The dual-transport design in
`docs/05-extension-spec.md` §4 is not belt-and-braces engineering for its own sake — it is
the direct consequence of this finding.
**Spike S1 (2 days, M0, lane EXT):** on a clean 26.04 VM, install a trivial native host
into each of the four manifest locations and record exactly which ones snap Firefox can
launch, and whether the launched process can reach `$XDG_RUNTIME_DIR`. Write the result to
`docs/adr/0003-native-messaging-under-snap.md`.
**Decision rule:** if native messaging works → prefer it, keep WS as fallback. If it does
not → WS becomes the primary path, `velox-nmhost` still ships for deb/flatpak/tarball
Firefox users, and the installer detects the snap and configures pairing automatically.
Either way, M2 ships. The *installer* must detect which Firefox is in use and say so.
---
## R2 — Wayland clipboard monitoring ⚠ HIGH (feature-shaping)
Ubuntu 26.04 defaults to GNOME on Wayland. A Wayland client **cannot** passively observe
clipboard changes made by other applications — that is a deliberate security property, not
a bug, and it is the mechanism IDM's clipboard capture relies on.
**Spike S2 (2 days, M1, lane GUI):** test, on this exact desktop, (a) whether Qt receives
`QClipboard::dataChanged` for copies made in another app, (b) whether Mutter exposes
`ext-data-control-v1` / `wlr-data-control`, (c) whether `org.freedesktop.portal.GlobalShortcuts`
gives a reliable "grab clipboard now" hotkey, (d) XWayland fallback behaviour.
**Ship-regardless design:** the extension context menu covers the browser case (where
almost all copied download links come from), the portal global shortcut covers explicit
capture, and the Add-URL dialog pre-fills from the clipboard when opened. Background
monitoring is a bonus if the spike says yes. **Do not let this block the release, and do
not promise it in the UI before S2 answers.**
---
## R3 — AMO review friction 🟠 MEDIUM
`<all_urls>` + `webRequestBlocking` + `nativeMessaging` is a heavyweight permission set;
a download manager legitimately needs it, but reviews take longer and can bounce.
**Mitigation:** run `web-ext lint` in CI from day one, no remote code execution *at all*
(no CDN scripts, no `eval`), ship readable source with a build-reproduction script, write
the permission justification in M1 rather than at submission, and submit an early
unlisted build in M3 to shake out review problems while there's still time.
---
## R4 — Servers that lie about ranges 🟠 MEDIUM
`Accept-Ranges: bytes` present but `206` never delivered; ETags that change per request;
CDNs that 403 a second connection; signed URLs that expire mid-download.
**Mitigation:** resumability is *proven* by an actual 206 with a matching `Content-Range`
(`docs/04` §2); `tools/testserver` implements each of these as an explicit hostile mode and
CORE's DoD requires passing all of them; `download.refreshUrl` exists so the user can paste
a fresh signed URL into a running task.
---
## R5 — Qt 6 LGPL compliance 🟢 LOW but do it right
Dynamic linking against unmodified system Qt satisfies LGPLv3. **Do not** static-link Qt
into the AppImage without reading the terms; if the AppImage bundles Qt, bundle it as
shared objects and ship the relink information. Record in `docs/adr/0002-qt-licensing.md`.
---
## R6 — ffmpeg/libav licensing for the media grabber 🟢 LOW
Depend on the distro's ffmpeg rather than bundling; keep the muxer behind a runtime check
so the app degrades gracefully when ffmpeg is absent. Never bundle a GPL build into a
package whose licence conflicts.
---
## R7 — Contract drift between lanes 🟠 MEDIUM
The classic parallel-development failure: GUI and extension each "fix" the protocol in
their own tree and integration in M2 becomes a rewrite.
**Mitigation:** the entire `contracts/` discipline — single owner, generated code, golden
fixtures, conformance suite as a merge gate. If a lane finds the contract wrong, it opens
a `contracts/`-only PR; it does **not** work around it locally. This is the single most
important process rule in the project.
---
## R8 — Scope creep into a browser-agnostic product 🟢 LOW
Chrome/Chromium support means losing blocking `webRequest` and rebuilding capture on
`declarativeNetRequest` + `downloads.onDeterminingFilename`, which is a different design.
Ship Firefox 1.0 first. Revisit after M7 as its own project, not as an M3 side quest.
+51
View File
@@ -0,0 +1,51 @@
# 07 — Packaging & install layout
Owner: lane **PKG/QA**. Target: Ubuntu 26.04 LTS.
## Install layout (.deb)
```
/usr/bin/veloxd
/usr/bin/velox-gui
/usr/bin/velox # CLI
/usr/libexec/velox/velox-nmhost # native messaging host
/usr/lib/x86_64-linux-gnu/libveloxcore.so.1
/usr/share/applications/velox.desktop
/usr/share/icons/hicolor/*/apps/velox.png
/usr/share/man/man1/velox.1.gz
/usr/lib/systemd/user/velox.service
/usr/lib/systemd/user/velox.socket # socket activation
/usr/lib/mozilla/native-messaging-hosts/com.velox.host.json
/etc/xdg/autostart/velox-gui.desktop # optional, off by default
```
`postinst` additionally drops per-user native-messaging manifests for the packaging formats
that need them, and **detects whether Firefox is a snap** — if so it prints (and the GUI's
first-run wizard shows) a one-line note that the extension will pair over loopback.
## Package matrix
| Format | Priority | Notes |
|---|---|---|
| `.deb` via PPA | **Primary** | The only format where native messaging, systemd user units and Secret Service all behave predictably |
| Flatpak | Secondary | The sandbox changes native messaging *again*; test explicitly, don't assume. Needs `--filesystem=xdg-download` and a portal-based folder picker |
| AppImage | Optional | Convenient for testing; if it bundles Qt, honour LGPLv3 relink terms (`docs/06` R5) |
| Snap | **Not planned** | Confinement fights both native messaging and arbitrary download destinations. Revisit only if there's demand |
## Firefox extension distribution
Signed XPI on **addons.mozilla.org**. Ship the source with a reproducible build script
(AMO requires it for minified/bundled submissions). The `.deb` does *not* bundle the XPI —
it links to AMO from the first-run wizard, so extension updates flow through Firefox's own
update channel rather than through apt.
## Release checklist
- [ ] `lintian` clean
- [ ] Fresh 26.04 VM: install → install extension → M2 vertical slice passes with snap Firefox
- [ ] Upgrade from N-1: DB migrates, in-flight `.veloxpart` files still resume
- [ ] Uninstall: manifests, units and sockets removed; user data untouched unless purged
- [ ] `systemctl --user` units enabled and socket-activation verified from cold boot
- [ ] Protocol `VERSION` matches between the shipped daemon and the shipped extension, and
a deliberate mismatch produces the "Velox needs updating" message rather than a hang
- [ ] Licence audit: Qt (LGPLv3, dynamic), libcurl, SQLite, ffmpeg, icon set
@@ -0,0 +1,26 @@
# ADR 0001 — Record architecture decisions
**Status:** accepted · **Date:** 2026-09-09
## Context
Several agents build this project in parallel, and each starts without the others' context.
Decisions that live only in a chat log get re-litigated or silently reversed.
## Decision
Every non-obvious decision gets a numbered file here: context, decision, consequences,
alternatives rejected and why. Keep it under a page. Amend by adding a new ADR that
supersedes the old one — never rewrite history.
## Expected ADRs
| # | Subject | Owner | Due |
|---|---|---|---|
| 0002 | Qt 6 licensing and how packages link it | PKG | M0 |
| 0003 | Native messaging under snap Firefox (spike S1) | EXT | M0 |
| 0004 | Wayland clipboard capability (spike S2) | GUI | M1 |
| 0005 | Protocol v1.0.0 freeze and the versioning rule | PROTO | M0 |
| 0006 | Product name and icon-set licence | PKG | M0 |
| 0007 | Segment-stealing heuristics and defaults | CORE | M1 |
| 0008 | Loopback WS threat model and pairing design | DAEMON | M1 |
+60
View File
@@ -0,0 +1,60 @@
# Agent brief — CORE (`libveloxcore`)
**Starts when PROTO freezes `VERSION`. The critical path runs through you.**
## You own
```
core/** tools/bench/** tools/fuzz/**
```
You may read `contracts/` and `docs/`. You write nowhere else — in particular **you never
touch `daemon/`**: if the daemon needs something, expose it as a core API and tell DAEMON.
## Read first
`docs/04-engine-design.md` end to end. It is your specification, not background reading.
## Hard architectural constraints
- **No JSON, no SQL, no Qt, no RPC in `core/`.** Your public API takes a `DownloadSpec`
and emits typed callbacks. If you find yourself including a protocol header, stop.
- C++23, `-Wall -Wextra -Werror`, no raw `new`/`delete`, no naked `pthread`.
- Every public header under `core/include/vdm/` compiles standalone.
- Errors are returned (`std::expected`-style `Result<T>`), not thrown, on the transfer path.
- **No allocation in the curl write callback.** The ring buffer is preallocated at task
start. This is checked in review and by a bench assertion.
## Build order
1. `util/``Result<T>`, event bus, thread pool, logging, byte-span helpers.
2. `net/http_client` — libcurl multi wrapper, one `curl_multi` per worker thread, poll
loop, proxy/auth/redirect/cookie plumbing.
3. `net/probe` — HEAD then ranged-GET fallback, header parsing, **`Content-Disposition`
RFC 5987/6266 including the legacy forms** (this is a classic source of mojibake — give
it its own test table and a fuzz target).
4. `io/sparse_file` + `io/write_buffer``posix_fallocate`, per-segment ring buffer sized
by `buffer_bytes`, `pwrite` at absolute offsets, `posix_fadvise(DONTNEED)`, timed
`fdatasync`.
5. `meta/veloxpart` — the resume sidecar in `docs/04` §5, CRC-verified, `fdatasync`'d at
segment boundaries. **Write the reader first and fuzz it** — this file is attacker-
adjacent (it lives in a world-writable-ish download dir).
6. `segment/segmenter` + `segment/stealer` — dynamic segment stealing, `min_segment_bytes`
floor, per-host connection caps.
7. `rate/token_bucket` — hierarchical global → queue → task.
8. `task/download_task` — the state machine in `docs/04` §1, retry/backoff policy, mirrors.
9. `rules/` — filename sanitization + collision policy + category matching (pure functions;
DAEMON supplies the rule table).
10. `media/`**M4, not now.** Leave the directory empty.
## Definition of done (M1)
- 5 GB download saturates a 1 Gbit link at ≤ 8 % of one core (recorded in `tools/bench/`).
- `kill -9` at ~60 % → resume completes → SHA-256 matches the reference byte for byte.
- Every hostile mode in `tools/testserver` handled: no-Range, lying `Accept-Ranges`,
ETag change mid-download, 401, 416, redirect chains, slow-loris, connection reset,
expiring signed URL, `Content-Length` mismatch.
- ASan + UBSan + TSan clean under a 20-task load test.
- Fuzz targets for `Content-Disposition`, the `.veloxpart.meta` reader, and URL parsing run
1 M+ execs with no crash.
- Public API documented in `core/include/vdm/README.md` and reviewed by DAEMON before M2.
## Do not
- Do not start with `io_uring`. It's a post-1.0 experiment gated on a ≥10 % bench win.
- Do not implement HLS/DASH in M1.
- Do not add a "just for testing" JSON dependency.
+59
View File
@@ -0,0 +1,59 @@
# Agent brief — DAEMON (`veloxd`)
**Starts with CORE. You are the only process that owns state.**
## You own
```
daemon/** cli/** nmhost/** packaging/nativehost/**
```
Read `contracts/`, `core/include/`, `docs/`. Never write in `core/`, `gui/`, or `extension/`.
## Read first
`docs/01-architecture.md` §2–§5, `contracts/README.md`, `docs/05-extension-spec.md` §4
(you implement the daemon half of pairing).
## Build order
1. **RPC server**`rpc/uds_server` (NDJSON over `$XDG_RUNTIME_DIR/velox/velox.sock`,
0600, `SO_PEERCRED` same-UID check) and `rpc/ws_server` (bind `127.0.0.1` only, first
free port in 5200052016, write the chosen port to `ws.port`). One dispatcher, generated
from `contracts/`. Never block the RPC loop — disk and network work goes to CORE's pools.
2. **Auth & pairing**`session.pair` triggers a user prompt (GUI dialog if connected,
else a desktop notification with actions). Tokens: 256-bit, stored **hashed**,
per-install, revocable. Failed-auth rate limit 5/min then 60 s lockout. Enforce
`x-transports` and `x-privileged` from the schema: privileged methods are refused over
WS with `-32003`.
3. **Store** — SQLite WAL. Tables: `tasks`, `segments`, `categories`, `queues`, `rules`,
`settings`, `history`, `pairings`. Numbered migrations in `store/migrations/`, applied
at startup, with a forward-only test from every released schema version.
**Credentials never go in SQLite** — Secret Service via libsecret.
4. **Scheduler & queues** — concurrency governor (global max active, per-queue max, per-host
caps), time windows, days-of-week, one-shot vs periodic, "when queue completes" actions.
5. **Event fan-out** — per-subscription filtering, and **`event.task.progress` batched at
≤ 4 Hz into a single array message**. Do not emit one message per task per tick; that is
how you turn 20 downloads into a GUI that burns a core.
6. **Capture endpoint**`capture.offer` must answer within **750 ms**, always. Apply the
rules table, resolve the category folder, dedupe against active tasks, return
`take`/`ignore`. If anything internally is slow, answer `ignore` and let Firefox have
it. Never make the browser wait.
7. **Integration** — systemd user units (`velox.service` + `velox.socket` for socket
activation), single-instance lock, XDG autostart, `org.freedesktop.Notifications`,
graceful shutdown that flushes buffers and meta files.
8. **`velox` CLI** — `add`, `ls`, `pause`, `resume`, `rm`, `queue`, `settings`, `--json`
output. Build this early: it is how you test the daemon before the GUI exists.
9. **`velox-nmhost`** — 4-byte-length-prefixed stdio ⇄ Unix socket pump. **Under 300 lines,
zero business logic**, and it must exit cleanly when Firefox closes the pipe. Install
manifests to all four locations listed in `docs/05` §4.
## Definition of done (M1)
- Passes the full conformance suite as a server, over **both** transports.
- Kill and restart the daemon mid-download: all tasks reload with correct state and resume.
- 1 000 tasks in the DB: `download.list` with paging under 50 ms.
- Pairing flow works from a real Firefox extension; unpair revokes immediately.
- `systemctl --user status velox` clean; socket activation verified from cold.
- Security review passed: no bind beyond loopback, no path traversal in `saveDir`
(canonicalize and check against allowed roots → `-32011`), no plaintext secrets.
## Do not
- Do not put download logic here — that's CORE. You schedule and persist; CORE transfers.
- Do not invent protocol fields. File a request with PROTO.
+67
View File
@@ -0,0 +1,67 @@
# Agent brief — EXT (Firefox extension)
**Starts the day PROTO freezes the contract. Develops against `tools/mockd` over the
WebSocket transport — you never wait for `veloxd`.**
## You own
```
extension/**
```
Read `contracts/`, `docs/`. Never write in `core/`, `daemon/`, `gui/`, or `nmhost/`
(the native host belongs to DAEMON; if you need a change there, file it).
## Read first
`docs/05-extension-spec.md` in full, then `docs/06` R1 (snap Firefox) and R3 (AMO).
## Your first task is a spike, not code
**Spike S1 (2 days, blocks nothing else):** on a clean Ubuntu 26.04 VM with snap Firefox,
determine empirically which of the four native-messaging manifest locations actually work,
and whether the launched host can reach `$XDG_RUNTIME_DIR`. Write
`docs/adr/0003-native-messaging-under-snap.md`. Then build the WebSocket transport first
regardless of the answer — it is the one that is known to work here
(`snap connections firefox` shows `network`/`network-bind` connected).
## Build order
1. **`transport/`** — the `Transport` interface, `WebSocketTransport` (port discovery over
5200052016 + `session.hello` verification), pairing flow with token in
`browser.storage.local`, reconnect with backoff, then `NativeTransport`, then
`transport/index.ts` picking at runtime with a manual override in Options.
2. **`capture/headers.ts`** — `onBeforeSendHeaders` stash keyed by `requestId`, ring buffer,
5-minute TTL, bounded size (a leak here eats the browser's memory).
3. **`capture/rules.ts`** — `shouldCapture()` exactly as specified in `docs/05` §2.
**Table-driven tests before implementation.** This function is where the bugs will be:
too eager and you hijack page navigations; too shy and you're not a download manager.
4. **`capture/index.ts`** — the `onHeadersReceived` blocking hook: gather cookies, call
`capture.offer` with a **750 ms budget**, `{cancel: true}` only on `take`.
**Fail-open is a hard requirement** — daemon down, slow, or erroring means Firefox
downloads normally. Write that test before the feature.
5. **`capture/downloads-api.ts`** — the `downloads.onCreated` safety net for what slips past.
6. **`context-menus.ts`**, **`popup/`** (live progress from relayed events, pause/resume,
status dot), **`options/`** (transport, pairing/unpair, monitored types synced via
`capture.getRules`, min size, exclusions, default category, bypass modifier).
7. **`content/` media detection** — `.m3u8`/`.mpd`/MIME sniffing plus a `MediaSource`
observer; in-page "Download this video ▾" panel listing variants from
`media.listVariants`. **The extension never parses manifests** — the daemon does.
Detect DRM/EME and grey the button out with "Protected content".
## Definition of done (M1)
- Intercepts a real download in real Firefox and hands it to `mockd`.
- **Fail-open proven by an automated test:** kill the mock mid-flow, the file still
downloads through Firefox, no error dialog, no lost download.
- Pairing works, unpair revokes, token survives a browser restart, and a wrong token is
rejected and rate-limited.
- `web-ext lint` clean; no remote code, no `eval`, no CDN script — AMO rejects those.
- `shouldCapture` test table covers: attachment, monitored extension, monitored MIME,
size threshold, excluded host, HTML navigation, blob URL, bypass modifier held,
streaming media, and a range request the page itself issued.
- Popup shows live progress at ≤ 4 Hz without pinning a core.
- Permission justification written and committed for AMO submission.
## Do not
- Do not implement any download logic in the extension. You collect URL + headers + cookies
and hand them over. That's the whole job.
- Do not add a framework (React/Vue) for a popup and an options page; plain TS keeps the
AMO review and the bundle small.
- Do not invent protocol fields — file a request with PROTO.
+56
View File
@@ -0,0 +1,56 @@
# Agent brief — GUI (`velox-gui`)
**Starts the day PROTO freezes the contract. You never wait for the daemon — you develop
entirely against `tools/mockd`.**
## You own
```
gui/**
```
Read `contracts/`, `docs/`. Never write in `core/`, `daemon/`, or `extension/`.
## Read first
`docs/03-gui-spec.md` — it is the spec, screen by screen. Build in the order below.
## Build order
1. **`rpc/` client** — wrap the generated C++ client: async calls on a worker thread,
Qt signals on the main thread, auto-reconnect with exponential backoff, an offline
banner instead of a modal error, and a "Connected/Reconnecting" dot in the status bar.
Get this right first; everything else depends on it.
2. **`models/DownloadTableModel`** — `QAbstractItemModel` over `TaskSummary`. Apply
`event.task.progress` batches as **row patches with narrow `dataChanged` ranges**.
Never `beginResetModel()` on a progress tick. Sorting/filtering via `QSortFilterProxyModel`
per category tree selection.
3. **Main window** — menus, toolbar, splitter, category tree, table, status bar, column
persistence in `QSettings`.
4. **`widgets/ProgressDelegate`** — the in-cell progress bar; also
`SegmentBarsWidget` and `SpeedGraphWidget` (60 s rolling, 1 Hz, painted with
`QPainterPath`, no per-frame allocation).
5. **Dialogs**, in this order: Add URL → Download File Info (async probe, spinner while it
resolves) → Download Progress → Options (all tabs, every control bound to a
`settings.*` key that exists in the schema) → Scheduler → Speed Limiter → Batch →
Grabber wizard.
6. **Tray + floating drop target** — frameless always-on-top drop widget accepting dropped
URLs and text; position persisted.
7. **Clipboard**`clipboard/monitor.*`, but read `docs/06` R2 first: this is
best-effort under Wayland. Implement the **explicit** paths (Add-URL prefill, portal
global shortcut) as the primary UX and treat passive monitoring as a bonus that spike S2
may or may not unlock. **Don't advertise it in the UI until S2 answers.**
8. **Theming**`resources/qss/idm-like.qss` + `dark.qss`, colours in one variables block,
follow `QStyleHints::colorScheme()`.
## Definition of done (M1)
- Every screen in `docs/03-gui-spec.md` exists and is driven **only** by `mockd`.
- 10 000 synthetic rows: scrolling holds 60 fps, memory flat over 10 minutes of progress
events (`mockd --tasks 10000`).
- Unhappy paths handled: `mockd --slow`, `--flaky`, `--drop-connection` produce a banner
and a clean recovery, never a freeze or a crash.
- Zero download logic in `gui/` — grep for `curl`, `pwrite`, `sqlite` must return nothing.
- All strings wrapped in `tr()`; a stub Arabic `.ts` proves the RTL layout survives.
- No blocking call on the UI thread: verified with a 200 ms watchdog in debug builds.
## Icons
Ship an **original or compatibly-licensed** icon set (Papirus/Breeze-derived is fine) laid
out in IDM's positions. Do not copy IDM's artwork. Record the icon licence in
`gui/resources/icons/LICENSE`.
+62
View File
@@ -0,0 +1,62 @@
# Agent brief — PKG/QA (build, packaging, test infrastructure)
**Starts in M0 alongside PROTO. You unblock everyone else and you own the release.**
## You own
```
packaging/** .github/workflows/** tools/testserver/** tests/integration/** tests/e2e/**
CMakeLists.txt CMakePresets.json .clang-format .clang-tidy .editorconfig .gitignore
```
Do not write feature code in any lane's directory.
## M0 — unblock the lanes (do these first, in this order)
1. **Toolchain script**`tools/bootstrap.sh` installing the `apt` list in the README,
verified on a clean 26.04 VM. The dev machine already has git 2.53, CMake 4.2.3,
g++ 15.2, ninja, Qt 6 dev, libcurl, SQLite, nlohmann-json and ffmpeg; only
`libqt6svg6-dev`, `libsecret-1-dev`, `nodejs`/`npm` and (optionally) `clang` are
missing. The script must still install the full list for clean machines.
⚠ CMake 4.x rejects `cmake_minimum_required` below 3.5 — check every dependency.
2. **CMake** — top-level `CMakeLists.txt` + `CMakePresets.json` with presets:
`dev` (Debug + ASan/UBSan), `tsan`, `release` (RelWithDebInfo + LTO), `ci`.
Ninja, C++23, `-Wall -Wextra -Werror`.
3. **`tools/testserver`** — the hostile HTTP server every lane tests against. Modes, each
toggleable by URL path or flag:
`no-range` · `lies-about-accept-ranges` · `etag-changes` · `flaky-reset` ·
`slow-loris` · `redirect-chain` · `401-basic` · `401-digest` · `403-without-referer` ·
`416-always` · `content-length-mismatch` · `expiring-signed-url` · `throttled` ·
`chunked-no-length` · `utf8-content-disposition` · `legacy-content-disposition`.
**CORE's definition of done is written in terms of this server, so it must exist first.**
4. **CI** — GitHub Actions: build matrix (gcc + clang), unit tests, ASan/UBSan/TSan jobs,
`clang-format --dry-run -Werror`, `clang-tidy`, `web-ext lint`, and **conformance as a
required check on every PR**.
## M1M5 — keep it honest
- Nightly integration run: real `veloxd` + `testserver`, 50 concurrent downloads, assert
checksums and zero leaked FDs.
- `tests/e2e/` with Playwright: real Firefox + real daemon + a real file on disk.
- 72-hour soak job (M7 gate): 500 queued tasks, memory and FD graphs must be flat.
- `tools/bench` results published per commit so a performance regression is visible the day
it lands, not in M7.
## M6 — packaging
- **`.deb`** (primary): `veloxd`, `velox-gui`, `velox`, `velox-nmhost`, desktop entry,
systemd **user** units, icons, man pages, and native-messaging manifests installed to
`/usr/lib/mozilla/native-messaging-hosts/` plus a postinst that also drops the per-user
snap and flatpak copies where applicable. `lintian` clean. Publish via PPA.
- **Flatpak** (secondary): note that the sandbox changes the native-messaging story again —
test it, don't assume it.
- **AppImage** (optional): if you bundle Qt, bundle it as shared objects and honour LGPLv3
relink requirements (`docs/06` R5).
- **AMO**: signed XPI, reproducible build script, permission justification from EXT.
- **Uninstall test**: removes manifests, units, and sockets; leaves user data alone unless
purged.
- **Upgrade test**: install N-1, create tasks, upgrade, confirm the DB migrates and
in-flight `.veloxpart` files still resume.
## Definition of done
- One command builds everything from a clean checkout on a clean 26.04 VM.
- CI red on: format, tidy, sanitizer failure, conformance failure, `web-ext lint` failure.
- A fresh VM can install the `.deb`, install the extension, and complete the M2 vertical
slice with **snap Firefox** — no manual steps beyond clicking "Allow" once at pairing.
+53
View File
@@ -0,0 +1,53 @@
# Agent brief — PROTO (contract owner)
**Runs first, alone, in M0. Then stays on call as gatekeeper for the whole project.**
## You own
```
contracts/** tools/mockd/** tests/conformance/**
```
## You may read everything. You may write nowhere else.
## Mission
Make it impossible for the CORE, DAEMON, GUI and EXT lanes to become incompatible without
CI noticing the same day.
## M0 deliverables (in order)
1. **`contracts/schema/`** — JSON Schema (draft 2020-12) for every type, method and event
in `contracts/README.md`. Two templates already exist
(`types/TaskSummary.schema.json`, `methods/capture.offer.schema.json`) — follow their
shape, including the `x-privileged` / `x-transports` / `x-deadlineMs` annotations.
2. **`contracts/openrpc.json`** — generated from the schemas; it is the doc humans read.
3. **`contracts/codegen/gen_cpp.py`** → emits `core/generated/velox_proto.{hpp,cpp}`:
plain structs, `to_json`/`from_json` (nlohmann), a `Method` enum, and a
`dispatch(method, json) -> json` skeleton. No exceptions on the hot path; parse errors
return a `Result`.
4. **`contracts/codegen/gen_ts.py`** → emits `extension/src/shared/protocol/`: discriminated
union types, a typed `call<M>()` signature, event payload types, and runtime validators
for anything crossing the WS boundary (the daemon is not allowed to trust the wire, and
neither is the extension).
5. **`contracts/fixtures/`** — every method gets a success fixture; auth, timeout, and
not-found cases get error fixtures. Use `$uuid` / `$isoDate` placeholders for values
that can't be fixed.
6. **`tools/mockd`** — Node/TS. Serves the fixtures over **both** transports (Unix socket
NDJSON and loopback WS), fakes plausible progress events at 4 Hz, and has flags for
`--slow`, `--flaky`, `--drop-connection`, `--refuse-pairing` so GUI and EXT can test
their unhappy paths before `veloxd` exists.
7. **`tests/conformance/`** — one suite, two runners: replays each fixture against a live
`veloxd` (C++ side) and through the generated TS client. Wired into CI as a **required
check on every lane's PR**.
## Definition of done
- `mockd` answers all fixtures over both transports.
- Both generated clients round-trip every fixture with no hand-written types anywhere.
- Conformance is a required CI check.
- `VERSION` frozen at `1.0.0` and the freeze announced to all lanes.
## Standing duties after M0
- You are the **only** committer to `contracts/`. Other lanes file requests; you implement,
bump `VERSION`, regenerate, update fixtures, and notify the lanes in one PR.
- Reject "just add a field locally" every single time. That request is the M2 integration
disaster arriving early enough to stop.
- Optional field or new method → minor. Rename/remove/retype → major + an ADR.