Skip to content

commands

Thomas Mangin edited this page Apr 8, 2026 · 2 revisions

Pre-Alpha. This page describes behavior that may change.

A plugin exposes commands by declaring them in its Registration at startup and handling them with OnExecuteCommand at runtime. Once declared, the command is visible on every operator surface automatically: the CLI shell, ze cli -c, the MCP server, the REST API, and the Looking Glass (if it is read-only). You do not wire it up five times. You declare it once.

This page covers the author side. The consumer side (how commands flow from the CLI through the dispatcher to the plugin) is in the architecture reference.

Declaring a command

Commands go in the Commands field of sdk.Registration. The engine learns about them during stage 1 (declare-registration).

err := p.Run(ctx, sdk.Registration{
    Commands: []sdk.CommandDecl{
        {Name: "my-plugin status",  Description: "Show current status"},
        {Name: "my-plugin check",   Description: "Trigger immediate check", Args: []string{"target"}},
    },
})
Field Purpose
Name The full command name. Use spaces to group (my-plugin status).
Description One line shown in help and command-list.
Args The expected positional arguments. Used for tab completion and help text.

Handling a command

Register OnExecuteCommand before calling Run(). The handler receives the command serial (a correlation id for the request), the command name, the arguments, and the peer selector if one was provided.

p.OnExecuteCommand(func(serial, command string, args []string, peer string) (status, data string, err error) {
    switch command {
    case "my-plugin status":
        return "done", `{"status":"running","uptime":3600}`, nil

    case "my-plugin check":
        if len(args) < 1 {
            return "error", "usage: my-plugin check <target>", nil
        }
        result := performCheck(args[0])
        return "done", result, nil

    default:
        return "error", "unknown command: " + command, nil
    }
})

Return values

OnExecuteCommand returns (status, data, error).

Success with data. Return a short status string ("done" is the usual choice) and the structured result as a JSON string.

return "done", `{"count":42,"items":["a","b"]}`, nil

Success without data. Return "done" and an empty data string.

return "done", "", nil

Handler error. Return a non-nil error. The SDK sends an error response and the status and data values are ignored.

return "", "", fmt.Errorf("operation failed: database timeout")

User-facing validation failure. Return "error" as the status with an explanatory message in data, and nil for the error. The engine treats the response as a successful RPC but the operator sees the error message.

return "error", "usage: my-plugin check <target>", nil

The distinction matters: a Go error from the handler is a plugin bug, and the engine logs it. A "error" status is an expected failure that the user should see.

The wire surface

Commands are delivered as execute-command RPCs over the plugin connection.

#17 ze-plugin-callback:execute-command {"serial":"abc123","command":"my-plugin status","args":[],"peer":""}
#17 ok {"status":"done","data":"{\"status\":\"running\"}"}

On a handler error:

#17 error {"message":"operation failed: database timeout"}

Fields in ExecuteCommandInput

Field Type Purpose
serial string Correlation id for the request.
command string Command name (e.g. "my-plugin status").
args []string Additional arguments. May be empty.
peer string Peer selector. May be empty.

The peer field is the selector from the CLI (e.g. *, a peer name, or an IP). Use it to narrow your command's effect to the peers the operator targeted.

A few patterns that come up

Forward to a worker. The handler runs on the plugin's event loop. If the command does real work, push the request onto a channel and return a status that says "accepted" or "in progress". The operator can poll for the result with a separate command.

JSON all the way. data is a string, but everything else in Ze treats the contents as JSON. Return JSON even when the output is trivial. It plays better with ze cli ... | json and with the MCP tools.

Tab completion. The Args field gives Ze a hint for tab completion. For dynamic completion (peer names, family names), the engine generates candidates from its own registry, so you usually do not need to do anything extra.

See also

Adapted from main/docs/plugin-development/commands.md.

Home

About

First Steps

Configuration

Operation

Interfaces

Plugins

Plugin Development

Chaos Testing

Blueprints

Development

Reference

Clone this wiki locally