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;