Start with the root plugin overview for the model and the manifest schema. This doc covers the CLI surface: installation flags and errors, the plugins subcommand tree, the JSONL dispatch protocol your plugin's binary must speak, and the on-disk layout.
objectiveai plugins install --owner <o> --repository <r> [--commit-sha <s>] [--upgrade] [--allow-untrusted]
| Flag | Meaning |
|---|---|
--owner |
GitHub owner (org or user). Matched against the install whitelist. |
--repository |
GitHub repository name. Becomes the plugin's identifier on disk. The literal objectiveai (case-insensitive) is reserved. |
--commit-sha |
Pin the manifest fetch to a specific commit. Defaults to the repo's default branch (HEAD). |
--upgrade |
Replace an existing install. Without this flag, install refuses when <plugins_dir>/<repository>.json already exists. With it, the existing binary, viewer/, and .json are deleted before the new install runs. Extra runtime data under <repository>/ is preserved either way. |
--allow-untrusted |
Bypass the install whitelist. Emits a warn-level notification before proceeding. |
| Error | When |
|---|---|
ReservedRepositoryName |
--repository is objectiveai (any casing). |
PluginNotWhitelisted |
The (owner, repository, commit_sha or HEAD, manifest.version) tuple doesn't match any whitelist entry, and --allow-untrusted was not passed. |
AlreadyInstalled |
A sibling .json exists at <plugins_dir>/<repository>.json and --upgrade was not passed. |
ManifestBadStatus / BinaryBadStatus / ViewerZipBadStatus |
The corresponding GitHub asset returned a non-2xx status. |
ManifestParse |
The fetched objectiveai.json didn't deserialize. |
BinaryWrite / ManifestPersist / ViewerZipExtract / Chmod |
A disk-write step failed. |
The full enum lives in objectiveai-rs/src/filesystem/plugins/install_error.rs.
The install pipeline is:
- Platform check (no disk touches).
- Network phase — fetch the manifest, the platform binary, and the viewer zip (if declared) into memory.
- Pre-flight refusal if
--repositoryis reserved or already installed (without--upgrade). - If
--upgrade, delete the prior install's three artifacts (binary,viewer/, manifest sibling). tokio::try_join!three concurrent writes: binary + chmod, viewer-zip extract, manifest persist.
A network failure leaves the disk in whatever state step 4 left it — empty if --upgrade, untouched if a fresh install. A write-phase failure can leave partial new state; re-running with --upgrade cleans up.
Default whitelist allows the ObjectiveAI GitHub org only:
{ owner: "(?i)ObjectiveAI", repository: ".*", commit_sha: ".*", version: ".*" }
Each field is a regex matched anchored (^…$). The owner pattern uses (?i) for case-insensitive matching because GitHub usernames are case-insensitive.
objectiveai plugins list [--offset 0] [--limit 100]
Returns the names of installed plugins that have a viewer_zip (i.e. those that would surface as a viewer tab). Sorted by manifest mtime descending. Pagination matches the convention used by the logs list commands.
objectiveai plugins get <name>
Returns the full persisted manifest for one plugin (a ManifestWithNameAndSource), or {"plugin": null} if no manifest exists at <plugins_dir>/<name>.json.
Two equivalent forms:
# Top-level catch-all — any unknown top-level subcommand is treated as a plugin.
objectiveai <name> <args…>
# Namespaced — explicit form, useful when the plugin name shadows a built-in.
objectiveai plugins <name> <args…>
<args…> is passed verbatim. The shell tokenizes; clap doesn't parse flags inside these dispatched calls. So:
objectiveai psyops --topic "wave physics" --steps 5
reaches the plugin's main() as argv = ["psyops", "--topic", "wave physics", "--steps", "5"].
When the host dispatches your plugin, it spawns the binary at <plugins_dir>/<repo>/plugin[.exe] with:
- argv =
[binary_path, ...trailing_args](the same args the user typed). - stdin =
/dev/null(no stdin protocol yet). - stderr = forwarded raw to the host's stderr.
- stdout = parsed one line at a time as JSONL
PluginOutput.
Internally tagged on "type" (snake_case). Defined in objectiveai-sdk-rs/src/cli/plugins/output.rs.
Unparseable lines (anything that isn't valid JSON or doesn't match a PluginOutput variant) are forwarded as string-valued notifications to the host's output — they still appear in the stream rather than being silently dropped.
The host wraps its own output with {"type":"begin"} and {"type":"end"} lines. Plugins do not emit these — they're added by the host's outer run() function for every cli invocation.
objectiveai-cli/test-fixtures/hello-plugin/ is the minimal example: a single main.rs that reads argv[1] and emits one {"type":"notification","hello":"<arg>"} line. Used by the cli's e2e dispatch test.
After objectiveai plugins install --owner X --repository my-plugin:
<plugins_dir>/ (~/.objectiveai/plugins on Unix)
├── my-plugin.json ← persisted ManifestWithNameAndSource (name + source URL + manifest)
└── my-plugin/
├── plugin ← native binary (plugin.exe on Windows; 0o755 on Unix)
├── viewer/ ← optional, only if viewer_zip declared
│ ├── index.html
│ └── assets/…
└── … ← any other files the plugin's runtime created
(preserved across `--upgrade`)
The cli's resolve_plugin(name) function looks at <plugins_dir>/<name>/plugin[.exe] for the dispatch target. The sibling <name>.json is what plugins list / plugins get consume.
Both files are tracked as "install data"; everything else under <my-plugin>/ is "extra data" the plugin's runtime is welcome to use for state. --upgrade deletes the install data only.