Agent Toolbelt (spec §5). Shows the ability checklist * (enable/disable, persisted to the plugin option), the audit-log table, and a * status line. The checklist is the only setting in v1 — defaults are the * product. */ // Exit if accessed directly. if ( ! defined( 'ABSPATH' ) ) { exit; } if ( ! class_exists( 'Agent_Toolbelt_Admin' ) ) { class Agent_Toolbelt_Admin { const NONCE_ACTION = 'agent_toolbelt_save'; const NONCE_FIELD = 'agent_toolbelt_nonce'; const AUDIT_LIMIT = 20; /** * Adds the Tools -> Agent Toolbelt submenu page. */ public static function register_menu() { add_management_page( __( 'Agent Toolbelt', 'agent-toolbelt' ), __( 'Agent Toolbelt', 'agent-toolbelt' ), 'manage_options', 'agent-toolbelt', array( __CLASS__, 'render_page' ) ); } /** * Renders the admin page: status line, ability checklist, audit table. */ public static function render_page() { if ( ! current_user_can( 'manage_options' ) ) { return; } $atb_saved = self::maybe_save(); echo '
'; echo '

' . esc_html__( 'Agent Toolbelt', 'agent-toolbelt' ) . '

'; if ( $atb_saved ) { echo '

' . esc_html__( 'Abilities updated.', 'agent-toolbelt' ) . '

'; } self::render_status(); self::render_checklist(); self::render_audit_log(); echo '
'; } /** * Persists the checklist on a valid POST. Only toggles the operations * shown in the form; any disabled slug that is not currently a * registered operation (e.g. an ability shipped in a later version) is * preserved, so its opt-in default is never silently cleared. * * @return bool True when a save happened. */ protected static function maybe_save() { $atb_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : ''; if ( 'POST' !== $atb_method ) { return false; } if ( ! isset( $_POST[ self::NONCE_FIELD ] ) || ! check_admin_referer( self::NONCE_ACTION, self::NONCE_FIELD ) ) { return false; } if ( ! current_user_can( 'manage_options' ) ) { return false; } $atb_shown = array_keys( Agent_Toolbelt::get_operations( true ) ); $atb_checked = isset( $_POST['agent_toolbelt_enabled'] ) ? array_map( 'sanitize_key', (array) wp_unslash( $_POST['agent_toolbelt_enabled'] ) ) : array(); // A shown operation left unchecked becomes disabled. $atb_disabled = array_values( array_diff( $atb_shown, $atb_checked ) ); // Preserve disabled entries for slugs not shown in this form. $atb_options = Agent_Toolbelt::get_options(); $atb_existing = isset( $atb_options['disabled_abilities'] ) ? (array) $atb_options['disabled_abilities'] : array(); $atb_preserved = array_diff( $atb_existing, $atb_shown ); $atb_options['disabled_abilities'] = array_values( array_unique( array_merge( $atb_disabled, $atb_preserved ) ) ); update_option( Agent_Toolbelt::OPTION_NAME, $atb_options ); return true; } /** * Status line (spec §5c): Abilities API present? kill switch? MCP hint. */ protected static function render_status() { $atb_abilities_available = function_exists( 'wp_register_ability' ); $atb_kill_switch_active = Agent_Toolbelt::is_kill_switch_active(); echo '

' . esc_html__( 'Status', 'agent-toolbelt' ) . '

'; echo '

'; echo esc_html__( 'Abilities API available:', 'agent-toolbelt' ) . ' ' . ( $atb_abilities_available ? esc_html__( 'yes', 'agent-toolbelt' ) : esc_html__( 'no', 'agent-toolbelt' ) ) . '
'; echo esc_html__( 'Kill switch (AGENT_TOOLBELT_DISABLED):', 'agent-toolbelt' ) . ' ' . ( $atb_kill_switch_active ? esc_html__( 'active', 'agent-toolbelt' ) : esc_html__( 'inactive', 'agent-toolbelt' ) ) . ''; echo '

'; if ( ! $atb_abilities_available ) { echo '

' . esc_html__( 'This site does not expose the WordPress Abilities API (requires WordPress 6.9+). The operations still work via WP-CLI.', 'agent-toolbelt' ) . '

'; } if ( $atb_kill_switch_active ) { echo '

' . esc_html__( 'The kill switch is on: no abilities are registered and agents cannot call any operation.', 'agent-toolbelt' ) . '

