Skip to main content

idlewarden_plugin_api/
capability.rs

1// SPDX-License-Identifier: Apache-2.0
2//! What a plugin is allowed to do, and how much the user is asked to trust it.
3
4use serde::{Deserialize, Serialize};
5
6/// Declared in the manifest, shown to the user at install time, enforced by the
7/// host. A plugin that asks for nothing can still observe and act through the
8/// Core, capabilities gate *direct* access to the machine.
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(tag = "kind", rename_all = "snake_case")]
11pub enum Capability {
12    /// Read captured frames of the game window.
13    Capture,
14    /// Emit mouse commands.
15    InputMouse,
16    /// Emit keyboard commands.
17    InputKeyboard,
18    /// Emit virtual gamepad commands (requires a third-party driver).
19    InputGamepad,
20    /// Read files under a named, user-approved directory (e.g. a save folder).
21    FsRead { path: String },
22    /// Reach the network. Never granted to `Unverified` plugins by default.
23    Net { host: String },
24    /// Talk to a mod the user installed in the game process (ADR-0014). Never
25    /// granted silently, at any trust level.
26    Bridge { name: String },
27}
28
29impl Capability {
30    pub fn label(&self) -> String {
31        match self {
32            Capability::Capture => "capture".into(),
33            Capability::InputMouse => "input.mouse".into(),
34            Capability::InputKeyboard => "input.keyboard".into(),
35            Capability::InputGamepad => "input.gamepad".into(),
36            Capability::FsRead { path } => format!("fs.read:{path}"),
37            Capability::Net { host } => format!("net:{host}"),
38            Capability::Bridge { name } => format!("bridge:{name}"),
39        }
40    }
41}
42
43/// How the plugin reached the user's machine. Drives which capabilities are
44/// granted without asking, and whether auto-update is allowed.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
46#[serde(rename_all = "snake_case")]
47pub enum TrustLevel {
48    /// Installed from a local file or arbitrary URL. Loud warning, no
49    /// auto-update, no capability granted without an explicit click.
50    Unverified,
51    /// Reviewed via a registry pull request, signed by a registered author key.
52    Verified,
53    /// Built and signed by the IdleWarden project itself.
54    Official,
55}
56
57impl TrustLevel {
58    pub fn allows_auto_update(self) -> bool {
59        self >= TrustLevel::Verified
60    }
61
62    /// Capabilities granted without an explicit per-capability prompt.
63    ///
64    /// A bridge puts code the Core cannot inspect inside the game process, so
65    /// it is excluded here for every trust level, `Official` included.
66    pub fn grants_silently(self, cap: &Capability) -> bool {
67        if matches!(cap, Capability::Bridge { .. }) {
68            return false;
69        }
70        match self {
71            TrustLevel::Official => true,
72            TrustLevel::Verified => !matches!(cap, Capability::Net { .. }),
73            TrustLevel::Unverified => false,
74        }
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    const LEVELS: [TrustLevel; 3] = [
83        TrustLevel::Unverified,
84        TrustLevel::Verified,
85        TrustLevel::Official,
86    ];
87
88    #[test]
89    fn a_bridge_is_never_granted_silently() {
90        let bridge = Capability::Bridge {
91            name: "cookie".into(),
92        };
93        for level in LEVELS {
94            assert!(
95                !level.grants_silently(&bridge),
96                "{level:?} granted a bridge silently"
97            );
98        }
99    }
100
101    #[test]
102    fn an_official_plugin_still_gets_everything_else_silently() {
103        assert!(TrustLevel::Official.grants_silently(&Capability::Capture));
104        assert!(TrustLevel::Official.grants_silently(&Capability::Net { host: "x".into() }));
105    }
106
107    #[test]
108    fn capability_labels_round_trip_into_the_registry_pattern() {
109        assert_eq!(
110            Capability::Bridge {
111                name: "cookie".into()
112            }
113            .label(),
114            "bridge:cookie"
115        );
116        assert_eq!(Capability::InputMouse.label(), "input.mouse");
117    }
118}