Skip to main content

capo_agent/extensions/
mod.rs

1#![cfg_attr(test, allow(clippy::expect_used, clippy::unwrap_used))]
2
3//! Extension subsystem — spawn-per-event subprocess hooks.
4//!
5//! See `docs/superpowers/specs/2026-05-21-capo-v0.7-design.md` for the
6//! design rationale (spawn-per-event vs long-lived; codex-style; minimal
7//! scope).
8//!
9//! Module structure mirrors `mcp/`:
10//!   manifest.rs  — TOML schema + parsing
11//!   wire.rs      — Event / Action serde types (JSONL line shape)
12//!   registry.rs  — validated, indexed view of loaded extensions
13//!   dispatcher.rs — spawn-and-await; chain semantics; timeout
14//!   diagnostic.rs — surfaced via `/extensions` slash command
15
16pub mod diagnostic;
17pub mod dispatcher;
18pub mod manifest;
19pub mod registry;
20pub mod wire;
21
22pub use diagnostic::{DiagnosticSeverity, ExtensionDiagnostic};
23pub use manifest::{parse_str, ExtensionEntry, ExtensionManifestFile};
24pub use registry::{ExtensionRegistry, RegisteredExtension};
25pub use wire::{Action, Event, EventName};
26
27/// Load the manifest at `path` (missing-file = empty registry, no error)
28/// and validate it. Returns the registry and any diagnostics collected.
29pub async fn load_extensions_manifest(
30    path: &std::path::Path,
31) -> (ExtensionRegistry, Vec<ExtensionDiagnostic>) {
32    use diagnostic::DiagnosticSeverity;
33
34    let raw = match tokio::fs::read_to_string(path).await {
35        Ok(s) => s,
36        Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
37            return (ExtensionRegistry::default(), Vec::new());
38        }
39        Err(err) => {
40            let display = path.display();
41            let diag = ExtensionDiagnostic {
42                extension_name: "<manifest>".into(),
43                severity: DiagnosticSeverity::Error,
44                message: format!("could not read {display}: {err}"),
45            };
46            return (ExtensionRegistry::default(), vec![diag]);
47        }
48    };
49    let parsed = match manifest::parse_str(&raw) {
50        Ok(p) => p,
51        Err(err) => {
52            let display = path.display();
53            let diag = ExtensionDiagnostic {
54                extension_name: "<manifest>".into(),
55                severity: DiagnosticSeverity::Error,
56                message: format!("could not parse {display}: {err}"),
57            };
58            return (ExtensionRegistry::default(), vec![diag]);
59        }
60    };
61    let mut diagnostics = Vec::new();
62    let registry = ExtensionRegistry::build(parsed, &mut diagnostics);
63    (registry, diagnostics)
64}