From f12ff7b602b1ea4d10dbe8f7ad5e0cee7d065d46 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Sun, 11 Oct 2026 09:59:23 -0700 Subject: [PATCH 1/4] Read launch-time test hooks only in development builds Home's main activity is exported, so any installed app can start it with intent extras, and Makepad copies `makepad.` extras into the environment. OctoSense code read several of them in release builds. - shell: `launch_input` is the one reader of MAKEPAD_APP_CONFIG. Its test actions, Mail's demo mailbox and module open parameters apply only in a development build (cfg(dev_mode): debug or --features dev-mode); a release build ignores them and logs that once. Desktop --test-action command-line flags are unchanged. - kernel: the launch proxy (makepad.OCTOS_PROXY) applies only in a development build; new `dev-mode` feature, forwarded from the shell through ai-host. - AppCard: every MAKEPAD_* hook (provisioning, seeding, auto prompt, fake GPS, proxy, dumps) goes through `launch_var`, development builds only; new `dev-mode` feature, forwarded from the shell through the module. - OpenHarmony: EntryAbility forwards no launch parameters unless build-home-ohos.py --dev-mode stages it (a separate bundle id required). - Android: Home's `dev-mode` feature comes from build-home.py's `--dev-mode` (App Studio's, #477): standalone only, never the installed Home's identity under its signer, and ROM staging refuses it. So a build that reads launch input is never the installed Home. Co-Authored-By: Claude Opus 5.5 --- apps/appcard/app/app/Cargo.toml | 7 ++ apps/appcard/app/app/build.rs | 8 ++ apps/appcard/app/app/src/app/l0_card.rs | 5 +- apps/appcard/app/app/src/lib.rs | 138 ++++++++++++++++----- apps/appcard/module/Cargo.toml | 5 + crates/ai-host/Cargo.toml | 3 + crates/kernel/Cargo.toml | 5 + crates/kernel/build.rs | 9 ++ crates/kernel/src/launch.rs | 31 ++++- crates/shell/Cargo.toml | 7 +- crates/shell/src/apps.rs | 17 ++- crates/shell/src/launch_input.rs | 152 ++++++++++++++++++++++++ crates/shell/src/lib.rs | 16 ++- phone/ohos/EntryAbility.ets | 17 ++- rom/scripts/build-home-ohos.py | 36 +++++- rom/tests/test_home_build.py | 32 +++++ 16 files changed, 418 insertions(+), 70 deletions(-) create mode 100644 crates/shell/src/launch_input.rs diff --git a/apps/appcard/app/app/Cargo.toml b/apps/appcard/app/app/Cargo.toml index e19eb19ad..8eb3a8b69 100644 --- a/apps/appcard/app/app/Cargo.toml +++ b/apps/appcard/app/app/Cargo.toml @@ -28,6 +28,13 @@ default = ["standalone"] # dev monitor. A host that mounts the app as a widget (`octos_app::AppShell`) # builds with `default-features = false` and brings its own. standalone = [] +# A development build (build.rs sets `dev_mode`; debug builds are development +# builds without it). Only a development build reads the launch-time hooks: +# the `MAKEPAD_*` settings an Android intent extra or OpenHarmony launch +# parameter can carry (provisioning, seeding, auto prompt, fake GPS, proxy, +# dumps; `launch_var` in src/lib.rs). The shell's `dev-mode` turns it on for +# the hosted app. +dev-mode = ["octosense-kernel/dev-mode"] [dependencies] # One framework source, locked by Octoscript-Makepad/runtime.json. diff --git a/apps/appcard/app/app/build.rs b/apps/appcard/app/app/build.rs index 93a7bdb79..bdaa66a22 100644 --- a/apps/appcard/app/app/build.rs +++ b/apps/appcard/app/app/build.rs @@ -22,6 +22,14 @@ fn main() { { println!("cargo:rustc-cfg=mobile"); } + // `dev_mode` — a development build, as the shell's build.rs decides it: + // debug assertions, or feature `dev-mode`. Only then does the app read + // its launch-time `MAKEPAD_*` hooks (`launch_var` in src/lib.rs): on a + // phone another app can set them when it starts this one. + println!("cargo:rustc-check-cfg=cfg(dev_mode)"); + if env::var_os("CARGO_FEATURE_DEV_MODE").is_some() || env::var_os("CARGO_CFG_DEBUG_ASSERTIONS").is_some() { + println!("cargo:rustc-cfg=dev_mode"); + } } // Pin source contents, resources and lockfiles, including local path patches. diff --git a/apps/appcard/app/app/src/app/l0_card.rs b/apps/appcard/app/app/src/app/l0_card.rs index c7f67de1e..d8af3fc48 100644 --- a/apps/appcard/app/app/src/app/l0_card.rs +++ b/apps/appcard/app/app/src/app/l0_card.rs @@ -319,8 +319,7 @@ fn kit_for(source: &str) -> String { // Measurement only. Nothing sets this in normal operation, and an unreadable // path is ignored rather than fatal — a missing override should render the // shipped look, not a blank screen. - let axis = std::env::var("MAKEPAD_L0_PALETTE_OVERRIDE") - .ok() + let axis = crate::launch_var("MAKEPAD_L0_PALETTE_OVERRIDE") .and_then(|p| std::fs::read_to_string(p).ok()) .unwrap_or_default(); // Axes BEFORE the env override, so a measurement run still wins over what @@ -1856,7 +1855,7 @@ fn render_capturing( // was chased through six layers on the evidence of a screenshot, which can // only ever say "not there" and never says which layer dropped it. Every // other layer already has a probe; this was the gap. - if let Ok(path) = std::env::var("MAKEPAD_DUMP_DSL") { + if let Some(path) = crate::launch_var("MAKEPAD_DUMP_DSL") { let _ = std::fs::write(&path, &dsl); } let captured = LAST_CAPTURED diff --git a/apps/appcard/app/app/src/lib.rs b/apps/appcard/app/app/src/lib.rs index 3bbca2521..043711ff9 100644 --- a/apps/appcard/app/app/src/lib.rs +++ b/apps/appcard/app/app/src/lib.rs @@ -30,6 +30,78 @@ use streaming_markdown_kit::{ use crate::backend::OctosUiAgent; +/// A launch-time `MAKEPAD_*` hook: the test, provisioning, seeding and +/// measurement settings this app reads from its environment. On Android +/// Makepad copies the launch extras `makepad.` into `MAKEPAD_`, +/// and OpenHarmony entry abilities pass `makepad.*` launch parameters the +/// same way, so on a phone another app can set them by starting this one. +/// Only a development build reads them (`dev_mode`: debug assertions or +/// feature `dev-mode`, build.rs); a release build behaves as if none were +/// set. Read every `MAKEPAD_*` hook through this, never `std::env::var`. +pub(crate) fn launch_var(name: &str) -> Option { + launch_set_in(cfg!(dev_mode), name).then(|| std::env::var(name).ok()).flatten() +} + +/// [`launch_var`] for a variable whose presence is the setting. +pub(crate) fn launch_flag(name: &str) -> bool { + launch_set_in(cfg!(dev_mode), name) +} + +/// Whether `name` is set and this build may read it. A release build +/// notes once that it ignored a launch-time hook, without its value. +fn launch_set_in(development: bool, name: &str) -> bool { + if std::env::var_os(name).is_none() { + return false; + } + if !development { + static NOTED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); + if !NOTED.swap(true, std::sync::atomic::Ordering::Relaxed) { + log::info!("launch input: {name} ignored; a release build reads no launch-time hooks"); + } + return false; + } + true +} + +#[cfg(test)] +mod launch_input_tests { + use super::launch_set_in; + + #[test] + fn a_release_build_reads_no_launch_hook() { + // A name no other test sets. + let name = "MAKEPAD_LAUNCH_INPUT_TEST_PROBE"; + std::env::set_var(name, "/sdcard/Download/provision"); + assert!(!launch_set_in(false, name), "a release build ignores it"); + assert!(launch_set_in(true, name), "a development build reads it"); + std::env::remove_var(name); + assert!(!launch_set_in(true, name)); + } + + /// Every `MAKEPAD_*` hook goes through `launch_var`/`launch_flag`, so + /// none bypasses the build check. + #[test] + fn no_source_reads_a_makepad_hook_directly() { + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut offenders = Vec::new(); + let mut stack = vec![root.clone()]; + while let Some(dir) = stack.pop() { + for entry in std::fs::read_dir(&dir).expect("read src") { + let path = entry.expect("entry").path(); + if path.is_dir() { + stack.push(path); + } else if path.extension().is_some_and(|e| e == "rs") { + let text = std::fs::read_to_string(&path).expect("read source"); + if text.contains("env::var(\"MAKEPAD_") || text.contains("env::var_os(\"MAKEPAD_") { + offenders.push(path.strip_prefix(&root).unwrap().display().to_string()); + } + } + } + } + assert!(offenders.is_empty(), "read MAKEPAD_* hooks through launch_var: {offenders:?}"); + } +} + /// Octos profiles supply system prompts server-side, so the client ships an /// empty placeholder. Replaces aichat's `BackendType::system_prompt` (and the /// `splash.md` `include_str!`) which baked huge LLM-shaped diagram preambles @@ -458,7 +530,7 @@ fn refresh_youtube_live_ids() { }; rt.block_on(async { let mut builder = reqwest::Client::builder(); - if let Ok(proxy) = std::env::var("MAKEPAD_OCTOS_PROXY") { + if let Some(proxy) = crate::launch_var("MAKEPAD_OCTOS_PROXY") { let proxy = proxy.trim().to_owned(); if !proxy.is_empty() { if let Ok(p) = reqwest::Proxy::all(&proxy) { @@ -1485,8 +1557,7 @@ fn pin_fullbleed_root_height(body: &str) -> String { let attrs = &body[root_open + 1..attr_end]; // The seed-render harness treats the card as THE SCREEN: a Fill root // means "the design frame", not the chat feed's tall fullbleed canvas. - let pin_h = std::env::var("MAKEPAD_SEED_L0_FILL_HEIGHT") - .ok() + let pin_h = crate::launch_var("MAKEPAD_SEED_L0_FILL_HEIGHT") .and_then(|v| v.parse::().ok()) .unwrap_or(FULLBLEED_FALLBACK_HEIGHT); let fixed = format!("height: {pin_h}"); @@ -5606,7 +5677,7 @@ impl Widget for ChatList { } item_widget.draw_all_unscoped(cx); // LOCAL DEBUG: where did this item actually land? - if std::env::var_os("MAKEPAD_GL_DRAW_TRACE").is_some() { + if crate::launch_flag("MAKEPAD_GL_DRAW_TRACE") { let r = item_widget.area().rect(cx); log::info!( "[CHATLIST] item {item_id} rect pos=({:.1},{:.1}) size=({:.1},{:.1})", @@ -6194,7 +6265,7 @@ impl App { /// silent in M1. W04 follow-up #5 — `/api/version` probe runs /// off-thread so we don't stall the caller. fn connect_transport(&mut self, cx: &mut Cx) { - if std::env::var_os("MAKEPAD_L0_REVIEW").is_some() { return; } + if crate::launch_flag("MAKEPAD_L0_REVIEW") { return; } let transport_config = Self::placeholder_transport_config(); log::info!( "connect transport: base_url={} profile_id={}", @@ -6696,20 +6767,20 @@ impl App { // that embeds this app as a library (`default-features = false`, // no `standalone`) gets neither — nor does a standalone build // launched with `OCTOS_APP_DEVGOAL=0`. An explicit - // `MAKEPAD_DEV_GOAL_FILE` still runs anywhere. + // `MAKEPAD_DEV_GOAL_FILE` still runs anywhere, in a development + // build (`launch_var`). #[cfg(target_os = "android")] - let mission: Option<(String, String)> = match std::env::var("MAKEPAD_DEV_GOAL_FILE") { - Ok(p) => std::fs::read_to_string(&p).ok().map(|g| (g, p)), - Err(_) if Self::bundled_mission_enabled() => { + let mission: Option<(String, String)> = match crate::launch_var("MAKEPAD_DEV_GOAL_FILE") { + Some(p) => std::fs::read_to_string(&p).ok().map(|g| (g, p)), + None if Self::bundled_mission_enabled() => { let p = "/data/data/dev.makepad.octos_app/files/dev_goal.txt".to_owned(); let _ = std::fs::write(&p, BUNDLED_MISSION); Some((BUNDLED_MISSION.to_owned(), p)) } - Err(_) => None, + None => None, }; #[cfg(not(target_os = "android"))] - let mission: Option<(String, String)> = std::env::var("MAKEPAD_DEV_GOAL_FILE") - .ok() + let mission: Option<(String, String)> = crate::launch_var("MAKEPAD_DEV_GOAL_FILE") .and_then(|p| std::fs::read_to_string(&p).ok().map(|g| (g, p))); if let Some((goal, goal_path)) = mission { { @@ -6993,9 +7064,9 @@ impl App { /// live LLM generation can be driven without touching the native composer via /// adb input. Fires once (clears the env var). Session/open and turn/start /// are ordered on the stdio pipe, so submitting right after `clear_chat` is - /// safe. + /// safe. A development build only (`launch_var`). fn fire_auto_prompt(&mut self, cx: &mut Cx) { - if let Ok(p) = std::env::var("MAKEPAD_AUTO_PROMPT") { + if let Some(p) = crate::launch_var("MAKEPAD_AUTO_PROMPT") { std::env::remove_var("MAKEPAD_AUTO_PROMPT"); let p = p.trim().to_string(); if !p.is_empty() { @@ -8279,7 +8350,7 @@ impl MatchEvent for App { target_arch = "wasm32" )))] { - std::env::var_os("MAKEPAD_MOBILE_PREVIEW").is_some() + crate::launch_flag("MAKEPAD_MOBILE_PREVIEW") || modifiers.logo || modifiers.control } }; @@ -8974,13 +9045,18 @@ impl MatchEvent for App { } } + // Launch-time provisioning below is for development builds only + // (`launch_var`): any app on the phone can start this one with these + // extras, so a release build provisions only through the person's + // own QR scan and PIN, or the login screen. + // // Provisioning deploy (non-rooted devices): `makepad.PROVISION_DIR` // (→ env MAKEPAD_PROVISION_DIR) names a world-readable staging dir // (`adb push …/octos-provision`) whose tree is copied into the app's // octos-home BEFORE octos spawns — deploying the GLM profile + a2app // memory tree onto a device that can't be written via su/run-as. #[cfg(target_os = "android")] - if let Ok(src) = std::env::var("MAKEPAD_PROVISION_DIR") { + if let Some(src) = crate::launch_var("MAKEPAD_PROVISION_DIR") { let home = kernel_home(); match deploy_provision(std::path::Path::new(&src), &home) { Ok(n) => log::info!("provision: deployed {n} files from {src}"), @@ -8995,7 +9071,7 @@ impl MatchEvent for App { // BEFORE the boot-auth decision, so a provisioned device lands // straight on the home shell — no LoginScreen typing. A QR-scan // onboarding can feed the same `apply_provision_string` entry later. - if let Ok(prov) = std::env::var("MAKEPAD_APP_CONFIG") { + if let Some(prov) = crate::launch_var("MAKEPAD_APP_CONFIG") { match crate::app::login::apply_provision_string(&prov) { Ok(()) => log::info!("provisioned from launch intent"), Err(e) => log::warn!("provisioning failed: {e}"), @@ -9005,7 +9081,7 @@ impl MatchEvent for App { // payload `{"llm_family":..,"llm_model":..,"llm_key":..}`, the same content // the composer's QR scan yields) writes the provider + key into the octos // profile config BEFORE the kernel spawns below, so the first turn uses it. - if let Ok(cfg) = std::env::var("MAKEPAD_PROVISION_CONFIG") { + if let Some(cfg) = crate::launch_var("MAKEPAD_PROVISION_CONFIG") { match crate::app::login::apply_provision_config_json(&cfg) { Ok(what) => log::info!("provisioned LLM from intent: {what}"), Err(e) => log::warn!("LLM provisioning failed: {e}"), @@ -9017,7 +9093,7 @@ impl MatchEvent for App { // A deployment may explicitly request re-admission after changing the // runtime bundle. Preserve previous receipts before opening any cards. // A stale receipt still fails closed during normal app launches. - if let Ok(migration) = std::env::var("MAKEPAD_REAPPROVE_CARDS") { + if let Some(migration) = crate::launch_var("MAKEPAD_REAPPROVE_CARDS") { std::env::remove_var("MAKEPAD_REAPPROVE_CARDS"); let result = crate::app::login::config_dir() .ok_or_else(|| "card approval directory unavailable".to_owned()) @@ -9076,7 +9152,7 @@ impl MatchEvent for App { // W04 / M2 — make sure the chat_screen / content_screen pair // matches the boot navigation state (defaults to Home → Chat). self.show_screen_for_nav(cx); - if std::env::var_os("MAKEPAD_L0_REVIEW").is_some() { + if crate::launch_flag("MAKEPAD_L0_REVIEW") { // Offline native review uses only the seeded ledger below. } else if authed { // Open the first session immediately so the composer is live. @@ -9090,7 +9166,7 @@ impl MatchEvent for App { // generation. `--es makepad.SEED_CARD_FILE ` surfaces as // MAKEPAD_SEED_CARD_FILE. Push AFTER the boot decision above (clear_chat // wipes CHAT_DATA), then refresh the empty-state + redraw so it shows. - if let Ok(path) = std::env::var("MAKEPAD_SEED_CARD_FILE") { + if let Some(path) = crate::launch_var("MAKEPAD_SEED_CARD_FILE") { match std::fs::read_to_string(&path) { Ok(body) => { let body_trim = body.trim(); @@ -9123,8 +9199,8 @@ impl MatchEvent for App { // source and can re-realize it when a tap arrives. SEED_CARD_FILE above // pushes already-lowered DSL, which is inert by construction: nothing on // device knows what card produced it or what a tap would mean. - if let Ok(card_path) = std::env::var("MAKEPAD_SEED_L0_FILE") { - let data_path = std::env::var("MAKEPAD_SEED_L0_DATA").unwrap_or_default(); + if let Some(card_path) = crate::launch_var("MAKEPAD_SEED_L0_FILE") { + let data_path = crate::launch_var("MAKEPAD_SEED_L0_DATA").unwrap_or_default(); let builtin = card_path.strip_prefix("builtin:"); let card = if let Some(name) = builtin { bundled_l0_source(name) @@ -9171,9 +9247,9 @@ impl MatchEvent for App { // passed these all along and nothing read them, so // every "detail view" capture was silently a capture // of the list. - if let Ok(event) = std::env::var("MAKEPAD_SEED_L0_EVENT") { + if let Some(event) = crate::launch_var("MAKEPAD_SEED_L0_EVENT") { let value = - std::env::var("MAKEPAD_SEED_L0_VALUE").unwrap_or_default(); + crate::launch_var("MAKEPAD_SEED_L0_VALUE").unwrap_or_default(); match app::l0_card::tap(cx, item, "root", &event, &value) { Ok(Some(_)) => { log::info!("SEED_L0 event {event}({value}) applied") @@ -9258,7 +9334,7 @@ impl MatchEvent for App { // by the path a real one does and bumps the same epoch. Nothing in the // language, the kit or the widgets knows the difference — which is the // point: it tests the real chain rather than a parallel one. - if let Some(track_path) = std::env::var("MAKEPAD_FAKE_GPS_FILE").ok().filter(|p| !p.is_empty()) { + if let Some(track_path) = crate::launch_var("MAKEPAD_FAKE_GPS_FILE").filter(|p| !p.is_empty()) { match std::fs::read_to_string(&track_path) { Ok(text) => { let track: Vec<(f64, f64)> = text @@ -9268,8 +9344,7 @@ impl MatchEvent for App { Some((a.trim().parse().ok()?, b.trim().parse().ok()?)) }) .collect(); - let step_ms: u64 = std::env::var("MAKEPAD_FAKE_GPS_MS") - .ok() + let step_ms: u64 = crate::launch_var("MAKEPAD_FAKE_GPS_MS") .and_then(|v| v.parse().ok()) .unwrap_or(1000); log::info!( @@ -9671,7 +9746,7 @@ impl AppMain for App { // phone's shape. Layout is the thing under test, and a card // laid out at 900x700 is a different card — the desktop // default is landscape and the device is not. - if let Ok(spec) = std::env::var("MAKEPAD_WINDOW_SIZE") { + if let Some(spec) = crate::launch_var("MAKEPAD_WINDOW_SIZE") { if let Some((w, h)) = spec.split_once(['x', 'X']) { if let (Ok(w), Ok(h)) = (w.trim().parse::(), h.trim().parse::()) { self.ui.window(cx, ids!(main_window)).resize(cx, dvec2(w, h)); @@ -9707,12 +9782,11 @@ impl AppMain for App { // frames to let the card settle first; sources land asynchronously and // a dump taken too early measures the pending state. if let Event::Draw(_) = event { - if let Ok(path) = std::env::var("MAKEPAD_DUMP_GEOMETRY") { + if let Some(path) = crate::launch_var("MAKEPAD_DUMP_GEOMETRY") { static DRAWS: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0); let n = DRAWS.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1; - let wait: u32 = std::env::var("MAKEPAD_DUMP_GEOMETRY_FRAMES") - .ok() + let wait: u32 = crate::launch_var("MAKEPAD_DUMP_GEOMETRY_FRAMES") .and_then(|v| v.parse().ok()) .unwrap_or(90); if n >= wait { diff --git a/apps/appcard/module/Cargo.toml b/apps/appcard/module/Cargo.toml index 4f4453233..d0f431d89 100644 --- a/apps/appcard/module/Cargo.toml +++ b/apps/appcard/module/Cargo.toml @@ -5,6 +5,11 @@ edition = "2021" license = "Apache-2.0" publish = false +[features] +# A development build: the app reads its launch-time test and provisioning +# hooks (octos-app's `dev_mode`). The shell's `dev-mode` turns it on. +dev-mode = ["octos-app/dev-mode"] + [dependencies] makepad-widgets = { workspace = true, features = ["appcard", "maps"] } makepad-app-module = { workspace = true } diff --git a/crates/ai-host/Cargo.toml b/crates/ai-host/Cargo.toml index 4d7b230eb..a0082436d 100644 --- a/crates/ai-host/Cargo.toml +++ b/crates/ai-host/Cargo.toml @@ -42,6 +42,9 @@ license.workspace = true default = [] octos-core = ["dep:octosense-kernel", "octosense-app-peers/octos-core", "octosense-llm-service?/octos-core"] llm = ["dep:octosense-llm-service"] +# A development build: the kernel then takes launch-time test settings +# (crates/kernel/build.rs `dev_mode`). The shell's `dev-mode` turns it on. +dev-mode = ["octosense-kernel?/dev-mode"] # The system toolbox for app agents (ADR 0002 section 6, ADR 0004 section # 12): `toolbox_peers`, over the octos research engine and the `model` # service's host. diff --git a/crates/kernel/Cargo.toml b/crates/kernel/Cargo.toml index 990493a5c..e39bebac8 100644 --- a/crates/kernel/Cargo.toml +++ b/crates/kernel/Cargo.toml @@ -14,6 +14,11 @@ authors = ["octos-app contributors"] description = "The OctoSense shell's octos kernel: one per process, shared by every consumer over UI Protocol frames" publish = false +[features] +# A development build (build.rs sets `dev_mode`; debug builds are development +# builds without it): only then does an Android kernel take the launch extra +# `makepad.OCTOS_PROXY`. The shells' and AppCard's `dev-mode` turn it on. +dev-mode = [] [dependencies] tokio = { version = "1", features = ["rt-multi-thread", "sync", "io-util", "process", "time", "macros"] } diff --git a/crates/kernel/build.rs b/crates/kernel/build.rs index 2d3c79ff1..ead8c42b2 100644 --- a/crates/kernel/build.rs +++ b/crates/kernel/build.rs @@ -7,10 +7,19 @@ //! it; else `octos-core`, which this crate links). `OCTOSENSE_OCTOS_REVISION` //! at build time overrides it. Empty when neither is found: the desktop then //! refuses a packaged kernel it cannot check (an explicit program still runs). +//! +//! `dev_mode` marks a development build, as the shell's build.rs does: any +//! build with debug assertions, or feature `dev-mode` (a shell's `dev-mode` +//! turns it on). Only then does an Android kernel take the launch extra +//! `makepad.OCTOS_PROXY` (`launch::launch_proxy`). use std::path::Path; fn main() { + println!("cargo:rustc-check-cfg=cfg(dev_mode)"); + if std::env::var_os("CARGO_FEATURE_DEV_MODE").is_some() || std::env::var_os("CARGO_CFG_DEBUG_ASSERTIONS").is_some() { + println!("cargo:rustc-cfg=dev_mode"); + } println!("cargo:rerun-if-env-changed=OCTOSENSE_OCTOS_REVISION"); let lock = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../Cargo.lock"); println!("cargo:rerun-if-changed={}", lock.display()); diff --git a/crates/kernel/src/launch.rs b/crates/kernel/src/launch.rs index d78119918..f18b43972 100644 --- a/crates/kernel/src/launch.rs +++ b/crates/kernel/src/launch.rs @@ -441,18 +441,28 @@ pub(crate) fn phone_stdio(program: PathBuf, core_dir: &Path, extra: &[(String, S env.extend(PHONE_KERNEL_ENV.iter().map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))); // Route the kernel's HTTPS through a proxy when the device has no route // of its own (an `adb reverse` tunnel): launch extra `makepad.OCTOS_PROXY`. - if let Ok(proxy) = std::env::var("MAKEPAD_OCTOS_PROXY") { - let proxy = proxy.trim().to_owned(); - if !proxy.is_empty() { - for k in ["HTTPS_PROXY", "HTTP_PROXY", "https_proxy", "http_proxy", "ALL_PROXY"] { - env.push((k.to_owned(), proxy.clone())); - } + if let Some(proxy) = launch_proxy(cfg!(dev_mode), std::env::var("MAKEPAD_OCTOS_PROXY").ok()) { + for k in ["HTTPS_PROXY", "HTTP_PROXY", "https_proxy", "http_proxy", "ALL_PROXY"] { + env.push((k.to_owned(), proxy.clone())); } } env.extend(extra.iter().cloned()); Launch::Stdio { program, args, env, cwd: Some(home) } } +/// The kernel's proxy from the launch extra `makepad.OCTOS_PROXY` +/// (`MAKEPAD_OCTOS_PROXY`), in a development build only (`dev_mode`: a +/// debug build or feature `dev-mode`, build.rs). Any app on the phone can +/// start Home with that extra, so a release build never routes the kernel's +/// traffic through it. +#[cfg_attr(not(target_os = "android"), allow(dead_code))] +fn launch_proxy(development: bool, value: Option) -> Option { + if !development { + return None; + } + value.map(|proxy| proxy.trim().to_owned()).filter(|proxy| !proxy.is_empty()) +} + /// Make what a start needs: the core dir, the cwd (a missing cwd fails the /// spawn's chdir with ENOENT, permanently, since the kernel would create it), /// on Android the kernel config's memory budget (as AppCard did), the @@ -678,6 +688,15 @@ mod tests { assert_eq!(args, ["serve", "--stdio", "--data-dir", "/f/core"]); } + #[test] + fn only_a_development_build_takes_the_launch_proxy() { + let proxy = Some(" http://127.0.0.1:8899 ".to_owned()); + assert_eq!(launch_proxy(false, proxy.clone()), None, "a release build ignores the launch extra"); + assert_eq!(launch_proxy(true, proxy).as_deref(), Some("http://127.0.0.1:8899")); + assert_eq!(launch_proxy(true, Some(" ".into())), None); + assert_eq!(launch_proxy(true, None), None); + } + #[test] fn kernel_config_is_merged_not_replaced() { let home = tmp("cfg"); diff --git a/crates/shell/Cargo.toml b/crates/shell/Cargo.toml index 5aa65ddfe..2950de5c8 100644 --- a/crates/shell/Cargo.toml +++ b/crates/shell/Cargo.toml @@ -195,8 +195,11 @@ app-aichat = ["dep:makepad-aichat"] # A development build for developer mode (ADR 0004 §13, src/dev_mode.rs): # Settings and OCTOSENSE_DEV_MODE may turn it on. Debug builds are # development builds without it (build.rs); a release build without it -# honours only the `--dev-grant-all` launch flag. -dev-mode = [] +# honours only the `--dev-grant-all` launch flag. Only a development build +# reads launch input: the app config's test actions and demo settings +# (src/launch_input.rs), the kernel's launch proxy and AppCard's +# provisioning and seeding hooks. +dev-mode = ["octosense-ai-host/dev-mode", "octosense-appcard?/dev-mode"] # The `wasm` host service (ADR 0011), which runs an app's own # WebAssembly functions (crates/wasm-host). On in every standard shell # build (tools/native_apps.py's BASE_DEFAULT); it runs where build.rs sets diff --git a/crates/shell/src/apps.rs b/crates/shell/src/apps.rs index 25084ffa1..25030c316 100644 --- a/crates/shell/src/apps.rs +++ b/crates/shell/src/apps.rs @@ -211,7 +211,8 @@ pub(crate) mod test_system_apps { /// app can open: `mail` keeps accounts and passwords for the Mail app; /// `glance` takes the cards apps publish to the glance screen (glance.rs). /// `mail_demo` in MAKEPAD_APP_CONFIG serves a demo mailbox from a file vault -/// instead (no keychain, no network): `MAKEPAD_APP_CONFIG='{"mail_demo":true}'`. +/// instead (no keychain, no network): `MAKEPAD_APP_CONFIG='{"mail_demo":true}'`, +/// in a development build only (`launch_input`). /// `news` fetches News's feeds on a timer, with no model (ADR 0002), into /// the Card runner's host directory, so it keeps fetching while News is /// closed. Last, every system app no service answers gets the notice @@ -229,12 +230,7 @@ pub fn register_mail_services() { static ONCE: std::sync::Once = std::sync::Once::new(); ONCE.call_once(|| { crate::glance::register(); - let demo = std::env::var("MAKEPAD_APP_CONFIG") - .ok() - .and_then(|text| makepad_strict_json::parse(text.as_bytes()).ok()) - .and_then(|config| config.get("mail_demo").and_then(|v| v.as_bool())) - .unwrap_or(false); - if demo { + if crate::launch_input::flag("mail_demo") { octosense_mail_service::register_demo() } else { octosense_mail_service::register() @@ -683,8 +679,9 @@ pub fn matches_running_app(app: &crate::clients::AppDef, running_id: &str, title /// What a launch of `app` opens `module` with. The Card runner opens the app /// the launcher row names — a system app by its `os.*` manifest id, an /// installed one by its own. Any other module opens with what -/// MAKEPAD_APP_CONFIG gives it (`{"module_open": {"": {...}}}`, or -/// `mail_endpoint` for a linked `mail` module), else empty. +/// MAKEPAD_APP_CONFIG gives it in a development build +/// (`{"module_open": {"": {...}}}`, or `mail_endpoint` for a linked +/// `mail` module; `launch_input`), else empty. pub fn module_open(module: &'static dyn AppModule, app: &crate::clients::AppDef) -> Result { let schema = module.open_schema(); if module.id() == "card" { @@ -695,7 +692,7 @@ pub fn module_open(module: &'static dyn AppModule, app: &crate::clients::AppDef) let instance = app.args.iter().find_map(|a| a.strip_prefix("--studio-instance=")).ok_or("Studio launch requires an admitted instance")?; return schema.validate(&format!("{{\"instance_id\":{}}}", makepad_strict_json::Value::Str(instance.into()).to_json()), &[]); } - let config = std::env::var("MAKEPAD_APP_CONFIG").ok().and_then(|text| makepad_strict_json::parse(text.as_bytes()).ok()); + let config = crate::launch_input::app_config(); if let Some(json) = config.as_ref().and_then(|c| c.get("module_open")).and_then(|v| v.get(module.id())).map(|v| v.to_json()) { return schema.validate(&json, &[]); } diff --git a/crates/shell/src/launch_input.rs b/crates/shell/src/launch_input.rs new file mode 100644 index 000000000..cf568335e --- /dev/null +++ b/crates/shell/src/launch_input.rs @@ -0,0 +1,152 @@ +//! Launch input: settings that arrive with a launch, which another app on +//! the device may have chosen. +//! +//! **Who can send it.** Home's main activity is exported: it is the app +//! drawer entry, the Home app, a share target and a deep-link target, so any +//! installed app can start it, with extras. When the activity is created, +//! Makepad's Android glue copies the extras `makepad.` into the +//! environment as `MAKEPAD_` (`APP_CONFIG`, `PROVISION_CONFIG`, +//! `OCTOS_PROXY`, the `SEED_*` hooks and others). On OpenHarmony the entry +//! ability passes the Want's `makepad.*` parameters on in the same way, but +//! only in a development build (`phone/ohos/EntryAbility.ets`). On a desktop +//! these variables come from the local user's own environment, as +//! command-line flags do. +//! +//! **The rule.** OctoSense reads launch input only in a development build +//! (`cfg(dev_mode)`: a debug build, or `--features dev-mode`; never a store +//! build). That covers the app config below (test actions, Mail's demo +//! mailbox, module open parameters), the kernel's launch proxy +//! (`octosense-kernel`) and AppCard's provisioning and seeding hooks +//! (`octos-app`). A release build ignores them, and says so once in the log +//! without echoing the value. `--test-action` flags on a desktop command +//! line are arguments only the local user can pass; they work in every +//! build (`App::run_test_actions` in `lib.rs`). +//! +//! Test builds that need these hooks on a phone are development builds +//! under a separate package name (`rom/scripts/build-home.py --dev-mode`). + +use crate::dev_mode::BuildKind; +use makepad_strict_json::Value; + +/// The app config: one JSON object, +/// `{"test_actions": ["", …], "mail_demo": true, +/// "module_open": {"": {…}}, "mail_endpoint": "…"}`. +pub const APP_CONFIG: &str = "MAKEPAD_APP_CONFIG"; + +/// Whether a build of this kind reads launch input: a development build +/// only. +pub fn honoured(build: BuildKind) -> bool { + build == BuildKind::Development +} + +/// The app config, parsed, when this build reads launch input. A release +/// build answers `None` whatever the variable holds. +pub fn app_config() -> Option { + let text = std::env::var(APP_CONFIG).ok()?; + let build = BuildKind::current(); + if !honoured(build) { + note_ignored(); + } + parse_app_config(build, &text) +} + +/// [`app_config`] for a given build and variable text. +pub fn parse_app_config(build: BuildKind, text: &str) -> Option { + if !honoured(build) { + return None; + } + makepad_strict_json::parse(text.as_bytes()).ok() +} + +/// The app config's `test_actions`, in order, as `--test-action` names; +/// none in a release build. +pub fn test_actions() -> Vec { + app_config().map(|config| test_actions_in(&config)).unwrap_or_default() +} + +/// [`test_actions`] for a given build and variable text. +pub fn test_actions_for(build: BuildKind, text: &str) -> Vec { + parse_app_config(build, text).map(|config| test_actions_in(&config)).unwrap_or_default() +} + +fn test_actions_in(config: &Value) -> Vec { + config + .get("test_actions") + .and_then(|actions| actions.as_arr()) + .map(|actions| actions.iter().filter_map(|action| action.as_str()).map(str::to_owned).collect()) + .unwrap_or_default() +} + +/// The `true`/`false` value `key` has in the app config, when this build +/// reads it. +pub fn flag(key: &str) -> bool { + app_config().and_then(|config| config.get(key).and_then(|value| value.as_bool())).unwrap_or(false) +} + +fn note_ignored() { + static ONCE: std::sync::Once = std::sync::Once::new(); + ONCE.call_once(|| { + makepad_widgets::log!( + "launch input: {APP_CONFIG} ignored; a release build takes no test actions or demo settings from a launch" + ) + }); +} + +#[cfg(test)] +mod tests { + use super::*; + + const CONFIG: &str = r#"{"test_actions":["capture:/sdcard/Download/frame.png","launch-mail","system-chat-send:hello"],"mail_demo":true}"#; + + #[test] + fn a_release_build_ignores_test_actions_from_the_launch() { + for build in [BuildKind::Release, BuildKind::Store] { + assert!(!honoured(build), "{build:?}"); + assert!(test_actions_for(build, CONFIG).is_empty(), "{build:?}"); + assert!(parse_app_config(build, CONFIG).is_none(), "{build:?}"); + } + } + + #[test] + fn a_development_build_keeps_them_in_order() { + assert!(honoured(BuildKind::Development)); + assert_eq!( + test_actions_for(BuildKind::Development, CONFIG), + ["capture:/sdcard/Download/frame.png", "launch-mail", "system-chat-send:hello"] + ); + let config = parse_app_config(BuildKind::Development, CONFIG).expect("parsed"); + assert_eq!(config.get("mail_demo").and_then(|v| v.as_bool()), Some(true)); + } + + #[test] + fn malformed_or_missing_actions_give_none() { + for text in ["", "not json", r#"{"test_actions":"launch-mail"}"#, r#"{"mail_demo":true}"#] { + assert!(test_actions_for(BuildKind::Development, text).is_empty(), "{text:?}"); + } + // Entries that are not strings are skipped, the rest kept. + assert_eq!(test_actions_for(BuildKind::Development, r#"{"test_actions":[1,"launch-news",null]}"#), ["launch-news"]); + } + + /// The shell has one reader of the app config: this module. Anything + /// else reading the variable directly would bypass the build check. + #[test] + fn no_other_shell_source_reads_the_app_config_directly() { + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut offenders = Vec::new(); + let mut stack = vec![root.clone()]; + while let Some(dir) = stack.pop() { + for entry in std::fs::read_dir(&dir).expect("read src") { + let path = entry.expect("entry").path(); + if path.is_dir() { + stack.push(path); + } else if path.extension().is_some_and(|e| e == "rs") && !path.ends_with("launch_input.rs") { + let text = std::fs::read_to_string(&path).expect("read source"); + if text.contains("env::var(\"MAKEPAD_APP_CONFIG\"") || text.contains("env::var_os(\"MAKEPAD_APP_CONFIG\"") { + offenders.push(path.strip_prefix(&root).unwrap().display().to_string()); + } + } + } + } + assert!(offenders.is_empty(), "read MAKEPAD_APP_CONFIG through launch_input: {offenders:?}"); + } +} diff --git a/crates/shell/src/lib.rs b/crates/shell/src/lib.rs index 60d5c6263..13b780040 100644 --- a/crates/shell/src/lib.rs +++ b/crates/shell/src/lib.rs @@ -19,6 +19,7 @@ pub mod ai_bus; pub mod app_chat; pub mod app_storage; pub mod dev_mode; +pub mod launch_input; pub mod approvals; pub mod apps; pub mod binds; @@ -5443,17 +5444,14 @@ impl App { /// `--test-action ` fires one WmAction at startup, so every /// binding can be driven from a script even where the host OS keeps a - /// chord for itself. + /// chord for itself. Command-line flags, which only the local user can + /// pass, work in every build. The app config's `test_actions` arrive + /// with a launch that another app may have made (an Android intent + /// extra), so only a development build takes them (`launch_input`). fn run_test_actions(&mut self, cx: &mut Cx) { let mut args: Vec = std::env::args().collect(); - if let Ok(config) = std::env::var("MAKEPAD_APP_CONFIG") { - if let Ok(config) = makepad_strict_json::parse(config.as_bytes()) { - if let Some(actions) = config.get("test_actions").and_then(|v| v.as_arr()) { - for action in actions.iter().filter_map(|v| v.as_str()) { - args.extend(["--test-action".into(), action.into()]); - } - } - } + for action in launch_input::test_actions() { + args.extend(["--test-action".into(), action]); } let mut i = 0; while i < args.len() { diff --git a/phone/ohos/EntryAbility.ets b/phone/ohos/EntryAbility.ets index 29324c2cf..08c84bff4 100644 --- a/phone/ohos/EntryAbility.ets +++ b/phone/ohos/EntryAbility.ets @@ -5,16 +5,25 @@ import { BusinessError } from '@kit.BasicServicesKit'; import makepad from 'libmakepad.so' import {ArkGlue} from '../makepad/makepad' +// Any app can start this exported ability with any parameters, and Makepad +// turns each `makepad.` launch parameter into the environment variables +// MAKEPAD_ and : test hooks, provisioning, a Studio connection, +// or any other variable Home reads. Only a development build passes them on; +// rom/scripts/build-home-ohos.py --dev-mode stages this file with `true`. +const FORWARD_LAUNCH_PARAMETERS: boolean = false; + export default class EntryAbility extends UIAbility { private studioReview: boolean = false; onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { let x = ArkGlue.initInstance(this.context); const parameters: Record = {}; - for (const key of Object.keys(want.parameters ?? {})) { - const value = want.parameters?.[key]; - if (key.startsWith('makepad.') && typeof value === 'string') { - parameters[key] = value; + if (FORWARD_LAUNCH_PARAMETERS) { + for (const key of Object.keys(want.parameters ?? {})) { + const value = want.parameters?.[key]; + if (key.startsWith('makepad.') && typeof value === 'string') { + parameters[key] = value; + } } } x.launchParameters = JSON.stringify(parameters); diff --git a/rom/scripts/build-home-ohos.py b/rom/scripts/build-home-ohos.py index 758eef2ac..93ed1b866 100644 --- a/rom/scripts/build-home-ohos.py +++ b/rom/scripts/build-home-ohos.py @@ -22,6 +22,26 @@ def read_json5(path): return json.loads(token.sub(lambda m: m.group() if m.group().startswith('"') else '', path.read_text())) +# The one line of phone/ohos/EntryAbility.ets that decides whether launch +# parameters reach Makepad's environment. Only a development build turns it on. +FORWARD_LAUNCH_PARAMETERS = 'const FORWARD_LAUNCH_PARAMETERS: boolean = false;' + + +def stage_entry_ability(project, home, dev_mode): + """Copy the product's EntryAbility over the template's. Any app can start + the exported ability with parameters, and Makepad turns each `makepad.*` + one into environment variables, so a release build forwards none; a + development build (`--dev-mode`) forwards them for test hooks.""" + source = (home / 'ohos/EntryAbility.ets').read_text() + if source.count(FORWARD_LAUNCH_PARAMETERS) != 1: + raise RuntimeError('EntryAbility.ets must declare FORWARD_LAUNCH_PARAMETERS false exactly once') + if dev_mode: + source = source.replace(FORWARD_LAUNCH_PARAMETERS, FORWARD_LAUNCH_PARAMETERS.replace('false', 'true')) + target = project / 'entry/src/main/ets/entryability/EntryAbility.ets' + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(source) + + def stage_app_icons(project, home): """Replace the pinned template's application, ability and launch icons.""" source = home / 'ohos/icons' @@ -44,6 +64,9 @@ def main(): p.add_argument('--packager', required=True, type=Path) p.add_argument('--compatible-sdk', default='6.0.1(21)') p.add_argument('--remote-port', type=int, help='Validation only: app-owned loopback inspection') + p.add_argument('--dev-mode', action='store_true', + help='Development test build: --features dev-mode, and launch parameters reach the app ' + '(test hooks). Needs a separate --bundle-id') p.add_argument('--offline', action='store_true') args = p.parse_args() for name in ('deveco_home', 'signing_config', 'packager'): @@ -52,6 +75,8 @@ def main(): p.error('--remote-port must be between 1024 and 65535') if not re.fullmatch(r'[A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z][A-Za-z0-9_]*)+', args.bundle_id): p.error('Invalid bundle ID') + if args.dev_mode and args.bundle_id == 'dev.makepad.octosense': + p.error('A --dev-mode build takes launch-time test hooks; give it a separate --bundle-id') if args.signing_config.is_relative_to(ROOT): p.error('Keep signing configuration outside the checkout') configs = json.loads(args.signing_config.read_text()) @@ -84,6 +109,8 @@ def main(): if args.remote_port: env['MAKEPAD_REMOTE'] = str(args.remote_port) cargo_args = ['-p', 'octosense-home', '--release', '--locked'] + if args.dev_mode: + cargo_args += ['--features', 'dev-mode'] if args.offline: cargo_args.append('--offline') command = [str(args.packager), 'makepad', 'ohos', '--deveco-home=' + str(args.deveco_home), '--arch=aarch64'] @@ -95,9 +122,9 @@ def main(): shutil.copytree(REPO / '.sources/makepad/tools/open_harmony/deveco', project, dirs_exist_ok=True) stage_app_icons(project, home) # Product-owned window policy: keep native system navigation visible and - # keep the floating controls inside its safe area. - shutil.copy2(home / 'ohos/EntryAbility.ets', - project / 'entry/src/main/ets/entryability/EntryAbility.ets') + # keep the floating controls inside its safe area. A release build passes + # no launch parameters on. + stage_entry_ability(project, home, args.dev_mode) subprocess.run(['patch', '--batch', '--forward', '-p1', '-i', str(home / 'ohos/keyboard.patch')], cwd=project, check=True) app_path = project / 'AppScope/app.json5' @@ -130,12 +157,13 @@ def main(): build['app']['signingConfigs'] = [] build_path.write_text(json.dumps(build, indent=2) + '\n') hap = project / 'entry/build/default/outputs/default/makepad-default-signed.hap' - output = ROOT / 'out/home' / ('ohos-validation' if args.remote_port else 'ohos') + output = ROOT / 'out/home' / (('ohos-validation' if args.remote_port else 'ohos') + ('-dev' if args.dev_mode else '')) output.mkdir(parents=True, exist_ok=True) shutil.copy2(hap, output / 'OctoSenseHome.hap') receipt = { 'schema_version': 1, 'variant': 'ohos', 'bundle_id': args.bundle_id, 'validation_remote': bool(args.remote_port), + 'dev_mode': args.dev_mode, 'source_revision': subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=ROOT, text=True).strip(), 'source_dirty': bool(subprocess.check_output(['git', 'status', '--porcelain'], cwd=ROOT, text=True)), 'packager': str(args.packager), diff --git a/rom/tests/test_home_build.py b/rom/tests/test_home_build.py index 5fd692895..73a9669b6 100644 --- a/rom/tests/test_home_build.py +++ b/rom/tests/test_home_build.py @@ -7,6 +7,7 @@ import json from pathlib import Path import re +import sys import tempfile import unittest from unittest import mock @@ -51,6 +52,37 @@ def test_product_icons_replace_the_template_on_every_build(self): self.assertEqual((entry_media / "unrelated.png").read_bytes(), b"keep") +class OpenHarmonyLaunchParameterTests(unittest.TestCase): + """Any app can start Home's exported ability with parameters, and Makepad + turns each `makepad.*` one into environment variables: only a development + build passes them on.""" + + def test_the_product_ability_forwards_none_by_default(self): + source = (ROOT.parent / "phone/ohos/EntryAbility.ets").read_text() + self.assertEqual(source.count(ohos.FORWARD_LAUNCH_PARAMETERS), 1) + self.assertLess(source.index("if (FORWARD_LAUNCH_PARAMETERS)"), source.index("want.parameters"), + "the parameters are read only behind the switch") + + def test_only_a_dev_mode_build_forwards_them(self): + with tempfile.TemporaryDirectory() as temp: + project = Path(temp) + staged = project / "entry/src/main/ets/entryability/EntryAbility.ets" + ohos.stage_entry_ability(project, ROOT.parent / "phone", False) + self.assertEqual(staged.read_text(), (ROOT.parent / "phone/ohos/EntryAbility.ets").read_text()) + ohos.stage_entry_ability(project, ROOT.parent / "phone", True) + text = staged.read_text() + self.assertIn("const FORWARD_LAUNCH_PARAMETERS: boolean = true;", text) + self.assertNotIn(ohos.FORWARD_LAUNCH_PARAMETERS, text) + + def test_a_dev_mode_build_needs_its_own_bundle_id(self): + argv = ["build-home-ohos.py", "--deveco-home", "/deveco", "--signing-config", "/private/signing.json", + "--packager", "/tools/cargo-makepad", "--dev-mode"] + with mock.patch.object(sys, "argv", argv), contextlib.redirect_stderr(io.StringIO()) as err, \ + self.assertRaises(SystemExit): + ohos.main() + self.assertIn("separate --bundle-id", err.getvalue()) + + class BuildTests(unittest.TestCase): def args(self, *extra): return build.arguments(["--sdk", "/sdk with spaces", "--android-sdk", "/android", *extra]) From 9512485ec65d0c25614348520e5cca1cdbf220c4 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Sun, 11 Oct 2026 10:00:44 -0700 Subject: [PATCH 2/4] Point launch-time test flows at development builds Docs and scripts that passed `makepad.*` launch extras to release packages now name a development test build: `rom/scripts/build-home.py --dev-mode` under a separate package name on Android, `build-home-ohos.py --dev-mode` on OpenHarmony, `--features dev-mode` for the desktop mail demo, and `DEV_MODE=1` / `--features dev-mode` for AppCard's own tools. `provision-appcard-llm.sh` requires PACKAGE=dev.makepad.octosense.. AGENTS.md states the rule; English and Chinese docs change together. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 2 +- apps/AGENTS.md | 5 ++- apps/README.md | 10 +++-- apps/README.zh-CN.md | 8 ++-- apps/appcard/docs/BUILDING-ANDROID.md | 10 ++++- apps/appcard/docs/PROVISIONING-LLM.md | 5 ++- .../docs/WEBVIEW-VIDEO-BLACK-HANDOFF.md | 4 ++ apps/appcard/docs/tools/device-l0-tap.py | 6 ++- apps/appcard/docs/tools/device-l0-test.py | 4 ++ apps/appcard/docs/tools/l0-visual.py | 3 ++ apps/appcard/docs/weather-styles/README.md | 4 +- apps/appcard/tools/build-android.sh | 8 ++++ apps/appcard/tools/octos-macos.py | 4 +- apps/appcard/tools/octos-ohos.py | 5 ++- crates/kernel/README.md | 6 ++- crates/kernel/README.zh-CN.md | 3 +- desktop/README.md | 6 +-- desktop/README.zh-CN.md | 6 +-- desktop/docs/android-appcard-build.md | 12 +++++- desktop/docs/code-walkthrough.md | 7 +++- desktop/docs/code-walkthrough.zh-CN.md | 4 +- desktop/scripts/provision-appcard-llm.sh | 22 ++++++++--- phone/README.md | 38 +++++++++++++++++-- phone/README.zh-CN.md | 27 +++++++++++-- phone/docs/android-appcard-build.md | 12 +++++- .../android/perf-home-swipes-oneplus-6.md | 5 ++- phone/scripts/provision-appcard-llm.sh | 22 ++++++++--- rom/docs/home-build.md | 26 ++++++++++++- tools/connected-e2e/ANDROID-BACKEND.md | 2 +- tools/connected-e2e/ANDROID-BACKEND.zh-CN.md | 2 +- tools/connected-e2e/android-notes.md | 4 ++ tools/connected-e2e/android-notes.zh-CN.md | 4 ++ 32 files changed, 229 insertions(+), 57 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 50e827c5e..50084b38e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,7 +56,7 @@ On a machine that already has clones of Makepad, OctoScript or Octoscript-Makepa 4. **One kernel per process.** The kernel service starts octos on the first consumer and shares it; consumers never spawn their own. Apps reach the assistant only through `crates/app-peers`, never through the raw kernel protocol. 5. **System apps ship inside the shells.** Their ids start with `os.`, they are packed by digest from each packaging's `system-apps.json`, and they are not released separately or through the store. 6. **Test UI without taking over the screen.** Run hosted apps and shells with hidden windows and drive them over the local control surface: `MAKEPAD_HIDE_WINDOWS=1 MAKEPAD_REMOTE=` (routes under `/help`; end with `/quit`), or `makepad_test`. Use `--test-action`, `MAKEPAD_WM_TEST_APP` and `--module` to reach a state without clicking. -7. **Devices.** Use only a device you were given for the task. Install test builds under a separate package name; never replace the installed Home or flash an image unless asked. Anything not run on a device is **unverified**, and says so. +7. **Devices.** Use only a device you were given for the task. Install test builds under a separate package name; never replace the installed Home or flash an image unless asked. Anything not run on a device is **unverified**, and says so. Launch-time test hooks (the `test_actions` in the `makepad.APP_CONFIG` intent extra and the other `makepad.*` extras or OpenHarmony launch parameters) work only in a development build (`cfg(dev_mode)`: debug, or `--features dev-mode`; `rom/scripts/build-home.py --dev-mode` builds one as a test package): any app on a phone can start Home with extras, so a release build ignores them. Read such a hook only through `crates/shell/src/launch_input.rs` (AppCard: `launch_var`), never with a bare `std::env::var`. 8. **Commit with a public identity.** Author and committer are your GitHub noreply address (for the maintainer, `ymote <151983+ymote@users.noreply.github.com>`) or another address you mean to publish; never a work or machine-local address. Check `git config user.email` before the first commit in a clone: a repository-local identity on a shared machine leaked a work address into this project's history once, and removing it took a history rewrite. 9. **Keep docs honest and bilingual.** Every command in a doc was run; anything not run is marked **unverified**. User-facing docs are `README.md` plus `README.zh-CN.md`, linked by the single switcher line under the title (`English | [简体中文](README.zh-CN.md)`), and change together. `AGENTS.md` files are English only. 10. **Keep signing keys, keystores, tokens and personal paths out of the repository.** `rom/tests/test_no_local_paths.py` checks for local paths. diff --git a/apps/AGENTS.md b/apps/AGENTS.md index 4d995e4ad..8fcd72ed9 100644 --- a/apps/AGENTS.md +++ b/apps/AGENTS.md @@ -29,8 +29,9 @@ app's own agent and `tools.json`, the system toolbox, `glance.publish` and --system` (add `MAKEPAD_REMOTE=` to drive it over HTTP; Photos also takes `--static photos=`). `--system` lets an `os.*` id and an empty digest through, as the shell does. These flags are on App Hub `main`. - `card-host` registers no host services: run Mail in a shell with - `MAKEPAD_APP_CONFIG='{"mail_demo":true}'`. + `card-host` registers no host services: run Mail in a development build + of a shell (debug, or `--features dev-mode`) with + `MAKEPAD_APP_CONFIG='{"mail_demo":true}'`; a release build ignores it. - Validate on a phone through Home (`phone/`) built as a separate test package; never replace the device's installed Home. - Text on a page follows the host's appearance. On a phone, a page drawn by a diff --git a/apps/README.md b/apps/README.md index 98cb27d3f..a3b47f8d7 100644 --- a/apps/README.md +++ b/apps/README.md @@ -285,7 +285,7 @@ standalone launcher and ROM image). Each packaging: [`crates/shell`](../crates/shell) (its `app-hub` feature; the photo engine's `octosense-photo-service` comes with the desktop-only `craft-engines`, weighed per engine in [ADR 0013](../docs/adr/0013-craft-engines-as-pinned-services.md)), and registers them at startup (`crates/shell/src/apps.rs`, `register_host_services`): Mail with `register()` for real accounts, or `register_demo()` - when the shell's app config has `mail_demo: true`. Mail, News and, with the photo + when the shell's app config has `mail_demo: true` (a development build only). Mail, News and, with the photo engine, Photos install `on_notify` callbacks to the shell's common notice renderer; Calendar installs its card publisher. The shell registers `NoticeService` for remaining system-app namespaces; `llm` with the octos @@ -455,12 +455,14 @@ password `demo`, sample messages, sends that go nowhere): ```sh # the desktop, from the repository root -MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --release -p octosense +MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --release -p octosense --features dev-mode # Home in a phone-sized window, from phone/ -MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --release -p octosense-home --features mobile-only +MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --release -p octosense-home --features mobile-only,dev-mode ``` -The demo keeps its password in a file, so no keychain prompt appears. +The demo keeps its password in a file, so no keychain prompt appears. Only a +development build (debug, or `--features dev-mode`) reads `MAKEPAD_APP_CONFIG`; +both commands are **unverified** since that change. **Quick Deck's dev fixture.** `card-host` has no `deck` service, and the shipped bundle has no fixture code. Quick Deck's UI tests use diff --git a/apps/README.zh-CN.md b/apps/README.zh-CN.md index 2417fc156..6b56426d1 100644 --- a/apps/README.zh-CN.md +++ b/apps/README.zh-CN.md @@ -222,7 +222,7 @@ WebView 页面、图库导出、邮件、新闻、Glance 或单次模型调用 path 依赖;photo 引擎的 `octosense-photo-service` 随仅限桌面的 `craft-engines` 引入, 按引擎权衡见 [ADR 0013](../docs/adr/0013-craft-engines-as-pinned-services.zh-CN.md)),并在启动时注册 (`crates/shell/src/apps.rs` 的 `register_host_services`):Mail 服务在真实账户下用 - `register()`,Shell 的应用配置中 `mail_demo: true` 时用 `register_demo()`。 + `register()`,Shell 的应用配置中 `mail_demo: true` 时(仅限开发版)用 `register_demo()`。 Mail、News 以及(带 photo 引擎时)Photos 安装 `on_notify` 回调,调用 Shell 共用通知渲染器;Calendar 安装卡片发布器。 Shell 为其余系统应用命名空间注册 `NoticeService`;`llm` 服务使用 octos 内核的 core 目录以及 Shell 的二维码扫描器和图片选择器(见 [`llm` 服务](#llm-服务))。App Hub 只在根目录 @@ -374,12 +374,12 @@ card-host --bundle /apps/photos/bundle --system --static photos=<原 ```sh # 桌面端,在仓库根目录 -MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --release -p octosense +MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --release -p octosense --features dev-mode # 手机尺寸窗口中的 Home,在 phone/ 中 -MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --release -p octosense-home --features mobile-only +MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --release -p octosense-home --features mobile-only,dev-mode ``` -演示邮箱的密码存放在文件中,因此不会弹出钥匙串提示。 +演示邮箱的密码存放在文件中,因此不会弹出钥匙串提示。只有开发版(调试构建或 `--features dev-mode`)读取 `MAKEPAD_APP_CONFIG`;这一改动后两条命令均**未验证**。 **Quick Deck 的开发夹具。** `card-host` 没有 `deck` 服务,而随 Shell 发布的 bundle 中没有任何夹具代码。Quick Deck 的 UI 测试使用 bundle 之外的 diff --git a/apps/appcard/docs/BUILDING-ANDROID.md b/apps/appcard/docs/BUILDING-ANDROID.md index b249fffec..bbd20ac6f 100644 --- a/apps/appcard/docs/BUILDING-ANDROID.md +++ b/apps/appcard/docs/BUILDING-ANDROID.md @@ -64,8 +64,14 @@ Then, if you don't already have the NDK, `cargo makepad android install-toolchai tools/build-android.sh # APK only tools/build-android.sh run # APK, install, launch, stream logcat SKIP_KERNEL=1 tools/build-android.sh # reuse the kernel already built +DEV_MODE=1 tools/build-android.sh # a development build (launch-time extras) ``` +Only a development build (`DEV_MODE=1`, i.e. `--features dev-mode`, or a debug +build) reads the launch-time `makepad.*` extras in step 5: any app on the phone +can start this one with extras, so a release build ignores them (`launch_var` +in `app/app/src/lib.rs`). The `DEV_MODE=1` line is **unverified**. + That script is the supported path and it does both halves: it cross-compiles the `octos` submodule for `aarch64-linux-android` and hands the binary to `cargo makepad` as `liboctos.so`. It discovers the NDK under @@ -114,7 +120,7 @@ cargo makepad android build -p octos-app --release ADB=adb # or /mnt/c/.../adb.exe on WSL $ADB install -r app/target/android/makepad-android-apk/octos_app/apk/octos_app.apk -# launch. Extras (all optional): +# launch. Extras (all optional; read only by a development build, DEV_MODE=1): # makepad.OCTOS_PROXY http proxy for the LLM + data fetches (phones w/o direct net) # makepad.APP_CONFIG 'base_url|profile|token' (headless auth provisioning) # makepad.AUTO_PROMPT auto-submit one prompt on boot (for testing) @@ -171,5 +177,5 @@ The per-profile LLM provider + key live in `_main.json` (`config.llm` + to talk to USB phones; the SDK's Linux `adb` can't see them. - Phones without a direct internet route reach out through a host proxy: run an HTTP CONNECT proxy on the host, `adb reverse tcp:8899 tcp:8899`, and pass - `--es makepad.OCTOS_PROXY 'http://127.0.0.1:8899'`. This routes both the LLM + `--es makepad.OCTOS_PROXY 'http://127.0.0.1:8899'` to a development build. This routes both the LLM (`api.z.ai` / provider) and the card data fetches (open-meteo, Yahoo, HN). diff --git a/apps/appcard/docs/PROVISIONING-LLM.md b/apps/appcard/docs/PROVISIONING-LLM.md index 5ccf5551e..38ece54d6 100644 --- a/apps/appcard/docs/PROVISIONING-LLM.md +++ b/apps/appcard/docs/PROVISIONING-LLM.md @@ -50,7 +50,10 @@ names (`octosense_llm_config::registry`). ## Provisioning without the camera (dev / headless) The same JSON payload can be applied via the launch intent (no scanning), which is -how the flow is tested: +how the flow is tested. Any app on the phone can start this one with extras, so +only a development build (`DEV_MODE=1 tools/build-android.sh`, i.e. +`--features dev-mode`) reads it; a release build ignores it and provisions only +through the scan and its PIN: ```bash adb shell am start -S -n dev.makepad.octos_app/.MakepadApp \ --es makepad.PROVISION_CONFIG '{"llm_family":"zai","llm_model":"glm-5.2","llm_key":"sk-XXXX"}' diff --git a/apps/appcard/docs/WEBVIEW-VIDEO-BLACK-HANDOFF.md b/apps/appcard/docs/WEBVIEW-VIDEO-BLACK-HANDOFF.md index 31e0fc221..fb0bef0e1 100644 --- a/apps/appcard/docs/WEBVIEW-VIDEO-BLACK-HANDOFF.md +++ b/apps/appcard/docs/WEBVIEW-VIDEO-BLACK-HANDOFF.md @@ -214,6 +214,10 @@ $ADB -s bf0a4730 forward tcp:9222 localabstract:$(… webview_devtools_remote_

# record a golden (review it by eye first) device-l0-test.py run [case…] # check against goldens; exit 1 on failure +The installed app must be a development build (`DEV_MODE=1 +tools/build-android.sh`): a release build ignores the launch-time +`makepad.SEED_*` extras these tools seed cards with. + Goldens live in docs/tools/golden/. Regenerate deliberately, never to make a failure go away — a golden updated without looking is a test that asserts whatever the code currently does. diff --git a/apps/appcard/docs/tools/l0-visual.py b/apps/appcard/docs/tools/l0-visual.py index ff2c2e8fc..e9e32e3aa 100755 --- a/apps/appcard/docs/tools/l0-visual.py +++ b/apps/appcard/docs/tools/l0-visual.py @@ -8,6 +8,9 @@ l0-visual.py capture all three and report drift l0-visual.py weather just one + +Needs a development build of the app (`DEV_MODE=1 tools/build-android.sh`), +the only kind that reads the launch-time `makepad.SEED_L0_*` extras. """ import importlib.util, subprocess, sys from pathlib import Path diff --git a/apps/appcard/docs/weather-styles/README.md b/apps/appcard/docs/weather-styles/README.md index 893595660..97d87687d 100644 --- a/apps/appcard/docs/weather-styles/README.md +++ b/apps/appcard/docs/weather-styles/README.md @@ -56,5 +56,7 @@ To make the choices live on-device, rebuild + deploy the app memory: 1. Author one `style-.splash` (copy the closest existing one; keep every number a `sys.weather` binding). 2. Verify on device via `--es makepad.SEED_CARD_FILE /data/local/tmp/x.splash` - (raw DSL → auto-wrapped in `runsplash`; no APK rebuild needed). + (raw DSL → auto-wrapped in `runsplash`; no APK rebuild needed once a + development build, `DEV_MODE=1 tools/build-android.sh`, is installed: a + release build ignores launch-time extras). 3. Add a row to the catalog table + a keyword to the selection rule in `app.md`. diff --git a/apps/appcard/tools/build-android.sh b/apps/appcard/tools/build-android.sh index d7d25f853..6a5e9bf8a 100755 --- a/apps/appcard/tools/build-android.sh +++ b/apps/appcard/tools/build-android.sh @@ -17,6 +17,7 @@ # tools/build-android.sh # build the APK # tools/build-android.sh run # build, install and launch # SKIP_KERNEL=1 tools/build-android.sh # reuse an already-built kernel +# DEV_MODE=1 tools/build-android.sh # a development build (launch-time hooks) # set -euo pipefail @@ -96,4 +97,11 @@ cd "$ROOT/app" # Android renders through the Vulkan backend (like the OpenHarmony build): the # map's compact vertex formats only exist there. `MAKEPAD=gles` forces OpenGL. export MAKEPAD="${MAKEPAD:-vulkan}" +# DEV_MODE=1 makes a development build, the only kind that reads the +# launch-time `makepad.*` hooks (provisioning, seeding, the auto prompt; see +# `launch_var` in app/app/src/lib.rs): any app on the phone can start this one +# with extras, so a release build ignores them. +if [[ "${DEV_MODE:-}" == 1 ]]; then + exec "$CARGO_MAKEPAD" makepad android "$ACTION" -p octos-app --release --features dev-mode +fi exec "$CARGO_MAKEPAD" makepad android "$ACTION" -p octos-app --release diff --git a/apps/appcard/tools/octos-macos.py b/apps/appcard/tools/octos-macos.py index fd0cb91ea..ec1d0c7c7 100644 --- a/apps/appcard/tools/octos-macos.py +++ b/apps/appcard/tools/octos-macos.py @@ -92,8 +92,10 @@ def main(): empty_data.write_text('{}\n') env['MAKEPAD_SEED_L0_DATA'] = str(empty_data) if not request.get('skip_build', False): + # A development build: only `dev-mode` reads the MAKEPAD_* hooks set + # above (`launch_var` in app/app/src/lib.rs). subprocess.run(['cargo', 'build', '-p', 'octos-app', '--bin', 'octos-app', - '--release', '--message-format=json'], cwd=root / 'app', env=env, check=True) + '--release', '--features', 'dev-mode', '--message-format=json'], cwd=root / 'app', env=env, check=True) if not Path(env['OCTOS_APP_CORE_BIN']).is_file(): raise SystemExit('Build the current native Octos core before launching this RunItem') evidence = Path(request.get('evidence_dir', '/tmp/octos-macos-generation')) diff --git a/apps/appcard/tools/octos-ohos.py b/apps/appcard/tools/octos-ohos.py index 0e9fa4c59..47dd0e2d8 100644 --- a/apps/appcard/tools/octos-ohos.py +++ b/apps/appcard/tools/octos-ohos.py @@ -56,8 +56,11 @@ def device_run(*args, quiet=False): def build_app(command): log = evidence / f'build-{build}-{command}.log' with log.open('w') as out: + # A development build: the launch parameters below (Studio, + # REAPPROVE_CARDS, AUTO_PROMPT, PROVISION_CONFIG) reach only a + # build with `dev-mode` (`launch_var` in app/app/src/lib.rs). p = subprocess.run([str(tool), 'ohos', '--deveco-home=' + os.environ['DEVECO_HOME'], - command, '-p', 'octos-app', '--release'], + command, '-p', 'octos-app', '--release', '--features', 'dev-mode'], cwd=root / 'app', stdout=out, stderr=subprocess.STDOUT) if p.returncode: raise SystemExit(f'Release build failed; private build log: {log}') diff --git a/crates/kernel/README.md b/crates/kernel/README.md index 757b4ef02..06a3b4127 100644 --- a/crates/kernel/README.md +++ b/crates/kernel/README.md @@ -33,8 +33,10 @@ How it starts, per platform (`src/launch.rs`): - **Android**: `/liboctos.so serve --stdio`, `HOME=` (the octos home), with AppCard's environment - (`OCTOS_SKILLS_PATH`, `OCTOS_OMIT_WORKSPACE_HINT`, `RUST_LOG`, the - `makepad.OCTOS_PROXY` proxy) and the kernel config's memory budget. The APK + (`OCTOS_SKILLS_PATH`, `OCTOS_OMIT_WORKSPACE_HINT`, `RUST_LOG`, and in a + development build only the launch extra `makepad.OCTOS_PROXY` as the proxy: + feature `dev-mode` or a debug build, since any app can start Home with + extras) and the kernel config's memory budget. The APK must bundle the kernel: `MAKEPAD_ANDROID_EXTRA_LIBS=liboctos.so=` (the shells' build scripts do it). - **OpenHarmony**: the canonical core in-process, diff --git a/crates/kernel/README.zh-CN.md b/crates/kernel/README.zh-CN.md index a35ca843a..f405a189e 100644 --- a/crates/kernel/README.zh-CN.md +++ b/crates/kernel/README.zh-CN.md @@ -31,7 +31,8 @@ Shell(`phone/` 中的 Home、`desktop/` 中的桌面)拥有它;**AI provid - **Android**:`/liboctos.so serve --stdio`,`HOME=` (octos home),使用 AppCard 的环境变量(`OCTOS_SKILLS_PATH`、 - `OCTOS_OMIT_WORKSPACE_HINT`、`RUST_LOG`、`makepad.OCTOS_PROXY` 代理)以及内核配置的 + `OCTOS_OMIT_WORKSPACE_HINT`、`RUST_LOG`,以及仅在开发版中作为代理的启动 extra + `makepad.OCTOS_PROXY`:feature `dev-mode` 或调试构建,因为任何应用都能带着 extra 启动 Home)以及内核配置的 内存预算。APK 必须打包内核:`MAKEPAD_ANDROID_EXTRA_LIBS=liboctos.so=` (Shell 的构建脚本会这样做)。 - **OpenHarmony**:在进程内运行标准内核 `octos_cli::embedded::serve_io(, ..)`, diff --git a/desktop/README.md b/desktop/README.md index d91a98ed1..42cedb3b4 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -124,7 +124,7 @@ App Hub's modules have no process form and always open in-process. | `OCTOSENSE_APP_DATA` | Where App Hub keeps installed apps (default `apps/` in the platform data directory). | | `OCTOSENSE_HUB`, `OCTOSENSE_HUB_ANCHOR` | App Hub catalog origin (path or URL) and trust anchor; default is the App Hub repository's `main`. | | `OCTOSENSE_SYSTEM_APPS` | The system-app selection file; the root `.cargo/config.toml` sets it to `desktop/system-apps.json`. | -| `MAKEPAD_APP_CONFIG='{"mail_demo":true}'` | Serve Mail's demo mailbox (see [Demos](#demos)). | +| `MAKEPAD_APP_CONFIG='{"mail_demo":true}'` | Serve Mail's demo mailbox (see [Demos](#demos)); development builds only (debug, or `--features dev-mode`), like all of `MAKEPAD_APP_CONFIG` ([`launch_input.rs`](../crates/shell/src/launch_input.rs)). | | `OCTOSENSE_MAIL_VAULT=file` | Keep Mail passwords in a 0600 file instead of the macOS keychain. | | `OCTOSENSE_LLM_VAULT=file` | Keep AI providers' keys in the owner-only octos profile instead of the macOS keychain. | | `OCTOS_APP_CORE_BIN`, `OCTOS_APP_CORE_DIR` | The octos kernel binary the shell's kernel service runs, unchecked (unset: the packaged `octos-kernel`, see [Build and run](#build-and-run)) and its core dir (default `~/octos-home/.octos`; the AI providers profile is `

/profiles/_main.json`). | @@ -363,10 +363,10 @@ A card that names no theme takes the shell’s light or dark palette. Toasts and ### Mail without an account ```sh -MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --release -p octosense +MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --release -p octosense --features dev-mode ``` -Open **Mail**, sign in on the host sheet with any address and password `demo`. The demo serves sample messages from a file vault: no network, no keychain. +Open **Mail**, sign in on the host sheet with any address and password `demo`. The demo serves sample messages from a file vault: no network, no keychain. Only a development build reads `MAKEPAD_APP_CONFIG`, hence `--features dev-mode` (**unverified** since that change; a debug `cargo run` works the same way). With a real account on an unsigned development build, macOS asks for keychain access again after every rebuild. For development, keep passwords in a file instead: diff --git a/desktop/README.zh-CN.md b/desktop/README.zh-CN.md index 7a64c2479..6afe92b0a 100644 --- a/desktop/README.zh-CN.md +++ b/desktop/README.zh-CN.md @@ -124,7 +124,7 @@ App Hub 的模块没有进程形态,总是在进程内打开。 | `OCTOSENSE_APP_DATA` | App Hub 保存已安装应用的位置(默认为平台数据目录下的 `apps/`)。 | | `OCTOSENSE_HUB`、`OCTOSENSE_HUB_ANCHOR` | App Hub 目录来源(路径或 URL)和信任锚;默认是 App Hub 仓库的 `main`。 | | `OCTOSENSE_SYSTEM_APPS` | 系统应用选择文件;根目录的 `.cargo/config.toml` 把它设为 `desktop/system-apps.json`。 | -| `MAKEPAD_APP_CONFIG='{"mail_demo":true}'` | 提供邮件的演示邮箱(见[演示](#演示))。 | +| `MAKEPAD_APP_CONFIG='{"mail_demo":true}'` | 提供邮件的演示邮箱(见[演示](#演示));与整个 `MAKEPAD_APP_CONFIG` 一样仅限开发版(调试构建或 `--features dev-mode`,见 [`launch_input.rs`](../crates/shell/src/launch_input.rs))。 | | `OCTOSENSE_MAIL_VAULT=file` | 把邮件密码保存在权限为 0600 的文件中,而不是 macOS 钥匙串。 | | `OCTOSENSE_LLM_VAULT=file` | 把 AI 提供商的密钥保存在仅所有者可读的 octos profile 中,而不是 macOS 钥匙串。 | | `OCTOS_APP_CORE_BIN`、`OCTOS_APP_CORE_DIR` | Shell 内核服务运行的 octos 内核二进制,不做检查(未设置:使用随附的 `octos-kernel`,见[构建与运行](#构建与运行))及其 core 目录(默认 `~/octos-home/.octos`;AI 提供商的 profile 为 `/profiles/_main.json`)。 | @@ -348,10 +348,10 @@ cargo run --release -p octosense --features app-appcard -- --module appcard ### 无账户试用邮件 ```sh -MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --release -p octosense +MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --release -p octosense --features dev-mode ``` -打开 **Mail**,在宿主面板上用任意地址和密码 `demo` 登录。演示模式从文件保险库提供示例邮件:不联网,不用钥匙串。 +打开 **Mail**,在宿主面板上用任意地址和密码 `demo` 登录。演示模式从文件保险库提供示例邮件:不联网,不用钥匙串。只有开发版读取 `MAKEPAD_APP_CONFIG`,所以要加 `--features dev-mode`(这一改动后**未验证**;调试构建的 `cargo run` 效果相同)。 在未签名的开发构建上使用真实账户时,每次重新构建后 macOS 都会再次请求钥匙串访问权限。开发时可以改用文件保存密码: diff --git a/desktop/docs/android-appcard-build.md b/desktop/docs/android-appcard-build.md index 0d277c2b1..1db7c78c5 100644 --- a/desktop/docs/android-appcard-build.md +++ b/desktop/docs/android-appcard-build.md @@ -117,7 +117,15 @@ developer's own `octos serve` is never touched. ## Provisioning the LLM key -The quoting-safe way is `scripts/provision-appcard-llm.sh `: it builds the JSON, ships it through both shells intact, restarts OctoSense with the extra, and waits for the app to log `provisioned LLM`. GLM keys come in two families: `zhipu` for a bigmodel.cn key (OpenAI-style endpoint) and `zai` for a z.ai key (Anthropic-style endpoint); using the wrong one yields HTTP 401 even with a valid key. +In a release build, providers come from **AI providers** (the desktop's QR +code and its PIN, or typed on the host's own sheet). Any app on the phone can +start Home with intent extras, so only a development build that links AppCard +(`--features app-appcard,dev-mode`), installed as its own test package (see +[phone/README.md](../../phone/README.md#build-and-run)), reads the launch-time +provisioning below; a release build ignores it (**unverified** on a device +since that change). + +The quoting-safe way is `PACKAGE=dev.makepad.octosense. scripts/provision-appcard-llm.sh `: it builds the JSON, ships it through both shells intact, restarts that test package with the extra, and waits for the app to log `provisioned LLM`. GLM keys come in two families: `zhipu` for a bigmodel.cn key (OpenAI-style endpoint) and `zai` for a z.ai key (Anthropic-style endpoint); using the wrong one yields HTTP 401 even with a valid key. The kernel needs an LLM profile before a request can run. The buildtool @@ -126,7 +134,7 @@ environment, and the app reads `MAKEPAD_PROVISION_CONFIG` on startup and writes the profile into the kernel's config: ```sh -"$ADB" shell "am start -S -n dev.makepad.octosense/.MakepadApp \ +"$ADB" shell "am start -S -n dev.makepad.octosense./.MakepadApp \ --es makepad.PROVISION_CONFIG '{\"llm_family\":\"zai\",\"llm_model\":\"glm-5.2\",\"llm_key\":\"\"}'" "$ADB" logcat -d -s Makepad | grep -iE 'provision|turn/start|turn FAILED' ``` diff --git a/desktop/docs/code-walkthrough.md b/desktop/docs/code-walkthrough.md index 9bacd7ce3..a1ceffb0a 100644 --- a/desktop/docs/code-walkthrough.md +++ b/desktop/docs/code-walkthrough.md @@ -105,10 +105,13 @@ require the full shell. For example, run Mail's demo service from the OctoSense root: ```sh -MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --locked --release -p octosense +MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --locked --release -p octosense --features dev-mode ``` -The demo account uses password `demo`; sends stay in the demo. Develop new store +The demo account uses password `demo`; sends stay in the demo. Only a +development build reads `MAKEPAD_APP_CONFIG` (`crates/shell/src/launch_input.rs`: +on a phone it arrives as an intent extra any app can set), hence +`--features dev-mode` (**unverified** since that change). Develop new store apps in OctoSense App Flow (formerly Design Flow) and use App Hub's admission/publishing flow. The [local-catalog recipe](../README.md#try-your-own-app-before-it-is-published) tests installation into the shell before publication. diff --git a/desktop/docs/code-walkthrough.zh-CN.md b/desktop/docs/code-walkthrough.zh-CN.md index 2054bb91b..e66bcd10e 100644 --- a/desktop/docs/code-walkthrough.zh-CN.md +++ b/desktop/docs/code-walkthrough.zh-CN.md @@ -89,10 +89,10 @@ cargo run --locked --release -p octosense-card-host --bin card-host -- --bundle 例如,在 OctoSense 根目录运行 Mail 的演示服务: ```sh -MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --locked --release -p octosense +MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --locked --release -p octosense --features dev-mode ``` -演示账户密码为 `demo`,发送留在演示环境。新商店应用在 OctoSense App Flow(原 Design Flow)中开发,使用 App Hub 的准入/发布流程。[本地签名目录配方](../README.zh-CN.md#发布前试用自己的应用)用于在发布前测试应用能否安装到 Shell。 +演示账户密码为 `demo`,发送留在演示环境。只有开发版读取 `MAKEPAD_APP_CONFIG`(`crates/shell/src/launch_input.rs`:在手机上它以任何应用都能设置的 intent extra 形式到达),所以要加 `--features dev-mode`(这一改动后**未验证**)。新商店应用在 OctoSense App Flow(原 Design Flow)中开发,使用 App Hub 的准入/发布流程。[本地签名目录配方](../README.zh-CN.md#发布前试用自己的应用)用于在发布前测试应用能否安装到 Shell。 [`desktop/system-apps.json`](../system-apps.json) 和 [`phone/system-apps.json`](../../phone/system-apps.json) 选择 `apps/` 中的 bundle。 diff --git a/desktop/scripts/provision-appcard-llm.sh b/desktop/scripts/provision-appcard-llm.sh index f50e28f84..36fe87dbe 100755 --- a/desktop/scripts/provision-appcard-llm.sh +++ b/desktop/scripts/provision-appcard-llm.sh @@ -1,11 +1,23 @@ #!/usr/bin/env bash -# Provision the LLM for AppCard inside OctoSense on the connected Android phone. -# provision-appcard-llm.sh +# Provision the LLM for AppCard inside a development test build of OctoSense +# Home on the connected Android phone. +# PACKAGE=dev.makepad.octosense. provision-appcard-llm.sh # family: zhipu (bigmodel.cn key) | zai (z.ai key) | deepseek | openai | anthropic | ... # The key goes only to the phone (launch-intent extra); it is not logged here. +# Any app can start Home with extras, so a release Home ignores launch-time +# provisioning: PACKAGE must be a development build that links AppCard +# (`--features app-appcard,dev-mode`), installed as its own test package. A +# release build takes providers from AI providers (QR code and PIN) instead. set -euo pipefail -[ $# -eq 3 ] || { echo "usage: $0 " >&2; exit 2; } +[ $# -eq 3 ] || { echo "usage: PACKAGE=dev.makepad.octosense. $0 " >&2; exit 2; } family=$1 model=$2 key=$3 +package=${PACKAGE:-} +case "$package" in + *[!a-z0-9_.]*|dev.makepad.octosense.bridge|dev.makepad.octosense.quickstep|dev.makepad.octosense.agent) package= ;; + dev.makepad.octosense.?*) ;; + *) package= ;; +esac +[ -n "$package" ] || { echo "set PACKAGE to the development test package (dev.makepad.octosense.), never the installed Home" >&2; exit 2; } ADB=${ADB:-$(command -v adb || true)} [ -n "$ADB" ] || { echo "adb not found; set ADB=/path/to/adb (cargo-makepad installs one under tools/cargo_makepad/android_*/platform-tools)" >&2; exit 1; } [ -x "$ADB" ] || { echo "adb not found; set ADB=/path/to/adb" >&2; exit 1; } @@ -14,8 +26,8 @@ ADB=${ADB:-$(command -v adb || true)} json=$(python3 -c 'import json,sys; print(json.dumps({"llm_family":sys.argv[1],"llm_model":sys.argv[2],"llm_key":sys.argv[3]}))' "$family" "$model" "$key") esc=${json//\"/\\\"} "$ADB" logcat -c >/dev/null 2>&1 || true -"$ADB" shell "am start -S -n dev.makepad.octosense/.MakepadApp --es makepad.PROVISION_CONFIG '$esc'" >/dev/null -echo "launched OctoSense with provisioning extra; open the AppCard tile now (waiting up to 40 s for the app to record it)…" +"$ADB" shell "am start -S -n $package/.MakepadApp --es makepad.PROVISION_CONFIG '$esc'" >/dev/null +echo "launched $package with provisioning extra; open the AppCard tile now (waiting up to 40 s for the app to record it)…" for i in $(seq 1 40); do line=$("$ADB" logcat -d -s Makepad 2>/dev/null | grep -E 'provisioned LLM|LLM provisioning failed' | tail -1 || true) [ -n "$line" ] && break; sleep 1 diff --git a/phone/README.md b/phone/README.md index f2351aad8..8840a07e9 100644 --- a/phone/README.md +++ b/phone/README.md @@ -56,7 +56,7 @@ cargo run --release -p octosense-home --features mobile-only | `-- --module ` | Host a linked module in-process instead of as a child process | | `-- --test-action ` | Fire a shell action at startup (see [Run on a desktop](#run-on-a-desktop)) | | `MAKEPAD_WM_TEST_APP=[:]` | Launch an app (count times) once the shell is up | -| `MAKEPAD_APP_CONFIG='{"mail_demo":true}'` | Serve Mail from a demo mailbox (password `demo`) | +| `MAKEPAD_APP_CONFIG='{"mail_demo":true}'` | Serve Mail from a demo mailbox (password `demo`); development builds only (debug, or `--features dev-mode`) | | `OCTOSENSE_HOME=` | Keep state somewhere other than `~/.octosense` | **Android APK.** `rom/scripts/build-home.sh` (a wrapper for @@ -112,6 +112,33 @@ this setting and silently omit Android Back handling and other platform integration in a custom package. After installing a test build, open News or Photos and verify that an edge Back swipe returns to OctoSense Home. +**Launch-time test hooks.** Any app on the phone can start Home with intent +extras, so a release Home ignores the `makepad.*` test and provisioning +extras: the `test_actions` and demo settings in `makepad.APP_CONFIG`, +AppCard's `makepad.PROVISION_CONFIG` and seeding hooks, and the kernel's +`makepad.OCTOS_PROXY`. Only a development build (a debug build, or +`--features dev-mode`) reads them ([`launch_input.rs`](../crates/shell/src/launch_input.rs)). +`--dev-mode` (above) builds one. Give it a test `--package-name` and install +only its Home APK (**unverified** on a device): + +```sh +rom/scripts/build-home.sh --variant standalone --development --dev-mode \ + --package-name dev.makepad.octosense.devtest \ + --sdk /path/to/makepad-android \ + --android-sdk /path/to/android-sdk \ + --gradle-home /path/to/gradle-8.11.1 \ + --java-home /path/to/full-jdk \ + --packager .sources/makepad/target/release/cargo-makepad +adb install rom/out/home/standalone-dev-mode/dev.makepad.octosense.devtest/OctoSenseHome.apk +adb shell am start -n dev.makepad.octosense.devtest/.MakepadApp \ + --es makepad.APP_CONFIG '{"test_actions":["launch-mail"]}' +``` + +On OpenHarmony, `rom/scripts/build-home-ohos.py --dev-mode` does the same +with a separate `--bundle-id`; a release Home's entry ability passes no +launch parameters on. Desktop `--test-action` flags are command-line +arguments, which only the local user can pass, and work in every build. + **OpenHarmony:** `python3 rom/scripts/build-home-ohos.py --deveco-home ... --packager ... --signing-config ...` builds a normal OpenHarmony app with an existing DevEco signing profile @@ -284,11 +311,14 @@ never holds a socket or a password. For a demo mailbox (password `demo`): ```sh # desktop, from phone/ -MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --release -p octosense-home --features mobile-only -# phone -adb shell am start -n /.MakepadApp --es makepad.APP_CONFIG '{"mail_demo":true}' +MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --release -p octosense-home --features mobile-only,dev-mode +# phone: a development test package (see Launch-time test hooks) +adb shell am start -n /.MakepadApp --es makepad.APP_CONFIG '{"mail_demo":true}' ``` +A release build ignores `mail_demo` (both commands are **unverified** since +that change). + The earlier native News, Photos and Maps modules are deleted (native-apps ADR 0004 §1, [#113](https://github.com/OctoSense-org/OctoSense/pull/113)); none of the system apps has a native module any more. diff --git a/phone/README.zh-CN.md b/phone/README.zh-CN.md index 2f6173ea7..12a6dac15 100644 --- a/phone/README.zh-CN.md +++ b/phone/README.zh-CN.md @@ -31,7 +31,7 @@ cargo run --release -p octosense-home --features mobile-only | `-- --module ` | 在进程内托管已链接的模块,而不是作为子进程 | | `-- --test-action ` | 启动时触发一个 Shell 动作(见[在桌面电脑上运行](#在桌面电脑上运行)) | | `MAKEPAD_WM_TEST_APP=[:]` | Shell 启动后打开某个应用(可指定次数) | -| `MAKEPAD_APP_CONFIG='{"mail_demo":true}'` | 用演示邮箱提供 Mail(密码为 `demo`) | +| `MAKEPAD_APP_CONFIG='{"mail_demo":true}'` | 用演示邮箱提供 Mail(密码为 `demo`);仅限开发版(调试构建或 `--features dev-mode`) | | `OCTOSENSE_HOME=` | 把状态数据保存在 `~/.octosense` 以外的位置 | **Android APK。** `rom/scripts/build-home.sh`(`build-home.py` 的包装脚本)构建 Home APK 及其 System Bridge APK,一起签名,把 octos 内核作为 `liboctos.so` 打包进去,并写出 `OctoSenseHome.apk`、`OctoSenseBridge.apk` 和一份 `build.json` 回执。它从不安装或刷机。在仓库根目录构建一对使用 Makepad 开发密钥签名的独立开发版: @@ -61,6 +61,23 @@ Home 默认使用 `dev.makepad.octosense`。要在已安装的应用旁构建测 平台集成功能。安装测试版后,打开 News 或 Photos,检查从屏幕边缘向内滑动返回 是否回到 OctoSense Home。 +**启动时的测试钩子。** 手机上的任何应用都能带着 intent extra 启动 Home,因此发布版 Home 会忽略 `makepad.*` 测试和配置 extra:`makepad.APP_CONFIG` 中的 `test_actions` 和演示设置、AppCard 的 `makepad.PROVISION_CONFIG` 和预置卡片钩子,以及内核的 `makepad.OCTOS_PROXY`。只有开发版(调试构建,或使用 `--features dev-mode`)才读取它们([`launch_input.rs`](../crates/shell/src/launch_input.rs))。`--dev-mode`(见上文)构建开发版:请为它指定测试用的 `--package-name`,并且只安装其中的 Home APK(**未在设备上验证**): + +```sh +rom/scripts/build-home.sh --variant standalone --development --dev-mode \ + --package-name dev.makepad.octosense.devtest \ + --sdk /path/to/makepad-android \ + --android-sdk /path/to/android-sdk \ + --gradle-home /path/to/gradle-8.11.1 \ + --java-home /path/to/full-jdk \ + --packager .sources/makepad/target/release/cargo-makepad +adb install rom/out/home/standalone-dev-mode/dev.makepad.octosense.devtest/OctoSenseHome.apk +adb shell am start -n dev.makepad.octosense.devtest/.MakepadApp \ + --es makepad.APP_CONFIG '{"test_actions":["launch-mail"]}' +``` + +在 OpenHarmony 上,`rom/scripts/build-home-ohos.py --dev-mode` 以另一个 `--bundle-id` 做同样的事;发布版 Home 的入口 ability 不转发任何启动参数。桌面上的 `--test-action` 是命令行参数,只有本机用户能传入,所有构建都支持。 + **OpenHarmony:** `python3 rom/scripts/build-home-ohos.py --deveco-home ... --packager ... --signing-config ...` 使用现有的 DevEco 签名配置构建普通的 OpenHarmony 应用([rom/docs/home-build.md(英文)](../rom/docs/home-build.md#openharmony-home))。**iOS 模拟器:** 在 `phone/` 中运行 `../.sources/makepad/target/release/cargo-makepad makepad apple ios --org=dev.makepad --app=octosense run-sim -p octosense-home --features mobile-only`。两者都不在 CI 中构建。 ## 应用图标 @@ -191,11 +208,13 @@ Mail 通过 `mail` 宿主服务([`apps/mail/host-service`](../apps/mail/host-s ```sh # desktop, from phone/ -MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --release -p octosense-home --features mobile-only -# phone -adb shell am start -n /.MakepadApp --es makepad.APP_CONFIG '{"mail_demo":true}' +MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --release -p octosense-home --features mobile-only,dev-mode +# 手机:开发版测试包(见“启动时的测试钩子”) +adb shell am start -n /.MakepadApp --es makepad.APP_CONFIG '{"mail_demo":true}' ``` +发布版会忽略 `mail_demo`(这一改动后两条命令均**未验证**)。 + 早期的原生 News、Photos 和 Maps 模块已删除(原生应用 ADR 0004 §1,[#113](https://github.com/OctoSense-org/OctoSense/pull/113));所有系统应用都不再有原生模块。 ### octos 内核 diff --git a/phone/docs/android-appcard-build.md b/phone/docs/android-appcard-build.md index 5a9aae224..998ebdb72 100644 --- a/phone/docs/android-appcard-build.md +++ b/phone/docs/android-appcard-build.md @@ -174,7 +174,15 @@ declined that finger by the time the tile sees it. ## Provisioning the LLM key -The quoting-safe way is `scripts/provision-appcard-llm.sh `: it builds the JSON, ships it through both shells intact, restarts OctoSense with the extra, and waits for the app to log `provisioned LLM`. GLM keys come in two families: `zhipu` for a bigmodel.cn key (OpenAI-style endpoint) and `zai` for a z.ai key (Anthropic-style endpoint); using the wrong one yields HTTP 401 even with a valid key. +In a release build, providers come from **AI providers** (the desktop's QR +code and its PIN, or typed on the host's own sheet). Any app on the phone can +start Home with intent extras, so only a development build that links AppCard +(`--features app-appcard,dev-mode`), installed as its own test package (see +[phone/README.md](../../phone/README.md#build-and-run)), reads the launch-time +provisioning below; a release build ignores it (**unverified** on a device +since that change). + +The quoting-safe way is `PACKAGE=dev.makepad.octosense. scripts/provision-appcard-llm.sh `: it builds the JSON, ships it through both shells intact, restarts that test package with the extra, and waits for the app to log `provisioned LLM`. GLM keys come in two families: `zhipu` for a bigmodel.cn key (OpenAI-style endpoint) and `zai` for a z.ai key (Anthropic-style endpoint); using the wrong one yields HTTP 401 even with a valid key. The kernel needs an LLM profile before a request can run. The buildtool @@ -183,7 +191,7 @@ environment, and the app reads `MAKEPAD_PROVISION_CONFIG` on startup and writes the profile into the kernel's config: ```sh -"$ADB" shell "am start -S -n dev.makepad.octosense/.MakepadApp \ +"$ADB" shell "am start -S -n dev.makepad.octosense./.MakepadApp \ --es makepad.PROVISION_CONFIG '{\"llm_family\":\"zai\",\"llm_model\":\"glm-5.2\",\"llm_key\":\"\"}'" "$ADB" logcat -d -s Makepad | grep -iE 'provision|turn/start|turn FAILED' ``` diff --git a/phone/docs/android/perf-home-swipes-oneplus-6.md b/phone/docs/android/perf-home-swipes-oneplus-6.md index 9fc009839..7ce5971dd 100644 --- a/phone/docs/android/perf-home-swipes-oneplus-6.md +++ b/phone/docs/android/perf-home-swipes-oneplus-6.md @@ -29,7 +29,10 @@ scripts can still incur their loading cost. ## Measurement method Use Makepad's `Cx::perf_monitor` and `PerfGraph` for diagnosis, enabled by -`makepad.APP_CONFIG='{"test_actions":["perf:on"]}'`. The retained diagnostic +`makepad.APP_CONFIG='{"test_actions":["perf:on"]}'`. Launch-time test actions +run only in a development build (`--features dev-mode`) installed as a +separate test package (`rom/scripts/build-home.py --dev-mode`); a release Home +ignores them. The retained diagnostic log separates `home`, `event`, `draw`, and `wait` costs. Whole-session monitor FPS includes deliberate idle periods and is not the swipe FPS. diff --git a/phone/scripts/provision-appcard-llm.sh b/phone/scripts/provision-appcard-llm.sh index f50e28f84..36fe87dbe 100755 --- a/phone/scripts/provision-appcard-llm.sh +++ b/phone/scripts/provision-appcard-llm.sh @@ -1,11 +1,23 @@ #!/usr/bin/env bash -# Provision the LLM for AppCard inside OctoSense on the connected Android phone. -# provision-appcard-llm.sh +# Provision the LLM for AppCard inside a development test build of OctoSense +# Home on the connected Android phone. +# PACKAGE=dev.makepad.octosense. provision-appcard-llm.sh # family: zhipu (bigmodel.cn key) | zai (z.ai key) | deepseek | openai | anthropic | ... # The key goes only to the phone (launch-intent extra); it is not logged here. +# Any app can start Home with extras, so a release Home ignores launch-time +# provisioning: PACKAGE must be a development build that links AppCard +# (`--features app-appcard,dev-mode`), installed as its own test package. A +# release build takes providers from AI providers (QR code and PIN) instead. set -euo pipefail -[ $# -eq 3 ] || { echo "usage: $0 " >&2; exit 2; } +[ $# -eq 3 ] || { echo "usage: PACKAGE=dev.makepad.octosense. $0 " >&2; exit 2; } family=$1 model=$2 key=$3 +package=${PACKAGE:-} +case "$package" in + *[!a-z0-9_.]*|dev.makepad.octosense.bridge|dev.makepad.octosense.quickstep|dev.makepad.octosense.agent) package= ;; + dev.makepad.octosense.?*) ;; + *) package= ;; +esac +[ -n "$package" ] || { echo "set PACKAGE to the development test package (dev.makepad.octosense.), never the installed Home" >&2; exit 2; } ADB=${ADB:-$(command -v adb || true)} [ -n "$ADB" ] || { echo "adb not found; set ADB=/path/to/adb (cargo-makepad installs one under tools/cargo_makepad/android_*/platform-tools)" >&2; exit 1; } [ -x "$ADB" ] || { echo "adb not found; set ADB=/path/to/adb" >&2; exit 1; } @@ -14,8 +26,8 @@ ADB=${ADB:-$(command -v adb || true)} json=$(python3 -c 'import json,sys; print(json.dumps({"llm_family":sys.argv[1],"llm_model":sys.argv[2],"llm_key":sys.argv[3]}))' "$family" "$model" "$key") esc=${json//\"/\\\"} "$ADB" logcat -c >/dev/null 2>&1 || true -"$ADB" shell "am start -S -n dev.makepad.octosense/.MakepadApp --es makepad.PROVISION_CONFIG '$esc'" >/dev/null -echo "launched OctoSense with provisioning extra; open the AppCard tile now (waiting up to 40 s for the app to record it)…" +"$ADB" shell "am start -S -n $package/.MakepadApp --es makepad.PROVISION_CONFIG '$esc'" >/dev/null +echo "launched $package with provisioning extra; open the AppCard tile now (waiting up to 40 s for the app to record it)…" for i in $(seq 1 40); do line=$("$ADB" logcat -d -s Makepad 2>/dev/null | grep -E 'provisioned LLM|LLM provisioning failed' | tail -1 || true) [ -n "$line" ] && break; sleep 1 diff --git a/rom/docs/home-build.md b/rom/docs/home-build.md index 51c6c775f..e918bc624 100644 --- a/rom/docs/home-build.md +++ b/rom/docs/home-build.md @@ -88,6 +88,24 @@ For a standalone release, replace `--development` with `--sign-key` and `--sign-cert` pointing to the existing application signer outside the checkout. Do not use the ROM platform key for ordinary distribution. +Launch-time test hooks (the `test_actions` and demo settings in the +`makepad.APP_CONFIG` intent extra, AppCard's provisioning and seeding extras, +the kernel's `makepad.OCTOS_PROXY`) work only in a development build: any app +on the phone can start Home with extras, so a release Home ignores them +(`crates/shell/src/launch_input.rs`). `--dev-mode` compiles that feature (see +"Developer options and a separate test Home" below for its rules); give it a +test `--package-name` so it installs beside the real Home. The plan is unit +tested; the build and a device run are **unverified**: + +```sh +rom/scripts/build-home.sh --variant standalone --development --dev-mode \ + --package-name dev.makepad.octosense.devtest \ + --sdk /path/to/makepad-android \ + --android-sdk /path/to/android-sdk \ + --gradle-home /path/to/gradle-8.11.1 \ + --java-home /path/to/full-jdk +``` + ROM Home and Bridge, signed with the existing ROM platform identity: ```sh @@ -261,5 +279,11 @@ pill; those gestures remain available to the host OS. Home paging and the central pull for the app library still work inside the content area. Artifacts and source/hash receipts go to `out/home/ohos/`. The optional `--remote-port` enables app-owned loopback inspection for validation and writes -to `out/home/ohos-validation/`; omit it for the normal package. Existing warnings +to `out/home/ohos-validation/`; omit it for the normal package. +The staged entry ability passes no launch parameters on: any app can start the +exported ability with parameters, and Makepad would turn each `makepad.*` one +into environment variables. `--dev-mode` builds a development HAP instead +(`--features dev-mode`, launch parameters passed on for test hooks) and writes +to a directory ending in `-dev`; it needs a `--bundle-id` other than +`dev.makepad.octosense` (**unverified**: no HAP was built). Existing warnings remain. Android device acceptance still requires an ordinary Android phone. diff --git a/tools/connected-e2e/ANDROID-BACKEND.md b/tools/connected-e2e/ANDROID-BACKEND.md index 5d9ffc257..edb827874 100644 --- a/tools/connected-e2e/ANDROID-BACKEND.md +++ b/tools/connected-e2e/ANDROID-BACKEND.md @@ -23,7 +23,7 @@ python3 tools/connected-e2e/android_backend_lab.py deploy \ --registrations "$LAB/prepared/backend-registrations.json" ``` -`JAVA_HOME` must point to the existing JDK for `apksigner`. For multiple attached devices, supply the assigned `--serial` to deploy; never publish it. Deployment refuses an already installed package. It transfers only the fresh signed fixture profile, configures one ADB loopback reverse, and passes the one-shot `makepad.APP_CONFIG` Intent extra. Android intentionally ignores a startup environment-only app config. A fresh suffix can be used consistently at APK build, packaging and deployment. +`JAVA_HOME` must point to the existing JDK for `apksigner`. For multiple attached devices, supply the assigned `--serial` to deploy; never publish it. Deployment refuses an already installed package. It transfers only the fresh signed fixture profile, configures one ADB loopback reverse, and passes the one-shot `makepad.APP_CONFIG` Intent extra. Android intentionally ignores a startup environment-only app config. Its `test_actions` run only because step 1 builds a development build (`--features dev-mode`): a release Home ignores launch-time test actions, since any app can start it with extras (`crates/shell/src/launch_input.rs`). A fresh suffix can be used consistently at APK build, packaging and deployment. The tracked `backend-login/glance.splash` is embedded by the preparation helper. Its publication is subject to the normal app's Glance grant. This source is test artwork and fixture UI, not a production listing. diff --git a/tools/connected-e2e/ANDROID-BACKEND.zh-CN.md b/tools/connected-e2e/ANDROID-BACKEND.zh-CN.md index 4a1924156..dcdc9c40c 100644 --- a/tools/connected-e2e/ANDROID-BACKEND.zh-CN.md +++ b/tools/connected-e2e/ANDROID-BACKEND.zh-CN.md @@ -10,7 +10,7 @@ 2. 在单独终端运行 `python3 tools/backend-login-fixture.py --directory "$LAB/server"` 并保持运行。metadata 只有本地公开端点;不要整体发布运行目录。 3. 按英文页的三个命令运行 `android_backend_lab.py prepare`、`android_backend.py` 和 `android_backend_lab.py deploy`。准备过程通过普通签名目录/安装校验,包含两个固定测试应用 ID。设置已有 JDK 的 `JAVA_HOME` 供 apksigner 使用;多设备时只选择指定的 `--serial`,不得公开设备编号。 -部署拒绝已存在的包,只传输新建的已签名测试目录,建立一个 ADB loopback reverse,并通过一次性 `makepad.APP_CONFIG` Intent extra 启动。Android 会清除只有环境变量的 app config,因此不能省略 Intent。需要新后缀时,APK 构建、封装和部署必须使用同一包名。准备脚本会嵌入已跟踪的 `backend-login/glance.splash`,发布仍经过普通应用 Glance 权限校验。测试插图和界面不是生产应用素材。 +部署拒绝已存在的包,只传输新建的已签名测试目录,建立一个 ADB loopback reverse,并通过一次性 `makepad.APP_CONFIG` Intent extra 启动。Android 会清除只有环境变量的 app config,因此不能省略 Intent。其中的 `test_actions` 之所以会执行,是因为第 1 步构建的是开发版(`--features dev-mode`):任何应用都能带着 extra 启动 Home,所以发布版 Home 会忽略启动时的测试动作(`crates/shell/src/launch_input.rs`)。需要新后缀时,APK 构建、封装和部署必须使用同一包名。准备脚本会嵌入已跟踪的 `backend-login/glance.splash`,发布仍经过普通应用 Glance 权限校验。测试插图和界面不是生产应用素材。 实际手机操作,不重放结果未知的输入: diff --git a/tools/connected-e2e/android-notes.md b/tools/connected-e2e/android-notes.md index 5b59f4cbe..ee3e786a0 100644 --- a/tools/connected-e2e/android-notes.md +++ b/tools/connected-e2e/android-notes.md @@ -73,6 +73,10 @@ the test package's `files/apps` using `run-as`, and launch `.MakepadApp` with th {"test_actions":["launch-hub:org.octosense.samples.githubnotes"]} ``` +The test action runs because the package above is a development build +(`--features dev-mode`); a release Home ignores launch-time test actions, +since any app can start it with extras (`crates/shell/src/launch_input.rs`). + Never install over the regular Home package or transfer an existing personal profile. The pinned Makepad remote implementation is a no-op on Android; `MAKEPAD_REMOTE` and ADB port forwarding do not enable it. Phone verification diff --git a/tools/connected-e2e/android-notes.zh-CN.md b/tools/connected-e2e/android-notes.zh-CN.md index d01a7d57a..763407d27 100644 --- a/tools/connected-e2e/android-notes.zh-CN.md +++ b/tools/connected-e2e/android-notes.zh-CN.md @@ -64,6 +64,10 @@ python3 tools/connected-e2e/android_notes.py \ {"test_actions":["launch-hub:org.octosense.samples.githubnotes"]} ``` +测试动作之所以会执行,是因为上面的测试包是开发版(`--features dev-mode`);任何应用 +都能带着 extra 启动 Home,所以发布版 Home 会忽略启动时的测试动作 +(`crates/shell/src/launch_input.rs`)。 + 不能覆盖日常 Home 或传输个人配置。固定版本的 Makepad remote 在 Android 是空实现, 设置 `MAKEPAD_REMOTE` 或 ADB 端口转发不会启用它。手机使用 ADB 输入和原始设备截图; Mac 浸泡使用 Makepad instrument,分别记录证据。 From 291aef5983047144f8292a963458de40c187c573 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Sun, 11 Oct 2026 07:16:32 -0700 Subject: [PATCH 3/4] Read launch input on desktop; gate it only on phones MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A desktop's environment is the local user's, like a command-line flag, so `MAKEPAD_APP_CONFIG` (test actions, Mail's demo mailbox, module-open settings) and AppCard's `MAKEPAD_*` hooks are read in every desktop build. The gate applies only on Android and OpenHarmony, where a launch Intent extra or launch parameter can come from another app: there a release build ignores them and only a development build reads them. - crates/shell/src/launch_input.rs: `honoured` keys on `cfg(native_mobile)`; tests say a phone release build ignores it while a desktop build (any profile) and a phone development build read it. - apps/appcard: `launch_set_in(mobile, development, ...)` — a desktop reads the hooks, a phone only in a development build. - Docs restored to plain desktop `cargo run` commands; the phone and OpenHarmony guidance keeps the development-build note. AGENTS.md states the desktop/phone split. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 2 +- apps/AGENTS.md | 6 +- apps/README.md | 13 +-- apps/README.zh-CN.md | 8 +- apps/appcard/app/app/src/lib.rs | 46 +++++----- apps/appcard/tools/octos-macos.py | 7 +- crates/shell/src/apps.rs | 10 +-- crates/shell/src/launch_input.rs | 118 ++++++++++++++----------- desktop/README.md | 6 +- desktop/README.zh-CN.md | 6 +- desktop/docs/code-walkthrough.md | 7 +- desktop/docs/code-walkthrough.zh-CN.md | 4 +- phone/README.md | 9 +- phone/README.zh-CN.md | 6 +- 14 files changed, 133 insertions(+), 115 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 50084b38e..812a8e5c8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,7 +56,7 @@ On a machine that already has clones of Makepad, OctoScript or Octoscript-Makepa 4. **One kernel per process.** The kernel service starts octos on the first consumer and shares it; consumers never spawn their own. Apps reach the assistant only through `crates/app-peers`, never through the raw kernel protocol. 5. **System apps ship inside the shells.** Their ids start with `os.`, they are packed by digest from each packaging's `system-apps.json`, and they are not released separately or through the store. 6. **Test UI without taking over the screen.** Run hosted apps and shells with hidden windows and drive them over the local control surface: `MAKEPAD_HIDE_WINDOWS=1 MAKEPAD_REMOTE=` (routes under `/help`; end with `/quit`), or `makepad_test`. Use `--test-action`, `MAKEPAD_WM_TEST_APP` and `--module` to reach a state without clicking. -7. **Devices.** Use only a device you were given for the task. Install test builds under a separate package name; never replace the installed Home or flash an image unless asked. Anything not run on a device is **unverified**, and says so. Launch-time test hooks (the `test_actions` in the `makepad.APP_CONFIG` intent extra and the other `makepad.*` extras or OpenHarmony launch parameters) work only in a development build (`cfg(dev_mode)`: debug, or `--features dev-mode`; `rom/scripts/build-home.py --dev-mode` builds one as a test package): any app on a phone can start Home with extras, so a release build ignores them. Read such a hook only through `crates/shell/src/launch_input.rs` (AppCard: `launch_var`), never with a bare `std::env::var`. +7. **Devices.** Use only a device you were given for the task. Install test builds under a separate package name; never replace the installed Home or flash an image unless asked. Anything not run on a device is **unverified**, and says so. Launch-time test hooks (the `test_actions` in the `makepad.APP_CONFIG` intent extra and the other `makepad.*` extras or OpenHarmony launch parameters) are the local user's own setting on a desktop, like a command-line flag, and read in every build there; on a phone (Android, OpenHarmony) any app can start Home with extras, so a release build ignores them and only a development build reads them (`cfg(dev_mode)`: debug, or `--features dev-mode`; `rom/scripts/build-home.py --dev-mode` builds one as a test package). Read such a hook only through `crates/shell/src/launch_input.rs` (AppCard: `launch_var`), never with a bare `std::env::var`. 8. **Commit with a public identity.** Author and committer are your GitHub noreply address (for the maintainer, `ymote <151983+ymote@users.noreply.github.com>`) or another address you mean to publish; never a work or machine-local address. Check `git config user.email` before the first commit in a clone: a repository-local identity on a shared machine leaked a work address into this project's history once, and removing it took a history rewrite. 9. **Keep docs honest and bilingual.** Every command in a doc was run; anything not run is marked **unverified**. User-facing docs are `README.md` plus `README.zh-CN.md`, linked by the single switcher line under the title (`English | [简体中文](README.zh-CN.md)`), and change together. `AGENTS.md` files are English only. 10. **Keep signing keys, keystores, tokens and personal paths out of the repository.** `rom/tests/test_no_local_paths.py` checks for local paths. diff --git a/apps/AGENTS.md b/apps/AGENTS.md index 8fcd72ed9..6dc737d23 100644 --- a/apps/AGENTS.md +++ b/apps/AGENTS.md @@ -29,9 +29,9 @@ app's own agent and `tools.json`, the system toolbox, `glance.publish` and --system` (add `MAKEPAD_REMOTE=` to drive it over HTTP; Photos also takes `--static photos=`). `--system` lets an `os.*` id and an empty digest through, as the shell does. These flags are on App Hub `main`. - `card-host` registers no host services: run Mail in a development build - of a shell (debug, or `--features dev-mode`) with - `MAKEPAD_APP_CONFIG='{"mail_demo":true}'`; a release build ignores it. + `card-host` registers no host services: run Mail in a shell with + `MAKEPAD_APP_CONFIG='{"mail_demo":true}'` (a desktop reads it; on a phone it + needs a development build). - Validate on a phone through Home (`phone/`) built as a separate test package; never replace the device's installed Home. - Text on a page follows the host's appearance. On a phone, a page drawn by a diff --git a/apps/README.md b/apps/README.md index a3b47f8d7..7723a09ed 100644 --- a/apps/README.md +++ b/apps/README.md @@ -285,7 +285,7 @@ standalone launcher and ROM image). Each packaging: [`crates/shell`](../crates/shell) (its `app-hub` feature; the photo engine's `octosense-photo-service` comes with the desktop-only `craft-engines`, weighed per engine in [ADR 0013](../docs/adr/0013-craft-engines-as-pinned-services.md)), and registers them at startup (`crates/shell/src/apps.rs`, `register_host_services`): Mail with `register()` for real accounts, or `register_demo()` - when the shell's app config has `mail_demo: true` (a development build only). Mail, News and, with the photo + when the shell's app config has `mail_demo: true` (a desktop reads it; a phone only in a development build). Mail, News and, with the photo engine, Photos install `on_notify` callbacks to the shell's common notice renderer; Calendar installs its card publisher. The shell registers `NoticeService` for remaining system-app namespaces; `llm` with the octos @@ -455,14 +455,15 @@ password `demo`, sample messages, sends that go nowhere): ```sh # the desktop, from the repository root -MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --release -p octosense --features dev-mode +MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --release -p octosense # Home in a phone-sized window, from phone/ -MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --release -p octosense-home --features mobile-only,dev-mode +MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --release -p octosense-home --features mobile-only ``` -The demo keeps its password in a file, so no keychain prompt appears. Only a -development build (debug, or `--features dev-mode`) reads `MAKEPAD_APP_CONFIG`; -both commands are **unverified** since that change. +The demo keeps its password in a file, so no keychain prompt appears. These are +desktop builds, so they read `MAKEPAD_APP_CONFIG` as a local command-line +setting; on a real phone the demo needs a development test build +(`rom/scripts/build-home.py --dev-mode`). **Quick Deck's dev fixture.** `card-host` has no `deck` service, and the shipped bundle has no fixture code. Quick Deck's UI tests use diff --git a/apps/README.zh-CN.md b/apps/README.zh-CN.md index 6b56426d1..b9b1ef556 100644 --- a/apps/README.zh-CN.md +++ b/apps/README.zh-CN.md @@ -222,7 +222,7 @@ WebView 页面、图库导出、邮件、新闻、Glance 或单次模型调用 path 依赖;photo 引擎的 `octosense-photo-service` 随仅限桌面的 `craft-engines` 引入, 按引擎权衡见 [ADR 0013](../docs/adr/0013-craft-engines-as-pinned-services.zh-CN.md)),并在启动时注册 (`crates/shell/src/apps.rs` 的 `register_host_services`):Mail 服务在真实账户下用 - `register()`,Shell 的应用配置中 `mail_demo: true` 时(仅限开发版)用 `register_demo()`。 + `register()`,Shell 的应用配置中 `mail_demo: true` 时(桌面端读取;手机端仅限开发版)用 `register_demo()`。 Mail、News 以及(带 photo 引擎时)Photos 安装 `on_notify` 回调,调用 Shell 共用通知渲染器;Calendar 安装卡片发布器。 Shell 为其余系统应用命名空间注册 `NoticeService`;`llm` 服务使用 octos 内核的 core 目录以及 Shell 的二维码扫描器和图片选择器(见 [`llm` 服务](#llm-服务))。App Hub 只在根目录 @@ -374,12 +374,12 @@ card-host --bundle /apps/photos/bundle --system --static photos=<原 ```sh # 桌面端,在仓库根目录 -MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --release -p octosense --features dev-mode +MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --release -p octosense # 手机尺寸窗口中的 Home,在 phone/ 中 -MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --release -p octosense-home --features mobile-only,dev-mode +MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --release -p octosense-home --features mobile-only ``` -演示邮箱的密码存放在文件中,因此不会弹出钥匙串提示。只有开发版(调试构建或 `--features dev-mode`)读取 `MAKEPAD_APP_CONFIG`;这一改动后两条命令均**未验证**。 +演示邮箱的密码存放在文件中,因此不会弹出钥匙串提示。这些都是桌面构建,会把 `MAKEPAD_APP_CONFIG` 当作本机命令行设置读取;在真实手机上,演示邮箱需要开发版测试构建(`rom/scripts/build-home.py --dev-mode`)。 **Quick Deck 的开发夹具。** `card-host` 没有 `deck` 服务,而随 Shell 发布的 bundle 中没有任何夹具代码。Quick Deck 的 UI 测试使用 bundle 之外的 diff --git a/apps/appcard/app/app/src/lib.rs b/apps/appcard/app/app/src/lib.rs index 043711ff9..7c0492f8a 100644 --- a/apps/appcard/app/app/src/lib.rs +++ b/apps/appcard/app/app/src/lib.rs @@ -35,28 +35,30 @@ use crate::backend::OctosUiAgent; /// Makepad copies the launch extras `makepad.` into `MAKEPAD_`, /// and OpenHarmony entry abilities pass `makepad.*` launch parameters the /// same way, so on a phone another app can set them by starting this one. -/// Only a development build reads them (`dev_mode`: debug assertions or -/// feature `dev-mode`, build.rs); a release build behaves as if none were -/// set. Read every `MAKEPAD_*` hook through this, never `std::env::var`. +/// On a phone (`cfg(mobile)`: Android or OpenHarmony) only a development +/// build reads them (`dev_mode`: debug assertions or feature `dev-mode`, +/// build.rs); a release build behaves as if none were set. A desktop reads +/// them, because its environment is the local user's, like a command-line +/// flag. Read every `MAKEPAD_*` hook through this, never `std::env::var`. pub(crate) fn launch_var(name: &str) -> Option { - launch_set_in(cfg!(dev_mode), name).then(|| std::env::var(name).ok()).flatten() + launch_set_in(cfg!(mobile), cfg!(dev_mode), name).then(|| std::env::var(name).ok()).flatten() } /// [`launch_var`] for a variable whose presence is the setting. pub(crate) fn launch_flag(name: &str) -> bool { - launch_set_in(cfg!(dev_mode), name) + launch_set_in(cfg!(mobile), cfg!(dev_mode), name) } -/// Whether `name` is set and this build may read it. A release build +/// Whether `name` is set and this build may read it. A phone release build /// notes once that it ignored a launch-time hook, without its value. -fn launch_set_in(development: bool, name: &str) -> bool { +fn launch_set_in(mobile: bool, development: bool, name: &str) -> bool { if std::env::var_os(name).is_none() { return false; } - if !development { + if mobile && !development { static NOTED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); if !NOTED.swap(true, std::sync::atomic::Ordering::Relaxed) { - log::info!("launch input: {name} ignored; a release build reads no launch-time hooks"); + log::info!("launch input: {name} ignored; a release build on a phone reads no launch-time hooks"); } return false; } @@ -68,14 +70,16 @@ mod launch_input_tests { use super::launch_set_in; #[test] - fn a_release_build_reads_no_launch_hook() { + fn only_a_phone_release_build_ignores_a_launch_hook() { // A name no other test sets. let name = "MAKEPAD_LAUNCH_INPUT_TEST_PROBE"; std::env::set_var(name, "/sdcard/Download/provision"); - assert!(!launch_set_in(false, name), "a release build ignores it"); - assert!(launch_set_in(true, name), "a development build reads it"); + assert!(!launch_set_in(true, false, name), "a phone release build ignores it"); + assert!(launch_set_in(true, true, name), "a phone development build reads it"); + assert!(launch_set_in(false, false, name), "a desktop release build reads it"); + assert!(launch_set_in(false, true, name), "a desktop development build reads it"); std::env::remove_var(name); - assert!(!launch_set_in(true, name)); + assert!(!launch_set_in(false, false, name), "unset is never read"); } /// Every `MAKEPAD_*` hook goes through `launch_var`/`launch_flag`, so @@ -6767,8 +6771,8 @@ impl App { // that embeds this app as a library (`default-features = false`, // no `standalone`) gets neither — nor does a standalone build // launched with `OCTOS_APP_DEVGOAL=0`. An explicit - // `MAKEPAD_DEV_GOAL_FILE` still runs anywhere, in a development - // build (`launch_var`). + // `MAKEPAD_DEV_GOAL_FILE` still runs anywhere (`launch_var`: on a + // phone a development build, a desktop always). #[cfg(target_os = "android")] let mission: Option<(String, String)> = match crate::launch_var("MAKEPAD_DEV_GOAL_FILE") { Some(p) => std::fs::read_to_string(&p).ok().map(|g| (g, p)), @@ -7064,7 +7068,8 @@ impl App { /// live LLM generation can be driven without touching the native composer via /// adb input. Fires once (clears the env var). Session/open and turn/start /// are ordered on the stdio pipe, so submitting right after `clear_chat` is - /// safe. A development build only (`launch_var`). + /// safe. On a phone a development build only; a desktop reads it + /// (`launch_var`). fn fire_auto_prompt(&mut self, cx: &mut Cx) { if let Some(p) = crate::launch_var("MAKEPAD_AUTO_PROMPT") { std::env::remove_var("MAKEPAD_AUTO_PROMPT"); @@ -9045,10 +9050,11 @@ impl MatchEvent for App { } } - // Launch-time provisioning below is for development builds only - // (`launch_var`): any app on the phone can start this one with these - // extras, so a release build provisions only through the person's - // own QR scan and PIN, or the login screen. + // On a phone the launch-time provisioning below is for development + // builds only (`launch_var`): any app can start this one with these + // extras, so a phone release build provisions only through the + // person's own QR scan and PIN, or the login screen. A desktop reads + // them, the way it reads a command-line flag. // // Provisioning deploy (non-rooted devices): `makepad.PROVISION_DIR` // (→ env MAKEPAD_PROVISION_DIR) names a world-readable staging dir diff --git a/apps/appcard/tools/octos-macos.py b/apps/appcard/tools/octos-macos.py index ec1d0c7c7..9a7a63924 100644 --- a/apps/appcard/tools/octos-macos.py +++ b/apps/appcard/tools/octos-macos.py @@ -92,10 +92,11 @@ def main(): empty_data.write_text('{}\n') env['MAKEPAD_SEED_L0_DATA'] = str(empty_data) if not request.get('skip_build', False): - # A development build: only `dev-mode` reads the MAKEPAD_* hooks set - # above (`launch_var` in app/app/src/lib.rs). + # A desktop build reads the MAKEPAD_* hooks set above as local + # settings (`launch_var` in app/app/src/lib.rs), so no feature is + # needed; only a phone gates them behind `dev-mode`. subprocess.run(['cargo', 'build', '-p', 'octos-app', '--bin', 'octos-app', - '--release', '--features', 'dev-mode', '--message-format=json'], cwd=root / 'app', env=env, check=True) + '--release', '--message-format=json'], cwd=root / 'app', env=env, check=True) if not Path(env['OCTOS_APP_CORE_BIN']).is_file(): raise SystemExit('Build the current native Octos core before launching this RunItem') evidence = Path(request.get('evidence_dir', '/tmp/octos-macos-generation')) diff --git a/crates/shell/src/apps.rs b/crates/shell/src/apps.rs index 25030c316..5a25f64af 100644 --- a/crates/shell/src/apps.rs +++ b/crates/shell/src/apps.rs @@ -211,8 +211,8 @@ pub(crate) mod test_system_apps { /// app can open: `mail` keeps accounts and passwords for the Mail app; /// `glance` takes the cards apps publish to the glance screen (glance.rs). /// `mail_demo` in MAKEPAD_APP_CONFIG serves a demo mailbox from a file vault -/// instead (no keychain, no network): `MAKEPAD_APP_CONFIG='{"mail_demo":true}'`, -/// in a development build only (`launch_input`). +/// instead (no keychain, no network): `MAKEPAD_APP_CONFIG='{"mail_demo":true}'`. +/// A desktop reads it; a phone only in a development build (`launch_input`). /// `news` fetches News's feeds on a timer, with no model (ADR 0002), into /// the Card runner's host directory, so it keeps fetching while News is /// closed. Last, every system app no service answers gets the notice @@ -679,9 +679,9 @@ pub fn matches_running_app(app: &crate::clients::AppDef, running_id: &str, title /// What a launch of `app` opens `module` with. The Card runner opens the app /// the launcher row names — a system app by its `os.*` manifest id, an /// installed one by its own. Any other module opens with what -/// MAKEPAD_APP_CONFIG gives it in a development build -/// (`{"module_open": {"": {...}}}`, or `mail_endpoint` for a linked -/// `mail` module; `launch_input`), else empty. +/// MAKEPAD_APP_CONFIG gives it (`{"module_open": {"": {...}}}`, or +/// `mail_endpoint` for a linked `mail` module; a desktop reads it, a phone +/// only in a development build, `launch_input`), else empty. pub fn module_open(module: &'static dyn AppModule, app: &crate::clients::AppDef) -> Result { let schema = module.open_schema(); if module.id() == "card" { diff --git a/crates/shell/src/launch_input.rs b/crates/shell/src/launch_input.rs index cf568335e..00936065c 100644 --- a/crates/shell/src/launch_input.rs +++ b/crates/shell/src/launch_input.rs @@ -1,29 +1,25 @@ -//! Launch input: settings that arrive with a launch, which another app on -//! the device may have chosen. +//! Launch input: settings that arrive with a launch. //! -//! **Who can send it.** Home's main activity is exported: it is the app -//! drawer entry, the Home app, a share target and a deep-link target, so any -//! installed app can start it, with extras. When the activity is created, -//! Makepad's Android glue copies the extras `makepad.` into the -//! environment as `MAKEPAD_` (`APP_CONFIG`, `PROVISION_CONFIG`, -//! `OCTOS_PROXY`, the `SEED_*` hooks and others). On OpenHarmony the entry -//! ability passes the Want's `makepad.*` parameters on in the same way, but -//! only in a development build (`phone/ohos/EntryAbility.ets`). On a desktop -//! these variables come from the local user's own environment, as -//! command-line flags do. +//! **Where it comes from.** On a desktop these variables are the local +//! user's own environment, the same trust as a command-line flag. On a +//! phone they are not: Home's main activity is exported (the app drawer +//! entry, the Home app, a share target and a deep-link target), so any +//! installed app can start it with extras, and Makepad's Android glue copies +//! the extras `makepad.` into the environment as `MAKEPAD_` +//! (`APP_CONFIG`, `PROVISION_CONFIG`, `SEED_*` and others). OpenHarmony passes +//! the Want's `makepad.*` parameters on the same way. //! -//! **The rule.** OctoSense reads launch input only in a development build -//! (`cfg(dev_mode)`: a debug build, or `--features dev-mode`; never a store -//! build). That covers the app config below (test actions, Mail's demo -//! mailbox, module open parameters), the kernel's launch proxy -//! (`octosense-kernel`) and AppCard's provisioning and seeding hooks -//! (`octos-app`). A release build ignores them, and says so once in the log -//! without echoing the value. `--test-action` flags on a desktop command -//! line are arguments only the local user can pass; they work in every -//! build (`App::run_test_actions` in `lib.rs`). +//! **The rule.** On Android and OpenHarmony (`cfg(native_mobile)`) OctoSense +//! reads the app config only in a development build (`cfg(dev_mode)`: a debug +//! build, or `--features dev-mode`; never a store build); a release build +//! there ignores it and says so once in the log, without the value. A desktop +//! reads it in every build, because its environment is the local user's. +//! `--test-action` flags on a desktop command line are arguments only the +//! local user can pass, and work in every build (`App::run_test_actions` in +//! `lib.rs`). //! -//! Test builds that need these hooks on a phone are development builds -//! under a separate package name (`rom/scripts/build-home.py --dev-mode`). +//! A phone build that needs these hooks is a development build under a +//! separate package name (`rom/scripts/build-home.py --dev-mode`). use crate::dev_mode::BuildKind; use makepad_strict_json::Value; @@ -33,40 +29,50 @@ use makepad_strict_json::Value; /// "module_open": {"": {…}}, "mail_endpoint": "…"}`. pub const APP_CONFIG: &str = "MAKEPAD_APP_CONFIG"; -/// Whether a build of this kind reads launch input: a development build -/// only. +/// Whether this build reads the app config. A desktop always does; Android +/// and OpenHarmony, where another app can set it, only in a development +/// build. pub fn honoured(build: BuildKind) -> bool { - build == BuildKind::Development + honoured_for(cfg!(native_mobile), build) } -/// The app config, parsed, when this build reads launch input. A release -/// build answers `None` whatever the variable holds. +/// [`honoured`] split out from the target for testing: `native_mobile` is +/// Android and OpenHarmony (and iOS, which has no such channel but is a +/// phone all the same). +fn honoured_for(native_mobile: bool, build: BuildKind) -> bool { + !native_mobile || build == BuildKind::Development +} + +/// The app config, parsed, when this build reads launch input. A phone +/// release build answers `None` whatever the variable holds. pub fn app_config() -> Option { - let text = std::env::var(APP_CONFIG).ok()?; let build = BuildKind::current(); if !honoured(build) { - note_ignored(); + if std::env::var_os(APP_CONFIG).is_some() { + note_ignored(); + } + return None; } - parse_app_config(build, &text) + std::env::var(APP_CONFIG).ok().and_then(|text| makepad_strict_json::parse(text.as_bytes()).ok()) } -/// [`app_config`] for a given build and variable text. -pub fn parse_app_config(build: BuildKind, text: &str) -> Option { - if !honoured(build) { +/// [`app_config`] for a given target and build, over the given text. +fn app_config_for(native_mobile: bool, build: BuildKind, text: &str) -> Option { + if !honoured_for(native_mobile, build) { return None; } makepad_strict_json::parse(text.as_bytes()).ok() } -/// The app config's `test_actions`, in order, as `--test-action` names; -/// none in a release build. +/// The app config's `test_actions`, in order, as `--test-action` names; none +/// in a phone release build. pub fn test_actions() -> Vec { app_config().map(|config| test_actions_in(&config)).unwrap_or_default() } -/// [`test_actions`] for a given build and variable text. -pub fn test_actions_for(build: BuildKind, text: &str) -> Vec { - parse_app_config(build, text).map(|config| test_actions_in(&config)).unwrap_or_default() +/// [`test_actions`] for a given target and build, over the given text. +fn test_actions_for(native_mobile: bool, build: BuildKind, text: &str) -> Vec { + app_config_for(native_mobile, build, text).map(|config| test_actions_in(&config)).unwrap_or_default() } fn test_actions_in(config: &Value) -> Vec { @@ -87,7 +93,7 @@ fn note_ignored() { static ONCE: std::sync::Once = std::sync::Once::new(); ONCE.call_once(|| { makepad_widgets::log!( - "launch input: {APP_CONFIG} ignored; a release build takes no test actions or demo settings from a launch" + "launch input: {APP_CONFIG} ignored; a release build on a phone takes no test actions or demo settings from a launch" ) }); } @@ -97,34 +103,40 @@ mod tests { use super::*; const CONFIG: &str = r#"{"test_actions":["capture:/sdcard/Download/frame.png","launch-mail","system-chat-send:hello"],"mail_demo":true}"#; + const ACTIONS: [&str; 3] = ["capture:/sdcard/Download/frame.png", "launch-mail", "system-chat-send:hello"]; #[test] - fn a_release_build_ignores_test_actions_from_the_launch() { + fn a_phone_release_build_ignores_test_actions_from_the_launch() { for build in [BuildKind::Release, BuildKind::Store] { - assert!(!honoured(build), "{build:?}"); - assert!(test_actions_for(build, CONFIG).is_empty(), "{build:?}"); - assert!(parse_app_config(build, CONFIG).is_none(), "{build:?}"); + assert!(!honoured_for(true, build), "phone {build:?}"); + assert!(test_actions_for(true, build, CONFIG).is_empty(), "phone {build:?}"); + assert!(app_config_for(true, build, CONFIG).is_none(), "phone {build:?}"); + } + } + + #[test] + fn a_desktop_build_reads_them_in_every_build() { + for build in [BuildKind::Release, BuildKind::Store, BuildKind::Development] { + assert!(honoured_for(false, build), "desktop {build:?}"); + assert_eq!(test_actions_for(false, build, CONFIG), ACTIONS, "desktop {build:?}"); } } #[test] - fn a_development_build_keeps_them_in_order() { - assert!(honoured(BuildKind::Development)); - assert_eq!( - test_actions_for(BuildKind::Development, CONFIG), - ["capture:/sdcard/Download/frame.png", "launch-mail", "system-chat-send:hello"] - ); - let config = parse_app_config(BuildKind::Development, CONFIG).expect("parsed"); + fn a_development_build_keeps_them_on_a_phone_too() { + assert!(honoured_for(true, BuildKind::Development)); + assert_eq!(test_actions_for(true, BuildKind::Development, CONFIG), ACTIONS); + let config = app_config_for(true, BuildKind::Development, CONFIG).expect("parsed"); assert_eq!(config.get("mail_demo").and_then(|v| v.as_bool()), Some(true)); } #[test] fn malformed_or_missing_actions_give_none() { for text in ["", "not json", r#"{"test_actions":"launch-mail"}"#, r#"{"mail_demo":true}"#] { - assert!(test_actions_for(BuildKind::Development, text).is_empty(), "{text:?}"); + assert!(test_actions_for(false, BuildKind::Release, text).is_empty(), "{text:?}"); } // Entries that are not strings are skipped, the rest kept. - assert_eq!(test_actions_for(BuildKind::Development, r#"{"test_actions":[1,"launch-news",null]}"#), ["launch-news"]); + assert_eq!(test_actions_for(false, BuildKind::Release, r#"{"test_actions":[1,"launch-news",null]}"#), ["launch-news"]); } /// The shell has one reader of the app config: this module. Anything diff --git a/desktop/README.md b/desktop/README.md index 42cedb3b4..d91a98ed1 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -124,7 +124,7 @@ App Hub's modules have no process form and always open in-process. | `OCTOSENSE_APP_DATA` | Where App Hub keeps installed apps (default `apps/` in the platform data directory). | | `OCTOSENSE_HUB`, `OCTOSENSE_HUB_ANCHOR` | App Hub catalog origin (path or URL) and trust anchor; default is the App Hub repository's `main`. | | `OCTOSENSE_SYSTEM_APPS` | The system-app selection file; the root `.cargo/config.toml` sets it to `desktop/system-apps.json`. | -| `MAKEPAD_APP_CONFIG='{"mail_demo":true}'` | Serve Mail's demo mailbox (see [Demos](#demos)); development builds only (debug, or `--features dev-mode`), like all of `MAKEPAD_APP_CONFIG` ([`launch_input.rs`](../crates/shell/src/launch_input.rs)). | +| `MAKEPAD_APP_CONFIG='{"mail_demo":true}'` | Serve Mail's demo mailbox (see [Demos](#demos)). | | `OCTOSENSE_MAIL_VAULT=file` | Keep Mail passwords in a 0600 file instead of the macOS keychain. | | `OCTOSENSE_LLM_VAULT=file` | Keep AI providers' keys in the owner-only octos profile instead of the macOS keychain. | | `OCTOS_APP_CORE_BIN`, `OCTOS_APP_CORE_DIR` | The octos kernel binary the shell's kernel service runs, unchecked (unset: the packaged `octos-kernel`, see [Build and run](#build-and-run)) and its core dir (default `~/octos-home/.octos`; the AI providers profile is `/profiles/_main.json`). | @@ -363,10 +363,10 @@ A card that names no theme takes the shell’s light or dark palette. Toasts and ### Mail without an account ```sh -MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --release -p octosense --features dev-mode +MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --release -p octosense ``` -Open **Mail**, sign in on the host sheet with any address and password `demo`. The demo serves sample messages from a file vault: no network, no keychain. Only a development build reads `MAKEPAD_APP_CONFIG`, hence `--features dev-mode` (**unverified** since that change; a debug `cargo run` works the same way). +Open **Mail**, sign in on the host sheet with any address and password `demo`. The demo serves sample messages from a file vault: no network, no keychain. With a real account on an unsigned development build, macOS asks for keychain access again after every rebuild. For development, keep passwords in a file instead: diff --git a/desktop/README.zh-CN.md b/desktop/README.zh-CN.md index 6afe92b0a..7a64c2479 100644 --- a/desktop/README.zh-CN.md +++ b/desktop/README.zh-CN.md @@ -124,7 +124,7 @@ App Hub 的模块没有进程形态,总是在进程内打开。 | `OCTOSENSE_APP_DATA` | App Hub 保存已安装应用的位置(默认为平台数据目录下的 `apps/`)。 | | `OCTOSENSE_HUB`、`OCTOSENSE_HUB_ANCHOR` | App Hub 目录来源(路径或 URL)和信任锚;默认是 App Hub 仓库的 `main`。 | | `OCTOSENSE_SYSTEM_APPS` | 系统应用选择文件;根目录的 `.cargo/config.toml` 把它设为 `desktop/system-apps.json`。 | -| `MAKEPAD_APP_CONFIG='{"mail_demo":true}'` | 提供邮件的演示邮箱(见[演示](#演示));与整个 `MAKEPAD_APP_CONFIG` 一样仅限开发版(调试构建或 `--features dev-mode`,见 [`launch_input.rs`](../crates/shell/src/launch_input.rs))。 | +| `MAKEPAD_APP_CONFIG='{"mail_demo":true}'` | 提供邮件的演示邮箱(见[演示](#演示))。 | | `OCTOSENSE_MAIL_VAULT=file` | 把邮件密码保存在权限为 0600 的文件中,而不是 macOS 钥匙串。 | | `OCTOSENSE_LLM_VAULT=file` | 把 AI 提供商的密钥保存在仅所有者可读的 octos profile 中,而不是 macOS 钥匙串。 | | `OCTOS_APP_CORE_BIN`、`OCTOS_APP_CORE_DIR` | Shell 内核服务运行的 octos 内核二进制,不做检查(未设置:使用随附的 `octos-kernel`,见[构建与运行](#构建与运行))及其 core 目录(默认 `~/octos-home/.octos`;AI 提供商的 profile 为 `/profiles/_main.json`)。 | @@ -348,10 +348,10 @@ cargo run --release -p octosense --features app-appcard -- --module appcard ### 无账户试用邮件 ```sh -MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --release -p octosense --features dev-mode +MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --release -p octosense ``` -打开 **Mail**,在宿主面板上用任意地址和密码 `demo` 登录。演示模式从文件保险库提供示例邮件:不联网,不用钥匙串。只有开发版读取 `MAKEPAD_APP_CONFIG`,所以要加 `--features dev-mode`(这一改动后**未验证**;调试构建的 `cargo run` 效果相同)。 +打开 **Mail**,在宿主面板上用任意地址和密码 `demo` 登录。演示模式从文件保险库提供示例邮件:不联网,不用钥匙串。 在未签名的开发构建上使用真实账户时,每次重新构建后 macOS 都会再次请求钥匙串访问权限。开发时可以改用文件保存密码: diff --git a/desktop/docs/code-walkthrough.md b/desktop/docs/code-walkthrough.md index a1ceffb0a..9bacd7ce3 100644 --- a/desktop/docs/code-walkthrough.md +++ b/desktop/docs/code-walkthrough.md @@ -105,13 +105,10 @@ require the full shell. For example, run Mail's demo service from the OctoSense root: ```sh -MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --locked --release -p octosense --features dev-mode +MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --locked --release -p octosense ``` -The demo account uses password `demo`; sends stay in the demo. Only a -development build reads `MAKEPAD_APP_CONFIG` (`crates/shell/src/launch_input.rs`: -on a phone it arrives as an intent extra any app can set), hence -`--features dev-mode` (**unverified** since that change). Develop new store +The demo account uses password `demo`; sends stay in the demo. Develop new store apps in OctoSense App Flow (formerly Design Flow) and use App Hub's admission/publishing flow. The [local-catalog recipe](../README.md#try-your-own-app-before-it-is-published) tests installation into the shell before publication. diff --git a/desktop/docs/code-walkthrough.zh-CN.md b/desktop/docs/code-walkthrough.zh-CN.md index e66bcd10e..2054bb91b 100644 --- a/desktop/docs/code-walkthrough.zh-CN.md +++ b/desktop/docs/code-walkthrough.zh-CN.md @@ -89,10 +89,10 @@ cargo run --locked --release -p octosense-card-host --bin card-host -- --bundle 例如,在 OctoSense 根目录运行 Mail 的演示服务: ```sh -MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --locked --release -p octosense --features dev-mode +MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --locked --release -p octosense ``` -演示账户密码为 `demo`,发送留在演示环境。只有开发版读取 `MAKEPAD_APP_CONFIG`(`crates/shell/src/launch_input.rs`:在手机上它以任何应用都能设置的 intent extra 形式到达),所以要加 `--features dev-mode`(这一改动后**未验证**)。新商店应用在 OctoSense App Flow(原 Design Flow)中开发,使用 App Hub 的准入/发布流程。[本地签名目录配方](../README.zh-CN.md#发布前试用自己的应用)用于在发布前测试应用能否安装到 Shell。 +演示账户密码为 `demo`,发送留在演示环境。新商店应用在 OctoSense App Flow(原 Design Flow)中开发,使用 App Hub 的准入/发布流程。[本地签名目录配方](../README.zh-CN.md#发布前试用自己的应用)用于在发布前测试应用能否安装到 Shell。 [`desktop/system-apps.json`](../system-apps.json) 和 [`phone/system-apps.json`](../../phone/system-apps.json) 选择 `apps/` 中的 bundle。 diff --git a/phone/README.md b/phone/README.md index 8840a07e9..b7d1bfbd3 100644 --- a/phone/README.md +++ b/phone/README.md @@ -56,7 +56,7 @@ cargo run --release -p octosense-home --features mobile-only | `-- --module ` | Host a linked module in-process instead of as a child process | | `-- --test-action ` | Fire a shell action at startup (see [Run on a desktop](#run-on-a-desktop)) | | `MAKEPAD_WM_TEST_APP=[:]` | Launch an app (count times) once the shell is up | -| `MAKEPAD_APP_CONFIG='{"mail_demo":true}'` | Serve Mail from a demo mailbox (password `demo`); development builds only (debug, or `--features dev-mode`) | +| `MAKEPAD_APP_CONFIG='{"mail_demo":true}'` | Serve Mail from a demo mailbox (password `demo`) | | `OCTOSENSE_HOME=` | Keep state somewhere other than `~/.octosense` | **Android APK.** `rom/scripts/build-home.sh` (a wrapper for @@ -311,13 +311,14 @@ never holds a socket or a password. For a demo mailbox (password `demo`): ```sh # desktop, from phone/ -MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --release -p octosense-home --features mobile-only,dev-mode +MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --release -p octosense-home --features mobile-only # phone: a development test package (see Launch-time test hooks) adb shell am start -n /.MakepadApp --es makepad.APP_CONFIG '{"mail_demo":true}' ``` -A release build ignores `mail_demo` (both commands are **unverified** since -that change). +The desktop command reads `MAKEPAD_APP_CONFIG` as a local setting; a phone +release build ignores it, so the phone line needs a development test package +(**unverified** since that change). The earlier native News, Photos and Maps modules are deleted (native-apps ADR 0004 §1, [#113](https://github.com/OctoSense-org/OctoSense/pull/113)); none of the system apps has a native module any more. diff --git a/phone/README.zh-CN.md b/phone/README.zh-CN.md index 12a6dac15..c2497ae0d 100644 --- a/phone/README.zh-CN.md +++ b/phone/README.zh-CN.md @@ -31,7 +31,7 @@ cargo run --release -p octosense-home --features mobile-only | `-- --module ` | 在进程内托管已链接的模块,而不是作为子进程 | | `-- --test-action ` | 启动时触发一个 Shell 动作(见[在桌面电脑上运行](#在桌面电脑上运行)) | | `MAKEPAD_WM_TEST_APP=[:]` | Shell 启动后打开某个应用(可指定次数) | -| `MAKEPAD_APP_CONFIG='{"mail_demo":true}'` | 用演示邮箱提供 Mail(密码为 `demo`);仅限开发版(调试构建或 `--features dev-mode`) | +| `MAKEPAD_APP_CONFIG='{"mail_demo":true}'` | 用演示邮箱提供 Mail(密码为 `demo`) | | `OCTOSENSE_HOME=` | 把状态数据保存在 `~/.octosense` 以外的位置 | **Android APK。** `rom/scripts/build-home.sh`(`build-home.py` 的包装脚本)构建 Home APK 及其 System Bridge APK,一起签名,把 octos 内核作为 `liboctos.so` 打包进去,并写出 `OctoSenseHome.apk`、`OctoSenseBridge.apk` 和一份 `build.json` 回执。它从不安装或刷机。在仓库根目录构建一对使用 Makepad 开发密钥签名的独立开发版: @@ -208,12 +208,12 @@ Mail 通过 `mail` 宿主服务([`apps/mail/host-service`](../apps/mail/host-s ```sh # desktop, from phone/ -MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --release -p octosense-home --features mobile-only,dev-mode +MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --release -p octosense-home --features mobile-only # 手机:开发版测试包(见“启动时的测试钩子”) adb shell am start -n /.MakepadApp --es makepad.APP_CONFIG '{"mail_demo":true}' ``` -发布版会忽略 `mail_demo`(这一改动后两条命令均**未验证**)。 +桌面命令会把 `MAKEPAD_APP_CONFIG` 当作本机设置读取;手机发布版会忽略它,所以手机那行需要开发版测试包(这一改动后**未验证**)。 早期的原生 News、Photos 和 Maps 模块已删除(原生应用 ADR 0004 §1,[#113](https://github.com/OctoSense-org/OctoSense/pull/113));所有系统应用都不再有原生模块。 From 5b8e4ec8d897d84401bf76df019fc7d5f64071d1 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Sun, 11 Oct 2026 07:16:32 -0700 Subject: [PATCH 4/4] Carry the makepad launch-input hardening as a stacked runtime patch Protects a release Home now, without waiting for a makepad sweep: the shell's pinned makepad gains the Studio/OCTOS_PROXY launch-input gate from OctoSense-org/makepad#119 as the next stacked runtime patch over the pinned base, so a shipped Android Home ignores a Studio host or app proxy from an Intent extra. The OpenHarmony template part of #119 is not carried here, since Home builds its own OpenHarmony entry ability. Co-Authored-By: Claude Opus 5.5 --- runtime-patches.lock.json | 9 +- .../makepad-launch-input-hardening.patch | 322 ++++++++++++++++++ 2 files changed, 329 insertions(+), 2 deletions(-) create mode 100644 tools/runtime-patches/makepad-launch-input-hardening.patch diff --git a/runtime-patches.lock.json b/runtime-patches.lock.json index 8b5c0021c..35933d1d4 100644 --- a/runtime-patches.lock.json +++ b/runtime-patches.lock.json @@ -4,7 +4,7 @@ "base_revision": "d653bc4493619ab979ccff165cf48c0c63b77d19", "patch": "tools/runtime-patches/makepad-settings.patch", "sha256": "3a79cddddc89dc4d064a9f0bdd17dc8111dcab9c796a334d95f61ecb9d5011aa", - "tree": "3f4fd959f1ee43445bc7ef13cf72465bb1951829", + "tree": "d047e32fdfb91aa26b95a62464645020bfa6a4f7", "product_fixes": [ "Settings integration: Android IME synchronization, accessibility and reduced motion, GLES program-cache validation, obscured-gesture cancellation and scroll-position observation.", "Contained-app isolation and secret-input restrictions come from the pinned upstream runtime; the Settings overlay preserves those gates.", @@ -48,7 +48,8 @@ "A native host may stage up to 64 MiB into an app's jail (MAX_IMPORT_BYTES; prepare_import and commit_import), so files.import can take documents above the 1 MiB script write limit. Staged bytes never enter the script heap; the jail's byte quota and entry cap still apply, and script writes, photo picks and exports keep 1 MiB.", "An OS resume restarts only the Video playback its pause stopped, including a start it cancelled; a player paused by the person, or paused or stopped by its app during the pause, stays paused (#406).", "Camera capture and recording use explicit per-call boolean audio/library options, defaulting to silent app-local output. Permission availability never opts into recording audio or exporting captures; async microphone checks retain capture intent and background provenance. Unsupported library export fails before capture.", - "A hidden Android native composer remembers animated IME insets without requesting a root layout every frame; showing it applies the latest inset before its first visible frame." + "A hidden Android native composer remembers animated IME insets without requesting a root layout every frame; showing it applies the latest inset before its first visible frame.", + "Launch-input hardening: the Studio connection and the OCTOS_PROXY app HTTP proxy, which Makepad itself acts on from an Android Intent extra, are honoured only when the build opts into launch-time development input (debuggable, or the manifest meta-data dev.makepad.LAUNCH_STUDIO). A shipped build ignores them and drops any Studio host a development build saved. The exported activity would otherwise let another installed app point it at a host or a proxy." ], "stacked": [ { @@ -190,6 +191,10 @@ { "patch": "tools/runtime-patches/makepad-hidden-composer-insets.patch", "sha256": "75935d129cfe4adf09555d3a94b0a7ad387f2ea97cf357f9d60c728d87239037" + }, + { + "patch": "tools/runtime-patches/makepad-launch-input-hardening.patch", + "sha256": "8eaafef3ef0591bec2a25c05d5187564d5dfb87a32e6b937b6d9c4e9f6fa5b84" } ] } diff --git a/tools/runtime-patches/makepad-launch-input-hardening.patch b/tools/runtime-patches/makepad-launch-input-hardening.patch new file mode 100644 index 000000000..71447f6e7 --- /dev/null +++ b/tools/runtime-patches/makepad-launch-input-hardening.patch @@ -0,0 +1,322 @@ +diff --git a/platform/src/app_main.rs b/platform/src/app_main.rs +index a4b48f3d5..242649c56 100644 +--- a/platform/src/app_main.rs ++++ b/platform/src/app_main.rs +@@ -195,6 +195,19 @@ pub(crate) fn resolve_studio_crate() -> Option { + }) + } + ++/// Whether a build honours launch-time development input that Makepad itself ++/// acts on: the Studio connection and the `OCTOS_PROXY` app HTTP proxy, both ++/// carried in an Android Intent extra (or an OpenHarmony launch parameter). ++/// An activity that handles those is usually exported, so another installed ++/// app can start it with extras; a shipped app must not act on them. True for ++/// a debuggable build, or one that opts in through its manifest (on Android, ++/// the `dev.makepad.LAUNCH_STUDIO` meta-data). The Android glue ++/// (`os::linux::android::android_jni`) passes both signals in. ++#[cfg_attr(not(target_os = "android"), allow(dead_code))] ++pub(crate) fn launch_dev_input_allowed(debuggable: bool, manifest_opt_in: bool) -> bool { ++ debuggable || manifest_opt_in ++} ++ + fn build_studio_http( + studio_host: &str, + studio_build: Option<&str>, +@@ -239,6 +252,14 @@ pub fn resolve_studio_http() -> String { + mod tests { + use super::*; + ++ #[test] ++ fn only_debuggable_or_opted_in_builds_honour_launch_dev_input() { ++ assert!(launch_dev_input_allowed(true, false), "a debuggable build"); ++ assert!(launch_dev_input_allowed(false, true), "a manifest opt-in"); ++ assert!(launch_dev_input_allowed(true, true)); ++ assert!(!launch_dev_input_allowed(false, false), "a shipped build ignores launch dev input"); ++ } ++ + #[test] + fn extract_studio_build_id_handles_app_paths() { + assert_eq!( +diff --git a/platform/src/os/linux/android/android_jni.rs b/platform/src/os/linux/android/android_jni.rs +index 1cfc4d0a1..6b7c9468c 100644 +--- a/platform/src/os/linux/android/android_jni.rs ++++ b/platform/src/os/linux/android/android_jni.rs +@@ -526,6 +526,136 @@ unsafe fn persist_string_pref( + true + } + ++/// Remove a persisted preference. Used to drop a Studio host a development ++/// build saved, so it cannot survive into a shipped build. ++unsafe fn remove_persisted_pref( ++ env: *mut jni_sys::JNIEnv, ++ activity: jni_sys::jobject, ++ key: &str, ++) -> bool { ++ let prefs = match get_prefs_object(env, activity) { ++ Some(v) => v, ++ None => return false, ++ }; ++ let editor = ndk_utils::call_object_method!( ++ env, ++ prefs, ++ "edit", ++ "()Landroid/content/SharedPreferences$Editor;" ++ ); ++ if editor.is_null() { ++ return false; ++ } ++ let key = match new_jstring(env, key) { ++ Some(v) => v, ++ None => return false, ++ }; ++ let editor = ndk_utils::call_object_method!( ++ env, ++ editor, ++ "remove", ++ "(Ljava/lang/String;)Landroid/content/SharedPreferences$Editor;", ++ key ++ ); ++ if editor.is_null() { ++ return false; ++ } ++ ndk_utils::call_void_method!(env, editor, "apply", "()V"); ++ true ++} ++ ++/// Whether this build is debuggable (`android:debuggable="true"`): the ++/// ApplicationInfo `FLAG_DEBUGGABLE` bit. ++unsafe fn activity_is_debuggable(env: *mut jni_sys::JNIEnv, activity: jni_sys::jobject) -> bool { ++ let app_info = ndk_utils::call_object_method!( ++ env, ++ activity, ++ "getApplicationInfo", ++ "()Landroid/content/pm/ApplicationInfo;" ++ ); ++ if app_info.is_null() { ++ return false; ++ } ++ let class = ((**env).GetObjectClass.unwrap())(env, app_info); ++ if class.is_null() { ++ return false; ++ } ++ let field = ((**env).GetFieldID.unwrap())(env, class, b"flags\0".as_ptr() as _, b"I\0".as_ptr() as _); ++ if field.is_null() { ++ return false; ++ } ++ const FLAG_DEBUGGABLE: i32 = 2; // ApplicationInfo.FLAG_DEBUGGABLE ++ ((**env).GetIntField.unwrap())(env, app_info, field) & FLAG_DEBUGGABLE != 0 ++} ++ ++/// A boolean `` on the application element, default false. Lets a ++/// specific signed build opt into launch-time development input without being ++/// debuggable. ++unsafe fn application_meta_bool( ++ env: *mut jni_sys::JNIEnv, ++ activity: jni_sys::jobject, ++ key: &str, ++) -> bool { ++ let pm = ndk_utils::call_object_method!( ++ env, ++ activity, ++ "getPackageManager", ++ "()Landroid/content/pm/PackageManager;" ++ ); ++ if pm.is_null() { ++ return false; ++ } ++ let package = ndk_utils::call_object_method!( ++ env, ++ activity, ++ "getPackageName", ++ "()Ljava/lang/String;" ++ ); ++ if package.is_null() { ++ return false; ++ } ++ const GET_META_DATA: i32 = 128; // PackageManager.GET_META_DATA ++ let app_info = ndk_utils::call_object_method!( ++ env, ++ pm, ++ "getApplicationInfo", ++ "(Ljava/lang/String;I)Landroid/content/pm/ApplicationInfo;", ++ package, ++ GET_META_DATA ++ ); ++ // getApplicationInfo for our own package never throws, but clear any ++ // pending exception so the next JNI call is not poisoned. ++ if (**env).ExceptionCheck.map(|f| f(env)).unwrap_or(0) != 0 { ++ ((**env).ExceptionClear.unwrap())(env); ++ return false; ++ } ++ if app_info.is_null() { ++ return false; ++ } ++ let class = ((**env).GetObjectClass.unwrap())(env, app_info); ++ if class.is_null() { ++ return false; ++ } ++ let field = ((**env).GetFieldID.unwrap())( ++ env, ++ class, ++ b"metaData\0".as_ptr() as _, ++ b"Landroid/os/Bundle;\0".as_ptr() as _, ++ ); ++ if field.is_null() { ++ return false; ++ } ++ let bundle = ((**env).GetObjectField.unwrap())(env, app_info, field); ++ if bundle.is_null() { ++ return false; ++ } ++ let key = match new_jstring(env, key) { ++ Some(v) => v, ++ None => return false, ++ }; ++ ndk_utils::call_bool_method!(env, bundle, "getBoolean", "(Ljava/lang/String;Z)Z", key, 0i32) != 0 ++} ++ + pub unsafe fn apply_studio_env_from_activity(activity: *const std::ffi::c_void) { + if activity.is_null() { + return; +@@ -533,32 +663,55 @@ pub unsafe fn apply_studio_env_from_activity(activity: *const std::ffi::c_void) + let env = attach_jni_env(); + let activity = activity as jni_sys::jobject; + ++ // A launch Intent's extras are untrusted: this activity is usually ++ // exported (an app's launcher entry, and OctoSense Home is a Home app and ++ // a share/deep-link target), so any installed app can start it with ++ // extras. The Studio connection and the OCTOS_PROXY JVM proxy below are ++ // things Makepad itself acts on, so they are honoured from launch input ++ // only when the build opts into launch-time development input: ++ // `android:debuggable="true"`, or the manifest meta-data ++ // `dev.makepad.LAUNCH_STUDIO` set true. A shipped (non-debuggable) app ++ // ignores them, and drops any Studio host it saved under an earlier ++ // development build. The other `MAKEPAD_*` passthroughs below are inert ++ // until an app reads them; the app decides whether to. ++ let allow_launch_dev = crate::app_main::launch_dev_input_allowed( ++ activity_is_debuggable(env, activity), ++ application_meta_bool(env, activity, "dev.makepad.LAUNCH_STUDIO"), ++ ); ++ + std::env::remove_var("STUDIO"); + std::env::remove_var("STUDIO_BUILD"); + std::env::remove_var("STUDIO_HOST"); + std::env::remove_var("STUDIO_CRATE"); + +- let intent_studio_host = get_intent_string_extra(env, activity, "makepad.STUDIO_HOST") +- .filter(|v| !v.trim().is_empty()); +- let intent_studio_crate = get_intent_string_extra(env, activity, "makepad.STUDIO_CRATE") +- .filter(|v| !v.trim().is_empty()); +- +- if let Some(studio_host) = intent_studio_host { +- let _ = persist_string_pref(env, activity, MAKEPAD_STUDIO_HOST_PREF_KEY, &studio_host); +- std::env::set_var("STUDIO_HOST", &studio_host); +- } else if let Some(studio_host) = +- get_persisted_string_pref(env, activity, MAKEPAD_STUDIO_HOST_PREF_KEY) +- { +- std::env::set_var("STUDIO_HOST", &studio_host); +- } ++ if allow_launch_dev { ++ let intent_studio_host = get_intent_string_extra(env, activity, "makepad.STUDIO_HOST") ++ .filter(|v| !v.trim().is_empty()); ++ let intent_studio_crate = get_intent_string_extra(env, activity, "makepad.STUDIO_CRATE") ++ .filter(|v| !v.trim().is_empty()); ++ ++ if let Some(studio_host) = intent_studio_host { ++ let _ = persist_string_pref(env, activity, MAKEPAD_STUDIO_HOST_PREF_KEY, &studio_host); ++ std::env::set_var("STUDIO_HOST", &studio_host); ++ } else if let Some(studio_host) = ++ get_persisted_string_pref(env, activity, MAKEPAD_STUDIO_HOST_PREF_KEY) ++ { ++ std::env::set_var("STUDIO_HOST", &studio_host); ++ } + +- if let Some(studio_crate) = intent_studio_crate { +- let _ = persist_string_pref(env, activity, MAKEPAD_STUDIO_CRATE_PREF_KEY, &studio_crate); +- std::env::set_var("STUDIO_CRATE", &studio_crate); +- } else if let Some(studio_crate) = +- get_persisted_string_pref(env, activity, MAKEPAD_STUDIO_CRATE_PREF_KEY) +- { +- std::env::set_var("STUDIO_CRATE", &studio_crate); ++ if let Some(studio_crate) = intent_studio_crate { ++ let _ = persist_string_pref(env, activity, MAKEPAD_STUDIO_CRATE_PREF_KEY, &studio_crate); ++ std::env::set_var("STUDIO_CRATE", &studio_crate); ++ } else if let Some(studio_crate) = ++ get_persisted_string_pref(env, activity, MAKEPAD_STUDIO_CRATE_PREF_KEY) ++ { ++ std::env::set_var("STUDIO_CRATE", &studio_crate); ++ } ++ } else { ++ // Never let a Studio host survive into a shipped build: an app built ++ // debuggable once, then released, must not keep dialing it. ++ let _ = remove_persisted_pref(env, activity, MAKEPAD_STUDIO_HOST_PREF_KEY); ++ let _ = remove_persisted_pref(env, activity, MAKEPAD_STUDIO_CRATE_PREF_KEY); + } + + // Generic app-config passthrough: `--es makepad.APP_CONFIG ` +@@ -673,10 +826,18 @@ pub unsafe fn apply_studio_env_from_activity(activity: *const std::ffi::c_void) + // `direct` / `none` / `off` / empty (or no extra at all) => NO proxy: both the + // octos LLM leg and the app-side sys.* fetches go straight out over the + // device's own network (Wi-Fi). Any other value is a proxy URL to tunnel. +- let proxy = get_intent_string_extra(env, activity, "makepad.OCTOS_PROXY").filter(|v| { +- let t = v.trim().to_ascii_lowercase(); +- !t.is_empty() && t != "direct" && t != "none" && t != "off" +- }); ++ // Makepad acts on this itself (the app-side JVM proxy below), so like the ++ // Studio connection it is honoured from launch input only when the build ++ // opts into launch-time development input; a shipped app always clears ++ // the proxy. ++ let proxy = allow_launch_dev ++ .then(|| { ++ get_intent_string_extra(env, activity, "makepad.OCTOS_PROXY").filter(|v| { ++ let t = v.trim().to_ascii_lowercase(); ++ !t.is_empty() && t != "direct" && t != "none" && t != "off" ++ }) ++ }) ++ .flatten(); + if let Some(proxy) = proxy { + std::env::set_var("MAKEPAD_OCTOS_PROXY", &proxy); + // App-side leg: MakepadNetwork opens connections with a bare +diff --git a/tools/cargo_makepad/src/android/java/dev/makepad/android/MakepadActivity.java b/tools/cargo_makepad/src/android/java/dev/makepad/android/MakepadActivity.java +index e5042071b..78d377fc0 100644 +--- a/tools/cargo_makepad/src/android/java/dev/makepad/android/MakepadActivity.java ++++ b/tools/cargo_makepad/src/android/java/dev/makepad/android/MakepadActivity.java +@@ -1521,6 +1521,24 @@ public class MakepadActivity + clearWarmResumeSurfaceSnapshot(); + } + ++ // Whether this build acts on launch-time development input that Makepad ++ // itself handles (the OCTOS_PROXY app proxy here, the Studio connection ++ // on the native side): a debuggable build, or one that opts in with the ++ // `dev.makepad.LAUNCH_STUDIO` manifest meta-data. A shipped app does ++ // neither, so an Intent extra from another app cannot point it at a proxy. ++ private boolean launchDevInputAllowed() { ++ try { ++ ApplicationInfo info = getPackageManager().getApplicationInfo( ++ getPackageName(), PackageManager.GET_META_DATA); ++ boolean debuggable = (info.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; ++ boolean optIn = info.metaData != null ++ && info.metaData.getBoolean("dev.makepad.LAUNCH_STUDIO", false); ++ return debuggable || optIn; ++ } catch (Exception e) { ++ return false; ++ } ++ } ++ + @Override + public void onCreate(Bundle savedInstanceState) { + if (mWebSocketsThread == null || !mWebSocketsThread.isAlive()) { +@@ -1544,9 +1562,12 @@ public class MakepadActivity + // is set — needed when the device has no direct internet route (e.g. an + // adb-reverse tunnel to the dev host). HttpURLConnection honours these + // JVM proxy system properties, so no per-request change is needed. ++ // This activity is usually exported, so the extra is untrusted: only a ++ // build that opts into launch-time development input acts on it (the ++ // native side keeps the same rule and clears the properties otherwise). + try { + Intent launchIntent = getIntent(); +- String proxy = launchIntent != null ++ String proxy = launchDevInputAllowed() && launchIntent != null + ? launchIntent.getStringExtra("makepad.OCTOS_PROXY") : null; + if (proxy != null && proxy.length() > 0) { + String hp = proxy.replaceFirst("^https?://", "").replaceAll("/.*$", "");