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
49 changes: 48 additions & 1 deletion src/Auth/class-wp-agent-access.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) {
Expand Down Expand Up @@ -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<string,mixed> $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<string,mixed> $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.
*/
Expand Down
8 changes: 6 additions & 2 deletions src/Auth/class-wp-agent-wordpress-authorization-policy.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
47 changes: 41 additions & 6 deletions tests/agents-access-ability-smoke.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
)
);
}
);

Expand All @@ -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 {
Expand Down Expand Up @@ -114,19 +126,32 @@ 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 {
if ( null === $principal->audience_id ) {
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;
}
};

Expand All @@ -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(
Expand All @@ -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 );

Expand Down
Loading