Skip to main content

winx_code_agent/
tool_registry.rs

1//! Typed source of truth for MCP tool identity and cross-cutting behavior.
2
3use std::fmt;
4
5/// Stable tool order used by discovery, policy bits, and dispatch.
6#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
7#[repr(u8)]
8pub enum ToolKind {
9    Initialize = 0,
10    BashCommand = 1,
11    ReadFiles = 2,
12    FileWriteOrEdit = 3,
13    MultiFileEdit = 4,
14    VerifyEdit = 5,
15    UndoEdit = 6,
16    ContextSave = 7,
17    ReadImage = 8,
18    CodeMap = 9,
19    ApplyPatch = 10,
20    /// Unified mutation surface. Appended so every persisted legacy policy bit
21    /// keeps its original meaning during the catalog migration.
22    EditFiles = 11,
23    /// Workflow tools append to the catalog without changing legacy policy bits.
24    Execution = 12,
25    Agent = 13,
26}
27
28#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub enum ToolAccess {
30    ReadOnly,
31    Neutral,
32    Destructive,
33}
34
35#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36pub enum ToolOutputContract {
37    Shared,
38    CodeMap,
39}
40
41#[derive(Clone, Copy, Debug, Eq, PartialEq)]
42pub enum ToolWorld {
43    Closed,
44    Open,
45}
46
47#[derive(Clone, Copy, Debug, Eq, PartialEq)]
48pub enum ToolSessionContract {
49    Initializes,
50    RequiresInitialized,
51}
52
53#[derive(Clone, Copy, Debug, Eq, PartialEq)]
54pub struct ToolDescriptor {
55    pub access: ToolAccess,
56    pub world: ToolWorld,
57    pub session: ToolSessionContract,
58    pub output_contract: ToolOutputContract,
59}
60
61impl ToolKind {
62    pub const ALL: [Self; 14] = [
63        Self::Initialize,
64        Self::BashCommand,
65        Self::ReadFiles,
66        Self::FileWriteOrEdit,
67        Self::MultiFileEdit,
68        Self::VerifyEdit,
69        Self::UndoEdit,
70        Self::ContextSave,
71        Self::ReadImage,
72        Self::CodeMap,
73        Self::ApplyPatch,
74        Self::EditFiles,
75        Self::Execution,
76        Self::Agent,
77    ];
78
79    pub const fn as_str(self) -> &'static str {
80        match self {
81            Self::Initialize => "Initialize",
82            Self::BashCommand => "BashCommand",
83            Self::ReadFiles => "ReadFiles",
84            Self::FileWriteOrEdit => "FileWriteOrEdit",
85            Self::MultiFileEdit => "MultiFileEdit",
86            Self::VerifyEdit => "VerifyEdit",
87            Self::UndoEdit => "UndoEdit",
88            Self::ContextSave => "ContextSave",
89            Self::ReadImage => "ReadImage",
90            Self::CodeMap => "CodeMap",
91            Self::ApplyPatch => "ApplyPatch",
92            Self::EditFiles => "EditFiles",
93            Self::Execution => "Execution",
94            Self::Agent => "Agent",
95        }
96    }
97
98    pub fn parse(name: &str) -> Option<Self> {
99        match name {
100            "Initialize" => Some(Self::Initialize),
101            "BashCommand" => Some(Self::BashCommand),
102            "ReadFiles" => Some(Self::ReadFiles),
103            "FileWriteOrEdit" => Some(Self::FileWriteOrEdit),
104            "MultiFileEdit" => Some(Self::MultiFileEdit),
105            "VerifyEdit" => Some(Self::VerifyEdit),
106            "UndoEdit" => Some(Self::UndoEdit),
107            "ContextSave" => Some(Self::ContextSave),
108            "ReadImage" => Some(Self::ReadImage),
109            "CodeMap" => Some(Self::CodeMap),
110            "ApplyPatch" => Some(Self::ApplyPatch),
111            "EditFiles" => Some(Self::EditFiles),
112            "Execution" => Some(Self::Execution),
113            "Agent" => Some(Self::Agent),
114            _ => None,
115        }
116    }
117
118    pub const fn bit(self) -> u16 {
119        1_u16 << self as u8
120    }
121
122    pub const fn descriptor(self) -> ToolDescriptor {
123        let (access, world) = match self {
124            Self::ReadFiles | Self::ReadImage | Self::CodeMap | Self::Execution => {
125                (ToolAccess::ReadOnly, ToolWorld::Closed)
126            }
127            Self::ContextSave => (ToolAccess::Neutral, ToolWorld::Closed),
128            // Initialize also supports reset and mode changes; hints cover every action.
129            Self::Initialize | Self::UndoEdit => (ToolAccess::Destructive, ToolWorld::Closed),
130            Self::BashCommand
131            | Self::Agent
132            | Self::FileWriteOrEdit
133            | Self::MultiFileEdit
134            | Self::ApplyPatch
135            | Self::VerifyEdit
136            | Self::EditFiles => (ToolAccess::Destructive, ToolWorld::Open),
137        };
138        ToolDescriptor {
139            access,
140            world,
141            session: if matches!(self, Self::Initialize) {
142                ToolSessionContract::Initializes
143            } else {
144                ToolSessionContract::RequiresInitialized
145            },
146            output_contract: if matches!(self, Self::CodeMap) {
147                ToolOutputContract::CodeMap
148            } else {
149                ToolOutputContract::Shared
150            },
151        }
152    }
153
154    pub const fn is_file_mutation(self) -> bool {
155        matches!(
156            self,
157            Self::FileWriteOrEdit | Self::MultiFileEdit | Self::ApplyPatch | Self::EditFiles
158        )
159    }
160
161    pub const fn requires_bash_companion(self) -> bool {
162        matches!(self, Self::VerifyEdit)
163    }
164}
165
166impl fmt::Display for ToolKind {
167    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
168        formatter.write_str(self.as_str())
169    }
170}
171
172/// Backwards-compatible name array derived from the typed stable order.
173pub const ALL_TOOL_NAMES: [&str; 14] = [
174    ToolKind::Initialize.as_str(),
175    ToolKind::BashCommand.as_str(),
176    ToolKind::ReadFiles.as_str(),
177    ToolKind::FileWriteOrEdit.as_str(),
178    ToolKind::MultiFileEdit.as_str(),
179    ToolKind::VerifyEdit.as_str(),
180    ToolKind::UndoEdit.as_str(),
181    ToolKind::ContextSave.as_str(),
182    ToolKind::ReadImage.as_str(),
183    ToolKind::CodeMap.as_str(),
184    ToolKind::ApplyPatch.as_str(),
185    ToolKind::EditFiles.as_str(),
186    ToolKind::Execution.as_str(),
187    ToolKind::Agent.as_str(),
188];
189
190#[cfg(test)]
191mod tests {
192    use std::collections::HashSet;
193
194    use super::*;
195
196    #[test]
197    fn names_bits_and_parser_are_one_to_one() {
198        assert_eq!(ALL_TOOL_NAMES, ToolKind::ALL.map(ToolKind::as_str));
199        let names = ToolKind::ALL.into_iter().map(ToolKind::as_str).collect::<HashSet<_>>();
200        let bits = ToolKind::ALL.into_iter().map(ToolKind::bit).collect::<HashSet<_>>();
201        assert_eq!(names.len(), ToolKind::ALL.len());
202        assert_eq!(bits.len(), ToolKind::ALL.len());
203        for kind in ToolKind::ALL {
204            assert_eq!(ToolKind::parse(kind.as_str()), Some(kind));
205        }
206        assert_eq!(ToolKind::parse("readfiles"), None);
207    }
208
209    #[test]
210    fn every_current_tool_is_principal_and_workspace_session_aware() {
211        assert_eq!(ToolKind::Initialize.descriptor().session, ToolSessionContract::Initializes);
212        assert!(ToolKind::ALL
213            .into_iter()
214            .filter(|kind| *kind != ToolKind::Initialize)
215            .all(|kind| { kind.descriptor().session == ToolSessionContract::RequiresInitialized }));
216    }
217
218    #[test]
219    fn workflow_tools_append_bits_and_preserve_access_contracts() {
220        assert_eq!(ToolKind::EditFiles.bit(), 1 << 11);
221        assert_eq!(ToolKind::Execution.bit(), 1 << 12);
222        assert_eq!(ToolKind::Agent.bit(), 1 << 13);
223        assert_eq!(ToolKind::Execution.descriptor().access, ToolAccess::ReadOnly);
224        assert_eq!(ToolKind::Execution.descriptor().world, ToolWorld::Closed);
225        assert_eq!(ToolKind::Agent.descriptor().access, ToolAccess::Destructive);
226        assert_eq!(ToolKind::Agent.descriptor().world, ToolWorld::Open);
227        for kind in [ToolKind::Execution, ToolKind::Agent] {
228            assert_eq!(kind.descriptor().output_contract, ToolOutputContract::Shared);
229            assert!(!kind.is_file_mutation());
230            assert!(!kind.requires_bash_companion());
231        }
232    }
233}