Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion agentic-admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
* Text Domain: agentic-admin
* Requires at least: 6.9
* Requires PHP: 8.2
* Tested up to: 7.1
*
* @package AgenticAdmin
*/
Expand Down
32 changes: 28 additions & 4 deletions includes/class-llm-proxy.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' );
Expand Down Expand Up @@ -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();
}
Expand Down
5 changes: 4 additions & 1 deletion readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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).
Expand Down
15 changes: 7 additions & 8 deletions src/extensions/components/ModelStatus.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
</Notice>
<Notice
status="warning"
Expand Down
Loading