'; } // Accuracy note (2026-07): core ships the Abilities API and its // REST endpoints; MCP exposure needs a bridge plugin (the official // `ai` plugin lists MCP as upcoming). Don't promise a "core MCP adapter". echo '

' . esc_html__( 'AI agents can call the enabled abilities below through the WordPress REST API (wp-abilities/v1, application-password auth) or through any MCP bridge that exposes WordPress Abilities. Only the enabled abilities are visible to them.', 'agent-toolbelt' ) . '

'; } /** * Ability checklist with risk badges and one-line descriptions. */ protected static function render_checklist() { echo '

' . esc_html__( 'Abilities', 'agent-toolbelt' ) . '

'; echo '
'; wp_nonce_field( self::NONCE_ACTION, self::NONCE_FIELD ); echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; foreach ( Agent_Toolbelt::get_operations( true ) as $atb_slug => $atb_operation ) { $atb_meta = $atb_operation->describe(); $atb_enabled = ! Agent_Toolbelt::is_ability_disabled( $atb_slug ); $atb_risk = isset( $atb_meta['risk'] ) ? (string) $atb_meta['risk'] : 'none'; $atb_summary = isset( $atb_meta['summary'] ) ? (string) $atb_meta['summary'] : ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; } echo '
' . esc_html__( 'Enabled', 'agent-toolbelt' ) . '' . esc_html__( 'Ability', 'agent-toolbelt' ) . '' . esc_html__( 'Risk', 'agent-toolbelt' ) . '' . esc_html__( 'What it does', 'agent-toolbelt' ) . '
' . esc_html( Agent_Toolbelt_Abilities::NAMESPACE_PREFIX . $atb_slug ) . '' . wp_kses( self::risk_badge( $atb_risk ), array( 'span' => array( 'style' => true ) ) ) . '' . esc_html( $atb_summary ) . '
'; submit_button( __( 'Save abilities', 'agent-toolbelt' ) ); echo '
'; } /** * Renders a small colored risk badge. * * @param string $atb_risk * @return string Escaped HTML. */ protected static function risk_badge( $atb_risk ) { $atb_colors = array( 'none' => '#3858e9', 'low' => '#00a32a', 'medium' => '#dba617', 'high' => '#d63638', ); $atb_color = isset( $atb_colors[ $atb_risk ] ) ? $atb_colors[ $atb_risk ] : '#787c82'; return '' . esc_html( $atb_risk ) . ''; } /** * Audit-log table (newest first, spec §4/§5). */ protected static function render_audit_log() { echo '

' . esc_html__( 'Audit log', 'agent-toolbelt' ) . '

'; $atb_rows = class_exists( 'Agent_Toolbelt_Log' ) ? Agent_Toolbelt_Log::read( array( 'limit' => self::AUDIT_LIMIT ) ) : array(); if ( empty( $atb_rows ) ) { echo '

' . esc_html__( 'No operations have been recorded yet.', 'agent-toolbelt' ) . '

'; return; } echo ''; echo ''; $atb_headers = array( __( 'When (UTC)', 'agent-toolbelt' ), __( 'Ability', 'agent-toolbelt' ), __( 'Caller', 'agent-toolbelt' ), __( 'User', 'agent-toolbelt' ), __( 'Dry run', 'agent-toolbelt' ), __( 'Result', 'agent-toolbelt' ), __( 'ms', 'agent-toolbelt' ), __( 'Summary', 'agent-toolbelt' ), ); foreach ( $atb_headers as $atb_head ) { echo ''; } echo ''; foreach ( $atb_rows as $atb_row ) { echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; } echo '
' . esc_html( $atb_head ) . '
' . esc_html( isset( $atb_row['created_at'] ) ? $atb_row['created_at'] : '' ) . '' . esc_html( isset( $atb_row['ability'] ) ? $atb_row['ability'] : '' ) . '' . esc_html( isset( $atb_row['caller'] ) ? $atb_row['caller'] : '' ) . '' . esc_html( isset( $atb_row['user_id'] ) ? $atb_row['user_id'] : '0' ) . '' . ( empty( $atb_row['dry_run'] ) ? esc_html__( 'no', 'agent-toolbelt' ) : esc_html__( 'yes', 'agent-toolbelt' ) ) . '' . esc_html( isset( $atb_row['result'] ) ? $atb_row['result'] : '' ) . '' . esc_html( isset( $atb_row['duration_ms'] ) ? $atb_row['duration_ms'] : '0' ) . '' . esc_html( isset( $atb_row['summary'] ) ? $atb_row['summary'] : '' ) . '
'; } } }