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
18 changes: 12 additions & 6 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions docs/core-system/wp-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,9 @@ wp datamachine agents delete my-agent --delete-files --yes

# Manage access grants
wp datamachine agents access grant my-agent 2 --role=operator
wp datamachine agents access grant-audience my-agent audience:public --role=operator
wp datamachine agents access revoke my-agent 2
wp datamachine agents access revoke-audience my-agent audience:public
wp datamachine agents access list my-agent

# Manage runtime bearer tokens. Raw token values are shown only at creation time.
Expand Down
110 changes: 98 additions & 12 deletions inc/Abilities/AgentAbilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,49 @@ private function registerAbilities(): void {
'meta' => array( 'show_in_rest' => true ),
)
);

wp_register_ability(
'datamachine/grant-agent-audience-access',
array(
'label' => 'Grant Agent Audience Access',
'description' => 'Grant a selected agent to an explicit non-user audience principal such as audience:public.',
'category' => 'datamachine-agent',
'input_schema' => array(
'type' => 'object',
'required' => array( 'agent', 'principal_id' ),
'properties' => array(
'agent' => array(
'type' => 'string',
'description' => 'Agent slug or ID to grant.',
),
'principal_type' => array(
'type' => 'string',
'description' => 'Principal type. Defaults to audience.',
),
'principal_id' => array(
'type' => 'string',
'description' => 'Principal identifier, such as public or automattician.',
),
'role' => array(
'type' => 'string',
'enum' => array( 'admin', 'operator', 'viewer' ),
'description' => 'Access role. Defaults to operator.',
),
),
),
'output_schema' => array(
'type' => 'object',
'properties' => array(
'success' => array( 'type' => 'boolean' ),
'grant' => array( 'type' => 'object' ),
'error' => array( 'type' => 'string' ),
),
),
'execute_callback' => array( self::class, 'grantAgentAudienceAccess' ),
'permission_callback' => fn() => PermissionHelper::can_manage(),
'meta' => array( 'show_in_rest' => true ),
)
);
};

