MCTF plugins are external processes that receive lifecycle hook events and can run arbitrary logic (notifications, policy checks, custom provisioning, …) without being compiled into the CLI. They are language-agnostic: any executable that speaks the protocol below works.
Status: foundation + the
before_deploy/after_deployhooks ship now. The remaining hooks are planned — see Hook roster.
A plugin entry is a command the CLI executes during a run. Treat
mctf.config.yml plugins as exactly as trusted as the docker-compose.yml
files beside them — only configure plugins you trust.
Add a plugins: list to mctf.config.yml:
plugins:
- name: audit # display name (defaults to the command)
command: ./plugins/logger.sh # executable; relative paths resolve to the event root
args: ["--verbose"] # optional
enabled: true # optional (default true)
config: # optional, opaque — passed to the plugin at initialize
file: ./audit.logFraming: newline-delimited JSON (one JSON object per line), JSON-RPC 2.0. The host writes requests to the plugin's stdin; the plugin writes responses to its stdout.
stdout is protocol-only. A plugin MUST write nothing but response objects to stdout. All human/debug logging goes to stderr (forwarded to the host's stderr) or a file. A stray stdout line desyncs the stream and the plugin is dropped.
Host → plugin:
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocol_version":1,"mctf_version":"...","config":{...}}}Plugin → host — its name and the hooks it wants:
{"jsonrpc":"2.0","id":1,"result":{"name":"audit","hooks":["before_deploy","after_deploy"]}}For each subscribed hook the host sends a request whose method is the hook
name and whose params is the hook payload; the plugin replies with any result
(the host ignores the body, only success/failure matters):
{"jsonrpc":"2.0","id":2,"method":"before_deploy","params":{ ...payload... }}
{"jsonrpc":"2.0","id":2,"result":{"ok":true}}The host sends {"method":"shutdown"} and closes stdin; the plugin exits.
Plugins never break an MCTF command. A plugin that fails to start, botches the
handshake, errors, crashes, or hangs is logged to stderr and dropped — the
command continues. Each hook call is bounded by a timeout; a hung plugin is
killed. (This mirrors the notify package.)
| Hook | Payload | Status |
|---|---|---|
before_deploy |
deploy payload (event, mode, units, dry_run) | shipped |
after_deploy |
deploy payload + outcome (success/failed) + error |
shipped |
before_build / after_build |
build payload | planned |
on_instance_spawn |
instance payload | planned |
on_health_failure |
— | planned (needs a health-monitoring loop) |
{
"event": "myctf",
"phase": "before", // "before" | "after"
"mode": "docker", // "docker" | "ssh"
"dry_run": false,
"units": [
{"challenge":"web/echo","category":"web","slug":"echo",
"project_name":"mctf-myctf-web-echo","image_tag":"a1b2c3d"}
],
"outcome": "success", // after only
"error": "" // after + failed only
}after_deploy(success) fires only on a real (non-dry-run) deploy;
after_deploy(failed) fires whenever the deploy errors.
A minimal POSIX-sh plugin lives at
examples/sample-event/plugins/logger.sh —
it logs each hook to a file and shows the full request/response contract.