diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/SecuredWriteCommands.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/SecuredWriteCommands.cs
new file mode 100644
index 00000000..b5e1a04a
--- /dev/null
+++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/SecuredWriteCommands.cs
@@ -0,0 +1,83 @@
+namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
+
+// ============================================================================
+// Two-person ("secured") write commands (M7 OPC UA / MxGateway UX, Task T14b).
+//
+// An Operator SUBMITS a pending secured write against an MxGateway data
+// connection; a distinct Verifier later APPROVES (executes — Task C3) or
+// REJECTS it. Separation of duties is enforced at the handler: a write may not
+// be verified by the same principal that submitted it. ListSecuredWrites is a
+// read-only query (any authenticated user). Role gating lives in
+// ManagementActor.GetRequiredRole (Operator for submit; Verifier for
+// reject/approve).
+// ============================================================================
+
+///
+/// Operator request to submit a pending secured write. The target connection
+/// must exist within the site and use the MxGateway protocol; the value is
+/// captured as interpreted per .
+/// Returns the newly-created .
+///
+/// Site identifier the write targets.
+/// Data connection name within the site.
+/// Fully-qualified tag path the value is written to.
+/// JSON-serialised value to write.
+/// Target data type name (e.g. Boolean, Double).
+/// Optional free-text comment supplied by the operator.
+public record SubmitSecuredWriteCommand(
+ string SiteId,
+ string ConnectionName,
+ string TagPath,
+ string ValueJson,
+ string ValueType,
+ string? Comment);
+
+///
+/// Verifier request to approve (and execute — handled by Task C3) a pending
+/// secured write. Declared here so the secured-write contract is complete; the
+/// approve→execute relay handler and dispatch arm are implemented in C3.
+///
+/// Identity of the pending secured write.
+/// Optional free-text comment supplied by the verifier.
+public record ApproveSecuredWriteCommand(long Id, string? Comment);
+
+///
+/// Verifier request to reject a pending secured write. The write must still be
+/// Pending, and the verifier must differ from the submitting operator
+/// (separation of duties). Returns the updated .
+///
+/// Identity of the pending secured write.
+/// Optional free-text comment supplied by the verifier.
+public record RejectSecuredWriteCommand(long Id, string? Comment);
+
+///
+/// Read-only query for secured writes, optionally filtered by status and site.
+/// A null filter matches every row.
+///
+/// Status filter; null matches every status.
+/// Site id filter; null matches every site.
+public record ListSecuredWritesCommand(string? Status, string? SiteId);
+
+///
+/// Result projection of a single pending secured write row, mirroring the
+/// PendingSecuredWrite entity shape.
+///
+public record SecuredWriteDto(
+ long Id,
+ string SiteId,
+ string ConnectionName,
+ string TagPath,
+ string ValueJson,
+ string ValueType,
+ string Status,
+ string OperatorUser,
+ string? OperatorComment,
+ DateTime SubmittedAtUtc,
+ string? VerifierUser,
+ string? VerifierComment,
+ DateTime? DecidedAtUtc,
+ DateTime? ExecutedAtUtc,
+ string? ExecutionError);
+
+/// Result wrapper for .
+public record SecuredWriteListResult(IReadOnlyList Items);
diff --git a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs
index a1d6e950..8ef7e113 100644
--- a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs
@@ -11,6 +11,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
+using ZB.MOM.WW.ScadaBridge.Commons.Entities.SecuredWrites;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Security;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
@@ -216,6 +217,15 @@ public class ManagementActor : ReceiveActor
or RetryParkedMessageCommand or DiscardParkedMessageCommand
or DebugSnapshotCommand => Roles.Deployer,
+ // Two-person secured write (M7 / T14b). Submit is an Operator action;
+ // approve/reject are Verifier actions. Separation of duties (a write may
+ // not be verified by its submitter) is enforced inside the handler — the
+ // role gate only ensures the caller holds the right coarse role.
+ SubmitSecuredWriteCommand => Roles.Operator,
+ RejectSecuredWriteCommand or ApproveSecuredWriteCommand => Roles.Verifier,
+ // ListSecuredWritesCommand is read-only -> falls through to "any
+ // authenticated user" below, like the other list/query commands.
+
// Read-only queries -- any authenticated user
_ => null
};
@@ -368,6 +378,13 @@ public class ManagementActor : ReceiveActor
DiscardParkedMessageCommand cmd => await HandleDiscardParkedMessage(sp, cmd, user),
DebugSnapshotCommand cmd => await HandleDebugSnapshot(sp, cmd, user),
+ // Secured writes (M7 / T14b). Approve (execute) is intentionally NOT
+ // dispatched here — the approve->execute relay is Task C3; it would
+ // 'NotSupported' at runtime until then.
+ SubmitSecuredWriteCommand cmd => await HandleSubmitSecuredWrite(sp, cmd, user),
+ RejectSecuredWriteCommand cmd => await HandleRejectSecuredWrite(sp, cmd, user),
+ ListSecuredWritesCommand cmd => await HandleListSecuredWrites(sp, cmd),
+
// Transport (#24) bundle operations
ExportBundleCommand cmd => await HandleExportBundle(sp, cmd, user.Username),
PreviewBundleCommand cmd => await HandlePreviewBundle(sp, cmd),
@@ -860,6 +877,91 @@ public class ManagementActor : ReceiveActor
return await commService.DiscardParkedMessageAsync(cmd.SiteIdentifier, request);
}
+ // ========================================================================
+ // Secured-write handlers (M7 / T14b)
+ //
+ // Two-person workflow: an Operator submits a pending write against an
+ // MxGateway data connection; a distinct Verifier approves (Task C3) or
+ // rejects it. The role gate (GetRequiredRole) only verifies the coarse role;
+ // the separation-of-duties rule (a write may not be verified by its
+ // submitter) is enforced here.
+ // ========================================================================
+
+ private static SecuredWriteDto ToSecuredWriteDto(PendingSecuredWrite e) => new(
+ e.Id, e.SiteId, e.ConnectionName, e.TagPath, e.ValueJson, e.ValueType,
+ e.Status, e.OperatorUser, e.OperatorComment, e.SubmittedAtUtc,
+ e.VerifierUser, e.VerifierComment, e.DecidedAtUtc, e.ExecutedAtUtc, e.ExecutionError);
+
+ private static async Task