5.7 KiB
Plugin CLI Reference
toolctrl-cli plugins ... — the author + operator command surface
for the plugin pipeline. Operator subcommands manage installed-plugin
rows; author subcommands scaffold / build / sign / lint / publish.
toolctrl-cli plugins --help
Author subcommands
scaffold
toolctrl-cli plugins scaffold \
--name slack-notifier \
--publisher de.acme \
--description "Send tenant events to Slack" \
[--target-dir ./my-plugins] [--dry-run]
Generates a new plugin skeleton under <targetDir>/<name>/:
slack-notifier/
├── plugin.toml # manifest seed (placeholder sig/hash)
├── src/index.ts # HookHandler example
├── package.json # @toolctrl/plugin-sdk devDependency
├── tsconfig.json # ES2022 strict
└── README.md # next-step checklist
--dry-run prints the file list without writing anything.
build
toolctrl-cli plugins build [--cwd ./slack-notifier] [--output plugin.wasm]
Detects the project's wasm toolchain (AssemblyScript via asconfig.json,
TinyGo via go.mod, Rust via Cargo.toml) and prints the canonical
build command. Does NOT auto-execute — pipe the command into your CI or
run it yourself. Exits non-zero when no toolchain marker is found.
sign
toolctrl-cli plugins sign \
--wasm plugin.wasm \
--manifest plugin.json \
--key ./keys/publisher.priv \
[--key-id staging-2026-05] \
[--out-sig plugin.wasm.sig]
- SHA-256-hashes the wasm.
- Builds the same canonical manifest projection used by the runtime verifier
(sorted-keys JSON minus
signature/signedBy/wasmSha256). - Signs
<canonical>:<wasmSha256>with the ed25519 private key. - Writes the raw signature hex to
<wasm>.sig(or--out-sig). - Prints the
wasmSha256/signature/signedBylines to splice back into the manifest.
The private key is read from disk and never echoed. --key-id is
logged only — useful for operators to track which signing key produced
which artifact.
lint
toolctrl-cli plugins lint --manifest plugin.json
Runs the host Zod schema, then layers author checks on top:
manifest_invalid(error) — Zod rejected the manifest.unknown_capability(error) — capability not in the host registry.invalid_host(error) —allowedHostsentry is not a hostname.unsigned_placeholder(warn) — signature/signedBy still zeros.unsigned_wasm(warn) — wasmSha256 still zeros.http_outbound_unrestricted(warn) —http.outbounddeclared butallowedHostsis empty.high_risk_capability(warn) —db.writedeclared.
Exit codes:
| Code | Meaning |
|---|---|
| 0 | Clean |
| 1 | At least one error |
| 2 | Warnings only |
publish
toolctrl-cli plugins publish \
--wasm plugin.wasm \
--manifest plugin.json \
[--signature plugin.wasm.sig] \
[--out ./dist]
Packs plugin.wasm + plugin.json (+ plugin.wasm.sig if present)
into <pluginId>-<version>.tar.gz ready for operator upload.
Local-only — no HTTP push. Operators must inspect what they
upload; auto-uploading would hide bad signatures from review.
Operator subcommands
| Command | Action |
|---|---|
plugins list |
List installed plugin rows (filter by tenantId + enabled state) |
plugins enable <tenantId> <pluginId> |
Enable an installed plugin |
plugins disable <tenantId> <pluginId> |
Disable an installed plugin |
plugins uninstall <tenantId> <pluginId> |
Operator-only deletion of the tenant Plugin row (cascades user configs) |
plugins verify-audit-chain <tenantId> [--pluginId <pluginId>] [--from <iso>] [--to <iso>] [--json] |
Verify the tenant-wide PluginCapabilityUse hash chain; --pluginId reports matching rows without slicing the chain |
These operate against the host database directly; they are intended for incident-response and don't substitute the tenant-admin UI. Uninstall is deliberately available only through this operator CLI path until plugin-owned data cleanup/down-migration semantics are reviewed.
Operator lifecycle commands use the same production gates as the service path:
production enables require PLUGIN_RUNTIME_PRODUCTION_ENABLED=true, reject
PLUGIN_REQUIRE_CAPABILITY_CONSENT=false, reject capabilities whose host
bindings are not production-reviewed, check current grants before enabling, and
write lifecycle audit events transactionally with the mutation. Disable remains
available as the emergency containment path.
Permission split: tenant routes and UI use app.plugins.use for installed
plugin reads, per-user non-secret settings, and settings/audit surfaces.
app.plugins.manage is still required for tenant API/UI install,
grant/revoke, enable/disable, and future update flows. CLI commands are operator
escape hatches against the host database and do not replace the tenant
permission model; CLI uninstall is not exposed as a tenant permissioned
API/UI flow.
Round-trip smoke
toolctrl-cli plugins scaffold --name smoke --publisher de.smoke --dry-run
toolctrl-cli plugins build --cwd ./smoke
# (run the printed build command in your wasm toolchain)
toolctrl-cli plugins lint --manifest ./smoke/plugin.json
toolctrl-cli plugins sign --wasm ./smoke/plugin.wasm --manifest ./smoke/plugin.json --key ./keys/dev.priv
toolctrl-cli plugins publish --wasm ./smoke/plugin.wasm --manifest ./smoke/plugin.json
The output is the file you upload through the tenant-admin plugins settings page.