|WP_Error What the ability returned. * @since 1.5.0 */ public function approve( PendingAction $action, int $decided_by ) { // Win the row before doing anything else. A concurrent second approval // (two tabs, a double-click, a proxy retry) loses the claim and returns // here without ever running the ability a second time. if ( ! $this->repository->claim( $action->id, $decided_by ) ) { return new WP_Error( 'albert_already_handled', __( 'This request has already been handled.', 'albert-ai-butler' ) ); } $ability = wp_get_ability( $action->ability_name ); if ( $ability === null ) { $error = new WP_Error( 'albert_pending_ability_missing', /* translators: %s: ability name. */ sprintf( __( 'The ability "%s" is no longer available, so this action cannot run.', 'albert-ai-butler' ), $action->ability_name ) ); $this->repository->record_outcome( $action->id, $decided_by, false, $this->error_shape( $error ) ); AuditTrail::approved( $action, $decided_by, false ); return $error; } // Arm a one-shot ticket bound to this exact call, so the interceptor lets // this single execution through and nothing else. Cleared in a finally so // a ticket never outlives the run it was minted for, even on a throw. ApprovalTicket::arm( $action->ability_name, $action->input ); $connection = ConnectionContext::client_id(); ConnectionContext::set( ( $action->client_id ?? '' ) !== '' ? $action->client_id : self::UNKNOWN_CONNECTION ); try { $result = $this->run_as( $action->user_id, static fn() => $ability->execute( $action->input ) ); } finally { ConnectionContext::set( $connection ); ApprovalTicket::clear(); } if ( is_wp_error( $result ) ) { $this->repository->record_outcome( $action->id, $decided_by, false, $this->error_shape( $result ) ); AuditTrail::approved( $action, $decided_by, false ); return $result; } // No payload: `guarded_execute()` returns the unredacted result, so // `sensitive_output_keys` does not apply on this path and a success // result could carry a credential. The error shape above is kept because // it is bounded to a code and a message. $this->repository->record_outcome( $action->id, $decided_by, true, [] ); AuditTrail::approved( $action, $decided_by, true ); return is_array( $result ) ? $result : []; } /** * Reject a staged action. The ability never runs. * * @param PendingAction $action The action to reject. * @param int $decided_by The user rejecting it. * * @return bool True when a still-pending row was rejected; false if it had * already been handled. * @since 1.5.0 */ public function reject( PendingAction $action, int $decided_by ): bool { $rejected = $this->repository->reject( $action->id, $decided_by ); // Only a genuine rejection is recorded. A lost race already has an // outcome and did not just acquire a second one. if ( $rejected ) { AuditTrail::rejected( $action, $decided_by ); } return $rejected; } /** * Run a callback as a given user, restoring the current user afterwards. * * @param int $user_id The user to run as. * @param callable $run The callback. * * @return mixed * @since 1.5.0 */ private function run_as( int $user_id, callable $run ) { $previous = get_current_user_id(); wp_set_current_user( $user_id ); try { return $run(); } finally { wp_set_current_user( $previous ); } } /** * The stored shape for a failed run. * * @param WP_Error $error The error. * * @return array * @since 1.5.0 */ private function error_shape( WP_Error $error ): array { // A WP_Error code may be an int or a string; the queue stores it as a // string identifier either way. return [ 'code' => (string) $error->get_error_code(), 'message' => $error->get_error_message(), ]; } }