| 1 | //! Versioned plugin activation policy. |
| 2 | //! |
| 3 | //! This is the single source of truth for which reviewed component adapters |
| 4 | //! this Codewhale build will execute. Compatibility, `active()` decisions, |
| 5 | //! consumption-boundary checks, and the capability hash all read this policy. |
| 6 | //! Enabling a new adapter later must change the policy (version and/or mask) |
| 7 | //! so existing trust receipts fail closed as `CapabilitiesChanged`. |
| 8 | |
| 9 | use sha2::Digest; |
| 10 | |
| 11 | /// Capability-hash domain for the current activation-policy binding. |
| 12 | pub const CAPABILITY_HASH_DOMAIN_V3: &[u8] = b"codewhale-plugin-capabilities-v3\0"; |
| 13 | |
| 14 | /// Historical policy domain kept so persisted v2 receipts are intentionally |
| 15 | /// invalidated when the declarative Commands, Agents, and Hooks adapters ship. |
| 16 | pub const CAPABILITY_HASH_DOMAIN_V2: &[u8] = b"codewhale-plugin-capabilities-v2\0"; |
| 17 | |
| 18 | /// Historical domain used before the activation policy was bound into the |
| 19 | /// receipt. Kept so discovery can prove a v1 receipt no longer matches. |
| 20 | pub const CAPABILITY_HASH_DOMAIN_V1: &[u8] = b"codewhale-plugin-capabilities-v1\0"; |
| 21 | |
| 22 | pub const ACTIVATION_POLICY_VERSION: u32 = 3; |
| 23 | |
| 24 | /// A runtime adapter or inventoried capability that a bundle may declare. |
| 25 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] |
| 26 | pub enum PluginActivationCapability { |
| 27 | Skills, |
| 28 | McpStdio, |
| 29 | McpRemote, |
| 30 | Commands, |
| 31 | Agents, |
| 32 | Hooks, |
| 33 | Lsp, |
| 34 | Native, |
| 35 | FilesystemRoots, |
| 36 | LifecycleMutation, |
| 37 | } |
| 38 | |
| 39 | impl PluginActivationCapability { |
| 40 | pub const ALL: &'static [Self] = &[ |
| 41 | Self::Skills, |
| 42 | Self::McpStdio, |
| 43 | Self::McpRemote, |
| 44 | Self::Commands, |
| 45 | Self::Agents, |
| 46 | Self::Hooks, |
| 47 | Self::Lsp, |
| 48 | Self::Native, |
| 49 | Self::FilesystemRoots, |
| 50 | Self::LifecycleMutation, |
| 51 | ]; |
| 52 | |
| 53 | #[must_use] |
| 54 | pub fn as_str(self) -> &'static str { |
| 55 | match self { |
| 56 | Self::Skills => "skills", |
| 57 | Self::McpStdio => "mcp-stdio", |
| 58 | Self::McpRemote => "mcp-remote", |
| 59 | Self::Commands => "commands", |
| 60 | Self::Agents => "agents", |
| 61 | Self::Hooks => "hooks", |
| 62 | Self::Lsp => "lsp", |
| 63 | Self::Native => "native", |
| 64 | Self::FilesystemRoots => "filesystem-roots", |
| 65 | Self::LifecycleMutation => "lifecycle-mutation", |
| 66 | } |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | /// The adapters this exact Codewhale build will activate, plus the inventoried |
| 71 | /// surfaces that stay inactive. Fields are public so tests can construct a |
| 72 | /// mutated policy and prove the capability hash moves. |
| 73 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 74 | pub struct PluginActivationPolicy { |
| 75 | pub version: u32, |
| 76 | pub supported: &'static [PluginActivationCapability], |
| 77 | pub inactive: &'static [PluginActivationCapability], |
| 78 | } |
| 79 | |
| 80 | impl PluginActivationPolicy { |
| 81 | #[must_use] |
| 82 | pub const fn current() -> Self { |
| 83 | Self { |
| 84 | version: ACTIVATION_POLICY_VERSION, |
| 85 | supported: &[ |
| 86 | PluginActivationCapability::Skills, |
| 87 | PluginActivationCapability::McpStdio, |
| 88 | PluginActivationCapability::McpRemote, |
| 89 | PluginActivationCapability::Commands, |
| 90 | PluginActivationCapability::Agents, |
| 91 | PluginActivationCapability::Hooks, |
| 92 | ], |
| 93 | inactive: &[ |
| 94 | PluginActivationCapability::Lsp, |
| 95 | PluginActivationCapability::Native, |
| 96 | PluginActivationCapability::FilesystemRoots, |
| 97 | PluginActivationCapability::LifecycleMutation, |
| 98 | ], |
| 99 | } |
| 100 | } |
| 101 | |
| 102 | #[must_use] |
| 103 | pub fn is_supported(self, capability: PluginActivationCapability) -> bool { |
| 104 | self.supported.contains(&capability) |
| 105 | } |
| 106 | |
| 107 | pub fn write_hash_material(self, hasher: &mut impl Digest) { |
| 108 | hasher.update(CAPABILITY_HASH_DOMAIN_V3); |
| 109 | hasher.update(b"policy-version\0"); |
| 110 | hasher.update(self.version.to_string().as_bytes()); |
| 111 | hasher.update(b"\0"); |
| 112 | for capability in self.supported { |
| 113 | hasher.update(b"supported\0"); |
| 114 | hasher.update(capability.as_str().as_bytes()); |
| 115 | hasher.update(b"\0"); |
| 116 | } |
| 117 | for capability in self.inactive { |
| 118 | hasher.update(b"inactive\0"); |
| 119 | hasher.update(capability.as_str().as_bytes()); |
| 120 | hasher.update(b"\0"); |
| 121 | } |
| 122 | } |
| 123 | } |
| 124 | |
| 125 | #[cfg(test)] |
| 126 | mod tests { |
| 127 | use super::*; |
| 128 | |
| 129 | #[test] |
| 130 | fn current_activation_policy_partitions_known_capabilities() { |
| 131 | let policy = PluginActivationPolicy::current(); |
| 132 | for capability in PluginActivationCapability::ALL { |
| 133 | let supported = policy.supported.contains(capability); |
| 134 | let inactive = policy.inactive.contains(capability); |
| 135 | assert_ne!( |
| 136 | supported, inactive, |
| 137 | "{capability:?} must be supported or inactive, not both or neither" |
| 138 | ); |
| 139 | } |
| 140 | } |
| 141 | } |
| 142 |