objectiveai_sdk/filesystem/plugins/manifest.rs
1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4use super::Platform;
5
6/// Declarative metadata a plugin ships with itself. The wire shape is
7/// JSON; the on-disk convention (sibling file, embedded resource,
8/// `--manifest` flag, …) is deliberately out of scope of this struct
9/// and will be settled in a follow-up.
10#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
11#[schemars(rename = "filesystem.plugins.Manifest")]
12pub struct Manifest {
13 /// One-line description of what the plugin does. Surfaced in
14 /// listings and the plugin's `--help`-equivalent UI.
15 pub description: String,
16
17 /// Version string. Semver convention is recommended but not
18 /// enforced — the host just displays whatever's here.
19 pub version: String,
20
21 /// Author or authors of the plugin. Free-form string.
22 #[serde(default, skip_serializing_if = "Option::is_none")]
23 #[schemars(extend("omitempty" = true))]
24 pub author: Option<String>,
25
26 /// Homepage or repository URL.
27 #[serde(default, skip_serializing_if = "Option::is_none")]
28 #[schemars(extend("omitempty" = true))]
29 pub homepage: Option<String>,
30
31 /// SPDX license identifier (or any string).
32 #[serde(default, skip_serializing_if = "Option::is_none")]
33 #[schemars(extend("omitempty" = true))]
34 pub license: Option<String>,
35
36 /// Release-asset filename per platform — what the cli should
37 /// download from the GitHub release tagged `v<version>` to install
38 /// the plugin's binary on each platform. Values are filenames
39 /// (e.g. `psyops-linux-x86_64`, `psyops-windows-x86_64.exe`), NOT
40 /// URLs; the URL is composed from the repository + tag + asset
41 /// name elsewhere.
42 ///
43 /// **Every platform field is optional.** Declare entries only for
44 /// the platforms this plugin actually ships a binary for; absent
45 /// platforms are simply not supported by this release. A plugin
46 /// shipping only Linux x86_64 declares one entry; a plugin
47 /// shipping all six declares six. All-None ↔ field omitted in
48 /// the wire shape.
49 #[serde(default, skip_serializing_if = "Binaries::is_empty")]
50 #[schemars(extend("omitempty" = true))]
51 pub binaries: Binaries,
52
53 /// GitHub-release asset filename for the plugin's viewer UI
54 /// bundle (a `.zip` whose root contains `index.html` plus
55 /// assets). When absent, the plugin has no viewer tab and the
56 /// viewer's startup scan ignores it for UI purposes.
57 #[serde(default, skip_serializing_if = "Option::is_none")]
58 #[schemars(extend("omitempty" = true))]
59 pub viewer_zip: Option<String>,
60
61 /// HTTP routes the viewer exposes on behalf of this plugin.
62 /// Each entry registers a handler at
63 /// `/plugin/<repository>/<path>` on the viewer's embedded axum
64 /// server; a hit emits a `PluginRequest { type, value }` event
65 /// to the React frontend, which dispatches to the plugin's
66 /// iframe via the postMessage bridge.
67 #[serde(default, skip_serializing_if = "Vec::is_empty")]
68 pub viewer_routes: Vec<ViewerRoute>,
69
70 /// Plugin author opts in to mobile viewer support by setting
71 /// this. Mobile viewer builds only surface plugins with this
72 /// flag true — mobile has no local backend binary, so plugin
73 /// UIs that require a backend will misbehave unless their
74 /// authors specifically design for "no-backend" mode. Defaults
75 /// to false (desktop-only).
76 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
77 pub mobile_ready: bool,
78}
79
80/// Release-asset filename per platform. Every field is optional;
81/// declare only the platforms a plugin ships for. The wire shape is
82/// a flat JSON object — absent platforms are omitted, never
83/// serialised as `null`.
84///
85/// Exposes a [`Self::get`] method that takes a [`Platform`] enum so
86/// callers can read the asset filename for the current host without
87/// pattern-matching the field set themselves.
88#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
89#[schemars(rename = "filesystem.plugins.Binaries")]
90pub struct Binaries {
91 #[serde(default, skip_serializing_if = "Option::is_none")]
92 #[schemars(extend("omitempty" = true))]
93 pub linux_x86_64: Option<String>,
94
95 #[serde(default, skip_serializing_if = "Option::is_none")]
96 #[schemars(extend("omitempty" = true))]
97 pub linux_aarch64: Option<String>,
98
99 #[serde(default, skip_serializing_if = "Option::is_none")]
100 #[schemars(extend("omitempty" = true))]
101 pub windows_x86_64: Option<String>,
102
103 #[serde(default, skip_serializing_if = "Option::is_none")]
104 #[schemars(extend("omitempty" = true))]
105 pub windows_aarch64: Option<String>,
106
107 #[serde(default, skip_serializing_if = "Option::is_none")]
108 #[schemars(extend("omitempty" = true))]
109 pub macos_x86_64: Option<String>,
110
111 #[serde(default, skip_serializing_if = "Option::is_none")]
112 #[schemars(extend("omitempty" = true))]
113 pub macos_aarch64: Option<String>,
114}
115
116impl Binaries {
117 /// Asset filename for the given platform, if declared.
118 pub fn get(&self, platform: Platform) -> Option<&String> {
119 match platform {
120 Platform::LinuxX86_64 => self.linux_x86_64.as_ref(),
121 Platform::LinuxAarch64 => self.linux_aarch64.as_ref(),
122 Platform::WindowsX86_64 => self.windows_x86_64.as_ref(),
123 Platform::WindowsAarch64 => self.windows_aarch64.as_ref(),
124 Platform::MacosX86_64 => self.macos_x86_64.as_ref(),
125 Platform::MacosAarch64 => self.macos_aarch64.as_ref(),
126 }
127 }
128
129 /// True when no platform has an asset declared.
130 pub fn is_empty(&self) -> bool {
131 self.linux_x86_64.is_none()
132 && self.linux_aarch64.is_none()
133 && self.windows_x86_64.is_none()
134 && self.windows_aarch64.is_none()
135 && self.macos_x86_64.is_none()
136 && self.macos_aarch64.is_none()
137 }
138
139 /// Count of declared platforms.
140 pub fn len(&self) -> usize {
141 [
142 &self.linux_x86_64,
143 &self.linux_aarch64,
144 &self.windows_x86_64,
145 &self.windows_aarch64,
146 &self.macos_x86_64,
147 &self.macos_aarch64,
148 ]
149 .iter()
150 .filter(|o| o.is_some())
151 .count()
152 }
153}
154
155/// One HTTP route a plugin's viewer registers on the host viewer's
156/// embedded axum server. The full path served is
157/// `/plugin/<repository>/<self.path>`; on a hit, the body is
158/// JSON-decoded and forwarded as a `PluginRequest { type: self.type,
159/// value: body }` event to the frontend.
160#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
161#[schemars(rename = "filesystem.plugins.ViewerRoute")]
162pub struct ViewerRoute {
163 /// Path relative to the plugin's namespace. Must start with `/`;
164 /// the host prepends `/plugin/<repository>` before registering.
165 pub path: String,
166
167 /// HTTP method this route handles. Methods other than the listed
168 /// five aren't supported (and don't appear in plugin practice).
169 pub method: HttpMethod,
170
171 /// String tag forwarded to the plugin's iframe as the `type`
172 /// field of the resulting `PluginRequest`. Plugin authors pick
173 /// any value they want; the host doesn't interpret it.
174 #[serde(rename = "type")]
175 pub r#type: String,
176}
177
178/// HTTP methods supported by [`ViewerRoute`]. Serializes as upper-case
179/// (`"GET"`, `"POST"`, …) on the wire.
180#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
181#[schemars(rename = "filesystem.plugins.HttpMethod")]
182#[serde(rename_all = "UPPERCASE")]
183pub enum HttpMethod {
184 Get,
185 Post,
186 Put,
187 Patch,
188 Delete,
189}
190
191/// A [`Manifest`] enriched with the plugin's identifying `name` and
192/// the `source` it was loaded from. Used when listing or describing
193/// installed plugins, where the bare manifest fields are not enough
194/// to identify which plugin they belong to or where they came from.
195///
196/// `name` sits before the manifest body; `source` sits after. The
197/// `manifest` field is `#[serde(flatten)]`-ed so the wire shape is
198/// one flat JSON object — `serde_json`'s `preserve_order` feature
199/// keeps the declared field order, so consumers see `name` first
200/// and `source` last.
201#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
202#[schemars(rename = "filesystem.plugins.ManifestWithNameAndSource")]
203pub struct ManifestWithNameAndSource {
204 /// The plugin's identifier — the filename it lives under in the
205 /// plugins directory (e.g. `psyops` for `~/.objectiveai/plugins/psyops`).
206 pub name: String,
207 #[serde(flatten)]
208 pub manifest: Manifest,
209 /// Where this manifest came from — e.g. an absolute filesystem path,
210 /// a URL, or a registry reference. Free-form string; the host
211 /// just displays it.
212 pub source: String,
213}