返回 CodeWhale
activation.rs
根目录 / crates / tui / src / plugins / activation.rs
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
142 lines RUST