Skip to main content

yosh_plugin_api/
lib.rs

1//! Capability declarations and string parsing shared between the host,
2//! the SDK, and the plugin manager. The C ABI types from the dlopen era
3//! are removed; the public WIT contract lives at `wit/yosh-plugin.wit`.
4
5/// Capability bitflag constants. Used by the host's linker construction
6/// (`src/plugin/linker.rs`) to decide which host imports get the real
7/// implementation vs a deny-stub. Also used by the manager to parse
8/// `plugins.toml` `capabilities = [...]` allowlists.
9pub const CAP_VARIABLES_READ: u32 = 0x01;
10pub const CAP_VARIABLES_WRITE: u32 = 0x02;
11pub const CAP_FILESYSTEM: u32 = 0x04;
12pub const CAP_IO: u32 = 0x08;
13pub const CAP_HOOK_PRE_EXEC: u32 = 0x10;
14pub const CAP_HOOK_POST_EXEC: u32 = 0x20;
15pub const CAP_HOOK_ON_CD: u32 = 0x40;
16pub const CAP_HOOK_PRE_PROMPT: u32 = 0x80;
17pub const CAP_FILES_READ: u32 = 0x100;
18pub const CAP_FILES_WRITE: u32 = 0x200;
19pub const CAP_COMMANDS_EXEC: u32 = 0x400;
20
21pub const CAP_ALL: u32 = CAP_VARIABLES_READ
22    | CAP_VARIABLES_WRITE
23    | CAP_FILESYSTEM
24    | CAP_IO
25    | CAP_HOOK_PRE_EXEC
26    | CAP_HOOK_POST_EXEC
27    | CAP_HOOK_ON_CD
28    | CAP_HOOK_PRE_PROMPT
29    | CAP_FILES_READ
30    | CAP_FILES_WRITE
31    | CAP_COMMANDS_EXEC;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub enum Capability {
35    VariablesRead,
36    VariablesWrite,
37    Filesystem,
38    Io,
39    HookPreExec,
40    HookPostExec,
41    HookOnCd,
42    HookPrePrompt,
43    FilesRead,
44    FilesWrite,
45    CommandsExec,
46}
47
48impl Capability {
49    pub fn to_bitflag(self) -> u32 {
50        match self {
51            Capability::VariablesRead => CAP_VARIABLES_READ,
52            Capability::VariablesWrite => CAP_VARIABLES_WRITE,
53            Capability::Filesystem => CAP_FILESYSTEM,
54            Capability::Io => CAP_IO,
55            Capability::HookPreExec => CAP_HOOK_PRE_EXEC,
56            Capability::HookPostExec => CAP_HOOK_POST_EXEC,
57            Capability::HookOnCd => CAP_HOOK_ON_CD,
58            Capability::HookPrePrompt => CAP_HOOK_PRE_PROMPT,
59            Capability::FilesRead => CAP_FILES_READ,
60            Capability::FilesWrite => CAP_FILES_WRITE,
61            Capability::CommandsExec => CAP_COMMANDS_EXEC,
62        }
63    }
64
65    pub fn as_str(self) -> &'static str {
66        match self {
67            Capability::VariablesRead => "variables:read",
68            Capability::VariablesWrite => "variables:write",
69            Capability::Filesystem => "filesystem",
70            Capability::Io => "io",
71            Capability::HookPreExec => "hooks:pre_exec",
72            Capability::HookPostExec => "hooks:post_exec",
73            Capability::HookOnCd => "hooks:on_cd",
74            Capability::HookPrePrompt => "hooks:pre_prompt",
75            Capability::FilesRead => "files:read",
76            Capability::FilesWrite => "files:write",
77            Capability::CommandsExec => "commands:exec",
78        }
79    }
80}
81
82/// Parse a single capability string. Returns `None` for unknown strings;
83/// callers decide whether to log a warning or fail.
84pub fn parse_capability(s: &str) -> Option<Capability> {
85    Some(match s {
86        "variables:read" => Capability::VariablesRead,
87        "variables:write" => Capability::VariablesWrite,
88        "filesystem" => Capability::Filesystem,
89        "io" => Capability::Io,
90        "hooks:pre_exec" => Capability::HookPreExec,
91        "hooks:post_exec" => Capability::HookPostExec,
92        "hooks:on_cd" => Capability::HookOnCd,
93        "hooks:pre_prompt" => Capability::HookPrePrompt,
94        "files:read" => Capability::FilesRead,
95        "files:write" => Capability::FilesWrite,
96        "commands:exec" => Capability::CommandsExec,
97        _ => return None,
98    })
99}
100
101/// Combine a slice of capabilities into a bitfield.
102pub fn capabilities_to_bitflags(caps: &[Capability]) -> u32 {
103    caps.iter().fold(0u32, |acc, c| acc | c.to_bitflag())
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn parse_known_strings() {
112        assert_eq!(parse_capability("io"), Some(Capability::Io));
113        assert_eq!(
114            parse_capability("hooks:pre_prompt"),
115            Some(Capability::HookPrePrompt)
116        );
117    }
118
119    #[test]
120    fn parse_unknown_returns_none() {
121        assert_eq!(parse_capability("variables:execute"), None);
122        assert_eq!(parse_capability(""), None);
123    }
124
125    #[test]
126    fn capability_round_trip() {
127        for cap in [
128            Capability::VariablesRead,
129            Capability::VariablesWrite,
130            Capability::Filesystem,
131            Capability::Io,
132            Capability::HookPreExec,
133            Capability::HookPostExec,
134            Capability::HookOnCd,
135            Capability::HookPrePrompt,
136            Capability::FilesRead,
137            Capability::FilesWrite,
138            Capability::CommandsExec,
139        ] {
140            assert_eq!(parse_capability(cap.as_str()), Some(cap));
141        }
142    }
143
144    #[test]
145    fn cap_all_covers_every_variant() {
146        let bits = capabilities_to_bitflags(&[
147            Capability::VariablesRead,
148            Capability::VariablesWrite,
149            Capability::Filesystem,
150            Capability::Io,
151            Capability::HookPreExec,
152            Capability::HookPostExec,
153            Capability::HookOnCd,
154            Capability::HookPrePrompt,
155            Capability::FilesRead,
156            Capability::FilesWrite,
157            Capability::CommandsExec,
158        ]);
159        assert_eq!(bits, CAP_ALL);
160    }
161
162    #[test]
163    fn parse_files_capabilities() {
164        assert_eq!(parse_capability("files:read"), Some(Capability::FilesRead));
165        assert_eq!(
166            parse_capability("files:write"),
167            Some(Capability::FilesWrite)
168        );
169    }
170
171    #[test]
172    fn files_capabilities_round_trip() {
173        for cap in [Capability::FilesRead, Capability::FilesWrite] {
174            assert_eq!(parse_capability(cap.as_str()), Some(cap));
175        }
176    }
177
178    #[test]
179    fn cap_all_includes_files_bits() {
180        assert_eq!(CAP_ALL & CAP_FILES_READ, CAP_FILES_READ);
181        assert_eq!(CAP_ALL & CAP_FILES_WRITE, CAP_FILES_WRITE);
182    }
183
184    #[test]
185    fn parse_commands_exec_capability() {
186        assert_eq!(
187            parse_capability("commands:exec"),
188            Some(Capability::CommandsExec)
189        );
190    }
191
192    #[test]
193    fn commands_exec_capability_round_trip() {
194        assert_eq!(
195            parse_capability(Capability::CommandsExec.as_str()),
196            Some(Capability::CommandsExec)
197        );
198        assert_eq!(Capability::CommandsExec.as_str(), "commands:exec");
199        assert_eq!(Capability::CommandsExec.to_bitflag(), CAP_COMMANDS_EXEC);
200    }
201
202    #[test]
203    fn cap_all_includes_commands_exec_bit() {
204        assert_eq!(CAP_ALL & CAP_COMMANDS_EXEC, CAP_COMMANDS_EXEC);
205    }
206}
207
208pub mod pattern;