Skip to content

Plugin System

Animus plugins are standalone executables that communicate with the host over newline-delimited JSON-RPC 2.0 on stdin/stdout. The host never loads third-party code into the daemon process as a dynamic library.

Source Files

AreaSource
Wire protocol typescrates/animus-plugin-protocol/src/lib.rs
External provider/session protocol cratesanimus-provider-protocol, animus-session-backend from launchapp-dev/animus-protocol declared in the workspace Cargo.toml
Plugin discoverycrates/orchestrator-plugin-host/src/discovery.rs
Plugin host/routercrates/orchestrator-plugin-host/src/host.rs
Subject routercrates/orchestrator-plugin-host/src/subject_router.rs
Lockfile and signature checkscrates/orchestrator-plugin-host/src/lockfile.rs, crates/orchestrator-plugin-host/src/signature_verifier.rs
Provider session bridgecrates/orchestrator-session-host/src/
Curated default pluginscrates/orchestrator-core/src/plugin_registry.rs
Web plugin resolutioncrates/orchestrator-cli/src/services/operations/ops_web.rs

Design Rules

  • Compatibility is defined by the wire protocol, not Rust crate linkage.
  • Discovery uses --manifest; runtime use starts with initialize.
  • Plugin processes start with env_clear().
  • The host forwards only a small base env allowlist, manifest-declared env vars, and request-local extras.
  • Daemon-managed subject, provider, trigger, log-storage, and health-probe spawns pin plugin cwd to project_root so cwd-relative state resolves predictably under .animus/.
  • Manifest probe failures become discovery warnings so operators can see why a binary was skipped.
  • Install state records enough metadata to explain where a plugin came from and which binary hash was approved.

Plugin Kinds

