diff --git a/src/Auth/class-wp-agent-access.php b/src/Auth/class-wp-agent-access.php index f9f2b88..63c0467 100644 --- a/src/Auth/class-wp-agent-access.php +++ b/src/Auth/class-wp-agent-access.php @@ -138,7 +138,9 @@ public static function list_accessible_agents_for_principal( AgentsAPI\AI\WP_Age } if ( $store instanceof WP_Agent_Principal_Access_Store ) { - $agent_ids = array_merge( $agent_ids, $store->get_agent_ids_for_principal( $principal, $minimum_role, $principal->workspace_id ) ); + foreach ( self::access_principals_for( $principal, $context ) as $access_principal ) { + $agent_ids = array_merge( $agent_ids, $store->get_agent_ids_for_principal( $access_principal, $minimum_role, $principal->workspace_id ) ); + } } if ( null === $principal->audience_id && self::CURRENT_USER_EFFECTIVE_AGENT_ID !== $principal->effective_agent_id ) { @@ -171,6 +173,51 @@ private static function agent_to_access_summary( WP_Agent $agent ): array { ); } + /** + * Expand a request principal into the principal grants that apply to it. + * + * @param AgentsAPI\AI\WP_Agent_Execution_Principal $principal Request principal. + * @param array $context Host-owned request context. + * @return AgentsAPI\AI\WP_Agent_Execution_Principal[] + */ + public static function access_principals_for( AgentsAPI\AI\WP_Agent_Execution_Principal $principal, array $context = array() ): array { + $principals = array( $principal ); + $audiences = array(); + + if ( null !== $principal->audience_id ) { + $audiences[] = $principal->audience_id; + } + + if ( ! array_key_exists( 'include_public_audience', $context ) || false !== (bool) $context['include_public_audience'] ) { + $audiences[] = self::PUBLIC_AUDIENCE_ID; + } + + /** + * Filter audience grants that apply to a request principal. + * + * @param string[] $audiences Audience IDs such as audience:public. + * @param AgentsAPI\AI\WP_Agent_Execution_Principal $principal Request principal. + * @param array $context Host-owned request context. + */ + /** @var mixed $filtered_audiences */ + $filtered_audiences = function_exists( 'apply_filters' ) ? apply_filters( 'agents_api_access_audiences_for_principal', $audiences, $principal, $context ) : $audiences; + $audiences = is_array( $filtered_audiences ) ? $filtered_audiences : array(); + + foreach ( array_values( array_unique( array_filter( array_map( 'strval', $audiences ) ) ) ) as $audience_id ) { + $principals[] = AgentsAPI\AI\WP_Agent_Execution_Principal::audience( + $audience_id, + $principal->effective_agent_id, + $principal->request_context, + $principal->request_metadata, + $principal->workspace_id, + $principal->client_id, + $principal->audience_claims + ); + } + + return $principals; + } + /** * Return the current WordPress user ID when WordPress is loaded. */ diff --git a/src/Auth/class-wp-agent-wordpress-authorization-policy.php b/src/Auth/class-wp-agent-wordpress-authorization-policy.php index 40af51a..d0376a8 100644 --- a/src/Auth/class-wp-agent-wordpress-authorization-policy.php +++ b/src/Auth/class-wp-agent-wordpress-authorization-policy.php @@ -82,8 +82,12 @@ public function can_access_agent( AgentsAPI\AI\WP_Agent_Execution_Principal $pri } if ( $access_store instanceof WP_Agent_Principal_Access_Store ) { - $grant = $access_store->get_access_for_principal( $agent_id, $principal, $principal->workspace_id ); - return $grant instanceof WP_Agent_Access_Grant && $grant->role_meets( $minimum_role ); + foreach ( WP_Agent_Access::access_principals_for( $principal, $context ) as $access_principal ) { + $grant = $access_store->get_access_for_principal( $agent_id, $access_principal, $principal->workspace_id ); + if ( $grant instanceof WP_Agent_Access_Grant && $grant->role_meets( $minimum_role ) ) { + return true; + } + } } return false; diff --git a/tests/agents-access-ability-smoke.php b/tests/agents-access-ability-smoke.php index 33127a1..bd13822 100644 --- a/tests/agents-access-ability-smoke.php +++ b/tests/agents-access-ability-smoke.php @@ -67,6 +67,14 @@ static function (): void { 'description' => 'Administers the site.', ) ); + + wp_register_agent( + 'public-agent', + array( + 'label' => 'Public Agent', + 'description' => 'Available to public audience principals.', + ) + ); } ); @@ -75,13 +83,17 @@ static function (): void { $grant = new WP_Agent_Access_Grant( 'editor-agent', 7, WP_Agent_Access_Grant::ROLE_OPERATOR, 'site:42' ); $access_store = new class( $grant ) implements WP_Agent_Access_Store, WP_Agent_Principal_Access_Store { - private WP_Agent_Access_Grant $audience_grant; + /** @var WP_Agent_Access_Grant[] */ + private array $audience_grants; /** * @param WP_Agent_Access_Grant $grant Test grant. */ public function __construct( private WP_Agent_Access_Grant $grant ) { - $this->audience_grant = new WP_Agent_Access_Grant( 'admin-agent', 0, WP_Agent_Access_Grant::ROLE_OPERATOR, 'site:42', null, null, null, array(), 'audience:docs-readers' ); + $this->audience_grants = array( + new WP_Agent_Access_Grant( 'admin-agent', 0, WP_Agent_Access_Grant::ROLE_OPERATOR, 'site:42', null, null, null, array(), 'audience:docs-readers' ), + new WP_Agent_Access_Grant( 'public-agent', 0, WP_Agent_Access_Grant::ROLE_OPERATOR, 'site:42', null, null, null, array(), 'audience:public' ), + ); } public function grant_access( WP_Agent_Access_Grant $grant ): WP_Agent_Access_Grant { @@ -114,7 +126,13 @@ public function get_access_for_principal( string $agent_id, AgentsAPI\AI\WP_Agen return null; } - return $this->audience_grant->agent_id === $agent_id && $this->audience_grant->audience_id === $principal->audience_id && $this->audience_grant->workspace_id === $workspace_id ? $this->audience_grant : null; + foreach ( $this->audience_grants as $grant ) { + if ( $grant->agent_id === $agent_id && $grant->audience_id === $principal->audience_id && $grant->workspace_id === $workspace_id ) { + return $grant; + } + } + + return null; } public function get_agent_ids_for_principal( AgentsAPI\AI\WP_Agent_Execution_Principal $principal, ?string $minimum_role = null, ?string $workspace_id = null ): array { @@ -122,11 +140,18 @@ public function get_agent_ids_for_principal( AgentsAPI\AI\WP_Agent_Execution_Pri return array(); } - if ( $this->audience_grant->audience_id !== $principal->audience_id || $this->audience_grant->workspace_id !== $workspace_id ) { - return array(); + $agent_ids = array(); + foreach ( $this->audience_grants as $grant ) { + if ( $grant->audience_id !== $principal->audience_id || $grant->workspace_id !== $workspace_id ) { + continue; + } + + if ( null === $minimum_role || $grant->role_meets( $minimum_role ) ) { + $agent_ids[] = $grant->agent_id; + } } - return null === $minimum_role || $this->audience_grant->role_meets( $minimum_role ) ? array( $this->audience_grant->agent_id ) : array(); + return $agent_ids; } }; @@ -151,6 +176,7 @@ static function ( $store ) use ( $access_store ) { $agents = WP_Agent_Access::list_accessible_agents_for_current_principal( WP_Agent_Access_Grant::ROLE_VIEWER, array( 'workspace_id' => 'site:42' ) ); agents_api_smoke_assert_equals( 'editor-agent', $agents[0]['slug'] ?? null, 'accessible agents list contains granted registered agent', $failures, $passes ); agents_api_smoke_assert_equals( 'Editor Agent', $agents[0]['label'] ?? null, 'accessible agents include agent label', $failures, $passes ); +agents_api_smoke_assert_equals( true, in_array( 'public-agent', array_column( $agents, 'slug' ), true ), 'logged-in principal inherits public audience grants in accessible list', $failures, $passes ); $can_access = AgentsAPI\AI\Auth\agents_can_access_agent( array( @@ -170,6 +196,15 @@ static function ( $store ) use ( $access_store ) { ); agents_api_smoke_assert_equals( false, $cannot_access['allowed'] ?? true, 'can-access ability returns allowed false below minimum role', $failures, $passes ); +$public_access = AgentsAPI\AI\Auth\agents_can_access_agent( + array( + 'agent' => 'public-agent', + 'minimum_role' => WP_Agent_Access_Grant::ROLE_OPERATOR, + 'workspace_id' => 'site:42', + ) +); +agents_api_smoke_assert_equals( true, $public_access['allowed'] ?? false, 'logged-in principal can access public audience agent', $failures, $passes ); + $ability_list = AgentsAPI\AI\Auth\agents_list_accessible_agents( array( 'workspace_id' => 'site:42' ) ); agents_api_smoke_assert_equals( 'editor-agent', $ability_list['agents'][0]['slug'] ?? null, 'list-accessible ability returns granted registered agent', $failures, $passes );