feat(grpc): T1B.1 — site_command.proto + SiteCommandDtoMapper + round-trip goldens

Phase 1B's contract slice: the wire shape and the canonical translation for the
28 central→site commands that leave ClusterClient. No behaviour changes yet —
SiteCommunicationActor and CentralCommunicationActor are untouched; the
dispatcher refactor (T1B.2) and the central transport seam (T1B.3) consume this.

Protos/site_command.proto (package scadabridge.sitecommand.v1, service
SiteCommandService): six domain RPCs, each with a `oneof` request/reply
envelope. The grouping is what carries deadline policy — every command inside a
group shares a CommunicationOptions timeout class today, so one RPC per group
keeps the deadline choice in one place on the client and one dispatch switch on
the server, while the oneof keeps each command individually typed:
ExecuteLifecycle(6) · ExecuteOpcUa(8) · ExecuteQuery(4) · ExecuteParked(5) ·
ExecuteRoute(4) · TriggerFailover(1). IntegrationCallRequest — the 29th entry on
SiteCommunicationActor's receive table — is deliberately excluded as dead code
(2026-07-22-integration-call-routing-is-dead-code.md).

Contract decisions worth knowing:

- Nullable COLLECTIONS ride in per-collection wrapper messages
  (DeployArtifactsCommand's six artifact lists, CertTrustResult.Certs,
  RouteToCallRequest.Parameters). proto3 repeated/map collapses null into empty,
  and that distinction is live at the site — the same silent-data-loss class the
  transport round-trip guard exposed in PLAN-05 T8. Goldens cover null, empty
  and populated for each.
- Nullable strings use the empty-string-means-null convention already set by
  AuditEventDtoMapper, with ONE exception: RouteToWaitForAttributeRequest's
  TargetValueEncoded, where "wait for the empty string" is a real target, so it
  carries a StringValue wrapper. Both behaviours are asserted, not assumed.
- Nullable enums ride in one-field messages (proto3 enums have no presence and
  no stock wrapper). Enum translation is an explicit switch in both directions —
  never by ordinal — so reordering a C# enum cannot re-map the wire; every wire
  enum reserves 0 for _UNSPECIFIED and decodes to a documented safe default
  rather than faulting a command from a version-skewed peer.
- New LooseValueCodec carries the surviving `object?` members (script params and
  return values, attribute values, tag read/write values) as a type-tagged union
  so a boxed value keeps its runtime CLR type, as it does today under Akka's
  type-preserving JSON serializer. Dates ride as invariant round-trip strings,
  not Timestamp, which would silently normalise away DateTime.Kind and
  DateTimeOffset.Offset. Lists/maps recurse; anything outside the tagged set
  falls back to JSON and is documented as CLR-type-lossy.
- DebugViewSnapshot gets its own full-fidelity alarm/attribute messages rather
  than reusing sitestream's AlarmStateUpdate, which flattens values to display
  strings — right for a live stream, lossy for a snapshot the UI treats as
  authoritative. The encoder omits an AlarmStateChanged.Condition that already
  equals the record's derived default, so computed alarms round-trip exactly
  (record equality compares the nullable backing field, not the property).

Tests are reflection-driven so the coverage cannot drift: the round-trip theory
enumerates the mapper's own ToProto overloads, the envelope guards enumerate the
generated oneof descriptors, and a missing golden fails the build. 216 new tests
green (Communication 532 total, Commons 684 total, solution build 0/0).

