contracts/ — the wire contract
This directory is the interface between every lane. Owner: agent PROTO.
Nobody else commits here. Everybody else generates from here.
Rules
- Generated code is committed. No lane may be blocked because it can't run Python.
- Hand-editing generated files is a merge blocker. Fix the schema and regenerate.
- Every method needs at least one fixture — a success case and, where meaningful, an
error case. A method with no fixture is not done.
- Versioning: adding an optional field or a new method → minor bump. Removing,
renaming, retyping, or changing a default → major bump and a written migration note
in
docs/adr/. session.hello rejects a major mismatch with error -32001 and a
message the GUI renders as "Velox needs updating".
- Changes arrive as a PR to
contracts/ alone, containing: schema edit + fixtures +
regenerated code + VERSION bump. Lanes rebase onto it. This is the only synchronization
point in the whole project — keep it cheap and frequent rather than big and rare.
Transport framing
| Client |
Transport |
Framing |
| GUI, CLI |
$XDG_RUNTIME_DIR/velox/velox.sock |
newline-delimited JSON (NDJSON) |
| nmhost ← Firefox |
stdio |
4-byte little-endian length prefix (Firefox's format) |
| nmhost → daemon |
same Unix socket |
NDJSON |
| Extension (fallback) |
ws://127.0.0.1:520xx |
one JSON message per WS text frame |
All four carry the same JSON-RPC 2.0 payloads. The framing differences stop at the
transport layer; no method behaves differently depending on how it arrived — except that
methods marked "privileged": true in the schema are refused over the WebSocket transport.
Method surface (v1.0.0 target — expand only via PR)
Session
| Method |
Params → Result |
session.hello |
{clientType, clientName, protocolVersion, token?} → {daemonVersion, protocolVersion, capabilities[], sessionId} |
session.pair |
{clientName, extensionId} → {token, expiresAt} (WS only; triggers user prompt) |
session.subscribe |
{events[]} → {ok} |
Downloads
| Method |
Params → Result |
download.probe |
{url, headers?, cookies?, referrer?, userAgent?} → {filename, sizeBytes?, mime, resumable, effectiveUrl, suggestedCategoryId} |
download.add |
{url, headers?, cookies?, referrer?, userAgent?, filename?, saveDir?, categoryId?, segments?, bufferBytes?, startMode:"now"|"later"|"queue", queueId?, description?, checksum?} → {taskId, state} |
download.addBatch |
{items[], defaults} → {taskIds[]} |
download.list |
{filter?, sort?, offset?, limit?} → {total, items: TaskSummary[]} |
download.get |
{taskId} → TaskDetail (includes segments[]) |
download.start | .pause | .resume | .cancel |
{taskIds[]} → {updated[]} |
download.remove |
{taskIds[], deleteFile:bool} → {removed[]} |
download.update |
{taskId, patch:{filename?, saveDir?, categoryId?, queueId?, description?, segments?, bufferBytes?}} → TaskSummary |
download.refreshUrl |
{taskId, url, headers?} → {ok} (IDM's "Refresh Download Address") |
Organisation
category.list · category.upsert · category.remove · queue.list · queue.upsert ·
queue.start · queue.stop · queue.reorder · rules.list · rules.upsert ·
schedule.get · schedule.set
Settings & limits
settings.get {keys?} · settings.set {values} · limiter.get · limiter.set {globalBps?, enabled}
Browser integration
| Method |
Notes |
capture.offer |
{url, method, headers, cookies, contentType?, contentLength?, contentDisposition?, tabUrl, filename?} → {action:"take"|"ignore", taskId?, reason?} — must answer within 750 ms; the extension gives up and lets Firefox handle it otherwise |
capture.getRules |
Extension mirrors the daemon's monitored types so the two never disagree |
media.listVariants |
{manifestUrl, headers} → {variants:[{id,resolution,bitrate,codec,sizeEstimate}]} |
media.addVariant |
{manifestUrl, variantId, ...addParams} → {taskId} |
Grabber
grabber.start {startUrl, depth, includePatterns[], excludePatterns[], fileTypes[]} →
{jobId}; grabber.status {jobId}; grabber.harvest {jobId, select[]} → {taskIds[]}
Events (server → client notifications)
| Event |
Payload |
event.task.added / .removed |
{taskId, summary?} |
event.task.state |
{taskId, state, error?} |
event.task.progress |
Batched array, emitted at ≤4 Hz: [{taskId, downloaded, speedBps, etaSec, segments:[{i,completed,speedBps}]}] |
event.speed.global |
{downBps, activeCount} |
event.auth.required |
{taskId, host, realm, scheme} |
event.notify |
{level, title, body, taskId?} |
event.settings.changed |
{keys[]} |
event.grabber.progress |
{jobId, found, crawled, done} |
Error codes
| Code |
Meaning |
-32600/-32601/-32602/-32603 |
Standard JSON-RPC |
-32001 |
Protocol major version mismatch |
-32002 |
Not paired / invalid token |
-32003 |
Method not permitted on this transport |
-32010 |
Task not found |
-32011 |
Invalid destination path (outside allowed roots, or not writable) |
-32012 |
Disk full |
-32013 |
Probe failed (with data.httpStatus) |
-32014 |
Rate limited (pairing brute-force lockout) |