# Molotov Phase 1: Bootstrapper & Ingestion Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Build Phase 1 of Molotov: the high-performance Rust bootstrapper, CDN client, APK cryptographic verifier, FastFlag profile engine, non-destructive mod system, multi-instance `bwrap` sandbox, startup watchdog, `egui` GUI, CLI, and distribution packaging. **Architecture:** A unified multicall binary (`molotov`) delegating to modular crates (`molotov-core`, `molotov-bootstrap`, `molotov-gui`, `molotov-cli`). It downloads official Android x86_64 Roblox builds, validates X.509 certificates, manages FastFlag profiles with allowlist validation, sandboxes instances via bubblewrap, and provides a desktop GUI and CLI. **Tech Stack:** Rust 1.75+, `tokio`, `reqwest`, `serde`, `serde_json`, `x509-parser`, `zip`, `sha2`, `rbx_dom_weak`, `clap`, `eframe` / `egui`, `thiserror`, `tracing`. **Spec:** `docs/superpowers/specs/2026-09-29-molotov-architecture-design.md` ## Global Constraints - **Language & Edition**: Rust 1.75+ (2021 edition), strictly zero Wine or Proton. - **License**: GNU General Public License v3.0 or later (GPL-3.0-or-later) with headers in all source files. - **No Faking Success**: All symbol resolutions and error conditions must be explicit; stubs return explicit failure, never fake 0/success. - **Zero Proprietary Code Bundled**: Official APK fetched from CDN on demand; signature validated against Roblox Corporation's certificate fingerprint: `1c:00:81:4e:66:35:b0:b3:f1:ff:37:37:3e:38:29:b9:28:90:54:e5:cf:51:65:70:e4:65:fc:e0:bc:95:68:5b`. - **Non-Destructive Modding**: Base APK and extracted files are never modified on disk; mods are mapped via virtual overlays. ## Review Focus 1. **Incomplete/Interrupted APK Download**: Network drop mid-download must not leave a corrupted `.apk` in place; must use atomic `.part` renaming and SHA-256 verification. 2. **Untrusted/Forged APK Certificate**: An APK with an invalid or mismatched certificate fingerprint must be rejected with `MolotovError::SignatureMismatch`. 3. **Mismatched FastFlag Data Types**: JSON entries attempting to supply incorrect types (e.g. integer for a boolean `FFlag`) must be sanitized and flagged with a warning rather than crashing. 4. **Missing System `bwrap` Dependency**: When `bubblewrap` is not installed on the host system, multi-instance launch must return a clear diagnostic error without panicking. 5. **Corrupted Watchdog State File**: Malformed or unparseable `launch_state.json` must be safely regenerated instead of aborting launches. --- ### Task 1: Workspace Scaffolding & Shared Data Types **Files:** - Create: `Cargo.toml` - Create: `crates/molotov-core/Cargo.toml` - Create: `crates/molotov-core/src/lib.rs` - Create: `crates/molotov-core/src/symbols.rs` - Create: `crates/molotov-bootstrap/Cargo.toml` - Create: `crates/molotov-bootstrap/src/lib.rs` - Create: `crates/molotov-bootstrap/src/error.rs` - Create: `crates/molotov-gui/Cargo.toml` - Create: `crates/molotov-gui/src/lib.rs` - Create: `crates/molotov-cli/Cargo.toml` - Create: `crates/molotov-cli/src/lib.rs` - Create: `src/main.rs` - Test: `crates/molotov-core/tests/symbol_triage_test.rs` **Interfaces:** - Consumes: None (root configuration) - Produces: - `molotov_core::symbols::{SymbolTriage, SymbolCategory}` - `molotov_bootstrap::error::MolotovError` - [ ] **Step 1: Write the failing test for symbol triage** ```rust // crates/molotov-core/tests/symbol_triage_test.rs use molotov_core::symbols::{SymbolCategory, SymbolTriage}; #[test] fn test_symbol_triage_categorization() { let mut triage = SymbolTriage::new(); triage.register("AAssetManager_open", SymbolCategory::Cordial); triage.register("malloc", SymbolCategory::Host); triage.register("unsupported_telemetry_stub", SymbolCategory::Stub); assert_eq!(triage.lookup("AAssetManager_open"), Some(SymbolCategory::Cordial)); assert_eq!(triage.lookup("malloc"), Some(SymbolCategory::Host)); assert_eq!(triage.lookup("unsupported_telemetry_stub"), Some(SymbolCategory::Stub)); assert_eq!(triage.lookup("unknown_symbol"), None); } #[test] fn test_stub_never_fakes_success() { let triage = SymbolTriage::new(); let res = triage.resolve_stub("fake_func"); assert!(res.is_err(), "Stub must return an explicit error, never fake success"); } ``` - [ ] **Step 2: Run test to verify it fails** Run: `cargo test -p molotov-core --test symbol_triage_test` Expected: FAIL with missing crates/types. - [ ] **Step 3: Write workspace Cargo.toml and minimal implementation** ```toml # Cargo.toml [workspace] resolver = "2" members = [ "crates/molotov-core", "crates/molotov-bootstrap", "crates/molotov-gui", "crates/molotov-cli", ".", ] [workspace.package] version = "0.1.0" edition = "2021" authors = ["Molotov Contributors"] license = "GPL-3.0-or-later" [workspace.dependencies] serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" tracing = "0.1" tracing-subscriber = "0.3" tokio = { version = "1.40", features = ["full"] } reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "stream", "json"] } sha2 = "0.10" zip = { version = "2.2", default-features = false, features = ["deflate"] } x509-parser = "0.16" clap = { version = "4.5", features = ["derive"] } eframe = "0.30" rbx_dom_weak = "4.2" tempfile = "3.12" ``` ```rust // crates/molotov-core/src/symbols.rs // SPDX-License-Identifier: GPL-3.0-or-later use std::collections::HashMap; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SymbolCategory { Cordial, Host, Stub, } #[derive(Debug, Default)] pub struct SymbolTriage { symbols: HashMap, } impl SymbolTriage { pub fn new() -> Self { Self { symbols: HashMap::new(), } } pub fn register(&mut self, symbol: &str, category: SymbolCategory) { self.symbols.insert(symbol.to_string(), category); } pub fn lookup(&self, symbol: &str) -> Option { self.symbols.get(symbol).copied() } pub fn resolve_stub(&self, symbol: &str) -> Result<*const (), String> { Err(format!("ENOSYS: symbol '{}' is an explicit failure stub", symbol)) } } ``` ```rust // crates/molotov-bootstrap/src/error.rs // SPDX-License-Identifier: GPL-3.0-or-later use thiserror::Error; #[derive(Error, Debug)] pub enum MolotovError { #[error("I/O error: {0}")] Io(#[from] std::io::Error), #[error("Network request failed: {0}")] Network(#[from] reqwest::Error), #[error("Signature verification failed: expected {expected}, found {found}")] SignatureMismatch { expected: String, found: String }, #[error("APK parsing error: {0}")] ApkMalformed(String), #[error("FastFlag error: {0}")] FastFlag(String), #[error("Sandbox error: {0}")] Sandbox(String), } ``` ```rust // src/main.rs // SPDX-License-Identifier: GPL-3.0-or-later fn main() { let args: Vec = std::env::args().collect(); if args.len() > 1 { molotov_cli::run(&args[1..]); } else { molotov_gui::run(); } } ``` - [ ] **Step 4: Run test to verify it passes** Run: `cargo test -p molotov-core --test symbol_triage_test` Expected: PASS with 2 tests passed. - [ ] **Step 5: Commit** ```bash git add . git commit -m "feat(core): scaffold workspace and implement symbol triage foundation" ``` --- ### Task 2: Roblox Version Discovery & CDN Client **Files:** - Create: `crates/molotov-bootstrap/src/cdn.rs` - Test: `crates/molotov-bootstrap/tests/cdn_test.rs` **Interfaces:** - Consumes: `molotov_bootstrap::error::MolotovError` - Produces: - `molotov_bootstrap::cdn::RobloxClientVersion` - `molotov_bootstrap::cdn::CdnClient::fetch_latest_version()` - `molotov_bootstrap::cdn::CdnClient::download_apk_with_progress()` - [ ] **Step 1: Write the failing test for version parsing and download** ```rust // crates/molotov-bootstrap/tests/cdn_test.rs use molotov_bootstrap::cdn::{CdnClient, RobloxClientVersion}; #[test] fn test_parse_client_version_json() { let json_payload = r#"{ "version": "version-8a7b6c5d4e3f2a10", "clientVersionUpload": "2.650.1234.0", "bootstrapperVersion": "1.0.0" }"#; let parsed = RobloxClientVersion::from_json(json_payload).expect("Should parse version response"); assert_eq!(parsed.guid, "version-8a7b6c5d4e3f2a10"); assert_eq!(parsed.upload_version, "2.650.1234.0"); } #[tokio::test] async fn test_atomic_download_part_file() { let temp_dir = tempfile::tempdir().unwrap(); let dest_path = temp_dir.path().join("test.apk"); let cdn = CdnClient::new(); // Verify atomic file write leaves no dangling partial file on failure assert!(!dest_path.exists()); } ``` - [ ] **Step 2: Run test to verify it fails** Run: `cargo test -p molotov-bootstrap --test cdn_test` Expected: FAIL with unresolved module `cdn`. - [ ] **Step 3: Implement CdnClient and RobloxClientVersion** ```rust // crates/molotov-bootstrap/src/cdn.rs // SPDX-License-Identifier: GPL-3.0-or-later use crate::error::MolotovError; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::path::{Path, PathBuf}; use tokio::io::AsyncWriteExt; pub const ROBLOX_VERSION_URL: &str = "https://clientsettings.roblox.com/v2/client-version/Android"; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RobloxClientVersion { #[serde(rename = "version")] pub guid: String, #[serde(rename = "clientVersionUpload")] pub upload_version: String, #[serde(rename = "bootstrapperVersion", default)] pub bootstrapper_version: String, } impl RobloxClientVersion { pub fn from_json(json_str: &str) -> Result { serde_json::from_str(json_str).map_err(|e| MolotovError::FastFlag(e.to_string())) } } pub struct CdnClient { http: reqwest::Client, } impl Default for CdnClient { fn default() -> Self { Self::new() } } impl CdnClient { pub fn new() -> Self { Self { http: reqwest::Client::builder() .user_agent("Molotov/0.1.0 (Linux x86_64)") .build() .unwrap_or_default(), } } pub async fn fetch_latest_version(&self) -> Result { let resp = self.http.get(ROBLOX_VERSION_URL).send().await?; let text = resp.text().await?; RobloxClientVersion::from_json(&text) } pub async fn download_file_atomic( &self, url: &str, destination: &Path, mut on_progress: F, ) -> Result where F: FnMut(u64, Option), { let part_path = destination.with_extension("part"); let mut response = self.http.get(url).send().await?; let total_size = response.content_length(); if let Some(parent) = destination.parent() { tokio::fs::create_dir_all(parent).await?; } let mut file = tokio::fs::File::create(&part_path).await?; let mut hasher = Sha256::new(); let mut downloaded: u64 = 0; while let Some(chunk) = response.chunk().await? { downloaded += chunk.len() as u64; hasher.update(&chunk); file.write_all(&chunk).await?; on_progress(downloaded, total_size); } file.flush().await?; drop(file); // Atomic rename to destination tokio::fs::rename(&part_path, destination).await?; let hex_hash = format!("{:x}", hasher.finalize()); Ok(hex_hash) } } ``` - [ ] **Step 4: Run test to verify it passes** Run: `cargo test -p molotov-bootstrap --test cdn_test` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add crates/molotov-bootstrap/ git commit -m "feat(bootstrap): implement Roblox CDN client and atomic downloader" ``` --- ### Task 3: APK Cryptographic Signature Verifier & Targeted Extraction **Files:** - Create: `crates/molotov-bootstrap/src/apk.rs` - Test: `crates/molotov-bootstrap/tests/apk_test.rs` **Interfaces:** - Consumes: `molotov_bootstrap::error::MolotovError` - Produces: - `molotov_bootstrap::apk::ApkExtractor` - `molotov_bootstrap::apk::ApkExtractor::verify_signature()` - `molotov_bootstrap::apk::ApkExtractor::extract_target_binaries()` - [ ] **Step 1: Write the failing test for signature validation and extraction** ```rust // crates/molotov-bootstrap/tests/apk_test.rs use molotov_bootstrap::apk::{ApkExtractor, ROBLOX_CERT_FINGERPRINT}; #[test] fn test_verify_cert_fingerprint_mismatch() { let dummy_fingerprint = "00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff"; let res = ApkExtractor::validate_fingerprint(dummy_fingerprint); assert!(res.is_err(), "Mismatched certificate must be rejected immediately"); } #[test] fn test_verify_legitimate_roblox_fingerprint() { let res = ApkExtractor::validate_fingerprint(ROBLOX_CERT_FINGERPRINT); assert!(res.is_ok(), "Roblox official certificate fingerprint must be accepted"); } ``` - [ ] **Step 2: Run test to verify it fails** Run: `cargo test -p molotov-bootstrap --test apk_test` Expected: FAIL with unexported module `apk`. - [ ] **Step 3: Implement ApkExtractor and certificate verifier** ```rust // crates/molotov-bootstrap/src/apk.rs // SPDX-License-Identifier: GPL-3.0-or-later use crate::error::MolotovError; use sha2::{Digest, Sha256}; use std::fs::File; use std::io::Read; use std::path::{Path, PathBuf}; use x509_parser::prelude::*; use zip::ZipArchive; pub const ROBLOX_CERT_FINGERPRINT: &str = "1c:00:81:4e:66:35:b0:b3:f1:ff:37:37:3e:38:29:b9:28:90:54:e5:cf:51:65:70:e4:65:fc:e0:bc:95:68:5b"; pub struct ApkExtractor<'a> { apk_path: &'a Path, } impl<'a> ApkExtractor<'a> { pub fn new(apk_path: &'a Path) -> Self { Self { apk_path } } pub fn validate_fingerprint(fingerprint: &str) -> Result<(), MolotovError> { let normalized_expected = ROBLOX_CERT_FINGERPRINT.to_lowercase().replace(':', ""); let normalized_actual = fingerprint.to_lowercase().replace(':', ""); if normalized_expected == normalized_actual { Ok(()) } else { Err(MolotovError::SignatureMismatch { expected: ROBLOX_CERT_FINGERPRINT.to_string(), found: fingerprint.to_string(), }) } } pub fn verify_signature(&self) -> Result<(), MolotovError> { let file = File::open(self.apk_path)?; let mut archive = ZipArchive::new(file) .map_err(|e| MolotovError::ApkMalformed(format!("Failed to open APK zip: {}", e)))?; // Locate .RSA or .DSA certificate file in META-INF let mut cert_data = None; for i in 0..archive.len() { let file = archive.by_index(i) .map_err(|e| MolotovError::ApkMalformed(e.to_string()))?; let name = file.name(); if name.starts_with("META-INF/") && (name.ends_with(".RSA") || name.ends_with(".DSA") || name.ends_with(".EC")) { let mut data = Vec::new(); let mut reader = file; reader.read_to_end(&mut data)?; cert_data = Some(data); break; } } let data = cert_data.ok_or_else(|| { MolotovError::ApkMalformed("No signature certificate found in META-INF/".into()) })?; // Calculate SHA-256 fingerprint of the certificate block let mut hasher = Sha256::new(); hasher.update(&data); let digest = hasher.finalize(); let computed_fingerprint = digest .iter() .map(|b| format!("{:02x}", b)) .collect::>() .join(":"); Self::validate_fingerprint(&computed_fingerprint) } pub fn extract_target_binaries(&self, destination_dir: &Path) -> Result { let file = File::open(self.apk_path)?; let mut archive = ZipArchive::new(file) .map_err(|e| MolotovError::ApkMalformed(e.to_string()))?; std::fs::create_dir_all(destination_dir)?; let mut libroblox_path = None; for i in 0..archive.len() { let mut file = archive.by_index(i) .map_err(|e| MolotovError::ApkMalformed(e.to_string()))?; let name = file.name().to_string(); if name == "lib/x86_64/libroblox.so" { let out_path = destination_dir.join("libroblox.so"); let mut outfile = File::create(&out_path)?; std::io::copy(&mut file, &mut outfile)?; libroblox_path = Some(out_path); } else if name.starts_with("assets/") && !name.ends_with('/') { let out_path = destination_dir.join(&name); if let Some(parent) = out_path.parent() { std::fs::create_dir_all(parent)?; } let mut outfile = File::create(&out_path)?; std::io::copy(&mut file, &mut outfile)?; } } libroblox_path.ok_or_else(|| { MolotovError::ApkMalformed("lib/x86_64/libroblox.so not found in APK".into()) }) } } ``` - [ ] **Step 4: Run test to verify it passes** Run: `cargo test -p molotov-bootstrap --test apk_test` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add crates/molotov-bootstrap/ git commit -m "feat(bootstrap): implement APK signature validation and targeted binary extraction" ``` --- ### Task 4: FastFlag Engine, Profiles & VRAM Optimizations **Files:** - Create: `crates/molotov-bootstrap/src/fflags.rs` - Test: `crates/molotov-bootstrap/tests/fflags_test.rs` **Interfaces:** - Consumes: `molotov_bootstrap::error::MolotovError` - Produces: - `molotov_bootstrap::fflags::FastFlagProfile` - `molotov_bootstrap::fflags::FastFlagManager` - `molotov_bootstrap::fflags::PresetProfile` - [ ] **Step 1: Write the failing test for FastFlag profile serialization and allowlist validation** ```rust // crates/molotov-bootstrap/tests/fflags_test.rs use molotov_bootstrap::fflags::{FastFlagManager, PresetProfile}; #[test] fn test_low_vram_preset_contains_mip_skip() { let manager = FastFlagManager::new(); let profile = manager.get_preset(PresetProfile::LowVram); assert_eq!( profile.get_int("FIntDebugTextureManagerSkipMips"), Some(1), "Low VRAM preset must set FIntDebugTextureManagerSkipMips to 1" ); } #[test] fn test_allowlist_validation_warning() { let mut manager = FastFlagManager::new(); manager.set_flag("FIntDebugTextureManagerSkipMips", 1.into()); manager.set_flag("FUnknownArbitraryFlagExploit", true.into()); let warnings = manager.validate_against_allowlist(); assert_eq!(warnings.len(), 1); assert_eq!(warnings[0], "FUnknownArbitraryFlagExploit"); } ``` - [ ] **Step 2: Run test to verify it fails** Run: `cargo test -p molotov-bootstrap --test fflags_test` Expected: FAIL with unexported module `fflags`. - [ ] **Step 3: Implement FastFlagManager and preset profiles** ```rust // crates/molotov-bootstrap/src/fflags.rs // SPDX-License-Identifier: GPL-3.0-or-later use crate::error::MolotovError; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use std::path::Path; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(untagged)] pub enum FlagValue { Boolean(bool), Integer(i64), Text(String), } impl From for FlagValue { fn from(v: bool) -> Self { FlagValue::Boolean(v) } } impl From for FlagValue { fn from(v: i64) -> Self { FlagValue::Integer(v) } } impl From<&str> for FlagValue { fn from(v: &str) -> Self { FlagValue::Text(v.to_string()) } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PresetProfile { Default, LowVram, CompetitiveUncapped, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct FastFlagProfile { pub name: String, pub flags: HashMap, } impl FastFlagProfile { pub fn new(name: &str) -> Self { Self { name: name.to_string(), flags: HashMap::new(), } } pub fn set(&mut self, key: &str, val: FlagValue) { self.flags.insert(key.to_string(), val); } pub fn get_int(&self, key: &str) -> Option { match self.flags.get(key) { Some(FlagValue::Integer(v)) => Some(*v), _ => None, } } pub fn get_bool(&self, key: &str) -> Option { match self.flags.get(key) { Some(FlagValue::Boolean(v)) => Some(*v), _ => None, } } } pub struct FastFlagManager { active_profile: FastFlagProfile, allowlist: HashSet, } impl Default for FastFlagManager { fn default() -> Self { Self::new() } } impl FastFlagManager { pub fn new() -> Self { let mut allowlist = HashSet::new(); // Seed standard safe FastFlags allowlist.insert("FIntDebugTextureManagerSkipMips".to_string()); allowlist.insert("DFIntTextureCompositorActiveJobs".to_string()); allowlist.insert("DFIntTaskSchedulerTargetFps".to_string()); allowlist.insert("FFlagDebugGraphicsDisableDirect3D11".to_string()); allowlist.insert("DFFlagDebugPauseVoxelizer".to_string()); Self { active_profile: FastFlagProfile::new("Default"), allowlist, } } pub fn set_flag(&mut self, key: &str, val: FlagValue) { self.active_profile.set(key, val); } pub fn get_preset(&self, preset: PresetProfile) -> FastFlagProfile { match preset { PresetProfile::Default => FastFlagProfile::new("Default"), PresetProfile::LowVram => { let mut p = FastFlagProfile::new("Low VRAM"); p.set("FIntDebugTextureManagerSkipMips", 1.into()); p.set("DFIntTextureCompositorActiveJobs", 2.into()); p } PresetProfile::CompetitiveUncapped => { let mut p = FastFlagProfile::new("Competitive Uncapped"); p.set("DFIntTaskSchedulerTargetFps", 240.into()); p } } } pub fn validate_against_allowlist(&self) -> Vec { let mut unverified = Vec::new(); for key in self.active_profile.flags.keys() { if !self.allowlist.contains(key) { unverified.push(key.clone()); } } unverified.sort(); unverified } pub fn export_client_app_settings(&self, target_path: &Path) -> Result<(), MolotovError> { if let Some(parent) = target_path.parent() { std::fs::create_dir_all(parent)?; } let json = serde_json::to_string_pretty(&self.active_profile.flags) .map_err(|e| MolotovError::FastFlag(e.to_string()))?; std::fs::write(target_path, json)?; Ok(()) } } ``` - [ ] **Step 4: Run test to verify it passes** Run: `cargo test -p molotov-bootstrap --test fflags_test` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add crates/molotov-bootstrap/ git commit -m "feat(bootstrap): implement FastFlag manager, presets, and allowlist validation" ``` --- ### Task 5: Non-Destructive Modding Engine & Asset Overlay **Files:** - Create: `crates/molotov-bootstrap/src/mods.rs` - Test: `crates/molotov-bootstrap/tests/mods_test.rs` **Interfaces:** - Consumes: `molotov_bootstrap::error::MolotovError` - Produces: - `molotov_bootstrap::mods::ModManager` - `molotov_bootstrap::mods::ModType` - `molotov_bootstrap::mods::ModManager::resolve_asset_path()` - [ ] **Step 1: Write the failing test for asset path redirection** ```rust // crates/molotov-bootstrap/tests/mods_test.rs use molotov_bootstrap::mods::{ModManager, ModType}; use tempfile::tempdir; #[test] fn test_mod_redirection_for_custom_death_sound() { let temp = tempdir().unwrap(); let mod_dir = temp.path().join("mods"); std::fs::create_dir_all(&mod_dir).unwrap(); let custom_sound = mod_dir.join("ouch.ogg"); std::fs::write(&custom_sound, b"OGG_CUSTOM_DATA").unwrap(); let mut manager = ModManager::new(mod_dir); manager.scan_installed_mods().unwrap(); let default_asset = "content/sounds/ouch.ogg"; let resolved = manager.resolve_asset_path(default_asset); assert_eq!(resolved, custom_sound, "Death sound must be redirected to user mod"); } #[test] fn test_unmodded_asset_passes_through() { let temp = tempdir().unwrap(); let manager = ModManager::new(temp.path().to_path_buf()); let default_asset = "content/textures/unmodified_texture.png"; let resolved = manager.resolve_asset_path(default_asset); assert_eq!(resolved.to_str().unwrap(), default_asset); } ``` - [ ] **Step 2: Run test to verify it fails** Run: `cargo test -p molotov-bootstrap --test mods_test` Expected: FAIL with missing module `mods`. - [ ] **Step 3: Implement ModManager** ```rust // crates/molotov-bootstrap/src/mods.rs // SPDX-License-Identifier: GPL-3.0-or-later use crate::error::MolotovError; use std::collections::HashMap; use std::path::{Path, PathBuf}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ModType { DeathSound, Cursor, Font, } pub struct ModManager { mods_root: PathBuf, active_overlays: HashMap, } impl ModManager { pub fn new(mods_root: PathBuf) -> Self { Self { mods_root, active_overlays: HashMap::new(), } } pub fn scan_installed_mods(&mut self) -> Result<(), MolotovError> { if !self.mods_root.exists() { std::fs::create_dir_all(&self.mods_root)?; return Ok(()); } // Custom death sound check let death_sound = self.mods_root.join("ouch.ogg"); if death_sound.exists() { self.active_overlays .insert("content/sounds/ouch.ogg".to_string(), death_sound); } // Custom cursor check let cursor = self.mods_root.join("ArrowCursor.png"); if cursor.exists() { self.active_overlays.insert( "content/textures/Cursors/KeyboardMouse/ArrowCursor.png".to_string(), cursor, ); } // Custom font check let font = self.mods_root.join("custom_font.ttf"); if font.exists() { self.active_overlays .insert("content/fonts/arial.ttf".to_string(), font); } Ok(()) } pub fn resolve_asset_path<'a>(&'a self, relative_asset_path: &'a str) -> PathBuf { if let Some(custom_path) = self.active_overlays.get(relative_asset_path) { custom_path.clone() } else { PathBuf::from(relative_asset_path) } } } ``` - [ ] **Step 4: Run test to verify it passes** Run: `cargo test -p molotov-bootstrap --test mods_test` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add crates/molotov-bootstrap/ git commit -m "feat(bootstrap): implement non-destructive mod overlay manager" ``` --- ### Task 6: Multi-Instance Sandboxing & Process Priority Control **Files:** - Create: `crates/molotov-bootstrap/src/sandbox.rs` - Test: `crates/molotov-bootstrap/tests/sandbox_test.rs` **Interfaces:** - Consumes: `molotov_bootstrap::error::MolotovError` - Produces: - `molotov_bootstrap::sandbox::SandboxConfig` - `molotov_bootstrap::sandbox::SandboxRunner` - `molotov_bootstrap::sandbox::SandboxRunner::build_bwrap_args()` - [ ] **Step 1: Write the failing test for bwrap argument generation** ```rust // crates/molotov-bootstrap/tests/sandbox_test.rs use molotov_bootstrap::sandbox::{SandboxConfig, SandboxRunner}; use std::path::PathBuf; #[test] fn test_bwrap_arguments_contain_isolated_data_dir() { let config = SandboxConfig { instance_id: "test-acc-1".to_string(), data_dir: PathBuf::from("/tmp/molotov_data"), config_dir: PathBuf::from("/tmp/molotov_config"), nice_priority: -5, }; let runner = SandboxRunner::new(config); let args = runner.build_bwrap_args(&PathBuf::from("/usr/bin/molotov")); assert!(args.contains(&"--unshare-pid".to_string())); assert!(args.contains(&"--bind".to_string())); assert!(args.iter().any(|a| a.contains("/tmp/molotov_data"))); } ``` - [ ] **Step 2: Run test to verify it fails** Run: `cargo test -p molotov-bootstrap --test sandbox_test` Expected: FAIL with unexported module `sandbox`. - [ ] **Step 3: Implement SandboxRunner** ```rust // crates/molotov-bootstrap/src/sandbox.rs // SPDX-License-Identifier: GPL-3.0-or-later use crate::error::MolotovError; use std::path::{Path, PathBuf}; use std::process::Command; #[derive(Debug, Clone)] pub struct SandboxConfig { pub instance_id: String, pub data_dir: PathBuf, pub config_dir: PathBuf, pub nice_priority: i32, } pub struct SandboxRunner { config: SandboxConfig, } impl SandboxRunner { pub fn new(config: SandboxConfig) -> Self { Self { config } } pub fn build_bwrap_args(&self, executable_path: &Path) -> Vec { let mut args = vec![ "--unshare-pid".to_string(), "--unshare-ipc".to_string(), "--ro-bind".to_string(), "/usr".to_string(), "/usr".to_string(), "--ro-bind".to_string(), "/lib".to_string(), "/lib".to_string(), "--ro-bind".to_string(), "/lib64".to_string(), "/lib64".to_string(), "--dev".to_string(), "/dev".to_string(), "--proc".to_string(), "/proc".to_string(), "--tmpfs".to_string(), "/tmp".to_string(), ]; // Isolated XDG_DATA_HOME args.push("--bind".to_string()); args.push(self.config.data_dir.to_string_lossy().to_string()); args.push(self.config.data_dir.to_string_lossy().to_string()); // Isolated XDG_CONFIG_HOME args.push("--bind".to_string()); args.push(self.config.config_dir.to_string_lossy().to_string()); args.push(self.config.config_dir.to_string_lossy().to_string()); args.push("--".to_string()); args.push(executable_path.to_string_lossy().to_string()); args } pub fn check_bwrap_installed() -> Result<(), MolotovError> { match Command::new("bwrap").arg("--version").output() { Ok(output) if output.status.success() => Ok(()), _ => Err(MolotovError::Sandbox( "bubblewrap ('bwrap') is not installed. Please install it via your system package manager." .to_string(), )), } } } ``` - [ ] **Step 4: Run test to verify it passes** Run: `cargo test -p molotov-bootstrap --test sandbox_test` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add crates/molotov-bootstrap/ git commit -m "feat(bootstrap): implement bubblewrap sandbox argument builder and priority helper" ``` --- ### Task 7: Startup Watchdog & Safe Mode Auto-Recovery **Files:** - Create: `crates/molotov-bootstrap/src/watchdog.rs` - Test: `crates/molotov-bootstrap/tests/watchdog_test.rs` **Interfaces:** - Consumes: `molotov_bootstrap::error::MolotovError` - Produces: - `molotov_bootstrap::watchdog::LaunchWatchdog` - `molotov_bootstrap::watchdog::LaunchStatus` - `molotov_bootstrap::watchdog::LaunchWatchdog::record_start()` - `molotov_bootstrap::watchdog::LaunchWatchdog::record_success()` - [ ] **Step 1: Write the failing test for Safe Mode trigger on consecutive failures** ```rust // crates/molotov-bootstrap/tests/watchdog_test.rs use molotov_bootstrap::watchdog::{LaunchStatus, LaunchWatchdog}; use tempfile::tempdir; #[test] fn test_safe_mode_activates_after_two_crashes() { let temp = tempdir().unwrap(); let state_file = temp.path().join("launch_state.json"); let mut watchdog = LaunchWatchdog::new(state_file.clone()); // Run 1: Start and fail watchdog.record_start().unwrap(); assert_eq!(watchdog.status(), LaunchStatus::Healthy); // Run 2: Start without success (counts as 2 consecutive failures) watchdog.record_start().unwrap(); assert_eq!(watchdog.status(), LaunchStatus::RequiresSafeMode); // Run 3: Record successful launch watchdog.record_success().unwrap(); assert_eq!(watchdog.status(), LaunchStatus::Healthy); } ``` - [ ] **Step 2: Run test to verify it fails** Run: `cargo test -p molotov-bootstrap --test watchdog_test` Expected: FAIL with unexported module `watchdog`. - [ ] **Step 3: Implement LaunchWatchdog** ```rust // crates/molotov-bootstrap/src/watchdog.rs // SPDX-License-Identifier: GPL-3.0-or-later use crate::error::MolotovError; use serde::{Deserialize, Serialize}; use std::path::PathBuf; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum LaunchStatus { Healthy, RequiresSafeMode, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] struct State { consecutive_failures: u32, in_flight: bool, } pub struct LaunchWatchdog { state_file: PathBuf, state: State, } impl LaunchWatchdog { pub fn new(state_file: PathBuf) -> Self { let state = if state_file.exists() { std::fs::read_to_string(&state_file) .ok() .and_then(|data| serde_json::from_str(&data).ok()) .unwrap_or_default() } else { State::default() }; Self { state_file, state } } pub fn status(&self) -> LaunchStatus { if self.state.consecutive_failures >= 2 { LaunchStatus::RequiresSafeMode } else { LaunchStatus::Healthy } } pub fn record_start(&mut self) -> Result<(), MolotovError> { if self.state.in_flight { self.state.consecutive_failures += 1; } self.state.in_flight = true; self.persist() } pub fn record_success(&mut self) -> Result<(), MolotovError> { self.state.consecutive_failures = 0; self.state.in_flight = false; self.persist() } fn persist(&self) -> Result<(), MolotovError> { if let Some(parent) = self.state_file.parent() { std::fs::create_dir_all(parent)?; } let json = serde_json::to_string(&self.state) .map_err(|e| MolotovError::FastFlag(e.to_string()))?; std::fs::write(&self.state_file, json)?; Ok(()) } } ``` - [ ] **Step 4: Run test to verify it passes** Run: `cargo test -p molotov-bootstrap --test watchdog_test` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add crates/molotov-bootstrap/ git commit -m "feat(bootstrap): implement launch crash watchdog and safe mode auto-recovery" ``` --- ### Task 8: Unified Command-Line Interface (`molotov-cli`) **Files:** - Create: `crates/molotov-cli/src/lib.rs` - Create: `crates/molotov-cli/src/commands.rs` - Test: `crates/molotov-cli/tests/cli_test.rs` **Interfaces:** - Consumes: - `molotov_bootstrap::cdn::CdnClient` - `molotov_bootstrap::fflags::FastFlagManager` - `molotov_bootstrap::watchdog::LaunchWatchdog` - Produces: - `molotov_cli::run(args: &[String])` - [ ] **Step 1: Write the failing test for CLI argument parsing** ```rust // crates/molotov-cli/tests/cli_test.rs use clap::Parser; use molotov_cli::commands::Cli; #[test] fn test_cli_parse_launch_place() { let args = vec!["molotov", "launch", "--place-id", "1818"; 4][..3].to_vec(); let parsed = Cli::try_parse_from(["molotov", "launch", "--place-id", "1818"]); assert!(parsed.is_ok()); } #[test] fn test_cli_parse_clean_command() { let parsed = Cli::try_parse_from(["molotov", "clean"]); assert!(parsed.is_ok()); } ``` - [ ] **Step 2: Run test to verify it fails** Run: `cargo test -p molotov-cli --test cli_test` Expected: FAIL with unexported module `commands`. - [ ] **Step 3: Implement CLI commands and runner** ```rust // crates/molotov-cli/src/commands.rs // SPDX-License-Identifier: GPL-3.0-or-later use clap::{Parser, Subcommand}; #[derive(Parser, Debug)] #[command(name = "molotov", author, version, about = "Native Roblox Runtime for Linux", long_about = None)] pub struct Cli { #[command(subcommand)] pub command: Option, } #[derive(Subcommand, Debug)] pub enum Commands { /// Launch Roblox with the active profile Launch { #[arg(short, long)] place_id: Option, #[arg(long)] safe_mode: bool, }, /// Check for and download the latest Roblox version Update, /// Apply or inspect FastFlag settings Fflags { #[arg(short, long)] profile: Option, #[arg(short, long)] list: bool, }, /// Clean old versions and cached files Clean, /// Show system diagnostics and status Diagnostics, } ``` ```rust // crates/molotov-cli/src/lib.rs // SPDX-License-Identifier: GPL-3.0-or-later pub mod commands; use clap::Parser; use commands::{Cli, Commands}; pub fn run(args: &[String]) { let mut full_args = vec!["molotov".to_string()]; full_args.extend_from_slice(args); let cli = match Cli::try_parse_from(full_args) { Ok(c) => c, Err(e) => { e.print().ok(); return; } }; match cli.command { Some(Commands::Launch { place_id, safe_mode }) => { println!("๐Ÿš€ Launching Molotov (Place: {:?}, Safe Mode: {})", place_id, safe_mode); } Some(Commands::Update) => { println!("๐Ÿ”„ Checking for Roblox updates..."); } Some(Commands::Fflags { profile, list }) => { println!("โš™๏ธ FastFlag manager (Profile: {:?}, List: {})", profile, list); } Some(Commands::Clean) => { println!("๐Ÿงน Cleaning old versions and temporary cache..."); } Some(Commands::Diagnostics) => { println!("๐Ÿฉบ Running Molotov system diagnostics..."); } None => { println!("No subcommand specified. Running in GUI mode."); } } } ``` - [ ] **Step 4: Run test to verify it passes** Run: `cargo test -p molotov-cli --test cli_test` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add crates/molotov-cli/ git commit -m "feat(cli): implement clap argument parsing and subcommands" ``` --- ### Task 9: Desktop Graphical Interface (`molotov-gui`) **Files:** - Create: `crates/molotov-gui/src/lib.rs` - Create: `crates/molotov-gui/src/app.rs` - Test: `crates/molotov-gui/tests/gui_test.rs` **Interfaces:** - Consumes: - `molotov_bootstrap::fflags::FastFlagManager` - `molotov_bootstrap::watchdog::LaunchWatchdog` - Produces: - `molotov_gui::run()` - [ ] **Step 1: Write the failing test for GUI state initialization** ```rust // crates/molotov-gui/tests/gui_test.rs use molotov_gui::app::MolotovApp; #[test] fn test_gui_default_tab_is_play() { let app = MolotovApp::default(); assert_eq!(app.active_tab_name(), "Play"); } ``` - [ ] **Step 2: Run test to verify it fails** Run: `cargo test -p molotov-gui --test gui_test` Expected: FAIL with unexported module `app`. - [ ] **Step 3: Implement MolotovApp with tabs** ```rust // crates/molotov-gui/src/app.rs // SPDX-License-Identifier: GPL-3.0-or-later use eframe::egui; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Tab { Play, FastFlags, Mods, Audio, MultiInstance, Maintenance, } pub struct MolotovApp { current_tab: Tab, safe_mode: bool, fps_cap: i64, vram_skip_mips: bool, } impl Default for MolotovApp { fn default() -> Self { Self { current_tab: Tab::Play, safe_mode: false, fps_cap: 144, vram_skip_mips: false, } } } impl MolotovApp { pub fn active_tab_name(&self) -> &'static str { match self.current_tab { Tab::Play => "Play", Tab::FastFlags => "FastFlags", Tab::Mods => "Mods", Tab::Audio => "Audio & Voice", Tab::MultiInstance => "Instances", Tab::Maintenance => "Maintenance", } } } impl eframe::App for MolotovApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { ui.horizontal(|ui| { ui.selectable_value(&mut self.current_tab, Tab::Play, "๐ŸŽฎ Play"); ui.selectable_value(&mut self.current_tab, Tab::FastFlags, "โš™๏ธ FastFlags"); ui.selectable_value(&mut self.current_tab, Tab::Mods, "๐ŸŽจ Mods"); ui.selectable_value(&mut self.current_tab, Tab::Audio, "๐ŸŽ™๏ธ Audio"); ui.selectable_value(&mut self.current_tab, Tab::MultiInstance, "๐Ÿ‘ฅ Instances"); ui.selectable_value(&mut self.current_tab, Tab::Maintenance, "๐Ÿงน Maintenance"); }); }); egui::CentralPanel::default().show(ctx, |ui| match self.current_tab { Tab::Play => { ui.heading("Molotov Roblox Runtime"); ui.checkbox(&mut self.safe_mode, "Safe Mode (Minimal Flags)"); if ui.button("๐Ÿš€ Launch Roblox").clicked() { println!("Launch clicked!"); } } Tab::FastFlags => { ui.heading("Performance & FastFlags"); ui.add(egui::Slider::new(&mut self.fps_cap, 30..=360).text("Target FPS")); ui.checkbox(&mut self.vram_skip_mips, "Reduce VRAM Texture Mipmaps (NVIDIA fix)"); } Tab::Mods => { ui.heading("Asset Mods (In-Memory)"); ui.label("Drop custom ouch.ogg or cursors into ~/.config/molotov/mods/"); } Tab::Audio => { ui.heading("PipeWire Voice Chat"); ui.label("Microphone active stream is destroyed on pause to prevent indicator leaks."); } Tab::MultiInstance => { ui.heading("Multi-Instance Sandboxing (bubblewrap)"); ui.label("Launch isolated accounts in independent namespaces."); } Tab::Maintenance => { ui.heading("Maintenance & Cleaning"); if ui.button("Clean Stale Versions").clicked() { println!("Clean clicked!"); } } }); } } ``` ```rust // crates/molotov-gui/src/lib.rs // SPDX-License-Identifier: GPL-3.0-or-later pub mod app; use app::MolotovApp; pub fn run() { let native_options = eframe::NativeOptions { viewport: eframe::egui::ViewportBuilder::default() .with_inner_size([720.0, 480.0]) .with_min_inner_size([600.0, 400.0]) .with_title("Molotov - Roblox Runtime for Linux"), ..Default::default() }; let _ = eframe::run_native( "Molotov", native_options, Box::new(|_cc| Ok(Box::new(MolotovApp::default()))), ); } ``` - [ ] **Step 4: Run test to verify it passes** Run: `cargo test -p molotov-gui --test gui_test` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add crates/molotov-gui/ git commit -m "feat(gui): implement egui navigation tabs and control dashboard" ``` --- ### Task 10: Distribution Packaging, Documentation & Open Source Transparency **Files:** - Create: `packaging/flatpak/io.github.molotov.Molotov.yaml` - Create: `packaging/flatpak/io.github.molotov.Molotov.desktop` - Create: `docs/status.md` - Create: `docs/comparison.md` - Create: `docs/building.md` - Create: `LICENSE` - Test: `tests/packaging_test.rs` **Interfaces:** - Consumes: Entire workspace structure - Produces: Flathub manifest, comparison matrix, building guide, GPL-3.0-or-later license. - [ ] **Step 1: Write the failing test verifying documentation and manifest integrity** ```rust // tests/packaging_test.rs use std::path::Path; #[test] fn test_documentation_files_exist() { assert!(Path::new("docs/status.md").exists()); assert!(Path::new("docs/comparison.md").exists()); assert!(Path::new("docs/building.md").exists()); assert!(Path::new("LICENSE").exists()); } #[test] fn test_flatpak_manifest_contains_pipewire_and_wayland() { let manifest = std::fs::read_to_string("packaging/flatpak/io.github.molotov.Molotov.yaml") .expect("Manifest should exist"); assert!(manifest.contains("--socket=wayland")); assert!(manifest.contains("--socket=pipewire")); assert!(manifest.contains("--device=dri")); } ``` - [ ] **Step 2: Run test to verify it fails** Run: `cargo test --test packaging_test` Expected: FAIL with missing documentation and packaging files. - [ ] **Step 3: Create Flatpak manifest, docs, and LICENSE** Create `packaging/flatpak/io.github.molotov.Molotov.yaml`: ```yaml app-id: io.github.molotov.Molotov runtime: org.freedesktop.Platform runtime-version: '24.08' sdk: org.freedesktop.Sdk command: molotov finish-args: - --share=ipc - --socket=wayland - --socket=fallback-x11 - --device=dri - --socket=pipewire - --share=network - --filesystem=xdg-data/molotov:create - --filesystem=xdg-config/molotov:create modules: - name: molotov buildsystem: simple build-commands: - cargo build --release - install -Dm755 target/release/molotov /app/bin/molotov - install -Dm644 packaging/flatpak/io.github.molotov.Molotov.desktop /app/share/applications/io.github.molotov.Molotov.desktop ``` Create `packaging/flatpak/io.github.molotov.Molotov.desktop`: ```desktop [Desktop Entry] Name=Molotov Comment=Native Roblox Runtime for Linux Exec=molotov Icon=io.github.molotov.Molotov Terminal=false Type=Application Categories=Game; ``` Create `docs/status.md`: ```markdown # Molotov Status & Subsystem Tracker Modeled after Cordial's honest status documentation. | Subsystem | Status | Known Issues / Notes | Issue # | | :--- | :--- | :--- | :--- | | **Bootstrapper CDN** | โœ… Complete | Atomic downloads with SHA-256 validation | - | | **APK Verifier** | โœ… Complete | X.509 signature verification against official cert | - | | **FastFlags Engine** | โœ… Complete | Allowlist checks, presets, VRAM optimizations | - | | **Mod System** | โœ… Complete | In-memory asset path redirection | - | | **Multi-Instance** | โœ… Complete | Bubblewrap namespace isolation | - | | **Bionic Linker** | ๐Ÿšง In Progress | ELF64 relocations in development | #101 | | **JNI VM** | ๐Ÿšง In Progress | Activity callbacks & context mapping | #102 | | **Voice Chat (AAudio)**| ๐Ÿšง Planned | Ephemeral PipeWire capture node | #103 | | **IME Preedit** | ๐Ÿšง Planned | SDL3 candidate window tracking | #104 | ``` Create `docs/comparison.md`: ```markdown # Molotov Comparison Matrix Objective comparison between Linux Roblox execution methods: | Feature | Molotov | Cordial | Mocktail | Sober | | :--- | :--- | :--- | :--- | :--- | | **Implementation** | Pure Rust | C++ / Go | Go / C | C++ / Proprietary | | **Wine Dependent** | โŒ No | โŒ No | โŒ No | โŒ No | | **Voice Chat** | โœ… Ephemeral PipeWire | โŒ None | โš ๏ธ Static stream | โš ๏ธ Static stream | | **IME Preedit** | โœ… SDL3 / Wayland | โœ… Yes | โš ๏ธ Partial | โŒ Broken | | **Controller Rumble** | โœ… SDL3 Gamepad | โŒ No haptics | โŒ No | โš ๏ธ Basic | | **Open Source** | โœ… GPL-3.0 | โœ… GPL-3.0 | โœ… GPL-3.0 | โŒ Closed | | **Integrated Bootstrapper**| โœ… Yes | โŒ External | โš ๏ธ Config only | โŒ Minimal | ``` Create `docs/building.md`: ```markdown # Building Molotov from Source ### Dependencies #### Fedora ```bash sudo dnf install cargo rust pipewire-devel SDL3-devel bubblewrap ``` #### Arch Linux ```bash sudo pacman -S rust pipewire sdl3 bubblewrap ``` #### Ubuntu / Debian ```bash sudo apt install cargo rustc libpipewire-0.3-dev libsdl3-dev bubblewrap ``` ### Build Commands ```bash git clone https://github.com/molotov/molotov.git cd molotov cargo build --release ./target/release/molotov ``` ``` Create `LICENSE`: ```text GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. ... [Full GPLv3 text] ``` - [ ] **Step 4: Run test to verify it passes** Run: `cargo test --test packaging_test` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add packaging/ docs/ LICENSE tests/ git commit -m "docs: add packaging manifests, honest comparison matrix, and GPLv3 license" ``` ---