*/ abstract protected function input_properties(): array; /** * @since 0.0.34 * @return array */ abstract protected function output_properties(): array; /** * @since 0.0.34 * @return array */ abstract protected function required_input(): array; /** * @since 0.0.34 * @return array */ abstract protected function annotations(): array; /** * @since 0.0.34 * @param array $input Input. * @return array|\WP_Error */ abstract protected function run( array $input ); /** * The floor, and not overridable. * * Classic Editor gates its own network save on `manage_network_options` and its per-user save on * `edit_user`, which lets an Editor change another user's editor from wp-admin. This suite is * stricter on purpose: an ability is reachable by an AI client and wp-admin is not. * * @since 0.0.34 * @return string */ final protected function permission_floor(): string { return 'manage_options'; } /** * @since 0.0.34 * @return bool */ protected function requires_confirmation(): bool { return false; } /** * Whether THIS call needs confirmation, as opposed to whether the ability ever does. * * `requires_confirmation()` governs the input schema — it is what puts `confirm` in the * properties, and without it `additionalProperties: false` would reject the key. This governs * the runtime gate. They are separate because an ability can have one input that warrants a * confirmation and another that does not, and gating the harmless one is friction with no risk * behind it. * * @since 0.0.34 * @param array $input Input. * @return bool */ protected function needs_confirmation_for( array $input ): bool { unset( $input ); return $this->requires_confirmation(); } /** * @since 0.0.34 * @return string */ protected function confirmation_message(): string { return __( 'Pass confirm: true to proceed.', 'acrossai-abilities-manager' ); } /** * @since 0.0.34 * @return array */ protected function sub_group_labels(): array { return array( 'events' => __( 'Events', 'acrossai-abilities-manager' ), 'venues' => __( 'Venues', 'acrossai-abilities-manager' ), 'organizers' => __( 'Organizers', 'acrossai-abilities-manager' ), 'categories' => __( 'Event Categories', 'acrossai-abilities-manager' ), 'calendar' => __( 'Calendar Settings', 'acrossai-abilities-manager' ), ); } /** * @since 0.0.34 * @return string */ protected function success_message(): string { return __( 'Done.', 'acrossai-abilities-manager' ); } /** * Assemble the ability definition. * * @since 0.0.34 * @return array */ protected function ability(): array { $sub_group = $this->sub_group(); $acrossai = array( 'tab_group' => self::TAB_GROUP, 'sub_group' => $sub_group, ); $label = $this->sub_group_labels()[ $sub_group ] ?? ''; if ( '' !== $label ) { $acrossai['sub_group_label'] = $label; } $properties = $this->input_properties(); $required = $this->required_input(); if ( $this->requires_confirmation() ) { $properties['confirm'] = array( 'type' => 'boolean', 'default' => false, 'description' => __( 'Must be true to perform this operation.', 'acrossai-abilities-manager' ), ); // Never schema-required: core validates input_schema before execute() runs, so a // required confirm yields a generic ability_invalid_input and the gate never fires. $required = array_values( array_diff( $required, array( 'confirm' ) ) ); } return array( 'name' => $this->slug(), 'args' => array( 'label' => $this->ability_label(), 'description' => $this->ability_description(), 'category' => self::CATEGORY, 'execute_callback' => array( $this, 'execute' ), 'permission_callback' => Events_Calendar_Guard::can( $this->permission_floor() ), 'input_schema' => array( 'type' => 'object', 'properties' => $properties, 'required' => $required, 'additionalProperties' => false, ), 'output_schema' => array( 'type' => 'object', 'properties' => array_merge( array( 'success' => array( 'type' => 'boolean' ) ), $this->output_properties(), array( 'message' => array( 'type' => 'string' ), 'error_code' => array( 'type' => 'string' ), ) ), 'required' => array( 'success' ), 'additionalProperties' => false, ), 'meta' => array( 'acrossai' => $acrossai, 'show_in_rest' => true, 'mcp' => array( 'public' => false, 'type' => 'tool', ), 'annotations' => $this->annotations(), ), ), ); } /** * Guards, then the ability, then the envelope. * * @since 0.0.34 * @param array $input Input. * @return array */ public function execute( array $input = array() ): array { $available = Events_Calendar_Guard::assert_available(); if ( is_wp_error( $available ) ) { return Events_Calendar_Guard::fail( $available ); } if ( $this->needs_confirmation_for( $input ) ) { $confirmed = Events_Calendar_Guard::assert_confirmed( $input, $this->confirmation_message() ); if ( is_wp_error( $confirmed ) ) { return Events_Calendar_Guard::fail( $confirmed ); } } $result = $this->run( $input ); if ( is_wp_error( $result ) ) { return Events_Calendar_Guard::fail( $result ); } $message = isset( $result['message'] ) ? (string) $result['message'] : $this->success_message(); unset( $result['message'] ); return Events_Calendar_Guard::ok( $result, $message ); } }