KindRoleRequired method family
providerDrives an AI provider or CLIagent/run, agent/resume, agent/cancel, health/check
subject_backendLists, reads, updates, and optionally watches work subjectsHost routes kind-scoped calls such as <kind>/list, <kind>/get, <kind>/update; control surfaces expose subject/*
trigger_backendWatches external event sources and emits dispatchable trigger eventstrigger/watch, trigger/event, trigger/ack
transport_backendHosts an inbound control transport such as HTTP or GraphQLtransport/start, transport/info or initialize metadata, transport/shutdown, health/check
web_uiLegacy/compat UI wrapper kindTreated as a browser-facing transport by animus web
log_storage_backendReceives and serves structured log entrieslog/entry, log_storage/tail
customPlugin-owned behavior, usually bridged to MCPAny plugin-defined method plus lifecycle methods
task_backendLegacy aliasReserved; new plugins should use subject_backend

transport_backend plugins that serve a browser UI advertise the $ui/web capability. animus web open uses that marker to prefer a UI URL over a raw API URL.

Wire Protocol

Runtime communication is newline-delimited JSON-RPC 2.0:

  1. Host spawns plugin.
  2. Host sends initialize.
  3. Plugin returns InitializeResult with protocol version, plugin info, and capabilities.
  4. Host sends initialized.
  5. Host sends plugin-specific requests.
  6. Plugin sends responses and may emit notifications.

PluginHost owns a single reader task for plugin stdout. Responses are matched to pending JSON-RPC ids and notifications are broadcast to subscribers. This is documented in Plugin Host Concurrency.

Discovery Order

discover_plugins(project_root) uses this order:

  1. Registry entries from ~/.animus/plugins.yaml.
    • Legacy fallback: ~/.config/animus/plugins.yaml is read only when the new registry is absent and ANIMUS_CONFIG_DIR is not set.
  2. Project-local plugin binaries in <project>/.animus/plugins/.
  3. The global install dir: $ANIMUS_PLUGIN_DIR when explicitly set, otherwise ~/.animus/plugins/.
  4. $ANIMUS_PLUGIN_PATH.
  5. $PATH, only when the caller opts into --include-system-path.

Directory scans consider executable names beginning with animus-plugin- or animus-provider-. Registry entries may point to binaries with any name. Plugin names are deduplicated by first match in the precedence chain, and a broken higher-precedence entry still reserves its name so a lower-precedence copy cannot silently shadow it.

Manifest probes are hardened:

  • stdin is closed
  • stdout and stderr are capped at 1 MiB
  • probe timeout is 5 seconds
  • plugin env is scrubbed to the base allowlist
  • failures become DiscoveryWarning rows

Installation State

animus plugin install writes three forms of state:

StatePathPurpose
Binary~/.animus/plugins/ or --plugin-dirExecutable plugin artifact
Registry~/.animus/plugins.yamlLogical name, binary path, install metadata, signature status
Lockfile<project>/.animus/plugins.lock or ~/.animus/plugins.lockApproved version, artifact sha256, optional signature-bundle sha256

The lockfile prevents silent binary replacement during later upgrades. animus plugin lock verify re-hashes installed binaries and reports mismatches.

Supported install sources:

  • public GitHub release: animus plugin install owner/repo[@tag]
  • local binary: animus plugin install --path ./target/release/my-plugin
  • direct HTTPS URL: animus plugin install --url <url> --sha256 <hex>

Direct URL installs require an expected SHA256. Release installs resolve the platform-specific asset, verify SHA256 and signature policy, probe --manifest, then copy the binary into the install directory.

Default Plugin Sets

The curated launchapp-dev defaults are defined in orchestrator-core::plugin_registry so daemon preflight and animus plugin install-defaults cannot drift.

FlagInstalls
no extra flagprovider plugins: Claude, Codex, Gemini, OpenCode, OAI
--include-oai-agentoptional OAI agent provider
--include-subjectsdefault task, requirements, Linear, SQLite, and markdown subject plugins
--include-transportsHTTP transport, GraphQL transport, and web UI

Daemon default preflight requires:

  • at least one provider plugin
  • a subject backend covering task
  • a subject backend covering requirement

--auto-install installs the curated defaults for unsatisfied roles.

Security Boundary

Plugin processes start with env_clear(). The host forwards:

  • base variables: PATH, HOME, USER, SHELL, TERM, TMPDIR, LANG, LC_ALL, RUST_LOG, RUST_BACKTRACE, TZ
  • variables declared in PluginManifest.env_required
  • request-local extras explicitly supplied by the provider session request

If a required declared variable is missing, the host logs a warning but still spawns the plugin. The plugin decides whether startup should fail.

Release-source installs support cosign keyless verification. Policy modes:

ModeBehavior
strictMissing, invalid, or untrusted signatures fail the install
warnVerification failures are recorded and warned, but install continues
disabledSignature verification is skipped

Additional install guards:

  • manifest name should match the repository basename unless --force is used
  • unknown GitHub owners trigger trust-on-first-use confirmation
  • first-party provider tool names are reserved; installing a plugin that claims them requires --allow-shadow-builtin

Runtime Hosting

PluginHost owns one live plugin process:

  • a single reader task consumes stdout
  • requests are written with JSON-RPC ids
  • pending responses are resolved by id
  • notifications go to a broadcast channel
  • shutdown resolves outstanding requests

The broadcast channel capacity defaults to 256 and can be overridden with:

bash
ANIMUS_PLUGIN_BROADCAST_CAPACITY=512

When PluginSpawnOptions.working_dir is set, the host calls Command::current_dir(...) before spawn. Daemon-owned runtime paths set this to the resolved project_root; ad-hoc plugin commands such as animus plugin ping and animus plugin call still inherit the caller's cwd unless the command itself exposes a project-root override.

Provider Path

Provider plugins are driven by orchestrator-session-host.

  1. Resolve the requested provider tool.
  2. Discover provider plugins.
  3. Spawn and initialize the chosen plugin.
  4. Send agent/run or agent/resume.
  5. Forward provider notifications as runner events.
  6. Keep the active session host so agent/cancel reaches the same process.

Provider dispatch binds the plugin cwd to the resolved project_root, so provider-owned state and any child CLI cwd-relative lookups stay anchored to the repository even when the daemon was started from some other shell directory.

There is no in-tree provider fallback. Missing providers return a hard error with the install command.

The resolver canonicalizes oai-runner and animus-oai-runner to oai. Reserved provider names are claude, codex, gemini, opencode, oai, and oai-runner.

Subject Path

Subject backends expose normalized work items through the animus-subject-protocol schema. The operator and daemon control surfaces use generic verbs such as subject/list and subject/get; the control dispatcher adapts those calls to the kind-scoped plugin methods used by the router.

The host-side router maps subject kinds to initialized plugin hosts:

  • exact kind registrations win over globs
  • glob registrations use kind.*
  • longest glob prefix wins
  • duplicate exact kinds and duplicate glob prefixes fail router setup

The current CLI and daemon path relies on these kind-scoped plugin calls:

Plugin methodPurpose
<kind>/listReturn filtered subjects for dispatch or CLI listing
<kind>/getFetch one subject
<kind>/createCreate one subject when the backend supports mutation
<kind>/updateApply a merge-style patch
<kind>/nextReturn the next runnable subject for a kind
<kind>/statusChange a subject's status

Protocol-level subject backends may also expose schema and watch capabilities, but current routing decisions are made from the initialized plugin manifest and capabilities.subject_kinds.

Subject backend spawns also pin cwd to project_root. Backends that persist state via relative paths such as .animus/subjects/tasks.db therefore resolve those paths under the repository root instead of under the daemon launch shell.

Trigger Path

Trigger plugins are long-lived watchers. The daemon sends trigger/watch with optional cursor and config, then receives trigger/event notifications. Each event carries an event id, optional trigger id, optional subject id/kind, optional action hint, and plugin-owned payload. The host sends trigger/ack after accepting an event.

The trigger supervisor pins cwd to project_root for the same reason: any plugin-relative checkpoints or repo-local config reads stay deterministic.

Trigger supervision has a daemon kill switch:

bash
ANIMUS_DAEMON_DISABLE_TRIGGERS=1 animus daemon start

Transport and Web Path

animus web serve and animus web open use normal plugin discovery, then partition discovered plugins into:

  • transport_backend
  • web_ui

The web command starts transport plugins and opens the UI URL advertised by a plugin with $ui/web capability. There is no in-tree web server. Spawn uses the plugin manifest's env_required contract, so missing required vars fail fast before handshake the same way animus plugin info, ping, and call do.

Operations

Useful operator commands:

bash
animus plugin list
animus plugin install-defaults --include-subjects --include-transports
animus plugin info --name <name>
animus plugin call <name> health/check --json '{}'
animus plugin lock verify
animus daemon preflight

Tests

Use these focused checks when changing plugin behavior:

bash
cargo test -p orchestrator-plugin-host
cargo test -p orchestrator-session-host
cargo test -p orchestrator-cli plugin
cargo animus-bin-check

Released under the Elastic License 2.0 (ELv2).