Spaces:
Sleeping
Sleeping
| defined( 'ABSPATH' ) || exit; | |
| /** | |
| * Attention Bridge — emit-only MVP. | |
| * | |
| * Cross-site attention routing channel. The local site (RCM) emits | |
| * structured context packets to a configured oversight URL. The | |
| * receiving side is OUT OF SCOPE for this Board — emit-only MVP. | |
| * | |
| * Architectural rules (stakeholder, locked): | |
| * Attention is portable. Authority is local. Oversight is routed. | |
| * Local site owns its operational truth. | |
| * SleeperAgents owns oversight routing. | |
| * SA-Core governs the exchange. | |
| * | |
| * Doctrine constraints honored: | |
| * - User-triggered emission ONLY. No auto-emit. No Sara coupling. | |
| * - Bearer-token auth for the outbound POST (shared secret). | |
| * - Schema versioning baked into the payload: 'sa.attention.v1'. | |
| * - Log-only storage: wp_sa_attention_log captures what was sent + whether it arrived. | |
| * - "Don't overbuild." This file does emit + log. Nothing else. | |
| * | |
| * Configuration (wp-config.php constants — required): | |
| * define('SA_SITE_ID', 'rcm'); | |
| * define('SA_ATTENTION_TARGET_URL', 'https://sleeperagents.org/wp-json/oversight/v1/attention'); | |
| * define('SA_ATTENTION_TOKEN', 'shared-secret-here'); | |
| * | |
| * Optional: | |
| * define('SA_TENANT', 'org-slug'); // single-field tenant per locked spec | |
| * | |
| * Each constant is also filterable for testing / runtime override: | |
| * sa_orch_attention_site_id, sa_orch_attention_target_url, | |
| * sa_orch_attention_token, sa_orch_attention_tenant | |
| */ | |
| class SA_Orch_Attention_Bridge { | |
| const SCHEMA_VERSION = 'sa.attention.v1'; | |
| const HANDLE_JS = 'sa-orch-attention-bridge'; | |
| const HANDLE_CSS = 'sa-orch-attention-bridge'; | |
| /** Hook registration — REST + admin enqueue. */ | |
| public static function register_hooks() { | |
| add_action( 'rest_api_init', [ __CLASS__, 'register_rest_routes' ] ); | |
| add_action( 'admin_enqueue_scripts', [ __CLASS__, 'maybe_enqueue' ] ); | |
| } | |
| /** | |
| * REST endpoints: | |
| * POST /sa-orch/v1/attention/emit — always (production-safe) | |
| * POST /sa-orch/v1/attention/dev-receive — local/development only (mock 200 receiver) | |
| */ | |
| public static function register_rest_routes() { | |
| register_rest_route( 'sa-orch/v1', '/attention/emit', [ | |
| 'methods' => 'POST', | |
| 'callback' => [ __CLASS__, 'emit' ], | |
| 'permission_callback' => function () { | |
| return current_user_can( 'manage_options' ); | |
| }, | |
| 'args' => [ | |
| 'surface' => [ 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ], | |
| 'ref' => [ 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ], | |
| 'page' => [ 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ], | |
| // 'mode' selects which target the dev verification flow exercises. | |
| // Currently accepted: 'default' (the configured / scaffold-failure target) | |
| // and 'success_mock' (routes to the dev-receive endpoint, dev-only). | |
| // In non-dev environments, 'success_mock' silently falls back to default. | |
| 'mode' => [ 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_key' ], | |
| ], | |
| ] ); | |
| // Dev-only mock receiver. Registered ONLY when the environment is | |
| // explicitly local or development. In production this route does not | |
| // exist — the registration itself is gated, not just the handler. | |
| if ( class_exists( 'SA_Orch_Env' ) && SA_Orch_Env::is_local_or_dev() ) { | |
| register_rest_route( 'sa-orch/v1', '/attention/dev-receive', [ | |
| 'methods' => 'POST', | |
| 'callback' => [ __CLASS__, 'dev_receive' ], | |
| // No capability check — this is a dev mock for self-call testing. | |
| // The whole route is only registered in dev environments. | |
| 'permission_callback' => '__return_true', | |
| ] ); | |
| } | |
| } | |
| /** | |
| * Dev-only mock receiver. Returns HTTP 200 with a confirmation body so | |
| * the success path can be exercised end-to-end without a real oversight | |
| * site standing up. Defense in depth: handler also bails if env is not | |
| * dev (in case the route somehow survives a misconfigured deploy). | |
| */ | |
| public static function dev_receive( WP_REST_Request $request ) { | |
| if ( ! class_exists( 'SA_Orch_Env' ) || ! SA_Orch_Env::is_local_or_dev() ) { | |
| return new WP_Error( | |
| 'dev_receive_not_available', | |
| 'dev-receive is only available in local/development environments.', | |
| [ 'status' => 404 ] | |
| ); | |
| } | |
| return rest_ensure_response( [ | |
| 'received' => true, | |
| 'env' => SA_Orch_Env::type(), | |
| 'message' => 'Dev mock receiver acknowledged the attention packet.', | |
| 'received_at' => gmdate( 'c' ), | |
| ] ); | |
| } | |
| /** | |
| * Read configuration with classification-aware precedence: | |
| * | |
| * 1. Defined constant (production-safe; human-authorized) | |
| * 2. Filter override (testing override; same shape as the constant) | |
| * 3. Dev scaffold default (only when env is local/development; clearly | |
| * non-production values; no real privileged | |
| * system exposed; tokens labeled dev-only) | |
| * 4. Empty string (production with no constants set; emit will | |
| * fail with attention_config_missing — preserved | |
| * behavior for the unconfigured production case) | |
| * | |
| * Per the Environment Configuration Classification rule, dev scaffolding | |
| * is allowed because: | |
| * - the values are clearly non-production (sa-dev-local / discard-port URL / dev-only token) | |
| * - no real external privileged system is exposed (target is unreachable) | |
| * - the token is explicitly labeled dev-only | |
| * - the env must be EXPLICITLY local/development (default 'production' never scaffolds) | |
| */ | |
| public static function get_config(): array { | |
| $defaults = self::dev_scaffold_defaults(); | |
| return [ | |
| 'site_id' => self::resolve_value( 'SA_SITE_ID', 'sa_orch_attention_site_id', $defaults['site_id'] ), | |
| 'target_url' => self::resolve_value( 'SA_ATTENTION_TARGET_URL', 'sa_orch_attention_target_url', $defaults['target_url'] ), | |
| 'token' => self::resolve_value( 'SA_ATTENTION_TOKEN', 'sa_orch_attention_token', $defaults['token'] ), | |
| 'tenant' => self::resolve_value( 'SA_TENANT', 'sa_orch_attention_tenant', $defaults['tenant'] ), | |
| ]; | |
| } | |
| /** | |
| * Resolve a single config value. Constants beat filters beat dev scaffolds | |
| * beat empty. The dev_default is only non-empty when env is local/dev. | |
| */ | |
| private static function resolve_value( string $constant_name, string $filter_name, string $dev_default ): string { | |
| if ( defined( $constant_name ) ) { | |
| return (string) constant( $constant_name ); | |
| } | |
| $filtered = apply_filters( $filter_name, '' ); | |
| if ( $filtered !== '' && $filtered !== null ) { | |
| return (string) $filtered; | |
| } | |
| return $dev_default; | |
| } | |
| /** | |
| * Dev-mode scaffolds. Empty in production. Populated in local/dev only. | |
| * | |
| * Values chosen to be unambiguously non-production: | |
| * site_id — 'sa-dev-local' (slug clearly marks origin as dev) | |
| * target_url — '<scheme>://<host>:9/sa-attention-dev-null' derived from | |
| * home_url() so the scaffold scales with the install's own | |
| * hostname (works on wordpress.localhost, dev.example.com, | |
| * etc.). Port 9 is the IANA discard service; reliably | |
| * refuses connections regardless of host. No real system | |
| * is exposed; emit attempts cleanly fail with status=failed. | |
| * token — 'dev-only-not-a-real-secret' (string self-labels its kind) | |
| * tenant — 'dev-tenant' | |
| */ | |
| private static function dev_scaffold_defaults(): array { | |
| if ( ! class_exists( 'SA_Orch_Env' ) || ! SA_Orch_Env::is_local_or_dev() ) { | |
| return [ 'site_id' => '', 'target_url' => '', 'token' => '', 'tenant' => '' ]; | |
| } | |
| return [ | |
| 'site_id' => 'sa-dev-local', | |
| 'target_url' => self::dev_failure_target_url(), | |
| 'token' => 'dev-only-not-a-real-secret', | |
| 'tenant' => 'dev-tenant', | |
| ]; | |
| } | |
| /** | |
| * Compute the dev failure-target URL from the install's own host. | |
| * Scales with deploy: same code yields wordpress.localhost:9 in local | |
| * dev, dev.example.com:9 in remote dev, etc. Production never calls this. | |
| */ | |
| private static function dev_failure_target_url(): string { | |
| $parts = wp_parse_url( home_url() ); | |
| $scheme = $parts['scheme'] ?? 'http'; | |
| $host = $parts['host'] ?? '127.0.0.1'; | |
| return $scheme . '://' . $host . ':9/sa-attention-dev-null'; | |
| } | |
| /** | |
| * The URL of the dev mock receiver — same install, REST endpoint at | |
| * sa-orch/v1/attention/dev-receive. Returns empty string in non-dev | |
| * environments so the success-mock mode silently falls back to default. | |
| */ | |
| private static function dev_success_mock_url(): string { | |
| if ( ! class_exists( 'SA_Orch_Env' ) || ! SA_Orch_Env::is_local_or_dev() ) { | |
| return ''; | |
| } | |
| return rest_url( 'sa-orch/v1/attention/dev-receive' ); | |
| } | |
| /** | |
| * Resolve the outbound target URL given the configured target and the | |
| * caller-requested mode. Returns: | |
| * [ url, mode_label ] | |
| * mode_label is a short human-readable string for UI display: | |
| * 'configured' — the configured / scaffold-failure target | |
| * 'dev failure target' — same as 'configured' when env is dev (annotated) | |
| * 'dev success mock' — the dev-receive URL (only when env is dev) | |
| */ | |
| private static function resolve_outbound_target( string $configured_url, string $mode ): array { | |
| $is_dev = class_exists( 'SA_Orch_Env' ) && SA_Orch_Env::is_local_or_dev(); | |
| if ( $mode === 'success_mock' ) { | |
| $mock = self::dev_success_mock_url(); | |
| if ( $is_dev && $mock !== '' ) { | |
| return [ $mock, 'dev success mock' ]; | |
| } | |
| // Production / non-dev silently falls back to configured. Production | |
| // safety: success_mock has no effect outside dev. | |
| } | |
| return [ | |
| $configured_url, | |
| $is_dev ? 'dev failure target' : 'configured', | |
| ]; | |
| } | |
| /** | |
| * Build the v1 attention payload. Pure function — no I/O. | |
| */ | |
| public static function build_payload( array $cfg, array $input ): array { | |
| $user = wp_get_current_user(); | |
| $user_id = isset( $user->ID ) ? (int) $user->ID : 0; | |
| $user_name = isset( $user->user_login ) ? (string) $user->user_login : ''; | |
| return [ | |
| 'schema_version' => self::SCHEMA_VERSION, | |
| 'site_id' => (string) $cfg['site_id'], | |
| 'tenant' => (string) $cfg['tenant'], | |
| 'surface' => isset( $input['surface'] ) && $input['surface'] !== '' ? (string) $input['surface'] : null, | |
| 'ref' => isset( $input['ref'] ) && $input['ref'] !== '' ? (string) $input['ref'] : null, | |
| 'page' => isset( $input['page'] ) && $input['page'] !== '' ? (string) $input['page'] : null, | |
| 'user' => [ | |
| 'id' => $user_id, | |
| 'login' => $user_name, | |
| ], | |
| 'observed_at' => gmdate( 'c' ), | |
| ]; | |
| } | |
| /** | |
| * REST emit handler. Assembles payload, POSTs to target URL with bearer auth, | |
| * logs the attempt to wp_sa_attention_log. Returns the result to the caller | |
| * so the UI can render success/error feedback. | |
| */ | |
| public static function emit( WP_REST_Request $request ) { | |
| $cfg = self::get_config(); | |
| // Required-config check. Surface missing config explicitly so the | |
| // operator knows what to set, rather than silently failing. | |
| $missing = []; | |
| if ( $cfg['site_id'] === '' ) $missing[] = 'SA_SITE_ID'; | |
| if ( $cfg['target_url'] === '' ) $missing[] = 'SA_ATTENTION_TARGET_URL'; | |
| if ( $cfg['token'] === '' ) $missing[] = 'SA_ATTENTION_TOKEN'; | |
| if ( $missing ) { | |
| return new WP_Error( | |
| 'attention_config_missing', | |
| 'Attention Bridge configuration missing: ' . implode( ', ', $missing ) | |
| . '. Define these constants in wp-config.php.', | |
| [ 'status' => 500 ] | |
| ); | |
| } | |
| $input = [ | |
| 'surface' => (string) $request->get_param( 'surface' ), | |
| 'ref' => (string) $request->get_param( 'ref' ), | |
| 'page' => (string) $request->get_param( 'page' ), | |
| ]; | |
| $payload = self::build_payload( $cfg, $input ); | |
| // Resolve which target this emit hits. 'success_mock' routes to the | |
| // dev-receive endpoint (dev-only); otherwise the configured target. | |
| $mode = (string) $request->get_param( 'mode' ); | |
| list( $outbound_url, $mode_label ) = self::resolve_outbound_target( $cfg['target_url'], $mode ); | |
| // POST with bearer auth. wp_remote_post handles transport errors gracefully. | |
| $resp = wp_remote_post( $outbound_url, [ | |
| 'method' => 'POST', | |
| 'timeout' => 10, | |
| 'headers' => [ | |
| 'Authorization' => 'Bearer ' . $cfg['token'], | |
| 'Content-Type' => 'application/json', | |
| 'X-SA-Schema-Version' => self::SCHEMA_VERSION, | |
| ], | |
| 'body' => wp_json_encode( $payload ), | |
| ] ); | |
| $http_status = null; | |
| $response_excerpt = null; | |
| $error_message = null; | |
| if ( is_wp_error( $resp ) ) { | |
| $error_message = $resp->get_error_message(); | |
| } else { | |
| $http_status = (int) wp_remote_retrieve_response_code( $resp ); | |
| $body = (string) wp_remote_retrieve_body( $resp ); | |
| $response_excerpt = function_exists( 'mb_substr' ) ? mb_substr( $body, 0, 500 ) : substr( $body, 0, 500 ); | |
| } | |
| // Determine status. Distinguishes "the receiver accepted the packet" from | |
| // "the attempt was made but did not arrive cleanly". This is the difference | |
| // between an attempted-emission log and a successful-receipt log; we | |
| // explicitly want the former so the operator can audit transport reality. | |
| // | |
| // success — HTTP 2xx received from the configured target | |
| // failed — anything else: transport error (connection/DNS/timeout) OR | |
| // a non-2xx HTTP response (4xx/5xx etc.) | |
| // | |
| // 'blocked_config' is intentionally NOT used here. Config validation | |
| // failed earlier returns a WP_Error with no I/O; no row is written. | |
| $is_2xx = ( $error_message === null && $http_status !== null && $http_status >= 200 && $http_status < 300 ); | |
| $status = $is_2xx ? 'success' : 'failed'; | |
| // Log the attempt. Log-only — no analytics, no aggregation, no replay. | |
| // target_url logs the actual URL hit (which is the resolved outbound, | |
| // not necessarily the configured one — so dev success_mock attempts | |
| // are honestly recorded as having gone to the dev-receive endpoint). | |
| global $wpdb; | |
| $tbl = $wpdb->prefix . SA_ORCH_DB_PREFIX . 'attention_log'; | |
| $wpdb->insert( $tbl, [ | |
| 'site_id' => (string) $cfg['site_id'], | |
| 'surface' => $payload['surface'], | |
| 'ref' => $payload['ref'], | |
| 'target_url' => (string) $outbound_url, | |
| 'payload_json' => wp_json_encode( $payload ), | |
| 'status' => $status, | |
| 'http_status' => $http_status, | |
| 'response_excerpt' => $response_excerpt, | |
| 'error_message' => $error_message, | |
| 'recorded_at' => current_time( 'mysql', true ), | |
| ] ); | |
| $log_id = (int) $wpdb->insert_id; | |
| return rest_ensure_response( [ | |
| 'log_id' => $log_id, | |
| 'status' => $status, | |
| 'http_status' => $http_status, | |
| 'response_excerpt' => $response_excerpt, | |
| 'error_message' => $error_message, | |
| 'payload' => $payload, // echo back so UI can confirm what went out | |
| 'mode_label' => $mode_label, // 'dev failure target' / 'dev success mock' / 'configured' | |
| 'target_url' => $outbound_url, // the URL actually hit, for UI display | |
| 'ok' => $is_2xx, | |
| ] ); | |
| } | |
| /** | |
| * Enqueue the floating "Send Attention" button on the same admin | |
| * surfaces where Sara appears, gated to manage_options. | |
| * Sara's bubble lives bottom-right; this button lives bottom-left. | |
| */ | |
| public static function maybe_enqueue( $hook ) { | |
| if ( ! current_user_can( 'manage_options' ) ) { | |
| return; | |
| } | |
| $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; | |
| $is_target = ( | |
| $page === 'fluent-boards' | |
| || $page === 'fluent-support' | |
| || strpos( $page, 'sa-orchestration' ) === 0 | |
| ); | |
| if ( ! $is_target ) { | |
| return; | |
| } | |
| wp_enqueue_script( | |
| self::HANDLE_JS, | |
| SA_ORCH_URL . 'assets/attention-bridge.js', | |
| [], | |
| SA_ORCH_VERSION, | |
| true | |
| ); | |
| $cfg = self::get_config(); | |
| $is_dev = class_exists( 'SA_Orch_Env' ) && SA_Orch_Env::is_local_or_dev(); | |
| wp_localize_script( self::HANDLE_JS, 'SA_ORCH_ATTENTION', [ | |
| 'rest_url' => esc_url_raw( rest_url( 'sa-orch/v1/attention/emit' ) ), | |
| 'nonce' => wp_create_nonce( 'wp_rest' ), | |
| 'page' => $page, | |
| 'site_id' => $cfg['site_id'], // for display only — not used as auth | |
| 'configured' => $cfg['site_id'] !== '' && $cfg['target_url'] !== '' && $cfg['token'] !== '', | |
| 'is_dev' => $is_dev, // Board #21 dev verification: when true, a second | |
| // "Send to dev mock" button appears alongside | |
| // the default "Send Attention". Production never | |
| // sees this flag; the second button never renders. | |
| ] ); | |
| } | |
| } | |