Install & publish plugins (git URLs)
Plugins can live in their own GitHub repo and be installed by URL — so you can make one and share it, and pull others in. A plugin repo is a full bundle: it can contribute tools, subagents, SKILL.md skills, workflows, console views, routes, MCP servers, and config — all from the one repo. See ADR 0027 for the design + safety model.
Install one
CLI:
python -m server plugin install https://github.com/owner/protoagent-plugin-x --ref v1.0
python -m server plugin list
python -m server plugin uninstall protoagent-plugin-x # code + lock + enabled ref
python -m server plugin uninstall protoagent-plugin-x --purge # also config section + secrets
python -m server plugin sync # re-clone the locked set (CI / fresh checkout)
python -m server plugin install-deps protoagent-plugin-x # explicit, separateUninstall removes the plugin's code, its plugins.lock entry, and its plugins.enabled reference (so nothing dangles). It keeps the plugin's config section + secrets by default (a reinstall restores your settings); pass --purge to remove those too. Declared pip deps are never auto-removed (shared venv) — they're reported so you can pip uninstall them if unused.
Console: the Plugins section → Download — paste the URL, review the manifest + capabilities, install, uninstall.
Installing from the console AUTO-ENABLES + runs the plugin (trust-by-default): it's added to plugins.enabled and hot-reloaded, so its tools, console views and background surfaces come up live — no separate enable step and no restart. Its declared settings are live too: the row's Configure dialog carries the plugin's config fields immediately after install (both the Discover directory and install-from-URL — no page refresh needed, #1643). The console flashes a one-time "this runs code on your machine" confirm for unofficial sources first (official protoLabsAI/* installs skip it; "don't show again" flips to full trust). Only install code you trust — for untrusted code, use an MCP server.
The CLI
plugin installstays fetch-only by design (install ≠ enable) for reproducible/scripted setups — enable explicitly viaplugins.enabled. SetPROTOAGENT_PLUGIN_INSTALL_NO_ENABLE=1to make the console behave the same way.
plugins:
enabled: [protoagent-plugin-x] # the console auto-adds this for you on installInstall pins the resolved commit SHA and records it in a committed plugins.lock, so plugin sync reproduces the exact set. The code itself is gitignored (re-cloned from the lock). On a fresh checkout (or a restored data dir) the console flags each locked-but-missing plugin and offers a one-click Sync plugins button (POST /api/plugins/sync) — the same re-clone the CLI does; plugins that are already in plugins.enabled come up live on the spot.
Upstream protoAgent ships the lock empty — a fresh clone starts with no third-party plugins, by design. Your installs append to it; forks and deployments commit their lock so their plugin set reproduces on every checkout. (That means the upstream developer's own installs show as a local diff on plugins.lock — expected; commit or discard as you see fit.)
Keep one up to date
Because the lock pins a commit SHA, an installed plugin doesn't move until you update it. The console surfaces this for you: the Plugins rail (Local tab) and Settings → Integrations show a freshness badge next to each plugin's version —
- up to date — the locked SHA matches the latest commit on its ref
- update available — the remote ref has moved ahead → an Update button appears
- pinned — the plugin was installed at a specific commit SHA (
--ref <sha>), so it intentionally never auto-updates (update it by reinstalling at a new ref) - check failed — the remote couldn't be reached (the row still works)
Clicking Update pulls the latest code at the plugin's recorded ref, rewrites the lock with the new SHA, and — if the plugin is enabled — hot-reloads it in place. A plugin that contributes a console view or background surface can't swap its already-mounted router live, so updating it recommends a restart to finish loading the new view (the UI tells you when).
The freshness check runs git ls-remote against the recorded source_url and is timeout-bounded + briefly cached, so it never hangs the panel. Pinned plugins skip the network entirely.
Keep a bundle fresh (the pin lifecycle)
A bundle (ADR 0040) pins each member so the combo it installs is the combo that was verified together — but a pin that nothing re-verifies rots silently: the first real bundle shipped pins that predated its members' console-view fixes, and every agent spawned from the archetype got 404 panels. ADR 0049 gives the pin a lifecycle that keeps "last verified working" literally true:
- Pin release tags, not raw SHAs (
ref: v0.1.1) — legible, and the freshness check above can follow them (annotated tags compare by peeled commit). - Record
verified_against:— the core version the pin set was last verified on. - Let CI own the pin — a verify job installs the manifest's pin set into a scratch agent and probes every declared console view on each PR + weekly, and a scheduled bump job opens a PR when a member tags a new release.
Start from the in-repo template — manifest, verify + bump scripts, and the GitHub workflow, with the rules commented inline: examples/bundles/template/.
Publish one
Start from the devkit. Enable the bundled
plugin-devkitplugin (plugins: { enabled: [plugin-devkit] }) — it's the canonical full-bundle example and it gives the agent the authoring tools:scaffold_plugin(writes a skeleton and enables it live),reload_plugins(re-exec after you edit it),enable_plugin,scaffold_bundle, plus aplugin-architectsubagent +design-pluginworkflow + thebuilding-pluginsskill. With it on, ask the agent to "build a plugin that …" and it scaffolds, enables, and tests it in the same session — no restart. Prefer the shell?python -m server plugin new "My Plugin" --view --skill(andplugin new-bundlefor an ADR-0040 stack) scaffold without the plugin enabled.
A plugin is a directory (its own repo) with a manifest + a register(). The conventional layout — everything here is picked up when the plugin is enabled:
my-plugin/
protoagent.plugin.yaml # manifest (id, name, version, requires_pip, views, …)
__init__.py # def register(registry): … — tools, subagents, etc.
skills/ # SKILL.md skills — auto-discovered (data, no code)
my-skill/SKILL.md
workflows/ # *.yaml workflow recipes — auto-discovered (data)
my-recipe.yamlregister(registry) contributes the code extensions:
def register(registry):
registry.register_tool(my_tool) # a LangChain tool
registry.register_subagent(my_subagent) # a SubagentConfig
registry.register_router(my_router) # FastAPI routes at /plugins/<id>
registry.register_mcp_server(my_factory) # a managed MCP server
registry.register_chat_command("issue", h) # a user-only /<name> control command
# skills/ and workflows/ are auto-discovered — no call needed. For a
# non-standard location: registry.register_workflow_dir("recipes")register_chat_command(name, handler) lets a plugin own a /<name> chat control command — the generalized form of the core /goal. The handler is async (rest, session_id) -> str | None: return a reply string to short-circuit the turn (the model never runs), or None to pass the message through. It is user-only by design — not an agent tool — so a plugin can expose a write action (file an issue, open a PR) that the model can't trigger autonomously. Close over registry.config to read your own settings. Precedence is goal > lifecycle > plugin command > workflow > subagent > skill; goal and lifecycle are reserved core tokens (a plugin can't claim either).
skills/ and workflows/ are data, so they're auto-discovered from those conventional subdirs — no boilerplate. Console views (a rail icon + page) are declared in the manifest — see Building a plugin view.
Declare pip dependencies (they are not auto-installed — see Safety):
# protoagent.plugin.yaml
id: my-plugin
name: My Plugin
version: 1.0.0
repository: https://github.com/owner/my-plugin
requires_pip: ["httpx>=0.27"]
min_protoagent_version: "0.20.0"min_protoagent_version is enforced at load: the plugin is refused when it needs a newer host. The host version it's compared against is the same shared resolver the A2A agent card advertises (infra.paths.package_version() — the repo pyproject.toml on a source checkout, installed package metadata on wheel/frozen installs), so the gate and the card always agree — a dev checkout's stale editable-install metadata can no longer refuse a valid plugin (#1644).
Get listed in the directory
Anyone can install your plugin from its git URL once it's a public repo. To make it discoverable and feature it on the plugin directory:
- Tag the repo with the
protoagent-pluginGitHub topic — that surfaces it in the topic search across GitHub. - Open a PR adding an entry to
sites/marketing/data/plugins.json(name,tagline,adds,install= your git URL,links). It renders as a card on the directory.
Safety
The model is informed trust + a verifiable supply chain, not a sandbox — an enabled plugin runs in-process as the agent (like a pip dependency). So:
- Install ≠ enable ≠ trust. Installing only fetches code + reads the manifest (data); it never imports the plugin. Enabling (
plugins.enabled) is the trust decision — review the manifest + capabilities first. - Deps are explicit.
requires_pipis declared, never auto-installed (pip runs arbitrary build code). Runplugin install-deps <id>after reviewing them; a missing dep gives a clear "run install-deps" message on enable. - Pinned + reproducible. Installs pin a commit SHA in
plugins.lock. - Optional source allowlist. Lock installs down to trusted orgs:yaml
plugins: sources: allow: ["github.com/yourorg/*"] - Audited. install / uninstall / install-deps are written to the audit log.
- Untrusted code? Use MCP instead — it runs out-of-process and is sandboxable. Git plugins are for code you've reviewed and trust.