Skip to content

Replace per-command abilities with single wp-cli/execute ability#7

Merged
superdav42 merged 3 commits intomainfrom
refactor/single-wp-cli-ability
Apr 10, 2026
Merged

Replace per-command abilities with single wp-cli/execute ability#7
superdav42 merged 3 commits intomainfrom
refactor/single-wp-cli-ability

Conversation

@superdav42
Copy link
Copy Markdown
Contributor

@superdav42 superdav42 commented Apr 10, 2026

Summary

  • Replace hundreds of individual WP-CLI ability registrations with a single wp-cli/execute ability that accepts raw command strings — agents pass commands the same way they would use bash
  • Switch to array-based proc_open (PHP 7.4+) for shell-free execution, eliminating injection risk entirely
  • Remove Network: true constraint so the plugin works on both single-site and multisite

Why

AI agents understand bash. Parsing 500+ individual tool definitions wastes tokens and adds no value — the agent already knows WP-CLI syntax. One ability with { command: "post list --format=json" } is both cheaper and more natural.

What changed

File Change
inc/class-wp-cli-abilities.php Rewritten (567→147 lines). Single ability registration.
inc/class-command-executor.php Rewritten. Accepts raw command strings, tokenizes with quote handling, executes via array-based proc_open.
inc/class-schema-builder.php Deleted — no per-command schemas needed.
cli-abilities-bridge.php Removed schema-builder require, removed Network: true, bumped to v2.0.0.
readme.txt Rewritten for v2.0 architecture.
AGENTS.md Updated structure and conventions.
composer.json Updated description.

Blocklist, permissions, system commands, and system executor are unchanged.

Summary by CodeRabbit

  • New Features

    • Single unified wp-cli/execute ability replaces individual command discovery.
    • Agents pass full WP-CLI commands as plain text with bash-like syntax.
    • Added command blocklist and permission-level classification for enhanced security.
    • Automatic multisite context handling via --url.
  • Documentation

    • Updated guides reflecting new single-ability execution model.
    • Removed wp abilities sync workflow documentation.

Point wp-cli at shared WordPress 7.0-RC2 multisite dev install at
../wordpress (wordpress.local:8080). Documents reset workflow and
WP-CLI usage in AGENTS.md.
Agents now pass WP-CLI commands as plain text strings instead of
parsing hundreds of individual tool definitions. This reduces token
overhead from ~500+ ability schemas to one.

- Rewrite executor to accept raw command strings with a tokenizer
- Use array-based proc_open (PHP 7.4+) for shell-free execution
- Delete schema builder (no per-command schemas needed)
- Remove Network: true constraint — works on single-site and multisite
- Check blocklist and permissions at execution time per command
- Bump version to 2.0.0
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 10, 2026

📝 Walkthrough

Walkthrough

The plugin shifts from discovering and registering many WP-CLI command abilities to offering a single wp-cli/execute ability that accepts raw command strings. The schema-builder and command-sync features were removed; execution now uses tokenization, a blocklist, permission classification, and array-based proc_open execution with multisite/context handling.

Changes

Cohort / File(s) Summary
Documentation & Configuration
AGENTS.md, readme.txt, wp-cli.yml
Docs updated to describe single wp-cli/execute model, local dev WP-CLI config added (path/url), changelog and install steps revised to remove wp abilities sync.
Plugin Metadata / Bootstrap
cli-abilities-bridge.php, composer.json
Plugin metadata bumped to 2.0.0 and description changed to execution-oriented wording; bootstrap no longer includes the schema-builder.
Core Command Execution
inc/class-command-executor.php
Execution API changed to accept a raw command string; added robust tokenization, command-path derivation, blocklist checks, permission classification, automatic flag injection (--path, --url, --no-color, --user), multisite URL handling, and array-based proc_open usage.
Ability Registration / Runtime
inc/class-wp-cli-abilities.php
Replaced per-command discovery/registration with a single wp-cli/execute ability and simplified category metadata; permission callback now centralizes checks and execution is delegated to the command executor.
Removed Components
inc/class-schema-builder.php
Removed WP_CLI_Abilities_Schema_Builder and its synopsis→JSON Schema generation logic (deleted file).

