'array', 'sanitize_callback' => [Ai2Web_Settings::class, 'sanitize'], 'default' => Ai2Web_Settings::defaults(), ]); } public static function render(): void { if (!current_user_can('manage_options')) { return; } $s = Ai2Web_Settings::all(); $manifest = Ai2Web_Manifest::build(); $score = self::score($manifest); $has_woo = class_exists('WooCommerce'); $pretty = (bool) get_option('permalink_structure'); echo '
'; echo '

' . esc_html__('AI2Web', 'ai2web') . '

'; echo '

' . esc_html__('Describe your site once so AI agents can discover, understand and act on it.', 'ai2web') . '

'; if (!$pretty) { echo '

'; printf( /* translators: %s: Permalinks settings URL */ wp_kses(__('AI2Web needs pretty permalinks. Set them under Settings → Permalinks (any option except “Plain”).', 'ai2web'), ['a' => ['href' => []]]), esc_url(admin_url('options-permalink.php')) ); echo '

'; } // Readiness panel. $band = $score['score'] >= 80 ? '#1f883d' : ($score['score'] >= 50 ? '#bf8700' : '#cf222e'); echo '
'; echo '

' . esc_html__('AI Readiness', 'ai2web') . '

'; echo '

'; echo '' . esc_html((string) $score['score']) . ''; echo ' / 100   '; echo '' . esc_html($score['tier']) . ''; echo '

'; echo ''; $home = home_url('/ai2w'); echo '

'; echo esc_html__('Manifest:', 'ai2web') . ' ' . esc_html($home) . ''; if (!empty($s['mcp_enabled'])) { $mcp = home_url('/ai2w/mcp'); echo '  ·  ' . esc_html__('MCP endpoint:', 'ai2web') . ' ' . esc_html($mcp) . ''; } echo '

'; echo '

' . esc_html__('Open in the AI2Web Validator', 'ai2web') . ''; if ($has_woo && current_user_can('manage_woocommerce')) { echo ' ' . esc_html__('View Agent Sales', 'ai2web') . ''; } echo '

'; echo '
'; // Settings form. echo '
'; settings_fields('ai2web_group'); $opt = Ai2Web_Settings::OPTION; echo ''; self::checkbox_row($opt, 'enabled', !empty($s['enabled']), __('Enable AI2Web', 'ai2web'), __('Serve the /ai2w manifest and endpoints.', 'ai2web')); echo ''; self::checkbox_row($opt, 'mcp_enabled', !empty($s['mcp_enabled']), __('MCP endpoint', 'ai2web'), __('Expose /ai2w/mcp so AI assistants (Claude, ChatGPT connectors) can use your actions directly.', 'ai2web')); self::checkbox_row($opt, 'nlweb', !empty($s['nlweb']), __('NLWeb endpoint', 'ai2web'), __('Expose /ai2w/nlweb/ask, an NLWeb-compatible (nlweb.ai) natural-language query over your posts, pages and products, so agents that speak NLWeb can search your site. Returns schema.org results.', 'ai2web')); self::checkbox_row($opt, 'agent_service', !empty($s['agent_service']), __('Agent service', 'ai2web'), __('Expose /ai2w/agent, a natural-language endpoint answered by WordPress\'s built-in AI Client using the provider you connected. It only appears in the manifest when a provider is available.', 'ai2web')); if (!function_exists('wp_ai_client_prompt')) { echo ''; } self::checkbox_row($opt, 'oauth2', !empty($s['oauth2']), __('OAuth2 (PKCE)', 'ai2web'), __('Let agents authenticate via an OAuth2 authorization-code + PKCE flow, where a logged-in user approves access. Anonymous, ownership-gated access remains the fallback.', 'ai2web')); if ($has_woo) { self::checkbox_row($opt, 'commerce_actions', !empty($s['commerce_actions']), __('WooCommerce actions', 'ai2web'), __('Product search, stock checks and order tracking (order tracking is verified by billing email).', 'ai2web')); self::checkbox_row($opt, 'returns_refunds', !empty($s['returns_refunds']), __('Return / refund requests', 'ai2web'), __('Let agents log return and refund requests for you to action. Never issues a refund automatically.', 'ai2web')); self::checkbox_row($opt, 'checkout', !empty($s['checkout']), __('Agent checkout', 'ai2web'), __('Let agents assemble a cart into a pending order and return a secure payment link. The customer pays in the browser; the agent never handles payment.', 'ai2web')); if (!empty($s['checkout'])) { self::checkbox_row($opt, 'acp', !empty($s['acp']), __('ACP checkout (Agentic Commerce Protocol)', 'ai2web'), __('Expose ACP checkout sessions at /ai2w/acp/checkout_sessions and a product feed at /ai2w/acp/feed, so shopper agents (e.g. ChatGPT Instant Checkout) can run a full cart -> shipping -> coupon -> pay flow.', 'ai2web')); if (!empty($s['acp'])) { if (Ai2Web_Stripe::available()) { echo ''; } else { echo ''; } } self::checkbox_row($opt, 'ap2', !empty($s['ap2']), __('AP2 (Agent Payments Protocol)', 'ai2web'), __('Also expose a Google AP2 merchant surface at /ai2w/ap2: the store returns a merchant-signed Cart Mandate for a buyer agent\'s Intent Mandate, and settles a Payment Mandate. Opt-in; it generates an RSA signing key (published as a JWKS) on first use.', 'ai2web')); if (!empty($s['ap2']) && !function_exists('openssl_sign')) { echo ''; } } } else { echo ''; } // Contact forms: surface detected providers so the user sees the submit_contact action was added. $form_labels = [ 'contact_form_7' => 'Contact Form 7', 'gravity_forms' => 'Gravity Forms', 'wpforms' => 'WPForms', 'fluent_forms' => 'Fluent Forms', 'elementor_forms' => 'Elementor Forms', ]; $forms_detected = array_keys(array_filter(Ai2Web_Forms::detect())); if (!empty($forms_detected)) { $names = array_map(static fn(string $p): string => $form_labels[$p] ?? $p, $forms_detected); echo ''; } else { echo ''; } echo ''; submit_button(); echo '
'; echo '
'; } private static function checkbox_row(string $opt, string $key, bool $checked, string $label, string $desc): void { echo '' . esc_html($label) . ''; echo ''; echo '

