diff --git a/agentic-admin.php b/agentic-admin.php index dec13c1..0e93ce2 100644 --- a/agentic-admin.php +++ b/agentic-admin.php @@ -20,7 +20,6 @@ * Text Domain: agentic-admin * Requires at least: 6.9 * Requires PHP: 8.2 - * Tested up to: 7.1 * * @package AgenticAdmin */ diff --git a/includes/class-llm-proxy.php b/includes/class-llm-proxy.php index a6b0e68..ae869ac 100644 --- a/includes/class-llm-proxy.php +++ b/includes/class-llm-proxy.php @@ -210,7 +210,8 @@ private static function proxy_streaming( string $url, array $headers, array $bod } // Set SSE headers. - header( 'Content-Type: text/event-stream' ); + header( 'Content-Type: text/event-stream; charset=utf-8' ); + header( 'X-Content-Type-Options: nosniff' ); header( 'Cache-Control: no-cache' ); header( 'Connection: keep-alive' ); header( 'X-Accel-Buffering: no' ); @@ -240,13 +241,36 @@ private static function proxy_streaming( string $url, array $headers, array $bod array( 'error' => 'LLM proxy request failed: ' . $response->get_error_message(), 'url' => $url, - ) + ), + JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT ); exit; } - $response_body = \wp_remote_retrieve_body( $response ); - echo $response_body; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + // Never relay the remote body verbatim. Each SSE event is decoded as + // JSON and re-encoded with wp_json_encode(), which escapes HTML- + // significant characters. Anything that is not a valid JSON event is + // dropped. + $json_flags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT; + $lines = preg_split( '/\r\n|\r|\n/', \wp_remote_retrieve_body( $response ) ); + + foreach ( $lines as $line ) { + $line = trim( $line ); + if ( ! str_starts_with( $line, 'data:' ) ) { + continue; + } + + $data = trim( substr( $line, 5 ) ); + if ( '[DONE]' === $data ) { + echo "data: [DONE]\n\n"; + continue; + } + + $event = json_decode( $data, true ); + if ( is_array( $event ) ) { + echo 'data: ' . wp_json_encode( $event, $json_flags ) . "\n\n"; + } + } if ( ob_get_level() ) { ob_flush(); } diff --git a/readme.txt b/readme.txt index 8a4a74f..b4b9177 100644 --- a/readme.txt +++ b/readme.txt @@ -52,7 +52,7 @@ This plugin runs AI locally in your browser by default, and your prompts and cha Separately from AI inference, some abilities query public data sources to do their job: a security scan checks your plugin versions against CVE databases, a checksum verification compares your files against WordPress.org, and a web search sends your query to a search engine. Every external request the plugin makes is listed here: **AI model download service (MLC-AI, hosted on Hugging Face and GitHub)** — Only when the local engine is used. -The local engine is the plugin's core service: it runs a language model in the administrator's browser. The model is not part of the plugin, because model files are over 1 GB and are published and versioned by the MLC-AI project. When the site owner selects a local model in the plugin's settings (Qwen 3 1.7B by default, ~1.2 GB, or Qwen 2.5 7B, ~4.5 GB), the administrator's browser downloads that model's weights from `https://huggingface.co/mlc-ai/` and its compiled model library from `https://raw.githubusercontent.com/mlc-ai/binary-mlc-llm-libs/`. The site owner's choice of engine and model determines what is downloaded. No account or API key is needed. Only HTTP GET requests for static files are made, directly from the browser (the WordPress server makes no requests for them), and no prompts, admin data, or telemetry are sent. Files are cached in the browser after the first download. +The local engine is the plugin's core service: it runs a language model in the administrator's browser. The model is not part of the plugin, because model files are over 1 GB and are published and versioned by the MLC-AI project. Nothing is downloaded until an administrator selects a model (Qwen 3 1.7B by default, ~1.2 GB, or Qwen 2.5 7B, ~4.5 GB) and clicks Load Model. The browser then downloads that model's weights from `https://huggingface.co/mlc-ai/` and its compiled model library from `https://raw.githubusercontent.com/mlc-ai/binary-mlc-llm-libs/`. The site owner's choice of engine and model determines what is downloaded. No account or API key is needed. Only HTTP GET requests for static files are made, directly from the browser (the WordPress server makes no requests for them), and no prompts, admin data, or telemetry are sent. Files are cached in the browser after the first download. Hugging Face terms: https://huggingface.co/terms-of-service — Privacy: https://huggingface.co/privacy GitHub terms: https://docs.github.com/en/site-policy/github-terms/github-terms-of-service — Privacy: https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement @@ -115,6 +115,9 @@ There is no build step for the PHP. The WebLLM engine is bundled into the plugin * Changed: AI Connectors are detected through the AI Client registry, as on the core Connectors screen, instead of reading API-key options. * Fixed: plugin, must-use plugin, and content paths are derived from the plugin's own location and WordPress functions instead of WP_PLUGIN_DIR, WPMU_PLUGIN_DIR, and WP_CONTENT_DIR. Core checksum verification no longer miscounts files when the content directory is moved. The debug log path follows WordPress's own error_log setting. * Fixed: read-file and uploads-scan resolve the site root with get_home_path(), so subdirectory installs work. +* Security: the external LLM proxy no longer relays the provider's response verbatim. Each streamed event is decoded and re-encoded as escaped JSON; anything else is dropped. +* Changed: "Tested up to" is declared only in readme.txt. +* Fixed: the model notice names where the model is downloaded from and its real size, instead of saying no data is sent to external servers. * Fixed: `.well-known` scanning resolves via get_home_path() instead of ABSPATH, so subdirectory installs scan the real site root. * Removed: 7 stale tab references and 6+ stale docs files (FEEDBACK-DEV.md). * Tests: 96 unit tests passing, plus the new manifest test suite (7 cases), index test suite (6 cases), and react-agent regression tests (3 cases for the per-call state cleanup fix). diff --git a/src/extensions/components/ModelStatus.jsx b/src/extensions/components/ModelStatus.jsx index dd1a680..462d636 100644 --- a/src/extensions/components/ModelStatus.jsx +++ b/src/extensions/components/ModelStatus.jsx @@ -570,14 +570,13 @@ const ModelStatus = ( { status="info" isDismissible={ false } > - The AI model runs entirely in your - browser using WebGPU. The first load - will download model data (250MB-1GB - depending on model), which is cached for - future use. Using a Service Worker, the - model stays loaded as you navigate - wp-admin — no reload needed! No data is - sent to external servers. + Load Model downloads the selected model + once from MLC-AI, hosted on Hugging Face + and GitHub (about 1.2 GB for the + default), and caches it in your browser. + The model then runs in your browser + using WebGPU. Your prompts and site data + are not sent to these hosts.