> $facts The fact catalog rules are checked against. */ public function __construct( private readonly PolicyRepository $policies, private readonly AgentRepository $agents, private readonly array $facts = array() ) { } /** * Import entries. * * @param array $entries Document entries: {agent, priority, rules}. * @return array{imported: int, skipped: list, created: list} */ public function import( array $entries ): array { // First name wins on a rare duplicate name; the report names the // agent, so an operator can see exactly where a policy attached. $agents_by_name = array(); foreach ( $this->agents->all() as $agent ) { if ( ! isset( $agents_by_name[ $agent->name ] ) ) { $agents_by_name[ $agent->name ] = $agent->id; } } // Fingerprints of existing policies, so duplicates inside the // document and against the site are both caught. $seen = array(); foreach ( $this->policies->all() as $existing ) { $seen[ self::fingerprint( $existing->agent_id, $existing->priority, $existing->rules ) ] = true; } $created = array(); $skipped = array(); foreach ( array_values( $entries ) as $index => $entry ) { $verdict = $this->import_entry( is_array( $entry ) ? $entry : array(), $agents_by_name, $seen ); if ( $verdict instanceof Policy ) { $created[] = $verdict; continue; } $skipped[] = array( 'index' => $index, 'reason' => $verdict, ); } return array( 'imported' => count( $created ), 'skipped' => $skipped, 'created' => $created, ); } /** * Identity of a policy for duplicate detection. * * @param int|null $agent_id Scope. * @param int $priority Priority. * @param array> $rules Rules list. */ public static function fingerprint( ?int $agent_id, int $priority, array $rules ): string { return ( $agent_id ?? 'global' ) . '|' . $priority . '|' . (string) wp_json_encode( $rules ); } /** * Import one entry; returns the new policy or the skip reason. * * @param array $entry Document entry. * @param array $agents_by_name Agent name → id. * @param array $seen Fingerprints of stored policies (grows as entries import). * @return Policy|string */ private function import_entry( array $entry, array $agents_by_name, array &$seen ) { $agent_name = $entry['agent'] ?? null; $agent_id = null; if ( null !== $agent_name && '' !== $agent_name ) { if ( ! is_string( $agent_name ) || ! isset( $agents_by_name[ $agent_name ] ) ) { return sprintf( /* translators: %s: agent name from the imported document. */ __( 'No agent named "%s" exists on this site; create it first or import the policy as global yourself.', 'agentic-daisy-ai-agent-firewall' ), is_string( $agent_name ) ? $agent_name : '' ); } $agent_id = $agents_by_name[ $agent_name ]; } $rules = isset( $entry['rules'] ) && is_array( $entry['rules'] ) ? $entry['rules'] : array(); $errors = PolicyEvaluator::validate_rules( $rules, $this->facts, RetentionService::window_seconds() ); if ( array() !== $errors ) { return implode( ' ', $errors ); } $priority = isset( $entry['priority'] ) && is_numeric( $entry['priority'] ) ? (int) $entry['priority'] : 10; $fingerprint = self::fingerprint( $agent_id, $priority, $rules ); if ( isset( $seen[ $fingerprint ] ) ) { return __( 'An identical policy (same scope, priority and rules) already exists.', 'agentic-daisy-ai-agent-firewall' ); } /** @var array> $rules */ $policy = $this->policies->create( $agent_id, $rules, $priority ); $seen[ $fingerprint ] = true; return $policy; } }