{
	"$schema": "https://playground.wordpress.net/blueprint-schema.json",
	"meta": {
		"title": "Agent Firewall demo",
		"description": "A shop site where an AI agent's edit was allowed, two deletions wait for approval, a read-only agent was refused, and an approval was stopped because a person had changed the page first.",
		"author": "agenticdaisy",
		"categories": [
			"Security"
		]
	},
	"landingPage": "/wp-admin/admin.php?page=agentic-daisy-ai-agent-firewall&tab=pending",
	"preferredVersions": {
		"php": "8.3",
		"wp": "latest"
	},
	"features": {
		"networking": false
	},
	"steps": [
		{
			"step": "installPlugin",
			"pluginData": {
				"resource": "wordpress.org/plugins",
				"slug": "agentic-daisy-ai-agent-firewall"
			},
			"options": {
				"activate": true
			},
			"ifAlreadyInstalled": "skip"
		},
		{
			"step": "login",
			"username": "admin",
			"password": "password"
		},
		{
			"step": "runPHP",
			"code": "<?php\n// Agent Firewall demo, step 1 of 4: a small site and two agents, created\n// through the plugin's own REST API as the site administrator.\nrequire '/wordpress/wp-load.php';\nwp_set_current_user( 1 );\nupdate_option( 'blogname', 'Corner Shop (Agent Firewall demo)' );\n\n$editor = wp_insert_user( array( 'user_login' => 'claude', 'user_pass' => wp_generate_password(), 'role' => 'editor', 'display_name' => 'Claude (Editor)' ) );\n$posts  = array(\n\t'sale'     => wp_insert_post( array( 'post_title' => 'Spring sale: 20% off', 'post_content' => 'Everything in store is 20% off this week.', 'post_status' => 'publish' ) ),\n\t'about'    => wp_insert_post( array( 'post_title' => 'About us', 'post_content' => 'A small shop on the corner since 1998.', 'post_status' => 'publish', 'post_type' => 'page' ) ),\n\t'shipping' => wp_insert_post( array( 'post_title' => 'Shipping policy', 'post_content' => 'Orders ship within 2 days.', 'post_status' => 'publish' ) ),\n);\n\n$api = static function ( string $method, string $route, array $body = array() ) {\n\t$request = new WP_REST_Request( $method, $route );\n\t$request->set_body_params( $body );\n\treturn rest_do_request( $request )->get_data();\n};\n\n$claude   = $api( 'POST', '/agenticdaisy-firewall/v1/agents', array( 'name' => 'Claude Desktop', 'agent_key' => 'claude-desktop', 'wp_user_id' => $editor ) );\n$reporter = $api( 'POST', '/agenticdaisy-firewall/v1/agents', array( 'name' => 'Weekly report bot', 'agent_key' => 'weekly-report-bot', 'wp_user_id' => $editor, 'scopes' => array( 'read' ) ) );\n\n// Kept for the next steps, and deleted by the last one.\nupdate_option(\n\t'adaf_demo_seed',\n\tarray(\n\t\t'posts'  => $posts,\n\t\t'tokens' => array(\n\t\t\t'claude'   => $api( 'POST', \"/agenticdaisy-firewall/v1/agents/{$claude['id']}/token\" )['token'],\n\t\t\t'reporter' => $api( 'POST', \"/agenticdaisy-firewall/v1/agents/{$reporter['id']}/token\" )['token'],\n\t\t),\n\t),\n\tfalse\n);\n"
		},
		{
			"step": "runPHP",
			"code": "<?php\n// Step 2: Claude, with its own token, through the real firewall. The edit\n// is allowed and can be undone from the ledger; both deletions are held.\nrequire '/wordpress/wp-load.php';\n$seed = get_option( 'adaf_demo_seed' );\n\n// Present the agent's token and have WordPress work out who is calling\n// again, exactly as it does for a request that arrives with one.\n$_SERVER['HTTP_AUTHORIZATION'] = 'Bearer ' . $seed['tokens']['claude'];\n$GLOBALS['current_user']       = null;\nwp_get_current_user();\n\n$send = static function ( string $method, string $route, array $body = array() ): void {\n\t$request = new WP_REST_Request( $method, $route );\n\t$request->set_body_params( $body );\n\trest_do_request( $request );\n};\n$posts = $seed['posts'];\n$send( 'POST', \"/wp/v2/posts/{$posts['sale']}\", array( 'title' => 'Spring sale: 30% off everything' ) );\n$send( 'DELETE', \"/wp/v2/pages/{$posts['about']}\" );\n$send( 'DELETE', \"/wp/v2/posts/{$posts['shipping']}\" );\n"
		},
		{
			"step": "runPHP",
			"code": "<?php\n// Step 3: a read-only agent tries to publish. Its token's scopes refuse it\n// before any policy runs.\nrequire '/wordpress/wp-load.php';\n$seed = get_option( 'adaf_demo_seed' );\n\n// Present the agent's token and have WordPress work out who is calling\n// again, exactly as it does for a request that arrives with one.\n$_SERVER['HTTP_AUTHORIZATION'] = 'Bearer ' . $seed['tokens']['reporter'];\n$GLOBALS['current_user']       = null;\nwp_get_current_user();\n\n$request = new WP_REST_Request( 'POST', '/wp/v2/posts' );\n$request->set_body_params( array( 'title' => 'Weekly report', 'status' => 'publish' ) );\nrest_do_request( $request );\n"
		},
		{
			"step": "runPHP",
			"code": "<?php\n// Step 4: a person updates the shipping policy while Claude's deletion of it\n// waits, then someone approves the deletion. The firewall sees the page\n// changed since it was held and refuses to act on stale intent.\nrequire '/wordpress/wp-load.php';\n$seed = get_option( 'adaf_demo_seed' );\nwp_set_current_user( 1 );\n\nglobal $wpdb;\n$shipping = (int) $seed['posts']['shipping'];\n$wpdb->update(\n\t$wpdb->posts,\n\tarray(\n\t\t'post_content'      => 'Orders now ship within 1 day.',\n\t\t'post_modified'     => gmdate( 'Y-m-d H:i:s', time() + 60 ),\n\t\t'post_modified_gmt' => gmdate( 'Y-m-d H:i:s', time() + 60 ),\n\t),\n\tarray( 'ID' => $shipping )\n);\nclean_post_cache( $shipping );\n\nforeach ( rest_do_request( new WP_REST_Request( 'GET', '/agenticdaisy-firewall/v1/pending' ) )->get_data() as $held ) {\n\tif ( 'rest:delete:post' === $held['ability_name'] ) {\n\t\trest_do_request( new WP_REST_Request( 'POST', \"/agenticdaisy-firewall/v1/pending/{$held['id']}/approve\" ) );\n\t}\n}\n\ndelete_option( 'adaf_demo_seed' );\n"
		}
	]
}