using System; using System.Collections.Generic; using System.Globalization; 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 CustomerAcceptanceBundleVerificationResult { public string ContentSha256 { get; set; } public string SourceCommit { get; set; } public string PackageSha256 { get; set; } public string RuntimeConfigurationSha256 { get; set; } public string CustomerProfileSha256 { get; set; } public string RolloutPolicySha256 { get; set; } public string RolloutCustomerId { get; set; } public string DatabaseScopeFingerprint { get; set; } public string RuntimeCliVersion { get; set; } public string RuntimeCliSha256 { get; set; } public string RuntimeCliSignerThumbprint { get; set; } public string VerifierCliSha256 { get; set; } public string VerifierSignerThumbprint { get; set; } public string PurchaseReadOnlySessionPreflightGeneratedAtUtc { get; set; } public string PurchaseFieldReadOnlyValidationHandoffGeneratedAtUtc { get; set; } public int PurchaseReadOnlySessionErpProcessId { get; set; } public string PurchaseReadOnlySessionErpSha256 { get; set; } public string PurchaseReadOnlySessionErpSignerThumbprint { get; set; } public string LeaveReadOnlySessionPreflightGeneratedAtUtc { get; set; } public string LeaveFieldReadOnlyValidationHandoffGeneratedAtUtc { get; set; } public int LeaveReadOnlySessionErpProcessId { get; set; } public string LeaveReadOnlySessionErpSha256 { get; set; } public string LeaveReadOnlySessionErpSignerThumbprint { get; set; } public string DiagnosisReadOnlySessionPreflightGeneratedAtUtc { get; set; } public string DiagnosisFieldReadOnlyValidationHandoffGeneratedAtUtc { get; set; } public int DiagnosisReadOnlySessionErpProcessId { get; set; } public string DiagnosisReadOnlySessionErpSha256 { get; set; } public string DiagnosisReadOnlySessionErpSignerThumbprint { get; set; } public string PurchaseWorkflowUatAuthorizationSourceSha256 { get; set; } public string PurchaseWorkflowUatAuthorizationContentSha256 { get; set; } public string PurchaseWorkflowUatAuthorizationIdSha256 { get; set; } public string LeaveWorkflowUatAuthorizationSourceSha256 { get; set; } public string LeaveWorkflowUatAuthorizationContentSha256 { get; set; } public string LeaveWorkflowUatAuthorizationIdSha256 { get; set; } public string MiniMaxVisionProbeEvidenceSha256 { get; set; } public string MiniMaxVisionProbeObservedAtUtc { get; set; } public string MiniMaxVisionProbeRegion { get; set; } public string MiniMaxVisionProbeContractVersion { get; set; } public string AccountBook { get; set; } public string PurchaseSubSystemId { get; set; } public string LeaveSubSystemId { get; set; } public string DiagnosisSubSystemId { get; set; } public string PurchaseModuleCode { get; set; } public string LeaveModuleCode { get; set; } public DateTime IssuedAtUtc { get; set; } public DateTime ExpiresAtUtc { get; set; } public string ValidatedBy { get; set; } public string CertificateThumbprint { get; set; } public IDictionary ToDictionary() { return new Dictionary { { "packageType", "customer_commercial_acceptance_bundle" }, { "schemaVersion", "1.8" }, { "contentSha256", ContentSha256 }, { "sourceCommit", SourceCommit }, { "packageSha256", PackageSha256 }, { "runtimeConfigurationSha256", RuntimeConfigurationSha256 }, { "customerProfileSha256", CustomerProfileSha256 }, { "rolloutPolicySha256", RolloutPolicySha256 }, { "rolloutCustomerId", RolloutCustomerId }, { "runtimeCliVersion", RuntimeCliVersion }, { "runtimeCliSha256", RuntimeCliSha256 }, { "runtimeCliSignerThumbprint", RuntimeCliSignerThumbprint }, { "verifierCliSha256", VerifierCliSha256 }, { "verifierSignerThumbprint", VerifierSignerThumbprint }, { "purchaseWorkflowUatAuthorizationSourceSha256", PurchaseWorkflowUatAuthorizationSourceSha256 }, { "purchaseWorkflowUatAuthorizationContentSha256", PurchaseWorkflowUatAuthorizationContentSha256 }, { "purchaseWorkflowUatAuthorizationIdSha256", PurchaseWorkflowUatAuthorizationIdSha256 }, { "leaveWorkflowUatAuthorizationSourceSha256", LeaveWorkflowUatAuthorizationSourceSha256 }, { "leaveWorkflowUatAuthorizationContentSha256", LeaveWorkflowUatAuthorizationContentSha256 }, { "leaveWorkflowUatAuthorizationIdSha256", LeaveWorkflowUatAuthorizationIdSha256 }, { "miniMaxVisionProbeEvidenceSha256", MiniMaxVisionProbeEvidenceSha256 }, { "miniMaxVisionProbeObservedAtUtc", MiniMaxVisionProbeObservedAtUtc }, { "miniMaxVisionProbeRegion", MiniMaxVisionProbeRegion }, { "miniMaxVisionProbeContractVersion", MiniMaxVisionProbeContractVersion }, { "erpScope", new Dictionary { { "accountBook", AccountBook }, { "databaseScopeFingerprint", DatabaseScopeFingerprint } } }, { "sessionScopes", new Dictionary { { "purchase", SessionScope( PurchaseSubSystemId, PurchaseReadOnlySessionPreflightGeneratedAtUtc, PurchaseFieldReadOnlyValidationHandoffGeneratedAtUtc, PurchaseReadOnlySessionErpProcessId, PurchaseReadOnlySessionErpSha256, PurchaseReadOnlySessionErpSignerThumbprint) }, { "leave", SessionScope( LeaveSubSystemId, LeaveReadOnlySessionPreflightGeneratedAtUtc, LeaveFieldReadOnlyValidationHandoffGeneratedAtUtc, LeaveReadOnlySessionErpProcessId, LeaveReadOnlySessionErpSha256, LeaveReadOnlySessionErpSignerThumbprint) }, { "diagnosis", SessionScope( DiagnosisSubSystemId, DiagnosisReadOnlySessionPreflightGeneratedAtUtc, DiagnosisFieldReadOnlyValidationHandoffGeneratedAtUtc, DiagnosisReadOnlySessionErpProcessId, DiagnosisReadOnlySessionErpSha256, DiagnosisReadOnlySessionErpSignerThumbprint) } } }, { "workflows", new Dictionary { { "purchase", PurchaseModuleCode }, { "leave", LeaveModuleCode } } }, { "diagnosticScenarioCount", 4 }, { "artifactCount", 23 }, { "issuedAtUtc", IssuedAtUtc }, { "expiresAtUtc", ExpiresAtUtc }, { "validatedBy", ValidatedBy }, { "certificateThumbprint", CertificateThumbprint }, { "signatureVerified", true }, { "allComponentEvidenceVerified", true }, { "registrationReady", false }, { "note", "总验收包通过后仍须由 ERP 在启动和写入前复核当前 V2 就绪行、运行时配置及签名画像的只读系统目录。" } }; } private static IDictionary SessionScope( string subSystemId, string preflightGeneratedAtUtc, string handoffGeneratedAtUtc, int erpProcessId, string erpSha256, string erpSignerThumbprint) { return new Dictionary { { "subSystemId", subSystemId }, { "readOnlySessionPreflightGeneratedAtUtc", preflightGeneratedAtUtc }, { "fieldReadOnlyValidationHandoffGeneratedAtUtc", handoffGeneratedAtUtc }, { "erpProcessId", erpProcessId }, { "erpSha256", erpSha256 }, { "erpSignerThumbprint", erpSignerThumbprint } }; } } /// /// 离线验证客户商用总验收包及其二十三个原始制品。总包把最终 ZIP、源码提交、 /// 运行时配置、客户只读画像、三个独立 ERP 会话的动态参数合同、命令发布策略、 /// 两条写链路和四类初始化诊断绑定到同一客户数据库作用域。 /// public static class CustomerAcceptanceBundleEvidenceVerifier { private const int MaximumBundleBytes = 1024 * 1024; private static readonly Regex Sha256Pattern = new Regex( @"^[a-f0-9]{64}$", RegexOptions.Compiled | RegexOptions.CultureInvariant); private static readonly Regex CommitPattern = new Regex( @"^[a-f0-9]{40}$", RegexOptions.Compiled | RegexOptions.CultureInvariant); private static readonly Regex SafeModule = new Regex( @"^[A-Za-z0-9_.:\-]{1,64}$", RegexOptions.Compiled | RegexOptions.CultureInvariant); private static readonly Regex SafeIdentifier = new Regex( @"^[A-Za-z0-9_.:\-]{1,128}$", RegexOptions.Compiled | RegexOptions.CultureInvariant); private static readonly Regex SafeProductVersion = new Regex( @"^[0-9]{1,4}\.[0-9]{1,4}\.[0-9]{1,4}$", RegexOptions.Compiled | RegexOptions.CultureInvariant); private static readonly Regex SafeReadinessCode = new Regex( @"^[a-z0-9_.\-]{1,128}$", RegexOptions.Compiled | RegexOptions.CultureInvariant); private static readonly Regex SafeFileName = new Regex( @"^[A-Za-z0-9][A-Za-z0-9_.\-]{0,127}$", RegexOptions.Compiled | RegexOptions.CultureInvariant); private static readonly string[] ArtifactNames = { "package", "runtimeConfiguration", "rolloutPolicy", "commercialPreflight", "purchaseReadOnlySessionPreflight", "purchaseFieldReadOnlyValidationHandoff", "leaveReadOnlySessionPreflight", "leaveFieldReadOnlyValidationHandoff", "diagnosisReadOnlySessionPreflight", "diagnosisFieldReadOnlyValidationHandoff", "miniMaxVisionProbe", "customerProfile", "legacyBuildEvidence", "purchaseAcceptance", "purchaseWorkflowUatAuthorization", "purchaseWriteIntegration", "leaveWorkflowUatAuthorization", "leaveAcceptance", "leaveWriteIntegration", "diagnosticMissingField", "diagnosticPermissionDenied", "diagnosticInvalidRelation", "diagnosticInitializationSqlFailure" }; private static readonly string[] RequiredPreflightChecks = { "windows_os", "package_hashes", "package_source_provenance", "package_archive_binding", "sqlserver_compatibility100_syntax", "legacy_build_tool", "legacy_build_contract_tests", "workflow_write_evidence_tools", "workflow_uat_case_catalog", "legacy_build_evidence", "legacy_erp_authenticode", "legacy_cli_authenticode", "legacy_bridge_authenticode", "desktop_host", "host_authenticode", "bridge_cli_runtime_identity", "webview2_runtime", "guga_supply_chain_audit", "guga_sprite", "guga_commercial_license_evidence", "astrbot_agpl_eula_compliance_evidence", "minimax_api_service_compliance_evidence", "minimax_online_vision_probe_evidence", "astrbot_credential", "desktop_secret_environment", "astrbot_transport", "astrbot_reachable", "astrbot_runtime_contract", "rollout_policy_file", "erp_agent_bridge", "erp_rollout_policy", "erp_session_scope", "astrbot_plugin_layout", "astrbot_plugin_contract", "minimax_direct_https_vlm", "pdf_invoice_pipeline", "attachment_snapshot_binding", "erp_wire_contract" }; public static CustomerAcceptanceBundleVerificationResult VerifyFile( string bundlePath, string evidenceRoot, string expectedSourceCommit, string expectedPackageSha256, string expectedAccountBook, string expectedSubSystemId, IAcceptanceSignatureVerifier signatureVerifier, DateTime nowUtc) { return VerifyFile( bundlePath, evidenceRoot, expectedSourceCommit, expectedPackageSha256, expectedAccountBook, expectedSubSystemId, expectedSubSystemId, expectedSubSystemId, null, signatureVerifier, nowUtc); } public static CustomerAcceptanceBundleVerificationResult VerifyFile( string bundlePath, string evidenceRoot, string expectedSourceCommit, string expectedPackageSha256, string expectedAccountBook, string expectedSubSystemId, string expectedDatabaseScopeFingerprint, IAcceptanceSignatureVerifier signatureVerifier, DateTime nowUtc) { return VerifyFile( bundlePath, evidenceRoot, expectedSourceCommit, expectedPackageSha256, expectedAccountBook, expectedSubSystemId, expectedSubSystemId, expectedSubSystemId, expectedDatabaseScopeFingerprint, signatureVerifier, nowUtc); } public static CustomerAcceptanceBundleVerificationResult VerifyFile( string bundlePath, string evidenceRoot, string expectedSourceCommit, string expectedPackageSha256, string expectedAccountBook, string expectedPurchaseSubSystemId, string expectedLeaveSubSystemId, string expectedDiagnosisSubSystemId, string expectedDatabaseScopeFingerprint, IAcceptanceSignatureVerifier signatureVerifier, DateTime nowUtc) { if (signatureVerifier == null) throw new ArgumentNullException("signatureVerifier"); expectedDatabaseScopeFingerprint = string.IsNullOrWhiteSpace( expectedDatabaseScopeFingerprint) ? null : expectedDatabaseScopeFingerprint.Trim().ToLowerInvariant(); if (expectedDatabaseScopeFingerprint != null && !Sha256Pattern.IsMatch(expectedDatabaseScopeFingerprint)) throw Error( "customer_acceptance_bundle_input_invalid", "预期数据库作用域指纹格式无效。"); nowUtc = nowUtc.Kind == DateTimeKind.Utc ? nowUtc : nowUtc.ToUniversalTime(); string rootPath = GetEvidenceRoot(evidenceRoot); JObject envelope = LoadStrict(bundlePath, MaximumBundleBytes, "总验收包"); EnsureExact(envelope, "schemaVersion", "contentSha256", "signatureAlgorithm", "certificateThumbprint", "signatureBase64", "content"); if (RequiredString(envelope, "schemaVersion", 1, 16) != "1.8" || RequiredString(envelope, "signatureAlgorithm", 1, 32) != "rsa-sha256") throw Error("customer_acceptance_bundle_schema_invalid", "总验收包版本或签名算法无效。"); string contentHash = RequiredHash(envelope, "contentSha256"); string thumbprint = RequiredString(envelope, "certificateThumbprint", 40, 64); string normalizedThumbprint = WindowsTrustedPeopleSignatureVerifier.NormalizeThumbprint( thumbprint); if (normalizedThumbprint == null) throw Error("customer_acceptance_bundle_schema_invalid", "总验收包证书指纹无效。"); byte[] signature; try { signature = Convert.FromBase64String( RequiredString(envelope, "signatureBase64", 32, 4096)); } catch { throw Error("customer_acceptance_bundle_signature_invalid", "总验收包签名不是有效 Base64。"); } JObject content = RequiredObject(envelope, "content"); EnsureExact(content, "packageType", "sourceCommit", "packageSha256", "erpScope", "sessionScopes", "runtimeConfigurationSha256", "customerProfileSha256", "commercialPreflightGeneratedAtUtc", "miniMaxVisionProbeEvidenceSha256", "miniMaxVisionProbeObservedAtUtc", "miniMaxVisionProbeRegion", "miniMaxVisionProbeContractVersion", "rolloutPolicySha256", "rolloutCustomerId", "workflows", "runtimeCliVersion", "runtimeCliSha256", "runtimeCliSignerThumbprint", "verifierCliSha256", "verifierSignerThumbprint", "purchaseWorkflowUatAuthorizationSourceSha256", "purchaseWorkflowUatAuthorizationContentSha256", "purchaseWorkflowUatAuthorizationIdSha256", "leaveWorkflowUatAuthorizationSourceSha256", "leaveWorkflowUatAuthorizationContentSha256", "leaveWorkflowUatAuthorizationIdSha256", "diagnostics", "artifacts", "issuedAtUtc", "expiresAtUtc", "validatedBy", "note"); if (RequiredString(content, "packageType", 1, 64) != "customer_commercial_acceptance_bundle") throw Error("customer_acceptance_bundle_schema_invalid", "总验收包内容类型无效。"); string canonical = content.ToString(Formatting.None); if (!string.Equals(contentHash, Sha256(Encoding.UTF8.GetBytes(canonical)), StringComparison.Ordinal)) throw Error("customer_acceptance_bundle_hash_mismatch", "总验收包内容哈希不一致。"); if (!signatureVerifier.Verify(thumbprint, Encoding.UTF8.GetBytes(canonical), signature)) throw Error("customer_acceptance_bundle_signature_invalid", "总验收包 RSA 签名未通过 TrustedPeople 验证。"); string sourceCommit = RequiredString(content, "sourceCommit", 40, 40) .ToLowerInvariant(); string packageHash = RequiredHash(content, "packageSha256"); string runtimeHash = RequiredHash(content, "runtimeConfigurationSha256"); string customerProfileHash = RequiredHash( content, "customerProfileSha256"); string rolloutHash = RequiredHash(content, "rolloutPolicySha256"); string rolloutCustomerId = RequiredString( content, "rolloutCustomerId", 1, 64); string runtimeCliVersion = RequiredString( content, "runtimeCliVersion", 5, 14); string runtimeCliHash = RequiredHash(content, "runtimeCliSha256"); string verifierCliHash = RequiredHash(content, "verifierCliSha256"); string purchaseUatSourceHash = RequiredHash( content, "purchaseWorkflowUatAuthorizationSourceSha256"); string purchaseUatContentHash = RequiredHash( content, "purchaseWorkflowUatAuthorizationContentSha256"); string purchaseUatAuthorizationIdHash = RequiredHash( content, "purchaseWorkflowUatAuthorizationIdSha256"); string leaveUatSourceHash = RequiredHash( content, "leaveWorkflowUatAuthorizationSourceSha256"); string leaveUatContentHash = RequiredHash( content, "leaveWorkflowUatAuthorizationContentSha256"); string leaveUatAuthorizationIdHash = RequiredHash( content, "leaveWorkflowUatAuthorizationIdSha256"); string miniMaxProbeHash = RequiredHash( content, "miniMaxVisionProbeEvidenceSha256"); string miniMaxProbeObservedAtText = RequiredString( content, "miniMaxVisionProbeObservedAtUtc", 20, 64); DateTime miniMaxProbeObservedAt = RequiredProbeUtc( miniMaxProbeObservedAtText, "miniMaxVisionProbeObservedAtUtc"); string miniMaxProbeRegion = RequiredString( content, "miniMaxVisionProbeRegion", 2, 16); string miniMaxProbeContractVersion = RequiredString( content, "miniMaxVisionProbeContractVersion", 1, 16); string verifierSignerThumbprint = WindowsTrustedPeopleSignatureVerifier.NormalizeThumbprint( RequiredString(content, "verifierSignerThumbprint", 40, 64)); string runtimeCliSignerThumbprint = WindowsTrustedPeopleSignatureVerifier.NormalizeThumbprint( RequiredString( content, "runtimeCliSignerThumbprint", 40, 64)); if (!CommitPattern.IsMatch(sourceCommit) || !SafeProductVersion.IsMatch(runtimeCliVersion)) throw Error("customer_acceptance_bundle_schema_invalid", "总验收包源码提交无效。"); if (!SafeIdentifier.IsMatch(rolloutCustomerId)) throw Error("customer_acceptance_bundle_schema_invalid", "总验收包客户部署标识无效。"); if (verifierSignerThumbprint == null || runtimeCliSignerThumbprint == null) throw Error("customer_acceptance_bundle_schema_invalid", "总验收包验证器签名指纹无效。"); RequireExpected(sourceCommit, expectedSourceCommit, "源码提交"); RequireExpected(packageHash, expectedPackageSha256, "商用 ZIP SHA-256"); JObject scope = RequiredObject(content, "erpScope"); EnsureExact( scope, "accountBook", "databaseScopeFingerprint"); string accountBook = RequiredString(scope, "accountBook", 1, 128); string databaseScopeFingerprint = RequiredHash( scope, "databaseScopeFingerprint"); RequireExpected(accountBook, expectedAccountBook, "账套"); RequireExpected( databaseScopeFingerprint, expectedDatabaseScopeFingerprint, "数据库作用域指纹"); JObject sessionScopes = RequiredObject(content, "sessionScopes"); EnsureExact(sessionScopes, "purchase", "leave", "diagnosis"); SessionScopeSummary purchaseSessionScope = ParseSessionScope( RequiredObject(sessionScopes, "purchase"), expectedPurchaseSubSystemId, "采购"); SessionScopeSummary leaveSessionScope = ParseSessionScope( RequiredObject(sessionScopes, "leave"), expectedLeaveSubSystemId, "请假"); SessionScopeSummary diagnosisSessionScope = ParseSessionScope( RequiredObject(sessionScopes, "diagnosis"), expectedDiagnosisSubSystemId, "诊断"); DateTime preflightGeneratedAt = RequiredUtc( content, "commercialPreflightGeneratedAtUtc"); DateTime issuedAt = RequiredUtc(content, "issuedAtUtc"); DateTime expiresAt = RequiredUtc(content, "expiresAtUtc"); if (issuedAt > nowUtc.AddMinutes(5) || issuedAt < nowUtc.AddDays(-7) || expiresAt <= nowUtc || expiresAt <= issuedAt || expiresAt - issuedAt > TimeSpan.FromDays(366) || preflightGeneratedAt > issuedAt.AddMinutes(5) || preflightGeneratedAt < issuedAt.AddDays(-7)) throw Error("customer_acceptance_bundle_expired", "总验收包或商用预检报告时间无效。"); ValidateSessionScopeTime(purchaseSessionScope, issuedAt); ValidateSessionScopeTime(leaveSessionScope, issuedAt); ValidateSessionScopeTime(diagnosisSessionScope, issuedAt); string validatedBy = RequiredString(content, "validatedBy", 1, 128); if (!SafeIdentifier.IsMatch(validatedBy)) throw Error("customer_acceptance_bundle_schema_invalid", "总验收包验证人标识无效。"); RequiredString(content, "note", 1, 500); JObject artifactObject = RequiredObject(content, "artifacts"); EnsureExact(artifactObject, ArtifactNames); Dictionary artifacts = ResolveArtifacts( artifactObject, rootPath); AssertHash(artifacts["package"], packageHash, "商用 ZIP"); AssertHash(artifacts["runtimeConfiguration"], runtimeHash, "运行时配置"); AssertHash(artifacts["customerProfile"], customerProfileHash, "客户只读画像"); AssertHash(artifacts["rolloutPolicy"], rolloutHash, "命令发布策略"); AssertHash(artifacts["miniMaxVisionProbe"], miniMaxProbeHash, "MiniMax 在线视觉探针"); PreflightVerificationState preflight = VerifyPreflight( artifacts["commercialPreflight"].Path, sourceCommit, preflightGeneratedAt, issuedAt); if (!string.Equals( preflight.PackageSha256, packageHash, StringComparison.Ordinal)) { throw Error( "customer_acceptance_bundle_package_binding_invalid", "商用预检没有绑定总验收包中的同一个最终 ZIP。"); } if (!string.Equals( preflight.RolloutPolicySha256, rolloutHash, StringComparison.Ordinal) || !string.Equals( preflight.RolloutCustomerId, rolloutCustomerId, StringComparison.Ordinal)) { throw Error( "customer_acceptance_bundle_rollout_invalid", "商用预检与总验收包的命令发布策略不一致。"); } if (!string.Equals(preflight.MiniMaxVisionProbeEvidenceSha256, miniMaxProbeHash, StringComparison.Ordinal) || preflight.MiniMaxVisionProbeObservedAtUtc != miniMaxProbeObservedAt || !string.Equals(preflight.MiniMaxVisionProbeRegion, miniMaxProbeRegion, StringComparison.Ordinal) || !string.Equals(preflight.MiniMaxVisionProbeContractVersion, miniMaxProbeContractVersion, StringComparison.Ordinal)) { throw Error( "customer_acceptance_bundle_minimax_probe_invalid", "商用预检与总验收包的 MiniMax 在线视觉探针不一致。"); } MiniMaxVisionProbeState miniMaxProbe = VerifyMiniMaxVisionProbe( artifacts["miniMaxVisionProbe"].Path, preflightGeneratedAt, issuedAt); if (miniMaxProbe.ObservedAtUtc != miniMaxProbeObservedAt || !string.Equals(miniMaxProbe.Region, miniMaxProbeRegion, StringComparison.Ordinal) || !string.Equals(miniMaxProbe.ContractVersion, miniMaxProbeContractVersion, StringComparison.Ordinal)) { throw Error( "customer_acceptance_bundle_minimax_probe_invalid", "MiniMax 在线视觉探针原始报告与签名总验收内容不一致。"); } try { CommandRolloutPolicy rollout = CommandRolloutPolicy.Load( artifacts["rolloutPolicy"].Path, rolloutHash, rolloutCustomerId); if (!rollout.Configured || !string.Equals( rollout.DefaultAction, "deny", StringComparison.Ordinal) || !string.Equals( rollout.DatabaseScopeFingerprint, databaseScopeFingerprint, StringComparison.Ordinal)) { throw new InvalidOperationException(); } } catch { throw Error( "customer_acceptance_bundle_rollout_invalid", "总验收包命令发布策略未通过严格离线校验。"); } AssertHash( artifacts["legacyBuildEvidence"], preflight.LegacyBuildEvidenceSha256, "旧 ERP 构建证据"); LegacyBuildVerificationState legacy = VerifyLegacyBuildEvidence( artifacts["legacyBuildEvidence"].Path, sourceCommit); if (!string.Equals( legacy.CliSha256, verifierCliHash, StringComparison.Ordinal) || !string.Equals( legacy.CertificateThumbprint, verifierSignerThumbprint, StringComparison.Ordinal)) { throw Error( "customer_acceptance_bundle_verifier_invalid", "总验收包声明的验证 CLI 与旧 ERP 构建证据不一致。"); } JObject workflows = RequiredObject(content, "workflows"); EnsureExact(workflows, "purchase", "leave"); WorkflowSummary purchaseSummary = ParseWorkflowSummary( RequiredObject(workflows, "purchase"), "purchase"); WorkflowSummary leaveSummary = ParseWorkflowSummary( RequiredObject(workflows, "leave"), "leave"); WorkflowUatAuthorizationEvidence purchaseUat = VerifyWorkflowUatAuthorization( artifacts["purchaseWorkflowUatAuthorization"], "purchase", purchaseSummary.ModuleCode, purchaseUatSourceHash, purchaseUatContentHash, purchaseUatAuthorizationIdHash, rolloutCustomerId, accountBook, purchaseSessionScope.SubSystemId, databaseScopeFingerprint, runtimeHash, customerProfileHash, rolloutHash, sourceCommit, packageHash, runtimeCliVersion, runtimeCliHash, runtimeCliSignerThumbprint, verifierCliHash, verifierSignerThumbprint, legacy, issuedAt, signatureVerifier); WorkflowUatAuthorizationEvidence leaveUat = VerifyWorkflowUatAuthorization( artifacts["leaveWorkflowUatAuthorization"], "leave", leaveSummary.ModuleCode, leaveUatSourceHash, leaveUatContentHash, leaveUatAuthorizationIdHash, rolloutCustomerId, accountBook, leaveSessionScope.SubSystemId, databaseScopeFingerprint, runtimeHash, customerProfileHash, rolloutHash, sourceCommit, packageHash, runtimeCliVersion, runtimeCliHash, runtimeCliSignerThumbprint, verifierCliHash, verifierSignerThumbprint, legacy, issuedAt, signatureVerifier); AssertHash( artifacts["purchaseWorkflowUatAuthorization"], purchaseUatSourceHash, "采购短时 UAT 授权"); AssertHash( artifacts["leaveWorkflowUatAuthorization"], leaveUatSourceHash, "请假短时 UAT 授权"); JObject diagnostics = RequiredObject(content, "diagnostics"); EnsureExact(diagnostics, "missingField", "permissionDenied", "invalidRelation", "initializationSqlFailure"); DiagnosticSummary missingSummary = ParseDiagnosticSummary( RequiredObject(diagnostics, "missingField"), "missing_field"); DiagnosticSummary permissionSummary = ParseDiagnosticSummary( RequiredObject(diagnostics, "permissionDenied"), "permission_denied"); DiagnosticSummary relationSummary = ParseDiagnosticSummary( RequiredObject(diagnostics, "invalidRelation"), "invalid_relation"); DiagnosticSummary sqlSummary = ParseDiagnosticSummary( RequiredObject(diagnostics, "initializationSqlFailure"), "initialization_sql_failure"); DiagnosticVerificationState diagnosticState = new DiagnosticVerificationState(); VerifyDiagnostic( missingSummary, artifacts["diagnosticMissingField"], accountBook, diagnosisSessionScope.SubSystemId, issuedAt, diagnosticState); VerifyDiagnostic( permissionSummary, artifacts["diagnosticPermissionDenied"], accountBook, diagnosisSessionScope.SubSystemId, issuedAt, diagnosticState); VerifyDiagnostic( relationSummary, artifacts["diagnosticInvalidRelation"], accountBook, diagnosisSessionScope.SubSystemId, issuedAt, diagnosticState); VerifyDiagnostic( sqlSummary, artifacts["diagnosticInitializationSqlFailure"], accountBook, diagnosisSessionScope.SubSystemId, issuedAt, diagnosticState); if (diagnosticState.EvidenceIds.Count != 4 || diagnosticState.ContentHashes.Count != 4 || diagnosticState.UserIds.Count != 1 || diagnosticState.UserNames.Count != 1 || diagnosticState.DatabaseScopeFingerprints.Count != 1 || !string.Equals( diagnosticState.DatabaseScopeFingerprints.Single(), databaseScopeFingerprint, StringComparison.Ordinal) || !AdministratorIdentity.IsBuiltIn( diagnosticState.UserIds.Single(), diagnosticState.UserNames.Single())) throw Error( "customer_acceptance_bundle_diagnostic_invalid", "四类诊断必须使用同一管理员并保留四份不同的原始证据。"); ReadOnlySessionPreflightState purchaseReadOnlySession = VerifyReadOnlySessionPreflight( artifacts["purchaseReadOnlySessionPreflight"].Path, purchaseSessionScope.ReadOnlySessionPreflightGeneratedAtUtc, issuedAt, databaseScopeFingerprint, accountBook, purchaseSessionScope.SubSystemId, purchaseUat.UserId, purchaseUat.UserName, legacy.ErpSha256, legacy.CertificateThumbprint, runtimeCliVersion, runtimeCliHash, runtimeCliSignerThumbprint, rolloutHash, rolloutCustomerId, true, false, false, false, new[] { purchaseSummary.ModuleCode }); FieldReadOnlyValidationHandoffState purchaseHandoff = VerifyFieldReadOnlyValidationHandoff( artifacts["purchaseFieldReadOnlyValidationHandoff"].Path, purchaseSessionScope.FieldReadOnlyValidationHandoffGeneratedAtUtc, issuedAt, artifacts["purchaseReadOnlySessionPreflight"].Sha256, purchaseReadOnlySession, "purchase", 1); ReadOnlySessionPreflightState leaveReadOnlySession = VerifyReadOnlySessionPreflight( artifacts["leaveReadOnlySessionPreflight"].Path, leaveSessionScope.ReadOnlySessionPreflightGeneratedAtUtc, issuedAt, databaseScopeFingerprint, accountBook, leaveSessionScope.SubSystemId, leaveUat.UserId, leaveUat.UserName, legacy.ErpSha256, legacy.CertificateThumbprint, runtimeCliVersion, runtimeCliHash, runtimeCliSignerThumbprint, rolloutHash, rolloutCustomerId, false, true, false, false, new[] { leaveSummary.ModuleCode }); FieldReadOnlyValidationHandoffState leaveHandoff = VerifyFieldReadOnlyValidationHandoff( artifacts["leaveFieldReadOnlyValidationHandoff"].Path, leaveSessionScope.FieldReadOnlyValidationHandoffGeneratedAtUtc, issuedAt, artifacts["leaveReadOnlySessionPreflight"].Sha256, leaveReadOnlySession, "leave", 1); ReadOnlySessionPreflightState diagnosisReadOnlySession = VerifyReadOnlySessionPreflight( artifacts["diagnosisReadOnlySessionPreflight"].Path, diagnosisSessionScope.ReadOnlySessionPreflightGeneratedAtUtc, issuedAt, databaseScopeFingerprint, accountBook, diagnosisSessionScope.SubSystemId, diagnosticState.UserIds.Single(), diagnosticState.UserNames.Single(), legacy.ErpSha256, legacy.CertificateThumbprint, runtimeCliVersion, runtimeCliHash, runtimeCliSignerThumbprint, rolloutHash, rolloutCustomerId, false, false, true, true, diagnosticState.ModuleCodes); FieldReadOnlyValidationHandoffState diagnosisHandoff = VerifyFieldReadOnlyValidationHandoff( artifacts["diagnosisFieldReadOnlyValidationHandoff"].Path, diagnosisSessionScope.FieldReadOnlyValidationHandoffGeneratedAtUtc, issuedAt, artifacts["diagnosisReadOnlySessionPreflight"].Sha256, diagnosisReadOnlySession, "diagnosis", diagnosticState.ModuleCodes.Count); VerifyWorkflow( purchaseSummary, "purchase", artifacts["purchaseAcceptance"], artifacts["purchaseWriteIntegration"], accountBook, purchaseSessionScope.SubSystemId, runtimeHash, customerProfileHash, sourceCommit, packageHash, purchaseUat, signatureVerifier, nowUtc); VerifyWorkflow( leaveSummary, "leave", artifacts["leaveAcceptance"], artifacts["leaveWriteIntegration"], accountBook, leaveSessionScope.SubSystemId, runtimeHash, customerProfileHash, sourceCommit, packageHash, leaveUat, signatureVerifier, nowUtc); return new CustomerAcceptanceBundleVerificationResult { ContentSha256 = contentHash, SourceCommit = sourceCommit, PackageSha256 = packageHash, RuntimeConfigurationSha256 = runtimeHash, CustomerProfileSha256 = customerProfileHash, RolloutPolicySha256 = rolloutHash, RolloutCustomerId = rolloutCustomerId, DatabaseScopeFingerprint = databaseScopeFingerprint, RuntimeCliVersion = runtimeCliVersion, RuntimeCliSha256 = runtimeCliHash, RuntimeCliSignerThumbprint = runtimeCliSignerThumbprint, VerifierCliSha256 = verifierCliHash, VerifierSignerThumbprint = verifierSignerThumbprint, PurchaseReadOnlySessionPreflightGeneratedAtUtc = purchaseReadOnlySession.GeneratedAtUtcText, PurchaseFieldReadOnlyValidationHandoffGeneratedAtUtc = purchaseHandoff.GeneratedAtUtcText, PurchaseReadOnlySessionErpProcessId = purchaseReadOnlySession.ErpProcessId, PurchaseReadOnlySessionErpSha256 = purchaseReadOnlySession.ErpSha256, PurchaseReadOnlySessionErpSignerThumbprint = purchaseReadOnlySession.ErpSignerThumbprint, LeaveReadOnlySessionPreflightGeneratedAtUtc = leaveReadOnlySession.GeneratedAtUtcText, LeaveFieldReadOnlyValidationHandoffGeneratedAtUtc = leaveHandoff.GeneratedAtUtcText, LeaveReadOnlySessionErpProcessId = leaveReadOnlySession.ErpProcessId, LeaveReadOnlySessionErpSha256 = leaveReadOnlySession.ErpSha256, LeaveReadOnlySessionErpSignerThumbprint = leaveReadOnlySession.ErpSignerThumbprint, DiagnosisReadOnlySessionPreflightGeneratedAtUtc = diagnosisReadOnlySession.GeneratedAtUtcText, DiagnosisFieldReadOnlyValidationHandoffGeneratedAtUtc = diagnosisHandoff.GeneratedAtUtcText, DiagnosisReadOnlySessionErpProcessId = diagnosisReadOnlySession.ErpProcessId, DiagnosisReadOnlySessionErpSha256 = diagnosisReadOnlySession.ErpSha256, DiagnosisReadOnlySessionErpSignerThumbprint = diagnosisReadOnlySession.ErpSignerThumbprint, PurchaseWorkflowUatAuthorizationSourceSha256 = purchaseUatSourceHash, PurchaseWorkflowUatAuthorizationContentSha256 = purchaseUatContentHash, PurchaseWorkflowUatAuthorizationIdSha256 = purchaseUatAuthorizationIdHash, LeaveWorkflowUatAuthorizationSourceSha256 = leaveUatSourceHash, LeaveWorkflowUatAuthorizationContentSha256 = leaveUatContentHash, LeaveWorkflowUatAuthorizationIdSha256 = leaveUatAuthorizationIdHash, MiniMaxVisionProbeEvidenceSha256 = miniMaxProbeHash, MiniMaxVisionProbeObservedAtUtc = miniMaxProbeObservedAtText, MiniMaxVisionProbeRegion = miniMaxProbeRegion, MiniMaxVisionProbeContractVersion = miniMaxProbeContractVersion, AccountBook = accountBook, PurchaseSubSystemId = purchaseSessionScope.SubSystemId, LeaveSubSystemId = leaveSessionScope.SubSystemId, DiagnosisSubSystemId = diagnosisSessionScope.SubSystemId, PurchaseModuleCode = purchaseSummary.ModuleCode, LeaveModuleCode = leaveSummary.ModuleCode, IssuedAtUtc = issuedAt, ExpiresAtUtc = expiresAt, ValidatedBy = validatedBy, CertificateThumbprint = normalizedThumbprint }; } private static SessionScopeSummary ParseSessionScope( JObject value, string expectedSubSystemId, string label) { EnsureExact( value, "subSystemId", "readOnlySessionPreflightGeneratedAtUtc", "fieldReadOnlyValidationHandoffGeneratedAtUtc"); string subSystemId = RequiredString(value, "subSystemId", 1, 128); RequireExpected(subSystemId, expectedSubSystemId, label + "子系统"); return new SessionScopeSummary { SubSystemId = subSystemId, ReadOnlySessionPreflightGeneratedAtUtc = RequiredUtc( value, "readOnlySessionPreflightGeneratedAtUtc"), FieldReadOnlyValidationHandoffGeneratedAtUtc = RequiredUtc( value, "fieldReadOnlyValidationHandoffGeneratedAtUtc") }; } private static void ValidateSessionScopeTime( SessionScopeSummary scope, DateTime issuedAtUtc) { if (scope.ReadOnlySessionPreflightGeneratedAtUtc > issuedAtUtc.AddMinutes(5) || scope.ReadOnlySessionPreflightGeneratedAtUtc < issuedAtUtc.AddDays(-7) || scope.FieldReadOnlyValidationHandoffGeneratedAtUtc > issuedAtUtc.AddMinutes(5) || scope.FieldReadOnlyValidationHandoffGeneratedAtUtc < issuedAtUtc.AddDays(-7)) throw Error( "customer_acceptance_bundle_expired", "总验收包中的独立 ERP 会话证据时间无效。"); } private static WorkflowUatAuthorizationEvidence VerifyWorkflowUatAuthorization( ArtifactFile artifact, string workflow, string moduleCode, string sourceHash, string contentHash, string authorizationIdHash, string rolloutCustomerId, string accountBook, string subSystemId, string databaseScopeFingerprint, string runtimeHash, string customerProfileHash, string rolloutHash, string sourceCommit, string packageHash, string runtimeCliVersion, string runtimeCliHash, string runtimeCliSignerThumbprint, string verifierCliHash, string verifierSignerThumbprint, LegacyBuildVerificationState legacy, DateTime issuedAt, IAcceptanceSignatureVerifier signatureVerifier) { WorkflowUatAuthorizationEvidence uat; try { uat = WorkflowUatAuthorizationVerifier.VerifyHistoricalFile( artifact.Path, signatureVerifier); } catch (CommandKernelException) { throw Error( "customer_acceptance_bundle_uat_invalid", "总验收包中的独立短时 UAT 授权未通过历史签名验证。"); } string calculatedAuthorizationIdHash = WorkflowUatAuthorizationVerifier.Sha256( Encoding.UTF8.GetBytes(uat.AuthorizationId)); WorkflowUatWorkflowAuthorization workflowAuthorization; if (!string.Equals(uat.SourceSha256, sourceHash, StringComparison.Ordinal) || !string.Equals(uat.ContentSha256, contentHash, StringComparison.Ordinal) || !string.Equals(calculatedAuthorizationIdHash, authorizationIdHash, StringComparison.Ordinal) || !string.Equals(uat.CustomerId, rolloutCustomerId, StringComparison.Ordinal) || !Same(uat.AccountBook, accountBook) || !Same(uat.SubSystemId, subSystemId) || !string.Equals( uat.DatabaseScopeFingerprint, databaseScopeFingerprint, StringComparison.Ordinal) || !string.Equals(uat.RuntimeConfigurationSha256, runtimeHash, StringComparison.Ordinal) || !string.Equals(uat.CustomerProfileSha256, customerProfileHash, StringComparison.Ordinal) || !string.Equals(uat.RolloutPolicySha256, rolloutHash, StringComparison.Ordinal) || !string.Equals(uat.SourceCommit, sourceCommit, StringComparison.Ordinal) || !string.Equals(uat.PackageSha256, packageHash, StringComparison.Ordinal) || !string.Equals( uat.RuntimeCli.Version, runtimeCliVersion, StringComparison.Ordinal) || !string.Equals( uat.RuntimeCli.Sha256, runtimeCliHash, StringComparison.Ordinal) || !string.Equals( uat.RuntimeCli.SignerThumbprint, runtimeCliSignerThumbprint, StringComparison.Ordinal) || !string.Equals(uat.VerifierCli.Sha256, verifierCliHash, StringComparison.Ordinal) || !string.Equals(uat.VerifierCli.SignerThumbprint, verifierSignerThumbprint, StringComparison.Ordinal) || !string.Equals(uat.ErpExecutable.Sha256, legacy.ErpSha256, StringComparison.Ordinal) || !string.Equals(uat.ErpExecutable.SignerThumbprint, legacy.CertificateThumbprint, StringComparison.Ordinal) || issuedAt < uat.IssuedAtUtc.AddMinutes(-5) || issuedAt > uat.ExpiresAtUtc.AddMinutes(5) || uat.Workflows.Count != 1 || !uat.Workflows.TryGetValue( workflow, out workflowAuthorization) || !string.Equals( workflowAuthorization.ModuleCode, moduleCode, StringComparison.Ordinal)) { throw Error( "customer_acceptance_bundle_uat_mismatch", "独立短时 UAT 授权与对应工作流、子系统、ERP/CLI 或发布范围不一致。"); } return uat; } private static void VerifyWorkflow( WorkflowSummary summary, string workflow, ArtifactFile acceptanceArtifact, ArtifactFile writeArtifact, string accountBook, string subSystemId, string runtimeHash, string customerProfileHash, string sourceCommit, string packageHash, WorkflowUatAuthorizationEvidence uat, IAcceptanceSignatureVerifier signatureVerifier, DateTime nowUtc) { BusinessAcceptanceEvidence acceptance = BusinessAcceptanceEvidenceVerifier.VerifyFile( acceptanceArtifact.Path, signatureVerifier, nowUtc); WorkflowWriteIntegrationEvidenceResult write = WorkflowWriteIntegrationEvidenceVerifier.VerifyFile( writeArtifact.Path, workflow, summary.ModuleCode, accountBook, subSystemId, runtimeHash, sourceCommit, packageHash, nowUtc); WorkflowUatWorkflowAuthorization uatWorkflow; if (!Same(acceptance.Workflow, workflow) || !Same(acceptance.ModuleCode, summary.ModuleCode) || !Same(acceptance.AccountBook, accountBook) || !Same(acceptance.SubSystemId, subSystemId) || !string.Equals(acceptance.RuntimeConfigurationSha256, runtimeHash, StringComparison.Ordinal) || !string.Equals(acceptance.CustomerProfileSha256, customerProfileHash, StringComparison.Ordinal) || !string.Equals(acceptance.ContentSha256, summary.AcceptanceContentSha256, StringComparison.Ordinal) || !string.Equals(acceptance.EvidenceId, summary.AcceptanceEvidenceId, StringComparison.Ordinal) || !string.Equals(acceptance.WriteIntegrationEvidenceSha256, writeArtifact.Sha256, StringComparison.Ordinal) || !string.Equals(write.ContentSha256, summary.WriteIntegrationContentSha256, StringComparison.Ordinal) || !string.Equals(write.EnvironmentId, summary.EnvironmentId, StringComparison.Ordinal) || !string.Equals(write.TestedBy, summary.TestedBy, StringComparison.Ordinal) || uat == null || !uat.Workflows.TryGetValue(workflow, out uatWorkflow) || !string.Equals( uatWorkflow.ModuleCode, summary.ModuleCode, StringComparison.Ordinal) || !string.Equals( uat.EnvironmentId, write.EnvironmentId, StringComparison.Ordinal) || !string.Equals( write.UserIdSha256, WorkflowUatAuthorizationVerifier.Sha256( Encoding.UTF8.GetBytes(uat.UserId)), StringComparison.Ordinal) || !string.Equals( write.UserNameSha256, WorkflowUatAuthorizationVerifier.Sha256( Encoding.UTF8.GetBytes(uat.UserName)), StringComparison.Ordinal) || !string.Equals( write.DatabaseScopeFingerprint, uat.DatabaseScopeFingerprint, StringComparison.Ordinal) || write.IsAdministrator != AdministratorIdentity.IsBuiltIn( uat.UserId, uat.UserName) || !string.Equals( uat.SourceSha256, write.UatAuthorizationSourceSha256, StringComparison.Ordinal) || !string.Equals( uat.ContentSha256, write.UatAuthorizationContentSha256, StringComparison.Ordinal) || !string.Equals( WorkflowUatAuthorizationVerifier.Sha256( Encoding.UTF8.GetBytes(uat.AuthorizationId)), write.UatAuthorizationIdSha256, StringComparison.Ordinal) || !string.Equals( write.RuntimeCliVersion, uat.RuntimeCli.Version, StringComparison.Ordinal) || !string.Equals( write.RuntimeCliSha256, uat.RuntimeCli.Sha256, StringComparison.Ordinal) || !string.Equals( write.RuntimeCliSignerThumbprint, uat.RuntimeCli.SignerThumbprint, StringComparison.Ordinal) || write.TestedAtUtc < uat.IssuedAtUtc.AddMinutes(-5) || write.TestedAtUtc > uat.ExpiresAtUtc.AddMinutes(5) || write.UatTokenSha256ByCase == null || write.UatTokenSha256ByCase.Count != uatWorkflow.Cases.Count || uatWorkflow.Cases.Any(pair => !write.UatTokenSha256ByCase.ContainsKey(pair.Key) || !string.Equals( write.UatTokenSha256ByCase[pair.Key], pair.Value.TokenSha256, StringComparison.Ordinal)) || Math.Abs((write.TestedAtUtc - summary.TestedAtUtc).TotalSeconds) > 1 || acceptance.IssuedAtUtc < write.TestedAtUtc.AddMinutes(-5)) throw Error( "customer_acceptance_bundle_workflow_mismatch", "工作流清单、写集成证据或总包摘要不一致。"); } private static WorkflowSummary ParseWorkflowSummary(JObject value, string workflow) { EnsureExact(value, "workflow", "moduleCode", "acceptanceEvidenceId", "acceptanceContentSha256", "writeIntegrationContentSha256", "environmentId", "testedAtUtc", "testedBy"); if (RequiredString(value, "workflow", 1, 32) != workflow) throw Error("customer_acceptance_bundle_schema_invalid", "工作流摘要类型无效。"); string moduleCode = RequiredString(value, "moduleCode", 1, 64); string environmentId = RequiredString(value, "environmentId", 8, 128); string testedBy = RequiredString(value, "testedBy", 1, 128); if (!SafeModule.IsMatch(moduleCode) || !SafeIdentifier.IsMatch(environmentId) || !SafeIdentifier.IsMatch(testedBy)) throw Error("customer_acceptance_bundle_schema_invalid", "工作流摘要标识无效。"); return new WorkflowSummary { ModuleCode = moduleCode, AcceptanceEvidenceId = RequiredString( value, "acceptanceEvidenceId", 8, 128), AcceptanceContentSha256 = RequiredHash( value, "acceptanceContentSha256"), WriteIntegrationContentSha256 = RequiredHash( value, "writeIntegrationContentSha256"), EnvironmentId = environmentId, TestedAtUtc = RequiredUtc(value, "testedAtUtc"), TestedBy = testedBy }; } private static DiagnosticSummary ParseDiagnosticSummary( JObject value, string expectedScenario) { EnsureExact(value, "scenarioCode", "evidenceId", "contentSha256", "moduleCode", "primaryFindingCode", "capturedAtUtc"); string scenario = RequiredString(value, "scenarioCode", 1, 64); string moduleCode = RequiredString(value, "moduleCode", 1, 64); if (scenario != expectedScenario || !SafeModule.IsMatch(moduleCode)) throw Error("customer_acceptance_bundle_schema_invalid", "诊断场景摘要无效。"); return new DiagnosticSummary { ScenarioCode = scenario, EvidenceId = RequiredString(value, "evidenceId", 8, 64), ContentSha256 = RequiredHash(value, "contentSha256"), ModuleCode = moduleCode, PrimaryFindingCode = RequiredString(value, "primaryFindingCode", 1, 128), CapturedAtUtc = RequiredUtc(value, "capturedAtUtc") }; } private static void VerifyDiagnostic( DiagnosticSummary summary, ArtifactFile artifact, string accountBook, string subSystemId, DateTime issuedAtUtc, DiagnosticVerificationState state) { ModuleDiagnosticEvidenceVerificationResult value; try { value = ModuleDiagnosticEvidenceVerifier.VerifyFile(artifact.Path); } catch (ModuleDiagnosticEvidenceException) { throw Error( "customer_acceptance_bundle_diagnostic_invalid", "总验收包中的诊断原始证据未通过严格验证。"); } if (!Same(value.AccountBook, accountBook) || !Same(value.SubSystemId, subSystemId) || !Same(value.ModuleCode, summary.ModuleCode) || !string.Equals(value.EvidenceId, summary.EvidenceId, StringComparison.Ordinal) || !string.Equals(value.ContentHash, summary.ContentSha256, StringComparison.Ordinal) || !string.Equals(value.PrimaryFindingCode, summary.PrimaryFindingCode, StringComparison.Ordinal) || Math.Abs((value.CapturedAtUtc - summary.CapturedAtUtc).TotalSeconds) > 1 || value.Outcome != "failed" || value.ModuleOpenSucceeded || value.Truncated || value.CapturedAtUtc > issuedAtUtc.AddMinutes(5) || value.CapturedAtUtc < issuedAtUtc.AddDays(-30)) throw Error( "customer_acceptance_bundle_diagnostic_mismatch", "诊断证据与总包场景摘要、作用域或时间窗口不一致。"); if (summary.ScenarioCode == "missing_field" && value.PrimaryFindingCode != "missing_column" && value.PrimaryFindingCode != "missing_object") throw Error("customer_acceptance_bundle_diagnostic_invalid", "缺字段场景没有观察到缺列或缺对象结论。"); if (summary.ScenarioCode == "permission_denied" && value.PrimaryFindingCode != "database_permission") throw Error("customer_acceptance_bundle_diagnostic_invalid", "缺权限场景没有观察到数据库权限结论。"); if (summary.ScenarioCode == "invalid_relation" && !InvalidRelationCode(value.PrimaryFindingCode)) throw Error("customer_acceptance_bundle_diagnostic_invalid", "无效关联场景没有观察到可接受的配置或合同结论。"); if (summary.ScenarioCode == "initialization_sql_failure" && value.FailedEventCount < 1) throw Error("customer_acceptance_bundle_diagnostic_invalid", "初始化 SQL 失败场景没有捕获失败 SQL 事件。"); if (!state.EvidenceIds.Add(value.EvidenceId) || !state.ContentHashes.Add(value.ContentHash) || string.IsNullOrWhiteSpace(value.UserId) || string.IsNullOrWhiteSpace(value.UserName) || string.IsNullOrWhiteSpace(value.DatabaseScopeFingerprint)) throw Error("customer_acceptance_bundle_diagnostic_invalid", "诊断证据重复或缺少管理员身份范围。"); state.UserIds.Add(value.UserId); state.UserNames.Add(value.UserName); state.DatabaseScopeFingerprints.Add( value.DatabaseScopeFingerprint); state.ModuleCodes.Add(value.ModuleCode); } private static bool InvalidRelationCode(string code) { return code == "procedure_parameter" || code == "conversion" || code == "constraint" || code == "missing_object" || code == "missing_column" || code == "module_initialization_error" || code == "unclassified_module_error"; } private static ReadOnlySessionPreflightState VerifyReadOnlySessionPreflight( string path, DateTime expectedGeneratedAtUtc, DateTime issuedAtUtc, string databaseScopeFingerprint, string accountBook, string subSystemId, string userId, string userName, string erpSha256, string erpSignerThumbprint, string runtimeCliVersion, string runtimeCliSha256, string runtimeCliSignerThumbprint, string rolloutPolicySha256, string rolloutCustomerId, bool requirePurchase, bool requireLeave, bool requireDiagnosis, bool requireAdministrator, IEnumerable expectedModuleCodes) { try { return VerifyReadOnlySessionPreflightCore( path, expectedGeneratedAtUtc, issuedAtUtc, databaseScopeFingerprint, accountBook, subSystemId, userId, userName, erpSha256, erpSignerThumbprint, runtimeCliVersion, runtimeCliSha256, runtimeCliSignerThumbprint, rolloutPolicySha256, rolloutCustomerId, requirePurchase, requireLeave, requireDiagnosis, requireAdministrator, expectedModuleCodes); } catch (CommandKernelException error) { if (string.Equals( error.Code, "customer_acceptance_bundle_readonly_preflight_invalid", StringComparison.Ordinal)) throw; throw Error( "customer_acceptance_bundle_readonly_preflight_invalid", "指定 ERP 会话的只读动态合同预检报告无效或与最终验收范围不一致。"); } catch { throw Error( "customer_acceptance_bundle_readonly_preflight_invalid", "指定 ERP 会话的只读动态合同预检报告无效或与最终验收范围不一致。"); } } private static ReadOnlySessionPreflightState VerifyReadOnlySessionPreflightCore( string path, DateTime expectedGeneratedAtUtc, DateTime issuedAtUtc, string databaseScopeFingerprint, string accountBook, string subSystemId, string userId, string userName, string erpSha256, string erpSignerThumbprint, string runtimeCliVersion, string runtimeCliSha256, string runtimeCliSignerThumbprint, string rolloutPolicySha256, string rolloutCustomerId, bool requirePurchase, bool requireLeave, bool requireDiagnosis, bool requireAdministrator, IEnumerable expectedModuleCodes) { HashSet requiredModuleCodes = new HashSet( expectedModuleCodes ?? new string[0], StringComparer.OrdinalIgnoreCase); if (requiredModuleCodes.Count < 1 || requiredModuleCodes.Count > 16 || requiredModuleCodes.Any(item => string.IsNullOrWhiteSpace(item) || !SafeModule.IsMatch(item))) throw ReadOnlyPreflightError(); JObject report = LoadStrict( path, 2 * 1024 * 1024, "ERP 只读会话预检报告"); EnsureExact( report, "schemaVersion", "evidenceType", "generatedAtUtc", "passed", "readOnlySessionReady", "productionWriteAuthorized", "erpProcessId", "erpExecutable", "cli", "rolloutPolicy", "session", "bridge", "requestedReadiness", "capabilityReadiness", "modules", "readOnlyBoundary", "checks", "note"); DateTime generatedAt = RequiredUtc(report, "generatedAtUtc"); if (RequiredString(report, "schemaVersion", 1, 16) != "1.5" || RequiredString(report, "evidenceType", 1, 64) != "lserp_readonly_session_preflight" || !RequiredBoolean(report, "passed") || !RequiredBoolean(report, "readOnlySessionReady") || RequiredBoolean(report, "productionWriteAuthorized") || Math.Abs((generatedAt - expectedGeneratedAtUtc).TotalSeconds) > 1 || generatedAt > issuedAtUtc.AddMinutes(5) || generatedAt < issuedAtUtc.AddDays(-7)) throw ReadOnlyPreflightError(); int erpProcessId = checked((int)RequiredLong( report, "erpProcessId", 1, int.MaxValue)); JObject erpExecutable = RequiredObject(report, "erpExecutable"); EnsureExact(erpExecutable, "sha256", "signerThumbprint"); string normalizedErpSigner = WindowsTrustedPeopleSignatureVerifier .NormalizeThumbprint(RequiredString( erpExecutable, "signerThumbprint", 40, 64)); if (!string.Equals( RequiredHash(erpExecutable, "sha256"), erpSha256, StringComparison.Ordinal) || normalizedErpSigner == null || !string.Equals( normalizedErpSigner, erpSignerThumbprint, StringComparison.Ordinal)) throw ReadOnlyPreflightError(); JObject cli = RequiredObject(report, "cli"); EnsureExact( cli, "component", "version", "protocolVersion", "bridgeOnly", "databaseDirectAccess", "sessionSource", "sha256", "signerThumbprint"); string signer = WindowsTrustedPeopleSignatureVerifier .NormalizeThumbprint(RequiredString( cli, "signerThumbprint", 40, 64)); if (RequiredString(cli, "component", 1, 64) != "lserp-agent-cli" || RequiredString(cli, "version", 5, 14) != runtimeCliVersion || RequiredString(cli, "protocolVersion", 1, 16) != "1.0" || !RequiredBoolean(cli, "bridgeOnly") || RequiredBoolean(cli, "databaseDirectAccess") || RequiredString(cli, "sessionSource", 1, 64) != "current_logged_in_erp_process" || !string.Equals( RequiredHash(cli, "sha256"), runtimeCliSha256, StringComparison.Ordinal) || signer == null || !string.Equals( signer, runtimeCliSignerThumbprint, StringComparison.Ordinal)) throw ReadOnlyPreflightError(); JObject rollout = RequiredObject(report, "rolloutPolicy"); EnsureExact( rollout, "customerId", "databaseScopeFingerprint", "sourceSha256", "defaultAction", "ruleCount"); if (!string.Equals( RequiredString(rollout, "customerId", 1, 128), rolloutCustomerId, StringComparison.Ordinal) || !string.Equals( RequiredHash(rollout, "databaseScopeFingerprint"), databaseScopeFingerprint, StringComparison.Ordinal) || !string.Equals( RequiredHash(rollout, "sourceSha256"), rolloutPolicySha256, StringComparison.Ordinal) || RequiredString(rollout, "defaultAction", 1, 16) != "deny") throw ReadOnlyPreflightError(); RequiredLong(rollout, "ruleCount", 1, 128); JObject session = RequiredObject(report, "session"); EnsureExact( session, "databaseScopeFingerprint", "userIdSha256", "userNameSha256", "accountBookSha256", "subSystemIdSha256", "subSystemNameSha256", "isAdministrator", "activeModule", "openModuleCount"); bool isAdministrator = RequiredBoolean(session, "isAdministrator"); if (!string.Equals( RequiredHash(session, "databaseScopeFingerprint"), databaseScopeFingerprint, StringComparison.Ordinal) || !string.Equals( RequiredHash(session, "userIdSha256"), ScopedReadOnlyPreflightHash("user-id", userId), StringComparison.Ordinal) || (!string.IsNullOrWhiteSpace(userName) && !string.Equals( RequiredHash(session, "userNameSha256"), ScopedReadOnlyPreflightHash("user-name", userName), StringComparison.Ordinal)) || !string.Equals( RequiredHash(session, "accountBookSha256"), ScopedReadOnlyPreflightHash("account-book", accountBook), StringComparison.Ordinal) || !string.Equals( RequiredHash(session, "subSystemIdSha256"), ScopedReadOnlyPreflightHash("subsystem-id", subSystemId), StringComparison.Ordinal) || (requireAdministrator && !isAdministrator)) throw ReadOnlyPreflightError(); RequiredHash(session, "userNameSha256"); RequiredHash(session, "subSystemNameSha256"); RequiredLong(session, "openModuleCount", 0, 10000); JToken activeModuleToken = session["activeModule"]; if (activeModuleToken == null) throw ReadOnlyPreflightError(); if (activeModuleToken.Type != JTokenType.Null) { JObject activeModule = activeModuleToken as JObject; EnsureExact(activeModule, "moduleCode", "navigationCode"); if (!SafeModule.IsMatch(RequiredString( activeModule, "moduleCode", 1, 64)) || !SafeModule.IsMatch(RequiredString( activeModule, "navigationCode", 1, 64))) throw ReadOnlyPreflightError(); } JObject bridge = RequiredObject(report, "bridge"); EnsureExact( bridge, "protocolVersion", "registeredCommandCount", "enabledCommandCount"); long registeredCount = RequiredLong( bridge, "registeredCommandCount", 1, 512); long enabledCount = RequiredLong( bridge, "enabledCommandCount", 1, 512); if (RequiredString(bridge, "protocolVersion", 1, 16) != "1.0" || enabledCount > registeredCount) throw ReadOnlyPreflightError(); JObject requested = RequiredObject(report, "requestedReadiness"); EnsureExact( requested, "purchaseWorkflow", "leaveWorkflow", "diagnosisWorkflow"); if (RequiredBoolean(requested, "purchaseWorkflow") != requirePurchase || RequiredBoolean(requested, "leaveWorkflow") != requireLeave || RequiredBoolean(requested, "diagnosisWorkflow") != requireDiagnosis) throw ReadOnlyPreflightError(); Dictionary expectedCapabilities = new Dictionary(StringComparer.Ordinal) { { "module.parameters", new[] { "1.1", "1.1", "read" } }, { "purchase.invoice.resolve", new[] { "1.4", "1.4", "draft" } }, { "purchase.invoice.create", new[] { "1.4", "1.4", "write" } }, { "hr.leave.resolve", new[] { "1.4", "1.4", "draft" } }, { "hr.leave.create", new[] { "1.2", "1.2", "write" } }, { "hr.leave.submit", new[] { "1.0", "1.0", "write" } }, { "module.diagnose", new[] { "1.0", "1.0", "read" } }, { "module.trace-initialization", new[] { "1.2", "1.0", "critical" } } }; JArray capabilities = report["capabilityReadiness"] as JArray; if (capabilities == null || capabilities.Count != expectedCapabilities.Count) throw ReadOnlyPreflightError(); HashSet capabilityNames = new HashSet(StringComparer.Ordinal); foreach (JToken token in capabilities) { JObject capability = token as JObject; EnsureExact( capability, "command", "required", "available", "contractValid", "version", "schemaVersion", "risk"); string name = RequiredString(capability, "command", 1, 128); string[] expected; bool expectedRequired = CapabilityRequired( name, requirePurchase, requireLeave, requireDiagnosis); bool actualRequired = RequiredBoolean(capability, "required"); bool available = RequiredBoolean(capability, "available"); bool contractValid = RequiredBoolean(capability, "contractValid"); if (!capabilityNames.Add(name) || !expectedCapabilities.TryGetValue(name, out expected) || actualRequired != expectedRequired || (expectedRequired && (!available || !contractValid)) || (available && !contractValid) || (!available && contractValid) || !ValidateCapabilityContract( capability, expected, available)) throw ReadOnlyPreflightError(); } JArray modules = report["modules"] as JArray; if (modules == null || modules.Count < 1 || modules.Count > 16) throw ReadOnlyPreflightError(); HashSet moduleCodes = new HashSet( StringComparer.OrdinalIgnoreCase); HashSet requestedModuleCodes = new HashSet( StringComparer.OrdinalIgnoreCase); foreach (JToken token in modules) { JObject module = token as JObject; string requestedCode; string moduleCode = ValidateReadOnlyModuleEvidence( module, out requestedCode); if (!requestedModuleCodes.Add(requestedCode) || !moduleCodes.Add(moduleCode)) throw ReadOnlyPreflightError(); } if (requiredModuleCodes.Any(item => !moduleCodes.Contains(item))) throw ReadOnlyPreflightError(); JObject boundary = RequiredObject(report, "readOnlyBoundary"); EnsureExact( boundary, "allowedOperations", "commandExecuteInvoked", "workflowPlanInvoked", "directDatabaseConnectionUsed", "businessWriteAttempted", "rawBusinessValuesEmitted", "rawParameterLabelsEmitted", "temporaryModuleInputsRetained"); string[] expectedOperations = { "cli.version", "bridge.health", "bridge.context", "bridge.capabilities", "bridge.plan:module.parameters" }; JArray operations = boundary["allowedOperations"] as JArray; if (operations == null || operations.Count != expectedOperations.Length) throw ReadOnlyPreflightError(); for (int index = 0; index < expectedOperations.Length; index++) { if (operations[index].Type != JTokenType.String || operations[index].Value() != expectedOperations[index]) throw ReadOnlyPreflightError(); } foreach (string property in new[] { "commandExecuteInvoked", "workflowPlanInvoked", "directDatabaseConnectionUsed", "businessWriteAttempted", "rawBusinessValuesEmitted", "rawParameterLabelsEmitted", "temporaryModuleInputsRetained" }) { if (RequiredBoolean(boundary, property)) throw ReadOnlyPreflightError(); } string[] expectedChecks = { "cli_integrity", "cli_runtime_identity", "exact_erp_process", "default_deny_rollout", "rollout_database_scope", "expected_database_scope", "expected_session_scope", "capability_contracts", "dynamic_module_contracts", "dynamic_module_execution_contracts", "session_stability", "read_only_boundary" }; JArray checks = report["checks"] as JArray; if (checks == null || checks.Count != expectedChecks.Length) throw ReadOnlyPreflightError(); HashSet checkCodes = new HashSet( StringComparer.Ordinal); foreach (JToken token in checks) { JObject check = token as JObject; EnsureExact(check, "code", "passed"); string code = RequiredString(check, "code", 1, 128); if (!checkCodes.Add(code) || !RequiredBoolean(check, "passed")) throw ReadOnlyPreflightError(); } if (expectedChecks.Any(item => !checkCodes.Contains(item))) throw ReadOnlyPreflightError(); RequiredString(report, "note", 1, 512); return new ReadOnlySessionPreflightState { GeneratedAtUtcText = generatedAt.ToString("o", CultureInfo.InvariantCulture), GeneratedAtUtc = generatedAt, ErpProcessId = erpProcessId, ErpSha256 = erpSha256, ErpSignerThumbprint = erpSignerThumbprint, RuntimeCliVersion = runtimeCliVersion, RuntimeCliSha256 = runtimeCliSha256, RuntimeCliSignerThumbprint = runtimeCliSignerThumbprint, ModuleCount = modules.Count, IsAdministrator = isAdministrator }; } private static bool CapabilityRequired( string name, bool requirePurchase, bool requireLeave, bool requireDiagnosis) { if (name == "module.parameters") return true; if (name.StartsWith("purchase.", StringComparison.Ordinal)) return requirePurchase; if (name.StartsWith("hr.leave.", StringComparison.Ordinal)) return requireLeave; if (name == "module.diagnose" || name == "module.trace-initialization") return requireDiagnosis; return false; } private static bool ValidateCapabilityContract( JObject capability, string[] expected, bool available) { foreach (string property in new[] { "version", "schemaVersion", "risk" }) { JToken token = capability[property]; if (token == null) return false; if (!available) { if (token.Type != JTokenType.Null) return false; continue; } if (token.Type != JTokenType.String) return false; } return !available || (capability.Value("version") == expected[0] && capability.Value("schemaVersion") == expected[1] && capability.Value("risk") == expected[2]); } private static FieldReadOnlyValidationHandoffState VerifyFieldReadOnlyValidationHandoff( string path, DateTime expectedGeneratedAtUtc, DateTime issuedAtUtc, string readOnlySessionPreflightSha256, ReadOnlySessionPreflightState readOnlySession, string expectedRole, int minimumExpectedRoleCount) { try { return VerifyFieldReadOnlyValidationHandoffCore( path, expectedGeneratedAtUtc, issuedAtUtc, readOnlySessionPreflightSha256, readOnlySession, expectedRole, minimumExpectedRoleCount); } catch (CommandKernelException error) { if (string.Equals( error.Code, "customer_acceptance_bundle_field_handoff_invalid", StringComparison.Ordinal)) throw; throw FieldReadOnlyHandoffError(); } catch { throw FieldReadOnlyHandoffError(); } } private static FieldReadOnlyValidationHandoffState VerifyFieldReadOnlyValidationHandoffCore( string path, DateTime expectedGeneratedAtUtc, DateTime issuedAtUtc, string readOnlySessionPreflightSha256, ReadOnlySessionPreflightState readOnlySession, string expectedRole, int minimumExpectedRoleCount) { if ((expectedRole != "purchase" && expectedRole != "leave" && expectedRole != "diagnosis") || minimumExpectedRoleCount < 1 || minimumExpectedRoleCount > 16) throw FieldReadOnlyHandoffError(); JObject report = LoadStrict( path, 256 * 1024, "现场只读参数交接报告"); EnsureExact( report, "schemaVersion", "evidenceType", "generatedAtUtc", "passed", "validationStage", "inputSha256", "approvedBySha256", "approvedAtUtc", "approvalExpiresAtUtc", "expectedPreflightScriptSha256", "handoffContractSha256", "runtimeCli", "sessionApprovalFingerprint", "erpProcessId", "expectedIsAdministrator", "moduleBindingFingerprint", "moduleBindingCount", "uniqueModuleCount", "moduleRoleCounts", "requestedReadiness", "databaseCredentialsIncluded", "validationOnly", "executionAttempted", "preflightEvidenceProduced", "preflightEvidenceSha256", "productionWriteAuthorized", "readOnlyBoundary", "checks", "note"); DateTime generatedAt = RequiredUtc(report, "generatedAtUtc"); DateTime approvedAt = RequiredUtc(report, "approvedAtUtc"); DateTime approvalExpiresAt = RequiredUtc( report, "approvalExpiresAtUtc"); if (RequiredString(report, "schemaVersion", 1, 16) != "1.1" || RequiredString(report, "evidenceType", 1, 64) != "lserp_field_readonly_validation_handoff" || RequiredString(report, "validationStage", 1, 16) != "final" || !RequiredBoolean(report, "passed") || Math.Abs((generatedAt - expectedGeneratedAtUtc).TotalSeconds) > 1 || generatedAt > issuedAtUtc.AddMinutes(5) || generatedAt < issuedAtUtc.AddDays(-7) || generatedAt < approvedAt || generatedAt > approvalExpiresAt || approvalExpiresAt <= approvedAt || approvalExpiresAt - approvedAt > TimeSpan.FromHours(24) || generatedAt < readOnlySession.GeneratedAtUtc.AddSeconds(-1) || checked((int)RequiredLong( report, "erpProcessId", 1, int.MaxValue)) != readOnlySession.ErpProcessId || RequiredBoolean(report, "expectedIsAdministrator") != readOnlySession.IsAdministrator || RequiredBoolean(report, "databaseCredentialsIncluded") || RequiredBoolean(report, "validationOnly") || !RequiredBoolean(report, "executionAttempted") || !RequiredBoolean(report, "preflightEvidenceProduced") || RequiredBoolean(report, "productionWriteAuthorized")) throw FieldReadOnlyHandoffError(); JObject runtimeCli = RequiredObject(report, "runtimeCli"); EnsureExact( runtimeCli, "component", "version", "sha256", "signerThumbprint"); string runtimeSigner = WindowsTrustedPeopleSignatureVerifier .NormalizeThumbprint(RequiredString( runtimeCli, "signerThumbprint", 40, 64)); if (RequiredString(runtimeCli, "component", 1, 64) != "lserp-agent-cli" || RequiredString(runtimeCli, "version", 5, 14) != readOnlySession.RuntimeCliVersion || RequiredHash(runtimeCli, "sha256") != readOnlySession.RuntimeCliSha256 || runtimeSigner == null || runtimeSigner != readOnlySession.RuntimeCliSignerThumbprint) throw FieldReadOnlyHandoffError(); string zeroHash = new string('0', 64); foreach (string property in new[] { "inputSha256", "approvedBySha256", "expectedPreflightScriptSha256", "handoffContractSha256", "sessionApprovalFingerprint", "moduleBindingFingerprint" }) { if (RequiredHash(report, property) == zeroHash) throw FieldReadOnlyHandoffError(); } if (!string.Equals( RequiredHash(report, "preflightEvidenceSha256"), readOnlySessionPreflightSha256, StringComparison.Ordinal)) throw FieldReadOnlyHandoffError(); long bindingCount = RequiredLong( report, "moduleBindingCount", 1, 16); long uniqueModuleCount = RequiredLong( report, "uniqueModuleCount", 1, 16); JObject roleCounts = RequiredObject(report, "moduleRoleCounts"); EnsureExact(roleCounts, "purchase", "leave", "diagnosis", "support"); long purchaseCount = RequiredLong(roleCounts, "purchase", 0, 16); long leaveCount = RequiredLong(roleCounts, "leave", 0, 16); long diagnosisCount = RequiredLong(roleCounts, "diagnosis", 0, 16); long supportCount = RequiredLong(roleCounts, "support", 0, 16); if (uniqueModuleCount != readOnlySession.ModuleCount || purchaseCount + leaveCount + diagnosisCount + supportCount != bindingCount || (expectedRole == "purchase" ? purchaseCount < minimumExpectedRoleCount || leaveCount != 0 || diagnosisCount != 0 : expectedRole == "leave" ? leaveCount < minimumExpectedRoleCount || purchaseCount != 0 || diagnosisCount != 0 : diagnosisCount < minimumExpectedRoleCount || purchaseCount != 0 || leaveCount != 0)) throw FieldReadOnlyHandoffError(); JObject requested = RequiredObject(report, "requestedReadiness"); EnsureExact( requested, "purchaseWorkflow", "leaveWorkflow", "diagnosisWorkflow"); if (RequiredBoolean(requested, "purchaseWorkflow") != (expectedRole == "purchase") || RequiredBoolean(requested, "leaveWorkflow") != (expectedRole == "leave") || RequiredBoolean(requested, "diagnosisWorkflow") != (expectedRole == "diagnosis")) throw FieldReadOnlyHandoffError(); JObject boundary = RequiredObject(report, "readOnlyBoundary"); string[] boundaryFields = { "directDatabaseConnectionUsed", "databaseCredentialAccepted", "businessWriteAttempted", "rawSessionValuesEmitted", "rawModuleCodesEmitted", "preflightExecuteCommandAllowed" }; EnsureExact(boundary, boundaryFields); foreach (string property in boundaryFields) { if (RequiredBoolean(boundary, property)) throw FieldReadOnlyHandoffError(); } string[] expectedChecks = { "strict_json", "input_integrity", "independent_approval_window", "database_credentials_absent", "exact_session_expectations", "dynamic_module_role_bindings", "preflight_source_integrity", "readonly_parameter_handoff" }; JArray checks = report["checks"] as JArray; if (checks == null || checks.Count != expectedChecks.Length) throw FieldReadOnlyHandoffError(); HashSet checkCodes = new HashSet( StringComparer.Ordinal); foreach (JToken token in checks) { JObject check = token as JObject; EnsureExact(check, "code", "passed"); string code = RequiredString(check, "code", 1, 128); if (!checkCodes.Add(code) || !RequiredBoolean(check, "passed")) throw FieldReadOnlyHandoffError(); } if (expectedChecks.Any(item => !checkCodes.Contains(item))) throw FieldReadOnlyHandoffError(); RequiredString(report, "note", 1, 512); return new FieldReadOnlyValidationHandoffState { GeneratedAtUtcText = generatedAt.ToString( "o", CultureInfo.InvariantCulture) }; } private static CommandKernelException FieldReadOnlyHandoffError() { return Error( "customer_acceptance_bundle_field_handoff_invalid", "现场只读参数交接报告无效,或未绑定同一个最终只读会话预检。" ); } private static string ValidateReadOnlyModuleEvidence( JObject module, out string requestedCode) { EnsureExact( module, "requestedCode", "moduleCode", "moduleKind", "contractSource", "sessionScopeBound", "configurationFingerprint", "contractFingerprint", "masterParameterCount", "detailParameterCount", "inputModeCounts", "editableParameterCount", "requiredParameterCount", "dedicatedAdapterParameterCount", "nativeExecutionProfiles", "payloadContract", "genericWriteExecutionAvailable", "writeExecutionBlocked", "writeCommand", "writeReadinessCode", "genericUpdateExecutionAvailable", "updateExecutionBlocked", "updateCommand", "updateReadinessCode", "planExecutionAllowed"); requestedCode = RequiredString(module, "requestedCode", 1, 64); string moduleCode = RequiredString(module, "moduleCode", 1, 64); string moduleKind = RequiredString(module, "moduleKind", 1, 32); if (!SafeModule.IsMatch(requestedCode) || !SafeModule.IsMatch(moduleCode) || (moduleKind != "document" && moduleKind != "master_data") || RequiredString(module, "contractSource", 1, 128) != "current_erp_database_low_code_configuration" || !RequiredBoolean(module, "sessionScopeBound") || RequiredBoolean(module, "planExecutionAllowed")) throw ReadOnlyPreflightError(); RequiredHash(module, "configurationFingerprint"); RequiredHash(module, "contractFingerprint"); long masterCount = RequiredLong( module, "masterParameterCount", 0, 512); long detailCount = RequiredLong( module, "detailParameterCount", 0, 512); long totalCount = masterCount + detailCount; if (totalCount > 512 || (moduleKind == "master_data" && detailCount != 0)) throw ReadOnlyPreflightError(); JObject inputModes = RequiredObject(module, "inputModeCounts"); EnsureExact(inputModes, "scalar", "lookupSingle", "unsupported"); long scalar = RequiredLong(inputModes, "scalar", 0, totalCount); long lookup = RequiredLong( inputModes, "lookupSingle", 0, totalCount); long unsupported = RequiredLong( inputModes, "unsupported", 0, totalCount); long editable = RequiredLong( module, "editableParameterCount", 0, totalCount); long required = RequiredLong( module, "requiredParameterCount", 0, totalCount); long dedicated = RequiredLong( module, "dedicatedAdapterParameterCount", 0, totalCount); if (scalar + lookup + unsupported != totalCount || required > editable || editable > scalar + lookup || dedicated != unsupported) throw ReadOnlyPreflightError(); JObject nativeProfiles = RequiredObject( module, "nativeExecutionProfiles"); EnsureExact(nativeProfiles, "create", "update"); bool createNativeAvailable = ValidateReadOnlyNativeExecutionProfile( RequiredObject(nativeProfiles, "create"), "create", moduleKind); bool updateNativeAvailable = ValidateReadOnlyNativeExecutionProfile( RequiredObject(nativeProfiles, "update"), "update", moduleKind); ValidateReadOnlyParameterPayload( RequiredObject(module, "payloadContract"), moduleKind, masterCount, detailCount, editable, required); ValidateReadOnlyExecutionReadiness( module, "write", "genericWriteExecutionAvailable", "writeExecutionBlocked", "writeCommand", "writeReadinessCode", "module.record.create", "dynamic_module_write_ready", createNativeAvailable, moduleKind); ValidateReadOnlyExecutionReadiness( module, "update", "genericUpdateExecutionAvailable", "updateExecutionBlocked", "updateCommand", "updateReadinessCode", "module.record.resolve-update", "dynamic_module_update_ready", updateNativeAvailable, moduleKind); return moduleCode; } private static bool ValidateReadOnlyNativeExecutionProfile( JObject profile, string action, string moduleKind) { EnsureExact( profile, "available", "nativeSaveFamily", "profileFingerprint", "code"); bool available = RequiredBoolean(profile, "available"); JToken familyToken = profile["nativeSaveFamily"]; JToken fingerprintToken = profile["profileFingerprint"]; JToken codeToken = profile["code"]; if (familyToken == null || fingerprintToken == null || codeToken == null) throw ReadOnlyPreflightError(); if (available) { string family = RequiredString( profile, "nativeSaveFamily", 1, 128); RequiredHash(profile, "profileFingerprint"); if (codeToken.Type != JTokenType.Null || !AllowedReadOnlyNativeSaveFamily(moduleKind, family) || (action == "update" && moduleKind == "document")) throw ReadOnlyPreflightError(); return true; } if (familyToken.Type != JTokenType.Null || fingerprintToken.Type != JTokenType.Null) throw ReadOnlyPreflightError(); string code = RequiredString(profile, "code", 1, 128); if (!SafeReadinessCode.IsMatch(code) || (action == "update" && moduleKind == "document" && code != "dynamic_module_update_bill_unsupported")) throw ReadOnlyPreflightError(); return false; } private static bool AllowedReadOnlyNativeSaveFamily( string moduleKind, string family) { if (moduleKind == "document") return family == "legacy.bill-save.p-bill-save-pr3" || family == "legacy.bill-save.p-bill-save-pr70"; return family == "legacy.base-save.p-base-save" || family == "legacy.base-save.p-base-save70"; } private static void ValidateReadOnlyParameterPayload( JObject payload, string moduleKind, long masterCount, long detailCount, long editableCount, long requiredCount) { EnsureExact( payload, "format", "valueEncoding", "moduleCodeRequired", "masterValuesRequired", "detailRowsRequired", "minimumDetailRows", "maximumDetailRows", "masterParameterIdCount", "requiredMasterParameterIdCount", "detailParameterIdCount", "requiredDetailParameterIdCount", "unknownParameterPolicy", "duplicateParameterPolicy", "lookupPolicy", "fieldConstraintPolicy", "configurationDriftPolicy"); bool document = moduleKind == "document"; long masterIds = RequiredLong( payload, "masterParameterIdCount", 0, masterCount); long requiredMasterIds = RequiredLong( payload, "requiredMasterParameterIdCount", 0, masterCount); long detailIds = RequiredLong( payload, "detailParameterIdCount", 0, detailCount); long requiredDetailIds = RequiredLong( payload, "requiredDetailParameterIdCount", 0, detailCount); if (RequiredString(payload, "format", 1, 64) != "parameter_entries_v1" || RequiredString(payload, "valueEncoding", 1, 64) != "invariant_text" || !RequiredBoolean(payload, "moduleCodeRequired") || !RequiredBoolean(payload, "masterValuesRequired") || RequiredBoolean(payload, "detailRowsRequired") != document || RequiredLong(payload, "minimumDetailRows", 0, 1) != (document ? 1 : 0) || RequiredLong(payload, "maximumDetailRows", 0, 1000) != (document ? 1000 : 0) || requiredMasterIds > masterIds || requiredDetailIds > detailIds || masterIds + detailIds != editableCount || requiredMasterIds + requiredDetailIds != requiredCount || (!document && (detailIds != 0 || requiredDetailIds != 0)) || RequiredString(payload, "unknownParameterPolicy", 1, 64) != "reject" || RequiredString(payload, "duplicateParameterPolicy", 1, 64) != "reject" || RequiredString(payload, "lookupPolicy", 1, 128) != "server_resolve_unique_or_stop" || RequiredString(payload, "fieldConstraintPolicy", 1, 128) != "server_enforced_from_current_low_code_configuration" || RequiredString(payload, "configurationDriftPolicy", 1, 64) != "reject_and_replan") throw ReadOnlyPreflightError(); } private static void ValidateReadOnlyExecutionReadiness( JObject module, string action, string availableName, string blockedName, string commandName, string readinessName, string expectedCommand, string expectedReadyCode, bool nativeAvailable, string moduleKind) { bool available = RequiredBoolean(module, availableName); if (RequiredBoolean(module, blockedName) == available) throw ReadOnlyPreflightError(); ValidateNullableReadinessCommand( module, commandName, available, expectedCommand); string readinessCode = RequiredString( module, readinessName, 1, 128); if (!SafeReadinessCode.IsMatch(readinessCode) || (available && (!nativeAvailable || readinessCode != expectedReadyCode)) || (!available && readinessCode == expectedReadyCode) || (action == "update" && moduleKind == "document" && (available || readinessCode != "dynamic_module_update_bill_unsupported"))) throw ReadOnlyPreflightError(); } private static void ValidateNullableReadinessCommand( JObject source, string name, bool available, string expectedCommand) { JToken token = source[name]; if (token == null || (available && (token.Type != JTokenType.String || token.Value() != expectedCommand)) || (!available && token.Type != JTokenType.Null)) throw ReadOnlyPreflightError(); } private static string ScopedReadOnlyPreflightHash( string scope, string value) { return Sha256(Encoding.UTF8.GetBytes( "lserp-readonly-preflight-v1|" + (scope ?? string.Empty) + "|" + (value ?? string.Empty))); } private static CommandKernelException ReadOnlyPreflightError() { return Error( "customer_acceptance_bundle_readonly_preflight_invalid", "指定 ERP 会话的只读动态合同预检报告无效或与最终验收范围不一致。"); } private static PreflightVerificationState VerifyPreflight( string path, string sourceCommit, DateTime expectedGeneratedAt, DateTime issuedAtUtc) { JObject report = LoadStrict(path, 4 * 1024 * 1024, "商用预检报告"); EnsureExact(report, "schemaVersion", "generatedAtUtc", "passed", "packageManifestVerified", "packageSha256", "packageSourceCommit", "legacyBuildEvidenceSha256", "rolloutPolicySha256", "rolloutCustomerId", "spriteSha256", "spriteLicenseEvidenceSha256", "astrBotComplianceEvidenceSha256", "miniMaxServiceComplianceEvidenceSha256", "miniMaxIntegrationMode", "miniMaxVisionProbeEvidenceSha256", "miniMaxVisionProbeObservedAtUtc", "miniMaxVisionProbeRegion", "miniMaxVisionProbeContractVersion", "checks", "nextStep"); DateTime generatedAt = RequiredUtc(report, "generatedAtUtc"); if (RequiredString(report, "schemaVersion", 1, 16) != "1.7" || !RequiredBoolean(report, "passed") || !RequiredBoolean(report, "packageManifestVerified") || !Same(RequiredString(report, "packageSourceCommit", 40, 40), sourceCommit) || Math.Abs((generatedAt - expectedGeneratedAt).TotalSeconds) > 1 || generatedAt > issuedAtUtc.AddMinutes(5) || generatedAt < issuedAtUtc.AddDays(-7)) throw Error("customer_acceptance_bundle_preflight_invalid", "商用预检报告未通过或来源不一致。"); string packageHash = RequiredHash(report, "packageSha256"); string legacyHash = RequiredHash(report, "legacyBuildEvidenceSha256"); string rolloutHash = RequiredHash(report, "rolloutPolicySha256"); string rolloutCustomerId = RequiredString( report, "rolloutCustomerId", 1, 64); if (!SafeIdentifier.IsMatch(rolloutCustomerId)) throw Error("customer_acceptance_bundle_preflight_invalid", "商用预检客户部署标识无效。"); RequiredHash(report, "spriteSha256"); RequiredHash(report, "spriteLicenseEvidenceSha256"); RequiredHash(report, "astrBotComplianceEvidenceSha256"); RequiredHash(report, "miniMaxServiceComplianceEvidenceSha256"); if (RequiredString(report, "miniMaxIntegrationMode", 1, 64) != "direct_https_vlm") throw Error("customer_acceptance_bundle_preflight_invalid", "商用预检 MiniMax 集成模式不是受审的 HTTPS VLM 直连模式。"); string miniMaxProbeHash = RequiredHash( report, "miniMaxVisionProbeEvidenceSha256"); string miniMaxProbeObservedAtText = RequiredString( report, "miniMaxVisionProbeObservedAtUtc", 20, 64); DateTime miniMaxProbeObservedAt = RequiredProbeUtc( miniMaxProbeObservedAtText, "miniMaxVisionProbeObservedAtUtc"); string miniMaxProbeRegion = RequiredString( report, "miniMaxVisionProbeRegion", 2, 16); string miniMaxProbeContractVersion = RequiredString( report, "miniMaxVisionProbeContractVersion", 1, 16); if ((miniMaxProbeRegion != "cn" && miniMaxProbeRegion != "global") || miniMaxProbeContractVersion != "0.0.4" || miniMaxProbeObservedAt > generatedAt.AddMinutes(5) || miniMaxProbeObservedAt < generatedAt.AddHours(-24)) throw Error("customer_acceptance_bundle_preflight_invalid", "商用预检 MiniMax 在线视觉探针区域、合同或时效无效。"); JArray checks = report["checks"] as JArray; if (checks == null || checks.Count < RequiredPreflightChecks.Length || checks.Count > 64) throw Error("customer_acceptance_bundle_preflight_invalid", "商用预检检查项数量无效。"); HashSet names = new HashSet(StringComparer.Ordinal); foreach (JToken token in checks) { JObject check = token as JObject; EnsureExact(check, "name", "passed", "code", "detail"); string name = RequiredString(check, "name", 1, 128); if (!names.Add(name) || !RequiredBoolean(check, "passed") || RequiredString(check, "code", 1, 128) != "ok") throw Error("customer_acceptance_bundle_preflight_invalid", "商用预检包含重复或失败检查项。"); RequiredString(check, "detail", 1, 1000); } if (RequiredPreflightChecks.Any(item => !names.Contains(item))) throw Error("customer_acceptance_bundle_preflight_invalid", "商用预检缺少当前版本必需门禁。"); RequiredString(report, "nextStep", 1, 500); return new PreflightVerificationState { PackageSha256 = packageHash, LegacyBuildEvidenceSha256 = legacyHash, RolloutPolicySha256 = rolloutHash, RolloutCustomerId = rolloutCustomerId, MiniMaxVisionProbeEvidenceSha256 = miniMaxProbeHash, MiniMaxVisionProbeObservedAtUtc = miniMaxProbeObservedAt, MiniMaxVisionProbeRegion = miniMaxProbeRegion, MiniMaxVisionProbeContractVersion = miniMaxProbeContractVersion }; } private static MiniMaxVisionProbeState VerifyMiniMaxVisionProbe( string path, DateTime preflightGeneratedAtUtc, DateTime issuedAtUtc) { JObject report = LoadStrict(path, 64 * 1024, "MiniMax 在线视觉探针报告"); EnsureExact(report, "schemaVersion", "observedAtUtc", "passed", "region", "endpoint", "contract", "syntheticSourceSha256", "result"); if (RequiredString(report, "schemaVersion", 1, 16) != "1.0" || !RequiredBoolean(report, "passed")) throw Error("customer_acceptance_bundle_minimax_probe_invalid", "MiniMax 在线视觉探针没有通过当前报告合同。"); string observedAtText = RequiredString( report, "observedAtUtc", 20, 64); if (!Regex.IsMatch( observedAtText, @"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?\+00:00$", RegexOptions.CultureInvariant)) throw Error("customer_acceptance_bundle_minimax_probe_invalid", "MiniMax 在线视觉探针原始时间格式不是探针生成器合同。"); DateTime observedAt = RequiredProbeUtc(observedAtText, "observedAtUtc"); string region = RequiredString(report, "region", 2, 16); string endpoint = RequiredString(report, "endpoint", 1, 256); string expectedEndpoint = region == "cn" ? "https://api.minimaxi.com/v1/coding_plan/vlm" : region == "global" ? "https://api.minimax.io/v1/coding_plan/vlm" : string.Empty; if (expectedEndpoint.Length == 0 || !string.Equals(endpoint, expectedEndpoint, StringComparison.Ordinal) || observedAt > preflightGeneratedAtUtc.AddMinutes(5) || observedAt < preflightGeneratedAtUtc.AddHours(-24) || observedAt > issuedAtUtc.AddMinutes(5)) throw Error("customer_acceptance_bundle_minimax_probe_invalid", "MiniMax 在线视觉探针区域、端点或 24 小时时效无效。"); JObject contract = RequiredObject(report, "contract"); EnsureExact(contract, "component", "version", "sourceCommit", "clientSourceSha256", "serverSourceSha256", "apiSourceHeader"); string version = RequiredString(contract, "version", 1, 16); if (RequiredString(contract, "component", 1, 64) != "minimax-coding-plan-mcp" || version != "0.0.4" || RequiredString(contract, "sourceCommit", 40, 40) != "fbac3b3e56922a1249e00eebe07d9ee68f4768dc" || RequiredHash(contract, "clientSourceSha256") != "08d4116a20e8a652ceb9e2b6f58b1e7cdfe464b14baff05977e08b4b05b66be3" || RequiredHash(contract, "serverSourceSha256") != "1dea28d6ba4ee46ba516d7eeedd325a5a102410bb7abb074fc4b0a8a66571864" || RequiredString(contract, "apiSourceHeader", 1, 64) != "Minimax-MCP" || RequiredHash(report, "syntheticSourceSha256") != "d37476a5273821c12ee4a72b512dc152db5729055b6febb8603985f86243abda") throw Error("customer_acceptance_bundle_minimax_probe_invalid", "MiniMax 在线视觉探针来源或固定合成图片合同无效。"); JObject result = RequiredObject(report, "result"); EnsureExact(result, "schemaVersion", "documentType", "lineCount", "uncertainFieldCount", "contentSha256"); string documentType = RequiredString(result, "documentType", 1, 32); if (RequiredString(result, "schemaVersion", 1, 16) != "1.0" || (documentType != "purchase_invoice" && documentType != "purchase_detail" && documentType != "unknown")) throw Error("customer_acceptance_bundle_minimax_probe_invalid", "MiniMax 在线视觉探针结果合同无效。"); RequiredLong(result, "lineCount", 0, 10000); RequiredLong(result, "uncertainFieldCount", 0, 10000); RequiredHash(result, "contentSha256"); return new MiniMaxVisionProbeState { ObservedAtUtcText = observedAtText, ObservedAtUtc = observedAt, Region = region, ContractVersion = version }; } private static LegacyBuildVerificationState VerifyLegacyBuildEvidence( string path, string sourceCommit) { JObject evidence = LoadStrict(path, 8 * 1024 * 1024, "旧 ERP 构建证据"); if (RequiredString(evidence, "schemaVersion", 1, 16) != "1.0" || !RequiredBoolean(evidence, "buildVerified") || RequiredBoolean(evidence, "releaseReadiness") || RequiredBoolean(evidence, "sourceWorktreeDirty") || !Same(RequiredString(evidence, "sourceCommit", 40, 40), sourceCommit) || RequiredString(evidence, "configuration", 1, 32) != "Release" || RequiredString(evidence, "platform", 1, 16) != "x86" || RequiredString(evidence, "targetFramework", 1, 16) != "v4.0" || RequiredString(evidence, "devExpressContract", 1, 16) != "15.2") throw Error("customer_acceptance_bundle_legacy_invalid", "旧 ERP 构建证据未绑定干净的相同源码提交。"); JObject authenticode = RequiredObject(evidence, "authenticode"); string certificateThumbprint = WindowsTrustedPeopleSignatureVerifier.NormalizeThumbprint( RequiredString( authenticode, "certificateThumbprint", 40, 64)); if (!RequiredBoolean(authenticode, "signed") || certificateThumbprint == null) throw Error("customer_acceptance_bundle_legacy_invalid", "旧 ERP 构建证据没有有效签名声明。"); JArray signedFiles = authenticode["files"] as JArray; if (signedFiles == null || signedFiles.Count(token => token.Type == JTokenType.String && string.Equals( token.Value(), "lserp-cli.exe", StringComparison.OrdinalIgnoreCase)) != 1 || signedFiles.Count(token => token.Type == JTokenType.String && string.Equals( token.Value(), "Ls_ERP.exe", StringComparison.OrdinalIgnoreCase)) != 1) { throw Error( "customer_acceptance_bundle_legacy_invalid", "旧 ERP 构建证据未声明签署最终 ERP 与 CLI。"); } JArray files = evidence["files"] as JArray; string cliSha256 = null; string erpSha256 = null; if (files == null || files.Count <= 0 || files.Count > 2000) throw Error("customer_acceptance_bundle_legacy_invalid", "旧 ERP 构建证据文件清单无效。"); foreach (JToken token in files) { JObject file = token as JObject; if (file == null) throw Error("customer_acceptance_bundle_legacy_invalid", "旧 ERP 构建证据文件清单无效。"); EnsureExact(file, "path", "sizeBytes", "sha256"); string relative = RequiredString(file, "path", 1, 256); RequiredLong(file, "sizeBytes", 1, 512L * 1024L * 1024L); string hash = RequiredHash(file, "sha256"); if (string.Equals( relative, "Runtime/lserp-cli.exe", StringComparison.OrdinalIgnoreCase)) { if (cliSha256 != null) throw Error("customer_acceptance_bundle_legacy_invalid", "旧 ERP 构建证据包含重复 CLI。"); cliSha256 = hash; } if (string.Equals( relative, "Runtime/Ls_ERP.exe", StringComparison.OrdinalIgnoreCase)) { if (erpSha256 != null) throw Error( "customer_acceptance_bundle_legacy_invalid", "旧 ERP 构建证据包含重复 ERP 主程序。"); erpSha256 = hash; } } if (cliSha256 == null || erpSha256 == null) throw Error( "customer_acceptance_bundle_legacy_invalid", "旧 ERP 构建证据缺少最终 ERP 或 CLI 哈希。"); return new LegacyBuildVerificationState { CliSha256 = cliSha256, ErpSha256 = erpSha256, CertificateThumbprint = certificateThumbprint }; } private static Dictionary ResolveArtifacts( JObject values, string rootPath) { Dictionary result = new Dictionary(StringComparer.Ordinal); HashSet names = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (string artifactName in ArtifactNames) { JObject value = RequiredObject(values, artifactName); EnsureExact(value, "fileName", "sizeBytes", "sha256"); string fileName = RequiredString(value, "fileName", 1, 128); if (!SafeFileName.IsMatch(fileName) || fileName == "." || fileName == ".." || Path.GetFileName(fileName) != fileName || !names.Add(fileName)) throw Error("customer_acceptance_bundle_artifact_invalid", "总验收包制品文件名无效或重复。"); long maximum = MaximumArtifactBytes(artifactName); long expectedSize = RequiredLong(value, "sizeBytes", 1, maximum); string expectedHash = RequiredHash(value, "sha256"); string fullPath = Path.GetFullPath(Path.Combine(rootPath, fileName)); if (!fullPath.StartsWith(rootPath, StringComparison.OrdinalIgnoreCase)) throw Error("customer_acceptance_bundle_artifact_invalid", "总验收包制品路径越界。"); FileInfo file = new FileInfo(fullPath); if (!file.Exists || file.Length != expectedSize || file.Length > maximum || (file.Attributes & FileAttributes.ReparsePoint) != 0) throw Error("customer_acceptance_bundle_artifact_invalid", "总验收包制品缺失、大小变化或为链接文件。"); string actualHash = Sha256File(fullPath); if (!string.Equals(expectedHash, actualHash, StringComparison.Ordinal)) throw Error("customer_acceptance_bundle_artifact_hash_mismatch", "总验收包制品 SHA-256 不一致。"); result[artifactName] = new ArtifactFile { Path = fullPath, Sha256 = actualHash, SizeBytes = expectedSize }; } return result; } private static long MaximumArtifactBytes(string name) { if (name == "package") return 4L * 1024L * 1024L * 1024L; if (name == "runtimeConfiguration") return 64L * 1024L; if (name == "rolloutPolicy") return CommandRolloutPolicy.MaximumBytes; if (name == "commercialPreflight") return 4L * 1024L * 1024L; if (name.EndsWith( "ReadOnlySessionPreflight", StringComparison.Ordinal)) return 2L * 1024L * 1024L; if (name.EndsWith( "FieldReadOnlyValidationHandoff", StringComparison.Ordinal)) return 256L * 1024L; if (name == "miniMaxVisionProbe") return 64L * 1024L; if (name == "legacyBuildEvidence") return 8L * 1024L * 1024L; if (name.EndsWith("Acceptance", StringComparison.Ordinal)) return 256L * 1024L; if (name.EndsWith("WriteIntegration", StringComparison.Ordinal)) return 4L * 1024L * 1024L; return 2L * 1024L * 1024L; } private static string GetEvidenceRoot(string path) { if (string.IsNullOrWhiteSpace(path)) throw Error("customer_acceptance_bundle_input_required", "请提供证据根目录。"); try { DirectoryInfo directory = new DirectoryInfo(Path.GetFullPath(path)); if (!directory.Exists || (directory.Attributes & FileAttributes.ReparsePoint) != 0) throw Error("customer_acceptance_bundle_input_invalid", "证据根目录不存在或为链接目录。"); return directory.FullName.TrimEnd( Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar; } catch (CommandKernelException) { throw; } catch { throw Error("customer_acceptance_bundle_input_invalid", "证据根目录无效。"); } } private static JObject LoadStrict(string path, int maximumBytes, string label) { if (string.IsNullOrWhiteSpace(path)) throw Error("customer_acceptance_bundle_input_required", "请提供" + label + "文件。"); try { FileInfo file = new FileInfo(Path.GetFullPath(path)); if (!file.Exists || file.Length <= 0 || file.Length > maximumBytes || (file.Attributes & FileAttributes.ReparsePoint) != 0) throw Error("customer_acceptance_bundle_input_invalid", label + "必须是大小受限的非空普通文件。"); using (FileStream stream = new FileStream( file.FullName, FileMode.Open, FileAccess.Read, FileShare.Read)) using (StreamReader text = new StreamReader( stream, new UTF8Encoding(false, true), true, 4096)) using (RejectCommentsJsonReader json = new RejectCommentsJsonReader(text)) { json.DateParseHandling = DateParseHandling.None; JObject value = JObject.Load(json, new JsonLoadSettings { DuplicatePropertyNameHandling = DuplicatePropertyNameHandling.Error, CommentHandling = CommentHandling.Ignore, LineInfoHandling = LineInfoHandling.Ignore }); if (json.Read()) throw Error("customer_acceptance_bundle_json_invalid", label + "包含多个 JSON 根值。"); return value; } } catch (CommandKernelException) { throw; } catch { throw Error("customer_acceptance_bundle_json_invalid", label + "不是严格 UTF-8 JSON 对象。"); } } private static void AssertHash(ArtifactFile artifact, string expected, string label) { if (!string.Equals(artifact.Sha256, expected, StringComparison.Ordinal)) throw Error("customer_acceptance_bundle_artifact_hash_mismatch", label + "与总包绑定哈希不一致。"); } private static void RequireExpected(string actual, string expected, string label) { if (!string.IsNullOrWhiteSpace(expected) && !string.Equals(actual, expected, StringComparison.OrdinalIgnoreCase)) throw Error("customer_acceptance_bundle_scope_mismatch", "总验收包与预期" + label + "不一致。"); } private static bool Same(string left, string right) { return string.Equals(left ?? string.Empty, right ?? string.Empty, StringComparison.OrdinalIgnoreCase); } private static JObject RequiredObject(JObject source, string name) { JObject value = source[name] as JObject; if (value == null) throw Error("customer_acceptance_bundle_schema_invalid", name + " 必须是对象。"); return value; } private static string RequiredString(JObject source, string name, int minimum, int maximum) { JToken token = source[name]; if (token == null || token.Type != JTokenType.String) throw Error("customer_acceptance_bundle_schema_invalid", name + " 必须是字符串。"); string value = token.Value(); if (value == null || value.Length < minimum || value.Length > maximum || value.Any(char.IsControl)) throw Error("customer_acceptance_bundle_schema_invalid", name + " 文本格式无效。"); return value; } private static string RequiredHash(JObject source, string name) { string value = RequiredString(source, name, 64, 64).ToLowerInvariant(); if (!Sha256Pattern.IsMatch(value)) throw Error("customer_acceptance_bundle_schema_invalid", name + " 必须是小写 SHA-256。"); return value; } private static bool RequiredBoolean(JObject source, string name) { JToken token = source[name]; if (token == null || token.Type != JTokenType.Boolean) throw Error("customer_acceptance_bundle_schema_invalid", name + " 必须是布尔值。"); return token.Value(); } private static long RequiredLong(JObject source, string name, long minimum, long maximum) { JToken token = source[name]; long value; if (token == null || token.Type != JTokenType.Integer || !long.TryParse(token.ToString(Formatting.None), NumberStyles.None, CultureInfo.InvariantCulture, out value) || value < minimum || value > maximum) throw Error("customer_acceptance_bundle_schema_invalid", name + " 整数格式无效。"); return value; } private static DateTime RequiredUtc(JObject source, string name) { string text = RequiredString(source, name, 20, 64); DateTime value; if (!DateTime.TryParse(text, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out value) || value.Kind != DateTimeKind.Utc) throw Error("customer_acceptance_bundle_schema_invalid", name + " 必须是 UTC ISO 8601 时间。"); return value; } private static DateTime RequiredProbeUtc(string text, string name) { DateTimeOffset value; if (string.IsNullOrEmpty(text) || !Regex.IsMatch( text, @"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,7})?(?:Z|[+-]\d{2}:\d{2})$", RegexOptions.CultureInvariant) || !DateTimeOffset.TryParse( text, CultureInfo.InvariantCulture, DateTimeStyles.None, out value)) throw Error("customer_acceptance_bundle_schema_invalid", name + " 必须是精确 UTC ISO 8601 时间。"); return value.UtcDateTime; } private static void EnsureExact(JObject source, params string[] names) { if (source == null || source.Properties().Count() != names.Length || names.Any(name => source.Property(name, StringComparison.Ordinal) == null)) throw Error("customer_acceptance_bundle_schema_invalid", "总验收包包含缺失或未知字段。"); } private static string Sha256File(string path) { using (FileStream stream = new FileStream( path, FileMode.Open, FileAccess.Read, FileShare.Read)) using (SHA256 sha = SHA256.Create()) { return BitConverter.ToString(sha.ComputeHash(stream)) .Replace("-", string.Empty).ToLowerInvariant(); } } private static string Sha256(byte[] value) { using (SHA256 sha = SHA256.Create()) { return BitConverter.ToString(sha.ComputeHash(value)) .Replace("-", string.Empty).ToLowerInvariant(); } } 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 result = base.Read(); if (result && TokenType == JsonToken.Comment) throw Error("customer_acceptance_bundle_json_invalid", "总验收包不允许 JSON 注释。"); return result; } } private sealed class ArtifactFile { public string Path { get; set; } public string Sha256 { get; set; } public long SizeBytes { get; set; } } private sealed class WorkflowSummary { public string ModuleCode { get; set; } public string AcceptanceEvidenceId { get; set; } public string AcceptanceContentSha256 { get; set; } public string WriteIntegrationContentSha256 { get; set; } public string EnvironmentId { get; set; } public DateTime TestedAtUtc { get; set; } public string TestedBy { get; set; } } private sealed class DiagnosticSummary { public string ScenarioCode { get; set; } public string EvidenceId { get; set; } public string ContentSha256 { get; set; } public string ModuleCode { get; set; } public string PrimaryFindingCode { get; set; } public DateTime CapturedAtUtc { get; set; } } private sealed class PreflightVerificationState { public string PackageSha256 { get; set; } public string LegacyBuildEvidenceSha256 { get; set; } public string RolloutPolicySha256 { get; set; } public string RolloutCustomerId { get; set; } public string MiniMaxVisionProbeEvidenceSha256 { get; set; } public DateTime MiniMaxVisionProbeObservedAtUtc { get; set; } public string MiniMaxVisionProbeRegion { get; set; } public string MiniMaxVisionProbeContractVersion { get; set; } } private sealed class SessionScopeSummary { public string SubSystemId { get; set; } public DateTime ReadOnlySessionPreflightGeneratedAtUtc { get; set; } public DateTime FieldReadOnlyValidationHandoffGeneratedAtUtc { get; set; } } private sealed class ReadOnlySessionPreflightState { public string GeneratedAtUtcText { get; set; } public DateTime GeneratedAtUtc { get; set; } public int ErpProcessId { get; set; } public string ErpSha256 { get; set; } public string ErpSignerThumbprint { get; set; } public string RuntimeCliVersion { get; set; } public string RuntimeCliSha256 { get; set; } public string RuntimeCliSignerThumbprint { get; set; } public int ModuleCount { get; set; } public bool IsAdministrator { get; set; } } private sealed class FieldReadOnlyValidationHandoffState { public string GeneratedAtUtcText { get; set; } } private sealed class MiniMaxVisionProbeState { public string ObservedAtUtcText { get; set; } public DateTime ObservedAtUtc { get; set; } public string Region { get; set; } public string ContractVersion { get; set; } } private sealed class LegacyBuildVerificationState { public string CliSha256 { get; set; } public string ErpSha256 { get; set; } public string CertificateThumbprint { get; set; } } private sealed class DiagnosticVerificationState { public DiagnosticVerificationState() { EvidenceIds = new HashSet(StringComparer.Ordinal); ContentHashes = new HashSet(StringComparer.Ordinal); UserIds = new HashSet(StringComparer.OrdinalIgnoreCase); UserNames = new HashSet(StringComparer.Ordinal); DatabaseScopeFingerprints = new HashSet( StringComparer.Ordinal); ModuleCodes = new HashSet(StringComparer.OrdinalIgnoreCase); } public ISet EvidenceIds { get; private set; } public ISet ContentHashes { get; private set; } public ISet UserIds { get; private set; } public ISet UserNames { get; private set; } public ISet DatabaseScopeFingerprints { get; private set; } public ISet ModuleCodes { get; private set; } } } }