Skip to main content

hm_plugin_protocol/
manifest.rs

1//! Plugin manifest types. A plugin advertises what it provides by
2//! returning a [`PluginManifest`] from its mandatory `hm_manifest`
3//! export at load time.
4
5use schemars::JsonSchema as DeriveJsonSchema;
6use serde::{Deserialize, Serialize};
7
8use crate::hook::{HookEventKind, HookPhase};
9
10/// JSON Schema fragment (serde-passthrough). Used to validate
11/// plugin-specific config blobs and `runner_args`.
12pub type JsonSchema = serde_json::Value;
13
14/// Clap-derived JSON describing a subcommand's argument schema.
15/// Produced by the SDK helper [`crate::manifest::clap_json_from`]
16/// (added in [`hm-plugin-sdk`]).
17pub type ClapJson = serde_json::Value;
18
19/// Returned by an Extism plugin's `hm_manifest()` export.
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeriveJsonSchema)]
21pub struct PluginManifest {
22    /// Must equal [`crate::HM_PLUGIN_API_VERSION`] or the host rejects
23    /// the plugin at load time.
24    pub api_version: u32,
25    /// Stable plugin identifier, e.g. `harmont-docker`. Used as the
26    /// key in the registry and in error messages.
27    pub name: String,
28    pub version: semver::Version,
29    pub description: String,
30    pub capabilities: Vec<Capability>,
31    /// Host functions the plugin needs. Load fails fast if any are
32    /// not exported by this build of `hm`.
33    pub required_host_fns: Vec<String>,
34    /// Optional JSON Schema describing plugin-specific configuration
35    /// that lives in the project's `.harmont/plugins.toml`.
36    pub config_schema: Option<JsonSchema>,
37    /// HTTPS hosts the plugin is permitted to contact via
38    /// `extism_pdk::http::request`. Defaults to empty (no HTTP).
39    /// The host wires this into extism's per-instance manifest at
40    /// load time; attempting to contact a host not in this list
41    /// fails inside the plugin.
42    #[serde(default)]
43    pub allowed_hosts: Vec<String>,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeriveJsonSchema)]
47#[serde(tag = "kind", rename_all = "snake_case")]
48pub enum Capability {
49    Subcommand(SubcommandSpec),
50    StepExecutor(StepExecutorSpec),
51    LifecycleHook(LifecycleHookSpec),
52    OutputFormatter(OutputFormatterSpec),
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeriveJsonSchema)]
56pub struct SubcommandSpec {
57    /// Top-level verb under `hm`. Two plugins may not claim the
58    /// same `verb`.
59    pub verb: String,
60    pub about: String,
61    /// Clap-shaped JSON for argument parsing (the host re-parses on
62    /// the plugin's behalf via `clap`).
63    pub args_schema: ClapJson,
64    pub subcommands: Vec<Self>,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeriveJsonSchema)]
68pub struct StepExecutorSpec {
69    /// Matched against `CommandStep.runner` at dispatch time.
70    pub runner: String,
71    /// At most one plugin may set `default: true`. The host runs that
72    /// executor when a step omits `runner`.
73    pub default: bool,
74    /// Optional JSON Schema for `CommandStep.runner_args`. The host
75    /// validates `runner_args` against this schema before dispatch.
76    pub step_schema: Option<JsonSchema>,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeriveJsonSchema)]
80pub struct LifecycleHookSpec {
81    pub events: Vec<HookEventKind>,
82    pub phase: HookPhase,
83    pub timeout_ms: u32,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeriveJsonSchema)]
87pub struct OutputFormatterSpec {
88    /// Selected via `--format <name>` on the command line.
89    pub name: String,
90    /// Advisory MIME type written into `--format <name> --output <file>` headers.
91    pub mime: String,
92}
93
94#[cfg(test)]
95#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn capability_tagged_serialization() {
101        let cap = Capability::StepExecutor(StepExecutorSpec {
102            runner: "docker".into(),
103            default: true,
104            step_schema: None,
105        });
106        let s = serde_json::to_string(&cap).unwrap();
107        assert!(s.contains(r#""kind":"step_executor""#), "got: {s}");
108        assert!(s.contains(r#""runner":"docker""#), "got: {s}");
109    }
110}