Files
lserp_cs_6.0/插件库/Lskj.AgentBridge/CustomerProfileRuntimeGate.cs
2026-08-14 14:28:28 +08:00

1079 lines
46 KiB
C#

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<string> 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<string>();
}
public bool Approved { get; set; }
public IList<string> OpenBlockerCodes { get; private set; }
}
public sealed class RuntimeCustomerProfileActivationStatus
{
public RuntimeCustomerProfileWorkflowStatus Purchase { get; set; }
public RuntimeCustomerProfileWorkflowStatus Leave { get; set; }
}
/// <summary>
/// 使用 ERP 当前已打开的数据库连接,只读取 SQL Server 系统目录。查询文本固定,
/// 不读取业务行、不执行客户存储过程,也不把对象名投影给桥接调用方。
/// </summary>
public sealed class SqlRuntimeCustomerProfileMetadataProbe :
IRuntimeCustomerProfileMetadataProbe
{
private const int MaximumCatalogEntries = 100000;
private readonly Func<DbConnection> _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<DbConnection> 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<string>(
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);
}
}
/// <summary>
/// 将签名验收清单绑定的客户画像与当前 SQL Server 系统目录做实时比较。
/// 成功不代表可单独激活;它只是在现有 V2 就绪行和签名门禁之外再增加一层。
/// </summary>
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<string, string>
PurchaseBlockerEvidence = new Dictionary<string, string>(
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<string, string>
LeaveBlockerEvidence = new Dictionary<string, string>(
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<string, string> expectedEvidence)
{
HashSet<string> codes = new HashSet<string>(StringComparer.Ordinal);
BlockerSet result = new BlockerSet
{
OpenCodes = new List<string>(),
Resolutions = new List<BlockerResolution>()
};
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<string> 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<BlockerResolution> 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<CatalogRequirement> ParseCatalogContract(
JObject contract)
{
EnsureExact(contract, "contractVersion", "requirements");
if (RequiredString(contract, "contractVersion", 16) != "1.0")
throw Contract();
JArray requirements = RequiredArray(contract, "requirements", 1, 128);
HashSet<string> identities = new HashSet<string>(
StringComparer.OrdinalIgnoreCase);
List<CatalogRequirement> result = new List<CatalogRequirement>();
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<string> 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<CatalogRequirement> requirements,
ISet<string> 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<string> expected = new HashSet<string>(
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<bool>();
}
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<long>();
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<string> PurchaseOpenBlockerCodes { get; set; }
public IList<string> LeaveOpenBlockerCodes { get; set; }
public IList<BlockerResolution> PurchaseResolutions { get; set; }
public IList<BlockerResolution> LeaveResolutions { get; set; }
public IList<CatalogRequirement> CriticalCatalogRequirements { get; set; }
}
private sealed class BlockerSet
{
public int OpenCount { get; set; }
public IList<string> OpenCodes { get; set; }
public IList<BlockerResolution> 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<string>(
StringComparer.OrdinalIgnoreCase);
RequiredParameters = new HashSet<string>(
StringComparer.OrdinalIgnoreCase);
}
public string SchemaName { get; set; }
public string ObjectName { get; set; }
public string ObjectKind { get; set; }
public ISet<string> RequiredColumns { get; private set; }
public ISet<string> RequiredParameters { get; private set; }
}
}
}