'OpenAI search discovery', 'GPTBot' => 'Training use']; /** @var bool */ private static bool $external_owner_present = false; /** @var bool */ private static bool $registered = false; public static function register(): void { if (self::$registered) { return; } if (function_exists('has_filter') && has_filter('robots_txt')) { self::$external_owner_present = true; } add_filter('robots_txt', [self::class, 'filter_robots'], 99, 2); self::$registered = true; } /** @return array */ public static function state(): array { $state = TheDotDevAgenticBridge_Storage::get()[self::OPTION_KEY] ?? []; return is_array($state) ? $state : []; } /** @return array{ok:bool, reason:string} */ public static function availability(): array { if (is_multisite()) return ['ok' => false, 'reason' => 'network_ambiguity']; if (self::physical_file_present()) return ['ok' => false, 'reason' => 'physical_robots_file']; if (self::$external_owner_present || (bool) apply_filters('thedotdev_agentic_bridge_crawler_policy_external_owner', false)) return ['ok' => false, 'reason' => 'other_policy_owner']; return ['ok' => true, 'reason' => '']; } /** @return array */ public static function choices(): array { return ['inherit' => 'Inherit current WordPress policy', 'allow' => 'Allow at site root', 'disallow' => 'Disallow at site root']; } /** @return array{ok:bool, proposal?:array, message:string} */ public static function preview(array $requested, int $actor_id): array { $available = self::availability(); if (!$available['ok']) return ['ok' => false, 'message' => self::reason_message($available['reason'])]; $policy = self::normalize_policy($requested); if ($policy === null) return ['ok' => false, 'message' => __('Choose a recognised crawler policy for each listed identity.', 'agentic-bridge')]; $state = self::state(); $current = is_array($state['active'] ?? null) ? $state['active'] : self::inherited(); $proposal = ['id' => wp_generate_uuid4(), 'state' => 'ready', 'actorId' => $actor_id, 'createdAt' => TheDotDevAgenticBridge_Readiness_Clock::iso(), 'expiresAt' => gmdate('c', time() + HOUR_IN_SECONDS), 'fingerprint' => self::fingerprint($current), 'current' => $current, 'proposed' => $policy]; TheDotDevAgenticBridge_Storage::update([self::OPTION_KEY => array_merge($state, ['proposal' => $proposal, 'audit' => self::append($state, 'previewed', $actor_id, 'ready')])]); return ['ok' => true, 'proposal' => $proposal, 'message' => __('Crawler policy preview is ready for named approval.', 'agentic-bridge')]; } /** @return array{ok:bool,message:string} */ public static function approve(string $id, int $actor_id): array { $state=self::state(); $proposal=is_array($state['proposal'] ?? null)?$state['proposal']:null; if (!is_array($proposal) || !hash_equals((string)($proposal['id']??''), $id) || self::stale($proposal)) return self::refuse($state, $actor_id, 'stale'); $proposal['state']='approved'; $proposal['approvedBy']=$actor_id; $proposal['approvedAt']=TheDotDevAgenticBridge_Readiness_Clock::iso(); TheDotDevAgenticBridge_Storage::update([self::OPTION_KEY=>array_merge($state,['proposal'=>$proposal,'audit'=>self::append($state,'approved',$actor_id,'approved')])]); return ['ok'=>true,'message'=>__('Crawler policy approved. Apply it when ready.', 'agentic-bridge')]; } /** @return array{ok:bool,message:string} */ public static function apply(string $id, string $fingerprint, int $actor_id): array { $state=self::state(); $proposal=is_array($state['proposal'] ?? null)?$state['proposal']:null; $available=self::availability(); if (!$available['ok']) return self::refuse($state,$actor_id,'conflict'); if (!is_array($proposal) || (string)($proposal['state']??'') !== 'approved' || !hash_equals((string)($proposal['id']??''),$id) || self::stale($proposal) || !hash_equals((string)($proposal['fingerprint']??''),$fingerprint) || !hash_equals(self::fingerprint(is_array($state['active']??null)?$state['active']:self::inherited()),$fingerprint)) return self::refuse($state,$actor_id,'stale'); $active=$proposal['proposed']; $verification=self::verify_policy($active); if ($verification['result'] !== 'passed') { return self::refuse($state, $actor_id, 'verification_failed'); } $proposal['state']='applied'; $proposal['appliedAt']=TheDotDevAgenticBridge_Readiness_Clock::iso(); $proposal['appliedBy']=$actor_id; $proposal['verification']=$verification; TheDotDevAgenticBridge_Storage::update([self::OPTION_KEY=>array_merge($state,['active'=>$active,'proposal'=>$proposal,'audit'=>self::append($state,'applied',$actor_id,(string)$verification['result'])])]); return ['ok'=>true,'message'=>__('Crawler policy applied to WordPress’s virtual robots response and checked locally.', 'agentic-bridge')]; } /** @return array{ok:bool,message:string} */ public static function rollback(string $id, int $actor_id): array { $state=self::state(); $proposal=is_array($state['proposal'] ?? null)?$state['proposal']:null; $available=self::availability(); if (!$available['ok'] || !is_array($proposal) || (string)($proposal['state']??'') !== 'applied' || !hash_equals((string)($proposal['id']??''),$id)) return self::refuse($state,$actor_id,'conflict'); $current=is_array($state['active']??null)?$state['active']:[]; if (!hash_equals(self::fingerprint($proposal['proposed'] ?? []),self::fingerprint($current))) return self::refuse($state,$actor_id,'conflict'); $proposal['state']='restored'; $proposal['restoredAt']=TheDotDevAgenticBridge_Readiness_Clock::iso(); $proposal['restoredBy']=$actor_id; TheDotDevAgenticBridge_Storage::update([self::OPTION_KEY=>array_merge($state,['active'=>$proposal['current'] ?? self::inherited(),'proposal'=>$proposal,'audit'=>self::append($state,'restored',$actor_id,'restored')])]); return ['ok'=>true,'message'=>__('Prior virtual crawler policy restored.', 'agentic-bridge')]; } public static function filter_robots(string $output, bool $public): string { $available=self::availability(); $state=self::state(); $active=is_array($state['active']??null)?$state['active']:[]; if (!$public || !$available['ok'] || $active === []) return $output; return self::render_policy($output, $active); } /** @return array */ private static function inherited(): array { return array_fill_keys(array_keys(self::BOTS),'inherit'); } /** @return array|null */ private static function normalize_policy(array $input): ?array { $out=[]; foreach (self::BOTS as $agent=>$_) { $choice=sanitize_key((string)($input[$agent]??'inherit')); if(!array_key_exists($choice,self::choices())) return null; $out[$agent]=$choice; } return $out; } /** @param array $policy */ private static function fingerprint(array $policy): string { ksort($policy); return hash('sha256',wp_json_encode($policy) ?: ''); } /** @param array $proposal */ private static function stale(array $proposal): bool { return strtotime((string)($proposal['expiresAt']??'')) < time(); } private static function physical_file_present(): bool { return defined('ABSPATH') && is_readable(rtrim(ABSPATH,'/').'/robots.txt'); } /** @return array{result:string,checkedAt:string,bots:array} */ private static function verify_policy(array $policy): array { $normalized = self::normalize_policy($policy); // This is a local simulation, not a claim about an external crawler response. $baseline = (string) apply_filters('thedotdev_agentic_bridge_crawler_policy_verification_baseline', "User-agent: *\nAllow: /\n"); $output = $normalized === null ? $baseline : self::render_policy($baseline, $normalized); $output = (string) apply_filters('thedotdev_agentic_bridge_crawler_policy_verification_output', $output, $baseline, $normalized); $bots = []; $passed = $normalized !== null; foreach (self::BOTS as $bot => $_) { $choice = $normalized[$bot] ?? 'inherit'; $expected = $choice === 'inherit' ? TheDotDevAgenticBridge_Readiness_Robots::evaluate_root($baseline, $bot) : ($choice === 'disallow' ? 'disallow' : 'allow'); $observed = TheDotDevAgenticBridge_Readiness_Robots::evaluate_root($output, $bot); $bots[$bot] = ['expected' => $expected, 'observed' => $observed]; if ($expected !== $observed) { $passed = false; } } return ['result' => $passed ? 'passed' : 'failed', 'checkedAt' => TheDotDevAgenticBridge_Readiness_Clock::iso(), 'bots' => $bots]; } /** @param array $policy */ private static function render_policy(string $output, array $policy): string { $lines=[]; foreach ($policy as $agent=>$choice) { if ($choice === 'inherit') continue; $lines[]='User-agent: '.$agent; $lines[]=($choice === 'allow' ? 'Allow: /' : 'Disallow: /'); $lines[]=''; } return $lines === [] ? $output : rtrim($output)."\n\n# Agentic Bridge virtual crawler policy\n".implode("\n",$lines); } /** @param array $state @return list> */ private static function append(array $state,string $action,int $actor,string $result): array { $audit=is_array($state['audit']??null)?$state['audit']:[]; array_unshift($audit,['action'=>$action,'actorId'=>$actor,'at'=>TheDotDevAgenticBridge_Readiness_Clock::iso(),'result'=>$result]); return array_slice($audit,0,30); } /** @return array{ok:false,message:string} */ private static function refuse(array $state,int $actor,string $result): array { TheDotDevAgenticBridge_Storage::update([self::OPTION_KEY=>array_merge($state,['audit'=>self::append($state,'refused',$actor,$result)])]); return ['ok'=>false,'message'=>__('Crawler policy was refused because its preview is stale or another owner is active.', 'agentic-bridge')]; } private static function reason_message(string $reason): string { return match($reason) {'physical_robots_file'=>__('Crawler policy is unavailable because a physical robots.txt file already owns this route.', 'agentic-bridge'),'other_policy_owner'=>__('Crawler policy is unavailable because another plugin already owns virtual robots rules.', 'agentic-bridge'),'network_ambiguity'=>__('Crawler policy is unavailable because this multisite scope is ambiguous.', 'agentic-bridge'),default=>__('Crawler policy is unavailable.', 'agentic-bridge')}; } }