Skip to content

Merge rocm-dash into rocm-cli: unified TUI + chat OAuth + dash demo/replay - #16

Merged
michaelroy-amd merged 1 commit into
mainfrom
rocm-dash-merge
Jun 17, 2026
Merged

Merge rocm-dash into rocm-cli: unified TUI + chat OAuth + dash demo/replay#16
michaelroy-amd merged 1 commit into
mainfrom
rocm-dash-merge

Conversation

@michaelroy-amd

Copy link
Copy Markdown
Member

What & Why

This PR folds the standalone rocm-dash telemetry dashboard into the rocm binary as a unified TUI, adds a no-key ChatGPT-OAuth chat backend, and aligns install/onboarding with the pipwheel format rename.

Today rocm-dash lives as a separate project, duplicating config, transport, and engine-discovery logic that the rocm CLI already owns. Merging it gives users a single binary (rocm dash) with embedded telemetry, removes the duplication, and lets the dashboard reuse the CLI's serve/model/engine plumbing.

What's included

  • Dashboard substrate — four new workspace crates:
    • rocm-dash-core — pure reducer, config, metrics, bench rollup/schema, VRAM/partition/efficiency models, protocol, persistence.
    • rocm-dash-collectors — bollard Docker collector, vLLM (Prometheus + log) and Lemonade scrapers, amd-smi/sysfs/cgroup/proc host collectors, engine registry.
    • rocm-dash-daemon — daemon server, runner, registry, snapshot/bench rings, demo session generator, persistence.
    • rocm-dash-tui — Ratatui front end with Overview / Hardware / Instances / Bench / Chat tabs.
  • Unified TUI screens on shared primitives: serve wizard, model picker, engine manager, doctor, update, install, logs, onboarding, automations, command, config/provider, runtime manager, services manager.
  • rocm dash launch verb with embedded-daemon auto-start and unified config.json (with legacy config.toml migration).
  • Chat backend — no-key ChatGPT OAuth path (agent.rs); chat api_key is ENV-only (the OAuth path takes no key).
  • rocm dash flags--demo (deterministic synthetic session; no GPU/daemon needed, ideal for CI and dev boxes), --replay <FILE>, and --chat-mock.
  • pip→wheel sweep — install/onboarding screens and the chat system prompt/validation now emit --format wheel.
  • Windows-clean build — the unix-socket daemon/TUI transport is #[cfg(unix)]-gated with graceful Windows stubs; two sh-spawning tests are gated. The live dashboard is unix-only for now; --demo/--replay work on Windows. Verified on a real MSVC build (0 errors / 0 warnings) plus a windows-gnu cross-check.
  • Docs + ROADMAP.md — documentation aligned to the merged state; ROADMAP captures the path to retire the legacy chat-first TUI and the per-process VRAM → model item.

Architecture invariants upheld

  • rocm-dash-core carries no tokio/ratatui/rocm-core types at the type boundary; the reducer is pure (State::apply → Vec<SideEffect>).
  • Chat api_key is ENV-only — never read from TOML, CLI flags, source, or logs (the OAuth path takes no key).
  • bollard lives only in the collectors crate; the ureq/reqwest HTTP partition is left intact.
  • Engine ports come from the engine registry (single port authority), not hardcoded at call sites.
  • agent.rs is the only file that names rig types.

Testing

  • cargo build --workspace --all-targets — clean.
  • cargo test --workspace -- --test-threads=1 — 1581 passed, 0 failed.
  • cargo clippy -p rocm-dash-tui --all-targets -- -D warnings — clean; workspace clippy clean.
  • cargo fmt --all --check — clean.
  • CI adds fmt, clippy, coverage (llvm-cov gate), build-and-test, and a Windows build job.

Under default parallel cargo test, two legacy chat tests flake on headless contention but pass in isolation, which is why CI and the gate run single-threaded.

Merge note

The branch is pre-squashed to a single signed commit, so any merge method (merge, squash, or rebase) yields exactly one clean commit on main.

@michaelroy-amd

Copy link
Copy Markdown
Member Author

ready to go!

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

rominf
rominf previously requested changes Jun 17, 2026

@rominf rominf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Architecture is solid and engineering quality is high — the crate layering is clean, the api-key-from-env invariant is structurally enforced, and demo determinism is real. Requesting changes on a handful of objective issues before merging; inline comments mark each one.

