$proposal @return array{ok: bool, error: string, proposal?: array} */ public static function stage(array $proposal): array { if (self::pending() !== null) { return ['ok' => false, 'error' => 'pending_proposal_exists']; } foreach (['id', 'organizationId', 'siteId', 'contentId', 'fieldKey', 'beforeValue', 'proposedValue', 'beforeFingerprint', 'evidence'] as $key) { if (!array_key_exists($key, $proposal)) { return ['ok' => false, 'error' => 'invalid_proposal']; } } $settings = TheDotDevAgenticBridge_Storage::get(); $organization_id = trim((string) ($settings['organization_id'] ?? '')); $site_id = trim((string) ($settings['site_id'] ?? '')); if ($organization_id === '' || $site_id === '' || !self::bounded($proposal['id'])) { return ['ok' => false, 'error' => 'invalid_proposal']; } if (!hash_equals($organization_id, (string) $proposal['organizationId'])) { return ['ok' => false, 'error' => 'organization_mismatch']; } if (!hash_equals($site_id, (string) $proposal['siteId'])) { return ['ok' => false, 'error' => 'site_mismatch']; } if (!hash_equals('agentSummary', (string) $proposal['fieldKey'])) { return ['ok' => false, 'error' => 'field_not_allowed']; } $content_id = is_int($proposal['contentId']) ? $proposal['contentId'] : 0; $post = get_post($content_id); if ($content_id <= 0 || !$post instanceof WP_Post || !TheDotDevAgenticBridge_Content_Metadata::is_configurable_content($post)) { return ['ok' => false, 'error' => 'content_not_configurable']; } $before = self::summary($content_id); $fingerprint = self::fingerprint($content_id, (string) get_permalink($post), $before); if (!hash_equals($before, (string) $proposal['beforeValue']) || !self::valid_fingerprint((string) $proposal['beforeFingerprint']) || !hash_equals($fingerprint, (string) $proposal['beforeFingerprint'])) { return ['ok' => false, 'error' => 'content_conflict']; } $proposed = trim((string) $proposal['proposedValue']); if ($proposed === '' || strlen($proposed) > TheDotDevAgenticBridge_Content_Metadata::MAX_SUMMARY_CHARS || TheDotDevAgenticBridge_Content_Metadata::contains_dangerous_text_public($proposed)) { return ['ok' => false, 'error' => 'unsafe_proposed_value']; } $record = [ 'id' => (string) $proposal['id'], 'organizationId' => $organization_id, 'siteId' => $site_id, 'contentId' => $content_id, 'fieldKey' => 'agentSummary', 'beforeValue' => $before, 'proposedValue' => $proposed, 'beforeFingerprint' => $fingerprint, 'evidence' => sanitize_text_field((string) $proposal['evidence']), 'state' => 'staged', 'stagedAt' => gmdate('c'), ]; TheDotDevAgenticBridge_Storage::update(['ai_proposal_pending' => $record]); return ['ok' => true, 'error' => '', 'proposal' => $record]; } /** @return array{ok: bool, error: string} */ public static function approve(string $id, int $actor_id): array { $record = self::pending(); if (!is_array($record) || !hash_equals((string) ($record['id'] ?? ''), $id) || $actor_id <= 0) { return ['ok' => false, 'error' => 'proposal_mismatch']; } if (($record['state'] ?? '') !== 'staged') { return ['ok' => false, 'error' => 'approval_not_available']; } $record['state'] = 'approved'; $record['approvedBy'] = $actor_id; $record['approvedAt'] = gmdate('c'); TheDotDevAgenticBridge_Storage::update(['ai_proposal_pending' => $record]); return ['ok' => true, 'error' => '']; } /** @return array{ok: bool, error: string, auditId?: string} */ public static function apply(string $id, int $actor_id): array { $record = self::pending(); if (!is_array($record) || !hash_equals((string) ($record['id'] ?? ''), $id) || $actor_id <= 0 || ($record['state'] ?? '') !== 'approved') { return ['ok' => false, 'error' => 'approval_required']; } $content_id = (int) ($record['contentId'] ?? 0); $post = get_post($content_id); $before = self::summary($content_id); $current = $post instanceof WP_Post ? self::fingerprint($content_id, (string) get_permalink($post), $before) : ''; if (!$post instanceof WP_Post || !hash_equals((string) ($record['beforeValue'] ?? ''), $before) || !hash_equals((string) ($record['beforeFingerprint'] ?? ''), $current)) { return ['ok' => false, 'error' => 'content_conflict']; } $snapshot = TheDotDevAgenticBridge_Content_Metadata::raw_record($content_id); $config = TheDotDevAgenticBridge_Content_Metadata::effective_config_for_post($post); $config['agentSummary'] = (string) $record['proposedValue']; $result = TheDotDevAgenticBridge_Content_Metadata::save_content_config($content_id, $config); if (empty($result['ok']) || !hash_equals((string) $record['proposedValue'], self::summary($content_id))) { return ['ok' => false, 'error' => 'apply_verification_failed']; } $audit_id = function_exists('wp_generate_uuid4') ? wp_generate_uuid4() : ('ai-proposal-' . gmdate('YmdHis')); $audit = self::audit(); $audit[] = ['id' => $audit_id, 'proposalId' => $id, 'contentId' => $content_id, 'beforeValue' => $before, 'appliedValue' => (string) $record['proposedValue'], 'rawSnapshot' => $snapshot, 'appliedAt' => gmdate('c'), 'appliedBy' => $actor_id, 'undoneAt' => '']; TheDotDevAgenticBridge_Storage::update(['ai_proposal_pending' => null, 'ai_proposal_audit' => $audit]); return ['ok' => true, 'error' => '', 'auditId' => $audit_id]; } /** @return array{ok: bool, error: string} */ public static function undo(string $audit_id, int $actor_id): array { $audit = self::audit(); foreach ($audit as $index => $entry) { if (!is_array($entry) || !hash_equals((string) ($entry['id'] ?? ''), $audit_id)) { continue; } if ($actor_id <= 0 || !empty($entry['undoneAt'])) { return ['ok' => false, 'error' => 'undo_not_available']; } $content_id = (int) ($entry['contentId'] ?? 0); if (!hash_equals((string) ($entry['appliedValue'] ?? ''), self::summary($content_id))) { return ['ok' => false, 'error' => 'restore_refused']; } $result = TheDotDevAgenticBridge_Content_Metadata::restore_raw_record($content_id, is_array($entry['rawSnapshot'] ?? null) ? $entry['rawSnapshot'] : []); if (empty($result['ok']) || !hash_equals((string) $entry['beforeValue'], self::summary($content_id))) { return ['ok' => false, 'error' => 'restore_verification_failed']; } $audit[$index]['undoneAt'] = gmdate('c'); TheDotDevAgenticBridge_Storage::update(['ai_proposal_audit' => $audit]); return ['ok' => true, 'error' => '']; } return ['ok' => false, 'error' => 'audit_not_found']; } /** @return array|null */ public static function pending(): ?array { $value = TheDotDevAgenticBridge_Storage::get()['ai_proposal_pending'] ?? null; return is_array($value) ? $value : null; } /** @return list> */ public static function audit(): array { $value = TheDotDevAgenticBridge_Storage::get()['ai_proposal_audit'] ?? []; return is_array($value) ? $value : []; } public static function fingerprint(int $content_id, string $url, string $value): string { return hash('sha256', wp_json_encode(['contentId' => $content_id, 'url' => $url, 'value' => $value], JSON_UNESCAPED_SLASHES) ?: ''); } private static function summary(int $content_id): string { $store = TheDotDevAgenticBridge_Content_Metadata::normalized_store(); return (string) ($store['by_content_id'][(string) $content_id]['agentSummary'] ?? ''); } private static function bounded(mixed $value): bool { return is_string($value) && trim($value) !== '' && strlen($value) <= self::MAX_ID; } private static function valid_fingerprint(string $value): bool { return (bool) preg_match('/^[a-f0-9]{64}$/', $value); } }