Skip to main content

PluginManager

Struct PluginManager 

Source
pub struct PluginManager { /* private fields */ }
Expand description

Manages plugin lifecycle: install, remove, list.

All operations are synchronous. Plugin watchers and agent config overlays are applied separately by the agent bootstrap layer.

Implementations§

Source§

impl PluginManager

Source

pub fn default_plugins_dir() -> PathBuf

Returns the canonical default plugins directory: ~/.local/share/zeph/plugins/.

Both the CLI and TUI must use this helper so they always point to the same directory.

Source

pub fn new( plugins_dir: PathBuf, managed_skills_dir: PathBuf, mcp_allowed_commands: Vec<String>, base_allowed_commands: Vec<String>, ) -> Self

Create a new manager.

§Parameters
  • plugins_dir — root installation directory for plugins.
  • managed_skills_dir — directory for user-managed skills (conflict detection).
  • mcp_allowed_commands — allowlist for MCP server commands from agent config.
  • base_allowed_commands — host’s tools.shell.allowed_commands. Used to emit a non-fatal warning when a plugin overlay would be silently dropped at load time (tighten-only invariant).
Source

pub fn with_download_timeout_secs(self, secs: u64) -> Self

Override the HTTP download timeout used by Self::add_remote.

Each phase (connect and body read) is independently bounded by this value. The default is 30 seconds.

Source

pub fn add(&self, source: &str) -> Result<AddResult, PluginError>

Install a plugin from a local directory path.

§Errors

Returns PluginError if the manifest is invalid, the source cannot be read, there are skill name conflicts, MCP commands are not allowlisted, or config overlay keys are not in the tighten-only safelist.

Source

pub async fn add_remote( &self, url: &str, expected_sha256: Option<&str>, ) -> Result<AddResult, PluginError>

Download and install a plugin from a remote URL with optional SHA-256 integrity pinning.

Downloads the archive at url, verifies its SHA-256 digest against expected_sha256 (when provided), extracts it to a temporary directory, and delegates to Self::add.

§Integrity check

When expected_sha256 is Some, the raw archive bytes are hashed with SHA-256 and compared against the expected lowercase hex string. If the digests do not match, PluginError::IntegrityCheckFailed is returned and the archive is never extracted.

When expected_sha256 is None, the archive is extracted without verification. Callers are encouraged to always supply the expected hash; unverified installs are permitted by default for backward compatibility but should be avoided in production.

§Errors
§Examples
let mgr = PluginManager::new(
    "/tmp/plugins".into(),
    "/tmp/managed".into(),
    vec![],
    vec![],
);
let result = mgr.add_remote(
    "https://example.com/my-plugin.tar.gz",
    Some("abc123def456..."),
).await?;
println!("installed: {}", result.name);
Source

pub fn scan_targets( &self, source: &str, ) -> Result<Vec<SkillScanInput>, PluginError>

Collect SkillScanInput entries for each skill in the plugin at source.

Validates the manifest and skill paths without copying any files. The returned inputs are passed to SkillSemanticScanner::scan by the caller (core/commands layer) before the blocking add() call proceeds. This keeps zeph-plugins free of any LLM dependency.

Missing SKILL.md files return PluginError::SkillEntryMissing — a manifest entry with no body is suspicious and treated as a blocking error, not a warning.

§Errors
§Examples
use zeph_plugins::PluginManager;

fn collect(mgr: &PluginManager, source: &str) -> Result<(), zeph_plugins::PluginError> {
    let inputs = mgr.scan_targets(source)?;
    for input in &inputs {
        println!("skill: {} — {}", input.skill_name, input.declared_purpose);
    }
    Ok(())
}
Source

pub fn remove(&self, name: &str) -> Result<RemoveResult, PluginError>

Remove an installed plugin by name.

Refuses to remove the plugin if any enabled plugin depends on it. The caller receives a PluginError::DependencyRequired error with a formatted hint listing the dependents.

§Note on TOCTOU

The dependent check and directory removal are not atomic. In a multi-process environment a concurrent enable of a dependent could race with this remove. This is acceptable for a single-user CLI tool where plugin operations are manual.

§Errors
Source

