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
2 changes: 2 additions & 0 deletions includes/class-admin-page.php
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,8 @@ public static function get_localized_data(): array {
'modelId' => $settings->get_field( 'agentic_admin_model_id', 'Qwen2.5-7B-Instruct-q4f16_1-MLC' ),
'confirmDestructive' => (bool) $settings->get_field( 'agentic_admin_confirm_destructive', 1 ),
'maxLogLines' => (int) $settings->get_field( 'agentic_admin_max_log_lines', 100 ),
'modelSource' => Settings::get_model_source(),
'canManageOptions' => current_user_can( 'manage_options' ),
),
'browserRequirements' => Utils::get_browser_requirements(),
'abilities' => $abilities_js_config,
Expand Down
67 changes: 67 additions & 0 deletions includes/class-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ public static function get_instance(): Settings {
public function __construct() {
$this->init_settings();

add_action( 'init', array( $this, 'register_model_source_setting' ) );

// UX: Add settings link to plugin list table.
add_filter(
'plugin_action_links_' . plugin_basename( AGENTIC_ADMIN_FILE ),
Expand Down Expand Up @@ -181,6 +183,71 @@ public function update_field( string $field, $value, string $type = 'text' ): vo
$this->settings[ $field ] = $cleaned;
}

/**
* Register the model source option.
*
* The local engine downloads model files only from the addresses the
* site owner enters here. The plugin ships no default. Exposed through
* the core /wp/v2/settings endpoint, which requires manage_options.
*
* @return void
*/
public function register_model_source_setting(): void {
register_setting(
'agentic_admin',
'agentic_admin_model_source',
array(
'type' => 'object',
'description' => __( 'Where the local AI engine downloads model files from.', 'agentic-admin' ),
'default' => array(
'weights_url' => '',
'library_url' => '',
),
'sanitize_callback' => array( __CLASS__, 'sanitize_model_source' ),
'show_in_rest' => array(
'schema' => array(
'type' => 'object',
'properties' => array(
'weights_url' => array( 'type' => 'string' ),
'library_url' => array( 'type' => 'string' ),
),
'additionalProperties' => false,
),
),
)
);
}

/**
* Sanitize the model source option.
*
* Only https addresses are kept: the admin screen is served over https on
* most sites, where browsers block http downloads as mixed content.
*
* @param mixed $value Raw value.
* @return array{weights_url: string, library_url: string}
*/
public static function sanitize_model_source( $value ): array {
$value = is_array( $value ) ? $value : array();
$clean = array();

foreach ( array( 'weights_url', 'library_url' ) as $key ) {
$url = isset( $value[ $key ] ) ? esc_url_raw( trim( (string) $value[ $key ] ), array( 'https' ) ) : '';
$clean[ $key ] = '' === $url ? '' : trailingslashit( $url );
}

return $clean;
}

/**
* Get the configured model source.
*
* @return array{weights_url: string, library_url: string}
*/
public static function get_model_source(): array {
return self::sanitize_model_source( get_option( 'agentic_admin_model_source', array() ) );
}

/**
* Save settings to database.
*
Expand Down
21 changes: 17 additions & 4 deletions readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ Agentic Admin transforms your WordPress admin panel into an intelligent command
== Screenshots ==

1. The Agentic Admin chat tab in wp-admin, mid-conversation. The model has just answered a question about installed plugins by calling the `plugin-list` tool locally — full ReAct trace (user question, thought process, tool call, answer) visible.
2. First-run model download in progress. The Qwen 3 1.7B weights (~1.2 GB) are fetched from the MLC-AI / HuggingFace CDN — once per browser, cancellable, cached for subsequent sessions.
2. First-run model download in progress. The Qwen 3 1.7B weights (~1.2 GB) are fetched from the configured model source — once per browser, cancellable, cached for subsequent sessions.
3. The Abilities browser, listing every tool the assistant can call against the WordPress Abilities API on this site.
4. Settings panel. See detected GPU + VRAM, tune context-window size per model based on your hardware, toggle thinking mode, and switch between the local engine (WebLLM + WebGPU), a remote OpenAI-compatible endpoint, or the WordPress 7.0 Connector.
4. Settings panel. Set the model source, see detected GPU + VRAM, tune context-window size per model based on your hardware, toggle thinking mode, and switch between the local engine (WebLLM + WebGPU), a remote OpenAI-compatible endpoint, or the WordPress 7.0 Connector.
5. Multi-step workflow execution. "Do a performance check" is recognized as a 2-step workflow — the assistant runs `site-health` and `error-log-read` in sequence, then summarizes the environment (WP version, PHP, memory, debug mode, error log status) in one answer.
6. WordPress 7.0 AI Connector integration. The Connector tab picks up any AI provider registered via WP 7.0's built-in Connector API — Anthropic, Google, OpenAI, or any third-party `ai_provider` plugin — and uses it as the model backend with zero extra setup.

Expand All @@ -51,8 +51,15 @@ 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. 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.
**Model source for the local engine (site-owner configured)** — Only when an administrator sets a model source and loads a model.
The local engine runs a language model in the administrator's browser. The model is not part of the plugin: model files are over 1 GB. The plugin contains no model download address. An administrator enters where models are downloaded from under Settings → Model source, and until then the local engine is off. Nothing is downloaded until an administrator then selects a model and clicks Load Model. The browser fetches the files directly from the configured source (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.

To use the models published by the MLC-AI project, enter these addresses. No account or API key is needed:

* Model weights URL: `https://huggingface.co/mlc-ai/`
* Model library URL: `https://raw.githubusercontent.com/mlc-ai/binary-mlc-llm-libs/main/web-llm-models/`

You can also host the same files yourself and enter your own https addresses. Use the same layout: each model's weights in `<weights URL>/<model ID>/resolve/main/`, and the compiled model libraries in `<library URL>/<WebLLM version>/` (the plugin adds the version it needs, currently `v0_2_80`).
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 @@ -92,6 +99,11 @@ The files under `build-extensions/` are generated from the sources in `src/` wit

There is no build step for the PHP. The WebLLM engine is bundled into the plugin from its npm package. The AI model weights and their compiled model libraries are loaded at runtime from the provider documented under External services above; these are large provider-hosted model files (over 1 GB), not part of the plugin code.

== Upgrade Notice ==

= 0.11.0 =
The local engine now downloads models only from a source you set. If you used the local engine before, open Settings → Model source and enter the addresses listed under External services. Models already in your browser cache are reused.

== Changelog ==

= 0.11.0 =
Expand All @@ -118,6 +130,7 @@ There is no build step for the PHP. The WebLLM engine is bundled into the plugin
* 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.
* Changed: the plugin no longer contains any model download address. The site owner sets the model source under Settings → Model source (the MLC-AI addresses are listed under External services), and the local engine stays off until it is set. WebLLM's built-in model list is removed at build time.
* 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
20 changes: 19 additions & 1 deletion src/extensions/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,23 @@ const App = () => {
);
const [ initProgress, setInitProgress ] = useState( 5 );

// "#model-source" links (e.g. from the Load Model notice) open Settings.
const tabFromHash = () =>
window.location.hash === '#model-source' ? 'settings' : 'chat';
const [ initialTab, setInitialTab ] = useState( tabFromHash );
const [ tabPanelKey, setTabPanelKey ] = useState( 0 );

useEffect( () => {
const onHashChange = () => {
if ( window.location.hash === '#model-source' ) {
setInitialTab( 'settings' );
setTabPanelKey( ( key ) => key + 1 );
}
};
window.addEventListener( 'hashchange', onHashChange );
return () => window.removeEventListener( 'hashchange', onHashChange );
}, [] );

const settings = window.agenticAdmin || {};
const {
i18n = {},
Expand Down Expand Up @@ -258,9 +275,10 @@ const App = () => {
return (
<div className="agentic-admin-app">
<TabPanel
key={ tabPanelKey }
className="agentic-admin-tabs"
tabs={ tabs }
initialTabName="chat"
initialTabName={ initialTab }
>
{ renderTab }
</TabPanel>
Expand Down
150 changes: 150 additions & 0 deletions src/extensions/components/ModelSourceCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/**
* Model Source Card
*
* Lets the site owner set where the local engine downloads model files
* from. Saved to the agentic_admin_model_source option through the core
* /wp/v2/settings endpoint (requires manage_options).
*/

import { useState, useEffect } from '@wordpress/element';
import apiFetch from '@wordpress/api-fetch';
import {
Button,
Card,
CardBody,
CardHeader,
Notice,
TextControl,
__experimentalVStack as VStack,
} from '@wordpress/components';
import {
getModelSource,
setModelSource,
isModelSourceConfigured,
subscribe,
} from '../services/model-source';

const ModelSourceCard = () => {
const canManage = Boolean(
window.agenticAdmin?.settings?.canManageOptions
);
const saved = getModelSource();
const [ weightsUrl, setWeightsUrl ] = useState( saved.weights_url );
const [ libraryUrl, setLibraryUrl ] = useState( saved.library_url );
const [ isSaving, setIsSaving ] = useState( false );
const [ notice, setNotice ] = useState( null );
const [ sourceReady, setSourceReady ] = useState( isModelSourceConfigured );

useEffect(
() => subscribe( () => setSourceReady( isModelSourceConfigured() ) ),
[]
);

// Scroll into view when opened from a #model-source link.
useEffect( () => {
if ( window.location.hash === '#model-source' ) {
document
.getElementById( 'agentic-admin-model-source' )
?.scrollIntoView( { behavior: 'smooth', block: 'start' } );
}
}, [] );

const handleSave = async () => {
setIsSaving( true );
setNotice( null );
try {
const response = await apiFetch( {
path: '/wp/v2/settings',
method: 'POST',
data: {
agentic_admin_model_source: {
weights_url: weightsUrl,
library_url: libraryUrl,
},
},
} );
const stored = response.agentic_admin_model_source || {};
setModelSource( stored );
setWeightsUrl( stored.weights_url || '' );
setLibraryUrl( stored.library_url || '' );
setNotice( { status: 'success', text: 'Model source saved.' } );
} catch ( err ) {
setNotice( {
status: 'error',
text: err?.message || 'Could not save the model source.',
} );
} finally {
setIsSaving( false );
}
};

return (
<Card id="agentic-admin-model-source">
<CardHeader>
<h3 style={ { margin: 0 } }>Model source</h3>
</CardHeader>
<CardBody>
<VStack spacing={ 3 }>
<p style={ { margin: 0 } }>
The local engine downloads the AI model from the
addresses below, and from nowhere else. The plugin has
no default: you choose the source. The addresses of the
models published by the MLC-AI project are listed in the
plugin&apos;s readme, under External services. You can
also host the same files yourself.
</p>
{ ! sourceReady && (
<Notice status="warning" isDismissible={ false }>
No model source is set, so the local engine is off.
The Remote and Connector engines still work.
</Notice>
) }
<TextControl
__nextHasNoMarginBottom
label="Model weights URL"
help="One folder per model, laid out as <model-id>/resolve/main/ (the Hugging Face layout)."
type="url"
value={ weightsUrl }
onChange={ setWeightsUrl }
disabled={ ! canManage || isSaving }
/>
<TextControl
__nextHasNoMarginBottom
label="Model library URL"
help="One folder per WebLLM version (for example v0_2_80) holding the compiled model libraries (.wasm). The plugin adds the version it needs."
type="url"
value={ libraryUrl }
onChange={ setLibraryUrl }
disabled={ ! canManage || isSaving }
/>
{ canManage ? (
<div>
<Button
variant="primary"
onClick={ handleSave }
isBusy={ isSaving }
disabled={ isSaving }
>
Save model source
</Button>
</div>
) : (
<p style={ { margin: 0 } }>
Only administrators can change the model source.
</p>
) }
{ notice && (
<Notice
status={ notice.status }
onDismiss={ () => setNotice( null ) }
>
{ notice.text }
</Notice>
) }
</VStack>
</CardBody>
</Card>
);
};

export default ModelSourceCard;
Loading
Loading