syntax = "proto3"; option csharp_namespace = "ZB.MOM.WW.ScadaBridge.Communication.Grpc"; package scadabridge.centralcontrol.v1; import "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; import "google/protobuf/wrappers.proto"; // The two ingest RPCs deliberately REUSE the batch/ack messages already defined // for the site-hosted SiteStreamService rather than redeclaring them. The site // telemetry actor builds AuditEventBatch / CachedTelemetryBatch today and hands // them to ISiteStreamAuditClient; duplicating the shapes here would fork one // wire contract into two that must be kept in lockstep by hand. import "Protos/sitestream.proto"; // Central-hosted control plane (Phase 1A of the ClusterClient→gRPC migration). // // Direction: SITE is the client, CENTRAL is the server — the inverse of // SiteStreamService, where central dials the site. That asymmetry is deliberate // and mirrors the direction the Akka ClusterClient traffic used to flow: these // seven calls are exactly the seven messages SiteCommunicationActor sends to // /user/central-communication. // // Every call is gated by ControlPlaneAuthInterceptor: `authorization: Bearer ` // plus the `x-scadabridge-site` metadata header naming which site's preshared key // central must verify against. service CentralControlService { // Store-and-forward handoff of one notification for central delivery. The // ack is idempotent on notification_id — a duplicate submit after a lost ack // must not produce a second delivery. rpc SubmitNotification(NotificationSubmitDto) returns (NotificationSubmitAckDto); // Notify.Status(id) round-trip for a notification that has already left the // site buffer. `found = false` sends the caller back to the site-local buffer // to decide Forwarding vs Unknown. rpc QueryNotificationStatus(NotificationStatusQueryDto) returns (NotificationStatusResponseDto); // Audit Log (#23) push telemetry. Reuses the SiteStreamService messages: the // batch a site drains from its SQLite hot path is byte-identical whichever // transport carries it. rpc IngestAuditEvents(sitestream.AuditEventBatch) returns (sitestream.IngestAck); // Audit Log (#23) M3 combined cached-call telemetry (audit row + SiteCalls // operational upsert, written in one central transaction). rpc IngestCachedTelemetry(sitestream.CachedTelemetryBatch) returns (sitestream.IngestAck); // Node-startup self-heal: the node's local deployed inventory in, fetch // tokens for whatever it is missing or stale out. rpc ReconcileSite(ReconcileSiteRequestDto) returns (ReconcileSiteResponseDto); // Periodic site health report (30 s cadence). The ack makes delivery // observable end-to-end so the sender can restore its per-interval counters // when a report is lost. rpc ReportSiteHealth(SiteHealthReportDto) returns (SiteHealthReportAckDto); // Application heartbeat. Returns Empty because the message is // fire-and-forget: nothing on the site consumes a reply, and a failure here // must never surface as a fault on the heartbeat timer path. rpc Heartbeat(HeartbeatDto) returns (google.protobuf.Empty); } // --------------------------------------------------------------------------- // Notification Outbox (#21) // --------------------------------------------------------------------------- // Site -> Central: submit a buffered notification for central delivery. // Mirrors Commons NotificationSubmit. message NotificationSubmitDto { string notification_id = 1; // GUID string, the idempotency key string list_name = 2; string subject = 3; string body = 4; string source_site_id = 5; string source_instance_id = 6; // empty string represents null string source_script = 7; // empty string represents null google.protobuf.Timestamp site_enqueued_at = 8; string origin_execution_id = 9; // GUID string; empty represents null string origin_parent_execution_id = 10; // GUID string; empty represents null string source_node = 11; // empty string represents null } // Central -> Site: ack sent after the Notifications row is persisted. message NotificationSubmitAckDto { string notification_id = 1; bool accepted = 2; string error = 3; // empty string represents null } // Site -> Central: Notify.Status(id) lookup against the central outbox. message NotificationStatusQueryDto { string correlation_id = 1; string notification_id = 2; } // Central -> Site: current central delivery state for a queried notification. message NotificationStatusResponseDto { string correlation_id = 1; bool found = 2; string status = 3; int32 retry_count = 4; string last_error = 5; // empty string represents null google.protobuf.Timestamp delivered_at = 6; // absent when null } // --------------------------------------------------------------------------- // Startup reconciliation (Deployment Manager) // --------------------------------------------------------------------------- // Site -> Central: the node's local deployed inventory at startup. message ReconcileSiteRequestDto { string site_identifier = 1; string node_id = 2; // Instance unique name -> revision hash of the config the node currently holds. map local_name_to_revision_hash = 3; } // Central -> Site: the gap the node must (re)fetch, plus orphans to log. message ReconcileSiteResponseDto { repeated ReconcileGapItemDto gap = 1; repeated string orphan_names = 2; string central_fetch_base_url = 3; } // One instance the node must (re)fetch, with a freshly-minted short-TTL token. message ReconcileGapItemDto { string instance_unique_name = 1; string deployment_id = 2; string revision_hash = 3; bool is_enabled = 4; string fetch_token = 5; } // --------------------------------------------------------------------------- // Health Monitoring (#11) // --------------------------------------------------------------------------- // Wire form of the Commons ConnectionHealth enum. // // CONNECTION_HEALTH_UNSPECIFIED exists only to keep the proto3 zero value from // meaning something. Mapping Connected onto 0 would make an absent/garbled // value decode as "healthy", which is precisely the wrong direction to fail; // the mapper decodes UNSPECIFIED as Error instead and never emits it. enum ConnectionHealthEnum { CONNECTION_HEALTH_UNSPECIFIED = 0; CONNECTION_HEALTH_CONNECTED = 1; CONNECTION_HEALTH_DISCONNECTED = 2; CONNECTION_HEALTH_CONNECTING = 3; CONNECTION_HEALTH_ERROR = 4; } message TagResolutionStatusDto { int32 total_subscribed = 1; int32 successfully_resolved = 2; } message TagQualityCountsDto { int32 good = 1; int32 bad = 2; int32 uncertain = 3; } message NodeStatusDto { string hostname = 1; bool is_online = 2; string role = 3; } // Point-in-time snapshot of the site-local SQLite audit queue. message SiteAuditBacklogSnapshotDto { int32 pending_count = 1; google.protobuf.Timestamp oldest_pending_utc = 2; // absent when the queue is empty int64 on_disk_bytes = 3; } // The three collection wrappers below exist so null and empty stay // distinguishable. proto3 cannot express presence on a `repeated` or `map` // field — an unset one and an empty one are the same bytes — but the // corresponding SiteHealthReport members are genuinely nullable // (SiteHealthCollector emits `ClusterNodes: _clusterNodes?.ToList()`), and the // central health surface reads null as "this producer doesn't report the // signal" rather than "the signal is empty". Wrapping in a message restores // message presence and makes the distinction survive the round-trip. message ConnectionEndpointMapDto { map entries = 1; } message TagQualityMapDto { map entries = 1; } message NodeStatusListDto { repeated NodeStatusDto nodes = 1; } // Site -> Central: periodic site health report. Mirrors Commons SiteHealthReport. // Additive-only evolution: field numbers are never reused. message SiteHealthReportDto { string site_id = 1; int64 sequence_number = 2; google.protobuf.Timestamp report_timestamp = 3; map data_connection_statuses = 4; map tag_resolution_counts = 5; int32 script_error_count = 6; int32 alarm_evaluation_error_count = 7; map store_and_forward_buffer_depths = 8; int32 dead_letter_count = 9; int32 deployed_instance_count = 10; int32 enabled_instance_count = 11; int32 disabled_instance_count = 12; string node_role = 13; string node_hostname = 14; ConnectionEndpointMapDto data_connection_endpoints = 15; // absent when null TagQualityMapDto data_connection_tag_quality = 16; // absent when null int32 parked_message_count = 17; NodeStatusListDto cluster_nodes = 18; // absent when null int32 site_audit_write_failures = 19; int32 audit_redaction_failure = 20; SiteAuditBacklogSnapshotDto site_audit_backlog = 21; // absent when no data yet int64 site_event_log_write_failures = 22; google.protobuf.DoubleValue oldest_parked_message_age_seconds = 23; // absent when nothing parked int32 script_queue_depth = 24; int32 script_busy_threads = 25; google.protobuf.DoubleValue script_oldest_busy_age_seconds = 26; // absent when the pool is idle // Nullable on purpose: absent means "replication not wired on this node", // which is NOT the same as false ("wired but currently disconnected"). google.protobuf.BoolValue local_db_replication_connected = 27; // Absent means UNKNOWN, never zero — a failed backlog read rendered as 0 // would report a broken replication pair as perfectly healthy. google.protobuf.Int64Value local_db_oplog_backlog = 28; } // Central -> Site: health report ack, so a lost report is observable. message SiteHealthReportAckDto { string site_id = 1; int64 sequence_number = 2; bool accepted = 3; string error = 4; // empty string represents null } // Site -> Central: application heartbeat (fire-and-forget; reply is Empty). message HeartbeatDto { string site_id = 1; string node_hostname = 2; bool is_active = 3; google.protobuf.Timestamp timestamp = 4; }