true, ] ); } } /** * List actors. Returns an array of WP_User objects. * By default excludes archived actors; pass include_archived=true to see all. */ public static function list_actors( bool $include_archived = false ): array { $args = [ 'role' => self::ROLE, 'orderby' => 'display_name', 'order' => 'ASC', 'number' => 200, ]; if ( ! $include_archived ) { $args['meta_query'] = [ 'relation' => 'OR', [ 'key' => self::META_IS_ACTIVE, 'value' => '1', 'compare' => '=' ], [ 'key' => self::META_IS_ACTIVE, 'compare' => 'NOT EXISTS' ], // legacy/unset → treat as active ]; } $users = get_users( $args ); return is_array( $users ) ? $users : []; } /** * Hydrate an actor's full record. Returns null if user does not exist * or does not have the sa_actor role (defensive — never expose * non-actor users through this surface). */ public static function get_actor( int $user_id ): ?array { $user = get_userdata( $user_id ); if ( ! $user || ! in_array( self::ROLE, (array) $user->roles, true ) ) { return null; } $owner_id = (int) get_user_meta( $user_id, self::META_OWNER_USER_ID, true ); $owner_user = $owner_id ? get_userdata( $owner_id ) : null; $is_active_v = get_user_meta( $user_id, self::META_IS_ACTIVE, true ); return [ 'user_id' => (int) $user->ID, 'name' => (string) $user->display_name, 'login' => (string) $user->user_login, 'email' => (string) $user->user_email, 'type' => (string) get_user_meta( $user_id, self::META_TYPE, true ), 'persona' => (string) get_user_meta( $user_id, self::META_PERSONA, true ), 'scope' => (string) get_user_meta( $user_id, self::META_SCOPE, true ), 'owner_user_id' => $owner_id, 'owner_login' => $owner_user ? (string) $owner_user->user_login : '', 'is_active' => ( $is_active_v === '' || $is_active_v === '1' ), 'subscription_tier' => (string) get_user_meta( $user_id, self::META_SUBSCRIPTION_TIER, true ), 'created_at' => (string) $user->user_registered, ]; } /** * Create an actor. Required: name, type, persona (creation discipline — * no nameless or roleless or purposeless actors). Owner defaults to * current user. Email is synthetic if not provided (actors are not * meant to log in). * * Returns the new user_id (int) or WP_Error on validation/creation failure. */ public static function create_actor( array $data ) { $name = isset( $data['name'] ) ? trim( (string) $data['name'] ) : ''; $type = isset( $data['type'] ) ? trim( (string) $data['type'] ) : ''; $persona = isset( $data['persona'] ) ? trim( (string) $data['persona'] ) : ''; $scope = isset( $data['scope'] ) ? trim( (string) $data['scope'] ) : ''; $email = isset( $data['email'] ) ? trim( (string) $data['email'] ) : ''; $owner = isset( $data['owner_user_id'] ) ? (int) $data['owner_user_id'] : (int) get_current_user_id(); if ( $name === '' ) return new WP_Error( 'actor_name_required', 'Actor name is required (intent — no random assistants).' ); if ( $type === '' ) return new WP_Error( 'actor_type_required', 'Actor type is required (defined role).' ); if ( $persona === '' ) return new WP_Error( 'actor_persona_required', 'Actor persona is required (scoped purpose).' ); // Synthesize a unique login from the name. WP requires unique user_login. $login_base = sanitize_user( strtolower( str_replace( ' ', '-', $name ) ), true ); if ( $login_base === '' ) $login_base = 'sa-actor'; $login = $login_base; $i = 1; while ( username_exists( $login ) ) { $login = $login_base . '-' . $i++; if ( $i > 200 ) { return new WP_Error( 'actor_login_exhausted', 'Could not generate unique login for this actor name.' ); } } // Synthesize an email if not provided. Actors live as WP users for // identity but are not expected to log in or receive mail. if ( $email === '' ) { $email = $login . '@sleeperagents.local'; } if ( email_exists( $email ) ) { $email = $login . '+' . wp_generate_password( 6, false ) . '@sleeperagents.local'; } // Random password — actors are not meant to log in. We never expose it. $password = wp_generate_password( 32, true, true ); $user_id = wp_insert_user( [ 'user_login' => $login, 'user_email' => $email, 'user_pass' => $password, 'display_name' => $name, 'first_name' => $name, 'role' => self::ROLE, ] ); if ( is_wp_error( $user_id ) ) { return $user_id; } update_user_meta( $user_id, self::META_TYPE, $type ); update_user_meta( $user_id, self::META_PERSONA, $persona ); update_user_meta( $user_id, self::META_SCOPE, $scope ); update_user_meta( $user_id, self::META_OWNER_USER_ID, $owner ); update_user_meta( $user_id, self::META_IS_ACTIVE, '1' ); update_user_meta( $user_id, self::META_SUBSCRIPTION_TIER, '' ); /** * Hook for downstream consumers (e.g. a future Asterion ledger writer * that records canonical actor identity in markdown). Today nothing * listens; the hook exists so the future write surface can attach * without re-architecting this Board. */ do_action( 'sa_orch_actor_created', (int) $user_id, $data ); return (int) $user_id; } /** Archive an actor (sets is_active=0). Returns true on success. */ public static function archive_actor( int $user_id ): bool { $actor = self::get_actor( $user_id ); if ( ! $actor ) return false; update_user_meta( $user_id, self::META_IS_ACTIVE, '0' ); do_action( 'sa_orch_actor_archived', $user_id ); return true; } /** Unarchive an actor (sets is_active=1). */ public static function unarchive_actor( int $user_id ): bool { $actor = self::get_actor( $user_id ); if ( ! $actor ) return false; update_user_meta( $user_id, self::META_IS_ACTIVE, '1' ); do_action( 'sa_orch_actor_unarchived', $user_id ); return true; } }