From 09560471a4dd3f39c1c23ef3a3c3a2dfb77e3a73 Mon Sep 17 00:00:00 2001 From: cjharriskc-ai Date: Tue, 4 Aug 2026 02:57:39 -0500 Subject: [PATCH] Ship recovery-first automatic fabric 0.3.1 --- Cargo.lock | 30 +- Cargo.toml | 2 +- README.md | 62 ++-- apps/desktop/package.json | 2 +- apps/desktop/src-tauri/Cargo.toml | 2 +- apps/desktop/src-tauri/src/lib.rs | 305 +++++++++++++++++- apps/desktop/src-tauri/tauri.conf.json | 2 +- apps/desktop/src/App.test.tsx | 6 +- apps/desktop/src/App.tsx | 5 +- apps/desktop/src/RecoveryCenter.test.tsx | 82 +++++ .../src/components/ComputeStrategyPanel.tsx | 81 ++--- apps/desktop/src/components/Onboarding.tsx | 4 +- .../desktop/src/components/RecoveryCenter.tsx | 97 ++++++ apps/desktop/src/store.ts | 181 ++++++++++- apps/desktop/src/styles.css | 4 + apps/edge/package.json | 2 +- apps/edge/src-tauri/tauri.conf.json | 2 +- crates/rampage-controller/src/main.rs | 157 ++++++++- crates/rampage-sdk/src/lib.rs | 14 +- docs/PLATFORM_MATRIX.md | 4 +- docs/RELEASE_EVIDENCE_0.3.1.md | 46 +++ docs/RELEASE_NOTES_0.3.1.md | 56 ++++ docs/SHA256SUMS-0.3.1.txt | 3 + docs/UNIVERSAL_FABRIC_BLUEPRINT.md | 192 +++++++++++ docs/assets/rampage-recovery-center.png | Bin 0 -> 160945 bytes docs/download/index.html | 8 +- docs/index.html | 27 +- packages/sdk-python/pyproject.toml | 2 +- .../sdk-python/src/rampage_sdk/__init__.py | 15 +- packages/sdk-python/tests/test_client.py | 27 ++ packages/sdk-python/uv.lock | 2 +- packages/sdk-ts/package.json | 2 +- packages/sdk-ts/src/index.test.ts | 17 + packages/sdk-ts/src/index.ts | 31 ++ scripts/Qualify-RampageRemoteAssist.ps1 | 8 +- scripts/Smoke-RampageInstaller.ps1 | 2 +- scripts/Stage-RampageDistribution.ps1 | 17 +- services/intelligence/pyproject.toml | 2 +- services/intelligence/uv.lock | 2 +- 39 files changed, 1366 insertions(+), 137 deletions(-) create mode 100644 apps/desktop/src/RecoveryCenter.test.tsx create mode 100644 apps/desktop/src/components/RecoveryCenter.tsx create mode 100644 docs/RELEASE_EVIDENCE_0.3.1.md create mode 100644 docs/RELEASE_NOTES_0.3.1.md create mode 100644 docs/SHA256SUMS-0.3.1.txt create mode 100644 docs/UNIVERSAL_FABRIC_BLUEPRINT.md create mode 100644 docs/assets/rampage-recovery-center.png diff --git a/Cargo.lock b/Cargo.lock index 7c42c71..41d03ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4653,7 +4653,7 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rampage-agent" -version = "0.3.0" +version = "0.3.1" dependencies = [ "anyhow", "axum", @@ -4683,7 +4683,7 @@ dependencies = [ [[package]] name = "rampage-cli" -version = "0.3.0" +version = "0.3.1" dependencies = [ "anyhow", "base64 0.22.1", @@ -4699,7 +4699,7 @@ dependencies = [ [[package]] name = "rampage-controller" -version = "0.3.0" +version = "0.3.1" dependencies = [ "anyhow", "axum", @@ -4731,7 +4731,7 @@ dependencies = [ [[package]] name = "rampage-desktop" -version = "0.3.0" +version = "0.3.1" dependencies = [ "aes-gcm 0.11.0", "base64 0.22.1", @@ -4757,7 +4757,7 @@ dependencies = [ [[package]] name = "rampage-edge" -version = "0.3.0" +version = "0.3.1" dependencies = [ "anyhow", "chrono", @@ -4779,7 +4779,7 @@ dependencies = [ [[package]] name = "rampage-edge-app" -version = "0.3.0" +version = "0.3.1" dependencies = [ "rampage-edge", "serde", @@ -4791,7 +4791,7 @@ dependencies = [ [[package]] name = "rampage-ledger" -version = "0.3.0" +version = "0.3.1" dependencies = [ "chrono", "hex", @@ -4804,7 +4804,7 @@ dependencies = [ [[package]] name = "rampage-mesh" -version = "0.3.0" +version = "0.3.1" dependencies = [ "anyhow", "chrono", @@ -4823,7 +4823,7 @@ dependencies = [ [[package]] name = "rampage-policy" -version = "0.3.0" +version = "0.3.1" dependencies = [ "chrono", "ed25519-dalek 2.2.0", @@ -4839,7 +4839,7 @@ dependencies = [ [[package]] name = "rampage-project" -version = "0.3.0" +version = "0.3.1" dependencies = [ "anyhow", "chrono", @@ -4852,7 +4852,7 @@ dependencies = [ [[package]] name = "rampage-protocol" -version = "0.3.0" +version = "0.3.1" dependencies = [ "chrono", "serde", @@ -4863,7 +4863,7 @@ dependencies = [ [[package]] name = "rampage-relay" -version = "0.3.0" +version = "0.3.1" dependencies = [ "anyhow", "chrono", @@ -4887,7 +4887,7 @@ dependencies = [ [[package]] name = "rampage-sdk" -version = "0.3.0" +version = "0.3.1" dependencies = [ "anyhow", "base64 0.22.1", @@ -4901,7 +4901,7 @@ dependencies = [ [[package]] name = "rampage-storage" -version = "0.3.0" +version = "0.3.1" dependencies = [ "aes-gcm 0.11.0", "chrono", @@ -6366,7 +6366,7 @@ dependencies = [ [[package]] name = "tauri-plugin-rampage-edge" -version = "0.3.0" +version = "0.3.1" dependencies = [ "serde", "tauri", diff --git a/Cargo.toml b/Cargo.toml index 82e7f81..b778ce3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ members = [ ] [workspace.package] -version = "0.3.0" +version = "0.3.1" edition = "2024" rust-version = "1.91" authors = ["ObtuseAI"] diff --git a/README.md b/README.md index e837dcb..1ee48cb 100644 --- a/README.md +++ b/README.md @@ -18,17 +18,18 @@ the authority to run wild. [![Rust 1.91](https://img.shields.io/badge/Rust-1.91%2B-0b0f17?logo=rust&logoColor=67f5c5)](Cargo.toml) [![AI authority](https://img.shields.io/badge/AI%20authority-proposals%20only-0b0f17?logo=probot&logoColor=67f5c5)](docs/ARCHITECTURE.md#recursive-improvement) -## [⬇ Download Rampage 0.3.0 for Windows](https://obtuseai.github.io/rampage/download/) +## [⬇ Download Rampage 0.3.1 for Windows](https://obtuseai.github.io/rampage/download/) **One installer. Desktop shortcut included.** -[Release notes and SHA-256](https://github.com/ObtuseAI/rampage/releases/tag/v0.3.0-remote-assist.1) · +[Release notes and SHA-256](docs/RELEASE_NOTES_0.3.1.md) · [All releases](https://github.com/ObtuseAI/rampage/releases) [**Launch the showcase**](https://obtuseai.github.io/rampage/) · [Architecture](docs/ARCHITECTURE.md) · +[Universal fabric blueprint](docs/UNIVERSAL_FABRIC_BLUEPRINT.md) · [Easy pairing](docs/PAIRING.md) · [Security](SECURITY.md) · -[0.3.0 release evidence](docs/RELEASE_EVIDENCE_0.3.0.md) · +[0.3.1 release evidence](docs/RELEASE_EVIDENCE_0.3.1.md) · [Owner-relay evidence](docs/OWNER_RELAY_EVIDENCE.md) · [Resumable-storage evidence](docs/RESUMABLE_STORAGE_EVIDENCE.md) · [Native distribution](docs/DISTRIBUTION.md) · @@ -62,12 +63,11 @@ machine that can actually perform it. ## Built—not imagined -Rampage 0.3.0 is the current Windows x64 Remote Assist candidate. It keeps the complete 0.2.3 -fabric-proof foundation, then gives the paired owner an optional live view/control lane for an -unlocked Windows worker. Access is off by default on the worker, visible in the app and tray while -active, bounded by renewable 30-second signed leases, authenticated to the pinned controller, and -revocable from either machine. It does not grant a shell, elevation, UAC, lock-screen, or Windows -secure-desktop authority. +Rampage 0.3.1 is the current Windows x64 recovery and automatic-role release. It preserves the +Remote Assist and fabric-proof foundation, then closes the complete device lifecycle: repair in +place, return a worker to pairing, revoke a stale enrolled identity from the owner, or factory-reset +the local Rampage runtime without uninstalling external model stores. A failed native status bridge +now produces an actionable recovery state instead of an infinite loading surface. | Proof surface | Validated result | | --- | --- | @@ -80,13 +80,15 @@ secure-desktop authority. | Signed fabric benchmark | One bounded CPU grant per capable live node; deterministic SHA-256 chains; node-pinned placement; signed receipts; aggregate and scale-over-fastest results in the desktop | | Universal capability contract | Signed offers advertise exact workload domain, adapter, operation, execution pattern, isolation, runtime digest, and qualification status; candidate profiles grant no authority | | Autonomous self-scan | Stable evidence digests cover routes, links, failures, denials, thermal/battery pressure, capability gaps, idle capacity, and protected-artifact replication | -| Compute Strategy | Read-only Maximum Model, Speed Boost, Throughput, Efficiency, and Autonomous placement previews with exact capacity and qualification blockers | +| Compute Strategy | Outcome-first Automatic, Biggest AI, Fastest AI, More Work, and Protect This PC placement previews with exact capacity and qualification blockers | +| Lifecycle recovery | One-screen Fix Rampage, Pair again, enrolled-device Forget, redacted receipt, and typed local factory reset; restart replay keeps revoked nodes and offers gone | | Remote Assist | Worker opt-in; paired-owner view/control; dedicated authenticated QUIC protocol; request-, node-, controller-, epoch-, size-, frame-rate-, and input-sequence bounds; visible active state; STOP/revoke | | Packaged product | Native Tauri shell, role-aware system tray, close-to-tray, start-at-login, governed sidecars, cold-start tolerance, controller-restart recovery, clean explicit shutdown, installer, and automatic desktop launcher | | Verification | Rust, desktop, TypeScript SDK, Python intelligence, Python SDK, deterministic universal-gateway, forced-relay mesh, packaging, and lifecycle gates | -The 0.3.0 source, package, and physical-device boundary is recorded in -[the current release evidence](docs/RELEASE_EVIDENCE_0.3.0.md). The immutable packaged 0.2.3 +The 0.3.1 source, package, and physical-device boundary is recorded in +[the current release evidence](docs/RELEASE_EVIDENCE_0.3.1.md). The previous 0.3.0 Remote Assist +record remains immutable in [its release evidence](docs/RELEASE_EVIDENCE_0.3.0.md). The packaged 0.2.3 qualification record—including artifact hashes, live Qwen proof, and its unsigned-release boundary—remains in [the 0.2.3 evidence](docs/RELEASE_EVIDENCE_0.2.3.md). The 0.2.2 pairing record remains in @@ -160,6 +162,20 @@ Once an opted-in worker is live, the owner can produce a fail-closed physical fr requires a fresh shipped Remote Assist offer, verifies the signed lease bounds and JPEG SHA-256, closes the session, and proves the controller returns `404` for the revoked session. It never injects input. +## Recovery Center: nobody gets trapped in a broken pairing + +The wrench in the app header—and **Already paired or stuck? Fix Rampage** on first-run setup—opens a +single recovery surface. **Fix Rampage** safely restarts a consistent installation. **Pair again** +removes only the worker's old fabric identity and returns it to nearby pairing. On the owner PC, +**Forget** revokes the selected identity, live offer, outstanding work, Remote Assist sessions, +artifact locations, and replay state. **Factory reset** is deliberately separate and requires the +full `RESET RAMPAGE` phrase. + +![Rampage 0.3.1 Recovery Center showing one-click repair, enrolled machines, stale-device Forget, and factory reset](docs/assets/rampage-recovery-center.png) + +This image is a source-current browser showcase of the real Recovery Center. Native deletion, +restart, auto-start, and sidecar behavior is verified separately in the packaged Windows campaign. + ## OnePool: pool the work, not the address space Remote memory and VRAM do not become magically coherent across commodity networks. Rampage instead @@ -194,20 +210,21 @@ Apple-silicon iOS simulator build proof, artifact identities, checksums, and rel ## Model Fabric: biggest model and fastest chat are different lanes -The desktop now defaults to **Maximum Model** and exposes five explicit ways to use added compute: +The desktop defaults to **Automatic** and exposes five outcome choices while assigning technical +device roles itself: | Toggle | What Rampage optimizes | | --- | --- | -| Maximum Model | Largest compatible aggregate model-memory placement | -| Speed Boost | Fastest evidence-supported single chat; slow distributed links are rejected | -| Throughput | Independent replicas for many concurrent users or agents | -| Efficiency | Smallest qualified placement that fits | -| Autonomous | Proposal-only strategy adaptation behind Governor gates | +| Automatic | Measure the work, devices, and paths; continuously choose the safest high-value role | +| Biggest AI | Largest compatible model placement; engine-native cross-node layouts only after qualification | +| Fastest AI | Fastest evidence-supported chat; slow distributed links are rejected | +| More Work | Independent replicas and shards for concurrent users, agents, builds, renders, and batches | +| Protect This PC | Preserve the foreground game, call, or production session by spending remote capacity first | On Windows, Local AI Autopilot now qualifies a pinned Ollama runtime and exact starter model in the -background. **Prove my speed** runs sustained, lease-bounded work on every capable live 0.3.0 node +background. **Prove my speed** runs sustained, lease-bounded work on every capable live 0.3.1 node and accepts a node only after its signed execution receipt arrives. This measures useful distributed -work; it does not turn network latency into fictional local VRAM. +work; it does not imply remotely addressable local VRAM. The planner reports visible versus compatible memory, requested weights plus KV cache, selected ranks, parallelism, predicted speedup, and the exact missing qualification. Planning remains @@ -257,7 +274,7 @@ never displayed or copied. Capability discovery does the rest. | Platform lane | Native deliverables | Current qualification boundary | | --- | --- | --- | -| Windows x64 | MSI and NSIS installers; automatic desktop shortcut; Start-menu entry; role-aware tray; bundled sidecars | 0.3.0 carries forward the packaged 0.2.3 foundation and adds source-qualified Windows Remote Assist; exact package hashes and physical-worker status are in the current release evidence | +| Windows x64 | MSI and NSIS installers; automatic desktop shortcut; Start-menu entry; role-aware tray; bundled sidecars | 0.3.1 adds complete recovery and enrolled-device revocation to the Remote Assist foundation; exact package hashes and physical-worker status are in the current release evidence | | Linux x64 | Debian package and AppImage with the same Tauri shell and native sidecars | Fresh Ubuntu 24.04 candidate gate; stable repository/AppImage signing remains channel-specific | | macOS Apple Silicon | Native app bundle and DMG | Fresh macOS 15/M1 candidate gate; stable publication requires Developer ID signing, Gatekeeper acceptance, notarization, and a stapled ticket | | Windows 10 x64 | MSI/NSIS lifecycle, desktop shortcut, tray, sidecars, restart, STOP, and uninstall campaign | Deliberately unqualified until a real self-hosted Windows 10 runner completes the fail-closed workflow | @@ -306,6 +323,9 @@ ambiguous evidence fails closed. 5. On the main PC, choose **Add machine**. Confirm the same four digits and press **Codes match—approve**. 6. Rampage securely enrolls and restarts the laptop automatically. Leave contribution limits on automatic or tune them. Press **STOP** whenever you want the node back. +If setup is ever interrupted, open **Fix Rampage**. A worker can return to pairing in two clicks; +the owner can forget the stale identity before approving the replacement. + The nearby flow uses only the private LAN. If Windows asks, allow Rampage on **private networks**. The complete invite remains available under **Advanced** for segmented networks where local discovery is intentionally blocked. See [Easy and secure pairing](docs/PAIRING.md). diff --git a/apps/desktop/package.json b/apps/desktop/package.json index ef6b949..9d751cd 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@rampage/desktop", - "version": "0.3.0", + "version": "0.3.1", "private": true, "type": "module", "scripts": { diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 122a5f7..510524b 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rampage-desktop" -version = "0.3.0" +version = "0.3.1" description = "Rampage personal compute fabric" authors = ["ObtuseAI"] edition = "2024" diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 4e28804..11ea8c3 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -7,7 +7,7 @@ use rand::{TryRng as _, rngs::SysRng}; use std::{ io::{Read, Write}, net::SocketAddr, - path::PathBuf, + path::{Path, PathBuf}, sync::{Arc, Mutex}, time::Duration, }; @@ -54,6 +54,19 @@ struct RemoteAssistStatusView { expires_at: Option, } +#[derive(Debug, Clone, serde::Serialize)] +#[serde(rename_all = "camelCase")] +struct RecoveryStatusView { + schema: &'static str, + version: &'static str, + role: &'static str, + state: &'static str, + healthy: bool, + issues: Vec, + can_leave_fabric: bool, + can_factory_reset: bool, +} + #[derive(Debug, serde::Deserialize)] #[serde(deny_unknown_fields)] struct RemoteAssistPolicyDisk { @@ -88,8 +101,11 @@ impl Default for WorkerRuntime { const BACKGROUND_ARG: &str = "--background"; const AUTOSTART_NAME: &str = "Rampage"; const OWNER_FABRIC_MARKER: &str = "owner-fabric-v1.ready"; +const SETUP_REQUIRED_MARKER: &str = "setup-required-v1.ready"; const REMOTE_ASSIST_POLICY_FILE: &str = "remote-assist-policy.json"; const REMOTE_ASSIST_ACTIVE_FILE: &str = "remote-assist-active.json"; +const LEAVE_FABRIC_CONFIRMATION: &str = "LEAVE FABRIC"; +const FACTORY_RESET_CONFIRMATION: &str = "RESET RAMPAGE"; #[cfg(target_os = "windows")] const AUTOSTART_RUN_KEY: &str = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run"; #[cfg(target_os = "windows")] @@ -181,6 +197,22 @@ fn kill_process_tree(child: CommandChild) { let _ = child.kill(); } +fn stop_all_sidecars(app: &AppHandle) -> Result { + let children = { + let state = app.state::(); + let mut sidecars = state + .0 + .lock() + .map_err(|_| "sidecar state lock poisoned".to_string())?; + sidecars.drain(..).collect::>() + }; + let count = children.len(); + for child in children { + kill_process_tree(child); + } + Ok(count) +} + fn runtime_dir(app: &AppHandle) -> Result { if let Some(path) = std::env::var_os("RAMPAGE_DATA_DIR") { return Ok(PathBuf::from(path)); @@ -206,6 +238,188 @@ fn controller_origin() -> Result { Ok(format!("http://{}", controller_bind()?)) } +fn setup_required(data_dir: &Path) -> bool { + data_dir.join(SETUP_REQUIRED_MARKER).is_file() +} + +fn fabric_role_at(data_dir: &Path) -> &'static str { + if setup_required(data_dir) { + "setup" + } else if worker_enrollment_exists(data_dir) { + "worker" + } else { + "owner" + } +} + +fn enrollment_file_state(data_dir: &Path) -> (&'static str, Vec) { + let setup = setup_required(data_dir); + let owner = data_dir.join(OWNER_FABRIC_MARKER).is_file(); + let invite = data_dir.join("remote-invite.json").is_file(); + let pin = data_dir.join("agent.controller-pin.json").is_file(); + let enrolled = data_dir.join("agent.enrolled").is_file(); + let identity = data_dir.join("agent.identity.json").is_file(); + let key = data_dir.join("agent.key").is_file(); + let mut issues = Vec::new(); + + if setup { + if owner || invite || pin || enrolled || identity || key { + issues.push( + "Setup mode still contains fabric credentials; run Reset Rampage again.".into(), + ); + return ("cleanup_required", issues); + } + return ("ready_to_configure", issues); + } + if owner { + if invite || pin { + issues.push("Owner and worker enrollment markers conflict on this device.".into()); + return ("repair_required", issues); + } + return ("owner_active", issues); + } + if pin { + if !enrolled || !identity || !key { + issues.push( + "The paired-worker identity is incomplete or was interrupted during an update." + .into(), + ); + return ("repair_required", issues); + } + return ("worker_paired", issues); + } + if invite { + return ("enrollment_pending", issues); + } + if enrolled || identity || key { + issues.push("Local worker identity exists without a pinned owner.".into()); + return ("repair_required", issues); + } + ("owner_starting", issues) +} + +fn recovery_status_at(data_dir: &Path) -> RecoveryStatusView { + let role = fabric_role_at(data_dir); + let (state, issues) = enrollment_file_state(data_dir); + RecoveryStatusView { + schema: "rampage.recovery-status.v1", + version: env!("CARGO_PKG_VERSION"), + role, + state, + healthy: issues.is_empty(), + issues, + can_leave_fabric: role == "worker", + can_factory_reset: true, + } +} + +fn validate_runtime_reset_target(data_dir: &Path) -> Result<(), String> { + if !data_dir.is_absolute() + || data_dir.file_name().and_then(|value| value.to_str()) != Some("runtime") + || data_dir + .parent() + .and_then(Path::file_name) + .and_then(|value| value.to_str()) + .is_none_or(|value| !value.eq_ignore_ascii_case("ai.obtuse.rampage")) + { + return Err("refusing to reset a path outside the Rampage runtime directory".into()); + } + if data_dir.exists() { + let metadata = std::fs::symlink_metadata(data_dir).map_err(|error| error.to_string())?; + if metadata.file_type().is_symlink() || !metadata.is_dir() { + return Err("Rampage runtime path is not a regular directory".into()); + } + } + Ok(()) +} + +fn remove_runtime_entry(path: &Path) -> Result<(), String> { + let metadata = std::fs::symlink_metadata(path).map_err(|error| error.to_string())?; + if metadata.file_type().is_symlink() || metadata.is_file() { + std::fs::remove_file(path).map_err(|error| error.to_string()) + } else if metadata.is_dir() { + std::fs::remove_dir_all(path).map_err(|error| error.to_string()) + } else { + Err(format!( + "refusing to reset unsupported runtime entry {}", + path.display() + )) + } +} + +fn reset_runtime_to_setup(data_dir: &Path) -> Result<(), String> { + validate_runtime_reset_target(data_dir)?; + std::fs::create_dir_all(data_dir).map_err(|error| error.to_string())?; + for entry in std::fs::read_dir(data_dir).map_err(|error| error.to_string())? { + let entry = entry.map_err(|error| error.to_string())?; + remove_runtime_entry(&entry.path())?; + } + write_new_marker( + &data_dir.join(SETUP_REQUIRED_MARKER), + b"rampage.setup-required.v1\n", + ) +} + +#[tauri::command] +fn recovery_status(app: AppHandle) -> Result { + Ok(recovery_status_at(&runtime_dir(&app)?)) +} + +#[tauri::command] +fn repair_connection(app: AppHandle) -> Result<(), String> { + let data_dir = runtime_dir(&app)?; + let (_, issues) = enrollment_file_state(&data_dir); + if !issues.is_empty() { + return Err("Enrollment files are inconsistent; use Leave fabric or Reset Rampage.".into()); + } + app.restart() +} + +#[tauri::command] +fn leave_fabric(app: AppHandle, confirmation: String) -> Result<(), String> { + if confirmation != LEAVE_FABRIC_CONFIRMATION { + return Err("exact LEAVE FABRIC confirmation is required".into()); + } + let data_dir = runtime_dir(&app)?; + if fabric_role_at(&data_dir) != "worker" { + return Err("Leave fabric is available only on a paired worker".into()); + } + stop_all_sidecars(&app)?; + reset_runtime_to_setup(&data_dir)?; + app.restart() +} + +#[tauri::command] +fn factory_reset(app: AppHandle, confirmation: String) -> Result<(), String> { + if confirmation != FACTORY_RESET_CONFIRMATION { + return Err("exact RESET RAMPAGE confirmation is required".into()); + } + let data_dir = runtime_dir(&app)?; + stop_all_sidecars(&app)?; + platform_set_autostart(&app, false)?; + reset_runtime_to_setup(&data_dir)?; + app.restart() +} + +#[tauri::command] +fn activate_owner_fabric(app: AppHandle) -> Result<(), String> { + let data_dir = runtime_dir(&app)?; + if worker_enrollment_exists(&data_dir) { + return Err("this device is still paired as a worker".into()); + } + let setup_marker = data_dir.join(SETUP_REQUIRED_MARKER); + if setup_marker.is_file() { + let owner_marker = data_dir.join(OWNER_FABRIC_MARKER); + write_new_marker(&owner_marker, b"rampage.owner-fabric.v1\n")?; + if let Err(error) = std::fs::remove_file(setup_marker) { + let _ = std::fs::remove_file(owner_marker); + return Err(format!("could not activate the owner fabric: {error}")); + } + app.restart(); + } + Ok(()) +} + #[tauri::command] fn local_stop(app: AppHandle) -> Result<(), String> { let data_dir = runtime_dir(&app)?; @@ -258,11 +472,7 @@ fn propagate_controller_stop(data_dir: PathBuf) { #[tauri::command] fn fabric_mode(app: AppHandle) -> Result<&'static str, String> { - Ok(if worker_enrollment_exists(&runtime_dir(&app)?) { - "worker" - } else { - "owner" - }) + Ok(fabric_role_at(&runtime_dir(&app)?)) } fn remote_assist_enabled_at(data_dir: &std::path::Path) -> bool { @@ -584,6 +794,15 @@ fn persist_remote_invite(app: &AppHandle, invitation: &str) -> Result<(), String let _ = std::fs::remove_file(&temporary); } write_result?; + let setup_marker = data_dir.join(SETUP_REQUIRED_MARKER); + if setup_marker.is_file() + && let Err(error) = std::fs::remove_file(&setup_marker) + { + let _ = std::fs::remove_file(&destination); + return Err(format!( + "could not leave setup mode after enrollment: {error}" + )); + } Ok(()) } @@ -816,7 +1035,8 @@ async fn run_fabric_benchmark(app: AppHandle) -> Result bool { - !data_dir.join(OWNER_FABRIC_MARKER).is_file() + !data_dir.join(SETUP_REQUIRED_MARKER).is_file() + && !data_dir.join(OWNER_FABRIC_MARKER).is_file() && (data_dir.join("remote-invite.json").is_file() || data_dir.join("agent.controller-pin.json").is_file()) } @@ -908,6 +1128,9 @@ fn configured_private_relay(data_dir: &std::path::Path) -> Result fn launch_fabric(app: &AppHandle) -> Result<(), String> { let data_dir = runtime_dir(app)?; std::fs::create_dir_all(&data_dir).map_err(|error| error.to_string())?; + if setup_required(&data_dir) { + return Ok(()); + } if worker_enrollment_exists(&data_dir) { return launch_remote_worker(app, &data_dir); } @@ -1120,7 +1343,10 @@ fn schedule_remote_assist_indicator( } fn install_desktop_lifecycle(app: &tauri::App) -> Result<(), Box> { - let role = if worker_enrollment_exists(&runtime_dir(app.handle())?) { + let data_dir = runtime_dir(app.handle())?; + let role = if setup_required(&data_dir) { + "Setup required" + } else if worker_enrollment_exists(&data_dir) { "Worker active" } else { "Owner fabric active" @@ -1234,11 +1460,14 @@ pub fn run() { .manage(LocalAiRuntime::default()) .manage(PairingManager::default()) .setup(|app| { + let setup_required = setup_required(&runtime_dir(app.handle())?); launch_fabric(app.handle()).map_err(std::io::Error::other)?; - local_ai::schedule( - app.state::().inner().clone(), - diagnostic_instance_is_bounded(), - ); + if !setup_required { + local_ai::schedule( + app.state::().inner().clone(), + diagnostic_instance_is_bounded(), + ); + } install_desktop_lifecycle(app)?; schedule_diagnostic_exit(app.handle()); Ok(()) @@ -1260,6 +1489,11 @@ pub fn run() { reject_pairing, autostart_enabled, set_autostart, + recovery_status, + repair_connection, + leave_fabric, + factory_reset, + activate_owner_fabric, remote_assist_status, set_remote_assist_enabled ]) @@ -1373,4 +1607,51 @@ mod tests { .unwrap(); assert!(!worker_enrollment_exists(temp.path())); } + + fn recovery_runtime() -> tempfile::TempDir { + let root = tempfile::tempdir().unwrap(); + std::fs::create_dir_all(root.path().join("ai.obtuse.rampage/runtime")).unwrap(); + root + } + + #[test] + fn recovery_detects_incomplete_worker_identity() { + let root = recovery_runtime(); + let runtime = root.path().join("ai.obtuse.rampage/runtime"); + std::fs::write(runtime.join("agent.controller-pin.json"), b"pinned").unwrap(); + let status = recovery_status_at(&runtime); + assert_eq!(status.role, "worker"); + assert_eq!(status.state, "repair_required"); + assert!(!status.healthy); + assert!(status.can_leave_fabric); + } + + #[test] + fn reset_rotates_to_clean_setup_without_following_unrelated_paths() { + let root = recovery_runtime(); + let runtime = root.path().join("ai.obtuse.rampage/runtime"); + std::fs::write(runtime.join("agent.key"), b"secret").unwrap(); + std::fs::write(runtime.join("agent.controller-pin.json"), b"pinned").unwrap(); + std::fs::create_dir(runtime.join("cas")).unwrap(); + std::fs::write(runtime.join("cas/object"), b"encrypted").unwrap(); + reset_runtime_to_setup(&runtime).unwrap(); + assert_eq!( + std::fs::read_to_string(runtime.join(SETUP_REQUIRED_MARKER)).unwrap(), + "rampage.setup-required.v1\n" + ); + assert_eq!(std::fs::read_dir(&runtime).unwrap().count(), 1); + let status = recovery_status_at(&runtime); + assert_eq!(status.role, "setup"); + assert_eq!(status.state, "ready_to_configure"); + assert!(status.healthy); + } + + #[test] + fn reset_refuses_broad_or_renamed_targets() { + let root = tempfile::tempdir().unwrap(); + assert!(reset_runtime_to_setup(root.path()).is_err()); + let wrong = root.path().join("not-rampage/runtime"); + std::fs::create_dir_all(&wrong).unwrap(); + assert!(reset_runtime_to_setup(&wrong).is_err()); + } } diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index a97f880..acbb6c4 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Rampage", - "version": "0.3.0", + "version": "0.3.1", "identifier": "ai.obtuse.rampage", "build": { "beforeDevCommand": "pnpm dev", diff --git a/apps/desktop/src/App.test.tsx b/apps/desktop/src/App.test.tsx index b70cfa2..fe4af0f 100644 --- a/apps/desktop/src/App.test.tsx +++ b/apps/desktop/src/App.test.tsx @@ -63,10 +63,10 @@ test("local stop does not depend on controller", () => { expect(useRampage.getState().capability).toBe("read_only"); }); -test("separates maximum model size from measured speed boost", () => { +test("separates the biggest AI outcome from the fastest AI outcome", () => { render(); - expect(screen.getByRole("button", { name: /maximum model/i })).toHaveAttribute("aria-pressed", "true"); - fireEvent.click(screen.getByRole("button", { name: /speed boost/i })); + expect(screen.getByRole("button", { name: /biggest ai/i })).toHaveAttribute("aria-pressed", "true"); + fireEvent.click(screen.getByRole("button", { name: /fastest ai/i })); expect(useRampage.getState().computeStrategy).toBe("speed_boost"); expect(screen.getByText(/use tensor peers only when measured links predict faster tokens/i)).toBeInTheDocument(); expect(localStorage.getItem("rampage.compute-strategy")).toBe("speed_boost"); diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index baf7186..fb1cbeb 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -1,4 +1,4 @@ -import { Activity, Boxes, BrainCircuit, CircleStop, Command, Eye, Grid2X2, MonitorUp, MousePointer2, Orbit, Play, RefreshCw, Rocket, ShieldCheck, UserPlus } from "lucide-react"; +import { Activity, Boxes, BrainCircuit, CircleStop, Command, Eye, Grid2X2, MonitorUp, MousePointer2, Orbit, Play, RefreshCw, Rocket, ShieldCheck, UserPlus, Wrench } from "lucide-react"; import { lazy, Suspense, useEffect } from "react"; import { CommandPalette } from "./components/CommandPalette"; import { ComputeStrategyPanel } from "./components/ComputeStrategyPanel"; @@ -7,6 +7,7 @@ import { PairingPanel } from "./components/PairingPanel"; import { Onboarding } from "./components/Onboarding"; import { OpsGrid } from "./components/OpsGrid"; import { RemoteAssistPanel } from "./components/RemoteAssistPanel"; +import { RecoveryCenter } from "./components/RecoveryCenter"; import { useRampage } from "./store"; const Arena = lazy(() => import("./components/Arena").then((module) => ({ default: module.Arena }))); @@ -37,11 +38,13 @@ export default function App() { {state.onboarding && } +
R
RAMPAGEPERSONAL COMPUTE FABRIC
{state.killLatch ? "OWNER STOPPED" : state.remoteAssistStatus.active ? "REMOTE CONTROL ACTIVE" : state.fabricRole === "worker" ? state.workerRuntime.state === "active" ? "WORKER ACTIVE" : state.workerRuntime.state === "starting" ? "WORKER CONNECTING" : "WORKER ATTENTION" : state.connected ? "FABRIC LIVE" : "LOCAL REDUCED"}{state.nodes.length} nodes{state.meshMode.replace("_", " ")}{state.diagnostic ? `self-scan ${state.diagnostic.health_score}/100` : state.capability.replaceAll("_", " ")}
+ {state.fabricRole === "owner" && } diff --git a/apps/desktop/src/RecoveryCenter.test.tsx b/apps/desktop/src/RecoveryCenter.test.tsx new file mode 100644 index 0000000..f27d919 --- /dev/null +++ b/apps/desktop/src/RecoveryCenter.test.tsx @@ -0,0 +1,82 @@ +import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { afterEach, beforeEach, expect, test, vi } from "vitest"; +import { RecoveryCenter } from "./components/RecoveryCenter"; +import { type RecoveryStatus, useRampage } from "./store"; + +const workerStatus: RecoveryStatus = { + schema: "rampage.recovery-status.v1", + version: "0.3.1", + role: "worker", + state: "enrolled_worker", + healthy: true, + issues: [], + canLeaveFabric: true, + canFactoryReset: true, + nodes: [], +}; + +afterEach(cleanup); + +beforeEach(() => { + useRampage.setState({ + recoveryOpen: true, + recoveryStatus: workerStatus, + repairConnection: vi.fn().mockResolvedValue(undefined), + leaveFabric: vi.fn().mockResolvedValue(undefined), + factoryReset: vi.fn().mockResolvedValue(undefined), + forgetNode: vi.fn().mockResolvedValue(undefined), + refreshRecovery: vi.fn().mockResolvedValue(undefined), + }); +}); + +test("lets a worker repair or return to pairing without copying an identity", async () => { + render(); + + fireEvent.click(screen.getByRole("button", { name: /fix rampage/i })); + expect(useRampage.getState().repairConnection).toHaveBeenCalledOnce(); + + fireEvent.click(screen.getByRole("button", { name: /pair again/i })); + expect(screen.getByRole("heading", { name: /start pairing over/i })).toBeVisible(); + fireEvent.click(screen.getByRole("button", { name: /leave fabric/i })); + + await waitFor(() => expect(useRampage.getState().leaveFabric).toHaveBeenCalledWith("LEAVE FABRIC")); +}); + +test("keeps enrolled-machine revocation in advanced recovery with a clear second confirmation", async () => { + const nodeId = "0198f1aa-9f18-7dc3-81a3-d78f22efb662"; + useRampage.setState({ + recoveryStatus: { + ...workerStatus, + role: "owner", + state: "owner_fabric", + canLeaveFabric: false, + nodes: [{ + nodeId, + displayName: "Studio Laptop", + platform: "windows-x86_64", + deviceKind: "desktop", + live: false, + local: false, + }], + }, + }); + render(); + + fireEvent.click(screen.getByText(/advanced recovery and enrolled devices/i)); + fireEvent.click(screen.getByRole("button", { name: /forget/i })); + expect(screen.getByRole("heading", { name: /forget studio laptop/i })).toBeVisible(); + fireEvent.click(screen.getByRole("button", { name: /forget machine/i })); + + await waitFor(() => expect(useRampage.getState().forgetNode).toHaveBeenCalledWith(nodeId, `FORGET ${nodeId}`)); +}); + +test("requires the full factory-reset phrase before destructive reset is enabled", () => { + render(); + + fireEvent.click(screen.getByText(/advanced recovery and enrolled devices/i)); + fireEvent.click(screen.getByRole("button", { name: /factory reset/i })); + const reset = screen.getByRole("button", { name: /reset rampage/i }); + expect(reset).toBeDisabled(); + fireEvent.change(screen.getByRole("textbox"), { target: { value: "RESET RAMPAGE" } }); + expect(reset).toBeEnabled(); +}); diff --git a/apps/desktop/src/components/ComputeStrategyPanel.tsx b/apps/desktop/src/components/ComputeStrategyPanel.tsx index a7e4db4..48ed772 100644 --- a/apps/desktop/src/components/ComputeStrategyPanel.tsx +++ b/apps/desktop/src/components/ComputeStrategyPanel.tsx @@ -1,4 +1,4 @@ -import { Activity, BatteryCharging, Bot, Gauge, Layers3, Network, Sparkles, Zap } from "lucide-react"; +import { Activity, Bot, Gamepad2, Gauge, Layers3, Network, Sparkles, Zap } from "lucide-react"; import { useRampage } from "../store"; import type { ComputeStrategy } from "../types"; @@ -9,40 +9,40 @@ const strategies: Array<{ description: string; icon: typeof Layers3; }> = [ + { + id: "autonomous_balanced", + label: "Automatic", + short: "RAMPAGE CHOOSES", + description: "Measure every device and path, then continuously choose the safest high-value role without technical setup.", + icon: Sparkles, + }, { id: "maximum_model_size", - label: "Maximum Model", - short: "BIGGEST LLM", + label: "Biggest AI", + short: "BIGGEST AI", description: "Combine only compatible, qualified model memory to fit the largest possible local model.", icon: Layers3, }, { id: "speed_boost", - label: "Speed Boost", - short: "FASTEST CHAT", + label: "Fastest AI", + short: "FASTEST AI", description: "Use tensor peers only when measured links predict faster tokens; otherwise select the fastest whole-model node.", icon: Zap, }, { id: "maximum_throughput", - label: "Throughput", - short: "MOST REQUESTS", + label: "More Work", + short: "MOST WORK", description: "Replicate the model across capable nodes for more simultaneous requests and agent work.", icon: Network, }, { id: "efficiency", - label: "Efficiency", - short: "LESS ENERGY", - description: "Choose the smallest qualified placement that fits while preserving owner reserves.", - icon: BatteryCharging, - }, - { - id: "autonomous_balanced", - label: "Autonomous", - short: "EVIDENCE ADAPTS", - description: "Let proposal-only intelligence recommend a strategy; the Governor still enforces every promotion gate.", - icon: Sparkles, + label: "Protect This PC", + short: "GAMING-SAFE", + description: "Keep foreground CPU, memory, GPU, and network reserves on this PC while moving eligible background work elsewhere.", + icon: Gamepad2, }, ]; @@ -76,7 +76,7 @@ export function ComputeStrategyPanel() { > {strategy.label} - {strategy.id === "maximum_model_size" && FOCUS} + {strategy.id === "autonomous_balanced" && RECOMMENDED} ); })} @@ -86,26 +86,29 @@ export function ComputeStrategyPanel() {

{selected.description}

{plan?.reason ?? "Connect the controller to calculate a signed-resource placement preview."}
-
- - - - -
+
+ Advanced model target +
+ + + + +
+
Requested{formatGiB((state.targetModelGiB + state.kvCacheGiB) * gib)}
Visible memory{plan ? formatGiB(plan.observed_fabric_bytes) : "—"}
diff --git a/apps/desktop/src/components/Onboarding.tsx b/apps/desktop/src/components/Onboarding.tsx index 89ecf52..7d45d3e 100644 --- a/apps/desktop/src/components/Onboarding.tsx +++ b/apps/desktop/src/components/Onboarding.tsx @@ -13,6 +13,7 @@ export function Onboarding() { const joinFabric = useRampage((state) => state.joinFabric); const beginPairing = useRampage((state) => state.beginPairing); const cancelPairing = useRampage((state) => state.cancelPairing); + const openRecovery = useRampage((state) => state.setRecoveryOpen); const pairing = useRampage((state) => state.workerPairing); const [step, setStep] = useState(0); const [choice, setChoice] = useState<"owner" | "worker">("owner"); @@ -66,12 +67,13 @@ export function Onboarding() { } return; } - step < steps.length - 1 ? setStep(step + 1) : finish(); + step < steps.length - 1 ? setStep(step + 1) : void finish().catch((reason: unknown) => setError(reason instanceof Error ? reason.message : "Could not start this fabric.")); }}> {step === 0 && choice === "worker" ? pairing.state === "idle" || pairing.state === "failed" ? "Find my fabric" : pairing.state === "approved" ? "Finishing securely…" : "Waiting for main PC…" : step < steps.length - 1 ? "Continue" : "Enter Rampage"} +
); diff --git a/apps/desktop/src/components/RecoveryCenter.tsx b/apps/desktop/src/components/RecoveryCenter.tsx new file mode 100644 index 0000000..234c238 --- /dev/null +++ b/apps/desktop/src/components/RecoveryCenter.tsx @@ -0,0 +1,97 @@ +import { AlertTriangle, Check, Clipboard, HardDrive, RefreshCw, RotateCcw, ShieldX, Trash2, Unplug, X } from "lucide-react"; +import { useMemo, useState } from "react"; +import { useRampage } from "../store"; + +type DestructiveAction = + | { kind: "leave"; phrase: "LEAVE FABRIC"; requiresTyping: false; title: string; detail: string } + | { kind: "reset"; phrase: "RESET RAMPAGE"; requiresTyping: true; title: string; detail: string } + | { kind: "forget"; phrase: string; requiresTyping: false; title: string; detail: string; nodeId: string }; + +export function RecoveryCenter() { + const open = useRampage((state) => state.recoveryOpen); + const status = useRampage((state) => state.recoveryStatus); + const setOpen = useRampage((state) => state.setRecoveryOpen); + const refresh = useRampage((state) => state.refreshRecovery); + const repair = useRampage((state) => state.repairConnection); + const leave = useRampage((state) => state.leaveFabric); + const reset = useRampage((state) => state.factoryReset); + const forget = useRampage((state) => state.forgetNode); + const [action, setAction] = useState(null); + const [confirmation, setConfirmation] = useState(""); + const [pending, setPending] = useState(false); + const [error, setError] = useState(null); + const canConfirm = action !== null && (!action.requiresTyping || confirmation === action.phrase); + const diagnostics = useMemo(() => status ? JSON.stringify(status, null, 2) : "", [status]); + + if (!open) return null; + const begin = (next: DestructiveAction) => { + setAction(next); + setConfirmation(""); + setError(null); + }; + const execute = async () => { + if (!action || (action.requiresTyping && confirmation !== action.phrase)) return; + setPending(true); + setError(null); + try { + if (action.kind === "leave") await leave(action.phrase); + if (action.kind === "reset") await reset(confirmation); + if (action.kind === "forget") { + await forget(action.nodeId, action.phrase); + setAction(null); + setConfirmation(""); + } + } catch (reason) { + setError(reason instanceof Error ? reason.message : "Recovery action failed."); + } finally { + setPending(false); + } + }; + + return
+
+
+

RECOVERY CENTER

Repair, leave, or start clean.

+ +
+ {!status ?
Inspecting local identity, sidecars, and enrollment…
: <> +
+ {status.healthy ? : } +
{status.healthy ? "Local lifecycle is consistent" : "Recovery attention required"}Rampage {status.version} · {status.role} · {status.state.replaceAll("_", " ")}
+ +
+ {status.issues.length > 0 &&
    {status.issues.map((issue) =>
  • {issue}
  • )}
} + +
+ + {status.canLeaveFabric && } +
+ +
+ Advanced recovery and enrolled devices + {status.role === "owner" &&
+
Enrolled machinesForget stale devices so old identities cannot reconnect.
+ {status.nodes.map((node) =>
+ +
{node.displayName}{node.platform} · {node.live ? "live signed offer" : "offline"}
+ {node.local ? OWNER LOCAL : } +
)} +
} +
+
Factory reset

Erase this device’s Rampage runtime, identities, local evidence, encrypted caches, and auto-start setting. Installed models outside Rampage are untouched.

+
+ +
+ } + {action &&
+ +

{action.title}

+

{action.detail}

+ {action.requiresTyping && } + {error &&

{error}

} +
+
} + {!action && error &&

{error}

} +
+
; +} diff --git a/apps/desktop/src/store.ts b/apps/desktop/src/store.ts index d96e3b5..6b6f659 100644 --- a/apps/desktop/src/store.ts +++ b/apps/desktop/src/store.ts @@ -17,7 +17,7 @@ const computeStrategies: ComputeStrategy[] = [ function storedComputeStrategy(): ComputeStrategy { const stored = localStorage.getItem("rampage.compute-strategy") as ComputeStrategy | null; - return stored && computeStrategies.includes(stored) ? stored : "maximum_model_size"; + return stored && computeStrategies.includes(stored) ? stored : "autonomous_balanced"; } function controllerHeaders(json = false): HeadersInit { @@ -97,6 +97,27 @@ export interface FabricBenchmarkResult { all_results_signed: true; } +export interface RecoveryNode { + nodeId: string; + displayName: string; + platform: string; + deviceKind: string; + live: boolean; + local: boolean; +} + +export interface RecoveryStatus { + schema: "rampage.recovery-status.v1"; + version: string; + role: "owner" | "worker" | "setup"; + state: string; + healthy: boolean; + issues: string[]; + canLeaveFabric: boolean; + canFactoryReset: boolean; + nodes: RecoveryNode[]; +} + const initialLocalAiRuntime: LocalAiRuntime = { state: "detecting", modelId: "qwen3:4b", @@ -130,6 +151,20 @@ const demoDiagnostic: FabricDiagnosticReport = { evidence: "Fresh signed offers show useful idle capacity inside every owner reserve.", }], }; +const demoRecoveryStatus: RecoveryStatus = { + schema: "rampage.recovery-status.v1", + version: "0.3.1", + role: "owner", + state: "owner_active", + healthy: true, + issues: [], + canLeaveFabric: false, + canFactoryReset: true, + nodes: [ + { nodeId: "0198f1aa-9f18-7dc3-81a3-d78f22efb660", displayName: "This Device", platform: "windows-x86_64", deviceKind: "desktop", live: true, local: true }, + { nodeId: "0198f1aa-9f18-7dc3-81a3-d78f22efb662", displayName: "Studio Laptop", platform: "windows-x86_64", deviceKind: "laptop", live: false, local: false }, + ], +}; interface RampageState { mode: "arena" | "grid"; @@ -146,7 +181,7 @@ interface RampageState { meshEndpointId: string | null; inviteCode: string | null; inviteBundle: string | null; - fabricRole: "owner" | "worker"; + fabricRole: "owner" | "worker" | "setup"; lastAction: string | null; runAtLogin: boolean; killLatch: boolean; @@ -169,6 +204,8 @@ interface RampageState { remoteDesktopFrame: RemoteDesktopFramePayload | null; remoteDesktopPending: boolean; remoteDesktopInputSequence: number; + recoveryOpen: boolean; + recoveryStatus: RecoveryStatus | null; setMode: (mode: "arena" | "grid") => void; setSelectedNode: (id: string) => void; setCommandOpen: (open: boolean) => void; @@ -179,7 +216,7 @@ interface RampageState { setKvCacheGiB: (gib: number) => void; planModelSession: () => Promise; copyGatewayConfig: () => Promise; - finishOnboarding: () => void; + finishOnboarding: () => Promise; refresh: () => Promise; createInvite: () => Promise; joinFabric: (invitation: string) => Promise; @@ -202,6 +239,12 @@ interface RampageState { refreshRemoteDesktopFrame: () => Promise; sendRemoteDesktopInput: (events: RemoteInputEvent[]) => Promise; closeRemoteDesktop: () => Promise; + setRecoveryOpen: (open: boolean) => void; + refreshRecovery: () => Promise; + repairConnection: () => Promise; + leaveFabric: (confirmation: string) => Promise; + factoryReset: (confirmation: string) => Promise; + forgetNode: (nodeId: string, confirmation: string) => Promise; } function offersToNodes(offers: ResourceOffer[]): FabricNode[] { @@ -295,6 +338,8 @@ export const useRampage = create((set, get) => ({ remoteDesktopFrame: null, remoteDesktopPending: false, remoteDesktopInputSequence: 0, + recoveryOpen: false, + recoveryStatus: null, setMode: (mode) => set({ mode }), setSelectedNode: (selectedNode) => set({ selectedNode }), setCommandOpen: (commandOpen) => set({ commandOpen }), @@ -367,18 +412,45 @@ export const useRampage = create((set, get) => ({ lastAction: "Universal AI gateway settings copied. Treat the shared local API key as a secret.", }); }, - finishOnboarding: () => { + finishOnboarding: async () => { localStorage.setItem("rampage.onboarded", "true"); - set({ onboarding: false }); + try { + await invoke("activate_owner_fabric"); + set({ onboarding: false, fabricRole: "owner" }); + } catch (error) { + localStorage.removeItem("rampage.onboarded"); + throw error; + } }, refresh: async () => { try { const [fabricRole, runAtLogin, localAiRuntime, remoteAssistStatus] = await Promise.all([ - invoke<"owner" | "worker">("fabric_mode").catch(() => "owner" as const), + invoke<"owner" | "worker" | "setup">("fabric_mode").catch(() => "owner" as const), invoke("autostart_enabled").catch(() => get().runAtLogin), invoke("local_ai_runtime_status").catch(() => get().localAiRuntime), invoke("remote_assist_status").catch(() => get().remoteAssistStatus), ]); + if (fabricRole === "setup") { + localStorage.removeItem("rampage.onboarded"); + set({ + onboarding: true, + fabricRole, + connected: false, + capability: "blocked", + nodes: [], + selectedNode: "", + workerRuntime: { state: "inactive", nodeId: null, message: null }, + localAiRuntime, + remoteAssistStatus, + runAtLogin, + killLatch: false, + gatewayModels: [], + diagnostic: null, + lastAction: "This device is clean and ready to create or join a fabric.", + lastSync: new Date(), + }); + return; + } if (fabricRole === "worker") { const workerRuntime = await invoke("worker_runtime_status").catch(() => ({ state: "failed" as const, @@ -835,6 +907,103 @@ export const useRampage = create((set, get) => ({ set({ lastAction: "Remote Assist viewer closed. The worker lease will expire within 30 seconds." }); } }, + setRecoveryOpen: (recoveryOpen) => { + set({ recoveryOpen }); + if (recoveryOpen) void get().refreshRecovery(); + }, + refreshRecovery: async () => { + let native: Omit; + try { + native = await invoke>("recovery_status"); + } catch (error) { + if (import.meta.env.DEV) { + set({ recoveryStatus: demoRecoveryStatus }); + return; + } + const role = get().fabricRole; + set({ + recoveryStatus: { + schema: "rampage.recovery-status.v1", + version: "unavailable", + role, + state: "status_unavailable", + healthy: false, + issues: [error instanceof Error ? error.message : "The native recovery bridge did not respond."], + canLeaveFabric: role === "worker", + canFactoryReset: true, + nodes: [], + }, + }); + return; + } + let nodes: RecoveryNode[] = []; + if (native.role === "owner") { + try { + localControllerToken ??= await invoke("controller_token"); + const [nodesResponse, offersResponse] = await Promise.all([ + fetch(`${controller}/v1/nodes`, { headers: controllerHeaders() }), + fetch(`${controller}/v1/offers`, { headers: controllerHeaders() }), + ]); + if (nodesResponse.ok && offersResponse.ok) { + const enrolled = await nodesResponse.json() as Array<{ + node_id: string; + display_name: string; + platform: string; + device_kind: string; + }>; + const offers = await offersResponse.json() as ResourceOffer[]; + const liveIds = new Set(offers.map((offer) => offer.node_id)); + nodes = enrolled.map((node) => ({ + nodeId: node.node_id, + displayName: node.display_name, + platform: node.platform, + deviceKind: node.device_kind, + live: liveIds.has(node.node_id), + local: node.display_name === "This Device", + })); + } + } catch { + // Native recovery remains available even if the owner controller is unhealthy. + } + } + set({ recoveryStatus: { ...native, nodes } }); + }, + repairConnection: async () => { + set({ lastAction: "Restarting Rampage and rebuilding the local connection path…" }); + await invoke("repair_connection"); + }, + leaveFabric: async (confirmation) => { + const prior = localStorage.getItem("rampage.onboarded"); + localStorage.removeItem("rampage.onboarded"); + try { + await invoke("leave_fabric", { confirmation }); + } catch (error) { + if (prior !== null) localStorage.setItem("rampage.onboarded", prior); + throw error; + } + }, + factoryReset: async (confirmation) => { + const keys = ["rampage.onboarded", "rampage.compute-strategy", "rampage.target-model", "rampage.target-model-gib", "rampage.kv-cache-gib"]; + const prior = new Map(keys.map((key) => [key, localStorage.getItem(key)])); + keys.forEach((key) => localStorage.removeItem(key)); + try { + await invoke("factory_reset", { confirmation }); + } catch (error) { + prior.forEach((value, key) => { if (value !== null) localStorage.setItem(key, value); }); + throw error; + } + }, + forgetNode: async (nodeId, confirmation) => { + localControllerToken ??= await invoke("controller_token"); + const response = await fetch(`${controller}/v1/nodes/${nodeId}/revoke`, { + method: "POST", + headers: controllerHeaders(true), + body: JSON.stringify({ confirmation }), + }); + if (!response.ok) throw new Error(await response.text()); + set({ lastAction: "Machine forgotten. Its offers, leases, sessions, and future access were revoked." }); + await Promise.all([get().refreshRecovery(), get().refresh()]); + }, toggleAutostart: async () => { const requested = !get().runAtLogin; try { diff --git a/apps/desktop/src/styles.css b/apps/desktop/src/styles.css index 2886243..e292e53 100644 --- a/apps/desktop/src/styles.css +++ b/apps/desktop/src/styles.css @@ -32,3 +32,7 @@ main{display:grid;grid-template-columns:minmax(0,1fr) 280px;min-height:0}.worksp .gateway-ready{display:flex;align-items:center;justify-content:space-between;gap:10px;margin:0 13px 8px;padding:7px 8px;border:1px solid #2b3547;background:#0b1018;color:#748096;font-size:.57rem}.gateway-ready.online{border-color:#2d6654;background:#0d1d18;color:#a4bdb5}.gateway-ready span{display:flex;gap:8px;align-items:center}.gateway-ready strong{font:750 .52rem var(--mono);letter-spacing:.08em;color:#6e7d93}.gateway-ready.online strong{color:var(--mint)}.gateway-ready button{border:1px solid #385164;border-radius:5px;background:#101a24;color:#b8c5d7;padding:4px 8px;font:700 .52rem var(--mono);cursor:pointer}.gateway-ready button:disabled{opacity:.4;cursor:not-allowed}.gateway-ready.online button{border-color:#3f806b;color:var(--mint);background:#122a22} .local-ai-autopilot,.fabric-proof{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:8px;margin:0 13px 8px;padding:7px 8px;border:1px solid #32405a;background:#0d131d;color:#8491a5;font-size:.57rem}.local-ai-autopilot>svg,.fabric-proof>svg{color:#8fa9cc}.local-ai-autopilot span,.fabric-proof span{min-width:0;display:flex;gap:8px;align-items:center}.local-ai-autopilot strong,.fabric-proof strong{flex:none;font:750 .52rem var(--mono);letter-spacing:.08em;color:#8fa9cc}.local-ai-autopilot small{font:700 .49rem var(--mono);letter-spacing:.05em;color:#687991}.local-ai-autopilot.ready{border-color:#2d6654;background:#0d1d18;color:#a4bdb5}.local-ai-autopilot.ready>svg,.local-ai-autopilot.ready strong{color:var(--mint)}.local-ai-autopilot.failed{border-color:#63333a;background:#1b1115;color:#c59ca1}.local-ai-autopilot.failed>svg,.local-ai-autopilot.failed strong{color:#ff9a9a}.local-ai-autopilot.installing,.local-ai-autopilot.pulling_model{border-color:#674c2b;background:#211911;color:#bfa580}.local-ai-autopilot.installing>svg,.local-ai-autopilot.pulling_model>svg,.local-ai-autopilot.installing strong,.local-ai-autopilot.pulling_model strong{color:#efb870}.fabric-proof button{border:1px solid #4a7088;border-radius:5px;background:#12202b;color:#b8d7e8;padding:4px 8px;font:700 .52rem var(--mono);cursor:pointer;white-space:nowrap}.fabric-proof button:disabled{opacity:.42;cursor:not-allowed}.fabric-proof.complete{border-color:#435b80;background:#0e1623;color:#aab9cf}.fabric-proof.complete>svg,.fabric-proof.complete strong{color:#9ccaff} .join-flow{display:grid;gap:10px}.join-state{position:relative;display:grid;justify-items:center;gap:7px;padding:18px;border:1px solid #2b3548;border-radius:11px;background:#0a0f17;color:#8794a8}.join-state>svg{color:#8a9ab1}.join-state strong{color:#e8effb;font-size:.8rem}.join-state span{font-size:.68rem;line-height:1.5}.join-state ul{margin:3px 0 0;padding-left:18px;text-align:left;font-size:.62rem;line-height:1.55}.join-state.active{border-color:#315849;background:#0d1b17}.join-state.active>svg,.join-state.success>svg{color:var(--mint)}.join-state.failed{border-color:#63333a;background:#1b1115}.join-state.failed>svg{color:#ff9a9a}.join-state output{font:800 2.25rem var(--mono);letter-spacing:.3em;color:var(--mint);padding-left:.3em;text-shadow:0 0 24px #76f7c555}.pairing-pulse{width:10px;height:10px;border-radius:50%;background:var(--mint);box-shadow:0 0 0 0 #76f7c566;animation:pairing-pulse 1.5s infinite}.cancel-join{justify-self:center;border:0;background:transparent;color:#8f9bad;text-decoration:underline;cursor:pointer;font-size:.66rem}.manual-invite{border-top:1px solid #252d3c;padding-top:9px;color:#78869a;font-size:.65rem}.manual-invite summary{cursor:pointer}.manual-invite textarea{margin-top:10px}.manual-invite button{margin-top:7px;border:1px solid #354157;border-radius:6px;background:#111925;color:#b8c4d5;padding:7px 10px;cursor:pointer}.pairing-panel{display:grid;gap:10px;margin-bottom:18px;padding-bottom:16px;border-bottom:1px solid var(--line)}.pairing-panel>header{display:flex;align-items:center;gap:9px;color:var(--mint)}.pairing-panel>header div{display:flex;flex-direction:column}.pairing-panel>header strong{font:750 .62rem var(--mono);letter-spacing:.08em}.pairing-panel>header span{font-size:.58rem;color:#7d899d;margin-top:2px}.pairing-searching{display:flex;gap:11px;align-items:center;padding:12px;border:1px solid #293649;border-radius:9px;background:#0d131d}.pairing-searching strong{font-size:.7rem}.pairing-searching p{margin:4px 0 0;color:#8491a5;font-size:.62rem;line-height:1.4}.pairing-searching small{display:block;margin-top:5px;color:#6f7e92;font-size:.56rem;line-height:1.4}.pairing-request{display:grid;gap:9px;padding:12px;border:1px solid #315849;border-radius:10px;background:#0d1b17}.pairing-device{display:flex;align-items:center;gap:8px}.pairing-device>svg{color:var(--mint)}.pairing-device div{display:flex;flex-direction:column}.pairing-device strong{font-size:.74rem}.pairing-device span{font-size:.57rem;color:#799187;text-transform:capitalize}.pairing-code{text-align:center;font:850 1.75rem var(--mono);letter-spacing:.28em;color:var(--mint);padding-left:.28em;text-shadow:0 0 20px #76f7c544}.pairing-request>p{display:flex;gap:6px;align-items:flex-start;margin:0;color:#8da39b;font-size:.59rem;line-height:1.45}.pairing-request>p svg{flex:none;color:var(--mint)}.pairing-actions{display:grid;grid-template-columns:.75fr 1.5fr;gap:6px}.pairing-actions button{display:flex;align-items:center;justify-content:center;gap:5px;border-radius:6px;padding:7px;border:1px solid #334052;background:#111925;color:#aab6c8;font-size:.58rem;cursor:pointer}.pairing-actions .pairing-approve{border-color:#3e806a;background:#153127;color:var(--mint)}.pairing-approved{display:flex;align-items:center;justify-content:center;gap:6px;color:var(--mint);font-size:.61rem}@keyframes pairing-pulse{70%{box-shadow:0 0 0 12px #76f7c500}100%{box-shadow:0 0 0 0 #76f7c500}} +.model-advanced{min-width:0;border:1px solid #273246;border-radius:7px;background:#0a0f17;padding:7px}.model-advanced>summary{cursor:pointer;color:#8f9db0;font:650 .58rem var(--mono);text-align:center;list-style-position:inside}.model-advanced[open]>summary{margin-bottom:7px;color:var(--mint)} +.onboarding-recovery{margin-top:12px;border:0;background:transparent;color:#8290a4;text-decoration:underline;cursor:pointer;font-size:.66rem} +.recovery-backdrop{position:fixed;inset:0;z-index:110;display:grid;place-items:center;padding:20px;background:#020408e8;backdrop-filter:blur(18px)}.recovery-center{position:relative;width:min(760px,calc(100vw - 40px));max-height:calc(100vh - 40px);overflow:auto;border:1px solid #344158;border-radius:16px;background:linear-gradient(145deg,#111925,#080c12);box-shadow:0 35px 130px #000;padding:20px}.recovery-center>header{display:flex;align-items:flex-start;justify-content:space-between;border-bottom:1px solid #283246;padding-bottom:14px}.recovery-center>header>div{display:flex;gap:11px;align-items:center}.recovery-center>header>div>svg{color:var(--mint)}.recovery-center>header .eyebrow{margin:0 0 4px}.recovery-center h1{margin:0;font-size:1.25rem}.recovery-center>header>button{width:32px;height:32px;display:grid;place-items:center;border:1px solid #344158;border-radius:7px;background:#111823;cursor:pointer}.recovery-loading{display:grid;justify-items:center;gap:10px;padding:42px;color:#8693a7;font-size:.72rem}.recovery-health{display:flex;align-items:center;gap:10px;margin:15px 0;padding:12px;border:1px solid #315849;border-radius:10px;background:#0d1b17}.recovery-health.attention{border-color:#674c2b;background:#211911}.recovery-health>svg{color:var(--mint);flex:none}.recovery-health.attention>svg{color:#efb870}.recovery-health>div{display:flex;flex-direction:column;gap:3px;flex:1}.recovery-health strong{font-size:.76rem}.recovery-health span{color:#8290a4;font-size:.62rem;text-transform:capitalize}.recovery-health button,.copy-diagnostics{display:flex;align-items:center;gap:5px;border:1px solid #3a4b63;border-radius:6px;background:#111a25;color:#b4c2d5;padding:7px 9px;cursor:pointer;font-size:.6rem}.recovery-issues{margin:0 0 14px;padding:10px 10px 10px 28px;border-left:2px solid #e2a85f;background:#1a150f;color:#d5b385;font-size:.67rem;line-height:1.5}.recovery-simple-actions{display:grid;grid-template-columns:1fr 1fr;gap:9px;margin:12px 0}.recovery-simple-actions>button{display:flex;align-items:center;gap:11px;text-align:left;border:1px solid #344158;border-radius:11px;background:#111923;color:#dce6f4;padding:14px;cursor:pointer}.recovery-simple-actions>button.recovery-primary{border-color:#3e806a;background:#123027;color:var(--mint)}.recovery-simple-actions span{display:flex;flex-direction:column;gap:3px}.recovery-simple-actions strong{font-size:.77rem}.recovery-simple-actions small{color:#8290a4;font-size:.59rem}.recovery-advanced{margin-top:14px;border-top:1px solid #283246;padding-top:12px}.recovery-advanced>summary{cursor:pointer;color:#8491a5;font-size:.66rem}.recovery-advanced[open]>summary{color:#b7c5d8;margin-bottom:12px}.recovery-actions{display:grid;gap:8px;margin:10px 0}.recovery-actions article{display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:10px;padding:11px;border:1px solid #2d384b;border-radius:9px;background:#0c121b}.recovery-actions article>svg{color:#93a6c1}.recovery-actions strong{font-size:.7rem}.recovery-actions p{margin:3px 0 0;color:#7f8da2;font-size:.59rem;line-height:1.45}.recovery-actions button,.recovery-devices button{border:1px solid #43526a;border-radius:6px;background:#121b27;color:#b7c4d6;padding:7px 9px;cursor:pointer;font-size:.59rem}.recovery-actions button.danger,.recovery-confirm button.danger{border-color:#733b46;background:#28151b;color:#ff9fa9}.recovery-devices{display:grid;gap:7px}.recovery-devices>div{display:flex;align-items:center;gap:8px;margin-bottom:2px}.recovery-devices>div>svg{color:var(--mint)}.recovery-devices>div>div{display:flex;flex-direction:column}.recovery-devices>div strong{font-size:.7rem}.recovery-devices>div span{font-size:.58rem;color:#7d8a9e}.recovery-devices article{display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:9px;padding:9px;border:1px solid #293448;border-radius:8px;background:#0b1119}.recovery-devices article>i{width:8px;height:8px;border-radius:50%;background:#646f80}.recovery-devices article>i.live{background:var(--mint);box-shadow:0 0 8px var(--mint)}.recovery-devices article>div{display:flex;flex-direction:column;gap:2px}.recovery-devices article strong{font-size:.67rem}.recovery-devices article span{color:#78869a;font-size:.56rem}.recovery-devices article small{color:#6f7c90;font:700 .5rem var(--mono)}.copy-diagnostics{margin-top:10px}.recovery-confirm{position:absolute;inset:0;z-index:2;display:grid;align-content:center;justify-items:center;text-align:center;padding:44px;background:#090d14f5;border-radius:16px}.recovery-confirm>svg{color:#ff9fa9}.recovery-confirm h2{margin:11px 0 5px}.recovery-confirm>p{max-width:520px;color:#8f9caf;font-size:.7rem;line-height:1.55}.recovery-confirm label{width:min(500px,100%);display:grid;gap:7px;margin:13px 0;color:#9aa7b9;font-size:.65rem}.recovery-confirm label strong{color:#ffabb4;font-family:var(--mono)}.recovery-confirm input{border:1px solid #583843;border-radius:7px;background:#100b0e;color:#fff;padding:10px;text-align:center;font-family:var(--mono)}.recovery-confirm>div{display:flex;gap:8px}.recovery-confirm button{border:1px solid #39475d;border-radius:7px;background:#121a25;padding:8px 13px;cursor:pointer}.recovery-confirm button:disabled{opacity:.45;cursor:not-allowed}.recovery-error{color:#ff9a9a;font-size:.66rem}.spin{animation:pairing-pulse 1.5s infinite} +.recovery-center{width:min(840px,calc(100vw - 40px));padding:24px}.recovery-center h1{font-size:1.45rem}.recovery-health strong,.recovery-simple-actions strong{font-size:.9rem}.recovery-health span,.recovery-simple-actions small,.recovery-issues,.recovery-advanced>summary{font-size:.75rem}.recovery-health button,.copy-diagnostics,.recovery-actions button,.recovery-devices button,.recovery-confirm button{min-height:40px;font-size:.72rem}.recovery-actions strong,.recovery-devices>div strong,.recovery-devices article strong{font-size:.82rem}.recovery-actions p,.recovery-devices>div span,.recovery-devices article span,.recovery-confirm>p,.recovery-confirm label,.recovery-error{font-size:.72rem}.recovery-devices article small{font-size:.62rem}.recovery-center button:focus-visible,.recovery-center summary:focus-visible,.recovery-center input:focus-visible{outline:2px solid var(--mint);outline-offset:3px}.onboarding-recovery{min-height:40px;font-size:.75rem}@media(max-width:650px){.recovery-simple-actions{grid-template-columns:1fr}.recovery-actions article{grid-template-columns:auto 1fr}.recovery-actions article>button{grid-column:1/3}.recovery-center{padding:18px}} diff --git a/apps/edge/package.json b/apps/edge/package.json index a7c3613..216fe2c 100644 --- a/apps/edge/package.json +++ b/apps/edge/package.json @@ -1,6 +1,6 @@ { "name": "@rampage/edge", - "version": "0.3.0", + "version": "0.3.1", "private": true, "type": "module", "scripts": { diff --git a/apps/edge/src-tauri/tauri.conf.json b/apps/edge/src-tauri/tauri.conf.json index 7bda58b..1c16b01 100644 --- a/apps/edge/src-tauri/tauri.conf.json +++ b/apps/edge/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Rampage Edge", - "version": "0.3.0", + "version": "0.3.1", "identifier": "ai.obtuse.rampage.edge", "build": { "beforeDevCommand": "pnpm dev", diff --git a/crates/rampage-controller/src/main.rs b/crates/rampage-controller/src/main.rs index eb9dddb..07a2801 100644 --- a/crates/rampage-controller/src/main.rs +++ b/crates/rampage-controller/src/main.rs @@ -265,6 +265,12 @@ struct RemoteDesktopInputRequest { events: Vec, } +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct RevokeNodeRequest { + confirmation: String, +} + const DEFAULT_MESH_UDP_PORT: u16 = 47_838; #[tokio::main] @@ -389,6 +395,7 @@ async fn main() -> anyhow::Result<()> { .route("/v1/resume", post(local_resume)) .route("/v1/enrollment/invites", post(create_invite)) .route("/v1/nodes", get(list_nodes)) + .route("/v1/nodes/{node_id}/revoke", post(revoke_node)) .route("/v1/nodes/enroll", post(enroll_node)) .route("/v1/offers", get(list_offers).post(register_offer)) .route("/v1/workload-capabilities", get(list_workload_capabilities)) @@ -617,6 +624,103 @@ async fn list_nodes( )) } +async fn revoke_node( + State(state): State, + Path(node_id): Path, + Json(request): Json, +) -> Result<(StatusCode, Json), (StatusCode, Json)> { + if request.confirmation != format!("FORGET {node_id}") { + return Err(( + StatusCode::FORBIDDEN, + Json(json!({"error": "exact device revocation confirmation is required"})), + )); + } + let _admission_guard = state.admission_gate.lock().await; + let identity = state + .nodes + .read() + .map_err(lock_error)? + .get(&node_id) + .cloned() + .ok_or_else(|| { + ( + StatusCode::NOT_FOUND, + Json(json!({"error": "node is not enrolled"})), + ) + })?; + let remote_sessions_closed = state + .remote_desktop_sessions + .read() + .map_err(lock_error)? + .values() + .filter(|session| session.lease.node_id == node_id) + .count(); + state + .ledger + .append( + "node.revoked", + &node_id.to_string(), + &json!({ + "node_id": node_id, + "display_name": identity.display_name, + "public_key": identity.public_key, + "remote_assist_sessions_closed": remote_sessions_closed, + "revoked_at": chrono::Utc::now() + }), + ) + .map_err(internal_error)?; + state.nodes.write().map_err(lock_error)?.remove(&node_id); + state.offers.write().map_err(lock_error)?.remove(&node_id); + state + .assignments + .write() + .map_err(lock_error)? + .retain(|_, assignment| assignment.lease.node_id != node_id); + state + .reservations + .write() + .map_err(lock_error)? + .retain(|reservation| reservation.node_id != node_id); + state + .shard_sets + .write() + .map_err(lock_error)? + .retain(|_, shard| shard.leases.iter().all(|lease| lease.node_id != node_id)); + state + .remote_desktop_sessions + .write() + .map_err(lock_error)? + .retain(|_, session| session.lease.node_id != node_id); + state + .artifact_replicas + .write() + .map_err(lock_error)? + .retain(|(_, replica_node), _| *replica_node != node_id); + state + .replica_evidence + .write() + .map_err(lock_error)? + .retain(|(_, replica_node), _| *replica_node != node_id); + for cancellation in state.model_cancellations.lock().await.values() { + let _ = cancellation.send(true); + } + refresh_diagnostics(&state).map_err(|error| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({"error": error})), + ) + })?; + Ok(( + StatusCode::OK, + Json(json!({ + "schema": "rampage.node-revocation-receipt.v1", + "node_id": node_id, + "revoked": true, + "remote_assist_sessions_closed": remote_sessions_closed + })), + )) +} + async fn health(State(state): State) -> Json { let killed = state.kill_latch_path.is_file(); Json(Health { @@ -5335,7 +5439,7 @@ fn restore_state(ledger: &Ledger, fencing_epoch: u64) -> anyhow::Result = HashMap::new(); let mut assignments: HashMap = HashMap::new(); let mut completed_receipts = HashMap::new(); - let mut shard_sets = HashMap::new(); + let mut shard_sets: HashMap = HashMap::new(); let mut artifact_replicas = HashMap::new(); let mut replica_evidence = HashMap::new(); let now = chrono::Utc::now(); @@ -5352,6 +5456,19 @@ fn restore_state(ledger: &Ledger, fencing_epoch: u64) -> anyhow::Result { + let Ok(node_id) = Uuid::parse_str(&event.subject_id) else { + continue; + }; + nodes.remove(&node_id); + offers.remove(&node_id); + assignments.retain(|_, assignment| assignment.lease.node_id != node_id); + shard_sets.retain(|_, shard| { + shard.leases.iter().all(|lease| lease.node_id != node_id) + }); + artifact_replicas.retain(|(_, replica_node), _| *replica_node != node_id); + replica_evidence.retain(|(_, replica_node), _| *replica_node != node_id); + } "resource.offer.registered" => { let offer: ResourceOfferV1 = serde_json::from_value(event.payload)?; if offer.expires_at > now { @@ -5879,6 +5996,44 @@ mod tests { assert!(validate_model_runtime_contracts(&offer).is_err()); } + #[test] + fn revoked_node_and_its_live_offer_do_not_return_after_restart() { + let ledger = Ledger::in_memory().unwrap(); + let offer = valid_model_offer(); + let identity = NodeIdentityV1 { + schema: NodeIdentityV1::SCHEMA.into(), + node_id: offer.node_id, + owner_id: Uuid::now_v7(), + display_name: "retired laptop".into(), + device_kind: DeviceKind::Desktop, + platform: "windows-x86_64".into(), + public_key: "b".repeat(64), + enrolled_at: chrono::Utc::now(), + fencing_epoch: 0, + }; + ledger + .append("node.enrolled", &identity.node_id.to_string(), &identity) + .unwrap(); + ledger + .append( + "resource.offer.registered", + &identity.node_id.to_string(), + &offer, + ) + .unwrap(); + ledger + .append( + "node.revoked", + &identity.node_id.to_string(), + &json!({"node_id": identity.node_id}), + ) + .unwrap(); + + let (nodes, offers, ..) = restore_state(&ledger, 0).unwrap(); + assert!(!nodes.contains_key(&identity.node_id)); + assert!(!offers.contains_key(&identity.node_id)); + } + #[test] fn edge_offer_is_bound_to_enrolled_device_class_and_battery_telemetry() { let mut offer = valid_model_offer(); diff --git a/crates/rampage-sdk/src/lib.rs b/crates/rampage-sdk/src/lib.rs index d82e183..1c7036b 100644 --- a/crates/rampage-sdk/src/lib.rs +++ b/crates/rampage-sdk/src/lib.rs @@ -4,7 +4,7 @@ use anyhow::Context; use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; use rampage_protocol::{ ArtifactRefV1, CapabilityLeaseV1, EnrollmentInviteV1, ExecutionReceiptV1, JobSpecV1, - ModelSessionRequestV1, ResourceOfferV1, StorageClass, + ModelSessionRequestV1, NodeIdentityV1, ResourceOfferV1, StorageClass, }; use serde::{Serialize, de::DeserializeOwned}; use serde_json::Value; @@ -53,6 +53,18 @@ impl RampageClient { .await } + pub async fn nodes(&self) -> anyhow::Result> { + self.get("/v1/nodes").await + } + + pub async fn revoke_node(&self, node_id: uuid::Uuid) -> anyhow::Result { + self.post( + &format!("/v1/nodes/{node_id}/revoke"), + &serde_json::json!({"confirmation": format!("FORGET {node_id}")}), + ) + .await + } + pub async fn plan(&self, job: &JobSpecV1) -> anyhow::Result { self.post("/v1/jobs/plan", job).await } diff --git a/docs/PLATFORM_MATRIX.md b/docs/PLATFORM_MATRIX.md index 1c913aa..67ae982 100644 --- a/docs/PLATFORM_MATRIX.md +++ b/docs/PLATFORM_MATRIX.md @@ -5,7 +5,7 @@ binary release claim. | Platform | Installable package | Controller | Worker | Intended donation profile | Evidence | | --- | --- | --- | --- | --- | --- | -| Windows 11 x64 | MSI and NSIS 0.3.0 Remote Assist candidate | Shipped | Shipped | CPU, RAM cache, NVIDIA GPU/VRAM, disk, exact local Ollama models; optional paired-owner desktop view/control | 0.2.3 packaged owner/worker lifecycle proof plus source-qualified 0.3.0 Remote Assist contracts, authenticated transport, worker opt-in, visible active state, short signed leases, replay fencing, and STOP/revoke; exact 0.3.0 package hashes and physical laptop control remain gated by the release evidence | +| Windows 11 x64 | MSI and NSIS 0.3.1 recovery candidate | Shipped | Shipped | CPU, RAM cache, NVIDIA GPU/VRAM, disk, exact local Ollama models; optional paired-owner desktop view/control | 0.3.1 adds repair, pair-again, owner Forget, factory reset, restart-safe revocation replay, and an outcome-first automatic strategy UI to the qualified 0.3.0 Remote Assist contracts; exact package hashes and physical laptop control remain gated by the release evidence | | Windows 10 x64 | Not qualified | Source likely portable | Source likely portable | Same as Windows 11, hardware-dependent | Unexecuted | | macOS Apple Silicon | Not shipped | Rust source portable | Rust source portable | CPU/GPU whole jobs, RAM cache, disk | Unexecuted; native packaging and discovery adapters required | | Linux x64 | Not shipped | Rust source portable | Rust source portable | CPU, vendor GPU/VRAM, RAM cache, disk | Unexecuted; GTK dependency warning must be resolved or accepted before release | @@ -20,7 +20,7 @@ packaging on its named GitHub-hosted runner; it does not convert an unsigned can release. Stable Windows and macOS publication additionally requires real platform credentials and independent signature/notarization verification. -The installable column describes the current 0.3.0 candidate lane. Later source-qualified capabilities +The installable column describes the current 0.3.1 candidate lane. Later source-qualified capabilities do not become packaged or signed merely because they pass a source campaign; each evidence page states that boundary explicitly. diff --git a/docs/RELEASE_EVIDENCE_0.3.1.md b/docs/RELEASE_EVIDENCE_0.3.1.md new file mode 100644 index 0000000..56ef78c --- /dev/null +++ b/docs/RELEASE_EVIDENCE_0.3.1.md @@ -0,0 +1,46 @@ +# Rampage 0.3.1 release evidence + +This is the qualification ledger for the Rampage 0.3.1 recovery release. Source tests, native +packages, public artifacts, and physical two-machine behavior are separate claims. + +## Qualification status + +| Gate | Command or artifact | Result | +| --- | --- | --- | +| Nine release versions | `scripts/Assert-RampageVersion.ps1 -Tag v0.3.1` | PASS — nine surfaces report 0.3.1 | +| Rust workspace compile | `cargo check --workspace --all-targets` | PASS | +| Controller lifecycle | `cargo test -p rampage-controller --bin rampage-controller` | PASS — 20 tests, including restart-safe node revocation | +| Native desktop recovery | `cargo test --workspace --no-fail-fast` | PASS — 19 desktop tests inside the full workspace campaign | +| Desktop UI and recovery | `pnpm --dir apps/desktop test -- --run` | PASS — 18 tests | +| TypeScript SDK | `pnpm --dir packages/sdk-ts test -- --run` | PASS — 12 tests | +| Python SDK | `uv run --project packages/sdk-python --with pytest --with httpx python -m pytest packages/sdk-python/tests -q` | PASS — 11 tests | +| Full workspace tests and policy | `cargo test --workspace --no-fail-fast`; `cargo clippy --workspace --all-targets -- -D warnings`; `scripts/Assert-RustSecBaseline.ps1` | PASS — all tests; no clippy warnings; 0 RustSec vulnerabilities and 18 target-reviewed warnings through 2026-10-31 | +| Desktop, edge, and TypeScript builds | `pnpm check` | PASS — desktop 18, edge 2, SDK 12 tests plus all production builds | +| Proposal-only intelligence | Ruff, mypy, and pytest | PASS — Ruff clean, mypy clean across 9 files, 17 tests | +| NSIS installer and desktop shortcut | `scripts/Smoke-RampageInstaller.ps1` | PASS — install 0, uninstall 0, six payloads, controller/intelligence ready, one signed node and offer, shortcut created then removed, no leaked sidecars | +| Public release assets | GitHub release | PENDING publication | +| Physical owner/laptop re-pair | Fresh 0.3.1 installs | PENDING physical laptop action | +| Physical owner-to-laptop view | `scripts/Qualify-RampageRemoteAssist.ps1 -ExpectedVersion 0.3.1` | PENDING live opted-in worker | + +## Locally qualified artifacts + +These packages came from the verified local source tree and passed the independent installer smoke +test. GitHub Actions builds the public release artifacts again from the tagged commit, so the public +asset hashes will be recorded separately after publication. + +| Package | Bytes | SHA-256 | +| --- | ---: | --- | +| `Rampage_0.3.1_x64_en-US.msi` | 79,290,368 | `a02b5995e082eb7be371f675ed80d5b0640a16499eb9de1b8e0f093ac7cd06ee` | +| `Rampage_0.3.1_x64-setup.exe` | 69,374,688 | `e2fffa0326e6a6e3322b294433911d94f47ef9bbd40e0647857ce52bc899a5a5` | + +The source-current Recovery Center capture is +`docs/assets/rampage-recovery-center.png`, SHA-256 +`f5503739ce0f1a069ea067a9bb807639d541872f7f6487a12efcb46fa414366d`. + +## Honest boundary + +The Recovery Center screenshot is a browser-rendered view of the real React component using labeled +showcase topology. Rust tests cover local reset target validation and identity cleanup; controller +tests cover revocation replay. The final physical line requires the owner and laptop to install the +same 0.3.1 package, pair over the real network, advertise a fresh signed worker offer, and complete +the fail-closed Remote Assist qualifier. diff --git a/docs/RELEASE_NOTES_0.3.1.md b/docs/RELEASE_NOTES_0.3.1.md new file mode 100644 index 0000000..54ea269 --- /dev/null +++ b/docs/RELEASE_NOTES_0.3.1.md @@ -0,0 +1,56 @@ +# Rampage 0.3.1 — Recovery without rituals + +Rampage 0.3.1 closes the device lifecycle that a real multi-machine desktop product needs. A broken, +stale, or interrupted pairing no longer requires uninstalling the app or finding hidden runtime +files. Recovery is available from both first-run setup and the main header. + +## What changed + +- **Fix Rampage** safely restarts a consistent native installation without erasing identity or work. +- **Pair again** stops the worker sidecars, removes its old fabric runtime, and returns the device to + the two-button nearby-pairing screen. +- Owner **Forget** revokes one exact enrolled identity and removes its offer, outstanding assignments, + reservations, shard sets, Remote Assist sessions, artifact locations, and possession evidence. +- Revocation is appended to the hash-chained ledger and replayed after controller restart, so a stale + node and offer do not reappear. +- **Factory reset** disables Rampage auto-start and clears only the bounded Rampage runtime after the + user types `RESET RAMPAGE`. External Ollama model storage is not deleted. +- A redacted recovery receipt makes support diagnosis copyable without exposing controller secrets or + private keys. +- SDK lifecycle methods are available in Rust, TypeScript, and Python for loopback owner automation. + +## Simpler compute outcomes + +Rampage now defaults to **Automatic** and auto-assigns technical roles from signed hardware, runtime, +link, power, thermal, and workload facts. Users can override the outcome with four plain choices: +**Biggest AI**, **Fastest AI**, **More Work**, or **Protect This PC**. Detailed model sizing remains in +the advanced disclosure instead of occupying the default surface. + +The strategies do not claim that commodity networks create physically shared RAM or VRAM. Whole +models, replicas, independent shards, caches, relays, and engine-native distributed layouts remain +separate lanes with separate qualification gates. The research and delivery sequence is in the +[universal fabric blueprint](UNIVERSAL_FABRIC_BLUEPRINT.md). + +## Security and lifecycle boundaries + +- Exact destructive confirmations are checked again by native Rust/controller code; the React dialog + is not the security boundary. +- Reset refuses paths outside the exact Rampage app-runtime shape and refuses a redirected runtime + root. +- Setup mode launches no fabric sidecars until the device creates or joins a fabric. +- Worker Remote Assist is still off by default, visible when active, limited to a paired owner, and + bounded by signed 30-second leases plus STOP/revoke. +- Intelligence remains proposal-only. Automatic improvements can act inside the owner's standing + envelope; authority-expanding proposals are automatically denied. + +## Install and recover + +1. Install Rampage on the owner PC and laptop with the Windows installer. The installer creates the + desktop shortcut. +2. Create the fabric on the owner, then choose **Join my fabric** and **Find my fabric** on the laptop. +3. Approve the matching four digits on the owner. +4. If the laptop ever retains a stale identity, choose **Fix Rampage → Pair again**, then approve it + again. On the owner, use **Advanced recovery → Forget** for the stale enrolled entry. + +Package hashes, verification commands, and the remaining physical two-machine gate are recorded in +[the 0.3.1 release evidence](RELEASE_EVIDENCE_0.3.1.md). diff --git a/docs/SHA256SUMS-0.3.1.txt b/docs/SHA256SUMS-0.3.1.txt new file mode 100644 index 0000000..c4c13e3 --- /dev/null +++ b/docs/SHA256SUMS-0.3.1.txt @@ -0,0 +1,3 @@ +a02b5995e082eb7be371f675ed80d5b0640a16499eb9de1b8e0f093ac7cd06ee Rampage_0.3.1_x64_en-US.msi +e2fffa0326e6a6e3322b294433911d94f47ef9bbd40e0647857ce52bc899a5a5 Rampage_0.3.1_x64-setup.exe +f5503739ce0f1a069ea067a9bb807639d541872f7f6487a12efcb46fa414366d rampage-recovery-center.png diff --git a/docs/UNIVERSAL_FABRIC_BLUEPRINT.md b/docs/UNIVERSAL_FABRIC_BLUEPRINT.md new file mode 100644 index 0000000..1630d83 --- /dev/null +++ b/docs/UNIVERSAL_FABRIC_BLUEPRINT.md @@ -0,0 +1,192 @@ +# Rampage universal fabric blueprint + +This is the engineering map for making Rampage broadly useful without confusing networked machines +with one physically coherent motherboard. It separates what is **shipped**, what can be **qualified by +an adapter**, and what remains **experimental**. A capability does not gain execution authority merely +because it appears in this document. + +## The one-screen product + +Rampage should make the technical decisions and expose outcomes: + +| Choice | User intent | Automatic fabric behavior | +| --- | --- | --- | +| **Automatic** | Make this machine and its work feel better | Continuously choose the highest measured benefit within the owner's standing limits | +| **Biggest AI** | Run the largest model that can be served correctly | Prefer one complete-model host, then qualified pipeline/tensor topologies only when memory and link gates pass | +| **Fastest AI** | Minimize time to first and next token | Prefer the fastest whole-model host, replicas, prefix locality, speculative work, and disaggregated stages only when measured faster | +| **More Work** | Maximize total completed work | Replicate services and spread independent agents, batches, builds, renders, evaluations, and transforms | +| **Protect This PC** | Keep the foreground game, call, render, or creative session smooth | Evacuate background work, cap local pressure, and spend remote capacity first | + +There is no manual CPU/GPU/cache/relay role picker in the normal path. Every device advertises +facts; the controller assigns short-lived roles per workload. Advanced controls remain available for +diagnosis, but a new user sees one recommendation and four understandable alternatives. + +## The automatic role engine + +```mermaid +flowchart LR + O["Observe\nhardware · power · thermals · runtimes"] --> B["Benchmark\ncompute · storage · path · codec"] + B --> D["Describe the work\nlatency · memory · locality · checkpointing"] + D --> P["Place roles\nmodel · shard · cache · relay · standby"] + P --> L["Lease\nexact operation · budget · expiry · fencing"] + L --> M["Measure\nlatency · throughput · failure · foreground impact"] + M --> A["Adapt\nkeep winner · rollback loser · quarantine failure"] + A --> D +``` + +A laptop can be a whole-model server for one request, a build worker for the next, and a cache or +relay later. A phone normally becomes a control surface, sensor source, foreground-safe evaluator, +or small-model worker—not fake shared VRAM. A GPU server can advertise several independently +qualified engines without giving Rampage shell access to the host. + +The owner defines the standing envelope once. Inside it, deterministic thresholds can adapt without +per-change prompts. A proposed change that tries to widen authority is denied rather than waiting for +the AI to approve itself. Pairing, worker Remote Assist opt-in, and STOP remain independent owner +security boundaries. + +## One capability market, several execution lanes + +| Lane | Best use | Admission rule | State | +| --- | --- | --- | --- | +| Whole-model placement | One interactive local LLM, game server, compiler, renderer, or tool on the strongest node | Exact installed runtime/model or application adapter; signed offer and result | Shipped for bounded Ollama text and independent jobs | +| Replicated service | Higher AI throughput, many agents, failover, render/build farms | Compatible artifacts and runtime digest; health and saturation routing | Shipped contract; adapters expand independently | +| Independent shards | Search, evaluation, tests, simulation, preprocessing, transcode, compilation units | Deterministic partition, deadline, minimum-success threshold, signed receipts | Shipped | +| Cross-node model | A model that cannot fit one qualified GPU/node | Engine-native tensor or pipeline plan; compatible devices; topology and failure proof | Planner shipped; execution gated | +| Prefill/decode split | Long prompts or high concurrency on differently shaped GPU pools | KV format match and transfer benchmark must beat aggregated serving | Experimental adapter | +| Speculative lane | Faster decode using spare smaller-model capacity | Tokenizer/model compatibility and measured acceptance/speed gain | Experimental adapter | +| Cache and storage | Model layers, build outputs, datasets, checkpoints, prefix/KV state where supported | Encrypted content addressing, quota, possession evidence, expiration | Artifact fabric shipped; engine caches gated | +| Interactive desktop | Help, administration, remote creative work, and game streaming | Paired identity, explicit worker opt-in, visible bounded session, codec/path qualification | Remote Assist shipped; low-latency media lane next | +| Network utility | Direct-path probe, private relay, store-and-forward, cache prepositioning | Owner-signed membership, rate caps, no arbitrary forwarding | Direct QUIC and owner relay shipped | + +Ray Serve LLM documents tensor, pipeline, expert, replica, data-parallel attention, prefix-aware +routing, and prefill/decode patterns across nodes. NVIDIA Dynamo likewise separates prefill and +decode, but explicitly treats KV transfer as the critical path and warns that TCP fallback can make +the split slower. Rampage should therefore qualify these as engine adapters, never as a universal +promise. See the [Ray Serve LLM architecture](https://docs.ray.io/en/latest/serve/llm/architecture/overview.html), +[Ray cross-node parallelism](https://docs.ray.io/en/latest/serve/llm/user-guides/cross-node-parallelism.html), +and [NVIDIA Dynamo disaggregated serving guide](https://docs.nvidia.com/dynamo/latest/user-guides/disaggregated-serving). + +### The large-model decision tree + +1. If the model fits one live qualified node, place the complete model there. This normally gives the + most predictable interactive latency. +2. If several nodes hold the complete model and traffic is concurrent, replicate and route by load, + prefix locality, health, and energy/foreground cost. +3. If the model does not fit one node, search only compatible engine-native tensor/pipeline layouts. +4. Reject a layout when collective traffic, weakest-rank compute, transfer time, or failure recovery + predicts a loss. Aggregate memory is a capacity ceiling, not proof of useful serving. +5. Evaluate prefill/decode separation and speculative decoding as measured alternatives, not assumed + upgrades. + +The llama.cpp RPC backend is not an acceptable shortcut today. Its own documentation calls it a +fragile, insecure proof of concept, and the project's current security guidance says not to expose +the RPC backend on untrusted networks. A critical unauthenticated RCE advisory lists no patched +version. Rampage must not launch or expose raw RPC until an independently reviewed safe upstream +version exists and passes the mesh adapter campaign. Sources: +[llama.cpp security guidance](https://github.com/ggml-org/llama.cpp/security), +[RPC proof-of-concept warning](https://github.com/ggml-org/llama.cpp/blob/master/tools/rpc/README.md), +and [GHSA-j8rj-fmpv-wcxw](https://github.com/ggml-org/llama.cpp/security/advisories/GHSA-j8rj-fmpv-wcxw). + +## Network Autopilot + +Rampage should optimize the path as carefully as the placement: + +1. Probe direct IPv4/IPv6 and owner-relay candidates in parallel; retain authenticated path identity. +2. Measure RTT distribution, jitter, loss, reordering, goodput, MTU behavior, and sustained—not burst— + transfer speed per direction. +3. Classify traffic: interactive input, video/audio, tokens, collectives, artifact transfer, evidence, + and background repair do not share one congestion goal. +4. Choose direct or relay per session, then migrate only with continuity and replay fencing. +5. Apply adaptive bitrate, resolution, frame rate, codec, keyframe cadence, and bounded forward error + correction to media; do not retransmit stale interactive frames. +6. Preposition immutable model/artifact chunks and resume by verified chunk digest rather than moving + the same bytes for every job. +7. Reserve foreground headroom. **Protect This PC** lowers or evacuates background traffic when game, + call, production, battery, thermal, or input-latency signals cross thresholds. +8. Continuously compare the observed result with the predicted result. Roll back routes or placements + whose gain disappears. + +These ideas follow observable industry patterns without copying proprietary implementations. +Parsec exposes separate network, encode, and decode latency and adjusts bitrate against congestion; +Sunshine exposes bitrate, hardware encoder, low-latency, and FEC controls; Microsoft notes that +remote-session quality depends strongly on available network capacity. Sources: +[Parsec latency guidance](https://support.parsec.app/hc/en-us/articles/32381352822804-Troubleshooting-Lag-Latency-and-Quality-Issues), +[Sunshine configuration](https://docs.lizardbyte.dev/projects/sunshine/latest/md_docs_2configuration.html), +and [Microsoft RDS network guidance](https://learn.microsoft.com/en-us/windows-server/remote/remote-desktop-services/network-guidance). + +## Remote experience plane + +Remote access products reveal three separate contracts that Rampage should keep separate: + +| Contract | Rampage meaning | +| --- | --- | +| Connection eligibility | Only an identity paired into this owner fabric can request a session | +| In-session permission | View, keyboard/mouse, clipboard, file transfer, audio, controller, and administration are separate capabilities | +| Lifecycle and revocation | Active indication, expiry, disconnect, worker opt-out, owner forget, STOP, leave fabric, and factory reset | + +RustDesk documents the same useful separation between access eligibility and in-session control +roles. Windows RDS demonstrates full desktop versus individually published applications. Rampage's +next remote adapter should add permission-scoped clipboard/file/audio/gamepad and application-window +streaming, while defaulting to the smallest capability that satisfies the task. Sources: +[RustDesk access control](https://rustdesk.com/docs/en/self-host/rustdesk-server-pro/permissions/), +[RustDesk control roles](https://rustdesk.com/docs/en/self-host/rustdesk-server-pro/control-role/), +and [Microsoft RDS overview](https://learn.microsoft.com/en-us/windows-server/remote/remote-desktop-services/overview). + +The low-latency lane should use native capture, hardware encode/decode when available, a zero-copy +GPU path where proven, cursor/input prioritization, multi-monitor/window selection, audio and +gamepad channels, quality adaptation, and a visible latency breakdown. It must retain Rampage's +paired identity, short lease, worker indicator, opt-out, and STOP semantics. + +## Self-improvement loop + +Rampage can improve automatically without giving an AI administrator authority: + +1. **Detect:** compare predictions to signed receipts, path telemetry, foreground impact, crashes, + thermal throttling, retry storms, cache misses, and unused capacity. +2. **Diagnose:** produce a typed hypothesis with affected adapter, expected gain, rollback trigger, + and evidence digest. +3. **Experiment:** replay or shadow first; then use a small traffic-, resource-, time-, and node-capped + canary lease for allowlisted low-risk changes. +4. **Decide:** the Rust Governor checks immutable thresholds. All required gates pass or nothing is + promoted. +5. **Watch:** compare canary and baseline with confidence and minimum sample requirements. Roll back + automatically on any guardrail breach. +6. **Remember:** store proposal, environment, artifacts, outcomes, and failure class by content digest + so the fabric does not repeat losing experiments. + +No AI-issued peer enrollment, signing key access, lease minting, policy-envelope edits, STOP bypass, +or destructive machine access is part of this loop. Within the existing envelope, the system acts +without per-change approval; outside it, it fails closed. + +## Qualification campaigns + +Every new adapter must publish a machine-readable campaign covering: + +- correctness against a single-node baseline; +- cold and warm latency, sustained throughput, and time-to-first-token/frame; +- weakest-link and heterogeneous-device behavior; +- restart, disconnect, packet loss, stale lease, duplicate request, and partial-result recovery; +- resource ceilings, foreground impact, thermal/battery response, and storage wear limits; +- artifact/model integrity, secret isolation, authenticated transport, and replay fencing; +- rollback and STOP behavior; +- a measured break-even surface that tells Autopilot when **not** to distribute. + +The promotion unit is not “backend installed.” It is `(backend digest, operation, hardware class, +driver/runtime compatibility, topology class, security profile, campaign digest)`. Unknown +combinations remain candidate-only. + +## Delivery sequence + +| Stage | Deliverable | Completion evidence | +| --- | --- | --- | +| 0.3.1 | Recovery Center, Pair again, owner Forget, factory reset, automatic outcome-first UI, SDK lifecycle API | Native package, restart-safe tests, two-machine re-pair and Remote Assist receipt | +| Next | Network Autopilot v1 and remote media telemetry | Direct/relay path race, adaptive media proof, visible encode/network/decode latency | +| Next | Qualified multi-GPU server adapter | Homogeneous CUDA tensor/pipeline campaign that beats or enables the single-node baseline | +| Next | Replicated AI service and prefix-aware routing | Concurrent-load proof, failover, cache-hit evidence, bounded autoscaling | +| Experimental | Prefill/decode and speculative lanes | KV/token compatibility, transfer break-even, quality equality, rollback proof | +| Experimental | Production/gaming adapters | Per-application operation contract and foreground-impact proof; no generic FPS promises | +| Experimental | Expanded mobile roles | Physical battery, thermal, lifecycle, network, and store-signing qualification | + +This ordering makes Rampage immediately easier to recover, then progressively faster and broader. +The interface can stay simple because complexity lives in signed capability discovery, measurements, +and adapter qualification—not in a settings maze. diff --git a/docs/assets/rampage-recovery-center.png b/docs/assets/rampage-recovery-center.png new file mode 100644 index 0000000000000000000000000000000000000000..0e18a3bf97344d11f223e7a1ae566dcd68a8db99 GIT binary patch literal 160945 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⋙C{FYm=e_+jS9$=JzuBTWjEX~;mt#oy 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 P0i?*&)z4*}Q$iB}ES$zW literal 0 HcmV?d00001 diff --git a/docs/download/index.html b/docs/download/index.html index ac43cef..136f314 100644 --- a/docs/download/index.html +++ b/docs/download/index.html @@ -3,16 +3,16 @@ - - + + Download Rampage for Windows diff --git a/docs/index.html b/docs/index.html index 2c23df4..ea22da2 100644 --- a/docs/index.html +++ b/docs/index.html @@ -44,7 +44,7 @@

FABRIC LIVE · OWNER CONTROLLED

Your machines.
Acting as one.

-

Rampage turns the computers and edge devices you already own into a private compute fabric, qualifies local AI automatically, serves it through OpenAI, Anthropic, or OpenRouter-style clients, proves useful capacity with signed execution receipts, and can visibly assist an opted-in paired Windows worker from the owner PC.

+

Rampage turns the computers and edge devices you already own into a private compute fabric, qualifies local AI automatically, serves it through OpenAI, Anthropic, or OpenRouter-style clients, proves useful capacity with signed execution receipts, visibly assists an opted-in paired Windows worker, and makes broken pairing recoverable in two clicks.

Download Rampage for Windows See how it works @@ -99,7 +99,7 @@

The hardware is already there.
The missing piece was trust.<

- WINDOWS X64 · 0.3.0 REMOTE ASSIST CANDIDATE

MSI + NSIS

+ WINDOWS X64 · 0.3.1 RECOVERY CANDIDATE

MSI + NSIS

The installer creates the desktop launcher, bundles the governed services, qualifies a pinned local AI runtime, and hands ongoing control to the role-aware system tray.

  • Signed sustained-compute receipts
  • Optional paired-owner Remote Assist
  • Unsigned until Authenticode credentials exist
@@ -130,6 +130,11 @@

The hardware is already there.
The missing piece was trust.< Rampage owner grid with a paired Studio Laptop selected and explicit View desktop and Control desktop actions
One visible session per worker · 30-second signed lease · pinned paired owner · worker toggle + STOP revoke
+
+
RECOVERY CENTER · COMPLETE LIFECYCLE
+ Rampage Recovery Center with Fix Rampage, enrolled machines, stale-device Forget, and factory reset +
Repair in place · pair again · revoke stale identity · typed bounded factory reset
+

This Remote Assist image is a source-current showcase with labeled demonstration topology, not a physical two-machine receipt. Windows capture/input, lease, digest, replay-fence, and UI contracts are tested separately. Rampage cannot cross UAC, the lock screen, the secure desktop, or higher-integrity applications.

Windows 10 remains an explicit real-machine gate: the dedicated self-hosted workflow must prove the operating-system caption, installer, desktop launcher, tray lifecycle, sidecars, restart behavior, STOP, and uninstall. A Windows Server build is useful candidate evidence; it is not relabeled as Windows 10 proof.

@@ -193,18 +198,18 @@

Useful compute.
No pretending.

-

COMPUTE STRATEGY

Biggest model.
Fastest chat. Different proof.

-

The new strategy plane previews exactly how compatible memory and measured links should be used—without turning a placement guess into execution authority.

+

COMPUTE STRATEGY

Automatic first.
Four clear outcomes.

+

Rampage assigns technical roles from signed device, runtime, link, power, thermal, and workload facts. People choose the outcome, not a settings maze.

-
DEFAULT FOCUS01

Maximum Model

Combine qualified compatible model memory to fit an LLM no single machine can hold.

-
MEASURED SPEED02

Speed Boost

Use tensor peers only when topology predicts faster tokens. Otherwise, stay on the fastest node.

-
CONCURRENCY03

Throughput

Place whole-model replicas for simultaneous users, agents, and batched work.

-
OWNER FIRST04

Efficiency

Choose the smallest qualified fit and preserve energy, thermals, and responsiveness.

-
NO PER-CHANGE PROMPT05

Autonomous

Adapt from signed evidence inside an owner envelope; reject authority expansion automatically.

+
RECOMMENDED01

Automatic

Measure every device and path, then continuously choose the safest high-value role.

+
CAPACITY02

Biggest AI

Fit the largest qualified model; use cross-node layouts only when an engine-specific campaign passes.

+
INTERACTIVE03

Fastest AI

Reject slow peers and optimize whole-model, replica, prefix, speculative, or split-phase lanes by proof.

+
CONCURRENCY04

More Work

Spread independent users, agents, builds, renders, evaluations, and batch work.

+
FOREGROUND FIRST05

Protect This PC

Evacuate background work and preserve the game, call, or production session.

Weights + KV cacheSigned runtime offersCompatibility groupsTopology gateRead-only plan
-
Rampage desktop showing the universal AI gateway, Maximum Model strategy, five-machine fabric grid, and a healthy autonomous self-scan
The real desktop: one strategy deck, an exact universal API surface, and an evidence-driven self-scan that acts inside the owner envelope without per-change approval.
+
Rampage desktop showing the universal AI gateway, compute strategy, five-machine fabric grid, and a healthy autonomous self-scan
The real desktop: one strategy deck, an exact universal API surface, and an evidence-driven self-scan that acts inside the owner envelope without per-change approval.

The planner stays read-only. The separate whole-model lane serves OpenAI, Anthropic Messages, and OpenRouter-style text requests through an exact one-shot lease, authenticated QUIC, local Ollama, and a signed transcript receipt. Cross-host tensor and pipeline launch remain fenced.

@@ -305,7 +310,7 @@

Stop wasting the machines you already own.

diff --git a/packages/sdk-python/pyproject.toml b/packages/sdk-python/pyproject.toml index b436da2..0eaee5c 100644 --- a/packages/sdk-python/pyproject.toml +++ b/packages/sdk-python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "rampage-sdk" -version = "0.3.0" +version = "0.3.1" requires-python = ">=3.12" dependencies = ["httpx>=0.28"] diff --git a/packages/sdk-python/src/rampage_sdk/__init__.py b/packages/sdk-python/src/rampage_sdk/__init__.py index 5b1ae6f..1bfde53 100644 --- a/packages/sdk-python/src/rampage_sdk/__init__.py +++ b/packages/sdk-python/src/rampage_sdk/__init__.py @@ -3,7 +3,7 @@ import base64 import time from typing import Any -from urllib.parse import urlparse +from urllib.parse import quote, urlparse import httpx @@ -26,6 +26,19 @@ def health(self) -> dict[str, Any]: def invite(self) -> dict[str, Any]: return self._post("/v1/enrollment/invites", {}) + def nodes(self) -> list[dict[str, Any]]: + response = self._client.get("/v1/nodes") + response.raise_for_status() + payload: list[dict[str, Any]] = response.json() + return payload + + def revoke_node(self, node_id: str) -> dict[str, Any]: + """Revoke one exact enrolled identity and all authority derived from it.""" + return self._post( + f"/v1/nodes/{quote(node_id, safe='')}/revoke", + {"confirmation": f"FORGET {node_id}"}, + ) + def discover(self, path: str) -> dict[str, Any]: return self._post("/v1/projects/discover", {"path": path}) diff --git a/packages/sdk-python/tests/test_client.py b/packages/sdk-python/tests/test_client.py index 167d18a..51a79be 100644 --- a/packages/sdk-python/tests/test_client.py +++ b/packages/sdk-python/tests/test_client.py @@ -81,6 +81,33 @@ def handler(request: httpx.Request) -> httpx.Response: assert client.topology() == [] +def test_node_revocation_is_exact_and_token_protected() -> None: + node_id = "0198f1aa-9f18-7dc3-81a3-d78f22efb662" + + def handler(request: httpx.Request) -> httpx.Response: + assert request.headers["x-rampage-token"] == "local-token" + assert request.url.path == f"/v1/nodes/{node_id}/revoke" + assert request.content == f'{{"confirmation":"FORGET {node_id}"}}'.encode() + return httpx.Response( + 200, + json={ + "schema": "rampage.node-revocation-receipt.v1", + "node_id": node_id, + "revoked": True, + "remote_assist_sessions_closed": 0, + }, + ) + + client = RampageClient(token="local-token") + client._client.close() + client._client = httpx.Client( + base_url="http://127.0.0.1:47831", + headers={"x-rampage-token": "local-token"}, + transport=httpx.MockTransport(handler), + ) + assert client.revoke_node(node_id)["revoked"] is True + + def test_capability_inventory_and_self_scan_are_token_protected() -> None: requests: list[str] = [] diff --git a/packages/sdk-python/uv.lock b/packages/sdk-python/uv.lock index 5ef9ea2..b9206c2 100644 --- a/packages/sdk-python/uv.lock +++ b/packages/sdk-python/uv.lock @@ -72,7 +72,7 @@ wheels = [ [[package]] name = "rampage-sdk" -version = "0.3.0" +version = "0.3.1" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/sdk-ts/package.json b/packages/sdk-ts/package.json index abd268b..dc408d6 100644 --- a/packages/sdk-ts/package.json +++ b/packages/sdk-ts/package.json @@ -1,6 +1,6 @@ { "name": "@rampage/sdk", - "version": "0.3.0", + "version": "0.3.1", "private": true, "type": "module", "exports": "./dist/index.js", diff --git a/packages/sdk-ts/src/index.test.ts b/packages/sdk-ts/src/index.test.ts index d0eb8c2..8e6b7a9 100644 --- a/packages/sdk-ts/src/index.test.ts +++ b/packages/sdk-ts/src/index.test.ts @@ -39,6 +39,23 @@ describe("RampageClient", () => { vi.unstubAllGlobals(); }); + it("revokes one exact enrolled identity with the required confirmation", async () => { + const nodeId = "0198f1aa-9f18-7dc3-81a3-d78f22efb662"; + const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({ + schema: "rampage.node-revocation-receipt.v1", + node_id: nodeId, + revoked: true, + remote_assist_sessions_closed: 1, + }), { status: 200, headers: { "content-type": "application/json" } })); + vi.stubGlobal("fetch", fetchMock); + const client = new RampageClient(undefined, "local-token"); + expect((await client.revokeNode(nodeId)).revoked).toBe(true); + const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]; + expect(url).toBe(`http://127.0.0.1:47831/v1/nodes/${nodeId}/revoke`); + expect(JSON.parse(init.body as string)).toEqual({ confirmation: `FORGET ${nodeId}` }); + vi.unstubAllGlobals(); + }); + it("previews model strategy without implying execution authority", async () => { const payload = { schema: "rampage.model-session-plan.v1", diff --git a/packages/sdk-ts/src/index.ts b/packages/sdk-ts/src/index.ts index 2a6073a..a9ae38c 100644 --- a/packages/sdk-ts/src/index.ts +++ b/packages/sdk-ts/src/index.ts @@ -25,6 +25,25 @@ export interface EnrollmentInvite { governor_public_key: string; } +export interface NodeIdentity { + schema: "rampage.node-identity.v1"; + node_id: string; + owner_id: string; + display_name: string; + device_kind: "desktop" | "laptop" | "server" | "steam_deck" | "phone" | "tablet" | "console"; + platform: string; + public_key: string; + enrolled_at: string; + fencing_epoch: number; +} + +export interface NodeRevocationReceipt { + schema: "rampage.node-revocation-receipt.v1"; + node_id: string; + revoked: true; + remote_assist_sessions_closed: number; +} + export interface ExecutionReceipt { schema: "rampage.execution-receipt.v1"; receipt_id: string; @@ -422,6 +441,18 @@ export class RampageClient { return this.request("/v1/enrollment/invites", { method: "POST", body: "{}" }); } + nodes(): Promise { + return this.request("/v1/nodes"); + } + + revokeNode(nodeId: string): Promise { + const encodedNodeId = encodeURIComponent(nodeId); + return this.request( + `/v1/nodes/${encodedNodeId}/revoke`, + this.json({ confirmation: `FORGET ${nodeId}` }), + ); + } + discover(path: string): Promise { return this.request("/v1/projects/discover", this.json({ path })); } diff --git a/scripts/Qualify-RampageRemoteAssist.ps1 b/scripts/Qualify-RampageRemoteAssist.ps1 index 36b19ca..b6dac8a 100644 --- a/scripts/Qualify-RampageRemoteAssist.ps1 +++ b/scripts/Qualify-RampageRemoteAssist.ps1 @@ -3,6 +3,8 @@ param( [string]$NodeId, [string]$ControllerBase = 'http://127.0.0.1:47831', [string]$DataDir = (Join-Path $env:APPDATA 'ai.obtuse.rampage\runtime'), + [ValidatePattern('^\d+\.\d+\.\d+$')] + [string]$ExpectedVersion = '0.3.1', [ValidateRange(5, 60)] [int]$TimeoutSeconds = 15 ) @@ -79,8 +81,8 @@ $health = (Invoke-RampageRequest -Method GET -Path '/health').Body if ($health.status -ne 'ready' -or $health.kill_latch -eq $true) { throw 'The owner controller is not ready or its STOP latch is active' } -if ($health.version -ne '0.3.0') { - throw "Remote Assist qualification requires controller 0.3.0, found $($health.version)" +if ($health.version -ne $ExpectedVersion) { + throw "Remote Assist qualification requires controller $ExpectedVersion, found $($health.version)" } $nodes = @((Invoke-RampageRequest -Method GET -Path '/v1/nodes').Body) @@ -113,7 +115,7 @@ if ($NodeId) { throw "Cannot qualify worker ${NodeId}: $reason" } } elseif ($eligible.Count -eq 0) { - throw 'No live paired worker is advertising opted-in Rampage 0.3.0 Remote Assist' + throw "No live paired worker is advertising opted-in Rampage $ExpectedVersion Remote Assist" } elseif ($eligible.Count -gt 1) { $choices = ($eligible.node_id | Sort-Object) -join ', ' throw "More than one Remote Assist worker is eligible; rerun with -NodeId. Choices: $choices" diff --git a/scripts/Smoke-RampageInstaller.ps1 b/scripts/Smoke-RampageInstaller.ps1 index 9b7adec..802ca89 100644 --- a/scripts/Smoke-RampageInstaller.ps1 +++ b/scripts/Smoke-RampageInstaller.ps1 @@ -1,5 +1,5 @@ param( - [string]$Installer = 'target\release\bundle\nsis\Rampage_0.3.0_x64-setup.exe' + [string]$Installer = 'target\release\bundle\nsis\Rampage_0.3.1_x64-setup.exe' ) $ErrorActionPreference = 'Stop' diff --git a/scripts/Stage-RampageDistribution.ps1 b/scripts/Stage-RampageDistribution.ps1 index 287bc1f..8079d26 100644 --- a/scripts/Stage-RampageDistribution.ps1 +++ b/scripts/Stage-RampageDistribution.ps1 @@ -7,6 +7,7 @@ param( ) $ErrorActionPreference = 'Stop' +$utf8NoBom = New-Object System.Text.UTF8Encoding($false) $root = Split-Path -Parent $PSScriptRoot $bundleRoot = Join-Path $root 'target/release/bundle' $sourceChanges = @(& git -C $root status --porcelain=v1 --untracked-files=all) @@ -113,7 +114,11 @@ $assets = Get-ChildItem -LiteralPath $stageRoot -File | Sort-Object Name | ForEa } } $checksumLines = $assets | ForEach-Object { "$($_.sha256) $($_.name)" } -Set-Content -LiteralPath (Join-Path $stageRoot "SHA256SUMS-$Platform") -Value $checksumLines -Encoding utf8NoBOM +[IO.File]::WriteAllLines( + (Join-Path $stageRoot "SHA256SUMS-$Platform"), + [string[]]$checksumLines, + $utf8NoBom +) $sourceCommit = (& git -C $root rev-parse HEAD).Trim() if ($LASTEXITCODE -ne 0) { throw 'could not resolve source commit' } @@ -127,7 +132,11 @@ $manifest = [ordered]@{ platform_signature_verified = $signatureVerified assets = @($assets) } -$manifest | ConvertTo-Json -Depth 6 | - Set-Content -LiteralPath (Join-Path $stageRoot "distribution-manifest-$Platform.json") -Encoding utf8NoBOM +$manifestJson = $manifest | ConvertTo-Json -Depth 6 +[IO.File]::WriteAllText( + (Join-Path $stageRoot "distribution-manifest-$Platform.json"), + $manifestJson + [Environment]::NewLine, + $utf8NoBom +) -$manifest | ConvertTo-Json -Depth 6 +$manifestJson diff --git a/services/intelligence/pyproject.toml b/services/intelligence/pyproject.toml index 39e7809..d113e61 100644 --- a/services/intelligence/pyproject.toml +++ b/services/intelligence/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "rampage-intelligence" -version = "0.3.0" +version = "0.3.1" description = "Durable, proposal-only intelligence plane for Rampage" requires-python = ">=3.12,<3.15" dependencies = [ diff --git a/services/intelligence/uv.lock b/services/intelligence/uv.lock index 21ddccf..7706ea0 100644 --- a/services/intelligence/uv.lock +++ b/services/intelligence/uv.lock @@ -1816,7 +1816,7 @@ wheels = [ [[package]] name = "rampage-intelligence" -version = "0.3.0" +version = "0.3.1" source = { editable = "." } dependencies = [ { name = "fastapi" },

Rampage for Windows

Your download should begin automatically.

-

Download Rampage 0.3.0 for Windows x64

-

Release notes and SHA-256

+

Download Rampage 0.3.1 for Windows x64

+

Release notes and SHA-256