diff --git a/src/MxGateway.Server/Grpc/MxAccessGatewayService.cs b/src/MxGateway.Server/Grpc/MxAccessGatewayService.cs
index 345c664..581a19c 100644
--- a/src/MxGateway.Server/Grpc/MxAccessGatewayService.cs
+++ b/src/MxGateway.Server/Grpc/MxAccessGatewayService.cs
@@ -52,6 +52,8 @@ public sealed class MxAccessGatewayService(
reply.Capabilities.Add("unary-invoke");
reply.Capabilities.Add("server-stream-events");
reply.Capabilities.Add("bulk-subscribe-commands");
+ reply.Capabilities.Add("unary-acknowledge-alarm");
+ reply.Capabilities.Add("server-stream-active-alarms");
return reply;
}
@@ -155,6 +157,83 @@ public sealed class MxAccessGatewayService(
}
}
+ ///
+ ///
+ /// PR A.3 — surfaces the public AcknowledgeAlarm RPC. The gateway resolves the
+ /// session and returns a successful reply; the actual worker-side ack call ships
+ /// in PR A.2 which adds the MxAccess alarm subscription + worker command
+ /// handler. Clients calling this method today receive an OK reply with a
+ /// "worker alarm path not yet wired" diagnostic — no PERMISSION_DENIED, no
+ /// UNIMPLEMENTED, so the .NET / Python / Go / Java / Rust SDK call sites land
+ /// on a stable surface.
+ ///
+ public override Task AcknowledgeAlarm(
+ AcknowledgeAlarmRequest request,
+ ServerCallContext context)
+ {
+ try
+ {
+ ArgumentNullException.ThrowIfNull(request);
+ if (string.IsNullOrEmpty(request.SessionId))
+ {
+ throw new RpcException(new Status(StatusCode.InvalidArgument, "session_id is required."));
+ }
+ if (string.IsNullOrEmpty(request.AlarmFullReference))
+ {
+ throw new RpcException(new Status(StatusCode.InvalidArgument, "alarm_full_reference is required."));
+ }
+
+ // Validate the session exists. Throws SessionManagerException → mapped to
+ // gRPC NotFound by the caller's MapException.
+ _ = ResolveSession(request.SessionId);
+
+ return Task.FromResult(new AcknowledgeAlarmReply
+ {
+ SessionId = request.SessionId,
+ CorrelationId = request.ClientCorrelationId,
+ ProtocolStatus = MxAccessGrpcMapper.Ok("AcknowledgeAlarm accepted; worker dispatch pending PR A.2."),
+ DiagnosticMessage = "Gateway-side AcknowledgeAlarm contract is live (PR A.3); worker-side MxAccess Acknowledge call ships in PR A.2.",
+ });
+ }
+ catch (Exception exception) when (exception is not RpcException)
+ {
+ throw MapException(exception);
+ }
+ }
+
+ ///
+ ///
+ /// PR A.3 — surfaces the public QueryActiveAlarms RPC as an empty stream until
+ /// PR A.2 adds the worker-side QueryActiveAlarmsCommand that walks the
+ /// MxAccess active-alarm collection. Clients can call the RPC and iterate the
+ /// stream; today the stream completes immediately. Once A.2 ships, this
+ /// handler will translate the request into a WorkerCommand and stream the
+ /// resulting snapshots.
+ ///
+ public override Task QueryActiveAlarms(
+ QueryActiveAlarmsRequest request,
+ IServerStreamWriter responseStream,
+ ServerCallContext context)
+ {
+ try
+ {
+ ArgumentNullException.ThrowIfNull(request);
+ if (string.IsNullOrEmpty(request.SessionId))
+ {
+ throw new RpcException(new Status(StatusCode.InvalidArgument, "session_id is required."));
+ }
+ _ = ResolveSession(request.SessionId);
+
+ // Empty stream — PR A.4 implements ConditionRefresh server-side once the
+ // worker's QueryActiveAlarmsCommand is available.
+ return Task.CompletedTask;
+ }
+ catch (Exception exception) when (exception is not RpcException)
+ {
+ throw MapException(exception);
+ }
+ }
+
private string? ResolveClientIdentity()
{
return identityAccessor.Current?.DisplayName ?? identityAccessor.Current?.KeyId;