| title | subtitle | brand |
|---|---|---|
exec-plugins - Contributing |
Requirements, structure, and validation for plugin submissions |
odcc |
Thank you for considering a contribution. This document covers everything needed to submit a plugin: structure requirements, category-specific interface contracts, and how to run the validator before submitting a pull request.
Read the category README for the category you are contributing to before starting. The interface contracts differ by category.
Every plugin lives in its own subfolder under its category:
<category>/<plugin-name>/
README.md
LICENSE
sbom.json
<plugin files>
All three metadata files are required for every plugin in every category. A plugin missing any of them will not pass validation.
README.md
: Documents the plugin. Required headings differ by category (see below).
Written in British English. No index of other plugins; each README covers
only its own plugin.
LICENSE
: The licence for this plugin. The licence may differ from other plugins in
the repository. Document it clearly. MIT is the default if there is no
reason to use another.
sbom.json
: Software Bill of Materials in CycloneDX JSON format. Must be valid JSON
and must contain bomFormat, specVersion, and components fields.
List all runtime dependencies, including system packages. For Debian
packages, use pkg:deb/debian/<package> as the purl.
Minimal sbom.json example:
{
"bomFormat": "CycloneDX",
"specVersion": "1.5",
"version": 1,
"metadata": {
"component": {
"type": "application",
"name": "my-plugin",
"version": "0.1.0",
"description": "One-line description"
}
},
"components": [
{
"type": "library",
"name": "bash",
"version": "5.2",
"description": "GNU Bourne Again shell",
"purl": "pkg:deb/debian/bash"
}
]
}Each category has a specific set of required headings. The validator checks
for these exactly. Use ATX-style headers (## Heading).
Agent scripts (agent-scripts/):
## Purpose
## Dependencies
## Installation
## scripts.conf
## Subcommands
## Examples
## Limitations
Auth hooks (auth/):
## Purpose
## Dependencies
## Installation
## Configuration
## Exit codes
## Examples
## Limitations
Manager plugins (manager/):
## Purpose
## Dependencies
## Installation
## Configuration
## Examples
## Limitations
Scripts receive a JSON context object on stdin. Discard it if not needed:
exec 0</dev/nullScripts must exit 0 on success and non-zero on failure. stdout and stderr are both captured and returned to the caller.
Scripts must use the subcommand pattern: the first argument selects the operation. Calling the script with no arguments must print usage to stderr and exit non-zero. Calling with an unrecognised subcommand must do the same.
Script names must match [a-zA-Z0-9_-]+. This is the allowlist name pattern
enforced by the agent.
Test scripts against a real scripts.conf entry before submitting. Verify
each subcommand runs correctly and that the no-args path exits non-zero.
Hooks receive full request context as environment variables and as JSON on
stdin. Read auth/README.md for the full variable and field reference.
Hooks must handle malformed or empty stdin without crashing - exit 1 (denied) on any unhandled error. This is a hard requirement: ctrl-exec treats a crash the same as a denial, but a hook that produces unexpected output or leaves background processes running creates operational problems.
Exit codes must be exactly 0, 1, 2, or 3. No output on stdout or stderr.
Manager plugins consume the ctrl-exec HTTP API. Do not hardcode hostnames
or script names - use GET /openapi-live.json or GET /discovery for
runtime enumeration.
Document the async pattern (POST /run → GET /status/{reqid}) wherever
the plugin submits long-running scripts. Include the recommended polling
interval and timeout strategy.
Run the validator against your plugin before opening a pull request. The
validator is at tools/validate-plugin in the repository root.
tools/validate-plugin agent-scripts/my-plugin
tools/validate-plugin auth/my-hook
tools/validate-plugin manager/my-managerThe validator checks:
- Required files present (
README.md,LICENSE,sbom.json) - Required README headings present for the category
sbom.jsonis valid JSON with required CycloneDX fields- For agent scripts: script is executable, has a shebang, name matches the allowlist pattern, exits non-zero with output when called with no args
- For auth hooks: hook is executable, has a shebang, exits with a valid code (0–3) on empty stdin and on a synthetic ping payload
A plugin must produce VALID with zero failures before submission. Pull
requests are validated automatically on open and on each subsequent push.
Example of a passing run:
Validating plugin: linux-audit (category: agent-scripts)
Path: agent-scripts/linux-audit
[ Required files ]
PASS README.md exists
PASS LICENSE exists
PASS sbom.json exists
[ README sections ]
PASS README has '## Purpose'
...
Results: 21 passed, 0 failed
VALID
Dependencies : Prefer Debian trixie system packages. If a dependency is not in the Debian trixie package set, document why and what the operator must install manually. OpenWRT scripts may depend on OpenWRT-specific tools.
Testing : Test against the minimum dependency set where practical. Include a worked test run in the README Examples section showing real output from a real host.
Privilege : Document any privilege requirements explicitly. If a script requires root, say so. If it can run as a less-privileged user, show the sudoers rule needed to enable this.
Idempotency : Scripts that modify state should be idempotent where possible. Document any cases where running the same subcommand twice has different effects.
Atomicity : Write operations that modify files should use a write-to-temp-then-rename pattern to avoid partial writes.
British English : All written content - README, comments, output strings - uses British English spelling throughout.