Summary
Build a generic CLI transport runtime in DMC that registers as a handler for agents/dispatch-message (agents-api v0.107.0+). The runtime reads a channel→command-template config map and shells out via proc_open to deliver outbound messages. Zero per-transport code — adding a new CLI bridge (kimaki, cc-connect, telegram-cli, future) is a config entry, not a plugin.
This is the WordPress-side counterpart to wp-coding-agents's bash-based bridge installers. wp-coding-agents writes the channel config during bridge install; DMC consumes it at dispatch time.
Why DMC, not data-machine
The CLI runtime requires disk-side capability (Environment::has_shell()). That's DMC's gate by design:
- DMC's whole identity is "this site can run external coding-agent runtimes." Subprocess exec is core to that.
\DataMachineCode\Environment::has_shell() is the exact capability check this runtime needs.
- Managed hosts (WordPress.com, VIP) can't install DMC → can't subprocess → the runtime correctly doesn't exist there. The capability gate and the plugin gate align.
agent-call (HTTP webhook) staying in data-machine is correct — any WP can wp_remote_post. CLI exec is fundamentally different.
- The DMC ↔ wp-coding-agents pairing is canonical and already documented (README "Co-located runtime" driver mode).
Current state
There's no consumer of agents/dispatch-message anywhere in the ecosystem yet. The substrate primitive ships on agents-api main (commit 46e40fb, file src/Channels/register-agents-dispatch-message-ability.php), but nothing claims the filter wp_agent_dispatch_message_handler.
On the Extra Chill VPS the gap is currently filled by /opt/agent-ping-webhook/webhook.py — a standalone Python BaseHTTPServer that nginx proxies, which subprocess.Popens kimaki send. It's broken (payload contract drift: receiver reads prompt, sender writes task) and architecturally outside any plugin. This issue is the proper replacement.
Proposed contract
Channel config
A registry keyed by channel name, populated via apply_filters( 'datamachine_code_cli_channels', [] ) and/or get_option( 'datamachine_code_cli_channels', [] ). Each entry:
```php
'kimaki' => [
'command' => 'kimaki', // absolute path or PATH-resolvable
'args' => [ 'send', '--channel', '{recipient}', '--prompt', '{message}' ],
'detach' => true, // background or wait
'timeout' => 600, // seconds; only when detach=false
'env' => [ /* extra env vars */ ],
'cwd' => null, // optional working dir
],
```
Variables substituted into args:
- `{recipient}` — `dispatch-message` input `recipient`
- `{message}` — input `message` (single arg; never interpolated into a shell string)
- `{conversation_id}` — input `conversation_id`
- `{channel}` — input `channel` (rarely needed but available)
Substitution is positional argv replacement, not shell interpolation. proc_open is invoked with an array of args, never a string, so no quoting/escaping pitfalls.
Handler registration
```php
\AgentsAPI\AI\Channels\register_dispatch_message_handler(
[ CliChannelTransport::class, 'dispatch' ],
20 // priority — leave room for higher-precedence handlers
);
```
The handler:
- Checks `Environment::has_shell()`. If false, returns the original `$handler` (lets others claim it). Logs at debug level.
- Looks up `$input['channel']` in the registry. If unknown, returns the original `$handler` (chain continues).
- Returns
[ self::class, 'execute' ].
`execute()` runs the command, returns canonical output:
```php
[
'sent' => true,
'channel' => $channel,
'recipient' => $recipient,
'message_id' => (string) $pid, // or null for synchronous mode
'metadata' => [ 'exit_code' => ..., 'duration_ms' => ..., 'output' => '...' ],
]
```
On failure, return `WP_Error` (the substrate handles that and fires `agents_dispatch_message_failed`).
Detach vs wait
Two modes:
- detach=true (default): `proc_open` with `start_new_session`, return immediately with PID as `message_id`. No wait, no exit code. Fits fire-and-forget scheduled flows. Mirrors `webhook.py`'s current behavior.
- detach=false: `proc_open` with stdout/stderr capture, `proc_close` to get exit code, respect `timeout`. Synchronous, returns full diagnostics in `metadata`.
Security
- Command is never a user-controlled string. It comes from registered config only.
- Args are an array; substitution is positional; no shell metacharacters can be injected via `{message}` etc.
- Environment passed explicitly; no inherited PATH surprises beyond what config specifies.
- Permission gate uses `agents_dispatch_message_permission` filter (agents-api default `manage_options`). DMC can tighten via that filter if needed.
Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ agents-api (substrate, no transport knowledge) │
│ agents/dispatch-message contract + handler filter │
└────────────────────▲────────────────────────────────────────────┘
│ register_dispatch_message_handler()
│
┌────────────────────┴────────────────────────────────────────────┐
│ data-machine-code (THIS issue) │
│ CliChannelTransport — generic subprocess runtime │
│ Gated by Environment::has_shell() │
│ No knowledge of kimaki, cc-connect, telegram, etc. │
└────────────────────▲────────────────────────────────────────────┘
│ writes channel config
│
┌────────────────────┴────────────────────────────────────────────┐
│ wp-coding-agents (separate issue — bash installer) │
│ bridges/kimaki.sh, cc-connect.sh, telegram.sh │
│ Each installs an mu-plugin or option that registers its │
│ channel→command-template entry. │
└─────────────────────────────────────────────────────────────────┘
```
Proposed file layout
```
inc/Channels/
CliChannelTransport.php # the runtime
CliChannelRegistry.php # config lookup + validation
tests/
smoke-cli-channel-transport.php
```
Acceptance criteria
Related
- Substrate: agents-api commit `46e40fb` (
agents/dispatch-message + handler filter, v0.107.0)
- Sibling issue (filed separately): wp-coding-agents — per-bridge config write + legacy
/opt/agent-ping-webhook/ retirement
- Related design space: data-machine #1490 (pluggable agent_call target adapter registry) — adjacent abstraction at the HTTP layer; this issue is the disk-side counterpart living in the correct plugin.
Out of scope
- Migrating `datamachine/agent-call` to also register as a `dispatch-message` handler for HTTP-channel symmetry. Worth doing eventually, separate issue.
- HTTP webhook config (already handled by `agent-call`).
- The legacy
/opt/agent-ping-webhook/ retirement (tracked in the wp-coding-agents issue).
Summary
Build a generic CLI transport runtime in DMC that registers as a handler for
agents/dispatch-message(agents-api v0.107.0+). The runtime reads a channel→command-template config map and shells out viaproc_opento deliver outbound messages. Zero per-transport code — adding a new CLI bridge (kimaki, cc-connect, telegram-cli, future) is a config entry, not a plugin.This is the WordPress-side counterpart to
wp-coding-agents's bash-based bridge installers. wp-coding-agents writes the channel config during bridge install; DMC consumes it at dispatch time.Why DMC, not data-machine
The CLI runtime requires disk-side capability (
Environment::has_shell()). That's DMC's gate by design:\DataMachineCode\Environment::has_shell()is the exact capability check this runtime needs.agent-call(HTTP webhook) staying in data-machine is correct — any WP canwp_remote_post. CLI exec is fundamentally different.Current state
There's no consumer of
agents/dispatch-messageanywhere in the ecosystem yet. The substrate primitive ships on agents-apimain(commit 46e40fb, filesrc/Channels/register-agents-dispatch-message-ability.php), but nothing claims the filterwp_agent_dispatch_message_handler.On the Extra Chill VPS the gap is currently filled by
/opt/agent-ping-webhook/webhook.py— a standalone PythonBaseHTTPServerthat nginx proxies, whichsubprocess.Popenskimaki send. It's broken (payload contract drift: receiver readsprompt, sender writestask) and architecturally outside any plugin. This issue is the proper replacement.Proposed contract
Channel config
A registry keyed by channel name, populated via
apply_filters( 'datamachine_code_cli_channels', [] )and/orget_option( 'datamachine_code_cli_channels', [] ). Each entry:```php
'kimaki' => [
'command' => 'kimaki', // absolute path or PATH-resolvable
'args' => [ 'send', '--channel', '{recipient}', '--prompt', '{message}' ],
'detach' => true, // background or wait
'timeout' => 600, // seconds; only when detach=false
'env' => [ /* extra env vars */ ],
'cwd' => null, // optional working dir
],
```
Variables substituted into args:
Substitution is positional argv replacement, not shell interpolation.
proc_openis invoked with an array of args, never a string, so no quoting/escaping pitfalls.Handler registration
```php
\AgentsAPI\AI\Channels\register_dispatch_message_handler(
[ CliChannelTransport::class, 'dispatch' ],
20 // priority — leave room for higher-precedence handlers
);
```
The handler:
[ self::class, 'execute' ].`execute()` runs the command, returns canonical output:
```php
[
'sent' => true,
'channel' => $channel,
'recipient' => $recipient,
'message_id' => (string) $pid, // or null for synchronous mode
'metadata' => [ 'exit_code' => ..., 'duration_ms' => ..., 'output' => '...' ],
]
```
On failure, return `WP_Error` (the substrate handles that and fires `agents_dispatch_message_failed`).
Detach vs wait
Two modes:
Security
Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ agents-api (substrate, no transport knowledge) │
│ agents/dispatch-message contract + handler filter │
└────────────────────▲────────────────────────────────────────────┘
│ register_dispatch_message_handler()
│
┌────────────────────┴────────────────────────────────────────────┐
│ data-machine-code (THIS issue) │
│ CliChannelTransport — generic subprocess runtime │
│ Gated by Environment::has_shell() │
│ No knowledge of kimaki, cc-connect, telegram, etc. │
└────────────────────▲────────────────────────────────────────────┘
│ writes channel config
│
┌────────────────────┴────────────────────────────────────────────┐
│ wp-coding-agents (separate issue — bash installer) │
│ bridges/kimaki.sh, cc-connect.sh, telegram.sh │
│ Each installs an mu-plugin or option that registers its │
│ channel→command-template entry. │
└─────────────────────────────────────────────────────────────────┘
```
Proposed file layout
```
inc/Channels/
CliChannelTransport.php # the runtime
CliChannelRegistry.php # config lookup + validation
tests/
smoke-cli-channel-transport.php
```
Acceptance criteria
agents/dispatch-messageinvocations with a registered `channel` are routed to the CLI runtime.Environment::has_shell()is false, the runtime declines (does not claim the filter).Related
agents/dispatch-message+ handler filter, v0.107.0)/opt/agent-ping-webhook/retirementOut of scope
/opt/agent-ping-webhook/retirement (tracked in the wp-coding-agents issue).