feat: add audited one-shot prepare gate

This commit is contained in:
2026-07-18 12:29:09 +09:00
parent 1302b1b92f
commit 5379511b8a
25 changed files with 6188 additions and 41 deletions

View File

@@ -2,6 +2,7 @@ using System.Runtime.InteropServices;
using MBN_STOCK_WEBVIEW.Infrastructure;
using MBN_STOCK_WEBVIEW.LegacyApplication;
using MBN_STOCK_WEBVIEW.LegacyBridge;
using MBN_STOCK_WEBVIEW.Playout.Safety;
using Microsoft.UI.Windowing;
using Microsoft.Web.WebView2.Core;
using MMoneyCoderSharp.Data;
@@ -21,6 +22,7 @@ public sealed partial class MainWindow : Window
private readonly CancellationTokenSource _lifetimeCancellation = new();
private readonly SemaphoreSlim _intentGate = new(1, 1);
private readonly PlayoutLaunchAuthorization _playoutLaunchAuthorization;
private readonly WindowSubclassProcedure _windowSubclassProcedure;
private readonly LegacyOperatorController _controller;
private readonly LegacyIndustrySelectionWorkflow _industryWorkflow;
@@ -36,6 +38,10 @@ public sealed partial class MainWindow : Window
public MainWindow()
{
// Capture and remove Gate A bearer material before XAML can create WebView2.
// Only the native process keeps the capability digest for this launch.
_playoutLaunchAuthorization =
PlayoutLaunchAuthorization.CaptureFromEnvironment();
_windowSubclassProcedure = OnWindowSubclassMessage;
InitializeComponent();
EnsureCloseConfirmationAttached();
@@ -72,6 +78,8 @@ public sealed partial class MainWindow : Window
{
_databaseRuntime = DatabaseRuntime.CreateDefault();
DataQueryExecutor.Configure(_databaseRuntime.Executor);
var databaseMutationAuthorization =
new LaunchDatabaseMutationAuthorization(_playoutLaunchAuthorization);
stockLookup = new LegacyParityStockSearchService(_databaseRuntime.Executor);
industrySelectionService = new LegacyIndustrySelectionService(
_databaseRuntime.Executor);
@@ -91,17 +99,23 @@ public sealed partial class MainWindow : Window
_databaseRuntime.Executor,
new OracleManualFinancialMutationExecutor(
_databaseRuntime.ConnectionFactory,
_databaseRuntime.Options.Resilience));
_databaseRuntime.Options.Resilience,
errorDetector: null,
mutationAuthorization: databaseMutationAuthorization));
manualFinancialStockSearchService = new LegacyStockSearchService(
_databaseRuntime.Executor);
namedPlaylistPersistenceService = new LegacyNamedPlaylistPersistenceService(
_databaseRuntime.Executor,
new OracleNamedPlaylistMutationExecutor(
_databaseRuntime.ConnectionFactory,
_databaseRuntime.Options.Resilience));
_databaseRuntime.Options.Resilience,
errorDetector: null,
mutationAuthorization: databaseMutationAuthorization));
var operatorCatalogMutationExecutor = new OperatorCatalogMutationExecutor(
_databaseRuntime.ConnectionFactory,
_databaseRuntime.Options.Resilience);
_databaseRuntime.Options.Resilience,
errorDetector: null,
mutationAuthorization: databaseMutationAuthorization);
themeCatalogPersistenceService = new LegacyThemeCatalogPersistenceService(
_databaseRuntime.Executor,
operatorCatalogMutationExecutor);
@@ -377,6 +391,15 @@ public sealed partial class MainWindow : Window
return;
}
if (_playoutLaunchAuthorization.IsGateA &&
IsPersistentWriteIntent(intent))
{
PostState(ReportIntentFailure(
intent,
"Gate A 검증 회차에서는 DB 및 로컬 영구 저장 변경이 허용되지 않습니다."));
return;
}
if (_controller.Current.Dialog?.IsConfirmation == true &&
intent is not LegacyReadyIntent and
not LegacyConfirmNativeDialogIntent and
@@ -469,6 +492,7 @@ public sealed partial class MainWindow : Window
LegacySaveCurrentNamedPlaylistToIntent or
LegacyDeleteSelectedNamedPlaylistIntent or
LegacyExecutePlayoutIntent or
LegacyGateAPrepareIntent or
LegacySetFadeDurationIntent or
LegacyToggleBackgroundIntent or
LegacyChooseBackgroundIntent)
@@ -961,6 +985,10 @@ public sealed partial class MainWindow : Window
await _controller.ConfirmManualListsAsync(cancellationToken),
LegacyImportManualListsIntent =>
await _controller.ImportLegacyManualListsAsync(cancellationToken),
LegacyGateAPrepareIntent prepare =>
await ExecuteGateAPrepareAsync(
prepare.Capability,
cancellationToken),
LegacyExecutePlayoutIntent playout =>
await ExecuteOperatorPlayoutAsync(
playout.Command,
@@ -1038,6 +1066,25 @@ public sealed partial class MainWindow : Window
outcome is LegacyNamedPlaylistMutationOutcome.CommittedFresh or
LegacyNamedPlaylistMutationOutcome.CommittedOptimistic;
private sealed class LaunchDatabaseMutationAuthorization
: IDatabaseMutationAuthorization
{
private readonly PlayoutLaunchAuthorization _authorization;
public LaunchDatabaseMutationAuthorization(
PlayoutLaunchAuthorization authorization)
{
_authorization = authorization ??
throw new ArgumentNullException(nameof(authorization));
}
public bool IsAuthorized(DatabaseMutationAuthorizationRequest request)
{
ArgumentNullException.ThrowIfNull(request);
return _authorization.AllowsDatabaseWrites;
}
}
private void PostState(LegacyOperatorSnapshot snapshot)
{
if (!_closing && _webViewReady && Browser.CoreWebView2 is not null)