' . esc_html($desc) . '

'; echo ''; } /** * Local port of the AI2Web scoring algorithm (spec sections 9 and 11), kept in sync with * the reference validator. Returns score, tier and per-check results. * * @param array $m * @return array{score:int,tier:string,checks:array} */ public static function score(array $m): array { $has = static function ($v): bool { return $v === true || (is_array($v) && ($v['enabled'] ?? false) === true); }; $cap = static fn(string $n) => $m['capabilities'][$n] ?? null; $errors = 0; if (($m['protocol'] ?? '') !== 'ai2w') { $errors++; } if (!preg_match('/^\d+\.\d+(\.\d+)?$/', (string) ($m['version'] ?? ''))) { $errors++; } foreach (['name', 'url', 'type'] as $k) { if (empty($m['site'][$k])) { $errors++; } } if (empty($m['capabilities'])) { $errors++; } $actions_exist = $has($cap('actions')) || (!empty($m['actions']) && is_array($m['actions'])) || $has($cap('commerce')) || $has($cap('booking')); $commerce = $cap('commerce'); $checkout_ok = !$has($commerce) || (is_array($commerce) && ($commerce['checkout'] ?? false) === true); $oauth_ok = isset($m['auth']['methods']) && is_array($m['auth']['methods']) && in_array('oauth2', $m['auth']['methods'], true) && (($m['auth']['oauth2']['pkce'] ?? false) === true); $consent_declared = !empty($m['consent']['requires_user_approval_for']); $checks = [ ['ok' => $errors === 0, 'pts' => 30, 'label' => __('Valid discovery manifest', 'ai2web'), 'hint' => __('fix manifest errors', 'ai2web')], ['ok' => $has($cap('content')), 'pts' => 6, 'label' => __('Content', 'ai2web'), 'hint' => __('expose a content module', 'ai2web')], ['ok' => $has($cap('commerce')) || $has($cap('booking')) || $has($cap('services')), 'pts' => 6, 'label' => __('Products / services / booking', 'ai2web'), 'hint' => __('add WooCommerce or a services module', 'ai2web')], ['ok' => $has($cap('search')), 'pts' => 4, 'label' => __('Search', 'ai2web'), 'hint' => __('add a search capability', 'ai2web')], ['ok' => $actions_exist, 'pts' => 5, 'label' => __('Actions', 'ai2web'), 'hint' => __('declare actions', 'ai2web')], ['ok' => $has($cap('events')), 'pts' => 6, 'label' => __('Events', 'ai2web'), 'hint' => __('publish subscribable events', 'ai2web')], ['ok' => !empty($m['agent_service']['enabled']), 'pts' => 4, 'label' => __('Agent service', 'ai2web'), 'hint' => __('expose /ai2w/agent', 'ai2web')], ['ok' => $checkout_ok, 'pts' => 4, 'label' => __('Checkout', 'ai2web'), 'hint' => __('commerce present but checkout missing', 'ai2web')], ['ok' => !empty($m['transports']['mcp']['enabled']), 'pts' => 8, 'label' => __('MCP transport', 'ai2web'), 'hint' => __('enable the MCP endpoint', 'ai2web')], ['ok' => !empty($m['transports']['rest']['enabled']) || !empty($m['transports']['feeds']), 'pts' => 4, 'label' => __('REST / feeds', 'ai2web'), 'hint' => __('expose REST or feeds', 'ai2web')], ['ok' => !$actions_exist || $oauth_ok, 'pts' => 8, 'label' => __('OAuth2 + PKCE', 'ai2web'), 'hint' => __('protected actions need oauth2 + pkce', 'ai2web')], ['ok' => !$actions_exist || $consent_declared, 'pts' => 7, 'label' => __('Consent declared', 'ai2web'), 'hint' => __('declare consent for sensitive actions', 'ai2web')], ['ok' => !empty($m['identity']), 'pts' => 4, 'label' => __('Identity', 'ai2web'), 'hint' => __('add identity (privacy policy)', 'ai2web')], ['ok' => !empty($m['contact']), 'pts' => 4, 'label' => __('Contact', 'ai2web'), 'hint' => __('add a support email above', 'ai2web')], ]; $score = 0; $out = []; foreach ($checks as $c) { if ($c['ok']) { $score += $c['pts']; } $out[] = ['ok' => (bool) $c['ok'], 'label' => $c['label'], 'hint' => $c['ok'] ? '' : $c['hint']]; } $score = min(100, $score); $basic = $errors === 0; $standard = $basic && !empty($m['transports']) && (!$actions_exist || $consent_declared) && !empty($m['contact']); $enterprise = $standard && !empty($m['identity']) && !empty($m['auth']) && !empty($m['rate_limits']); $tier = $enterprise ? 'Enterprise' : ($standard ? 'Standard' : ($basic ? 'Basic' : 'Invalid')); return ['score' => $score, 'tier' => $tier, 'checks' => $out]; } }