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
18impl AsScalar<3> for AppManagerError {
19    fn as_scalar(&self) -> [u32; 3] { [self.to_u32().unwrap(), 0, 0] }
20}
21
22impl FromScalar<3> for AppManagerError {
23    fn from_scalar([e, ..]: [u32; 3]) -> Self {
24        AppManagerError::from_u32(e).unwrap_or(AppManagerError::InternalError)
25    }
26}
27
28#[derive(Debug, Clone, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
29#[event(AppEvent)]
30pub struct SubscribeAppEvents;
31
32#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
33pub enum AppEvent {
34    AppLaunched {
35        #[rkyv(with = WithAppId)]
36        app_id: AppId,
37        pid: PID,
38        launched_by: PID,
39    },
40
41    AppCrashed {
42        #[rkyv(with = WithAppId)]
43        app_id: AppId,
44        pid: PID,
45        launched_by: PID,
46        exit_code: u32,
47        panic_message: Option<String>,
48    },
49
50    LaunchError {
51        #[rkyv(with = WithAppId)]
52        app_id: AppId,
53        error: LaunchError,
54    },
55
56    /// A rescan (triggered by `RefreshInstalledApps` or `RemoveInstalledApp`) added, removed, or
57    /// updated apps
58    ///
59    /// `installed`: covers both app ids that weren't in the registry before and app
60    /// ids that were already registered but whose manifest changed
61    /// `removed` covers app ids no longer found
62    AppSetChanged {
63        #[rkyv(with = rkyv::with::Map<WithAppId>)]
64        installed: Vec<AppId>,
65        #[rkyv(with = rkyv::with::Map<WithAppId>)]
66        removed: Vec<AppId>,
67    },
68}
69
70#[derive(Debug, server::Message)]
71pub struct LaunchApp(pub AppId);
72#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
73pub struct AppQrMatchRules {
74    #[rkyv(with = WithAppId)]
75    pub id: AppId,
76    pub rules_json: Vec<u8>,
77}
78
79/// One permission subgroup of an app, the unit the user sees, approves, and denies. The
80/// individual messages behind it stay internal to the OS.
81#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
82pub struct InstalledAppPermissionSubgroup {
83    pub key: String,
84    pub label: String,
85    pub approved: bool,
86}
87
88/// A top-level permission group (the part of a subgroup key before the first `.`), under
89/// which the permission UI collapses its subgroups.
90#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
91pub struct InstalledAppPermissionGroup {
92    pub key: String,
93    pub label: String,
94    pub subgroups: Vec<InstalledAppPermissionSubgroup>,
95}
96
97#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
98pub struct InstalledAppInfo {
99    pub app_id: String,
100    pub name: String,
101    pub publisher: String,
102    pub can_launch: bool,
103    pub can_remove: bool,
104    pub version: String,
105    pub size_bytes: u64,
106    pub description: String,
107    /// Auto-granted permissions (shown but not user-toggleable).
108    pub basic_permissions: Vec<InstalledAppPermissionGroup>,
109    /// Permissions the user can allow or deny.
110    pub approvable_permissions: Vec<InstalledAppPermissionGroup>,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
114pub enum SetAppPermissionGrantResult {
115    Updated,
116    AppNotFound,
117    PermissionNotFound,
118    NotUserGrantable,
119    Unauthorized,
120    StorageUnavailable,
121    InternalError,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
125pub struct PermissionRequestInfo {
126    #[rkyv(with = WithAppId)]
127    pub app_id: AppId,
128    pub app_name: String,
129    /// Subgroup key the grant is recorded under (e.g. `peripherals.camera-use`).
130    pub subgroup: String,
131    /// User-facing name of the subgroup, shown in the prompt.
132    pub label: String,
133}
134
135#[derive(Debug, Clone, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
136pub enum PermissionRequestInfoResult {
137    Prompt(PermissionRequestInfo),
138    AlreadyApproved,
139    Denied,
140    NotGrantable,
141    AppNotFound,
142    Unauthorized,
143    InternalError,
144}
145
146#[derive(
147    Debug, Clone, serde::Serialize, serde::Deserialize, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize,
148)]
149pub struct ThirdPartyCertificateInfo {
150    pub name: String,
151    pub company: String,
152    pub contact_email: String,
153    pub support_url: String,
154    pub public_key: String,
155    #[serde(default)]
156    pub not_before_unix_seconds: Option<u64>,
157    #[serde(default)]
158    pub not_after_unix_seconds: Option<u64>,
159    pub serial_number: String,
160    pub issuer: String,
161    pub subject: String,
162    pub basic_constraints: String,
163    pub key_usage: String,
164    pub extended_key_usage: String,
165}
166
167impl ThirdPartyCertificateInfo {
168    /// Whether the current time falls within the certificate's validity window. A missing bound or
169    /// an unreadable clock counts as invalid.
170    pub fn is_currently_valid(&self) -> bool {
171        let Ok(elapsed) = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) else {
172            return false;
173        };
174        let now = elapsed.as_secs();
175        matches!(
176            (self.not_before_unix_seconds, self.not_after_unix_seconds),
177            (Some(not_before), Some(not_after)) if not_before <= now && now <= not_after
178        )
179    }
180}
181
182#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
183pub enum ImportThirdPartyCertificateResult {
184    Imported(ThirdPartyCertificateInfo),
185    Invalid,
186}
187
188#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
189pub enum RemoveThirdPartyCertificateResult {
190    Removed,
191    NotFound,
192    AppRequiresKey(String),
193    InternalError,
194}
195
196#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
197pub enum RemoveInstalledAppResult {
198    Removed,
199    NotFound,
200    NotSideloaded,
201    Running,
202    InternalError,
203}
204
205#[derive(Debug, Clone, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
206#[response(Option<String>)]
207pub enum GetAppName {
208    ByAppId {
209        #[rkyv(with = WithAppId)]
210        id: AppId,
211        locale: String,
212    },
213
214    ByPid {
215        pid: PID,
216        locale: String,
217    },
218}
219
220impl GetAppName {
221    pub fn new_by_app_id(id: &AppId, locale: &str) -> Self {
222        Self::ByAppId { id: *id, locale: locale.to_string() }
223    }
224
225    pub fn new_by_pid(pid: PID, locale: &str) -> Self { Self::ByPid { pid, locale: locale.to_string() } }
226}
227
228#[derive(Debug, Clone, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
229#[response(Vec<AppQrMatchRules>)]
230pub struct GetQrMatchRules;
231
232/// Filter applied by [`ListApps`]. A `None` axis matches either value; the axes are
233/// independent, so a Flux app may be built-in or sideloaded.
234#[derive(Debug, Clone, Default, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
235pub struct AppFilter {
236    pub is_flux: Option<bool>,
237    pub third_party: Option<bool>,
238}
239
240impl AppFilter {
241    /// Filter to non-Flux apps.
242    pub fn standard_only() -> Self { Self { is_flux: Some(false), ..Default::default() } }
243
244    /// Filter to Flux child apps only.
245    pub fn flux_only() -> Self { Self { is_flux: Some(true), ..Default::default() } }
246
247    /// Filter to sideloaded third-party apps only.
248    pub fn third_party_only() -> Self { Self { third_party: Some(true), ..Default::default() } }
249}
250
251#[derive(Debug, Clone, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
252#[response(Vec<InstalledAppInfo>)]
253pub struct ListApps {
254    pub locale: String,
255    pub filter: AppFilter,
256}
257
258/// Fetch the raw bytes of a single app's bundled icon, keyed by its hex app id
259/// (as returned in [`InstalledAppInfo::app_id`]). Returns `None` when the app
260/// is unknown, has no bundled icon, or the icon cannot be read.
261#[derive(Debug, Clone, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
262#[response(Option<Vec<u8>>)]
263pub struct GetAppIcon {
264    pub app_id: String,
265}
266
267/// How the user answered a permission prompt (or moved a Settings toggle) for one
268/// permission subgroup of an app.
269#[derive(Debug, Clone, Copy, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
270pub enum PermissionGrantDecision {
271    /// Persist an approval ("Allow Always").
272    Allow,
273    /// Persist a denial ("Never Allow").
274    Deny,
275    /// Deny for the current run only ("Not Now"): the broker auto-denies further requests
276    /// for the same subgroup without re-prompting until the app is relaunched.
277    /// Not persisted; cleared when the app next launches.
278    DenyForRun,
279}
280
281#[derive(Debug, Clone, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
282#[response(SetAppPermissionGrantResult)]
283pub struct SetAppPermissionGrant {
284    pub app_id: String,
285    /// Subgroup key (e.g. `peripherals.camera-use`); the grant covers every message in it.
286    pub subgroup: String,
287    pub decision: PermissionGrantDecision,
288}
289
290#[derive(Debug, Clone, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
291#[response(PermissionRequestInfoResult)]
292pub struct GetPermissionRequestInfo {
293    /// The requesting app's id, captured by the kernel when the request was parked, so it is
294    /// stable even if the sender exits and its pid is recycled before the broker asks.
295    pub sender_app_id: [u8; 16],
296    /// The target server's SID as captured by the kernel when the request was parked; it
297    /// identifies the exact server even when one process hosts several.
298    pub server_sid: [u32; 4],
299    pub message_id: usize,
300    pub locale: String,
301}
302
303#[derive(Debug, Clone, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
304#[response(Vec<ThirdPartyCertificateInfo>)]
305pub struct GetThirdPartyCertificates;
306
307#[derive(Debug, Clone, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
308#[response(ImportThirdPartyCertificateResult)]
309pub struct ImportThirdPartyCertificate {
310    pub certificate_pem: Vec<u8>,
311}
312
313#[derive(Debug, Clone, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
314#[response(RemoveThirdPartyCertificateResult)]
315pub struct RemoveThirdPartyCertificate {
316    pub public_key: String,
317    pub locale: String,
318}
319
320#[derive(Debug, Clone, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
321#[response(RemoveInstalledAppResult)]
322pub struct RemoveInstalledApp {
323    #[rkyv(with = WithAppId)]
324    pub app_id: AppId,
325}