namespace = 'agenticdaisy-firewall/v1'; $this->rest_base = 'settings'; } /** * Register routes. */ public function register_routes(): void { register_rest_route( 'agenticdaisy-firewall/v1', '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_settings' ), 'permission_callback' => array( $this, 'permissions_check' ), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'update_settings' ), 'permission_callback' => array( $this, 'permissions_check' ), 'args' => array( 'anomaly_email' => array( 'description' => 'Email the site administrator when an agent trips a rate limit or an ability changes definition.', 'type' => 'boolean', 'required' => false, // Strictly boolean: coercing a typo would silently // disable security alerting, which is the one // outcome nobody would notice. 'validate_callback' => 'rest_validate_request_arg', ), 'lockdown' => array( 'description' => 'Kill switch: while engaged, every agent action (reads included) is denied before policy runs.', 'type' => 'boolean', 'required' => false, // Strictly boolean for the same reason, in both // directions: a typo must neither lift a lockdown // nor impose one. 'validate_callback' => 'rest_validate_request_arg', ), ), ), ) ); } /** * Admin capability gate; never __return_true (checklist ยง1.14). * * @param WP_REST_Request $request Request. * @return true|\WP_Error */ public function permissions_check( $request ) { if ( current_user_can( 'manage_options' ) ) { return true; } return new \WP_Error( 'rest_forbidden', __( 'You are not allowed to change these settings.', 'agentic-daisy-ai-agent-firewall' ), array( 'status' => rest_authorization_required_code() ) ); } /** * Read the settings. * * @param WP_REST_Request $request Request. */ public function get_settings( $request ): WP_REST_Response { return new WP_REST_Response( $this->current(), 200 ); } /** * Write the settings. * * @param WP_REST_Request $request Request. */ public function update_settings( $request ): WP_REST_Response { // Partial updates: only the provided switches change, so flipping // the kill switch mid-incident cannot silently touch alerting. if ( $request->has_param( 'anomaly_email' ) ) { // Stored as '1'/'0' because that is what AnomalyNotifier compares // against; the API speaks booleans in both directions. update_option( AnomalyNotifier::OPTION_EMAIL, true === $request->get_param( 'anomaly_email' ) ? '1' : '0' ); } if ( $request->has_param( 'lockdown' ) ) { if ( true === $request->get_param( 'lockdown' ) ) { $this->lockdown->engage(); } else { $this->lockdown->release(); } } return new WP_REST_Response( $this->current(), 200 ); } /** * Current settings as booleans. * * @return array */ private function current(): array { return array( 'anomaly_email' => '1' === (string) get_option( AnomalyNotifier::OPTION_EMAIL, '1' ), 'lockdown' => $this->lockdown->engaged(), ); } /** * Item schema. * * @return array */ public function get_item_schema(): array { return array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'adaf_settings', 'type' => 'object', 'properties' => array( 'anomaly_email' => array( 'type' => 'boolean' ), 'lockdown' => array( 'type' => 'boolean' ), ), ); } }