app
Adom USB
Public Unreviewedby John Lauer
See every USB device across the entire Adom ecosystem — laptop, WSL2, cloud container, Azure VM, workcells — in one dashboard, and mount / unmount / reset / test / force-class / sniff any of them. USB/IP slingshot for the whole mesh.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
//! `adom-usb app` — the Hydrogen webview USB device dashboard.
//!
//! One table of every USB device across the Adom ecosystem, with Local/Cloud
//! filters and per-device actions (mount/unmount/reset/test/force-class/sniff).
//! Server state is the single source of truth; the UI polls `GET /state` and
//! every button POSTs to an endpoint the AI can also drive with curl.
//!
//! M1 scope: live inventory + a mount-state overlay that records intent and
//! emits the exact agent command each action will run. The real agent calls
//! (usbipd-win over the relay, `usbip attach` in the target, usbmon, etc.) slot
//! into `agent_command()` / the action handlers as they are wired — the HTTP
//! contract here does not change when they land.
pub mod tab;
use crate::devices;
use crate::{note, ok, warn, AppArgs};
use serde_json::{json, Value};
use std::collections::{HashMap, VecDeque};
use std::sync::{Arc, Mutex};
use tiny_http::{Header, Method, Response, Server};
const UI_HTML: &str = include_str!("ui.html");
const ICON_SVG: &str = include_str!("../../docs/icon.svg");
const CONSOLE_MAX: usize = 500;
const ACTION_LOG_MAX: usize = 100;
struct AppState {
demo: bool,
dev: bool,
/// device_id → Some(host) mounted / None unmounted (live overlay).
mounts: Mutex<HashMap<String, Option<String>>>,
actions: Mutex<VecDeque<Value>>,
console: Mutex<VecDeque<Value>>,
state_ts: Mutex<u128>,
pending_eval: Mutex<Option<(String, String)>>,
eval_results: Mutex<HashMap<String, Value>>,
}
fn now_ms() -> u128 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0)
}
fn json_resp(code: u16, body: Value) -> Response<std::io::Cursor<Vec<u8>>> {
Response::from_string(body.to_string())
.with_status_code(code)
.with_header(Header::from_bytes("Content-Type", "application/json").unwrap())
.with_header(Header::from_bytes("Cache-Control", "no-cache").unwrap())
.with_header(Header::from_bytes("Content-Security-Policy", "frame-ancestors *").unwrap())
}
fn read_body(req: &mut tiny_http::Request) -> Result<Value, String> {
let mut body = String::new();
std::io::Read::read_to_string(req.as_reader(), &mut body).map_err(|e| format!("read body: {}", e))?;
if body.is_empty() { return Ok(Value::Null); }
serde_json::from_str(&body).map_err(|e| format!("parse JSON: {}", e))
}
fn snapshot(state: &AppState) -> Value {
let overlay = state.mounts.lock().unwrap().clone();
devices::snapshot(state.demo, &overlay)
}
fn find_device(snap: &Value, id: &str) -> Option<Value> {
snap.get("devices")?
.as_array()?
.iter()
.find(|d| d.get("id").and_then(|v| v.as_str()) == Some(id))
.cloned()
}
fn log_action(state: &AppState, entry: Value) {
let mut a = state.actions.lock().unwrap();
if a.len() >= ACTION_LOG_MAX { a.pop_front(); }
a.push_back(entry);
*state.state_ts.lock().unwrap() = now_ms();
}
/// The concrete command a live agent will run for an op. Surfaced in the action
/// log so a human (and the AI) can see exactly what the mount will execute, and
/// so it is trivially diffable against the eventual real implementation.
fn agent_command(op: &str, dev: &Value, target: &str, mechanism: &str) -> String {
let bus = dev.get("bus_id").and_then(|v| v.as_str()).unwrap_or("?");
let src = dev.get("source_id").and_then(|v| v.as_str()).unwrap_or("?");
let vid = dev.get("vid_hex").and_then(|v| v.as_str()).unwrap_or("????");
let pid = dev.get("pid_hex").and_then(|v| v.as_str()).unwrap_or("????");
match (op, mechanism) {
("mount", "usbipd-attach-wsl") =>
format!("[{src}] usbipd bind --busid {bus} && usbipd attach --wsl --busid {bus}"),
("mount", "reverse-tunnel") =>
format!("[{src}] usbipd bind --busid {bus} ; adom-desktop tunnel --reverse --expose {src}:3240 ; [{target}] usbip attach -r 127.0.0.1 -b {bus}"),
("mount", "direct-tcp") =>
format!("[{target}] usbip attach -r {src} -b {bus}"),
("mount", "native") =>
format!("[{target}] (native) re-bind {vid}:{pid} to the host driver"),
("unmount", _) =>
format!("[{target}] usbip detach (device {vid}:{pid}) ; [{src}] usbipd unbind --busid {bus}"),
("reset", _) => format!("[{target}] usb reset ioctl on {vid}:{pid} (detach+reattach)"),
("test", _) => format!("[{target}] lsusb -v -d {vid}:{pid} + enumerate/echo health check"),
("class_override", _) => format!("[{target}] echo <driver> > driver_override ; rebind {vid}:{pid}"),
("sniff", _) => format!("[{target}] modprobe usbmon ; capture usbmonX for {vid}:{pid} → pcap"),
_ => format!("[{target}] {op} {vid}:{pid}"),
}
}
pub fn run(args: AppArgs) -> Result<(), String> {
ok(format!("adom-usb dashboard starting on port {}", args.port));
if args.demo {
note!(" {}demo mode: seeding a representative cross-ecosystem device set{}", crate::DIM, crate::RESET);
} else {
note!(" {}live mode: real lsusb on this container + registered agents only{}", crate::DIM, crate::RESET);
}
let state = Arc::new(AppState {
demo: args.demo,
dev: args.dev,
mounts: Mutex::new(HashMap::new()),
actions: Mutex::new(VecDeque::new()),
console: Mutex::new(VecDeque::with_capacity(CONSOLE_MAX)),
state_ts: Mutex::new(now_ms()),
pending_eval: Mutex::new(None),
eval_results: Mutex::new(HashMap::new()),
});
let addr = format!("0.0.0.0:{}", args.port);
let server = Server::http(&addr).map_err(|e| format!("bind {}: {}", addr, e))?;
if !args.no_tab {
let proxy_url = tab::proxy_url(args.port);
match tab::add_webview_tab(&args.tab_name, &proxy_url, ICON_SVG) {
Ok(_) => ok(format!("opened Hydrogen tab '{}' → {}", args.tab_name, proxy_url)),
Err(e) => warn(format!("couldn't open tab: {}. Manually: {}", e, proxy_url)),
}
}
note!(" {}Press ctrl-c or POST /shutdown to stop.{}", crate::DIM, crate::RESET);
for mut req in server.incoming_requests() {
let url = req.url().to_string();
let path = url.split_once('?').map(|(p, _)| p).unwrap_or(&url).to_string();
let method = req.method().clone();
let st = Arc::clone(&state);
let result: (u16, Value) = match (method.clone(), path.as_str()) {
(Method::Get, "/") | (Method::Get, "/index.html") => {
let _ = req.respond(
Response::from_string(UI_HTML).with_status_code(200)
.with_header(Header::from_bytes("Content-Type", "text/html; charset=utf-8").unwrap())
.with_header(Header::from_bytes("Cache-Control", "no-cache").unwrap())
.with_header(Header::from_bytes("Content-Security-Policy", "frame-ancestors *").unwrap()),
);
continue;
}
(Method::Get, "/favicon.svg") => {
let _ = req.respond(
Response::from_string(ICON_SVG).with_status_code(200)
.with_header(Header::from_bytes("Content-Type", "image/svg+xml").unwrap())
.with_header(Header::from_bytes("Content-Security-Policy", "frame-ancestors *").unwrap()),
);
continue;
}
(Method::Get, "/health") => {
let snap = snapshot(&st);
let n = snap.get("devices").and_then(|d| d.as_array()).map(|a| a.len()).unwrap_or(0);
(200, json!({"ok": true, "demo": st.demo, "devices": n, "version": env!("CARGO_PKG_VERSION")}))
}
(Method::Get, "/state") => {
let mut snap = snapshot(&st);
snap["dev"] = json!(st.dev);
snap["state_ts"] = json!(*st.state_ts.lock().unwrap() as u64);
snap["actions"] = json!(st.actions.lock().unwrap().iter().rev().take(20).cloned().collect::<Vec<_>>());
// Installed plugins + which sniffer matches each device.
let installed = crate::plugins::discover_installed();
snap["plugins"] = crate::plugins::to_json(&installed);
if let Some(devs) = snap.get_mut("devices").and_then(|d| d.as_array_mut()) {
for d in devs.iter_mut() {
let class = d.get("class").and_then(|v| v.as_str()).unwrap_or("").to_string();
let vid = d.get("vid").and_then(|v| v.as_u64()).unwrap_or(0) as u16;
let pid = d.get("pid").and_then(|v| v.as_u64()).unwrap_or(0) as u16;
let class = class.as_str();
d["sniffer_plugin"] = json!(crate::plugins::match_for(class, vid, pid, &installed).map(|p| p.slug.clone()));
// Firmware device-test pack: which stub personas can we flash to this chip?
if let Some(fw) = crate::plugins::firmware_for(class, vid, pid, &installed) {
d["firmware_plugin"] = json!(fw.slug);
d["firmware_personas"] = fw.personas.clone();
} else {
d["firmware_plugin"] = Value::Null;
d["firmware_personas"] = json!([]);
}
}
}
(200, snap)
}
(Method::Get, "/plugins") => {
let installed = crate::plugins::discover_installed();
let available = crate::plugins::discover_registry(&installed);
(200, json!({
"installed": crate::plugins::to_json(&installed),
"available": crate::plugins::to_json(&available),
"tag": crate::plugins::PLUGIN_TAG,
"api_version": crate::plugins::PLUGIN_API_VERSION,
}))
}
(Method::Post, "/refresh") => {
*st.state_ts.lock().unwrap() = now_ms();
(200, snapshot(&st))
}
(Method::Post, "/mount") => match read_body(&mut req) {
Ok(body) => handle_mount(&st, &body),
Err(e) => (400, json!({"error": e})),
},
(Method::Post, "/unmount") => match read_body(&mut req) {
Ok(body) => handle_unmount(&st, &body),
Err(e) => (400, json!({"error": e})),
},
(Method::Post, "/flash") => match read_body(&mut req) {
Ok(body) => handle_flash(&st, &body),
Err(e) => (400, json!({"error": e})),
},
(Method::Post, "/reset") => handle_device_op(&st, &mut req, "reset"),
(Method::Post, "/test") => handle_device_op(&st, &mut req, "test"),
(Method::Post, "/class-override") => handle_device_op(&st, &mut req, "class_override"),
(Method::Post, "/sniff") => handle_device_op(&st, &mut req, "sniff"),
(Method::Post, "/console") => match read_body(&mut req) {
Ok(v) => {
let mut c = st.console.lock().unwrap();
if c.len() >= CONSOLE_MAX { c.pop_front(); }
c.push_back(v);
(200, json!({"ok": true}))
}
Err(e) => (400, json!({"error": e})),
},
(Method::Get, "/console") => {
let c: Vec<Value> = st.console.lock().unwrap().iter().cloned().collect();
(200, json!({"messages": c}))
}
(Method::Post, "/eval") if st.dev => match read_body(&mut req) {
Ok(v) => {
let code = v.get("code").and_then(|v| v.as_str()).unwrap_or("").to_string();
let id = format!("ev{:x}", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_nanos());
*st.pending_eval.lock().unwrap() = Some((id.clone(), code));
(200, json!({"id": id}))
}
Err(e) => (400, json!({"error": e})),
},
(Method::Get, "/eval/pending") if st.dev => {
let p = st.pending_eval.lock().unwrap().take();
match p {
Some((id, code)) => (200, json!({"id": id, "code": code})),
None => (200, json!({})),
}
}
(Method::Post, path) if st.dev && path.starts_with("/eval/") && path.ends_with("/result") => {
let id = path.trim_start_matches("/eval/").trim_end_matches("/result").to_string();
match read_body(&mut req) {
Ok(v) => {
let r = v.get("result").cloned().unwrap_or(Value::Null);
st.eval_results.lock().unwrap().insert(id, r);
(200, json!({"ok": true}))
}
Err(e) => (400, json!({"error": e})),
}
}
(Method::Post, "/shutdown") => {
if !args.no_tab { let _ = tab::remove_webview_tab(&args.tab_name); }
let _ = req.respond(json_resp(200, json!({"ok": true, "shutdown": true})));
ok("shutdown requested, exiting");
std::process::exit(0);
}
_ => (404, json!({"error": format!("unknown endpoint: {} {}", method, path)})),
};
let _ = req.respond(json_resp(result.0, result.1));
}
Ok(())
}
fn handle_mount(st: &AppState, body: &Value) -> (u16, Value) {
let device_id = match body.get("device_id").and_then(|v| v.as_str()) {
Some(s) => s.to_string(),
None => return (400, json!({"error": "device_id required"})),
};
let target_id = match body.get("target_id").and_then(|v| v.as_str()) {
Some(s) => s.to_string(),
None => return (400, json!({"error": "target_id required"})),
};
let snap = snapshot(st);
let dev = match find_device(&snap, &device_id) {
Some(d) => d,
None => return (404, json!({"error": format!("unknown device: {}", device_id)})),
};
// Validate the target against the device's computed routing matrix.
let targets = dev.get("targets").and_then(|v| v.as_array()).cloned().unwrap_or_default();
let target = targets.iter().find(|t| t.get("host_id").and_then(|v| v.as_str()) == Some(target_id.as_str()));
let target = match target {
Some(t) => t.clone(),
None => return (400, json!({"error": format!("{} cannot be mounted on {} (not in routing matrix)", device_id, target_id)})),
};
let blocked = target.get("blocked_reason").and_then(|v| v.as_str()).unwrap_or("");
if !blocked.is_empty() {
return (409, json!({"error": blocked, "blocked": true}));
}
let mechanism = target.get("mechanism").and_then(|v| v.as_str()).unwrap_or("native");
let cmd = agent_command("mount", &dev, &target_id, mechanism);
st.mounts.lock().unwrap().insert(device_id.clone(), Some(target_id.clone()));
log_action(st, json!({
"ts": now_ms() as u64, "op": "mount", "device_id": device_id,
"device": dev.get("name"), "target": target_id, "mechanism": mechanism,
"command": cmd, "status": "applied"
}));
(200, json!({"ok": true, "device_id": device_id, "mounted_on": target_id, "mechanism": mechanism, "command": cmd}))
}
fn handle_unmount(st: &AppState, body: &Value) -> (u16, Value) {
let device_id = match body.get("device_id").and_then(|v| v.as_str()) {
Some(s) => s.to_string(),
None => return (400, json!({"error": "device_id required"})),
};
let snap = snapshot(st);
let dev = match find_device(&snap, &device_id) {
Some(d) => d,
None => return (404, json!({"error": format!("unknown device: {}", device_id)})),
};
let was = dev.get("mounted_on").and_then(|v| v.as_str()).unwrap_or("").to_string();
if was.is_empty() {
return (409, json!({"error": "device is not mounted"}));
}
let cmd = agent_command("unmount", &dev, &was, "");
st.mounts.lock().unwrap().insert(device_id.clone(), None);
log_action(st, json!({
"ts": now_ms() as u64, "op": "unmount", "device_id": device_id,
"device": dev.get("name"), "target": was, "command": cmd, "status": "applied"
}));
(200, json!({"ok": true, "device_id": device_id, "command": cmd}))
}
/// Flash a stub-firmware persona onto a device via a matching firmware plugin
/// (e.g. make an RP2040 enumerate as a HID keyboard). Dispatches to the plugin's
/// `flash` entrypoint and records the exact command.
fn handle_flash(st: &AppState, body: &Value) -> (u16, Value) {
let device_id = match body.get("device_id").and_then(|v| v.as_str()) {
Some(s) => s.to_string(),
None => return (400, json!({"error": "device_id required"})),
};
let persona = match body.get("persona").and_then(|v| v.as_str()) {
Some(s) => s.to_string(),
None => return (400, json!({"error": "persona required (see firmware_personas in /state)"})),
};
let snap = snapshot(st);
let dev = match find_device(&snap, &device_id) {
Some(d) => d,
None => return (404, json!({"error": format!("unknown device: {}", device_id)})),
};
let class = dev.get("class").and_then(|v| v.as_str()).unwrap_or("");
let vid = dev.get("vid").and_then(|v| v.as_u64()).unwrap_or(0) as u16;
let pid = dev.get("pid").and_then(|v| v.as_u64()).unwrap_or(0) as u16;
let bus = dev.get("bus_id").and_then(|v| v.as_str()).unwrap_or("?");
let installed = crate::plugins::discover_installed();
let fw = match crate::plugins::firmware_for(class, vid, pid, &installed) {
Some(p) => p,
None => return (409, json!({"error": "no firmware device-test pack installed for this chip", "blocked": true,
"hint": "search the wiki: adom-usb plugins search"})),
};
// Validate the persona exists in the pack's catalog.
let known = fw.personas.as_array().map(|a| a.iter()
.any(|p| p.get("id").and_then(|x| x.as_str()) == Some(persona.as_str()))).unwrap_or(false);
if !known {
return (400, json!({"error": format!("unknown persona '{}' for {}", persona, fw.slug),
"available": fw.personas.clone()}));
}
let cmd = format!("[{src}] {ep} flash --persona {persona} --vid {vid:04x} --pid {pid:04x} --busid {bus} --target {src}",
src = dev.get("source_id").and_then(|v| v.as_str()).unwrap_or("?"),
ep = fw.entrypoint, persona = persona, vid = vid, pid = pid, bus = bus);
log_action(st, json!({
"ts": now_ms() as u64, "op": "flash", "device_id": device_id,
"device": dev.get("name"), "persona": persona, "plugin": fw.slug.clone(),
"command": cmd, "status": "queued"
}));
(200, json!({"ok": true, "op": "flash", "device_id": device_id, "persona": persona,
"plugin": fw.slug.clone(), "method": fw.flash_method.clone(), "command": cmd, "status": "queued",
"note": "after flashing, the device re-enumerates as the persona's USB class — refresh to see it"}))
}
fn handle_device_op(st: &AppState, req: &mut tiny_http::Request, op: &str) -> (u16, Value) {
let body = match read_body(req) {
Ok(b) => b,
Err(e) => return (400, json!({"error": e})),
};
let device_id = match body.get("device_id").and_then(|v| v.as_str()) {
Some(s) => s.to_string(),
None => return (400, json!({"error": "device_id required"})),
};
let snap = snapshot(st);
let dev = match find_device(&snap, &device_id) {
Some(d) => d,
None => return (404, json!({"error": format!("unknown device: {}", device_id)})),
};
let caps = dev.get("capabilities").cloned().unwrap_or(json!({}));
let allowed = caps.get(op).and_then(|v| v.as_bool()).unwrap_or(false);
if !allowed {
let reason = caps.get("reason").and_then(|v| v.as_str())
.unwrap_or("not available for this device in its current state");
return (409, json!({"error": reason, "blocked": true}));
}
let target = dev.get("mounted_on").and_then(|v| v.as_str()).unwrap_or("");
// For sniff, prefer a matching installed plugin's entrypoint; otherwise the
// built-in usbmon capture. This is the extension seam the ecosystem hooks.
let (cmd, plugin) = if op == "sniff" {
let class = dev.get("class").and_then(|v| v.as_str()).unwrap_or("");
let vid = dev.get("vid").and_then(|v| v.as_u64()).unwrap_or(0) as u16;
let pid = dev.get("pid").and_then(|v| v.as_u64()).unwrap_or(0) as u16;
let bus = dev.get("bus_id").and_then(|v| v.as_str()).unwrap_or("?");
let installed = crate::plugins::discover_installed();
match crate::plugins::match_for(class, vid, pid, &installed) {
Some(p) => (
format!("[{target}] {} sniff --vid {:04x} --pid {:04x} --busid {} --target {} --out <pcap>",
p.entrypoint, vid, pid, bus, target),
Some(p.slug.clone()),
),
None => (agent_command(op, &dev, target, ""), None),
}
} else {
(agent_command(op, &dev, target, ""), None)
};
log_action(st, json!({
"ts": now_ms() as u64, "op": op, "device_id": device_id,
"device": dev.get("name"), "target": target, "command": cmd, "plugin": plugin,
"status": "queued" // executes once the live agent for `target` is wired
}));
(200, json!({"ok": true, "op": op, "device_id": device_id, "command": cmd, "plugin": plugin, "status": "queued"}))
}