agent-plugins 0.1.1

Sans-IO implementation of the Agent Plugins specification (agent-plugins.org): load, validate, and prepare agent plugins from any IO
Documentation
//! An implementation of the [Agent Plugins specification] v1.0.0 —
//! the open, vendor-neutral format for packaging Agent Skills and MCP
//! servers into distributable plugins.
//!
//! [Agent Plugins specification]: https://agent-plugins.org/specification
//!
//! # Design
//!
//! The crate is built around three ideas, each expressed in its types:
//!
//! **Parse, don't validate.** Constraint-carrying values get their own types:
//! a [`PluginName`] proves §5.5, a [`RelativePath`] proves `./`-rooted
//! containment, an [`McpUrl`] proves the §7.2.1 URL rules, a [`Cwd`] can only
//! be one of the three permitted working-directory forms. If you hold the
//! value, the check already happened.
//!
//! **Failure boundaries as return types.** The spec is precise about how far
//! a failure spreads: a bad manifest rejects the plugin ([`Rejection`]), a
//! bad `mcp.json` disables one component ([`McpOutcome::Disabled`]), a bad
//! skill or server entry skips only itself (a [`Diagnostic`]). Nothing fails
//! silently and nothing fails wider than the spec allows.
//!
//! **Sans-IO.** The [`Loading`] state machine speaks the specification and
//! never touches a filesystem. It asks [`Question`]s; you answer from
//! wherever the plugin lives. Batteries are included for the common cases —
//! [`load_dir`] for real directories, [`MemSource`] for tests and archives,
//! the [`Source`] trait for anything else synchronous — and the machine
//! itself for async or exotic IO.
//!
//! # Loading from a directory
//!
//! ```no_run
//! use agent_plugins::McpOutcome;
//!
//! let plugin = agent_plugins::load_dir("path/to/my-plugin")?;
//!
//! println!("{} v{}", plugin.manifest.name,
//!          plugin.manifest.version.as_deref().unwrap_or("unversioned"));
//! for skill in &plugin.skills {
//!     println!("skill {}: {}", skill.meta.name, skill.meta.description);
//! }
//! if let McpOutcome::Configured(mcp) = &plugin.mcp {
//!     println!("{} MCP server(s)", mcp.servers.len());
//! }
//! for diagnostic in &plugin.diagnostics {
//!     eprintln!("note: {diagnostic}");
//! }
//! # Ok::<(), agent_plugins::Rejection>(())
//! ```
//!
//! # Loading from anywhere: drive the machine yourself
//!
//! The loader asks four kinds of questions. Answer them from an archive, a
//! database, an HTTP range request, an async runtime — the machine cannot
//! tell the difference:
//!
//! ```no_run
//! use agent_plugins::{Answer, Loading, Question};
//!
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
//! # async fn stat(p: &agent_plugins::PackagePath) -> Answer { todo!() }
//! # async fn fetch(p: &agent_plugins::PackagePath) -> Answer { todo!() }
//! # async fn index(p: &agent_plugins::PackagePath) -> Answer { todo!() }
//! let mut loading = Loading::start();
//! while let Some(question) = loading.question() {
//!     let answer = match &question {
//!         Question::Kind(path) => stat(path).await,
//!         Question::Confined(_) => Answer::Confined(true), // archives have no symlinks
//!         Question::Read(path) => fetch(path).await,
//!         Question::List(path) => index(path).await,
//!     };
//!     loading.answer(answer)?;
//! }
//! let plugin = loading.finish()?;
//! # Ok(())
//! # }
//! ```
//!
//! # Launching a stdio MCP server
//!
//! Turn validated configuration into a runnable process with the §9.1
//! environment contract already sequenced:
//!
//! ```no_run
//! use agent_plugins::{Anchors, McpOutcome, McpServer};
//!
//! # let plugin = agent_plugins::load_dir("p")?;
//! let anchors = Anchors::new("/plugins/my-plugin", "/plugins/data/my-plugin");
//! if let McpOutcome::Configured(mcp) = &plugin.mcp {
//!     for entry in &mcp.servers {
//!         if let McpServer::Stdio(server) = &entry.server {
//!             std::fs::create_dir_all(&anchors.plugin_data).ok(); // §9.1
//!             let mut child = server.launch_plan(&anchors).to_command().spawn()?;
//!         }
//!     }
//! }
//! # Ok::<(), Box<dyn std::error::Error>>(())
//! ```
//!
//! # Skill bodies are documents, not strings
//!
//! A skill's instructions arrive as a [`Document`]: a preamble plus a tree of
//! [`Section`]s. Skills load progressively, and this lets a client hand a
//! model one section rather than the whole file — and follow the
//! [`LinkTarget::InPackage`] links to the `references/` and `scripts/` files
//! the body points at.
//!
//! ```
//! # use agent_plugins::{Document, LinkTarget};
//! let skill = Document::parse(
//!     "# Deploy\n\nShip it.\n\n## Rollback\n\nSee [the runbook](references/runbook.md).\n",
//! );
//!
//! for (depth, section) in skill.outline() {
//!     println!("{:indent$}{}", "", section.heading(), indent = depth * 2);
//! }
//!
//! let rollback = skill.section(["Deploy", "Rollback"]).unwrap();
//! assert_eq!(rollback.slug(), "rollback");
//! assert_eq!(
//!     rollback.links()[0].target,
//!     LinkTarget::InPackage("references/runbook.md".into()),
//! );
//! ```
//!
//! # Conformance
//!
//! This crate implements the full client-side checklist of the specification
//! (Appendix A): manifest validation with the exact §5.2 fatal/non-fatal
//! split, fixed-location component discovery, per-boundary failure isolation,
//! §7.2.1 transport validation, and §9 placeholder expansion. The test suite
//! runs a fixture corpus shared with the sibling TypeScript implementation.

mod diag;
mod fs;
mod launch;
mod load;
mod manifest;
mod markdown;
mod mcp;
mod mem;
mod name;
mod path;
mod skill;
mod source;
mod template;

pub use diag::{Diagnostic, Origin, Rule};
pub use fs::{load_dir, DirSource};
pub use launch::{LaunchPlan, Program};
pub use load::{
    Answer, AnswerMismatch, DirEntry, FileKind, LoadedPlugin, Loading, McpOutcome, Question,
    Rejection,
};
pub use manifest::{
    Author, Extensions, Manifest, ManifestRejection, SpecVersion, PLUGIN_SCHEMA_1_0_0,
};
pub use markdown::{Document, HeadingLevel, Link, LinkTarget, Section, Slug};
pub use mcp::{
    Command, Cwd, McpConfig, McpDisabledReason, McpServer, McpUrl, RemoteServer, ServerEntry,
    ServerInvalid, StdioServer, Transport, UrlInvalid, MCP_SCHEMA_1_0_0,
};
pub use mem::MemSource;
pub use name::{InvalidName, PluginName};
pub use path::{PackagePath, RelativePath, RelativePathError};
pub use skill::{parse_skill_md, Skill, SkillInvalid, SkillMeta};
pub use source::{load, Source};
pub use template::{Anchors, Placeholder, Segment, Template};