Skip to main content

rightkit_agent_engine_api_integrations/
plugin_signature.rs

1//! Per-plugin signature provenance records.
2
3use serde::{Deserialize, Serialize};
4use std::path::{Path, PathBuf};
5
6#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
7#[serde(rename_all = "lowercase")]
8pub enum SignatureStatus {
9    Signed,
10    Unsigned,
11    Unknown,
12}
13
14#[derive(Clone, Debug, Serialize, Deserialize)]
15#[serde(rename_all = "camelCase")]
16pub struct PluginSignatureRecord {
17    pub status: SignatureStatus,
18    #[serde(default)]
19    pub signed_by: String,
20    #[serde(default)]
21    pub signing_public_key: String,
22    #[serde(default)]
23    pub signature: String,
24    #[serde(default)]
25    pub installed_at: String,
26    #[serde(default)]
27    pub install_source: String,
28}
29
30impl PluginSignatureRecord {
31    pub fn signed(
32        signed_by: String,
33        signing_public_key: String,
34        signature: String,
35        install_source: String,
36        now_iso: String,
37    ) -> Self {
38        Self {
39            status: SignatureStatus::Signed,
40            signed_by,
41            signing_public_key,
42            signature,
43            installed_at: now_iso,
44            install_source,
45        }
46    }
47
48    pub fn unsigned(install_source: String, now_iso: String) -> Self {
49        Self {
50            status: SignatureStatus::Unsigned,
51            signed_by: String::new(),
52            signing_public_key: String::new(),
53            signature: String::new(),
54            installed_at: now_iso,
55            install_source,
56        }
57    }
58}
59
60pub fn sidecar_path(plugin_install_dir: &Path) -> PathBuf {
61    plugin_install_dir.join(".signature.json")
62}
63
64pub fn read_record(plugin_install_dir: &Path) -> PluginSignatureRecord {
65    let path = sidecar_path(plugin_install_dir);
66    match std::fs::read_to_string(&path) {
67        Ok(raw) => serde_json::from_str(&raw).unwrap_or_else(|_| unknown_record()),
68        Err(_) => unknown_record(),
69    }
70}
71
72pub fn write_record(plugin_install_dir: &Path, record: &PluginSignatureRecord) {
73    let path = sidecar_path(plugin_install_dir);
74    match serde_json::to_string_pretty(record) {
75        Ok(json) => {
76            if let Err(error) = std::fs::write(&path, json) {
77                eprintln!(
78                    "[plugin-signature] failed to write provenance to {}: {error}",
79                    path.display()
80                );
81            }
82        }
83        Err(error) => eprintln!("[plugin-signature] serialise record failed: {error}"),
84    }
85}
86
87pub fn is_acceptable(record: &PluginSignatureRecord, policy_reject_unsigned: bool) -> bool {
88    match record.status {
89        SignatureStatus::Signed | SignatureStatus::Unknown => true,
90        SignatureStatus::Unsigned => !policy_reject_unsigned,
91    }
92}
93
94fn unknown_record() -> PluginSignatureRecord {
95    PluginSignatureRecord {
96        status: SignatureStatus::Unknown,
97        signed_by: String::new(),
98        signing_public_key: String::new(),
99        signature: String::new(),
100        installed_at: String::new(),
101        install_source: String::new(),
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    fn scratch() -> PathBuf {
110        let dir = std::env::temp_dir().join(format!(
111            "rk-plugin-signature-{}-{}",
112            std::process::id(),
113            std::time::SystemTime::now()
114                .duration_since(std::time::UNIX_EPOCH)
115                .unwrap()
116                .as_nanos()
117        ));
118        std::fs::create_dir_all(&dir).unwrap();
119        dir
120    }
121
122    #[test]
123    fn a_written_record_reads_back() {
124        let dir = scratch();
125        let record = PluginSignatureRecord::signed(
126            "Acme".into(),
127            "pk".into(),
128            "sig".into(),
129            "marketplace".into(),
130            "2026-11-02T00:00:00Z".into(),
131        );
132        write_record(&dir, &record);
133        assert_eq!(sidecar_path(&dir), dir.join(".signature.json"));
134        let back = read_record(&dir);
135        assert_eq!(back.status, SignatureStatus::Signed);
136        assert_eq!(back.signed_by, "Acme");
137        assert_eq!(back.signing_public_key, "pk");
138        assert_eq!(back.signature, "sig");
139        assert_eq!(back.install_source, "marketplace");
140        assert_eq!(back.installed_at, "2026-11-02T00:00:00Z");
141        let raw = std::fs::read_to_string(sidecar_path(&dir)).unwrap();
142        assert!(raw.contains("\"status\": \"signed\""), "{raw}");
143        assert!(raw.contains("\"signedBy\": \"Acme\""), "{raw}");
144        let _ = std::fs::remove_dir_all(&dir);
145    }
146
147    #[test]
148    fn a_missing_or_corrupt_sidecar_reads_as_unknown() {
149        let dir = scratch();
150        assert_eq!(read_record(&dir).status, SignatureStatus::Unknown);
151        std::fs::write(sidecar_path(&dir), "{ nope").unwrap();
152        let record = read_record(&dir);
153        assert_eq!(record.status, SignatureStatus::Unknown);
154        assert!(record.signed_by.is_empty());
155        let _ = std::fs::remove_dir_all(&dir);
156    }
157
158    #[test]
159    fn only_unsigned_plugins_are_refused_and_only_under_the_policy() {
160        let unsigned = PluginSignatureRecord::unsigned("local".into(), "t".into());
161        assert_eq!(unsigned.status, SignatureStatus::Unsigned);
162        assert!(unsigned.signed_by.is_empty());
163        assert!(is_acceptable(&unsigned, false));
164        assert!(!is_acceptable(&unsigned, true));
165        let signed = PluginSignatureRecord::signed(
166            "A".into(),
167            "k".into(),
168            "s".into(),
169            "m".into(),
170            "t".into(),
171        );
172        assert!(is_acceptable(&signed, true));
173        assert!(is_acceptable(&unknown_record(), true));
174    }
175}