Skip to main content

core_api/host/
permissions.rs

1//! OS authorization for one computer's Host and the services it runs; independent
2//! of any Space.
3//!
4//! Services declare requirements and report observations from the processes that
5//! actually access the resource. An executing process is not necessarily a
6//! separate OS authorization identity: helpers can share a responsible app.
7//! Host reports one row per component and permission. Reading a report must never request authorization.
8//! A previous access probe is historical evidence, not a current OS setting.
9use serde::{Deserialize, Serialize};
10use std::collections::BTreeMap;
11
12pub type PlatformPermissionRequirements = BTreeMap<String, Vec<PermissionRequirement>>;
13
14/// Automation is authorized per target application, not as one global switch.
15#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
16#[serde(tag = "id", rename_all = "camelCase", deny_unknown_fields)]
17pub enum PermissionKey {
18    LocalNetwork {},
19    Bluetooth {},
20    Microphone {},
21    Reminders {},
22    Automation {
23        #[serde(rename = "targetBundleId")]
24        target_bundle_id: String,
25    },
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "camelCase", deny_unknown_fields)]
30pub struct PermissionRequirement {
31    pub permission: PermissionKey,
32    /// Human-readable affected capability; denial need not disable the service.
33    pub feature: String,
34    pub reason: String,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "camelCase")]
39pub enum PermissionState {
40    Unknown,
41    NotDetermined,
42    Granted,
43    /// This platform has no per-application consent for this resource. This says
44    /// nothing about device, account, session, sandbox or system-policy access.
45    NotRequired,
46    Denied,
47    Restricted,
48    Unsupported,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "camelCase")]
53pub enum PermissionEvidence {
54    /// A passive OS authorization query, not hardware availability.
55    System,
56    /// Platform semantics, e.g. Linux has no TCC consent for Bluetooth.
57    /// This is not evidence that an operation or device is available.
58    Platform,
59    /// A previous explicit request/probe; observedAtMs is mandatory.
60    Probe,
61    Unavailable,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
65#[serde(rename_all = "camelCase")]
66pub enum PermissionAction {
67    Request,
68    OpenSettings,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
72#[serde(rename_all = "camelCase", deny_unknown_fields)]
73pub struct PermissionObservation {
74    pub permission: PermissionKey,
75    /// PID of the process where this observation was made, not its OS grant owner.
76    pub process_id: u32,
77    pub state: PermissionState,
78    pub evidence: PermissionEvidence,
79    pub observed_at_ms: Option<i64>,
80    pub error: Option<String>,
81}
82
83impl PermissionObservation {
84    pub fn is_satisfied(&self) -> bool {
85        self.process_id != 0
86            && self.error.is_none()
87            && self.evidence != PermissionEvidence::Unavailable
88            && (self.evidence != PermissionEvidence::Probe || self.observed_at_ms.is_some())
89            && matches!(
90                self.state,
91                PermissionState::Granted | PermissionState::NotRequired
92            )
93    }
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
97#[serde(rename_all = "camelCase", deny_unknown_fields)]
98pub struct ServicePermissions {
99    /// Reporting service PID; individual observations may come from its sidecar.
100    pub process_id: u32,
101    pub permissions: Vec<PermissionObservation>,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
105#[serde(rename_all = "camelCase", deny_unknown_fields)]
106pub struct HostPermission {
107    pub component_id: String,
108    pub component_name: String,
109    pub permission: PermissionKey,
110    pub feature: String,
111    pub reason: String,
112    /// None means that the actual executor could not be observed.
113    pub observation: Option<PermissionObservation>,
114    /// Collection failure, distinct from an observed OS denial.
115    pub error: Option<String>,
116    /// Mechanisms supported locally; remote callers can only read status.
117    pub supported_actions: Vec<PermissionAction>,
118}
119
120impl HostPermission {
121    pub fn is_satisfied(&self) -> bool {
122        self.error.is_none()
123            && self.observation.as_ref().is_some_and(|observation| {
124                observation.permission == self.permission && observation.is_satisfied()
125            })
126    }
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
130#[serde(rename_all = "camelCase", deny_unknown_fields)]
131pub struct PermissionDeclarationError {
132    pub component_id: String,
133    pub message: String,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
137#[serde(rename_all = "camelCase", deny_unknown_fields)]
138pub struct HostPermissions {
139    pub host_id: String,
140    pub host_version: String,
141    pub platform: String,
142    pub observed_at_ms: i64,
143    pub permissions: Vec<HostPermission>,
144    /// Missing, malformed or unsupported declarations must remain visible.
145    /// An empty permission list is not proof that the inventory is complete.
146    pub declaration_errors: Vec<PermissionDeclarationError>,
147}
148
149/// Local-control mutation. A Hub may later relay `GET /v1/permissions`; it must
150/// not relay this request, and it does not store or decide the grant. The server
151/// must verify the local control credential and Host identity. This DTO has no
152/// caller-controlled isLocal flag or arbitrary Settings URL. The component
153/// selects an installed service (or "host"); the key selects its permission.
154/// The component is always explicit.
155#[derive(Debug, Clone, Serialize, Deserialize)]
156#[serde(rename_all = "camelCase", deny_unknown_fields)]
157pub struct HostPermissionRequest {
158    pub host_id: String,
159    pub component_id: String,
160    pub permission: PermissionKey,
161}
162
163pub const HOST_PERMISSIONS_PATH: &str = "/v1/permissions";
164pub const HOST_PERMISSION_REQUEST_PATH: &str = "/internal/permissions/request";
165pub const HOST_PERMISSION_SETTINGS_PATH: &str = "/internal/permissions/open-settings";