Status: normative for crates/evo-plugin-tool (lint / sign / verify / pack / install implemented in v1; this document is the long-term build contract, and a future crate README may still defer here).
Audience: evo-core implementers, packagers, CI and UI integrators. Packaging narrative remains in PLUGIN_PACKAGING.md sections 7 and 9.
The steward binary is evo (/opt/evo/bin/evo). The SDK CLI is evo-plugin-tool in the same directory, built from the workspace crate of the same name. It must call evo_trust’s signing and verification APIs (not reimplemented message formats).
The CLI implements the full author workflow plus operator-side admin sugar that wraps the steward's wire ops:
Author workflow: lint, sign, verify, pack, install. The install path covers ownership / mode promotion via --chown. uninstall and purge are reachable through the operator-side admin uninstall_plugin / admin purge_plugin_state wire ops rather than as separate top-level CLI verbs.
Catalogue tooling: catalogue lint <path> validates a catalogue document (rack / shelf / subject grammar). catalogue validate-shelf-schema [--schemas-path <dir>] walks a per-shelf schemas tree and validates every <rack>/<shelf>.v<N>.toml file. The schemas-path resolution cascade is --schemas-path flag, $EVO_SCHEMAS_DIR, then /usr/share/evo-catalogue-schemas/ (distribution-installed). Non-zero exit on any file failure.
Admin wire-op sugar. Each subcommand opens a Unix-socket connection to the running steward, negotiates the relevant operator capability, and dispatches the matching wire op. The --socket flag overrides the default path.
| Subcommand | Capability | Wire op(s) |
|---|---|---|
admin enable / disable / uninstall / purge_state <plugin> |
plugins_admin |
enable_plugin / disable_plugin / uninstall_plugin / purge_plugin_state |
admin reload catalogue --inline=<toml> | --path=<file> [--dry-run] |
plugins_admin |
reload_catalogue |
admin reload manifest <plugin> --inline=<toml> | --path=<file> [--dry-run] |
plugins_admin |
reload_manifest |
admin reconcile {list,project,now} |
reconciliation_admin (for now) |
list_reconciliation_pairs / project_reconciliation_pair / reconcile_pair_now |
admin flight {list,set <class> <on|off>,all <on|off>} |
(none — uses request op against flight_mode rack shelves) |
project_rack + per-shelf request flight_mode.{query,set} |
admin grammar {list,plan,migrate,accept} |
grammar_admin |
list_grammar_orphans / migrate_grammar_orphans (dry_run for plan) / accept_grammar_orphans |
admin diagnose <plugin> |
(read-only) | list_plugins + on-disk manifest |
The admin grammar plan --from-type=X --to-type=Y form wraps migrate_grammar_orphans with dry_run = true and pretty-prints the planned migration count, target-type breakdown, and first / last sample IDs. The admin grammar migrate form wraps the same op with dry_run = false and accepts --reason, --batch-size, --max-subjects for chunked execution. See CATALOGUE.md §5.3 for the operator surface and CLIENT_API.md §4.17 for the wire shape.
manifest.plugin.nameis the source of truth (reverse-DNS perPLUGIN_PACKAGING.md§4).- After reading
manifest.toml, the install path (and the effective bundle on disk) must use the nameplugin.name. If the archive or stage directory’s top-level folder name differs fromplugin.name, the tool renames (or rewrites the path to)plugin.nameas part of promotion, then places under the target search root, e.g..../plugins/<plugin.name>/. Do not leave a final tree whose directory name disagrees with the manifest.
v1 supports all of:
- A local path to an unpacked bundle directory, or a path under
plugin-stage/(seePLUGIN_PACKAGING§7). - A local file in one of the pack formats (this document section 7 and
PLUGIN_PACKAGING§9). - A http(s) URL to a bundle: full client (TLS, redirects within reason, timeouts, size limit to be chosen in code and documented in
--help; no silent unbounded download).
Failing operations must not leave a partial or half-valid tree inside any configured search_roots.
chown/ owner metadata on the final plugin directory: optional in non-root invocations, required when running as root. Expose a flag such as--chown user:group(exact spelling in clap) when the operator runs the tool with sufficient privilege; if omitted under a non-privileged invocation, the tool does not change ownership (unpack/promote runs with the process’s effective user).- Root-invocation guard. When the tool's effective UID is
0(the typicalsudo evo-plugin-tool install ...shape) and--chownis not supplied,installrefuses with a structured error before any filesystem mutation. Without the guard, the bundle ends uproot:root 0600and a steward running as a non-root service user cannot read its own plugin manifest. Operators pass--chown <user>:<group>to set the runtime account that will own the bundle, or--chown root:rootif the steward really runs as root.installfrom a non-root account proceeds without the guard (it cannot write outside the operator's own permissions anyway).
- Same algorithm and message as the steward:
signing_message, ed25519, 64-bytemanifest.sig, as implemented inevo_trust(and exercised bycrates/evo-trust/tests/verify.rs). - Private key: PEM on disk, passed with
--key, in the formevo_trustand the test harness already use (seeload_trust_root/ signing helpers). Encrypted PEM is out of v1 unless already supported by a shared helper inevo_trustat implementation time; otherwise document "clear PEM only for v1".
- Default trust behaviour matches the shipped steward: same default directories as
StewardConfig/PLUGIN_PACKAGING§5 —/opt/evo/trust,/etc/evo/trust.d, revocations path,degrade_trust, union of keys,*.meta.tomlper key. - Override flags in the CLI (paths for opt + etc trust dirs, revocations, strict/degrade) so CI and laptops can pass temp dirs without a full FHS layout.
- Only the formats in
PLUGIN_PACKAGING§9:.tar.gz/.tgz,.tar.xz/.txz,.zip; default format.tar.gzwhen the implementation must choose. - Sniffing and extension rules: as in that section (magic bytes, extension,
file(1)-class behaviour where practical).
Stable across releases for the same class of error (numeric values fixed in the first public release; document in --help and this table).
| Code | Meaning |
|---|---|
0 |
Success. |
1 |
Usage / flags / bad CLI input (incl. manifest parse where not trust-related). |
2 |
Trust / signature / key authorisation (evo_trust / verification failure, wrong manifest.sig length, etc.). |
3 |
I/O and filesystem (unreadable path, full disk, failed rename, atomic promote failure). |
4 |
Network (URL fetch: DNS, TLS, timeout, size exceeded, non-2xx) when install fetches. |
(If a subcommand has no network path, 4 is unused in that run.)
--outalways wins when the operator passes it.- If
--outis omitted and the implementation invents a default filename: accept both conventions: prefer<plugin.name>-<version>.<ext>when the manifest exposes a suitable version field for the archive name; else<plugin.name>.<ext>(version segment omitted). The implementation documents which manifest field supplies<version>(e.g. plugin or package version per schema).
- Built as
evo-plugin-tool; installed next to the steward,/opt/evo/bin/evo-plugin-tool, as in the filesystem tree inPLUGIN_PACKAGING§3.
The following figures mirror this document; they are not additional normative rules.
flowchart LR
subgraph dev["Author machine"]
D[Plugin bundle directory<br/>manifest.toml + artefact]
D --> L[lint]
L --> S[sign --key]
S --> P[pack]
end
P -->|archive| ship[Ship .tar.gz / .tar.xz / .zip]
ship --> I[install]
subgraph check["Before promote"]
I --> V[verify]
end
V -->|OK| T["search_root / plugin.name"]
install runs the same trust checks as verify (and implied lint via manifest/artefact load). Operators may run verify alone in CI on an unpacked tree.
flowchart TD
SRC{Source?}
SRC -->|local directory| LD[Use bundle path]
SRC -->|local archive| EX1[Extract to temp]
SRC -->|https or http| DL[Download to temp, size cap]
DL --> EX1
EX1 --> B[Single top-level bundle dir]
LD --> M[Read manifest plugin name]
B --> M
M --> N{Top dir name matches manifest?}
N -->|no| R[Promote using manifest name for path]
N -->|yes| R
R --> V[verify trust and signature]
V -->|fail| X[No writes under search roots]
V -->|ok| C[Copy to search root under plugin name]
C --> O[Optional chown on Unix]
Labels avoid :: in raw node text. The code still checks the real Rust types (TrustError, ureq::Error, io::Error).
flowchart TD
E[anyhow Error] --> T{is TrustError}
T -->|yes| c2[exit 2]
T -->|no| U{is ureq Error}
U -->|yes| c4[exit 4]
U -->|no| I{is io Error}
I -->|yes| c3[exit 3]
I -->|no| c1[exit 1]
evo_trust—signing_message,verify_out_of_process_bundle,load_trust_root,RevocationSet, key meta.StewardConfig/docs/engineering/CONFIG.md/SCHEMAS.md§3.3 — default paths forverifyparity.PLUGIN_PACKAGING§7 (Strategy A/B), §9 (subcommands, archive rules).