using System; using System.Collections.Generic; using System.Data; using System.Data.Common; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using Lskj.CommandKernel; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace Lskj.AgentBridge { public sealed class RuntimeCustomerProfileMetadataSnapshot { public string DatabaseName { get; set; } public int SqlServerMajorVersion { get; set; } public int CompatibilityLevel { get; set; } public long UserTableCount { get; set; } public long UserViewCount { get; set; } public long UserProcedureCount { get; set; } public long UserTriggerCount { get; set; } public bool AgentWorkflowObjectsPresent { get; set; } public ISet CatalogEntries { get; set; } } public interface IRuntimeCustomerProfileMetadataProbe { RuntimeCustomerProfileMetadataSnapshot Capture(); } public interface IRuntimeCustomerProfileVerifier { void Verify( string expectedProfileSha256, string workflow, string moduleCode, string fieldMappingSha256, string readContractEvidenceSha256, string writeIntegrationEvidenceSha256); } public sealed class RuntimeCustomerProfileWorkflowStatus { public RuntimeCustomerProfileWorkflowStatus() { OpenBlockerCodes = new List(); } public bool Approved { get; set; } public IList OpenBlockerCodes { get; private set; } } public sealed class RuntimeCustomerProfileActivationStatus { public RuntimeCustomerProfileWorkflowStatus Purchase { get; set; } public RuntimeCustomerProfileWorkflowStatus Leave { get; set; } } /// /// 使用 ERP 当前已打开的数据库连接,只读取 SQL Server 系统目录。查询文本固定, /// 不读取业务行、不执行客户存储过程,也不把对象名投影给桥接调用方。 /// public sealed class SqlRuntimeCustomerProfileMetadataProbe : IRuntimeCustomerProfileMetadataProbe { private const int MaximumCatalogEntries = 100000; private readonly Func _connectionProvider; private readonly int _commandTimeoutSeconds; public const string MetadataQuery = @" SELECT CONVERT(nvarchar(128), DB_NAME()) AS database_name, CONVERT(int, SERVERPROPERTY('ProductMajorVersion')) AS sql_server_major_version, CONVERT(int, current_database.compatibility_level) AS compatibility_level, CONVERT(bigint, (SELECT COUNT_BIG(1) FROM sys.tables WHERE is_ms_shipped = 0)) AS user_table_count, CONVERT(bigint, (SELECT COUNT_BIG(1) FROM sys.views WHERE is_ms_shipped = 0)) AS user_view_count, CONVERT(bigint, (SELECT COUNT_BIG(1) FROM sys.procedures WHERE is_ms_shipped = 0)) AS user_procedure_count, CONVERT(bigint, (SELECT COUNT_BIG(1) FROM sys.triggers WHERE is_ms_shipped = 0)) AS user_trigger_count, CONVERT(bit, CASE WHEN ( SELECT COUNT_BIG(1) FROM sys.objects WHERE schema_id = SCHEMA_ID(N'dbo') AND ( (type = 'U' AND name IN ( N'p_agent_business_audit', N'p_agent_business_source_document', N'p_agent_command_idempotency', N'p_agent_integration_outbox', N'p_agent_purchase_currency_crosswalk', N'p_agent_purchase_row_scope', N'p_agent_workflow_adapter_evidence', N'p_agent_workflow_adapter_evidence_v2' )) OR (type = 'P' AND name IN ( N'p_lserp_agent_workflow_read', N'p_lserp_agent_workflow_read_compat100', N'p_lserp_agent_workflow_readiness', N'p_lserp_agent_workflow_readiness_v2', N'p_lserp_agent_workflow_readiness_v3', N'p_lserp_agent_workflow_write', N'p_lserp_agent_workflow_write_leave_compat100', N'p_lserp_agent_workflow_write_purchase_compat100' )) ) ) = 16 THEN 1 ELSE 0 END) AS agent_workflow_objects_present FROM sys.databases AS current_database WHERE current_database.database_id = DB_ID(); SELECT TOP (100001) catalog_entry.entry_kind, catalog_entry.schema_name, catalog_entry.object_name, catalog_entry.object_kind, catalog_entry.member_name FROM ( SELECT CONVERT(varchar(16), 'object') AS entry_kind, CONVERT(nvarchar(128), SCHEMA_NAME(catalog_object.schema_id)) AS schema_name, CONVERT(nvarchar(128), catalog_object.name) AS object_name, CONVERT(varchar(16), CASE catalog_object.type WHEN 'U' THEN 'table' WHEN 'V' THEN 'view' WHEN 'P' THEN 'procedure' ELSE 'invalid' END) AS object_kind, CONVERT(nvarchar(128), N'') AS member_name FROM sys.objects AS catalog_object WHERE catalog_object.is_ms_shipped = 0 AND catalog_object.type IN ('U', 'V', 'P') UNION ALL SELECT CONVERT(varchar(16), 'column'), CONVERT(nvarchar(128), SCHEMA_NAME(catalog_object.schema_id)), CONVERT(nvarchar(128), catalog_object.name), CONVERT(varchar(16), CASE catalog_object.type WHEN 'U' THEN 'table' WHEN 'V' THEN 'view' ELSE 'invalid' END), CONVERT(nvarchar(128), catalog_column.name) FROM sys.objects AS catalog_object INNER JOIN sys.columns AS catalog_column ON catalog_column.object_id = catalog_object.object_id WHERE catalog_object.is_ms_shipped = 0 AND catalog_object.type IN ('U', 'V') UNION ALL SELECT CONVERT(varchar(16), 'parameter'), CONVERT(nvarchar(128), SCHEMA_NAME(catalog_procedure.schema_id)), CONVERT(nvarchar(128), catalog_procedure.name), CONVERT(varchar(16), 'procedure'), CONVERT(nvarchar(128), catalog_parameter.name) FROM sys.procedures AS catalog_procedure INNER JOIN sys.parameters AS catalog_parameter ON catalog_parameter.object_id = catalog_procedure.object_id AND catalog_parameter.parameter_id > 0 WHERE catalog_procedure.is_ms_shipped = 0 ) AS catalog_entry ORDER BY catalog_entry.object_kind, catalog_entry.schema_name, catalog_entry.object_name, catalog_entry.entry_kind, catalog_entry.member_name;"; public SqlRuntimeCustomerProfileMetadataProbe( Func connectionProvider, int commandTimeoutSeconds) { if (connectionProvider == null) throw new ArgumentNullException("connectionProvider"); _connectionProvider = connectionProvider; _commandTimeoutSeconds = Math.Max( 5, Math.Min(commandTimeoutSeconds <= 0 ? 30 : commandTimeoutSeconds, 60)); } public RuntimeCustomerProfileMetadataSnapshot Capture() { DbConnection connection; try { connection = _connectionProvider(); } catch { throw Unavailable(); } if (connection == null || connection.State != ConnectionState.Open) throw Unavailable(); try { using (DbCommand command = connection.CreateCommand()) { command.CommandType = CommandType.Text; command.CommandText = MetadataQuery; command.CommandTimeout = _commandTimeoutSeconds; using (DbDataReader reader = command.ExecuteReader( CommandBehavior.SequentialAccess)) { return ReadSnapshot(reader); } } } catch (CommandKernelException) { throw; } catch { throw Unavailable(); } } internal static RuntimeCustomerProfileMetadataSnapshot ReadSnapshot( DbDataReader reader) { try { if (reader == null || !reader.Read()) throw Contract(); RuntimeCustomerProfileMetadataSnapshot snapshot = new RuntimeCustomerProfileMetadataSnapshot { DatabaseName = RequiredString(reader, "database_name", 128), SqlServerMajorVersion = RequiredInt( reader, "sql_server_major_version", 9, 99), CompatibilityLevel = RequiredInt( reader, "compatibility_level", 80, 200), UserTableCount = RequiredLong( reader, "user_table_count", 0, 10000000), UserViewCount = RequiredLong( reader, "user_view_count", 0, 10000000), UserProcedureCount = RequiredLong( reader, "user_procedure_count", 0, 10000000), UserTriggerCount = RequiredLong( reader, "user_trigger_count", 0, 10000000), AgentWorkflowObjectsPresent = RequiredBoolean( reader, "agent_workflow_objects_present"), CatalogEntries = new HashSet( StringComparer.OrdinalIgnoreCase) }; if (reader.Read() || !reader.NextResult()) throw Contract(); int count = 0; while (reader.Read()) { count += 1; if (count > MaximumCatalogEntries) throw Contract(); string entryKind = RequiredChoice( reader, "entry_kind", "object", "column", "parameter"); string objectKind = RequiredChoice( reader, "object_kind", "table", "view", "procedure"); string schemaName = RequiredString(reader, "schema_name", 128); string objectName = RequiredString(reader, "object_name", 128); string memberName = OptionalString( reader, "member_name", entryKind == "object" ? 0 : 128); if ((entryKind == "object" && memberName.Length != 0) || (entryKind == "column" && objectKind == "procedure") || (entryKind == "parameter" && objectKind != "procedure")) throw Contract(); if (!snapshot.CatalogEntries.Add(CatalogEntryKey( entryKind, schemaName, objectName, objectKind, memberName))) throw Contract(); } if (reader.NextResult()) throw Contract(); return snapshot; } catch (CommandKernelException) { throw; } catch { throw Contract(); } } internal static string CatalogEntryKey( string entryKind, string schemaName, string objectName, string objectKind, string memberName) { return (entryKind ?? string.Empty) + "\u001f" + (schemaName ?? string.Empty) + "\u001f" + (objectName ?? string.Empty) + "\u001f" + (objectKind ?? string.Empty) + "\u001f" + (memberName ?? string.Empty); } private static string RequiredChoice( DbDataReader reader, string name, params string[] allowed) { string value = RequiredString(reader, name, 16); if (!allowed.Contains(value, StringComparer.Ordinal)) throw Contract(); return value; } private static string RequiredString( DbDataReader reader, string name, int maximumLength) { object raw = reader[name]; string value = raw == null || raw == DBNull.Value ? string.Empty : Convert.ToString(raw).Trim(); if (value.Length == 0 || value.Length > maximumLength || value.Any(char.IsControl)) throw Contract(); return value; } private static string OptionalString( DbDataReader reader, string name, int maximumLength) { object raw = reader[name]; string value = raw == null || raw == DBNull.Value ? string.Empty : Convert.ToString(raw); if (value.Length > maximumLength || value.Any(char.IsControl)) throw Contract(); return value; } private static int RequiredInt( DbDataReader reader, string name, int minimum, int maximum) { return checked((int)RequiredLong(reader, name, minimum, maximum)); } private static long RequiredLong( DbDataReader reader, string name, long minimum, long maximum) { long value; try { value = Convert.ToInt64(reader[name]); } catch { throw Contract(); } if (value < minimum || value > maximum) throw Contract(); return value; } private static bool RequiredBoolean(DbDataReader reader, string name) { object raw = reader[name]; if (raw is bool) return (bool)raw; if (raw is byte && ((byte)raw == 0 || (byte)raw == 1)) return (byte)raw == 1; if (raw is int && ((int)raw == 0 || (int)raw == 1)) return (int)raw == 1; throw Contract(); } private static CommandKernelException Contract() { return Error( "profile_runtime_metadata_contract_invalid", "客户画像系统目录结果不符合固定协议,写命令保持禁用。"); } private static CommandKernelException Unavailable() { return Error( "profile_runtime_metadata_unavailable", "无法用 ERP 当前连接完成只读系统目录复核,写命令保持禁用。"); } private static CommandKernelException Error(string code, string message) { return new CommandKernelException(code, message, 6); } } /// /// 将签名验收清单绑定的客户画像与当前 SQL Server 系统目录做实时比较。 /// 成功不代表可单独激活;它只是在现有 V2 就绪行和签名门禁之外再增加一层。 /// public sealed class FileRuntimeCustomerProfileVerifier : IRuntimeCustomerProfileVerifier { private const int MaximumProfileBytes = 1024 * 1024; private static readonly Regex Sha256Pattern = new Regex( "^[a-f0-9]{64}$", RegexOptions.Compiled | RegexOptions.CultureInvariant); private static readonly Regex SafeDatabaseName = new Regex( "^[A-Za-z0-9_.-]{1,128}$", RegexOptions.Compiled | RegexOptions.CultureInvariant); private static readonly Regex SafeModuleCode = new Regex( "^[A-Za-z0-9_.:-]{1,64}$", RegexOptions.Compiled | RegexOptions.CultureInvariant); private static readonly Regex SafeCatalogName = new Regex( "^[A-Za-z_][A-Za-z0-9_]{0,127}$", RegexOptions.Compiled | RegexOptions.CultureInvariant); private static readonly Regex SafeParameterName = new Regex( "^@[A-Za-z_][A-Za-z0-9_]{0,127}$", RegexOptions.Compiled | RegexOptions.CultureInvariant); private static readonly Regex SafeBlockerCode = new Regex( "^[a-z0-9_.:-]{3,128}$", RegexOptions.Compiled | RegexOptions.CultureInvariant); private static readonly IDictionary PurchaseBlockerEvidence = new Dictionary( StringComparer.Ordinal) { { "purchase_currency_field_not_configured", "field_mapping" }, { "purchase_currency_crosswalk_not_approved", "write_integration" }, { "purchase_row_scope_not_approved", "write_integration" }, { "purchase_compat100_write_contract_not_approved", "write_integration" }, { "purchase_windows_integration_not_verified", "write_integration" } }; private static readonly IDictionary LeaveBlockerEvidence = new Dictionary( StringComparer.Ordinal) { { "leave_flow_type_rules_stale", "write_integration" }, { "leave_agent_schema_not_deployed", "write_integration" }, { "leave_compat100_write_contract_not_approved", "write_integration" }, { "leave_windows_integration_not_verified", "write_integration" } }; private readonly string _path; private readonly IRuntimeCustomerProfileMetadataProbe _probe; public FileRuntimeCustomerProfileVerifier( string path, IRuntimeCustomerProfileMetadataProbe probe) { if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("客户画像路径不能为空。", "path"); if (probe == null) throw new ArgumentNullException("probe"); _path = Path.GetFullPath(path); _probe = probe; } public static RuntimeCustomerProfileActivationStatus InspectActivation( string path) { ProfileBaseline baseline = ParseBaseline(Load(path).Profile); return new RuntimeCustomerProfileActivationStatus { Purchase = WorkflowStatus( baseline.PurchaseActivationAllowed && baseline.PurchaseOpenBlockerCount == 0, baseline.PurchaseOpenBlockerCodes), Leave = WorkflowStatus( baseline.LeaveOpenBlockerCount == 0, baseline.LeaveOpenBlockerCodes) }; } public void Verify( string expectedProfileSha256, string workflow, string moduleCode, string fieldMappingSha256, string readContractEvidenceSha256, string writeIntegrationEvidenceSha256) { if (!Sha256Pattern.IsMatch(expectedProfileSha256 ?? string.Empty)) throw Error( "profile_runtime_hash_mismatch", "签名清单没有绑定有效的客户画像哈希,写命令保持禁用。"); if (workflow != "purchase" && workflow != "leave") throw Contract(); if (!SafeModuleCode.IsMatch(moduleCode ?? string.Empty)) throw Error( "profile_workflow_module_mismatch", "签名清单没有绑定有效的工作流模块,写命令保持禁用。"); if (!Sha256Pattern.IsMatch(fieldMappingSha256 ?? string.Empty) || !Sha256Pattern.IsMatch(readContractEvidenceSha256 ?? string.Empty) || !Sha256Pattern.IsMatch(writeIntegrationEvidenceSha256 ?? string.Empty)) throw Error( "profile_workflow_resolution_mismatch", "签名清单没有提供有效的逐项阻断解决证据哈希,写命令保持禁用。"); ProfileDocument document = Load(_path); if (!string.Equals( document.Sha256, expectedProfileSha256, StringComparison.Ordinal)) throw Error( "profile_runtime_hash_mismatch", "当前客户画像与签名验收清单不一致,写命令保持禁用。"); ProfileBaseline baseline = ParseBaseline(document.Profile); if ((workflow == "purchase" && (!baseline.PurchaseActivationAllowed || baseline.PurchaseOpenBlockerCount != 0)) || (workflow == "leave" && baseline.LeaveOpenBlockerCount != 0)) throw Error( "profile_workflow_activation_blocked", "客户画像尚未批准当前工作流激活,写命令保持禁用。"); string expectedProfileModule = workflow == "purchase" ? baseline.PurchaseModuleCode : baseline.LeaveModuleCode; if (!string.Equals( expectedProfileModule, moduleCode, StringComparison.Ordinal)) throw Error( "profile_workflow_module_mismatch", "客户画像选定模块与签名验收清单不一致,写命令保持禁用。"); VerifyResolutionBindings( workflow == "purchase" ? baseline.PurchaseResolutions : baseline.LeaveResolutions, fieldMappingSha256, readContractEvidenceSha256, writeIntegrationEvidenceSha256); RuntimeCustomerProfileMetadataSnapshot snapshot = _probe.Capture(); if (!CriticalCatalogMatches( baseline.CriticalCatalogRequirements, snapshot == null ? null : snapshot.CatalogEntries)) throw Error( "profile_critical_catalog_contract_changed", "当前数据库关键字段或过程参数已漂移,写命令保持禁用。"); if (snapshot == null || !string.Equals( baseline.DatabaseName, snapshot.DatabaseName, StringComparison.OrdinalIgnoreCase) || baseline.SqlServerMajorVersion != snapshot.SqlServerMajorVersion || baseline.CompatibilityLevel != snapshot.CompatibilityLevel) throw Error( "profile_runtime_metadata_changed", "当前数据库身份或兼容级别与签名客户画像不一致,写命令保持禁用。"); } private static ProfileDocument Load(string path) { try { FileInfo file = new FileInfo(path); if (!file.Exists || file.Length <= 0 || file.Length > MaximumProfileBytes || (file.Attributes & FileAttributes.ReparsePoint) != 0) throw Contract(); byte[] bytes; using (FileStream stream = new FileStream( file.FullName, FileMode.Open, FileAccess.Read, FileShare.Read)) { if (stream.Length <= 0 || stream.Length > MaximumProfileBytes) throw Contract(); bytes = new byte[checked((int)stream.Length)]; int offset = 0; while (offset < bytes.Length) { int read = stream.Read(bytes, offset, bytes.Length - offset); if (read <= 0) throw Contract(); offset += read; } if (stream.ReadByte() != -1) throw Contract(); } string json = new UTF8Encoding(false, true).GetString(bytes); JObject profile; using (StringReader text = new StringReader(json)) using (RejectCommentsJsonReader reader = new RejectCommentsJsonReader(text)) { reader.DateParseHandling = DateParseHandling.None; reader.FloatParseHandling = FloatParseHandling.Decimal; reader.MaxDepth = 64; reader.SupportMultipleContent = false; profile = JObject.Load(reader, new JsonLoadSettings { DuplicatePropertyNameHandling = DuplicatePropertyNameHandling.Error, CommentHandling = CommentHandling.Ignore, LineInfoHandling = LineInfoHandling.Ignore }); if (reader.Read()) throw Contract(); } return new ProfileDocument { Profile = profile, Sha256 = Sha256(bytes) }; } catch (CommandKernelException) { throw; } catch { throw Contract(); } } private static ProfileBaseline ParseBaseline(JObject profile) { EnsureExact(profile, "schemaVersion", "profileType", "database", "safety", "purchaseTargetSelection", "purchaseActivationBlockers", "leaveActivationBlockers", "menus", "modules"); if (RequiredString(profile, "schemaVersion", 16) != "1.2" || RequiredString(profile, "profileType", 64) != "readonly_low_code_metadata_review") throw Contract(); JObject safety = RequiredObject(profile, "safety"); EnsureExact(safety, "source", "businessRowsRead", "storedProceduresExecuted", "writesPerformed", "runtimeEnabled", "requiresCustomerReview"); if (RequiredString(safety, "source", 128) != "system_catalog_and_low_code_configuration_only" || RequiredBoolean(safety, "businessRowsRead") || RequiredBoolean(safety, "storedProceduresExecuted") || RequiredBoolean(safety, "writesPerformed") || RequiredBoolean(safety, "runtimeEnabled") || !RequiredBoolean(safety, "requiresCustomerReview")) throw Contract(); JObject selection = RequiredObject( profile, "purchaseTargetSelection"); EnsureExact(selection, "selectedModuleCode", "selectedRole", "selectionState", "activationAllowed", "onlineRevalidationRequiredBeforeActivation", "candidatesEvaluated"); bool purchaseActivationAllowed = RequiredBoolean( selection, "activationAllowed"); string purchaseModuleCode = RequiredString( selection, "selectedModuleCode", 64); if (!SafeModuleCode.IsMatch(purchaseModuleCode) || RequiredString(selection, "selectedRole", 128) != "purchase_invoice_draft_write_candidate") throw Contract(); string selectionState = RequiredString( selection, "selectionState", 128); if (!RequiredBoolean( selection, "onlineRevalidationRequiredBeforeActivation") || RequiredArray(selection, "candidatesEvaluated", 1, 32) == null) throw Contract(); BlockerSet purchaseBlockers = ParseActivationBlockers( RequiredArray(profile, "purchaseActivationBlockers", 1, 64), PurchaseBlockerEvidence); BlockerSet leaveBlockers = ParseActivationBlockers( RequiredArray(profile, "leaveActivationBlockers", 1, 64), LeaveBlockerEvidence); int purchaseOpenBlockerCount = purchaseBlockers.OpenCount; int leaveOpenBlockerCount = leaveBlockers.OpenCount; bool purchaseShouldBeAllowed = purchaseOpenBlockerCount == 0; if (purchaseActivationAllowed != purchaseShouldBeAllowed || selectionState != (purchaseShouldBeAllowed ? "selected_and_activation_approved" : "selected_but_activation_blocked")) throw Contract(); RequiredArray(profile, "menus", 1, 64); JObject modules = RequiredObject(profile, "modules"); if (modules.Properties().Count() == 0 || modules.Properties().Count() > 64) throw Contract(); JObject leaveModule = modules["leave"] as JObject; string leaveModuleCode = leaveModule == null ? string.Empty : RequiredString(leaveModule, "moduleCode", 64); if (!SafeModuleCode.IsMatch(leaveModuleCode)) throw Contract(); JObject database = RequiredObject(profile, "database"); EnsureExact(database, "name", "sqlServerMajorVersion", "compatibilityLevel", "compatibilityContract", "userTableCount", "userViewCount", "userProcedureCount", "userTriggerCount", "agentWorkflowObjectsPresent", "criticalCatalogContract"); string databaseName = RequiredString(database, "name", 128); if (!SafeDatabaseName.IsMatch(databaseName) || RequiredString(database, "compatibilityContract", 128) != "fixed_scalar_and_schema_validated_xml_rowsets_in_trusted_erp_process") throw Contract(); return new ProfileBaseline { DatabaseName = databaseName, SqlServerMajorVersion = RequiredInt( database, "sqlServerMajorVersion", 9, 99), CompatibilityLevel = RequiredInt( database, "compatibilityLevel", 80, 200), UserTableCount = RequiredLong( database, "userTableCount", 0, 10000000), UserViewCount = RequiredLong( database, "userViewCount", 0, 10000000), UserProcedureCount = RequiredLong( database, "userProcedureCount", 0, 10000000), UserTriggerCount = RequiredLong( database, "userTriggerCount", 0, 10000000), AgentWorkflowObjectsPresent = RequiredBoolean( database, "agentWorkflowObjectsPresent"), PurchaseActivationAllowed = purchaseActivationAllowed, PurchaseModuleCode = purchaseModuleCode, LeaveModuleCode = leaveModuleCode, PurchaseOpenBlockerCount = purchaseOpenBlockerCount, LeaveOpenBlockerCount = leaveOpenBlockerCount, PurchaseOpenBlockerCodes = purchaseBlockers.OpenCodes, LeaveOpenBlockerCodes = leaveBlockers.OpenCodes, PurchaseResolutions = purchaseBlockers.Resolutions, LeaveResolutions = leaveBlockers.Resolutions, CriticalCatalogRequirements = ParseCatalogContract( RequiredObject(database, "criticalCatalogContract")) }; } private static BlockerSet ParseActivationBlockers( JArray blockers, IDictionary expectedEvidence) { HashSet codes = new HashSet(StringComparer.Ordinal); BlockerSet result = new BlockerSet { OpenCodes = new List(), Resolutions = new List() }; foreach (JToken token in blockers) { JObject blocker = token as JObject; if (blocker == null) throw Contract(); EnsureExact(blocker, "code", "status", "resolution", "evidence"); string code = RequiredString(blocker, "code", 128); string status = RequiredString(blocker, "status", 16); RequiredString(blocker, "evidence", 2000); if (!SafeBlockerCode.IsMatch(code) || !codes.Add(code) || !expectedEvidence.ContainsKey(code) || (status != "open" && status != "resolved")) throw Contract(); JToken resolutionToken = blocker["resolution"]; if (status == "open") { if (resolutionToken == null || resolutionToken.Type != JTokenType.Null) throw Contract(); result.OpenCount += 1; result.OpenCodes.Add(code); } else { JObject resolution = resolutionToken as JObject; if (resolution == null) throw Contract(); EnsureExact( resolution, "evidenceArtifact", "evidenceSha256", "approvedBy", "approvedAtUtc"); string artifact = RequiredString( resolution, "evidenceArtifact", 32); string hash = RequiredString( resolution, "evidenceSha256", 64); RequiredString(resolution, "approvedBy", 128); string approvedAt = RequiredString( resolution, "approvedAtUtc", 64); DateTime parsed; if (artifact != expectedEvidence[code] || !Sha256Pattern.IsMatch(hash) || !DateTime.TryParseExact( approvedAt, "o", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.RoundtripKind, out parsed) || parsed.Kind != DateTimeKind.Utc) throw Contract(); result.Resolutions.Add(new BlockerResolution { Code = code, EvidenceArtifact = artifact, EvidenceSha256 = hash }); } } if (codes.Count != blockers.Count || codes.Count != expectedEvidence.Count || expectedEvidence.Keys.Any(code => !codes.Contains(code))) throw Contract(); return result; } private static RuntimeCustomerProfileWorkflowStatus WorkflowStatus( bool approved, IEnumerable openCodes) { RuntimeCustomerProfileWorkflowStatus result = new RuntimeCustomerProfileWorkflowStatus { Approved = approved }; foreach (string code in openCodes ?? new string[0]) result.OpenBlockerCodes.Add(code); return result; } private static void VerifyResolutionBindings( IList resolutions, string fieldMappingSha256, string readContractEvidenceSha256, string writeIntegrationEvidenceSha256) { if (resolutions == null || resolutions.Count == 0) throw Error( "profile_workflow_resolution_mismatch", "客户画像没有提供当前工作流的逐项阻断解决证据,写命令保持禁用。"); foreach (BlockerResolution resolution in resolutions) { string expected = resolution.EvidenceArtifact == "field_mapping" ? fieldMappingSha256 : resolution.EvidenceArtifact == "read_contract" ? readContractEvidenceSha256 : resolution.EvidenceArtifact == "write_integration" ? writeIntegrationEvidenceSha256 : null; if (expected == null || !string.Equals( resolution.EvidenceSha256, expected, StringComparison.Ordinal)) throw Error( "profile_workflow_resolution_mismatch", "客户画像阻断解决证据与签名验收制品不一致,写命令保持禁用。"); } } private static IList ParseCatalogContract( JObject contract) { EnsureExact(contract, "contractVersion", "requirements"); if (RequiredString(contract, "contractVersion", 16) != "1.0") throw Contract(); JArray requirements = RequiredArray(contract, "requirements", 1, 128); HashSet identities = new HashSet( StringComparer.OrdinalIgnoreCase); List result = new List(); int members = 0; foreach (JToken token in requirements) { JObject item = token as JObject; if (item == null) throw Contract(); EnsureExact(item, "schemaName", "objectName", "objectKind", "requiredColumns", "requiredParameters"); string schemaName = RequiredString(item, "schemaName", 128); string objectName = RequiredString(item, "objectName", 128); string objectKind = RequiredString(item, "objectKind", 16); if (!SafeCatalogName.IsMatch(schemaName) || !SafeCatalogName.IsMatch(objectName) || (objectKind != "table" && objectKind != "view" && objectKind != "procedure") || !identities.Add( schemaName + "\u001f" + objectName + "\u001f" + objectKind)) throw Contract(); CatalogRequirement requirement = new CatalogRequirement { SchemaName = schemaName, ObjectName = objectName, ObjectKind = objectKind }; AddMembers( requirement.RequiredColumns, RequiredArray(item, "requiredColumns", 0, 128), SafeCatalogName); AddMembers( requirement.RequiredParameters, RequiredArray(item, "requiredParameters", 0, 128), SafeParameterName); if ((objectKind == "procedure" && requirement.RequiredColumns.Count != 0) || (objectKind != "procedure" && requirement.RequiredParameters.Count != 0)) throw Contract(); members += requirement.RequiredColumns.Count + requirement.RequiredParameters.Count; if (members > 2048) throw Contract(); result.Add(requirement); } return result; } private static void AddMembers( ISet destination, JArray source, Regex format) { foreach (JToken token in source) { string value = token != null && token.Type == JTokenType.String ? Convert.ToString(token).Trim() : string.Empty; if (!format.IsMatch(value) || !destination.Add(value)) throw Contract(); } } private static bool CriticalCatalogMatches( IEnumerable requirements, ISet actual) { if (requirements == null || actual == null) return false; foreach (CatalogRequirement requirement in requirements) { if (!actual.Contains(SqlRuntimeCustomerProfileMetadataProbe.CatalogEntryKey( "object", requirement.SchemaName, requirement.ObjectName, requirement.ObjectKind, string.Empty))) return false; foreach (string column in requirement.RequiredColumns) if (!actual.Contains( SqlRuntimeCustomerProfileMetadataProbe.CatalogEntryKey( "column", requirement.SchemaName, requirement.ObjectName, requirement.ObjectKind, column))) return false; foreach (string parameter in requirement.RequiredParameters) if (!actual.Contains( SqlRuntimeCustomerProfileMetadataProbe.CatalogEntryKey( "parameter", requirement.SchemaName, requirement.ObjectName, requirement.ObjectKind, parameter))) return false; } return true; } private static void EnsureExact(JObject source, params string[] names) { if (source == null) throw Contract(); HashSet expected = new HashSet( names, StringComparer.Ordinal); if (source.Properties().Count() != expected.Count || source.Properties().Any(item => !expected.Contains(item.Name))) throw Contract(); } private static JObject RequiredObject(JObject source, string name) { JObject value = source[name] as JObject; if (value == null) throw Contract(); return value; } private static JArray RequiredArray( JObject source, string name, int minimum, int maximum) { JArray value = source[name] as JArray; if (value == null || value.Count < minimum || value.Count > maximum) throw Contract(); return value; } private static string RequiredString( JObject source, string name, int maximumLength) { JToken token = source[name]; string value = token != null && token.Type == JTokenType.String ? Convert.ToString(token).Trim() : string.Empty; if (value.Length == 0 || value.Length > maximumLength || value.Any(char.IsControl)) throw Contract(); return value; } private static bool RequiredBoolean(JObject source, string name) { JToken token = source[name]; if (token == null || token.Type != JTokenType.Boolean) throw Contract(); return token.Value(); } private static int RequiredInt( JObject source, string name, int minimum, int maximum) { return checked((int)RequiredLong(source, name, minimum, maximum)); } private static long RequiredLong( JObject source, string name, long minimum, long maximum) { JToken token = source[name]; if (token == null || token.Type != JTokenType.Integer) throw Contract(); long value = token.Value(); if (value < minimum || value > maximum) throw Contract(); return value; } private static string Sha256(byte[] value) { using (SHA256 sha = SHA256.Create()) { return string.Concat( sha.ComputeHash(value).Select(item => item.ToString("x2"))); } } private static CommandKernelException Contract() { return Error( "profile_runtime_contract_invalid", "客户画像文件不符合固定只读契约,写命令保持禁用。"); } private static CommandKernelException Error(string code, string message) { return new CommandKernelException(code, message, 6); } private sealed class RejectCommentsJsonReader : JsonTextReader { public RejectCommentsJsonReader(TextReader reader) : base(reader) { } public override bool Read() { bool available = base.Read(); if (available && TokenType == JsonToken.Comment) throw new JsonReaderException("JSON comments are not allowed."); return available; } } private sealed class ProfileDocument { public JObject Profile { get; set; } public string Sha256 { get; set; } } private sealed class ProfileBaseline { public string DatabaseName { get; set; } public int SqlServerMajorVersion { get; set; } public int CompatibilityLevel { get; set; } public long UserTableCount { get; set; } public long UserViewCount { get; set; } public long UserProcedureCount { get; set; } public long UserTriggerCount { get; set; } public bool AgentWorkflowObjectsPresent { get; set; } public bool PurchaseActivationAllowed { get; set; } public string PurchaseModuleCode { get; set; } public string LeaveModuleCode { get; set; } public int PurchaseOpenBlockerCount { get; set; } public int LeaveOpenBlockerCount { get; set; } public IList PurchaseOpenBlockerCodes { get; set; } public IList LeaveOpenBlockerCodes { get; set; } public IList PurchaseResolutions { get; set; } public IList LeaveResolutions { get; set; } public IList CriticalCatalogRequirements { get; set; } } private sealed class BlockerSet { public int OpenCount { get; set; } public IList OpenCodes { get; set; } public IList Resolutions { get; set; } } private sealed class BlockerResolution { public string Code { get; set; } public string EvidenceArtifact { get; set; } public string EvidenceSha256 { get; set; } } private sealed class CatalogRequirement { public CatalogRequirement() { RequiredColumns = new HashSet( StringComparer.OrdinalIgnoreCase); RequiredParameters = new HashSet( StringComparer.OrdinalIgnoreCase); } public string SchemaName { get; set; } public string ObjectName { get; set; } public string ObjectKind { get; set; } public ISet RequiredColumns { get; private set; } public ISet RequiredParameters { get; private set; } } } }