Spaces:
Sleeping
Sleeping
| defined( 'ABSPATH' ) || exit; | |
| /** | |
| * Actor Registry — WP-User-backed identity layer (Board #22). | |
| * | |
| * Actors are persistent identity-bound roles — NOT disposable NPCs. Each | |
| * actor is a real WordPress user with the 'sa_actor' role and structured | |
| * metadata recorded in user_meta. The wp_users row + meta together IS the | |
| * actor's persistent identity for this Board. | |
| * | |
| * Doctrine constraints (locked, non-negotiable): | |
| * Actors may: speak, draft, advise, observe, coordinate | |
| * Actors may NOT: progress Boards, accept/close tasks, mutate Asterion, | |
| * silently act | |
| * | |
| * Sara grants voice. Asterion grants permission. Human grants progress. | |
| * | |
| * Implementation notes: | |
| * - 'sa_actor' role gets only 'read' capability — nothing else. Actors | |
| * are not real operators; they exist to be attributed. | |
| * - Asterion ledger MD writes for actor records are out of this Board's | |
| * scope. The dormant SA_Orch_Asterion writer stays dormant. Future | |
| * Board can add literal canonical writes when the write surface | |
| * activates. | |
| * - Archive (sa_actor_is_active='0'), never delete. Actors are retired, | |
| * not destroyed. Continuity preserved. | |
| */ | |
| class SA_Orch_Actor_Registry { | |
| const ROLE = 'sa_actor'; | |
| // user_meta keys — namespaced 'sa_actor_*' to avoid collision with other plugins | |
| const META_TYPE = 'sa_actor_type'; | |
| const META_PERSONA = 'sa_actor_persona'; | |
| const META_SCOPE = 'sa_actor_scope'; | |
| const META_OWNER_USER_ID = 'sa_actor_owner_user_id'; | |
| const META_IS_ACTIVE = 'sa_actor_is_active'; | |
| const META_SUBSCRIPTION_TIER = 'sa_actor_subscription_tier'; | |
| /** Idempotent role registration. Hooked on init. */ | |
| public static function register_role() { | |
| if ( ! get_role( self::ROLE ) ) { | |
| add_role( self::ROLE, __( 'SA Actor', 'sa-orchestration' ), [ | |
| 'read' => 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; | |
| } | |
| } | |