Class Uai1 Validator - Source Excerpt 01
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
);