Guidance for an AI agent (or new human maintainer) working on this repo. It captures architecture, conventions, and the non-obvious lessons that are easy to regress.
A Rust native launcher/manager for OpenC3 COSMOS. Two faces from one binary:
- a GUI (iced 0.13, the
tiny-skiasoftware renderer — no GPU) for setup, starting/stopping COSMOS, container status, logs, and the bridge; and - a CLI (
clap) with subcommands (run,stop,cleanup,cli,util,microservices,bridgeenroll, …).
Its headline feature is the Iroh bridge: it runs COSMOS interfaces that need host hardware (serial ports, USB, etc.) outside Docker on the host, tunneling their bytes to COSMOS running in containers.
Default features include gui. Build with --no-default-features for a smaller
headless CLI-only binary.
cargo build --features gui # GUI build (default)
cargo build --no-default-features # headless CLI only (no iced/rfd/image/fs4)
cargo clippy --features gui # lint GUI; treat warnings as errors mentally
cargo clippy # default == gui feature on
cargo test --features gui # unit testsgui = ["dep:iced", "dep:rfd", "dep:image", "dep:fs4"]. Many functions are gated#[cfg_attr(not(feature = "gui"), allow(dead_code))]because they're only called from the GUI — keep that annotation when adding GUI-only helpers, or the headless build warns.- Keep clippy clean on both feature sets. A change that's clean under
guican warn under--no-default-features(e.g. a helper that becomes unused), and vice-versa. Also check platform-gating: a fn used only under#[cfg(target_os = "linux")]will look dead on macOS (seeprocess::stdout_string, annotated#[cfg_attr(not(target_os="linux"), allow(dead_code))]).
The Python half of the bridge lives in the OpenC3 COSMOS core repo, NOT here:
openc3/python/openc3/microservices/bridge_microservice.py— the Iroh hub.openc3/python/openc3/microservices/host_interface_microservice.py— the host-side runner this app spawns (opens the real device).openc3/python/openc3/interfaces/bridge_interface.py— the COSMOS-sideInterfacethat tunnels raw bytes.openc3/python/openc3/models/host_microservice_model.pyandopenc3/lib/openc3/models/interface_model.rb(Ruby) — theBRIDGE/BRIDGE_OPTION/BRIDGE_PROTOCOLconfig and theHostMicroserviceModelthe app polls overapi/host_microservices.
Any change to the bridge wire protocol (ALPNs, PRIME/READY/GO handshake, control-channel JSON, the host-microservices/interface-status APIs) must be made in lockstep across this repo and the COSMOS core repo. They were developed together; the Rust side here is only the middle relay + host-process supervisor.
main.rs— entry; CLI dispatch;windows_subsystem="windows";ensure_tool_path().cli.rs— clap command definitions.commands.rs— CLI command implementations (run,stop,cleanup, …).context.rs—Context/Paths(root resolution, runtime detection, docker engine installed/running checks).docker.rs—docker composeinvocation + the Linux docker-group re-exec (sg docker).dockerapi.rs— Docker Engine API over the socket/pipe via bollard (status + stats).monitor.rs— container status snapshot (bollard, CLI fallback), health, totals.install.rs— installers (Docker, Python/uv, COSMOS env), Windows features, virtualization check, the notifier/dialog/progress plumbing.enroll.rs— the app's Iroh identity + bridge enrollment (auto via local Dockerbridgeenroll, or a manual token). Caches the hub ticket.bridge.rs—BridgeClient(talks to the hub: files, log, authorize, interface-status APIs).operator.rs— supervises host-interface microservices; drives bridge auto-enroll; publishes status to the GUI.hostfiles.rs— mirrors scope plugin files to the host so host interfaces can import custom code; fingerprints venv inputs.gui.rs— the entire iced app (pages: Splash/Install/Main, modals, tray).tray.rs— system-tray/menu-bar icon + Dock-visibility (macOS).single_instance.rs— single-instance guard (file lock) + "show existing window".settings.rs— persisted settings (openc3-cosmos-app-settings.json).process.rs— subprocess helpers (run,capture,run_streamed,no_window).download.rs— curl/wget wrappers.env_file.rs,logging.rs,util.rs— support.
device <-> host_interface_microservice (host, spawned by this app's operator)
<--host/<name>--> bridge_microservice (hub, in COSMOS Docker)
<--stream/<name>--> bridge_interface (COSMOS Interface, in Docker)
- ALPNs:
stream/<name>(COSMOS data leg),host/<name>(host data leg),ctrl/<name>+hostctrl/<name>(control),api/*(control APIs: host_microservices, log, authorize, enroll, files, interface_status). - Pairing: the hub rendezvous-pairs the two legs by name. It opens+primes
each bi-stream with a 1-byte
PRIMEimmediately on arrival (soaccept_bi()returning is not proof the peer is present — see handshake). - READY/GO handshake (in
bridge_interface.py/host_interface_microservice.py): after pairing, the host sendsREADY(it's up), COSMOS repliesGO; only then does the host open the device. This enforces: COSMOS won't report "connected" until the host is ready, and the host won't touch hardware until COSMOS is up. - Control channel: COSMOS sends connect/disconnect (coalesced on the host to
the net final state to avoid replaying a backlog); the host pushes live
InterfaceStatusup, surfaced in CmdTlmServer and openc3-cosmos-app. - BRIDGE_PROTOCOL: protocols declared
BRIDGE_PROTOCOLrun on the host (next to the device); plainPROTOCOLs run in COSMOS onbridge_interface. - Host errors → host disconnects and parks (does not auto-reconnect); COSMOS is authoritative and re-drives the connection.
This project's primary dev machine is macOS with Homebrew-managed Rust (no
rustup), so you cannot cross-compile the Windows target here. macOS code
paths (incl. objc2/tray) are compile/run-testable; Windows and Linux paths
are review-verified only — call that out and lean on the standard APIs.
- Every subprocess must be windowless. Route through
process::{run, capture, run_streamed}(they callno_window()), or callprocess::no_window(&mut cmd)before spawn. A bareCommandflashes a console window — this bitprobe_cosmos(a polledcurl). - Exe icon:
build.rsembedsassets/icons/icon.icoviawinresource(a Windows-host build-dep) so Explorer/taskbar/shortcuts show the logo. - Static CRT:
.cargo/config.tomlsets+crt-staticso the exe doesn't needVCRUNTIME140.dll. - PATH:
ensure_tool_path()(main.rs) prepends Docker Desktop'sresources\binto PATH at startup unconditionally — Docker's system-PATH update isn't seen by an already-running process, so first-run detection would otherwise fail until restart. - Optional features: checked via the
Win32_OptionalFeatureWMI class (readable without admin — no UAC), enabled via an elevated PowerShell (Start-Process -Verb RunAs) that runsEnable-WindowsOptionalFeature. A restart prompt follows. BIOS virtualization is detected viaHypervisorPresent/VirtualizationFirmwareEnabled(detect-and-inform only — can't be enabled in software). wsl --updateruns before starting Docker.- Installer is WiX
.msionly; code-signed via Azure Trusted Signing (SmartScreen). Product nameOpenC3_COSMOS(no spaces).
- Docker Desktop installs from the official DMG (not Homebrew — brew's cask
shells out to
sudowith no TTY and trips on existing credential helpers). - Tray + Dock: hiding to the tray switches the app to
NSApplicationActivationPolicy::Accessory(no Dock icon) viaobjc2; showing restoresRegular. GOTCHA:setActivationPolicy:returnsBOOL, so themsg_send!must declare aboolreturn — declaring()aborts at runtime with "expected 'B', found 'v'" (objc2 verifies encodings). Seetray.rs. - DMG is notarized in CI on the final artifact (cargo-packager's inline
notarytool submit --waitis silent/unbounded and hung — do notarization ourselves with a timeout+staple).
- No tray (
tray::ENABLED == cfg!(any(windows, macos))). So the window close (X) shows a "Quit?" confirmation modal instead of hiding. - Docker group: joining prefers
pkexec(graphical polkit prompt — works with no terminal) over a TTY-lesssudo; on failure it pops up the exact manual commands (groupadd/usermod -aG docker $USER/newgrp docker). In-session it re-execs viasg dockerso Docker works without a re-login.
- User messaging (
install.rs):install::progress(msg)/notify(...)routes to the GUI activity log (when a notifier is set) or stdout (CLI).notify_dialog(msg)additionally pops a dismissible modal in the GUI (used for post-install NEXT STEPS). Long-running output (e.g.docker composeimage pulls) is streamed live viaprocess::run_streamed(cmd, |line| install::progress(line)), and the GUI shows a spinner + latest line whilebusy. - Single instance (
single_instance.rs): an advisory file lock on<root>/openc3-cosmos-app.lock(chosen over a fixed TCP port — no collision, OS releases on crash). A second launch drops anopenc3-cosmos-app.showmarker and exits; the running app polls it and raises its window. Fail-open if locking errors. - Self-update (
update.rs, GUI-only): a background thread checks the GitHub Releases API (DEFAULT_REPO, overridable viaOPENC3_COSMOS_APP_GITHUB_REPO) on startup and every 8h, storing the latestReleasein a shared slot. The Main page's tick surfaces an Update available modal when the release is newer thanenv!("CARGO_PKG_VERSION")and isn't thesettings.skipped_version. Install downloads the platform/arch installer asset and launches it (opendmg /startmsi /xdg-opendeb), then cleanly shuts down the app so the installer can replace it. It falls back to opening the release page (without quitting) when no native asset matches; Skip this version persists to settings; Later dismisses until the next check. The Settings page also has a Check for updates now button (update::check_now→CheckOutcome), which reports even when up-to-date/failed (via the dialog popup) and ignores the skip preference. Release tags arev<semver>—strip_tagnormalizes them; keepDEFAULT_REPOin sync with the actual repo path. - COSMOS self-update (
update.rs+commands::upgrade_cosmos, GUI-only): a parallel background thread (spawn_cosmos_checker, startup + 8h) checks the tags ofcosmos-project(Core, GitHub) orcosmos-enterprise-project(Enterprise, Forgejorepos.openc3.com— needs the enterprise token) viacosmos_latest, comparing the highest semver tag against the installedOPENC3_TAGread fromcosmos/.env(installed_cosmos_version). No-op in dev mode or when COSMOS isn't installed. A newer, non-cosmos_skipped_versionrelease surfaces a COSMOS update available modal on Main (one modal at a time — the app-update prompt takes precedence). Update runscommands::upgrade_cosmos: read.env.local+compose.override.yamlinto memory →stop→remove_dir_all(cosmos)→install::cosmos(tag)(fresh) → restore those two files →run. Data safety: telemetry/config lives in Docker volumes (untouched by a folder replace); user config/secrets live in.env.local/compose.override.yaml(COSMOS 7.3.0+ convention, explicitly preserved). The Check for updates now button covers both app and COSMOS (check_now(env_path, enterprise, token)→CheckOutcome::AppUpdate|CosmosUpdate|UpToDate|Failed, app checked first). - Tray icon: a purpose-built "COS/MOS" badge (
assets/tray.png, generated bytools/gen_tray_icon.py). Do NOT use the downscaled app logo (blurry at 16px) or hand-drawn Unicode triangles (tofu on Windows). Disclosure carets in the Container-Status header come from the embedded icon font (assets/openc3-icons.ttf,tools/gen_icons.py) for the same reason. - iced performance: the software renderer is very slow in debug builds —
[profile.dev.package."*"] opt-level = 3in Cargo.toml is the real fix. Don't add per-frame fast-tick / poll-skip band-aids to "speed up" the UI. - Container status/stats: via the Docker socket (bollard,
dockerapi.rs);monitor.rsfalls back todocker compose ps/statsif the socket is unreachable. Health: a one-shot container that exited 0 (e.g.openc3-cosmos-init) counts as healthy — seeContainerStatus::is_healthy. - Settings (
settings.rs):openc3-cosmos-app-settings.jsonin the app root (gitignored — contains the enterprise access token; rotate if it leaks). Includescosmos_url,run_locally,edition,enterprise_token,dev_mode,dev_folder. - Editions: Core (github
cosmos-project) vs Enterprise (private Forgejo atrepos.openc3.com, needs an access token sent as anAuthorization: tokenheader). - Run-locally = false: don't offer Docker/COSMOS installs or start/stop; only the host Python runtime is needed (for the bridge microservices); "Open in Browser" uses the configured COSMOS URL.
- Development Mode: sets
OPENC3_TAG/OPENC3_ENTERPRISE_TAG=latestandOPENC3_DEVEL(editable openc3 install in host venvs), and uses a chosen dev folder'sopenc3.sh/compose.yaml. Toggling it restarts the operator and re-enrolls the bridge against the new compose context (clears an auto-enrolled ticket; a manual token is preserved). Seesingle_instance-adjacent logic ingui.rsandenroll::forget_cached_ticket.
target/, dist/, python/ (venv), cosmos/ (COSMOS install), bin/,
bridge/ (contains identity.key — a private key), host_files/,
microservices/ (host-microservice working dirs), openc3-cosmos-app-settings.json,
*.profile.json.gz, .DS_Store. See .gitignore.
.github/workflows/openc3-cosmos-app-release.yml builds native installers on per-OS
runners (dmg on macOS, deb+appimage on Linux, WiX msi on Windows) plus a Linux
.tar.gz. workflow_dispatch builds artifacts only; pushing a v*
tag additionally attaches them to a GitHub Release. It was moved from the COSMOS
monorepo and de-monorepo'd (no working-directory: openc3-cosmos-app, artifact paths are
repo-root-relative). Required repo secrets for signed/notarized output (all
optional — absent → unsigned build):
- macOS:
APPLE_CERTIFICATE,APPLE_CERTIFICATE_PASSWORD,APPLE_SIGNING_IDENTITY,APPLE_API_KEY,APPLE_API_ISSUER,APPLE_API_KEY_P8. - Windows:
AZURE_TS_ENDPOINT,AZURE_TS_ACCOUNT,AZURE_TS_PROFILE,AZURE_TENANT_ID,AZURE_CLIENT_ID,AZURE_CLIENT_SECRET.
- No
remoteis configured yet — push when ready. - Several bridge/GUI features are live-untested on Windows/Linux (see the testing caveat); prefer verifying on real hardware before shipping.