Develop a plugin
For: plugin authors.
Use the local development loop to reload a plugin while you edit it. Before releasing, test the candidate on a cluster, inspect the proposed install and run its checks. The companion pages cover those separate stages and failure drills.
The dev loop
oz0 dev # in the plugin's folder, or: oz0 dev ./my-plugin
dev lints the plugin, starts a dev server on free ports and a node, installs the plugin from its working tree (no commit needed) and waits until the node reports its functions. Then it watches the folder and reinstalls the plugin within seconds of every change.
Dev server ready. In another terminal:
export OZ0_CONTEXT=/tmp/.../operator/context.json
Web UI: https://127.0.0.1:... (user admin, password in /tmp/.../admin-password)
Temporal UI: http://127.0.0.1:...
Node started (log: /tmp/.../node.log)
my-plugin dev-... ready in 14.0s: my_plugin.hello
Call a tool:
oz0 tool call my_plugin.hello --args '{}'
Watching for changes; Ctrl+C stops.
my-plugin dev-... reloaded in 8.5s: my_plugin.hello, my_plugin.goodbye
When the tenant's tool policy would not let one of the plugin's functions run freely, dev says so after it installs the plugin, with the fix:
The tenant's tool policy does not let these functions run freely:
my_plugin.save waits for a person's approval at each call by the tool policy: it is undeclared (no destructiveHint)
fix: add destructiveHint: false (or true) to my_plugin.save, or readOnlyHint: true if it only reads; or let it run: oz0 policy allow my_plugin.save
Ctrl+C or SIGTERM stops the dev server and its node within about ten seconds, and a second one at once. On Linux the node also stops when dev is killed.
ANTHROPIC_API_KEY and OPENAI_API_KEY from your environment become the dev tenant's keys. Without one, dev says how to store a key or serve a local model. In a folder without a plugin, dev makes a first run with the hello agent on recorded answers instead.
What it needs:
orchestrator-zero-nodenext to the server binary, onPATHor in an installed server'sdist/folder, or--node-binary.- A runtime bundle:
--runtime-bundle, or$OZ0_RUNTIME_BUNDLE, or the newest in./dist/runtime, or an installed server'sdist/folder.
The node's uv, Python and Node.js are cached between runs (--cache-dir), so only the first start is slow. The server starts empty every time.
The dev server's gateway sends model calls to the providers' public APIs. --llm-anthropic-url and --llm-openai-url (or OZ0_LLM_ANTHROPIC_URL and OZ0_LLM_OPENAI_URL) send them somewhere else instead, such as a company proxy or a stand-in for tests; the docs' own test runs the quickstart that way.
Secrets in the plugin's requires.secrets come from your environment: dev and plugin test copy each one that is set into the dev server's tenant before installing, so export JEV_API_KEY=... before oz0 dev is all a tool with a key needs. A declared secret that is not set keeps the node from starting the plugin, and dev says which.
On a machine with sandbox tier 1, the dev node runs your plugin's servers as an installed plugin's run: in a sandbox that reaches only the hosts of your manifest's egress: and the tenant's list, so a host you forgot to declare is refused here first, with what to add. A tool that says sandbox: host or sandbox: own runs, since you are the operator of that node. A stand-in for your API on localhost cannot be reached from that sandbox: start oz0 dev or oz0 plugin test with --plugin-sandbox auto, which keeps the host's network.
The sandbox's only way out is the egress proxy, which its HTTP_PROXY and HTTPS_PROXY point at, and Node.js is told to use them with NODE_USE_ENV_PROXY=1. A client that ignores them, such as a raw socket, fails with a name lookup error: no refusal, no denial on record, and nothing for oz0 egress learn to propose.
Plugins it uses
A plugin that calls another plugin's tools or agents declares them under uses in its manifest. dev installs those plugins in the dev tenant before the working copy, and grants the working copy what its uses asks for there, as an admin's install with --accept-uses would:
oz0 dev ./pr-reviewer --with ../github # repeatable: --with ../a --with ../b
Dependency github dev-9c41... ready in 12.3s (from /home/you/github)
Granted in the dev tenant: pr-reviewer wants github.get_pull and delegation to github, from plugin github
pr-reviewer dev-4be0... ready in 8.1s: pr_reviewer.review
Watching for changes; Ctrl+C stops.
Dependency github dev-0d2a... reloaded in 7.9s (from /home/you/github)
- A folder given with
--withis installed as it is on disk and reloads on every change, like the working copy, so you can change both at once. - A
usesentry that no--withfolder gives is installed from itssource(such assource: hub:ai.oz0/github), from the dev server's trusted plugin indexes. - An entry with neither is reported: its tools and agents are not in the dev tenant until you give its folder.
- When the installed version is outside the range the entry asks for,
devsays so; the install's own check decides. - The grant is the dev tenant's only, and its audit log records it (
uses.grant). On a real cluster a tenant admin grants it at install.
plugin test --with ../github installs them the same way before it runs the evals.
Call a tool by hand
Agents call tools for you. To call one yourself:
oz0 tool call my_plugin.hello --args '{"name": "you"}'
job tool-1c9e... calls my_plugin.hello on node node-91378eb09343
tool:my_plugin.hello → my_plugin.hello {"name":"you"}
tool:my_plugin.hello ← my_plugin.hello Hello, you!
Hello, you!
trace: oz0 job trace tool-1c9e..., https://127.0.0.1:40121/jobs/tool-1c9e...
The call is a job of its own, tool-<id>, named tool:<function> in job list, on an online node of the tenant whose plugin has the function ready (--node <id> picks one, --tenant the tenant). It goes through the same checks as an agent's call: the tenant's tool policy, the plugins' pre_tool_call and post_tool_call hooks, and approvals, so a call that needs one waits until someone decides it (oz0 approval list). A refused call fails with the rule that refused it and how to fix it.
It starts like any job, too: kill switches of the tenant and of the tool stop it, and it counts against your quotas. --id names the job; an ID a running job has is refused, as are the IDs of routines, events and chat channels. A node that runs a private candidate refuses calls of other jobs, this one included. --json prints the job.
Run the MCP server by hand
Your tool is a normal MCP server, so you can also run it outside Orchestrator Zero:
uv run python -m my_plugin.server # speaks MCP over stdio
Any MCP client or inspector can talk to it.
Related tasks
- Check a plugin candidate: Test a candidate against a tenant before installing it.
- Lint and test a plugin: Check plugin definitions and run their declared tests.
- Run failure drills: Exercise failures and turn written procedures into tested plugin work.
Previous section links
- Section: a changesets candidate check
- Section: a failure drill
- Section: lint
- Section: names of your own
- Section: preview an install against a real tenant
- Section: start from a written procedure
- Section: test it
- Section: the fake and testsdrillyaml
- Section: try it on a real cluster
- Section: what your tools answers cost