Skip to main content

majordomus_cli/capability/builtin/
mod.rs

1//! The executable capabilities this executable ships: one Rust module per capability
2//! module, each owning its typed inputs and outputs, its handlers, its benchmark cases
3//! and its `module()` composition; [`modules`] composes the application from them with
4//! `compose_modules!`, and that list is the only root composition there is. Adding a
5//! capability to an existing module touches that module's file alone; every projection,
6//! benchmark target and generated document follows from the descriptor.
7//!
8//! ```
9//! use majordomus_cli::capability::builtin;
10//!
11//! // the application is its modules, and each module stamps its namespace on what it holds
12//! let modules = builtin::modules();
13//! assert!(modules.iter().any(|m| m.id.as_str() == "repository"));
14//! for module in &modules {
15//!     for e in &module.capabilities {
16//!         assert_eq!(
17//!             e.capability.id.namespace(),
18//!             module.id.as_str(),
19//!             "{} is composed into the wrong module",
20//!             e.capability.id
21//!         );
22//!     }
23//! }
24//!
25//! // and `all` is the same set flattened, for a registry built without module metadata
26//! let flattened = builtin::all().len();
27//! assert_eq!(
28//!     flattened,
29//!     modules.iter().map(|m| m.capabilities.len()).sum::<usize>()
30//! );
31//! ```
32
33pub mod artifacts;
34pub(crate) mod brief;
35pub(crate) mod capabilities;
36pub mod commands;
37pub mod commit;
38pub mod continuity;
39pub(crate) mod continuity_transfer;
40pub(crate) mod convergence;
41pub mod dashboard;
42pub(crate) mod delivery;
43pub(crate) mod deploy;
44pub(crate) mod design;
45pub(crate) mod devcontext;
46pub mod devtask;
47pub(crate) mod directories;
48pub(crate) mod distribution;
49pub mod economics;
50pub(crate) mod editors;
51pub mod entity;
52pub mod environment;
53pub mod episodes;
54pub mod evidence;
55pub(crate) mod executions;
56pub mod gates;
57pub(crate) mod graph;
58pub(crate) mod guard;
59pub mod health;
60pub(crate) mod integration;
61pub mod intent_realization;
62pub mod intents;
63pub mod knowledge_base;
64pub mod lifecycle;
65pub mod mcp;
66pub(crate) mod mesh;
67pub(crate) mod models;
68pub(crate) mod monitor;
69pub mod objects;
70pub mod obligations;
71pub mod observatory;
72pub mod pack;
73pub(crate) mod peers;
74pub(crate) mod perf;
75pub mod plan;
76pub(crate) mod product;
77pub mod quality;
78pub mod reasoning;
79pub mod recover;
80pub mod release;
81pub mod repository;
82pub mod rules;
83mod scope;
84pub mod served;
85pub mod server;
86pub mod session_domain;
87pub(crate) mod shell;
88pub mod skills;
89pub mod trace;
90mod views;
91pub mod web;
92pub(crate) mod worktree;
93
94use crate::compose_modules;
95
96use super::handler::Executable;
97use super::model::{HttpExposure, HttpMethod, McpExposure};
98use super::module::ModuleDescriptor;
99
100pub use artifacts::{
101    ArtifactReport, ArtifactState, ArtifactTallies, ArtifactVerdict, ArtifactView, ArtifactsInput,
102    ARTIFACTS_URI,
103};
104pub use capabilities::{CapabilitiesInput, CapabilityList, CapabilitySummary, DescribeInput};
105pub use continuity::{ActiveTask, Continuity, Divergence, OpenSession, Record, CONTINUITY_URI};
106pub use deploy::{
107    DeploymentCheck, DeploymentList, DeploymentView, GetDeploymentInput, DEPLOYMENTS_URI,
108};
109// the Cockpit's Design page renders these two; everything else the module declares is
110// read as JSON through the executor, like every other capability's output
111pub(crate) use design::{DesignReport, TokenList};
112pub use devcontext::DEVCONTEXT_POLICY_URI;
113pub use devtask::{DevMilestoneInput, DevTaskInput};
114pub use directories::{
115    ContractView, DirectoriesInput, DirectoryNode, DirectoryReport, DirectoryState,
116    DirectoryTallies, EffectiveEntry, DIRECTORIES_URI,
117};
118// A release artifact and a generated artifact are different things, and `artifacts`
119// already answers to the plain names; distribution's carry the `Release` prefix so that
120// the two never collide — in this module, and in the one schema component namespace the
121// OpenAPI document has.
122pub use distribution::{
123    BuildReport, CheckState, DistributionReport, InstallCheck, InstallabilityReport,
124    ReleaseArtifactInput, ReleaseArtifactView, ReleaseView, ReleasesReport, TargetView,
125};
126pub use environment::{EnvironmentInput, EnvironmentProvenance, ExplainInput, ENVIRONMENT_URI};
127pub use episodes::{AttachInput, DetachInput, EpisodeList};
128pub use executions::{
129    CancelReport, EventHistory, ExecutionLinks, ExecutionList, ExecutionView, ProtocolReport,
130    EXECUTIONS_URI, EXECUTION_PROTOCOL_URI,
131};
132pub use gates::{CompletionInput, GateModelEntry, GateModelReport, COMPLETION_URI, GATES_URI};
133pub use graph::{GraphInput, GraphList, GRAPHS_URI};
134pub use health::{Health, HealthCheck, HealthStatus, HEALTH_URI};
135pub use knowledge_base::{
136    AgeSource, KnowledgeCandidate, KnowledgeCandidates, KnowledgeRecord, KnowledgeRecordInput,
137    KnowledgeStatus, LedgerMark, ReferenceResolution, ResolvedReference, ResolvedRelation,
138    KNOWLEDGE_CANDIDATES_URI, KNOWLEDGE_STATUS_URI,
139};
140pub use lifecycle::{
141    Balance, ClosedSession, ClosedSessions, Episode, EpisodeStanding, Episodes, Orphan, Pointer,
142    PointerLayout, ProviderLifecycle, ProviderLifecycles, Recovery, RuntimeView, Stranded,
143    EPISODES_URI, RECOVERY_URI,
144};
145pub use mesh::{MeshIdentityReport, NodeList, RegisterInput, MESH_URI};
146pub use models::{ModelsFilter, ModelsReport, RouteInput, VendorView, MODELS_URI};
147pub use objects::{
148    resolve, AnswerView, Comparison, DriftedObject, GetInput, ListInput, ObjectList,
149    ObjectStanding, Resolved, ResourceView, SearchHit, SearchInput, SearchResult, VerifyInput,
150    VerifyReport, SEARCH_DEFAULT_LIMIT, SEARCH_MAX_LIMIT,
151};
152pub use obligations::{
153    Closure, Evidence, Obligation, ObligationClosure, ObligationState, Vocabulary, CLOSURE_URI,
154    OBLIGATIONS_URI,
155};
156pub use peers::{AnnounceInput, PeerList};
157pub use plan::{PlanIssueFilter, PlanMilestoneFilter, PlanRecordInput, PLAN_URI};
158pub use quality::{QualityAnswer, QualityInput, QUALITY_URI};
159pub use repository::{RepositoryReport, REPOSITORY_URI};
160pub use scope::{normalise_path, ClassifyInput, ScopeReport, SCOPE_URI};
161pub use server::{
162    Checkouts, Desired, LeaseView, ServerStanding, ServerStatus, ServerStatusInput, ServerView,
163    SERVER_URI,
164};
165pub use session_domain::{IdentityReport, MachineReport, IDENTITY_URI, MACHINE_URI};
166pub use trace::{TraceCommitInput, TraceIssueInput, TraceReportInput, TRACEABILITY_URI};
167pub(crate) mod why;
168
169pub use commands::{CommandGraphReport, CommandIndex, CommandSummary};
170pub use views::{Empty, ObjectSummary, ObjectView};
171pub use web::{SurfaceReport, SURFACES_URI};
172pub use worktree::{
173    InspectInput as WorktreeInspectInput, StatusInput as WorktreeStatusInput, WORKTREES_URI,
174};
175
176/// The application: its modules, in one place. A new module is one line here; a new
177/// capability in an existing module is no line here.
178pub fn modules() -> Vec<ModuleDescriptor> {
179    compose_modules![
180        release,
181        commit,
182        repository,
183        objects,
184        entity,
185        capabilities,
186        commands,
187        graph,
188        guard,
189        health,
190        dashboard,
191        continuity,
192        knowledge_base,
193        lifecycle,
194        obligations,
195        gates,
196        deploy,
197        delivery,
198        served,
199        evidence,
200        rules,
201        executions,
202        mesh,
203        monitor,
204        models,
205        reasoning,
206        episodes,
207        peers,
208        brief,
209        server,
210        mcp,
211        session_domain,
212        shell,
213        pack,
214        perf,
215        plan,
216        recover,
217        directories,
218        devcontext,
219        artifacts,
220        economics,
221        editors,
222        environment,
223        quality,
224        distribution,
225        why,
226        intents,
227        intent_realization,
228        skills,
229        web,
230        design,
231        devtask,
232        worktree,
233        integration,
234        convergence,
235        trace,
236        product,
237        observatory
238    ]
239}
240
241/// Every executable of every module, flattened, for a registry built without module
242/// metadata (tests, benchmarks, doctests). The application builds from [`modules`].
243pub fn all() -> Vec<Executable> {
244    modules().into_iter().flat_map(|m| m.capabilities).collect()
245}
246
247pub(crate) fn mcp(tool: &str) -> Option<McpExposure> {
248    Some(McpExposure {
249        tool: Some(tool.into()),
250        resource: None,
251    })
252}
253pub(crate) fn get(path: &str) -> Option<HttpExposure> {
254    Some(HttpExposure {
255        method: HttpMethod::Get,
256        path: path.into(),
257    })
258}
259pub(crate) fn post(path: &str) -> Option<HttpExposure> {
260    Some(HttpExposure {
261        method: HttpMethod::Post,
262        path: path.into(),
263    })
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269    use crate::capability::{CapabilityKind, Effect};
270
271    /// Only a command may claim to write the repository, and one that does must be a POST.
272    ///
273    /// Two facts about the whole builtin registry rather than about one module, because the
274    /// harm of getting either wrong is not local: the exposure ceiling of every surface is
275    /// derived from the effect, and a capability whose effect is understated is projected
276    /// onto surfaces whose ceiling exists to exclude it. A query that claimed the effect
277    /// would be worse still — bound to GET, announced to MCP as read-only, and followed by
278    /// every crawler and prefetcher that ever met the Cockpit.
279    ///
280    /// The registry refuses both at construction; this states them where a reader looking
281    /// for the invariant will find it, and fails on a declaration that slipped past.
282    #[test]
283    fn only_a_command_writes_the_repository_and_only_by_post() {
284        for e in all() {
285            let c = &e.capability;
286            if c.execution.effect != Effect::RepositoryMutation {
287                continue;
288            }
289            assert_eq!(
290                c.kind,
291                CapabilityKind::Command,
292                "{} claims to write the repository and is not a command",
293                c.id
294            );
295            if let Some(http) = &c.exposure.http {
296                assert_eq!(
297                    http.method.as_str(),
298                    "POST",
299                    "{} writes the repository and is reachable by {}",
300                    c.id,
301                    http.method.as_str()
302                );
303            }
304        }
305    }
306
307    /// Every capability that writes the repository, named.
308    ///
309    /// A list rather than a count, and it is meant to be edited: adding one is a deliberate
310    /// widening of what this executable may do to a tracked file, and it should be visible
311    /// in a diff rather than absorbed silently. The entries converge as ADR 0040 is worked
312    /// through, so this grows — one line per lifecycle command that stops being the shell
313    /// tool's alone.
314    #[test]
315    fn the_capabilities_that_write_the_repository_are_these() {
316        // A BTreeSet rather than a Vec and a sort: the set is ordered by construction, and
317        // `order-check` counts every sort site in the crate against a baseline it may not
318        // exceed. A test that reached for one would spend that budget on itself.
319        let executables = all();
320        let writers: std::collections::BTreeSet<&str> = executables
321            .iter()
322            .filter(|e| e.capability.execution.effect == Effect::RepositoryMutation)
323            .map(|e| e.capability.id.as_str())
324            .collect();
325        assert_eq!(
326            writers.into_iter().collect::<Vec<_>>(),
327            // reasoning.record writes checkout state under .ai/local, never a tracked file,
328            // and is classified with the writers so that every surface asks before it runs.
329            [
330                "continuity.publish",
331                "continuity.resume",
332                "continuity.sync",
333                // runs any executable capability, writers included (ADR 0126)
334                "executions.start",
335                // writes one new intent record, never a stage (ADR 0144)
336                "intents.propose",
337                "plan.transition",
338                "reasoning.record",
339                "recover.orphans"
340            ]
341        );
342    }
343}