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
131 changes: 125 additions & 6 deletions inc/Core/Auth/AgentAccessStoreAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
namespace DataMachine\Core\Auth;

use DataMachine\Core\Database\Agents\AgentAccess;
use DataMachine\Core\Database\Agents\Agents;

defined( 'ABSPATH' ) || exit;

Expand All @@ -24,11 +25,20 @@ class AgentAccessStoreAdapter implements \WP_Agent_Access_Store {
*/
private AgentAccess $access_repository;

/**
* Agent identity repository used to map Data Machine IDs to Agents API slugs.
*
* @var Agents
*/
private Agents $agents_repository;

/**
* @param AgentAccess|null $access_repository Optional repository for tests.
* @param Agents|null $agents_repository Optional repository for tests.
*/
public function __construct( ?AgentAccess $access_repository = null ) {
public function __construct( ?AgentAccess $access_repository = null, ?Agents $agents_repository = null ) {
$this->access_repository = $access_repository ?? new AgentAccess();
$this->agents_repository = $agents_repository ?? new Agents();
}

/**
Expand Down Expand Up @@ -62,21 +72,23 @@ public static function filter_access_store( $store ) {
* Create or update an access grant.
*/
public function grant_access( \WP_Agent_Access_Grant $grant ): \WP_Agent_Access_Grant {
return $this->access_repository->grant_access( $grant );
$resolved = $this->access_repository->grant_access( $this->grant_for_storage( $grant ) );
return $this->grant_for_contract( $resolved, $grant->agent_id );
}

/**
* Revoke a user's access grant for an agent.
*/
public function revoke_access( string $agent_id, int $user_id, ?string $workspace_id = null ): bool {
return $this->access_repository->revoke_access( $agent_id, $user_id, $workspace_id );
return $this->access_repository->revoke_access( $this->storage_agent_id( $agent_id ), $user_id, $workspace_id );
}

/**
* Fetch a user's access grant for an agent.
*/
public function get_access( string $agent_id, int $user_id, ?string $workspace_id = null ): ?\WP_Agent_Access_Grant {
return $this->access_repository->get_access( $agent_id, $user_id, $workspace_id );
$grant = $this->access_repository->get_access( $this->storage_agent_id( $agent_id ), $user_id, $workspace_id );
return $grant ? $this->grant_for_contract( $grant, $agent_id ) : null;
}

/**
Expand All @@ -85,7 +97,30 @@ public function get_access( string $agent_id, int $user_id, ?string $workspace_i
* @return string[]
*/
public function get_agent_ids_for_user( int $user_id, ?string $minimum_role = null, ?string $workspace_id = null ): array {
return array_map( 'strval', $this->access_repository->get_agent_ids_for_user( $user_id, $minimum_role, $workspace_id ) );
$agent_ids = $this->access_repository->get_agent_ids_for_user( $user_id, $minimum_role, $workspace_id );
if ( empty( $agent_ids ) ) {
return array();
}

$rows = $this->agents_repository->get_agents_by_ids( array_map( 'intval', $agent_ids ) );
$slugs_by_id = array();
foreach ( $rows as $row ) {
$agent_id = (int) ( $row['agent_id'] ?? 0 );
$slug = sanitize_title( (string) ( $row['agent_slug'] ?? '' ) );
if ( $agent_id > 0 && '' !== $slug ) {
$slugs_by_id[ $agent_id ] = $slug;
}
}

$slugs = array();
foreach ( $agent_ids as $agent_id ) {
$agent_id = (int) $agent_id;
if ( isset( $slugs_by_id[ $agent_id ] ) ) {
$slugs[] = $slugs_by_id[ $agent_id ];
}
}

return $slugs;
}

/**
Expand All @@ -94,6 +129,90 @@ public function get_agent_ids_for_user( int $user_id, ?string $minimum_role = nu
* @return \WP_Agent_Access_Grant[]
*/
public function get_users_for_agent( string $agent_id, ?string $workspace_id = null ): array {
return $this->access_repository->get_users_for_agent( $agent_id, $workspace_id );
return array_map(
fn( \WP_Agent_Access_Grant $grant ): \WP_Agent_Access_Grant => $this->grant_for_contract( $grant, $agent_id ),
$this->access_repository->get_users_for_agent( $this->storage_agent_id( $agent_id ), $workspace_id )
);
}

/**
* Convert an Agents API slug to the Data Machine numeric storage ID.
*/
private function storage_agent_id( string $agent_id ): string {
if ( is_numeric( $agent_id ) ) {
return $agent_id;
}

$row = $this->agents_repository->get_by_slug( $agent_id );
if ( $row && ! empty( $row['agent_id'] ) ) {
return (string) (int) $row['agent_id'];
}

return $agent_id;
}

/**
* Convert a contract grant into Data Machine's numeric storage shape.
*/
private function grant_for_storage( \WP_Agent_Access_Grant $grant ): \WP_Agent_Access_Grant {
$storage_agent_id = $this->storage_agent_id( $grant->agent_id );
if ( $storage_agent_id === $grant->agent_id ) {
return $grant;
}

return new \WP_Agent_Access_Grant(
$storage_agent_id,
$grant->user_id,
$grant->role,
$grant->workspace_id,
$grant->grant_id,
$grant->granted_by_user_id,
$grant->granted_at,
$grant->metadata
);
}

/**
* Convert a Data Machine numeric grant into the Agents API slug shape.
*/
private function grant_for_contract( \WP_Agent_Access_Grant $grant, string $requested_agent_id = '' ): \WP_Agent_Access_Grant {
$agent_slug = $this->contract_agent_id( $grant->agent_id );
if ( ! is_numeric( $requested_agent_id ) ) {
$requested_slug = sanitize_title( $requested_agent_id );
if ( '' !== $requested_slug ) {
$agent_slug = $requested_slug;
}
}

if ( $agent_slug === $grant->agent_id ) {
return $grant;
}

return new \WP_Agent_Access_Grant(
$agent_slug,
$grant->user_id,
$grant->role,
$grant->workspace_id,
$grant->grant_id,
$grant->granted_by_user_id,
$grant->granted_at,
$grant->metadata
);
}

/**
* Convert a Data Machine numeric storage ID to an Agents API slug.
*/
private function contract_agent_id( string $agent_id ): string {
if ( ! is_numeric( $agent_id ) ) {
return sanitize_title( $agent_id );
}

$row = $this->agents_repository->get_agent( (int) $agent_id );
if ( $row && ! empty( $row['agent_slug'] ) ) {
return sanitize_title( (string) $row['agent_slug'] );
}

return $agent_id;
}
}
152 changes: 152 additions & 0 deletions inc/Engine/Agents/PersistedAgentProjector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<?php
/**
* Project persisted Data Machine agents into the Agents API registry.
*
* @package DataMachine\Engine\Agents
* @since 0.110.3
*/

namespace DataMachine\Engine\Agents;

use DataMachine\Core\Database\Agents\Agents;

defined( 'ABSPATH' ) || exit;

/**
* Runtime projection for durable `datamachine_agents` rows.
*/
class PersistedAgentProjector {

/**
* Register persisted agents as WP_Agent definitions.
*
* Data Machine's database remains the source of truth. This method only
* mirrors current rows into the request-local Agents API registry so APIs
* that list or resolve registered agents can see bundle-installed agents.
*
* @param Agents|null $agents_repository Optional repository for tests.
* @return string[] Registered slugs.
*/
public static function register_persisted_agents( ?Agents $agents_repository = null ): array {
if ( ! function_exists( 'wp_register_agent' ) || ! function_exists( 'wp_has_agent' ) ) {
return array();
}

if ( ! class_exists( Agents::class ) ) {
return array();
}

$agents_repository = $agents_repository ?? new Agents();
$registered = array();

foreach ( $agents_repository->get_all() as $row ) {
$slug = sanitize_title( (string) ( $row['agent_slug'] ?? '' ) );
if ( '' === $slug || wp_has_agent( $slug ) ) {
continue;
}

$agent = wp_register_agent( $slug, self::definition_from_row( $row ) );
if ( $agent instanceof \WP_Agent ) {
$registered[] = $agent->get_slug();
}
}

return $registered;
}

/**
* Build WP_Agent registration args from a persisted agent row.
*
* @param array<string,mixed> $row Data Machine agent row.
* @return array<string,mixed>
*/
public static function definition_from_row( array $row ): array {
$config = is_array( $row['agent_config'] ?? null ) ? $row['agent_config'] : array();
$owner_id = (int) ( $row['owner_id'] ?? 0 );

return array(
'label' => self::label_from_row( $row ),
'description' => self::description_from_row( $row, $config ),
'owner_resolver' => static fn(): int => $owner_id,
'default_config' => $config,
'meta' => self::meta_from_row( $row, $config ),
);
}

/**
* Resolve display label from durable row fields.
*
* @param array<string,mixed> $row Data Machine agent row.
*/
private static function label_from_row( array $row ): string {
$label = trim( (string) ( $row['agent_name'] ?? '' ) );
if ( '' !== $label ) {
return $label;
}

return sanitize_title( (string) ( $row['agent_slug'] ?? 'agent' ) );
}

/**
* Resolve description from known durable config shapes.
*
* @param array<string,mixed> $row Data Machine agent row.
* @param array<string,mixed> $config Agent config.
*/
private static function description_from_row( array $row, array $config ): string {
foreach ( array( $config, $config['intelligence_wiki_brain'] ?? null ) as $source ) {
if ( ! is_array( $source ) ) {
continue;
}

foreach ( array( 'description', 'agent_description', 'summary' ) as $key ) {
$description = trim( (string) ( $source[ $key ] ?? '' ) );
if ( '' !== $description ) {
return $description;
}
}
}

$label = self::label_from_row( $row );
return '' !== $label ? sprintf( 'Data Machine agent: %s.', $label ) : __( 'Data Machine persisted agent.', 'data-machine' );
}

/**
* Build diagnostics/provenance metadata for the runtime definition.
*
* @param array<string,mixed> $row Data Machine agent row.
* @param array<string,mixed> $config Agent config.
* @return array<string,mixed>
*/
private static function meta_from_row( array $row, array $config ): array {
$bundle = is_array( $config['datamachine_bundle'] ?? null ) ? $config['datamachine_bundle'] : array();
$brain = is_array( $config['intelligence_wiki_brain'] ?? null ) ? $config['intelligence_wiki_brain'] : array();

$meta = array(
'source_plugin' => 'data-machine',
'source_type' => 'persisted-agent',
'datamachine_agent_id' => (int) ( $row['agent_id'] ?? 0 ),
'datamachine_owner_id' => (int) ( $row['owner_id'] ?? 0 ),
);

foreach ( array( 'bundle_slug', 'bundle_version', 'source_ref', 'source_revision' ) as $key ) {
$value = trim( (string) ( $bundle[ $key ] ?? '' ) );
if ( '' !== $value ) {
$meta[ 'datamachine_' . $key ] = $value;
}
}

if ( ! empty( $bundle['bundle_slug'] ) ) {
$meta['source_package'] = (string) $bundle['bundle_slug'];
}

foreach ( array( 'domain', 'wiki_slug', 'source_slug', 'brain_slug', 'bundle_slug' ) as $key ) {
$value = trim( (string) ( $brain[ $key ] ?? '' ) );
if ( '' !== $value ) {
$meta[ 'intelligence_wiki_brain_' . $key ] = $value;
}
}

return $meta;
}
}
11 changes: 11 additions & 0 deletions inc/Engine/Agents/datamachine-register-agents.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

use DataMachine\Core\FilesRepository\DirectoryManager;
use DataMachine\Engine\Agents\AgentRegistry;
use DataMachine\Engine\Agents\PersistedAgentProjector;

defined( 'ABSPATH' ) || exit;

Expand Down Expand Up @@ -143,3 +144,13 @@ function datamachine_register_default_admin_agent(): void {
);
}
add_action( 'wp_agents_api_init', 'datamachine_register_default_admin_agent', 10 );

/**
* Project durable Data Machine agents into the Agents API runtime registry.
*
* @since 0.110.3
*/
function datamachine_register_persisted_agents(): void {
PersistedAgentProjector::register_persisted_agents();
}
add_action( 'wp_agents_api_init', 'datamachine_register_persisted_agents', 20 );
Loading
Loading