Blocking

  1. Daemon socket security_token is silently ignored, no socket permissions set after bind, and the default path is world-writable /tmp/rocmdashd.sock. Any local user on a multi-user host can subscribe to all telemetry data.
  2. #![allow(dead_code)] on all four new crates — these are now landing in full; a crate-wide suppression hides real dead code and all future rot.
  3. UTF-8 byte-slice panic in overview.rs&model[..20] / &r.cell[..10] panic on any multibyte char straddling the boundary; trunc() (char-aware, already imported) is the fix.
  4. Dead module monitor — declared pub mod monitor in ui/mod.rs but the file contains only a TODO and is referenced nowhere.
  5. Duration::from_secs_f64 panic on hostile/typo config — negative or NaN values panic rather than returning a config error.

Non-blocking

  • LlmConfig derives Debug with api_key: Option<String> in the clear — safe today but a future tracing::debug!(?cfg) would leak it.
  • ChatGPT OAuth token cache written without explicit .token_dir() — lands in ~/.config/chatgpt/auth.json world-readable by default.

Positive signals

  • api-key-from-env invariant is structurally enforced (single ingestion point, key never reaches wire/persist/replay format — verified at protocol level). ✅
  • All six chat agent tools are read-only; mutating actions require the approval modal. ✅
  • demo.rs determinism is real (fixed wall-clock anchor, seeded RNG, byte-identical test). ✅
  • Config migration is one-shot, non-clobbering, and race-benign. ✅
  • cfg(unix) gating fails cleanly (explicit Err) everywhere it appears. ✅

@rominf
rominf force-pushed the rocm-dash-merge branch from 006b624 to a6dd6ae Compare June 17, 2026 09:01
@rominf
rominf dismissed their stale review June 17, 2026 09:02

Review superseded — internal references removed from branch and re-reviewed.

Fold the standalone rocm-dash telemetry/monitoring app into rocm-cli so
that one binary provides the CLI, an interactive TUI, an embedded
telemetry daemon, and an LLM chat assistant. `rocm dash` launches the
dashboard; `rocm serve <model> --managed` surfaces live gen_tps in it
end-to-end.

- Unified TUI screens on a shared, pure-reducer base with an async
  job-bridge and approval gate: services, serve wizard, engine manager,
  model picker, doctor, update, install, logs, runtime manager,
  onboarding, automations, freeform command, and config/provider.
- Telemetry daemon + collectors (amd-smi, sysfs/cgroup, Docker, vLLM
  Prometheus/log, lemonade, llama.cpp slots, bench tail) feeding a
  snapshot/bench ring with persistence and a demo/replay engine.
- Managed-service engine registry -> scrape-target seam: the CLI's
  service records drive collector discovery, with the registry as the
  authoritative port source for managed services.
- Config unification: a dashboard sub-config nested under the rocm-cli
  config plus a one-shot legacy TOML -> JSON migration onto ~/.rocm.
- No-key ChatGPT OAuth chat backend (rig-core native provider, device-
  code login) behind the swappable agent seam; chat API key stays
  environment-only across every surface.
- `rocm dash --demo/--replay/--chat-mock` for GPU-less and offline use,
  fully cross-platform including Windows.
- Edition-2024 migration of the new crates and a ratcheted llvm-cov
  coverage gate scoped to them.

Notable: live dashboard socket is Unix-only for now (--demo/--replay
also run on Windows); the legacy assistant TUI remains until feature
parity is reached.
@rominf
rominf force-pushed the rocm-dash-merge branch from a6dd6ae to 3517967 Compare June 17, 2026 09:04

@rominf rominf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restoring inline review comments after cleanup pass that removed internal references from the branch. Blocking issues below are still open; the non-blocking internal-ref items from the prior review have been resolved on the branch.

/// Benchmark-row history kept for late-joining clients (matches TUI `BENCH_CAP`).
const BENCH_RING_CAP: usize = 200;

pub async fn run(listen: &str, _token: Option<&str>, opts: RunnerOptions) -> anyhow::Result<()> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[BLOCKING — security] _token is silently discarded; Command::Hello also discards the client token (~line 139). The DashboardConfig.daemon.token field and the protocol Hello.token field exist — auth was clearly intended. Either enforce it (constant-time compare via the subtle crate) or remove the parameter and document that FS permissions are the ACL.

