'text/plain; charset=utf-8',
'robots.txt' => 'text/plain; charset=utf-8',
'agenticweb.md' => 'text/markdown; charset=utf-8',
'schema.jsonld' => 'application/ld+json; charset=utf-8',
'llm-info.html' => 'text/html; charset=utf-8',
'webmcp-registration.js' => 'application/javascript; charset=utf-8',
'.well-known/agent-permissions.json' => 'application/json; charset=utf-8',
'.well-known/agent-card.json' => 'application/json; charset=utf-8',
'.well-known/agent-skills/index.json' => 'application/json; charset=utf-8',
'.well-known/mcp/server-card.json' => 'application/json; charset=utf-8',
'.well-known/api-catalog' => 'application/linkset+json; charset=utf-8',
'.well-known/ucp' => 'application/json; charset=utf-8',
'sitemap.xml' => 'application/xml; charset=utf-8',
];
// Conditional-source package files. The generator ships sitemap.xml only
// when the customer's live site has no sitemap; it never clobbers an
// existing one. These files are accepted when present but never required,
// and their rewrite routes are registered only when actually installed so
// we do not shadow an existing Yoast/Rank Math/core sitemap.
private const OPTIONAL_FILES = [
'sitemap.xml',
];
public static function boot(): void {
add_action('init', [__CLASS__, 'add_rewrite_rules']);
add_filter('query_vars', [__CLASS__, 'add_query_vars']);
add_action('template_redirect', [__CLASS__, 'serve_virtual_file']);
add_filter('robots_txt', [__CLASS__, 'serve_robots_txt'], PHP_INT_MAX, 2);
add_action('admin_menu', [__CLASS__, 'add_admin_page']);
add_action('admin_post_agentreadywebsite_upload_package', [__CLASS__, 'handle_upload']);
}
public static function activate(): void {
self::add_rewrite_rules();
flush_rewrite_rules();
}
public static function deactivate(): void {
flush_rewrite_rules();
}
public static function add_rewrite_rules(): void {
$installed = self::package_files();
foreach (array_keys(self::PACKAGE_FILES) as $path) {
if ($path === 'robots.txt') {
continue;
}
// Never-clobber: optional files (e.g. sitemap.xml) only get a route
// when we actually installed them, so an existing site sitemap is
// left untouched.
if (in_array($path, self::OPTIONAL_FILES, true) && !isset($installed[$path])) {
continue;
}
add_rewrite_rule(
'^' . preg_quote($path, '/') . '$',
'index.php?' . self::QUERY_VAR . '=' . rawurlencode($path),
'top'
);
}
}
public static function add_query_vars(array $vars): array {
$vars[] = self::QUERY_VAR;
return $vars;
}
public static function add_admin_page(): void {
add_management_page(
'AgentReadyWebsite Installer',
'AgentReadyWebsite',
'manage_options',
'agentreadywebsite-installer',
[__CLASS__, 'render_admin_page']
);
}
public static function render_admin_page(): void {
if (!current_user_can('manage_options')) {
wp_die(esc_html__('You do not have permission to manage AgentReadyWebsite files.', 'agentreadywebsite-installer'));
}
$package = self::package_files();
$meta = self::package_meta();
$notice_input = filter_input(INPUT_GET, 'agentready_notice', FILTER_UNSAFE_RAW);
$notice = is_string($notice_input) ? sanitize_key(wp_unslash($notice_input)) : '';
?>
AgentReadyWebsite Installer
Upload the AgentReadyWebsite delivery zip. The plugin validates the package and serves each file at the canonical URL without requiring FTP or root-directory access.
AgentReadyWebsite package installed successfully.
The uploaded zip did not match the expected AgentReadyWebsite package shape.
The package could not be uploaded. Please try again or contact support.
Install package
Installed status
No AgentReadyWebsite package is installed yet.
Installed at:
| File |
Status |
Public URL |
$content_type) : ?>
|
|
|
gmdate('c'),
'file_count' => count($files),
], false);
self::add_rewrite_rules();
flush_rewrite_rules();
self::redirect_with_notice('installed');
}
public static function serve_virtual_file(): void {
$path = get_query_var(self::QUERY_VAR);
if (!$path) {
return;
}
$path = rawurldecode((string) $path);
$package = self::package_files();
if (!isset(self::PACKAGE_FILES[$path], $package[$path])) {
status_header(404);
exit;
}
self::send_file_response($path, $package[$path]);
}
public static function serve_robots_txt(string $output, bool $public): string {
$package = self::package_files();
return isset($package['robots.txt']) ? $package['robots.txt'] : $output;
}
private static function send_file_response(string $path, string $content): void {
nocache_headers();
header('Content-Type: ' . self::PACKAGE_FILES[$path]);
header('X-AgentReadyWebsite-Installer: active');
echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
exit;
}
private static function extract_valid_package(string $zip_path): array {
if (!class_exists('ZipArchive')) {
throw new RuntimeException('ZipArchive is not available.');
}
$zip = new ZipArchive();
if ($zip->open($zip_path) !== true) {
throw new RuntimeException('Unable to open zip.');
}
$files = [];
for ($index = 0; $index < $zip->numFiles; $index++) {
$name = self::normalize_zip_name((string) $zip->getNameIndex($index));
if (!$name || str_ends_with($name, '/')) {
continue;
}
if (!array_key_exists($name, self::PACKAGE_FILES)) {
$zip->close();
throw new RuntimeException('Unexpected file in package.');
}
$content = $zip->getFromIndex($index);
if ($content === false) {
$zip->close();
throw new RuntimeException('Unable to read package file.');
}
$files[$name] = $content;
}
$zip->close();
$required = array_diff(array_keys(self::PACKAGE_FILES), self::OPTIONAL_FILES);
$missing = array_diff($required, array_keys($files));
if ($missing) {
throw new RuntimeException('Missing required package files.');
}
self::validate_json_file($files, 'schema.jsonld');
self::validate_json_file($files, '.well-known/agent-permissions.json');
self::validate_json_file($files, '.well-known/agent-card.json');
self::validate_json_file($files, '.well-known/agent-skills/index.json');
self::validate_json_file($files, '.well-known/mcp/server-card.json');
self::validate_json_file($files, '.well-known/api-catalog');
self::validate_json_file($files, '.well-known/ucp');
return $files;
}
private static function validate_json_file(array $files, string $path): void {
json_decode($files[$path], true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException('Invalid JSON in package file.');
}
}
private static function normalize_zip_name(string $name): string {
$name = str_replace('\\', '/', $name);
$name = ltrim($name, '/');
if (str_contains($name, '../') || str_starts_with($name, '../')) {
return '';
}
return $name;
}
private static function package_files(): array {
$package = get_option(self::OPTION_PACKAGE, []);
return is_array($package) ? $package : [];
}
private static function package_meta(): array {
$meta = get_option(self::OPTION_META, []);
return is_array($meta) ? $meta : [];
}
private static function redirect_with_notice(string $notice): void {
wp_safe_redirect(add_query_arg(
['page' => 'agentreadywebsite-installer', 'agentready_notice' => $notice],
admin_url('tools.php')
));
exit;
}
}
AgentReadyWebsite_Installer::boot();
register_activation_hook(__FILE__, ['AgentReadyWebsite_Installer', 'activate']);
register_deactivation_hook(__FILE__, ['AgentReadyWebsite_Installer', 'deactivate']);