agent_plugins/lib.rs
1//! An implementation of the [Agent Plugins specification] v1.0.0 —
2//! the open, vendor-neutral format for packaging Agent Skills and MCP
3//! servers into distributable plugins.
4//!
5//! [Agent Plugins specification]: https://agent-plugins.org/specification
6//!
7//! # Design
8//!
9//! The crate is built around three ideas, each expressed in its types:
10//!
11//! **Parse, don't validate.** Constraint-carrying values get their own types:
12//! a [`PluginName`] proves §5.5, a [`RelativePath`] proves `./`-rooted
13//! containment, an [`McpUrl`] proves the §7.2.1 URL rules, a [`Cwd`] can only
14//! be one of the three permitted working-directory forms. If you hold the
15//! value, the check already happened.
16//!
17//! **Failure boundaries as return types.** The spec is precise about how far
18//! a failure spreads: a bad manifest rejects the plugin ([`Rejection`]), a
19//! bad `mcp.json` disables one component ([`McpOutcome::Disabled`]), a bad
20//! skill or server entry skips only itself (a [`Diagnostic`]). Nothing fails
21//! silently and nothing fails wider than the spec allows.
22//!
23//! **Sans-IO.** The [`Loading`] state machine speaks the specification and
24//! never touches a filesystem. It asks [`Question`]s; you answer from
25//! wherever the plugin lives. Batteries are included for the common cases —
26//! [`load_dir`] for real directories, [`MemSource`] for tests and archives,
27//! the [`Source`] trait for anything else synchronous — and the machine
28//! itself for async or exotic IO.
29//!
30//! # Loading from a directory
31//!
32//! ```no_run
33//! use agent_plugins::McpOutcome;
34//!
35//! let plugin = agent_plugins::load_dir("path/to/my-plugin")?;
36//!
37//! println!("{} v{}", plugin.manifest.name,
38//! plugin.manifest.version.as_deref().unwrap_or("unversioned"));
39//! for skill in &plugin.skills {
40//! println!("skill {}: {}", skill.meta.name, skill.meta.description);
41//! }
42//! if let McpOutcome::Configured(mcp) = &plugin.mcp {
43//! println!("{} MCP server(s)", mcp.servers.len());
44//! }
45//! for diagnostic in &plugin.diagnostics {
46//! eprintln!("note: {diagnostic}");
47//! }
48//! # Ok::<(), agent_plugins::Rejection>(())
49//! ```
50//!
51//! # Loading from anywhere: drive the machine yourself
52//!
53//! The loader asks four kinds of questions. Answer them from an archive, a
54//! database, an HTTP range request, an async runtime — the machine cannot
55//! tell the difference:
56//!
57//! ```no_run
58//! use agent_plugins::{Answer, Loading, Question};
59//!
60//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
61//! # async fn stat(p: &agent_plugins::PackagePath) -> Answer { todo!() }
62//! # async fn fetch(p: &agent_plugins::PackagePath) -> Answer { todo!() }
63//! # async fn index(p: &agent_plugins::PackagePath) -> Answer { todo!() }
64//! let mut loading = Loading::start();
65//! while let Some(question) = loading.question() {
66//! let answer = match &question {
67//! Question::Kind(path) => stat(path).await,
68//! Question::Confined(_) => Answer::Confined(true), // archives have no symlinks
69//! Question::Read(path) => fetch(path).await,
70//! Question::List(path) => index(path).await,
71//! };
72//! loading.answer(answer)?;
73//! }
74//! let plugin = loading.finish()?;
75//! # Ok(())
76//! # }
77//! ```
78//!
79//! # Launching a stdio MCP server
80//!
81//! Turn validated configuration into a runnable process with the §9.1
82//! environment contract already sequenced:
83//!
84//! ```no_run
85//! use agent_plugins::{Anchors, McpOutcome, McpServer};
86//!
87//! # let plugin = agent_plugins::load_dir("p")?;
88//! let anchors = Anchors::new("/plugins/my-plugin", "/plugins/data/my-plugin");
89//! if let McpOutcome::Configured(mcp) = &plugin.mcp {
90//! for entry in &mcp.servers {
91//! if let McpServer::Stdio(server) = &entry.server {
92//! std::fs::create_dir_all(&anchors.plugin_data).ok(); // §9.1
93//! let mut child = server.launch_plan(&anchors).to_command().spawn()?;
94//! }
95//! }
96//! }
97//! # Ok::<(), Box<dyn std::error::Error>>(())
98//! ```
99//!
100//! # Skill bodies are documents, not strings
101//!
102//! A skill's instructions arrive as a [`Document`]: a preamble plus a tree of
103//! [`Section`]s. Skills load progressively, and this lets a client hand a
104//! model one section rather than the whole file — and follow the
105//! [`LinkTarget::InPackage`] links to the `references/` and `scripts/` files
106//! the body points at.
107//!
108//! ```
109//! # use agent_plugins::{Document, LinkTarget};
110//! let skill = Document::parse(
111//! "# Deploy\n\nShip it.\n\n## Rollback\n\nSee [the runbook](references/runbook.md).\n",
112//! );
113//!
114//! for (depth, section) in skill.outline() {
115//! println!("{:indent$}{}", "", section.heading(), indent = depth * 2);
116//! }
117//!
118//! let rollback = skill.section(["Deploy", "Rollback"]).unwrap();
119//! assert_eq!(rollback.slug(), "rollback");
120//! assert_eq!(
121//! rollback.links()[0].target,
122//! LinkTarget::InPackage("references/runbook.md".into()),
123//! );
124//! ```
125//!
126//! # Conformance
127//!
128//! This crate implements the full client-side checklist of the specification
129//! (Appendix A): manifest validation with the exact §5.2 fatal/non-fatal
130//! split, fixed-location component discovery, per-boundary failure isolation,
131//! §7.2.1 transport validation, and §9 placeholder expansion. The test suite
132//! runs a fixture corpus shared with the sibling TypeScript implementation.
133
134mod diag;
135mod fs;
136mod launch;
137mod load;
138mod manifest;
139mod markdown;
140mod mcp;
141mod mem;
142mod name;
143mod path;
144mod skill;
145mod source;
146mod template;
147
148pub use diag::{Diagnostic, Origin, Rule};
149pub use fs::{load_dir, DirSource};
150pub use launch::{LaunchPlan, Program};
151pub use load::{
152 Answer, AnswerMismatch, DirEntry, FileKind, LoadedPlugin, Loading, McpOutcome, Question,
153 Rejection,
154};
155pub use manifest::{
156 Author, Extensions, Manifest, ManifestRejection, SpecVersion, PLUGIN_SCHEMA_1_0_0,
157};
158pub use markdown::{Document, HeadingLevel, Link, LinkTarget, Section, Slug};
159pub use mcp::{
160 Command, Cwd, McpConfig, McpDisabledReason, McpServer, McpUrl, RemoteServer, ServerEntry,
161 ServerInvalid, StdioServer, Transport, UrlInvalid, MCP_SCHEMA_1_0_0,
162};
163pub use mem::MemSource;
164pub use name::{InvalidName, PluginName};
165pub use path::{PackagePath, RelativePath, RelativePathError};
166pub use skill::{parse_skill_md, Skill, SkillInvalid, SkillMeta};
167pub use source::{load, Source};
168pub use template::{Anchors, Placeholder, Segment, Template};