Skip to main content

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}