Sequence Diagram

sequenceDiagram
    participant Client
    participant Abilities as WordPress<br/>Abilities API
    participant Executor as Command<br/>Executor
    participant Blocklist as Blocklist<br/>Cache
    participant Perms as Permissions<br/>Classifier
    participant WPCLI as WP-CLI via<br/>proc_open

    Client->>Abilities: Call wp-cli/execute(command)
    Abilities->>Executor: execute(command)
    Executor->>Executor: Tokenize command (quotes/escapes)
    Executor->>Executor: Derive command_path from tokens
    Executor->>Blocklist: is_blocked(command_path)
    alt blocked
        Blocklist-->>Executor: WP_Error (403)
        Executor-->>Abilities: Return error
    else allowed
        Executor->>Perms: classify(command_path)
        Perms-->>Executor: level
        Executor->>Perms: check_level(level)
        alt permission denied
            Perms-->>Executor: WP_Error (403)
            Executor-->>Abilities: Return error
        else permission granted
            Executor->>Executor: Inject flags (--path,--url,--no-color,--user)
            Executor->>WPCLI: proc_open(array of args)
            WPCLI-->>Executor: stdout/stderr/exit
            Executor->>Executor: Parse output / update current site URL if needed
            Executor-->>Abilities: Return result
        end
    end
    Abilities-->>Client: JSON response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A single command I now behold,
Tokens tamed, no schema to mold.
Blocklists guard the burrowed way,
Permissions whisper what I may,
Proc_open hums — hop, execute, hooray! 🎉

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title "Replace per-command abilities with single wp-cli/execute ability" accurately and concisely summarizes the primary architectural change across the codebase.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/single-wp-cli-ability

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
inc/class-command-executor.php (1)

170-227: Unclosed quotes are silently accepted.

The tokenizer doesn't validate that quotes are properly closed. An input like post list --title="test will include the trailing content in $current without error. This could lead to unexpected behavior.

Consider adding validation for unclosed quotes and returning a WP_Error when detected.

♻️ Optional: Add unclosed quote detection
 		if ($current !== '') {
 			$tokens[] = $current;
 		}

+		if ($in_single || $in_double) {
+			return []; // Return empty to trigger "Could not parse" error
+		}
+
 		return $tokens;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@inc/class-command-executor.php` around lines 170 - 227, The tokenize method
currently accepts inputs with unclosed single or double quotes (function
tokenize), causing silent inclusion of trailing text; add detection after the
loop to check the state flags $in_single and $in_double and, if either is true,
return a WP_Error (or throw/propagate an error consistent with the plugin's
error handling) instead of returning tokens; update any callers of tokenize to
handle WP_Error (or the chosen error type) so malformed commands like
--title="test are rejected with a clear error.
inc/class-wp-cli-abilities.php (1)

35-36: Minor: Use Yoda condition.

Per coding guidelines, PHP code should use Yoda conditions.

♻️ Suggested fix
-		if (self::$instance === null) {
+		if (null === self::$instance) {

As per coding guidelines: "Use Yoda conditions in PHP code"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@inc/class-wp-cli-abilities.php` around lines 35 - 36, The conditional
currently checks "self::$instance === null" which violates the project's Yoda
condition rule; change the comparison to "null === self::$instance" wherever
this singleton instantiation appears (the block that sets "self::$instance = new
self()"), i.e., replace "if (self::$instance === null)" with "if (null ===
self::$instance)" to conform to Yoda style for the self::$instance check.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@inc/class-command-executor.php`:
- Around line 238-251: extract_command_path() currently collects all non-flag
tokens so positional args like IDs become part of the path and make classify()
pick the wrong leaf; modify extract_command_path() to stop collecting tokens
when a token looks like a positional argument (e.g., purely numeric or starts
with a digit or contains characters that indicate an argument rather than a
verb/subcommand) so the returned path contains only command and subcommand
words; ensure this change fixes classify()’s leaf-based lookup (the functions to
update are extract_command_path and any code that relies on its output in
classify(), which compares the leaf against READ_ACTIONS and
DESTRUCTIVE_ACTIONS).

---

Nitpick comments:
In `@inc/class-command-executor.php`:
- Around line 170-227: The tokenize method currently accepts inputs with
unclosed single or double quotes (function tokenize), causing silent inclusion
of trailing text; add detection after the loop to check the state flags
$in_single and $in_double and, if either is true, return a WP_Error (or
throw/propagate an error consistent with the plugin's error handling) instead of
returning tokens; update any callers of tokenize to handle WP_Error (or the
chosen error type) so malformed commands like --title="test are rejected with a
clear error.

In `@inc/class-wp-cli-abilities.php`:
- Around line 35-36: The conditional currently checks "self::$instance === null"
which violates the project's Yoda condition rule; change the comparison to "null
=== self::$instance" wherever this singleton instantiation appears (the block
that sets "self::$instance = new self()"), i.e., replace "if (self::$instance
=== null)" with "if (null === self::$instance)" to conform to Yoda style for the
self::$instance check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f8df3cb0-94f5-485b-8123-6c08535da298

