# Plugin Protocol V1
This document defines the executable plugin protocol for `osp`.
Plugins are separate binaries discovered at runtime (Option B).
## Goals
- Keep plugin integration stable across independent releases.
- Let backbone CLI/repl render plugin output consistently.
- Avoid in-process ABI coupling.
## Process Model
- Backbone binary: `osp`
- Plugin binaries: `osp-<id>` (for example `osp-acme-inventory`)
- Transport: subprocess invocation + JSON over stdout
## Required Plugin Commands
- `--describe`
- Prints `DescribeV1` JSON to stdout.
- Stdout must contain only that JSON payload.
- Human diagnostics belong on stderr.
- Exit code 0 on success.
- Normal execution
- Backbone resolves the plugin from the selected top-level command and then
invokes the plugin as `<plugin-exe> <selected-command> <remaining-argv...>`.
- Example: `osp inventory host web-01` invokes the plugin with argv
`["inventory", "host", "web-01"]`.
- Backbone also sets `OSP_COMMAND=<selected-top-level-command>`.
- `OSP_COMMAND` and `argv[1]` carry the same selected command on purpose.
Plugins may use either, but they should agree.
- Stdout must contain exactly one `ResponseV1` JSON payload and nothing else.
- Exit code 0 means backbone should parse stdout as protocol output.
- Non-zero exit always means process-level failure, even if stdout contains
JSON-like data.
## Help Delegation
- `osp <plugin-command> --help` and `osp <plugin-command> help` are passed
through directly to the plugin process.
- For delegated help, backbone does not require `ResponseV1` JSON.
- Plugins may print plain help text to stdout and stderr in this mode.
- Exit code 0 is preferred for normal help output; exit code 2 is acceptable
for usage-style failures.
## Describe Caching (Backbone Behavior)
- Backbone caches successful `--describe` payloads.
- Cache key is `(executable path, file size, file mtime)`.
- Default cache file:
`<platform-cache-dir>/osp/describe-v1.json`.
- On Linux this is typically `~/.cache/osp/describe-v1.json`.
- If `XDG_CACHE_HOME` is set, the Linux cache path is
`$XDG_CACHE_HOME/osp/describe-v1.json`.
## DescribeV1
```json
{
"protocol_version": 1,
"plugin_id": "acme-inventory",
"plugin_version": "0.1.0",
"min_osp_version": "0.1.0",
"commands": [
{
"name": "inventory",
"about": "Inventory lookups",
"subcommands": [
{ "name": "host", "about": "Look up one host", "subcommands": [] },
{ "name": "service", "about": "Look up one service", "subcommands": [] }
]
}
]
}
```
Rules:
- `protocol_version` must be exactly `1`.
- `plugin_id` must be unique within discovery scope.
- `commands[].name` is top-level command claimed by the plugin.
## ResponseV1
```json
{
"protocol_version": 1,
"ok": true,
"data": {},
"error": null,
"messages": [
{ "level": "info", "text": "Using profile: uio" }
],
"meta": {
"format_hint": "table",
"columns": ["uid", "cn"],
"column_align": ["left", "default"]
}
}
```
Rules:
- `protocol_version` must be exactly `1`.
- `ok=true` implies `error=null`.
- `ok=false` implies `error` is present.
- `data` is always present (empty object/array is allowed).
- `messages` is optional and defaults to an empty list.
- `meta.columns` controls column order when the payload is rendered as a table.
- `meta.column_align` is optional and follows `meta.columns` positionally.
Allowed values: `default | left | center | right`.
Message levels:
- `error`
- `warning`
- `success`
- `info`
- `trace`
Backbone behavior:
- plugin `messages` are rendered by `osp-ui` on stderr using the same
grouping/theme/verbosity rules as built-in commands.
- plugin data remains on stdout.
- `ok=false` is still a protocol-level response and must use exit code 0.
- Plugins should use `error` plus optional `messages` for application-level
failures they want backbone to render cleanly.
- Plugins should reserve non-zero exits for process-level failures such as
crashes, missing prerequisites, or transport/setup errors.
## Error Shape
```json
{
"code": "AUTH_FAILED",
"message": "Inventory backend unavailable",
"details": {}
}
```
## Exit Code Guidance
- Inside the plugin process, use exit code `0` for successful protocol
responses, including `ok=false` application-level failures.
- Use non-zero exits only for process-level failures such as crashes, missing
prerequisites, or setup/transport problems.
- When a plugin is invoked through `osp`, non-zero plugin exits are collapsed
into OSP's generic plugin-failure exit family rather than preserving a
plugin-specific numeric taxonomy.
- If callers need to distinguish auth, config, upstream, or application
failures, put that meaning in the structured `error.code` field, not in the
process exit code.
## Compatibility Policy
- Backbone rejects unsupported `protocol_version`.
- Backbone may reject plugins below required `min_osp_version`.
- New fields must be additive and optional.
## Runtime Hints Environment
Backbone injects runtime hints into each plugin subprocess. Plugins can use
`osp_core::runtime::RuntimeHints::from_env()` to parse them.
Required hints:
- `OSP_UI_VERBOSITY=error|warning|success|info|trace`
- `OSP_DEBUG_LEVEL=0|1|2|3`
- `OSP_FORMAT=auto|json|table|md|mreg|value`
- `OSP_COLOR=auto|always|never`
- `OSP_UNICODE=auto|always|never`
- `OSP_TERMINAL_KIND=cli|repl|unknown`
Optional hints:
- `OSP_PROFILE=<active-profile>`
- `OSP_TERMINAL=<raw-TERM-value>`
Additional process env:
- `OSP_COMMAND=<selected-top-level-command>`
## Config-Driven Plugin Env
Backbone can also project selected config values into plugin subprocess env.
This is intended for plugin-specific settings owned by the app config, not for
the runtime hints above.
Config namespaces:
- shared across all plugins:
- `extensions.plugins.env.<name> = <value>`
- scoped to one plugin id:
- `extensions.plugins.<plugin-id>.env.<name> = <value>`
Env mapping rules:
- `<name>` is normalized to uppercase with non-alphanumeric characters replaced
by `_`.
- Backbone prefixes the name with `OSP_PLUGIN_CFG_`.
- Example:
- `extensions.plugins.env.api.url = "https://example"`
- `extensions.plugins.acme-inventory.env.api.token = "sekrit"`
- become `OSP_PLUGIN_CFG_API_URL` and `OSP_PLUGIN_CFG_API_TOKEN`
- plugin-specific values override shared values when they map to the same env
name.
- app-owned plugin config is injected after runtime hints, so later
`OSP_PLUGIN_CFG_*` values intentionally win if a plugin reuses the same env
name across shared and plugin-specific config.
- scalar config values are stringified directly.
- list values are encoded as JSON arrays.
Examples:
- `extensions.plugins.env.api.url = "https://common.example"`
- `extensions.plugins.acme-inventory.env.api.token = "sekrit"`