if ( doing_action( 'wp_abilities_api_init' ) ) {
Expand Down Expand Up @@ -1460,11 +1503,12 @@ public static function getAgent( array $input ): array {
}

// Enrich with access grants.
$access_repo = new \DataMachine\Core\Database\Agents\AgentAccess();
$access = array_map(
$access_repo = new \DataMachine\Core\Database\Agents\AgentAccess();
$access = array_map(
static fn( \WP_Agent_Access_Grant $grant ): array => $grant->to_array(),
$access_repo->get_users_for_agent( (string) (int) $agent['agent_id'] )
);
$principal_access = $access_repo->get_principals_for_agent( (string) (int) $agent['agent_id'] );

// Check for agent directory.
$directory_manager = new DirectoryManager();
Expand All @@ -1473,22 +1517,64 @@ public static function getAgent( array $input ): array {
return array(
'success' => true,
'agent' => array(
'agent_id' => (int) $agent['agent_id'],
'agent_slug' => (string) $agent['agent_slug'],
'agent_name' => (string) $agent['agent_name'],
'owner_id' => (int) $agent['owner_id'],
'agent_config' => is_array( $agent['agent_config'] ?? null )
'agent_id' => (int) $agent['agent_id'],
'agent_slug' => (string) $agent['agent_slug'],
'agent_name' => (string) $agent['agent_name'],
'owner_id' => (int) $agent['owner_id'],
'agent_config' => is_array( $agent['agent_config'] ?? null )
? $agent['agent_config']
: ( json_decode( $agent['agent_config'] ?? '{}', true ) ? json_decode( $agent['agent_config'] ?? '{}', true ) : array() ),
'created_at' => $agent['created_at'] ?? '',
'updated_at' => $agent['updated_at'] ?? '',
'agent_dir' => $agent_dir,
'has_files' => is_dir( $agent_dir ),
'access' => $access,
'created_at' => $agent['created_at'] ?? '',
'updated_at' => $agent['updated_at'] ?? '',
'agent_dir' => $agent_dir,
'has_files' => is_dir( $agent_dir ),
'access' => $access,
'principal_access' => $principal_access,
),
);
}

/**
* Grant an agent to an explicit audience/non-user principal.
*
* @param array $input Ability input.
* @return array Result.
*/
public static function grantAgentAudienceAccess( array $input ): array {
$agent_id = self::resolve_agent_input_id( $input );
if ( is_wp_error( $agent_id ) ) {
return array(
'success' => false,
'error' => $agent_id->get_error_message(),
);
}

$principal_type = sanitize_key( (string) ( $input['principal_type'] ?? 'audience' ) );
$principal_id = sanitize_title( (string) ( $input['principal_id'] ?? '' ) );
$role = (string) ( $input['role'] ?? \WP_Agent_Access_Grant::ROLE_OPERATOR );

if ( '' === $principal_type || '' === $principal_id || 'user' === $principal_type ) {
return array(
'success' => false,
'error' => 'A non-user principal_type and principal_id are required.',
);
}

try {
$grant = ( new AgentAccess() )->grant_principal_access( (string) $agent_id, $principal_type, $principal_id, $role );
} catch ( \Throwable $e ) {
return array(
'success' => false,
'error' => $e->getMessage(),
);
}

return array(
'success' => true,
'grant' => $grant,
);
}

/**
* Update an agent's mutable fields.
*
Expand Down
57 changes: 47 additions & 10 deletions inc/Abilities/Chat/AgentsChatHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,16 @@ public function registerHandler( $handler, array $input ) {
* @return bool
*/
public function checkPermission( bool $allowed, array $input ): bool {
unset( $input );
if ( $allowed || PermissionHelper::can( 'chat' ) ) {
return true;
}

return $allowed || PermissionHelper::can( 'chat' );
$agent = sanitize_title( (string) ( $input['agent'] ?? '' ) );
if ( '' === $agent || ! class_exists( '\WP_Agent_Access' ) || ! class_exists( '\WP_Agent_Access_Grant' ) ) {
return false;
}

return \WP_Agent_Access::can_current_principal_access_agent( $agent, \WP_Agent_Access_Grant::ROLE_VIEWER );
}

/**
Expand All @@ -71,20 +78,16 @@ public function execute( array $input ): array|WP_Error {
return new WP_Error( 'empty_message', __( 'Message cannot be empty.', 'data-machine' ), array( 'status' => 400 ) );
}

$user_id = PermissionHelper::acting_user_id();
if ( $user_id <= 0 ) {
$user_id = get_current_user_id();
$agent_id = $this->resolveAgentId( (string) ( $input['agent'] ?? '' ) );
if ( $agent_id instanceof WP_Error ) {
return $agent_id;
}

$user_id = $this->resolveRuntimeUserId( $agent_id, (string) ( $input['agent'] ?? '' ) );
if ( $user_id <= 0 ) {
return new WP_Error( 'no_user', __( 'No user context available.', 'data-machine' ), array( 'status' => 400 ) );
}

$agent_id = $this->resolveAgentId( (string) ( $input['agent'] ?? '' ) );
if ( $agent_id instanceof WP_Error ) {
return $agent_id;
}

$client_context = is_array( $input['client_context'] ?? null ) ? $input['client_context'] : array();
$mode = $this->resolveMode( $input, $client_context );
$agent_config = PluginSettings::resolveModelForAgentMode( 0 === $agent_id ? null : $agent_id, $mode );
Expand Down Expand Up @@ -150,6 +153,40 @@ private function resolveAgentId( string $agent ) {
return (int) ( $row['agent_id'] ?? 0 );
}

/**
* Resolve the WordPress user context used by Data Machine's chat runtime.
*
* Anonymous audience chats execute under the selected agent owner after the
* Agents API access check succeeds, keeping model credentials and tool policy
* scoped to the site-owned brain agent instead of an anonymous WP user.
*
* @param int $agent_id Internal Data Machine agent ID.
* @param string $agent Requested Agents API agent slug/id.
* @return int Runtime WordPress user ID, or 0 when no safe context exists.
*/
private function resolveRuntimeUserId( int $agent_id, string $agent ): int {
$user_id = PermissionHelper::acting_user_id();
if ( $user_id <= 0 ) {
$user_id = get_current_user_id();
}

if ( $user_id > 0 ) {
return $user_id;
}

$agent_slug = sanitize_title( $agent );
if ( '' === $agent_slug || ! class_exists( '\WP_Agent_Access' ) || ! class_exists( '\WP_Agent_Access_Grant' ) ) {
return 0;
}

if ( ! \WP_Agent_Access::can_current_principal_access_agent( $agent_slug, \WP_Agent_Access_Grant::ROLE_VIEWER ) ) {
return 0;
}

$row = ( new Agents() )->get_agent( $agent_id );
return $row ? (int) ( $row['owner_id'] ?? 0 ) : 0;
}

/**
* Resolve the Data Machine execution mode from canonical Agents API input.
*
Expand Down
Loading
Loading