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
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@
"php tests/routine-smoke.php",
"php tests/event-trigger-smoke.php",
"php tests/subagents-smoke.php",
"php tests/access-decision-filter-smoke.php",
"php tests/no-product-imports-smoke.php"
],
"test": [
Expand Down
40 changes: 22 additions & 18 deletions src/Auth/class-wp-agent-access.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,13 @@ public static function list_accessible_agents_for_current_principal( string $min
/**
* List registered agents accessible to a principal.
*
* Iterates every registered agent through the same
* {@see WP_Agent_WordPress_Authorization_Policy::can_access_agent()}
* decision used by the check path. This guarantees that
* `agents/list-accessible-agents` and `agents/can-access-agent` can
* never disagree: the list is literally the set of agents for which
* the check returns true.
*
* @param AgentsAPI\AI\WP_Agent_Execution_Principal $principal Execution principal.
* @param string $minimum_role Minimum access role.
* @param array<string,mixed> $context Host-owned request context.
Expand All @@ -131,29 +138,26 @@ public static function list_accessible_agents_for_principal( AgentsAPI\AI\WP_Age
return array();
}

$agent_ids = array();
$store = self::get_store( $context );
if ( $store instanceof WP_Agent_Access_Store && $principal->acting_user_id > 0 ) {
$agent_ids = array_merge( $agent_ids, $store->get_agent_ids_for_user( $principal->acting_user_id, $minimum_role, $principal->workspace_id ) );
}
$store = self::get_store( $context );
$policy = new WP_Agent_WordPress_Authorization_Policy( $store );

if ( $store instanceof WP_Agent_Principal_Access_Store ) {
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 ) );
$agents = array();
$seen = array();

$registered = function_exists( 'wp_get_agents' ) ? wp_get_agents() : array();
foreach ( $registered as $agent ) {
$slug = sanitize_title( $agent->get_slug() );
if ( '' === $slug || isset( $seen[ $slug ] ) ) {
continue;
}
}

if ( null === $principal->audience_id && self::CURRENT_USER_EFFECTIVE_AGENT_ID !== $principal->effective_agent_id ) {
$agent_ids[] = $principal->effective_agent_id;
}
$seen[ $slug ] = true;

$agent_ids = array_values( array_unique( array_filter( array_map( 'sanitize_title', $agent_ids ) ) ) );
$agents = array();
foreach ( $agent_ids as $agent_id ) {
$agent = function_exists( 'wp_get_agent' ) ? wp_get_agent( $agent_id ) : null;
if ( $agent instanceof WP_Agent ) {
$agents[] = self::agent_to_access_summary( $agent );
if ( ! $policy->can_access_agent( $principal, $slug, $minimum_role, $context ) ) {
continue;
}

$agents[] = self::agent_to_access_summary( $agent );
}

return $agents;
Expand Down
41 changes: 41 additions & 0 deletions src/Auth/class-wp-agent-wordpress-authorization-policy.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ public function can( AgentsAPI\AI\WP_Agent_Execution_Principal $principal, strin
/**
* Check whether a principal can access an agent at a minimum role.
*
* The store-derived decision — including the effective-agent
* short-circuit — is wrapped in the {@see 'wp_agent_can_access_agent'}
* filter so hosts can tighten as well as widen access without
* materializing grant rows.
*
* @param AgentsAPI\AI\WP_Agent_Execution_Principal $principal Execution principal.
* @param string $agent_id Agent identifier.
* @param string $minimum_role Minimum access role.
Expand All @@ -65,6 +70,42 @@ public function can_access_agent( AgentsAPI\AI\WP_Agent_Execution_Principal $pri
return false;
}

$allowed = $this->resolve_default_access_decision( $principal, $agent_id, $minimum_role, $context );

if ( function_exists( 'apply_filters' ) ) {
/**
* Filters the final agent access decision.
*
* Hosts that derive access from live state (capabilities, roles,
* memberships) can grant or deny access without materializing
* grant rows. The filter wraps the store-derived decision —
* including the effective-agent short-circuit — so hosts can
* tighten as well as widen access.
*
* @param bool $allowed Default store-derived decision.
* @param AgentsAPI\AI\WP_Agent_Execution_Principal $principal Execution principal.
* @param string $agent_id Agent identifier.
* @param string $minimum_role Minimum access role.
* @param array<string,mixed> $context Host-owned authorization context.
*/
$allowed = (bool) apply_filters( 'wp_agent_can_access_agent', $allowed, $principal, $agent_id, $minimum_role, $context );
}

return $allowed;
}

/**
* Resolve the default (store-derived) access decision before host filtering.
*
* Includes the effective-agent short-circuit so the host filter can
* tighten as well as widen the decision.
*
* @param AgentsAPI\AI\WP_Agent_Execution_Principal $principal Execution principal.
* @param string $agent_id Agent identifier.
* @param string $minimum_role Minimum access role.
* @param array<string,mixed> $context Host-owned authorization context.
*/
private function resolve_default_access_decision( AgentsAPI\AI\WP_Agent_Execution_Principal $principal, string $agent_id, string $minimum_role, array $context ): bool {
if ( $principal->effective_agent_id === $agent_id ) {
return true;
}
Expand Down
Loading