$vars */ public static function register_query_vars(array $vars): array { $vars[] = self::ARTIFACT_QUERY_VAR; return $vars; } public static function public_route_decision(): string { if (TheDotDevAgenticBridge_Storage::kill_switch_enabled()) { return self::ROUTE_KILL_SWITCH; } if (!TheDotDevAgenticBridge_Storage::artifacts_published()) { return self::ROUTE_UNPUBLISHED; } return self::ROUTE_SERVE; } public static function should_serve_public_artifacts(): bool { return self::public_route_decision() === self::ROUTE_SERVE; } public static function maybe_serve_artifact(): void { $response = self::public_artifact_response_for_request(); if ($response === null) { return; } status_header($response['status']); if ($response['content_type'] !== '') { header('Content-Type: ' . $response['content_type']); } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Plugin-owned artifact bytes (plain text or JSON). echo $response['body']; exit; } /** * Resolve which public artifact (if any) this request asks for. * Prefer rewrite/query var (including $_GET); fall back to exact home-relative path. */ public static function resolve_requested_artifact(): string { $from_query = self::artifact_from_query_vars(); if ($from_query === 'llms' || $from_query === 'agent-map') { return $from_query; } return self::resolve_artifact_from_request_path(); } private static function artifact_from_query_vars(): string { foreach ([self::ARTIFACT_QUERY_VAR] as $query_var) { $from_query = (string) get_query_var($query_var); if ($from_query === 'llms' || $from_query === 'agent-map') { return $from_query; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Public read-only artifact route selector (no state mutation); single raw GET read, then scalar check + wp_unslash + sanitize_key before any use. $raw_artifact = $_GET[$query_var] ?? null; if (is_string($raw_artifact)) { $unslashed = wp_unslash($raw_artifact); $from_query = sanitize_key($unslashed); if ($from_query === 'llms' || $from_query === 'agent-map') { return $from_query; } } } return ''; } /** * Exact path match only: {home_base}/llms.txt or {home_base}/agent-map.json * (optional trailing slash — WP canonical may add one if we ever miss priority 0). * Supports optional query strings via path extraction; rejects lookalikes/suffixes/admin paths. */ public static function resolve_artifact_from_request_path(): string { $path = self::normalized_request_path(); if ($path === '') { return ''; } $base = self::home_base_path(); $llms = $base . '/llms.txt'; $map = $base . '/agent-map.json'; if ($path === $llms || $path === $llms . '/') { return 'llms'; } if ($path === $map || $path === $map . '/') { return 'agent-map'; } return ''; } /** * Home URL path without trailing slash ('' for site root). * Uses WordPress URL APIs so subdirectory installs resolve correctly. */ public static function home_base_path(): string { $home_path = wp_parse_url(home_url('/'), PHP_URL_PATH); if (!is_string($home_path) || $home_path === '' || $home_path === '/') { return ''; } return untrailingslashit($home_path); } /** * @return array{status: int, content_type: string, body: string}|null Null when this request is not an artifact route. */ public static function public_artifact_response_for_request(): ?array { $artifact = self::resolve_requested_artifact(); if ($artifact !== 'llms' && $artifact !== 'agent-map') { return null; } $decision = self::public_route_decision(); if ($decision === self::ROUTE_KILL_SWITCH) { return [ 'status' => 503, 'content_type' => 'text/plain; charset=utf-8', 'body' => 'Agentic Bridge artifacts are temporarily unavailable.', ]; } if ($decision === self::ROUTE_UNPUBLISHED) { return [ 'status' => 404, 'content_type' => '', 'body' => '', ]; } $payload = self::published_artifact_payload(); if ($artifact === 'llms') { return [ 'status' => 200, 'content_type' => 'text/plain; charset=utf-8', 'body' => wp_strip_all_tags((string) ($payload['llms_txt'] ?? '')), ]; } $encoded = wp_json_encode($payload['agent_map'] ?? new stdClass(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); return [ 'status' => 200, 'content_type' => 'application/json; charset=utf-8', 'body' => is_string($encoded) ? $encoded : '', ]; } /** * Safely extract a normalized request path from REQUEST_URI (path only; query ignored). */ private static function normalized_request_path(): string { if (!isset($_SERVER['REQUEST_URI'])) { return ''; } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized immediately and constrained below; not stored. $raw_value = wp_unslash((string) $_SERVER['REQUEST_URI']); $raw = sanitize_text_field($raw_value); if ($raw === '' || str_contains($raw, "\0")) { return ''; } $path = wp_parse_url($raw, PHP_URL_PATH); if (!is_string($path) || $path === '') { // REQUEST_URI is path-shaped input. Strip query/fragment directly; // never manufacture an origin or initiate any request. $candidate = str_starts_with($raw, '/') ? $raw : '/' . $raw; $path = strtok($candidate, '?#'); } if (!is_string($path) || $path === '' || str_contains($path, "\0") || str_contains($path, '..')) { return ''; } $decoded = rawurldecode($path); if (!is_string($decoded) || $decoded === '' || str_contains($decoded, "\0") || str_contains($decoded, '..')) { return ''; } return $decoded; } /** @return array{llms_txt: string, agent_map: array, content_ids: list} */ public static function published_artifact_payload(): array { if (!TheDotDevAgenticBridge_Storage::artifacts_published()) { return TheDotDevAgenticBridge_Artifact_Validator::empty_safe_payload(); } $settings = TheDotDevAgenticBridge_Storage::get(); $last_known = is_array($settings['last_known_good'] ?? null) ? $settings['last_known_good'] : []; $sanitized = TheDotDevAgenticBridge_Artifact_Validator::sanitize_published_lkg($last_known); return $sanitized ?? TheDotDevAgenticBridge_Artifact_Validator::empty_safe_payload(); } /** @return array{llms_txt: string, agent_map: array, content_ids: list} */ public static function generate_current(): array { TheDotDevAgenticBridge_Content_Metadata::prune_stale_metadata(); $entries = []; $content_ids = []; foreach (TheDotDevAgenticBridge_Content_Metadata::artifact_eligible_content_items() as $item) { $post = get_post((int) $item['id']); if (!$post instanceof WP_Post) { continue; } $config = TheDotDevAgenticBridge_Content_Metadata::effective_config_for_post($post); $content_ids[] = (int) $item['id']; $entry = [ 'url' => (string) $item['url'], 'title' => (string) $item['title'], 'type' => (string) $item['type'], 'contentId' => (int) $item['id'], 'intent' => (string) $config['intent'], 'summary' => (string) ($config['agentSummary'] ?? ''), 'allowedActions' => TheDotDevAgenticBridge_Content_Metadata::export_allowed_actions($config['allowedActions'] ?? []), 'blockedClaims' => array_values(array_merge( is_array($config['blockedClaims'] ?? null) ? $config['blockedClaims'] : [], is_array($config['customBlockedClaims'] ?? null) ? $config['customBlockedClaims'] : [], )), ]; $relationship = TheDotDevAgenticBridge_Content_Metadata::export_relationship($config); if ($relationship !== null) { $entry['relationship'] = $relationship; } $entries[] = $entry; } $agent_map = [ 'schemaVersion' => self::SCHEMA_VERSION, 'siteUrl' => home_url('/'), 'siteName' => get_bloginfo('name'), 'generatedAt' => gmdate('c'), 'state' => 'published', 'policy' => [ 'metadataOnly' => true, 'executable' => false, ], 'entries' => $entries, ]; return [ 'llms_txt' => TheDotDevAgenticBridge_Artifact_Validator::build_llms_txt_from_map($agent_map), 'agent_map' => $agent_map, 'content_ids' => $content_ids, ]; } /** * Preserves the bounded preview draft after metadata/default/exclusion changes. * Staleness is derived from source_fingerprint mismatch in is_preview_stale(). */ public static function mark_preview_stale(): void { // Intentionally does not clear artifact_preview_draft. } public static function clear_preview_draft(): void { TheDotDevAgenticBridge_Storage::update(['artifact_preview_draft' => null]); } public static function invalidate_preview_draft(): void { self::clear_preview_draft(); } public static function is_preview_stale(?array $draft = null): bool { $draft = $draft ?? self::preview_draft(); if (!is_array($draft) || empty($draft['source_fingerprint'])) { return true; } return !hash_equals((string) $draft['source_fingerprint'], TheDotDevAgenticBridge_Content_Metadata::source_fingerprint()); } /** @return array|null */ public static function preview_draft(): ?array { $draft = TheDotDevAgenticBridge_Storage::get()['artifact_preview_draft'] ?? null; if (!is_array($draft)) { return null; } if (strlen((string) ($draft['llms_txt'] ?? '')) > TheDotDevAgenticBridge_Artifact_Validator::MAX_LLMS_BYTES) { return null; } return $draft; } /** @return array{ok: bool, draft: array|null, error: string} */ public static function generate_preview_draft(): array { TheDotDevAgenticBridge_Content_Metadata::prune_stale_metadata(); if (TheDotDevAgenticBridge_Content_Metadata::artifact_publish_cap_exceeded()) { return ['ok' => false, 'draft' => null, 'error' => 'draft_entry_cap_exceeded']; } $payload = self::generate_current(); $agent_map = $payload['agent_map']; $encoded = wp_json_encode($agent_map) ?: ''; if (strlen((string) $payload['llms_txt']) > TheDotDevAgenticBridge_Artifact_Validator::MAX_LLMS_BYTES || strlen($encoded) > TheDotDevAgenticBridge_Artifact_Validator::MAX_AGENT_MAP_BYTES) { return ['ok' => false, 'draft' => null, 'error' => 'draft_oversize']; } $validation = TheDotDevAgenticBridge_Artifact_Validator::validate_agent_map_strict($agent_map, true); if (!$validation['valid']) { return ['ok' => false, 'draft' => null, 'error' => 'draft_invalid']; } $draft = [ 'llms_txt' => $payload['llms_txt'], 'agent_map' => $agent_map, 'content_ids' => $payload['content_ids'], 'source_fingerprint' => TheDotDevAgenticBridge_Content_Metadata::source_fingerprint(), 'generated_at' => gmdate('c'), ]; TheDotDevAgenticBridge_Storage::update(['artifact_preview_draft' => $draft]); return ['ok' => true, 'draft' => $draft, 'error' => '']; } /** @return array{ok: bool, error: string} */ public static function publish_from_preview(): array { $draft = self::preview_draft(); if ($draft === null || empty($draft['source_fingerprint'])) { return ['ok' => false, 'error' => 'missing_preview']; } if (self::is_preview_stale($draft)) { return ['ok' => false, 'error' => 'stale_preview']; } $validation = TheDotDevAgenticBridge_Artifact_Validator::validate_preview_draft_for_publish($draft); if (!$validation['ok']) { return ['ok' => false, 'error' => $validation['error']]; } // Apache + plain permalinks: install exact-path .htaccess bridge before claiming publish. $bridge = TheDotDevAgenticBridge_Artifact_Route_Bridge::ensure_for_publish(); if (!$bridge['ok']) { return ['ok' => false, 'error' => $bridge['error']]; } self::archive_current_revision(); TheDotDevAgenticBridge_Storage::save_last_known_good([ 'llms_txt' => (string) ($draft['llms_txt'] ?? ''), 'agent_map' => is_array($draft['agent_map'] ?? null) ? $draft['agent_map'] : [], 'content_ids' => TheDotDevAgenticBridge_Artifact_Validator::normalize_content_ids($draft['content_ids'] ?? []), ]); self::clear_preview_draft(); return ['ok' => true, 'error' => '']; } public static function publish_artifacts(): void { $result = self::generate_preview_draft(); if (!$result['ok']) { return; } self::publish_from_preview(); } /** @return array{ok: bool, error: string} */ public static function rollback_revision(int $revision_index): array { $settings = TheDotDevAgenticBridge_Storage::get(); $revisions = is_array($settings['artifact_revisions'] ?? null) ? $settings['artifact_revisions'] : []; if (!isset($revisions[$revision_index]) || !is_array($revisions[$revision_index])) { return ['ok' => false, 'error' => 'revision_not_found']; } $sanitized = TheDotDevAgenticBridge_Artifact_Validator::sanitize_published_lkg($revisions[$revision_index]); if ($sanitized === null) { return ['ok' => false, 'error' => 'revision_unsafe']; } $original_ids = TheDotDevAgenticBridge_Artifact_Validator::normalize_content_ids($revisions[$revision_index]['content_ids'] ?? []); if ($original_ids !== [] && ($sanitized['content_ids'] ?? []) === []) { return ['ok' => false, 'error' => 'revision_all_ineligible']; } self::archive_current_revision(); TheDotDevAgenticBridge_Storage::save_last_known_good($sanitized); self::clear_preview_draft(); return ['ok' => true, 'error' => '']; } /** @return array{added: list, removed: list, changed: list, metadataChanged: bool} */ public static function preview_change_summary(?array $draft = null): array { $draft = $draft ?? self::preview_draft(); $published = TheDotDevAgenticBridge_Storage::get()['last_known_good'] ?? []; $published_ids = TheDotDevAgenticBridge_Artifact_Validator::normalize_content_ids($published['content_ids'] ?? []); $draft_ids = TheDotDevAgenticBridge_Artifact_Validator::normalize_content_ids(is_array($draft) ? ($draft['content_ids'] ?? []) : []); $added = array_values(array_diff($draft_ids, $published_ids)); $removed = array_values(array_diff($published_ids, $draft_ids)); $changed = []; $metadata_changed = false; $published_map = []; foreach (is_array($published['agent_map']['entries'] ?? null) ? $published['agent_map']['entries'] : [] as $entry) { if (is_array($entry)) { $published_map[(int) ($entry['contentId'] ?? 0)] = $entry; } } if (is_array($draft)) { foreach (($draft['agent_map']['entries'] ?? []) as $entry) { if (!is_array($entry)) { continue; } $id = (int) ($entry['contentId'] ?? 0); if ($id <= 0) { continue; } if (!isset($published_map[$id])) { continue; } $pub_fp = TheDotDevAgenticBridge_Artifact_Validator::semantic_map_fingerprint(['entries' => [$published_map[$id]]]); $draft_fp = TheDotDevAgenticBridge_Artifact_Validator::semantic_map_fingerprint(['entries' => [$entry]]); if ($pub_fp !== $draft_fp) { $changed[] = $id; $metadata_changed = true; } } } return [ 'added' => $added, 'removed' => $removed, 'changed' => $changed, 'metadataChanged' => $metadata_changed, ]; } private static function archive_current_revision(): void { if (!TheDotDevAgenticBridge_Storage::artifacts_published()) { return; } $settings = TheDotDevAgenticBridge_Storage::get(); $last_known = is_array($settings['last_known_good'] ?? null) ? $settings['last_known_good'] : []; if ($last_known === []) { return; } $sanitized = TheDotDevAgenticBridge_Artifact_Validator::sanitize_published_lkg($last_known); if ($sanitized === null) { return; } $revisions = is_array($settings['artifact_revisions'] ?? null) ? $settings['artifact_revisions'] : []; array_unshift($revisions, $sanitized); $revisions = array_slice($revisions, 0, self::MAX_REVISIONS); TheDotDevAgenticBridge_Storage::update(['artifact_revisions' => $revisions]); } }