if path.exists() {
std::fs::remove_file(&path).with_context(|| format!("removing stale socket {path:?}"))?;
}
let listener = UnixListener::bind(&path).with_context(|| format!("binding {path:?}"))?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[BLOCKING — security] No set_permissions after UnixListener::bind. Combined with the /tmp/rocmdashd.sock default path (world-writable, predictable), any local user on a multi-user host can connect and receive all telemetry. Add std::fs::set_permissions(&path, Permissions::from_mode(0o600))? immediately after this line, or create the parent dir at 0o700. Consider moving the default socket path under paths.telemetry_state_dir() (already ~/.rocm/data/telemetry/, user-owned) instead of /tmp.

// config keeps its in-place `&mut` mutation convention untouched.

fn default_dashboard_listen() -> String {
"unix:/tmp/rocmdashd.sock".to_owned()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[BLOCKING — security] World-writable, predictable socket path. Any local user can squat this path before the daemon starts (TOCTOU) or connect to it directly. Move the default to a per-user directory, e.g. paths.telemetry_state_dir().join("rocmdashd.sock")~/.rocm/data/telemetry/ is already user-owned by construction.

//!
//! See `../wiki/concepts/tea-reducer-pattern.md` for the architectural pattern.

#![allow(dead_code)] // scaffold; remove as modules flesh out

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[BLOCKING] Crate-wide #![allow(dead_code)] on all four new crates hides real dead code (e.g. unused ring helpers, Backoff::reset) and will hide all future rot. These crates are landing in full — the scaffold comment no longer applies. Remove this attribute and annotate any genuinely-intentional unused items individually with #[allow(dead_code)] + a reason.

//! Every collector implements one of the traits in `rocm_dash_core::traits`.
//! Stubs return `CollectorError::Unsupported` so the daemon can start with nothing wired.

#![allow(dead_code)]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[BLOCKING] Same as rocm-dash-core: remove crate-wide #![allow(dead_code)]. Annotate individual intentional unused items instead.

model
};
let cell = if r.cell.len() > 10 {
&r.cell[..10]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[BLOCKING — panic] Same: &r.cell[..10] panics on multibyte char boundaries. Use trunc(&r.cell, 10) instead.

pub mod logs_view;
pub mod modal;
pub mod model_picker;
pub mod monitor;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[BLOCKING — dead code] monitor.rs contains only a doc-comment and a TODO — no draw, no on_key, no public API — and is referenced nowhere in the codebase. Delete the file and this declaration until the module is implemented; shipping an empty stub misleads future readers about what's actually available.


pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Duration, D::Error> {
let secs = f64::deserialize(d)?;
Ok(Duration::from_secs_f64(secs))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[BLOCKING] Duration::from_secs_f64 panics on negative, NaN, or overflow input. A config file with gpu_tick = -1 or gpu_tick = NaN panics at startup instead of producing a user-visible error. Validate before converting:

pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Duration, D::Error> {
    let secs = f64::deserialize(d)?;
    if !secs.is_finite() || secs < 0.0 {
        return Err(serde::de::Error::custom(
            format!("duration must be a non-negative finite number, got {secs}")
        ));
    }
    Ok(Duration::from_secs_f64(secs))
}


/// Fully-resolved chat endpoint configuration. `api_key` is sourced from the
/// environment only — never from TOML/CLI/source (see `main.rs`).
#[derive(Debug, Clone, PartialEq, Eq)]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Non-blocking] LlmConfig derives Debug with api_key: Option<String> in the clear. No logging happens today (the env-only invariant holds), but a future tracing::debug!(?cfg) would silently leak the key. Consider implementing a redacting Debug or wrapping with secrecy::Secret<String>.

// module is private, so we let inference name it); its `verification_uri`
// and `user_code` fields are public.
let client = chatgpt::Client::builder()
.oauth()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Non-blocking] .oauth() without .token_dir(...) writes the OAuth token cache to ~/.config/chatgpt/auth.json at the process umask (typically 0o644, world-readable). Point the dir at a rocm-owned location created with mode 0o700 so the plaintext access/refresh tokens are not readable by other local users.

@rominf rominf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merging as is to unblock others. I will fix all the issues in the follow-up PR.

@michaelroy-amd
michaelroy-amd added this pull request to the merge queue Jun 17, 2026
Merged via the queue into main with commit 81386c8 Jun 17, 2026
6 checks passed
@rominf
rominf deleted the rocm-dash-merge branch July 22, 2026 10:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants