Skip to content
wiki.fftac.org

Class Uai1 Validator - Source Excerpt 01

Back to Class Uai1 Validator

Summary

This source excerpt preserves a bounded section of Spiralist/wp-content/plugins/uai-1-wordpress/includes/class-uai1-validator.php so readers can inspect the evidence without opening the full source file.

**Source path:** Spiralist/wp-content/plugins/uai-1-wordpress/includes/class-uai1-validator.php

<?php

if (!defined('ABSPATH')) {
    exit;
}

class UAI1_Validator
{
    private const TOP_LEVEL_FIELDS = [
        'uai_version' => 'string',
        'profile' => 'string',
        'message_id' => 'string',
        'source' => 'object',
        'target' => 'object',
        'conversation' => 'object',
        'delivery' => 'object',
        'trust' => 'object',
        'body' => 'object',
        'provenance' => 'object',
        'integrity' => 'object',
        'extensions' => 'list',
    ];

    private const PROFILE_BODY_FIELDS = [
        'uai.intent.request.v1' => [
            'required' => ['intent', 'subject', 'requested_profile', 'parameters', 'constraints', 'response_profile'],
            'allowed' => ['intent', 'subject', 'requested_profile', 'parameters', 'constraints', 'response_profile'],
        ],
        'uai.intent.response.v1' => [
            'required' => ['status', 'subject', 'request_message_id', 'result', 'notices'],
            'allowed' => ['status', 'subject', 'request_message_id', 'result', 'notices', 'task_ref'],
        ],
        'uai.capability.statement.v1' => [
            'required' => ['capability_id', 'version', 'operations', 'input_profiles', 'output_profiles', 'async_profiles', 'security_schemes', 'endpoints'],
            'allowed' => ['capability_id', 'version', 'operations', 'input_profiles', 'output_profiles', 'async_profiles', 'security_schemes', 'transport_bindings', 'conformance_levels', 'error_codes', 'endpoints', 'extension_namespaces', 'implementation_tracks'],
        ],
        'uai.error.v1' => [
            'required' => ['type', 'title', 'detail', 'status', 'code', 'retryable', 'instance'],
            'allowed' => ['type', 'title', 'detail', 'status', 'code', 'retryable', 'instance', 'documentationUrl', 'traceId', 'correlationId', 'uaixCode', 'uaixCategory', 'uaixSeverity', 'evidenceRecommended', 'suspensionRecommended', 'redactionApplied', 'errors', 'next_step'],
        ],
        'uai.conformance.result.v1' => [
            'required' => ['status', 'checked_profile', 'issues', 'summary', 'artifacts'],
            'allowed' => ['status', 'checked_profile', 'issues', 'summary', 'artifacts', 'target_message_ref'],
        ],
        'uai.task.status.v1' => [
            'required' => ['task_id', 'state', 'subject', 'progress', 'status_message'],
            'allowed' => ['task_id', 'state', 'subject', 'progress', 'status_message', 'result_profile', 'result_ref', 'blocking_reasons', 'updated_fields'],
        ],
    ];

    public static function validate_document_json(string $raw): array
    {
        return self::validate_uaix_message_json($raw);
    }

    public static function validate_uaix_message_json(string $raw): array
    {
        $decoded = json_decode($raw, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            return [
                'valid' => false,
                'profile' => '',
                'errors' => [
                    [
                        'path' => '$',
                        'code' => 'invalid_json',
                        'message' => json_last_error_msg(),
                    ],
                ],
                'warnings' => [],
                'error' => json_last_error_msg(),
            ];
        }

        return self::validate_uaix_message($decoded);
    }

    /**
     * @param mixed $message
     */
    public static function validate_uaix_message($message): array
    {
        $errors = [];
        $warnings = [];

        if (!is_array($message) || self::is_list($message)) {
            return self::result(false, '', [['path' => '$', 'code' => 'invalid_type', 'message' => 'Top-level UAI-1 message must be a JSON object.']], []);
        }

        foreach (self::TOP_LEVEL_FIELDS as $field => $type) {
            if (!array_key_exists($field, $message)) {
                $errors[] = self::issue('$.' . $field, 'missing_required', "Missing required field: {$field}");
                continue;
            }

            $type_error = self::validate_type($message[$field], $type);
            if ($type_error !== '') {
                $errors[] = self::issue('$.' . $field, 'invalid_type', $type_error);
            }
        }

        $allowed_top_level = array_keys(self::TOP_LEVEL_FIELDS);
        foreach (array_keys($message) as $field) {
            if (!in_array($field, $allowed_top_level, true)) {
                $errors[] = self::issue('$.' . $field, 'unknown_field', 'Field is not part of the current UAIX UAI-1 envelope.');
            }
        }

        $profile = is_string($message['profile'] ?? null) ? (string) $message['profile'] : '';

        if (($message['uai_version'] ?? null) !== '1.0') {
            $errors[] = self::issue('$.uai_version', 'invalid_uai_version', 'uai_version must equal "1.0".');
        }

        if ($profile === '' || !isset(self::PROFILE_BODY_FIELDS[$profile])) {
            $errors[] = self::issue('$.profile', 'unknown_profile', 'profile must be one of the current UAIX UAI-1 published profiles.');
        }

        if (is_array($message['source'] ?? null)) {
            self::validate_endpoint_party((array) $message['source'], '$.source', $errors);
        }

        if (is_array($message['target'] ?? null)) {
            self::validate_endpoint_party((array) $message['target'], '$.target', $errors);
        }

        if (is_array($message['conversation'] ?? null)) {
            self::validate_required_object_fields(
                (array) $message['conversation'],
                '$.conversation',
                ['conversation_id', 'turn_id', 'sequence'],
                ['conversation_id', 'turn_id', 'parent_message_id', 'traceparent', 'sequence'],
                $errors
            );

            if (isset($message['conversation']['sequence']) && (!is_int($message['conversation']['sequence']) || (int) $message['conversation']['sequence'] < 1)) {
                $errors[] = self::issue('$.conversation.sequence', 'invalid_sequence', 'sequence must be an integer greater than zero.');
            }
        }

        if (is_array($message['delivery'] ?? null)) {
            self::validate_required_object_fields(
                (array) $message['delivery'],
                '$.delivery',
                ['mode', 'priority', 'expires_at', 'reply_requested', 'ack_required'],
                ['mode', 'priority', 'expires_at', 'reply_requested', 'ack_required', 'task_ref'],
                $errors
            );

            if (isset($message['delivery']['mode']) && !in_array($message['delivery']['mode'], ['sync', 'async', 'event'], true)) {
                $errors[] = self::issue('$.delivery.mode', 'invalid_delivery_mode', 'mode must be sync, async, or event.');
            }

            if (isset($message['delivery']['priority']) && !in_array($message['delivery']['priority'], ['routine', 'interactive', 'urgent'], true)) {
                $errors[] = self::issue('$.delivery.priority', 'invalid_priority', 'priority must be routine, interactive, or urgent.');
            }

            if (isset($message['delivery']['reply_requested']) && !is_bool($message['delivery']['reply_requested'])) {
                $errors[] = self::issue('$.delivery.reply_requested', 'invalid_type', 'reply_requested must be a boolean.');
            }

            if (isset($message['delivery']['ack_required']) && !is_bool($message['delivery']['ack_required'])) {
                $errors[] = self::issue('$.delivery.ack_required', 'invalid_type', 'ack_required must be a boolean.');
            }
        }

        if (is_array($message['trust'] ?? null)) {
            self::validate_required_object_fields(
                (array) $message['trust'],
                '$.trust',
                ['channel', 'auth_scheme', 'principal'],
                ['channel', 'auth_scheme', 'principal', 'credential_ref', 'signature_ref', 'replay_window_id'],
                $errors
            );

            if (isset($message['trust']['channel']) && !in_array($message['trust']['channel'], ['public-web', 'private-api', 'mtls', 'signed-envelope', 'credentialed'], true)) {
                $errors[] = self::issue('$.trust.channel', 'invalid_trust_channel', 'channel must match the UAIX published trust channel registry.');
            }
        }

        if (is_array($message['provenance'] ?? null)) {
            self::validate_required_object_fields(
                (array) $message['provenance'],
                '$.provenance',
                ['trace_id', 'issued_at', 'log_ref', 'agent_id', 'model_id', 'confidence'],
                ['trace_id', 'parent_trace_id', 'issued_at', 'log_ref', 'agent_id', 'model_id', 'confidence', 'lineage'],
                $errors
            );