Skip to main content
KeyOS API Reference

app_manager/
messages.rs

1// SPDX-FileCopyrightText: 2025 Foundation Devices, Inc. <hello@foundation.xyz>
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4use num_traits::{FromPrimitive, ToPrimitive};
5use server::{AsScalar, FromScalar, WithAppId};
6use xous::{AppId, PID};
7
8use crate::error::{AppManagerError, LaunchError};
9
10#[derive(Debug, server::Message)]
11#[response(Result<PID, AppManagerError>)]
12pub struct LaunchAppBlocking(pub AppId);
13
14#[derive(Debug, server::Message)]
15#[response(Result<(), AppManagerError>)]
16pub struct RefreshInstalledApps;
17
18/// Rescan the app set after a completed in-place replacement. The supplied app is reported as
19/// installed even when its manifest is unchanged, so subscribers also refresh resources such as
20/// icons that are not part of the manifest.
21#[derive(Debug, server::Message)]
22#[response(Result<(), AppManagerError>)]
23pub struct RefreshInstalledApp(pub AppId);
24
25impl AsScalar<3> for AppManagerError {
26    fn as_scalar(&self) -> [u32; 3] { [self.to_u32().unwrap(), 0, 0] }
27}
28
29impl FromScalar<3> for AppManagerError {
30    fn from_scalar([e, ..]: [u32; 3]) -> Self {
31        AppManagerError::from_u32(e).unwrap_or(AppManagerError::InternalError)
32    }
33}
34
35#[derive(Debug, Clone, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
36#[event(AppEvent)]
37pub struct SubscribeAppEvents;
38
39#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
40pub enum AppEvent {
41    AppLaunching {
42        #[rkyv(with = WithAppId)]
43        app_id: AppId,
44        launched_by: PID,
45    },
46
47    AppLaunched {
48        #[rkyv(with = WithAppId)]
49        app_id: AppId,
50        pid: PID,
51        launched_by: PID,
52    },
53
54    AppCrashed {
55        #[rkyv(with = WithAppId)]
56        app_id: AppId,
57        pid: PID,
58        launched_by: PID,
59        exit_code: u32,
60        panic_message: Option<String>,
61    },
62
63    LaunchError {
64        #[rkyv(with = WithAppId)]
65        app_id: AppId,
66        error: LaunchError,
67        launched_by: PID,
68    },
69
70    AppRemoving {
71        #[rkyv(with = WithAppId)]
72        app_id: AppId,
73    },
74
75    AppRemovalFailed {
76        #[rkyv(with = WithAppId)]
77        app_id: AppId,
78        result: RemoveInstalledAppResult,
79    },
80
81    /// A rescan or successful install added, removed, or updated app bundles.
82    ///
83    /// `installed`: covers both app ids that weren't in the registry before and app
84    /// ids whose bundle changed
85    /// `removed` covers app ids no longer found
86    AppSetChanged {
87        #[rkyv(with = rkyv::with::Map<WithAppId>)]
88        installed: Vec<AppId>,
89        #[rkyv(with = rkyv::with::Map<WithAppId>)]
90        removed: Vec<AppId>,
91    },
92
93    AllowedPublishersChanged,
94}
95
96#[derive(Debug, server::Message)]
97pub struct LaunchApp(pub AppId);
98
99#[derive(Debug, server::Message)]
100pub struct RemoveApp(pub AppId);
101
102#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
103pub struct AppQrMatchRules {
104    #[rkyv(with = WithAppId)]
105    pub id: AppId,
106    pub rules_json: Vec<u8>,
107}
108
109/// One permission subgroup of an app, the unit the user sees, approves, and denies. The
110/// individual messages behind it stay internal to the OS.
111#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
112pub struct InstalledAppPermissionSubgroup {
113    pub key: String,
114    pub label: String,
115    pub approved: bool,
116}
117
118/// A top-level permission group (the part of a subgroup key before the first `.`), under
119/// which the permission UI collapses its subgroups.
120#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
121pub struct InstalledAppPermissionGroup {
122    pub key: String,
123    pub label: String,
124    pub subgroups: Vec<InstalledAppPermissionSubgroup>,
125}
126
127#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
128pub struct InstalledAppInfo {
129    pub app_id: String,
130    pub name: String,
131    /// Short fingerprint of the third-party publisher that signed the app, whether or not its
132    /// certificate currently allows a launch; empty for built-in apps and for a signer no stored
133    /// certificate matches.
134    pub publisher_fingerprint: String,
135    /// Publisher display name: the certificate name the user confirmed at import for a
136    /// certified third-party app, the manifest's publisher for a Foundation-signed app,
137    /// empty otherwise.
138    pub publisher_name: String,
139    /// Whether the bundle carries a Foundation signature header; the signature itself is
140    /// only verified at launch.
141    pub is_foundation_signed: bool,
142    /// Why launching the app would fail right now, or `None` while it would succeed. The signature
143    /// is only checked by an actual launch, so a launch can still fail with an error this never
144    /// reports.
145    pub launch_error: Option<LaunchError>,
146    pub can_remove: bool,
147    /// Whether this is a Flux child app: it runs inside the Flux emulator, so
148    /// direct-launch affordances (e.g. an Open App button) don't apply to it.
149    pub is_flux: bool,
150    pub version: String,
151    pub size_bytes: u64,
152    /// Sha256 of `app.elf` without its cosign2 header.
153    pub app_hash: [u8; 32],
154    pub description: String,
155    /// Auto-granted permissions (shown but not user-toggleable).
156    pub basic_permissions: Vec<InstalledAppPermissionGroup>,
157    /// Permissions the user can allow or deny.
158    pub approvable_permissions: Vec<InstalledAppPermissionGroup>,
159}
160
161#[derive(Debug, Clone, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
162pub enum SetAppPermissionGrantResult {
163    Updated,
164    AppNotFound,
165    PermissionNotFound,
166    NotUserGrantable,
167    Unauthorized,
168    StorageUnavailable,
169    InternalError,
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
173pub struct PermissionRequestInfo {
174    #[rkyv(with = WithAppId)]
175    pub app_id: AppId,
176    pub app_name: String,
177    /// Subgroup key the grant is recorded under (e.g. `peripherals.camera-use`).
178    pub subgroup: String,
179    /// The subgroup's localized display name, ready to show in the prompt as-is.
180    pub label: String,
181}
182
183#[derive(Debug, Clone, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
184pub enum PermissionRequestInfoResult {
185    Prompt(PermissionRequestInfo),
186    AlreadyApproved,
187    Denied,
188    NotGrantable,
189    AppNotFound,
190    Unauthorized,
191    InternalError,
192}
193
194#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
195pub struct ThirdPartyCertificateInfo {
196    pub name: String,
197    pub company: String,
198    pub contact_email: String,
199    pub support_url: String,
200    /// Compressed secp256k1 public key encoded as lowercase hexadecimal.
201    pub public_key: String,
202    /// SHA-256 of the compressed 33-byte public key, encoded as 64 lowercase hex characters.
203    pub fingerprint: String,
204    /// The first and last four fingerprint bytes separated by an ellipsis.
205    pub short_fingerprint: String,
206    /// When the certificate was first imported, or None if its file carries an unreadable timestamp.
207    pub added_unix_seconds: Option<u64>,
208    pub not_before_unix_seconds: u64,
209    pub not_after_unix_seconds: u64,
210    pub serial_number: String,
211    pub issuer: String,
212    pub subject: String,
213    pub basic_constraints: String,
214    pub key_usage: String,
215    pub extended_key_usage: String,
216}
217
218impl ThirdPartyCertificateInfo {
219    /// Whether the device clock falls inside the validity window, the only state in which the
220    /// certificate authorizes an app.
221    pub fn is_usable(&self) -> bool { !self.has_expired() && !self.is_not_yet_valid() }
222
223    pub fn has_expired(&self) -> bool { now_unix_seconds() > self.not_after_unix_seconds }
224
225    pub fn is_not_yet_valid(&self) -> bool { now_unix_seconds() < self.not_before_unix_seconds }
226}
227
228/// The device clock, in seconds since the Unix epoch.
229pub fn now_unix_seconds() -> u64 {
230    std::time::SystemTime::now()
231        .duration_since(std::time::UNIX_EPOCH)
232        .map(|elapsed| elapsed.as_secs())
233        .unwrap_or_default()
234}
235
236/// Why a publisher certificate cannot be used. The window variants carry the bound the device clock
237/// falls outside of, so a caller holding no certificate can still name the date.
238#[derive(Debug, Clone, Copy, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
239pub enum ThirdPartyCertificateError {
240    /// Not a certificate this device accepts.
241    Invalid,
242    Expired {
243        not_after_unix_seconds: u64,
244    },
245    NotYetValid {
246        not_before_unix_seconds: u64,
247    },
248    /// The certificate holds a different key than the fingerprint the user confirmed.
249    FingerprintMismatch,
250    Internal,
251}
252
253#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
254pub enum RemoveThirdPartyCertificateResult {
255    Removed,
256    NotFound,
257    AppRequiresKey(String),
258    InternalError,
259}
260
261#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
262pub enum RemoveInstalledAppResult {
263    Removed,
264    NotSideloaded,
265    /// The Flux emulator cannot be removed while Flux apps are installed; remove those first.
266    FluxAppsInstalled,
267    InternalError,
268}
269
270#[derive(Debug, Clone, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
271#[response(Option<String>)]
272pub enum GetAppName {
273    ByAppId {
274        #[rkyv(with = WithAppId)]
275        id: AppId,
276        locale: String,
277    },
278
279    ByPid {
280        pid: PID,
281        locale: String,
282    },
283}
284
285impl GetAppName {
286    pub fn new_by_app_id(id: &AppId, locale: &str) -> Self {
287        Self::ByAppId { id: *id, locale: locale.to_string() }
288    }
289
290    pub fn new_by_pid(pid: PID, locale: &str) -> Self { Self::ByPid { pid, locale: locale.to_string() } }
291}
292
293#[derive(Debug, Clone, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
294#[response(Vec<AppQrMatchRules>)]
295/// limits the response to the listed apps; an empty list returns all apps.
296pub struct GetQrMatchRules {
297    #[rkyv(with = rkyv::with::Map<WithAppId>)]
298    pub app_ids: Vec<AppId>,
299}
300
301/// Filter applied by [`ListApps`]. A `None` axis matches either value; the axes are
302/// independent, so a Flux app may be built-in or sideloaded.
303#[derive(Debug, Clone, Default, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
304pub struct AppFilter {
305    pub is_flux: Option<bool>,
306    /// `true` matches apps installed under the sideload root, whatever signed them; `false`
307    /// built-ins shipped with the firmware.
308    pub sideloaded: Option<bool>,
309}
310
311impl AppFilter {
312    /// Filter to non-Flux apps.
313    pub fn standard_only() -> Self { Self { is_flux: Some(false), ..Default::default() } }
314
315    /// Filter to Flux child apps only.
316    pub fn flux_only() -> Self { Self { is_flux: Some(true), ..Default::default() } }
317
318    /// Filter to sideloaded apps only.
319    pub fn sideloaded_only() -> Self { Self { sideloaded: Some(true), ..Default::default() } }
320}
321
322#[derive(Debug, Clone, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
323#[response(Vec<InstalledAppInfo>)]
324pub struct ListApps {
325    pub locale: String,
326    pub filter: AppFilter,
327}
328
329/// Which themed variant of an app's bundled icon to read.
330#[derive(Debug, Clone, Copy, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
331pub enum IconVariant {
332    Light,
333    Dark,
334}
335
336/// Fetch the raw bytes of a single app's bundled icon, keyed by its hex app id
337/// (as returned in [`InstalledAppInfo::app_id`]). Returns `None` when the app
338/// is unknown, has no bundled icon, or the icon cannot be read. A dark-variant
339/// request falls back to the light icon for apps that ship no dark icon.
340#[derive(Debug, Clone, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
341#[response(Option<Vec<u8>>)]
342pub struct GetAppIcon {
343    pub app_id: String,
344    pub variant: IconVariant,
345}
346
347/// How the user answered a permission prompt (or moved a Settings toggle) for one
348/// permission subgroup of an app.
349#[derive(Debug, Clone, Copy, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
350pub enum PermissionGrantDecision {
351    /// Persist an approval ("Allow Always").
352    Allow,
353    /// Persist a denial ("Never Allow").
354    Deny,
355    /// Deny for the current run only ("Not Now"): the broker auto-denies further requests
356    /// for the same subgroup without re-prompting until the app is relaunched.
357    /// Not persisted; cleared when the app next launches.
358    DenyForRun,
359}
360
361#[derive(Debug, Clone, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
362#[response(SetAppPermissionGrantResult)]
363pub struct SetAppPermissionGrant {
364    pub app_id: String,
365    /// Subgroup key (e.g. `peripherals.camera-use`); the grant covers every message in it.
366    pub subgroup: String,
367    pub decision: PermissionGrantDecision,
368}
369
370#[derive(Debug, Clone, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
371#[response(PermissionRequestInfoResult)]
372pub struct GetPermissionRequestInfo {
373    /// The requesting app's id, captured by the kernel when the request was parked, so it is
374    /// stable even if the sender exits and its pid is recycled before the broker asks.
375    pub sender_app_id: [u8; 16],
376    /// The target server's SID as captured by the kernel when the request was parked; it
377    /// identifies the exact server even when one process hosts several.
378    pub server_sid: [u32; 4],
379    pub message_id: usize,
380    pub locale: String,
381}
382
383#[derive(Debug, Clone, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
384#[response(Vec<ThirdPartyCertificateInfo>)]
385pub struct GetThirdPartyCertificates;
386
387#[derive(Debug, Clone, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
388#[response(Result<ThirdPartyCertificateInfo, ThirdPartyCertificateError>)]
389pub struct PreviewThirdPartyCertificate {
390    pub certificate_pem: Vec<u8>,
391}
392
393#[derive(Debug, Clone, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
394#[response(Result<ThirdPartyCertificateInfo, ThirdPartyCertificateError>)]
395pub struct ImportThirdPartyCertificate {
396    pub certificate_pem: Vec<u8>,
397    /// The fingerprint the user was shown and accepted. Callers must state it, so a publisher can
398    /// only be allowed under the identity that was actually confirmed.
399    pub expected_fingerprint: String,
400}
401
402#[derive(Debug, Clone, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
403#[response(RemoveThirdPartyCertificateResult)]
404pub struct RemoveThirdPartyCertificate {
405    pub fingerprint: String,
406    pub locale: String,
407}
408
409/// Storage an app archive may be installed from: the places a user can put a file, and no
410/// system location. It bounds what [`InstallAppArchive`] can be pointed at, so a caller cannot
411/// walk app-manager through the system volume with a crafted path.
412#[derive(Debug, Clone, Copy, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
413pub enum ArchiveLocation {
414    Internal,
415    Usb,
416    Airlock,
417}
418
419#[derive(Debug, Clone, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
420pub struct InstallAppArchiveResult {
421    pub app_name: String,
422}
423
424/// Why an install did not happen. Every variant is a state the archive or the device is in, so
425/// the caller can say something specific without the server sending it a string.
426#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
427pub enum InstallError {
428    /// The file is not a readable app archive, or breaks the archive format's rules.
429    NotAnApp,
430    /// The archive's manifest carries no valid publisher signature.
431    InvalidSignature,
432    /// A Flux app, which only the Flux emulator can run, and the emulator is not installed.
433    FluxEmulatorMissing,
434    /// The archive claims the app id of an app that ships with the firmware, which no
435    /// installed app may replace.
436    BuiltInApp,
437    /// An app with this id is installed, but from another publisher, so this archive would be a
438    /// different app taking over its permission grants and stored data rather than an update.
439    PublisherMismatch,
440    /// The app is already installed and running, so its bundle cannot be replaced.
441    AppRunning,
442    /// The filesystem refused an operation the install needed.
443    Fs(fs::Error),
444    /// Anything else; the server logs what actually happened.
445    Internal,
446}
447
448/// Install an app from an archive the user picked on local storage.
449#[derive(Debug, Clone, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
450#[response(Result<InstallAppArchiveResult, InstallError>)]
451pub struct InstallAppArchive {
452    pub path: String,
453    pub location: ArchiveLocation,
454    /// Locale for the installed app's name in the response.
455    pub locale: String,
456}