Architecture

The exact contract between the Manager and the CLI, including the single callback endpoint and its command ladder.

CaptainCore splits along one line: the Manager holds state and never opens an SSH connection, the CLI opens SSH connections and holds no long-term state of its own that the Manager cannot rebuild.

Manager to CLI

Every remote operation the Manager wants is an HTTP POST to CAPTAINCORE_CLI_ADDRESS. The dispatcher is CaptainCore\Run in app/Run.php.

Method Path Use
Run::CLI( $command ) /run Run synchronously and return the body
Run::CLI( $command, true ) /run/background Fire and forget
Run::CLI_Stream( $command ) /run/stream Stream raw output, used for binary responses
Run::task( $command ) /tasks Queue a task and get a tracking token back

Every request carries a token header set to captaincore_get_cli_token(), a Content-Type: application/json; charset=utf-8 header, and a 45 second timeout by default.

The body takes one of two shapes, built by Run::build_body():

{ "command": "backup generate mysite-production" }
{ "args": ["backup", "generate", "mysite-production"], "payload": "..." }

The array form is the safer one. The CLI server execs the argv verbatim with no shell and no re-tokenizing. The string form is re-tokenized by the server, which is why Run::safe_environment() exists: it forces an environment name to one of a known list before it reaches a command string.

The server also exposes /task/{id} (GET, PUT, DELETE), /task/{id}/stream, /tasks, /progress, /progress/{pid} and a /ws WebSocket. It authenticates each request by matching the token header against the tokens array in ~/.captaincore/data/config.json. An unknown token resolves to captain id 0.

CLI to Manager

The CLI reports back through exactly one endpoint:

POST /wp-json/captaincore/v1/api

It is registered with 'permission_callback' => '__return_true' and 'show_in_index' => false. Authorization is the token check inside captaincore_api_func() itself:

if ( empty( $post ) || empty( $post->token ) || ! is_string( $post->token )
    || ! hash_equals( (string) captaincore_get_cli_token(), $post->token ) ) {
    return new WP_Error( 'token_invalid', 'Invalid Token', [ 'status' => 404 ] );
}

is_string() is required because a JSON true would type-juggle past a loose comparison, and hash_equals() makes the check constant time. The command field is validated the same way, because a non-string command would satisfy every branch of the ladder at once. A site_id that does not resolve to a site is rejected, except for the three commands that are not site-scoped.

The command ladder

captaincore_api_func() dispatches on $command. These are the names it accepts:

Command What it does
copy Sends the site-copy completion email
production-to-staging Sends the push-to-staging completion email
staging-to-production Sends the push-to-production completion email
snapshot-add Records a generated snapshot
backup-download-notify Notifies that a backup download is ready
core-update-run Records a fleet core-update run
monitor-notify Uptime monitor notification
malware-alert Malware detection alert
capture-alert Visual capture alert
token Returns a token for the supplied token_key
update-fathom Writes Fathom analytics data
update-site Updates a site record
update-environment Updates an environment record
sync-data The main inventory sync: plugins, themes, users, core, checksums
session-snapshot Stores a user-session and privilege snapshot
new-capture Stores a new visual capture and refreshes the environments cache
site-get-raw Returns the raw site row
site-delete Deletes a site
account-get-raw Returns the raw account row
configuration-get Returns global configuration
default-get Returns global site defaults
providers-list-raw Returns all provider rows
usage-update Updates visits and storage on an environment

configuration-get, default-get and providers-list-raw are the three that run without a valid site_id. They are how captaincore configuration sync and captaincore default-sync pull fleet settings into the CLI’s SQLite database.

The bootstrap endpoint

There is a second CLI-facing route, POST /wp-json/captaincore/v1/cli/connect. Its permission callback accepts either an authenticated administrator (Basic Auth with an application password, used by the first captaincore connect) or the CLI token in the JSON body (used by captaincore connect --sync).

It returns the CLI token, api_url, gui_url, configurations, defaults, and bulk SELECT results for sites, environments, accounts, providers, domains and the three account junction tables. This is the whole fleet in one response, which is why the token gate on it is written the same constant-time way as /api.

Why the Manager never touches customer servers

There is no SSH client in the plugin. Sites store their connection details in wp_captaincore_environments, and those details are read by the CLI, not used by the Manager. Anything that has to happen on a customer server becomes a Run::CLI dispatch.

The practical consequences: the WordPress host does not need outbound SSH, does not need rclone or restic, and does not hold the long-running processes. A backup that takes twenty minutes is a task on the CLI server, polled through /task/{id}, not a PHP request holding a connection open.

What runs on cron

Renewal and billing processing runs from a real system crontab rather than WP-Cron. The captaincore_cron action fires captaincore_cron_run():

CaptainCore\Accounts::auto_switch_plans();
CaptainCore\Accounts::process_renewals();
CaptainCore\Accounts::process_pending_ach_payments();
CaptainCore\Scripts::run_scheduled();

The activator carries an explicit note not to re-add wp_schedule_event() for this, because traffic-dependent WP-Cron timing caused concurrent-run races.

Fleet operations run as WP-CLI commands, typically from cron on the fleet server. The Manager registers:

captaincore web-risk-check, scheduled-reports, top-plugins, scan-queue, component-queue, update-queue, restic-cache, security-log-sizes, error-log-sizes, mu-manifest-generate, dns, mailgun, provider-sync, remote, site-label, session-alerts and core-update-runs.

On the CLI side, scheduled work is whatever you put in the server’s crontab. captaincore cron does not schedule anything; it prints the scheduled_tasks block from the synced global configuration.

Two Mailgun actions are also scheduled through WordPress hooks: schedule_mailgun_verify and schedule_mailgun_retry.