Codegen is checked in under SiteCommandGrpc/ per the sitestream recipe; the
<Protobuf> ItemGroup stays commented out (an active one segfaults protoc in the
linux_arm64 Docker image). docker/regen-proto.sh now handles every proto in that
ItemGroup instead of just sitestream, and re-comments idempotently.
This commit is contained in:
Joseph Doherty
2026-07-22 18:39:54 -04:00
parent aa60f43866
commit 59b13d317b
13 changed files with 37742 additions and 24 deletions
@@ -0,0 +1,857 @@
syntax = "proto3";
option csharp_namespace = "ZB.MOM.WW.ScadaBridge.Communication.Grpc";
package scadabridge.sitecommand.v1;
import "google/protobuf/timestamp.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/wrappers.proto";
// ─────────────────────────────────────────────────────────────────────────────
// Site command plane (central → site) — the gRPC replacement for the per-site
// ClusterClient command/control channel handled by SiteCommunicationActor.
//
// The 28 migrated commands are grouped into six domain RPCs rather than 28
// individual RPCs, because the grouping is what carries the DEADLINE policy:
// every command inside one group shares a timeout class today (see
// CommunicationOptions), so one RPC per group keeps the deadline choice in one
// place on the client and one dispatch switch on the server. A `oneof` request
// / reply envelope preserves per-command typing inside the group.
//
// IntegrationCallRequest (the 29th command) is deliberately NOT here — it is
// dead code at both ends; see
// docs/known-issues/2026-07-22-integration-call-routing-is-dead-code.md.
//
// EVOLUTION RULE (same as sitestream.proto): field numbers are never reused and
// changes are additive only. New commands take the next free oneof tag; an
// older peer simply sees an unset oneof and answers with a clean error.
//
// The generated C# is CHECKED IN under Communication/SiteCommandGrpc/ because
// protoc segfaults inside the linux_arm64 Docker build image. Regenerate with
// docker/regen-proto.sh (or by hand per the recipe in the .csproj) — never by
// leaving an active <Protobuf> item in the project file.
// ─────────────────────────────────────────────────────────────────────────────
service SiteCommandService {
// Deployment + instance lifecycle (6 commands).
rpc ExecuteLifecycle(LifecycleRequest) returns (LifecycleReply);
// Interactive OPC UA / MxGateway design-time commands (8 commands).
rpc ExecuteOpcUa(OpcUaRequest) returns (OpcUaReply);
// Remote read-only queries: event log + debug view (4 commands).
rpc ExecuteQuery(QueryRequest) returns (QueryReply);
// Parked store-and-forward message + cached-operation actions (5 commands).
rpc ExecuteParked(ParkedRequest) returns (ParkedReply);
// Inbound-API Route.To() relays (4 commands).
rpc ExecuteRoute(RouteRequest) returns (RouteReply);
// Operator-initiated manual site-pair failover (1 command).
rpc TriggerFailover(TriggerSiteFailoverDto) returns (SiteFailoverAckDto);
}
// ─────────────────────────────────────────────────────────────────────────────
// Shared value carrier
// ─────────────────────────────────────────────────────────────────────────────
// Explicit null marker. Needed because a map<string, LooseValue> entry always
// has a value message present, so "the dictionary holds a null for this key"
// cannot be expressed by absence the way a nullable message FIELD can.
message LooseNull {}
message LooseValueList { repeated LooseValue items = 1; }
message LooseValueMap { map<string, LooseValue> entries = 1; }
// Type-tagged carrier for the `object?` members the command plane still uses
// (script parameters and return values, attribute values, tag read/write
// values). The tags cover every CLR type these fields actually carry, so those
// values round-trip with their runtime type intact; anything else falls back to
// `json_value` (see the mapper's documented lossiness note).
//
// Date/time and decimal ride as invariant round-trip STRINGS rather than
// google.protobuf.Timestamp: Timestamp normalises everything to UTC and would
// silently drop DateTime.Kind / DateTimeOffset.Offset, which for an operator's
// tag value is data loss, not normalisation.
message LooseValue {
oneof kind {
LooseNull null_value = 1;
bool bool_value = 2;
int32 int32_value = 3;
int64 int64_value = 4;
double double_value = 5;
float float_value = 6;
string string_value = 7;
string decimal_value = 8; // invariant-culture round-trip
string date_time_value = 9; // DateTime, "O" (preserves Kind)
string date_time_offset_value = 10; // DateTimeOffset, "O" (preserves Offset)
string guid_value = 11; // "D"
string time_span_value = 12; // "c" (constant/round-trip)
bytes bytes_value = 13;
LooseValueList list_value = 14;
LooseValueMap map_value = 15;
string json_value = 16; // fallback; decodes to JsonElement
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Enums. Every enum reserves 0 for _UNSPECIFIED so a value that is absent on
// the wire is never mistaken for the first CLR member. The mapper translates
// explicitly in both directions (never by ordinal), so reordering a C# enum
// cannot silently re-map the wire.
// ─────────────────────────────────────────────────────────────────────────────
enum DeploymentStatusDto {
DEPLOYMENT_STATUS_DTO_UNSPECIFIED = 0;
DEPLOYMENT_STATUS_DTO_PENDING = 1;
DEPLOYMENT_STATUS_DTO_IN_PROGRESS = 2;
DEPLOYMENT_STATUS_DTO_SUCCESS = 3;
DEPLOYMENT_STATUS_DTO_FAILED = 4;
}
enum BrowseNodeClassDto {
BROWSE_NODE_CLASS_DTO_UNSPECIFIED = 0;
BROWSE_NODE_CLASS_DTO_OBJECT = 1;
BROWSE_NODE_CLASS_DTO_VARIABLE = 2;
BROWSE_NODE_CLASS_DTO_METHOD = 3;
BROWSE_NODE_CLASS_DTO_OTHER = 4;
}
enum BrowseFailureKindDto {
BROWSE_FAILURE_KIND_DTO_UNSPECIFIED = 0;
BROWSE_FAILURE_KIND_DTO_CONNECTION_NOT_FOUND = 1;
BROWSE_FAILURE_KIND_DTO_CONNECTION_NOT_CONNECTED = 2;
BROWSE_FAILURE_KIND_DTO_NOT_BROWSABLE = 3;
BROWSE_FAILURE_KIND_DTO_TIMEOUT = 4;
BROWSE_FAILURE_KIND_DTO_SERVER_ERROR = 5;
}
enum ReadTagValuesFailureKindDto {
READ_TAG_VALUES_FAILURE_KIND_DTO_UNSPECIFIED = 0;
READ_TAG_VALUES_FAILURE_KIND_DTO_CONNECTION_NOT_FOUND = 1;
READ_TAG_VALUES_FAILURE_KIND_DTO_CONNECTION_NOT_CONNECTED = 2;
READ_TAG_VALUES_FAILURE_KIND_DTO_TIMEOUT = 3;
READ_TAG_VALUES_FAILURE_KIND_DTO_SERVER_ERROR = 4;
}
enum VerifyFailureKindDto {
VERIFY_FAILURE_KIND_DTO_UNSPECIFIED = 0;
VERIFY_FAILURE_KIND_DTO_UNREACHABLE = 1;
VERIFY_FAILURE_KIND_DTO_AUTH_FAILED = 2;
VERIFY_FAILURE_KIND_DTO_UNTRUSTED_CERTIFICATE = 3;
VERIFY_FAILURE_KIND_DTO_TIMEOUT = 4;
VERIFY_FAILURE_KIND_DTO_SERVER_ERROR = 5;
}
enum StoreAndForwardCategoryDto {
STORE_AND_FORWARD_CATEGORY_DTO_UNSPECIFIED = 0;
STORE_AND_FORWARD_CATEGORY_DTO_EXTERNAL_SYSTEM = 1;
STORE_AND_FORWARD_CATEGORY_DTO_NOTIFICATION = 2;
STORE_AND_FORWARD_CATEGORY_DTO_CACHED_DB_WRITE = 3;
}
enum AlarmStateDto {
ALARM_STATE_DTO_UNSPECIFIED = 0;
ALARM_STATE_DTO_ACTIVE = 1;
ALARM_STATE_DTO_NORMAL = 2;
}
enum AlarmLevelDto {
ALARM_LEVEL_DTO_UNSPECIFIED = 0;
ALARM_LEVEL_DTO_NONE = 1;
ALARM_LEVEL_DTO_LOW = 2;
ALARM_LEVEL_DTO_LOW_LOW = 3;
ALARM_LEVEL_DTO_HIGH = 4;
ALARM_LEVEL_DTO_HIGH_HIGH = 5;
}
enum AlarmKindDto {
ALARM_KIND_DTO_UNSPECIFIED = 0;
ALARM_KIND_DTO_COMPUTED = 1;
ALARM_KIND_DTO_NATIVE_OPC_UA = 2;
ALARM_KIND_DTO_NATIVE_MX_ACCESS = 3;
}
enum AlarmShelveStateDto {
ALARM_SHELVE_STATE_DTO_UNSPECIFIED = 0;
ALARM_SHELVE_STATE_DTO_UNSHELVED = 1;
ALARM_SHELVE_STATE_DTO_ONE_SHOT_SHELVED = 2;
ALARM_SHELVE_STATE_DTO_TIMED_SHELVED = 3;
ALARM_SHELVE_STATE_DTO_PERMANENT_SHELVED = 4;
}
// ─────────────────────────────────────────────────────────────────────────────
// RPC 1 — ExecuteLifecycle
// ─────────────────────────────────────────────────────────────────────────────
message RefreshDeploymentCommandDto {
string deployment_id = 1;
string instance_unique_name = 2;
string revision_hash = 3;
string deployed_by = 4;
google.protobuf.Timestamp timestamp = 5;
string central_fetch_base_url = 6;
string fetch_token = 7;
}
message EnableInstanceCommandDto {
string command_id = 1;
string instance_unique_name = 2;
google.protobuf.Timestamp timestamp = 3;
}
message DisableInstanceCommandDto {
string command_id = 1;
string instance_unique_name = 2;
google.protobuf.Timestamp timestamp = 3;
}
message DeleteInstanceCommandDto {
string command_id = 1;
string instance_unique_name = 2;
google.protobuf.Timestamp timestamp = 3;
}
message DeploymentStateQueryRequestDto {
string correlation_id = 1;
string instance_unique_name = 2;
google.protobuf.Timestamp timestamp = 3;
}
message SharedScriptArtifactDto {
string name = 1;
string code = 2;
string parameter_definitions = 3; // empty string represents null
string return_definition = 4; // empty string represents null
}
message ExternalSystemArtifactDto {
string name = 1;
string endpoint_url = 2;
string auth_type = 3;
string auth_configuration = 4; // empty string represents null
string method_definitions_json = 5; // empty string represents null
int32 timeout_seconds = 6;
}
message DatabaseConnectionArtifactDto {
string name = 1;
string connection_string = 2;
int32 max_retries = 3;
google.protobuf.Duration retry_delay = 4;
}
message NotificationListArtifactDto {
string name = 1;
repeated string recipient_emails = 2;
}
message DataConnectionArtifactDto {
string name = 1;
string protocol = 2;
string primary_configuration_json = 3; // empty string represents null
string backup_configuration_json = 4; // empty string represents null
int32 failover_retry_count = 5;
}
message SmtpConfigurationArtifactDto {
string name = 1;
string server = 2;
int32 port = 3;
string auth_mode = 4;
string from_address = 5;
string username = 6; // empty string represents null
string password = 7; // empty string represents null
string oauth_config = 8; // empty string represents null
}
// Each artifact collection on DeployArtifactsCommand is a NULLABLE list, and
// proto3 `repeated` cannot distinguish null from empty. Wrapping each in its
// own message makes the null/empty distinction a message-presence question,
// which proto3 does model — the same silent-data-loss class the transport
// round-trip guard caught in PLAN-05 T8.
message SharedScriptArtifactListDto { repeated SharedScriptArtifactDto items = 1; }
message ExternalSystemArtifactListDto { repeated ExternalSystemArtifactDto items = 1; }
message DatabaseConnectionArtifactListDto { repeated DatabaseConnectionArtifactDto items = 1; }
message NotificationListArtifactListDto { repeated NotificationListArtifactDto items = 1; }
message DataConnectionArtifactListDto { repeated DataConnectionArtifactDto items = 1; }
message SmtpConfigurationArtifactListDto { repeated SmtpConfigurationArtifactDto items = 1; }
message DeployArtifactsCommandDto {
string deployment_id = 1;
SharedScriptArtifactListDto shared_scripts = 2; // absent => null
ExternalSystemArtifactListDto external_systems = 3; // absent => null
DatabaseConnectionArtifactListDto database_connections = 4; // absent => null
NotificationListArtifactListDto notification_lists = 5; // absent => null
DataConnectionArtifactListDto data_connections = 6; // absent => null
SmtpConfigurationArtifactListDto smtp_configurations = 7; // absent => null
google.protobuf.Timestamp timestamp = 8;
}
message DeploymentStatusResponseDto {
string deployment_id = 1;
string instance_unique_name = 2;
DeploymentStatusDto status = 3;
string error_message = 4; // empty string represents null
google.protobuf.Timestamp timestamp = 5;
}
message InstanceLifecycleResponseDto {
string command_id = 1;
string instance_unique_name = 2;
bool success = 3;
string error_message = 4; // empty string represents null
google.protobuf.Timestamp timestamp = 5;
}
message DeploymentStateQueryResponseDto {
string correlation_id = 1;
string instance_unique_name = 2;
bool is_deployed = 3;
string applied_deployment_id = 4; // empty string represents null
string applied_revision_hash = 5; // empty string represents null
google.protobuf.Timestamp timestamp = 6;
}
message ArtifactDeploymentResponseDto {
string deployment_id = 1;
string site_id = 2;
bool success = 3;
string error_message = 4; // empty string represents null
google.protobuf.Timestamp timestamp = 5;
}
message LifecycleRequest {
oneof command {
RefreshDeploymentCommandDto refresh_deployment = 1;
EnableInstanceCommandDto enable_instance = 2;
DisableInstanceCommandDto disable_instance = 3;
DeleteInstanceCommandDto delete_instance = 4;
DeploymentStateQueryRequestDto deployment_state_query = 5;
DeployArtifactsCommandDto deploy_artifacts = 6;
}
}
message LifecycleReply {
oneof reply {
DeploymentStatusResponseDto deployment_status = 1;
InstanceLifecycleResponseDto instance_lifecycle = 2;
DeploymentStateQueryResponseDto deployment_state_query = 3;
ArtifactDeploymentResponseDto artifact_deployment = 4;
}
}
// ─────────────────────────────────────────────────────────────────────────────
// RPC 2 — ExecuteOpcUa
// ─────────────────────────────────────────────────────────────────────────────
message BrowseNodeCommandDto {
string connection_name = 1;
string parent_node_id = 2; // empty string represents null (browse root)
string continuation_token = 3; // empty string represents null (first page)
string site_identifier = 4; // empty string represents null
}
message BrowseNodeDto {
string node_id = 1;
string display_name = 2;
BrowseNodeClassDto node_class = 3;
bool has_children = 4;
string data_type = 5; // empty string represents null
google.protobuf.Int32Value value_rank = 6; // absent => null
google.protobuf.BoolValue writable = 7; // absent => null
}
message BrowseFailureDto {
BrowseFailureKindDto kind = 1;
string message = 2;
}
message BrowseNodeResultDto {
repeated BrowseNodeDto children = 1;
bool truncated = 2;
BrowseFailureDto failure = 3; // absent => null (success)
string continuation_token = 4; // empty string represents null (final page)
}
message SearchAddressSpaceCommandDto {
string connection_name = 1;
string query = 2;
int32 max_depth = 3;
int32 max_results = 4;
string site_identifier = 5; // empty string represents null
}
message AddressSpaceMatchDto {
BrowseNodeDto node = 1;
string path = 2;
}
message SearchAddressSpaceResultDto {
repeated AddressSpaceMatchDto matches = 1;
bool cap_reached = 2;
BrowseFailureDto failure = 3; // absent => null (success)
}
message ReadTagValuesCommandDto {
string connection_name = 1;
repeated string tag_paths = 2;
}
message TagReadOutcomeDto {
string tag_path = 1;
bool success = 2;
LooseValue value = 3; // absent => null
string quality = 4;
google.protobuf.Timestamp timestamp = 5;
string error_message = 6; // empty string represents null
}
message ReadTagValuesFailureDto {
ReadTagValuesFailureKindDto kind = 1;
string message = 2;
}
message ReadTagValuesResultDto {
repeated TagReadOutcomeDto outcomes = 1;
ReadTagValuesFailureDto failure = 2; // absent => null (success)
}
message VerifyEndpointCommandDto {
string connection_name = 1;
string protocol = 2;
string config_json = 3;
string site_identifier = 4; // empty string represents null
}
message ServerCertInfoDto {
string thumbprint = 1;
string subject = 2;
string issuer = 3;
google.protobuf.Timestamp not_before_utc = 4;
google.protobuf.Timestamp not_after_utc = 5;
string der_base64 = 6;
}
// failure_kind is a NULLABLE enum on the CLR side, and proto3 enums have no
// presence, so it rides inside a one-field message exactly like Int32Value.
message VerifyFailureKindValue { VerifyFailureKindDto value = 1; }
message VerifyEndpointResultDto {
bool success = 1;
VerifyFailureKindValue failure_kind = 2; // absent => null (success)
string error = 3; // empty string represents null
ServerCertInfoDto cert = 4; // absent => null
}
message TrustServerCertCommandDto {
string connection_name = 1;
string der_base64 = 2;
string thumbprint = 3;
string site_identifier = 4; // empty string represents null
}
message ListServerCertsCommandDto {
string site_identifier = 1; // empty string represents null
}
message RemoveServerCertCommandDto {
string thumbprint = 1;
string site_identifier = 2; // empty string represents null
}
message TrustedCertInfoDto {
string thumbprint = 1;
string subject = 2;
string issuer = 3;
google.protobuf.Timestamp not_before_utc = 4;
google.protobuf.Timestamp not_after_utc = 5;
bool rejected = 6;
}
// CertTrustResult.Certs is a nullable list (null for trust/remove, populated
// for list) — same null-vs-empty problem as the artifact collections.
message TrustedCertInfoListDto { repeated TrustedCertInfoDto items = 1; }
message CertTrustResultDto {
bool success = 1;
string error = 2; // empty string represents null
TrustedCertInfoListDto certs = 3; // absent => null
}
message WriteTagRequestDto {
string correlation_id = 1;
string connection_name = 2;
string tag_path = 3;
LooseValue value = 4; // absent => null
google.protobuf.Timestamp timestamp = 5;
}
message WriteTagResponseDto {
string correlation_id = 1;
bool success = 2;
string error_message = 3; // empty string represents null
google.protobuf.Timestamp timestamp = 4;
}
message OpcUaRequest {
oneof command {
BrowseNodeCommandDto browse_node = 1;
SearchAddressSpaceCommandDto search_address_space = 2;
ReadTagValuesCommandDto read_tag_values = 3;
VerifyEndpointCommandDto verify_endpoint = 4;
TrustServerCertCommandDto trust_server_cert = 5;
ListServerCertsCommandDto list_server_certs = 6;
RemoveServerCertCommandDto remove_server_cert = 7;
WriteTagRequestDto write_tag = 8;
}
}
message OpcUaReply {
oneof reply {
BrowseNodeResultDto browse_node = 1;
SearchAddressSpaceResultDto search_address_space = 2;
ReadTagValuesResultDto read_tag_values = 3;
VerifyEndpointResultDto verify_endpoint = 4;
CertTrustResultDto cert_trust = 5;
WriteTagResponseDto write_tag = 6;
}
}
// ─────────────────────────────────────────────────────────────────────────────
// RPC 3 — ExecuteQuery
// ─────────────────────────────────────────────────────────────────────────────
message EventLogQueryRequestDto {
string correlation_id = 1;
string site_id = 2;
google.protobuf.Timestamp from = 3; // absent => null
google.protobuf.Timestamp to = 4; // absent => null
string event_type = 5; // empty string represents null
string severity = 6; // empty string represents null
string instance_id = 7; // empty string represents null
string keyword_filter = 8; // empty string represents null
string continuation_token = 9; // empty string represents null
int32 page_size = 10;
google.protobuf.Timestamp timestamp = 11;
}
message EventLogEntryDto {
string id = 1;
google.protobuf.Timestamp timestamp = 2;
string event_type = 3;
string severity = 4;
string instance_id = 5; // empty string represents null
string source = 6;
string message = 7;
string details = 8; // empty string represents null
}
message EventLogQueryResponseDto {
string correlation_id = 1;
string site_id = 2;
repeated EventLogEntryDto entries = 3;
string continuation_token = 4; // empty string represents null
bool has_more = 5;
bool success = 6;
string error_message = 7; // empty string represents null
google.protobuf.Timestamp timestamp = 8;
}
message DebugSnapshotRequestDto {
string instance_unique_name = 1;
string correlation_id = 2;
}
message SubscribeDebugViewRequestDto {
string instance_unique_name = 1;
string correlation_id = 2;
}
message UnsubscribeDebugViewRequestDto {
string instance_unique_name = 1;
string correlation_id = 2;
}
// UnsubscribeDebugView is a Tell today (CommunicationService.UnsubscribeDebugView
// fires and forgets). A unary RPC must still answer something, so the site sends
// this empty ack; the central transport ignores it to keep the caller-visible
// fire-and-forget semantics identical.
message UnsubscribeDebugViewAckDto {}
message AlarmConditionStateDto {
bool active = 1;
bool acknowledged = 2;
google.protobuf.BoolValue confirmed = 3; // absent => null (not confirmable)
AlarmShelveStateDto shelve = 4;
bool suppressed = 5;
int32 severity = 6;
}
message DebugAttributeValueDto {
string instance_unique_name = 1;
string attribute_path = 2;
string attribute_name = 3;
LooseValue value = 4; // absent => null
string quality = 5;
google.protobuf.Timestamp timestamp = 6;
}
// Full-fidelity projection of Commons AlarmStateChanged. Deliberately NOT the
// sitestream AlarmStateUpdate: that one flattens the value to a display string
// and the shelve state to free text, which is right for a live stream but would
// lose data on a snapshot the central UI renders as authoritative state.
message DebugAlarmStateDto {
string instance_unique_name = 1;
string alarm_name = 2;
AlarmStateDto state = 3;
int32 priority = 4;
google.protobuf.Timestamp timestamp = 5;
AlarmLevelDto level = 6;
string message = 7;
AlarmKindDto kind = 8;
// Absent means "not explicitly set" — the CLR record then derives the
// computed default from state + priority. See the mapper's note on why the
// encoder omits a condition that already equals that derived default.
AlarmConditionStateDto condition = 9;
string source_reference = 10;
string alarm_type_name = 11;
string category = 12;
string operator_user = 13;
string operator_comment = 14;
google.protobuf.Timestamp original_raise_time = 15; // absent => null
string current_value = 16;
string limit_value = 17;
string native_source_canonical_name = 18;
bool is_configured_placeholder = 19;
}
message DebugViewSnapshotDto {
string instance_unique_name = 1;
repeated DebugAttributeValueDto attribute_values = 2;
repeated DebugAlarmStateDto alarm_states = 3;
google.protobuf.Timestamp snapshot_timestamp = 4;
bool instance_not_found = 5;
}
message QueryRequest {
oneof command {
EventLogQueryRequestDto event_log_query = 1;
DebugSnapshotRequestDto debug_snapshot = 2;
SubscribeDebugViewRequestDto subscribe_debug_view = 3;
UnsubscribeDebugViewRequestDto unsubscribe_debug_view = 4;
}
}
message QueryReply {
oneof reply {
EventLogQueryResponseDto event_log_query = 1;
DebugViewSnapshotDto debug_view_snapshot = 2;
UnsubscribeDebugViewAckDto unsubscribe_debug_view = 3;
}
}
// ─────────────────────────────────────────────────────────────────────────────
// RPC 4 — ExecuteParked
// ─────────────────────────────────────────────────────────────────────────────
message ParkedMessageQueryRequestDto {
string correlation_id = 1;
string site_id = 2;
int32 page_number = 3;
int32 page_size = 4;
google.protobuf.Timestamp timestamp = 5;
}
message ParkedMessageEntryDto {
string message_id = 1;
string target_system = 2;
string method_name = 3;
string error_message = 4;
int32 attempt_count = 5;
google.protobuf.Timestamp original_timestamp = 6;
google.protobuf.Timestamp last_attempt_timestamp = 7;
int32 max_attempts = 8;
StoreAndForwardCategoryDto category = 9;
string origin_instance = 10; // empty string represents null
}
message ParkedMessageQueryResponseDto {
string correlation_id = 1;
string site_id = 2;
repeated ParkedMessageEntryDto messages = 3;
int32 total_count = 4;
int32 page_number = 5;
int32 page_size = 6;
bool success = 7;
string error_message = 8; // empty string represents null
google.protobuf.Timestamp timestamp = 9;
}
message ParkedMessageRetryRequestDto {
string correlation_id = 1;
string site_id = 2;
string message_id = 3;
google.protobuf.Timestamp timestamp = 4;
}
message ParkedMessageRetryResponseDto {
string correlation_id = 1;
bool success = 2;
string error_message = 3; // empty string represents null
}
message ParkedMessageDiscardRequestDto {
string correlation_id = 1;
string site_id = 2;
string message_id = 3;
google.protobuf.Timestamp timestamp = 4;
}
message ParkedMessageDiscardResponseDto {
string correlation_id = 1;
bool success = 2;
string error_message = 3; // empty string represents null
}
message RetryParkedOperationDto {
string correlation_id = 1;
string tracked_operation_id = 2; // TrackedOperationId GUID, "D" format
}
message DiscardParkedOperationDto {
string correlation_id = 1;
string tracked_operation_id = 2; // TrackedOperationId GUID, "D" format
}
message ParkedOperationActionAckDto {
string correlation_id = 1;
bool applied = 2;
string error_message = 3; // empty string represents null
}
message ParkedRequest {
oneof command {
ParkedMessageQueryRequestDto parked_message_query = 1;
ParkedMessageRetryRequestDto parked_message_retry = 2;
ParkedMessageDiscardRequestDto parked_message_discard = 3;
RetryParkedOperationDto retry_parked_operation = 4;
DiscardParkedOperationDto discard_parked_operation = 5;
}
}
message ParkedReply {
oneof reply {
ParkedMessageQueryResponseDto parked_message_query = 1;
ParkedMessageRetryResponseDto parked_message_retry = 2;
ParkedMessageDiscardResponseDto parked_message_discard = 3;
ParkedOperationActionAckDto parked_operation_action = 4;
}
}
// ─────────────────────────────────────────────────────────────────────────────
// RPC 5 — ExecuteRoute
// ─────────────────────────────────────────────────────────────────────────────
message RouteToCallRequestDto {
string correlation_id = 1;
string instance_unique_name = 2;
string script_name = 3;
LooseValueMap parameters = 4; // absent => null (distinct from empty)
google.protobuf.Timestamp timestamp = 5;
string parent_execution_id = 6; // empty string represents null
}
message RouteToCallResponseDto {
string correlation_id = 1;
bool success = 2;
LooseValue return_value = 3; // absent => null
string error_message = 4; // empty string represents null
google.protobuf.Timestamp timestamp = 5;
}
message RouteToGetAttributesRequestDto {
string correlation_id = 1;
string instance_unique_name = 2;
repeated string attribute_names = 3;
google.protobuf.Timestamp timestamp = 4;
string parent_execution_id = 5; // empty string represents null
}
message RouteToGetAttributesResponseDto {
string correlation_id = 1;
LooseValueMap values = 2; // non-nullable on the CLR side; absent => empty
bool success = 3;
string error_message = 4; // empty string represents null
google.protobuf.Timestamp timestamp = 5;
}
message RouteToSetAttributesRequestDto {
string correlation_id = 1;
string instance_unique_name = 2;
map<string, string> attribute_values = 3;
google.protobuf.Timestamp timestamp = 4;
string parent_execution_id = 5; // empty string represents null
}
message RouteToSetAttributesResponseDto {
string correlation_id = 1;
bool success = 2;
string error_message = 3; // empty string represents null
google.protobuf.Timestamp timestamp = 4;
}
message RouteToWaitForAttributeRequestDto {
string correlation_id = 1;
string instance_unique_name = 2;
string attribute_name = 3;
// NOT the empty-string-means-null convention: "wait for this attribute to
// become the empty string" is a legitimate target that must stay distinct
// from "no target supplied", so this one nullable string rides a wrapper.
google.protobuf.StringValue target_value_encoded = 4;
google.protobuf.Duration timeout = 5;
google.protobuf.Timestamp timestamp = 6;
string parent_execution_id = 7; // empty string represents null
bool require_good_quality = 8;
}
message RouteToWaitForAttributeResponseDto {
string correlation_id = 1;
bool matched = 2;
LooseValue value = 3; // absent => null
string quality = 4; // empty string represents null
bool timed_out = 5;
bool success = 6;
string error_message = 7; // empty string represents null
google.protobuf.Timestamp timestamp = 8;
}
message RouteRequest {
oneof command {
RouteToCallRequestDto route_to_call = 1;
RouteToGetAttributesRequestDto route_to_get_attributes = 2;
RouteToSetAttributesRequestDto route_to_set_attributes = 3;
RouteToWaitForAttributeRequestDto route_to_wait_for_attribute = 4;
}
}
message RouteReply {
oneof reply {
RouteToCallResponseDto route_to_call = 1;
RouteToGetAttributesResponseDto route_to_get_attributes = 2;
RouteToSetAttributesResponseDto route_to_set_attributes = 3;
RouteToWaitForAttributeResponseDto route_to_wait_for_attribute = 4;
}
}
// ─────────────────────────────────────────────────────────────────────────────
// RPC 6 — TriggerFailover
// ─────────────────────────────────────────────────────────────────────────────
message TriggerSiteFailoverDto {
string correlation_id = 1;
string site_id = 2;
}
message SiteFailoverAckDto {
string correlation_id = 1;
bool accepted = 2;
string target_address = 3; // empty string represents null
string error_message = 4; // empty string represents null
}