pub fn list_installed(&self) -> Result<Vec<InstalledPlugin>, PluginError>

List all installed plugins.

§Errors

Returns PluginError if the plugins directory cannot be read.

Source

pub fn collect_skill_dirs(&self) -> Result<Vec<PathBuf>, PluginError>

Returns all skill directory paths from installed plugins.

§Errors

Returns PluginError if the plugins directory cannot be read.

Source

pub async fn check_auto_updates(&self) -> Vec<AutoUpdateResult>

Check and apply updates for all installed plugins with auto_update = true.

For each eligible plugin the method:

  1. Reads .plugin-source.toml to retrieve the original download URL and SHA-256.
  2. Downloads the archive from that URL.
  3. Compares the downloaded archive’s SHA-256 with the stored value.
  4. If the hashes differ, stages the new version in a temporary directory adjacent to the plugin root, then atomically swaps via rename — an interrupted update leaves the plugin intact rather than half-deleted.
  5. Returns a result per plugin; failures are warnings and never abort startup.

Plugins installed from local paths (no .plugin-source.toml or no URL) are skipped with AutoUpdateStatus::NoSource.

§Examples
let mgr = PluginManager::new(
    "/tmp/plugins".into(),
    "/tmp/managed".into(),
    vec![],
    vec![],
);
let results = mgr.check_auto_updates().await;
for r in &results {
    println!("{}: {:?}", r.name, r.status);
}
Source

pub fn enable(&self, name: &str) -> Result<(), PluginError>

Enable an installed plugin by removing its .disabled marker file.

Before enabling the target, all plugins listed in plugin.dependencies are enabled recursively (depth-first). The method detects dependency cycles and returns PluginError::DependencyCycle before touching the filesystem.

A plugin with no .disabled marker is considered already enabled; this method is a no-op for such plugins (idempotent).

§Errors
Source

pub fn disable( &self, name: &str, force: bool, ) -> Result<DisableResult, PluginError>

Disable an installed plugin by creating a .disabled marker file.

Refuses to disable the plugin if any enabled plugin depends on it, unless force is true. When force is true the operation proceeds regardless, and the returned DisableResult lists the dependents that were overridden so callers can warn the user.

Disabling an already-disabled plugin is a no-op (idempotent).

§Note on TOCTOU

The dependent check and marker creation are not atomic. In a multi-process environment a concurrent enable of a dependent could race with this disable. This is acceptable for a single-user CLI tool where plugin operations are manual.

§Errors
§Examples
let mgr = PluginManager::new(
    "/tmp/plugins".into(),
    "/tmp/managed".into(),
    vec![],
    vec![],
);
// Normal disable — fails if any enabled plugin depends on "my-plugin".
mgr.disable("my-plugin", false)?;

// Forced disable — proceeds even if dependents exist.
let result = mgr.disable("my-plugin", true)?;
if !result.forced_over_dependents.is_empty() {
    eprintln!("Warning: disabled despite dependents: {:?}", result.forced_over_dependents);
}
Source

pub async fn add_remote_ephemeral( &self, url: &str, sha256: Option<&str>, ) -> Result<TempDir, PluginError>

Download a plugin archive and load it as a session-scoped ephemeral plugin.

Unlike Self::add_remote, this method:

  • Requires https:// (never http://) via validate_url_scheme_ephemeral.
  • Extracts into a tempfile::TempDir that is never copied to the permanent plugins store.
  • Runs a blocking (non-advisory) SKILL.md injection scan before returning.
  • Returns ownership of the TempDir. The caller must hold it for the session duration; dropping it cleans up the extracted archive automatically.
§Security invariants
  • Never accepts http:// or any scheme other than https://.
  • Never writes to self.plugins_dir.
  • Never applies config overlays from the plugin manifest.
§Errors
§Examples
let mgr = PluginManager::new(
    "/tmp/plugins".into(),
    "/tmp/managed".into(),
    vec![],
    vec![],
);
let _temp = mgr.add_remote_ephemeral(
    "https://example.com/my-plugin.tar.gz",
    Some("abc123def456..."),
).await?;
// _temp stays alive for the session; drop it to clean up

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more