📥 Commits

Reviewing files that changed from the base of the PR and between cc53d24 and 1cedc02.

📒 Files selected for processing (8)
  • AGENTS.md
  • cli-abilities-bridge.php
  • composer.json
  • inc/class-command-executor.php
  • inc/class-schema-builder.php
  • inc/class-wp-cli-abilities.php
  • readme.txt
  • wp-cli.yml
💤 Files with no reviewable changes (1)
  • inc/class-schema-builder.php

Comment on lines +238 to +251
private static function extract_command_path(array $tokens): string {

if ($current_user_id > 0) {
$cmd_parts[] = '--user=' . escapeshellarg((string) $current_user_id);
}
$path_parts = [];

// Ensure non-interactive.
$cmd_parts[] = '--no-color';
foreach ($tokens as $token) {
if (str_starts_with($token, '-')) {
break;
}

$command = implode(' ', $cmd_parts);
$path_parts[] = $token;
}

$result = self::run($command, $command_path);
return implode(' ', $path_parts);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how classify() handles the leaf extraction
ast-grep --pattern $'public static function classify(string $command_path): string {
  $$$
}'

Repository: Ultimate-Multisite/cli-abilities-bridge

Length of output: 65


🏁 Script executed:

fd -type f -name "*command-permissions*"

Repository: Ultimate-Multisite/cli-abilities-bridge

Length of output: 253


🏁 Script executed:

rg "function classify" -A 20 --type php

Repository: Ultimate-Multisite/cli-abilities-bridge

Length of output: 1356


🏁 Script executed:

rg "extract_command_path" -B 5 -A 15 --type php

Repository: Ultimate-Multisite/cli-abilities-bridge

Length of output: 2501


🏁 Script executed:

rg "READ_ACTIONS|DESTRUCTIVE_ACTIONS" -B 2 -A 10 inc/class-command-permissions.php

Repository: Ultimate-Multisite/cli-abilities-bridge

Length of output: 897


Positional arguments included in command path may cause overly restrictive permission classification.

The extract_command_path() method includes all non-flag tokens as part of the command path. For commands like post get 123 --format=json, this results in post get 123 being extracted instead of just post get.

The classify() method then uses the last word (leaf) of this path to determine permission level. With positional arguments included, the leaf becomes 123 instead of get. Since 123 doesn't match any action in READ_ACTIONS or DESTRUCTIVE_ACTIONS, the command defaults to LEVEL_WRITE, requiring manage_network capability for what should be a LEVEL_READ operation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@inc/class-command-executor.php` around lines 238 - 251,
extract_command_path() currently collects all non-flag tokens so positional args
like IDs become part of the path and make classify() pick the wrong leaf; modify
extract_command_path() to stop collecting tokens when a token looks like a
positional argument (e.g., purely numeric or starts with a digit or contains
characters that indicate an argument rather than a verb/subcommand) so the
returned path contains only command and subcommand words; ensure this change
fixes classify()’s leaf-based lookup (the functions to update are
extract_command_path and any code that relies on its output in classify(), which
compares the leaf against READ_ACTIONS and DESTRUCTIVE_ACTIONS).

@superdav42 superdav42 merged commit a4154ec into main Apr 10, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant