Skip to main content

Data Contracts

CitadelMesh uses Protocol Buffers (protobuf) for all data contracts, ensuring type-safe, versioned, and efficient serialization across polyglot services. This document details all protobuf schemas, evolution strategies, and best practices.

Protobuf Packages

proto/citadel/v1/
├── telemetry.proto # Canonical telemetry and metrics
├── commands.proto # Control commands and results
├── incidents.proto # Security and operational incidents
├── policy.proto # Policy evaluations and decisions
├── twin.proto # Digital twin state and mutations
└── events.proto # Event type definitions

Core Message Types

Telemetry (telemetry.proto)

Purpose: Canonical representation of all building sensor data

syntax = "proto3";
package citadel.v1;

import "google/protobuf/timestamp.proto";

// Single telemetry point
message Point {
// Digital twin entity ID (e.g., "hvac.zone1.temp")
string entity_id = 1;

// Metric name (e.g., "temp.zone", "access.door")
string metric = 2;

// Numeric value
double value = 3;

// SI unit preferred (°C, kW, lux, etc.)
string unit = 4;

// Event timestamp (from source system)
google.protobuf.Timestamp timestamp = 5;

// Data quality indicator
string quality = 6; // "good" | "bad" | "uncertain"

// Additional metadata
map<string, string> attributes = 7;
}

// Batch of telemetry points
message PointBatch {
repeated Point points = 1;

// Source system identifier
string source_system = 2;

// Collection timestamp
google.protobuf.Timestamp collected_at = 3;
}

Example Usage:

from citadel.v1 import telemetry_pb2
from google.protobuf.timestamp_pb2 import Timestamp

point = telemetry_pb2.Point(
entity_id="hvac.zone1.temp",
metric="temp.zone",
value=72.5,
unit="°F",
timestamp=Timestamp(seconds=int(time.time())),
quality="good",
attributes={
"floor": "2",
"building": "building_a",
"zone_type": "office"
}
)

Commands (commands.proto)

Purpose: Control commands for building systems

syntax = "proto3";
package citadel.v1;

import "google/protobuf/timestamp.proto";

// Control command
message Command {
// Unique command ID (ULID)
string id = 1;

// Target entity (door, HVAC zone, light, etc.)
string target_id = 2;

// Action to perform (e.g., "unlock", "set_temp", "dim")
string action = 3;

// Action parameters
map<string, string> params = 4;

// Time-to-live for command
int32 ttl_seconds = 5;

// OPA-issued safety token
string safety_token = 6;

// Issuing agent/user SPIFFE ID
string issued_by = 7;

// Issue timestamp
google.protobuf.Timestamp issued_at = 8;

// Command priority
Priority priority = 9;
}

// Command priority levels
enum Priority {
PRIORITY_UNSPECIFIED = 0;
PRIORITY_LOW = 1; // Optimization, nice-to-have
PRIORITY_NORMAL = 2; // Standard operations
PRIORITY_HIGH = 3; // Urgent but not emergency
PRIORITY_EMERGENCY = 4; // Life safety, bypasses some checks
}

// Command execution result
message CommandResult {
// Reference to original command
string command_id = 1;

// Execution success
bool success = 2;

// Result message (error details if failed)
string message = 3;

// Vendor response data
map<string, string> response_data = 4;

// Execution timestamp
google.protobuf.Timestamp executed_at = 5;

// Execution time in milliseconds
int32 execution_time_ms = 6;
}

// Safety policy violation
message PolicyViolation {
// Reference to blocked command
string command_id = 1;

// Violated policy name
string policy_name = 2;

// Human-readable reason
string reason = 3;

// Policy evaluation context (for replay)
map<string, string> context = 4;

// Violation timestamp
google.protobuf.Timestamp occurred_at = 5;
}

Example Usage:

from citadel.v1 import commands_pb2

command = commands_pb2.Command(
id=ulid(),
target_id="door.lobby.main",
action="unlock_door",
params={
"duration_seconds": "300",
"reason": "Security incident response"
},
ttl_seconds=600,
safety_token="<OPA token>",
issued_by="spiffe://citadel.mesh/agent/security",
priority=commands_pb2.PRIORITY_HIGH
)

Incidents (incidents.proto)

Purpose: Security and operational incidents

syntax = "proto3";
package citadel.v1;

import "google/protobuf/timestamp.proto";

// Security or operational incident
message Incident {
// Unique incident ID (ULID)
string id = 1;

// Incident type
IncidentType type = 2;

// Severity level
Severity severity = 3;

// Location/zone identifier
string location = 4;

// Related signals (camera IDs, sensor IDs, events)
repeated string signals = 5;

// AI-generated hypothesis or description
string hypothesis = 6;

// Actions planned or executed
repeated string actions = 7;

// Owner (agent or human)
string owner = 8;

// Current status
IncidentStatus status = 9;

// Creation timestamp
google.protobuf.Timestamp created_at = 10;

// Resolution timestamp (if resolved)
google.protobuf.Timestamp resolved_at = 11;

// Additional metadata
map<string, string> metadata = 12;
}

enum IncidentType {
INCIDENT_TYPE_UNSPECIFIED = 0;
UNAUTHORIZED_ACCESS = 1;
FORCED_ENTRY = 2;
LOITERING = 3;
AFTER_HOURS_ACCESS = 4;
TAILGATING = 5;
FIRE_ALARM = 6;
INTRUSION_ALARM = 7;
HVAC_FAILURE = 8;
POWER_OUTAGE = 9;
}

enum Severity {
SEVERITY_UNSPECIFIED = 0;
INFO = 1;
LOW = 2;
MEDIUM = 3;
HIGH = 4;
CRITICAL = 5;
}

enum IncidentStatus {
STATUS_UNSPECIFIED = 0;
OPEN = 1;
IN_PROGRESS = 2;
ESCALATED = 3;
RESOLVED = 4;
CLOSED = 5;
}

Example Usage:

from citadel.v1 import incidents_pb2

incident = incidents_pb2.Incident(
id=ulid(),
type=incidents_pb2.FORCED_ENTRY,
severity=incidents_pb2.CRITICAL,
location="door.server_room",
signals=[
"door.server_room.sensor",
"camera.corridor_02.analytics"
],
hypothesis="Door forced open, no valid access card presented",
actions=["lock_adjacent_doors", "alert_security", "capture_video"],
owner="spiffe://citadel.mesh/agent/security",
status=incidents_pb2.OPEN
)

Policy (policy.proto)

Purpose: Policy evaluation requests and results

syntax = "proto3";
package citadel.v1;

import "google/protobuf/timestamp.proto";
import "google/protobuf/struct.proto";

// Policy evaluation request
message EvaluationRequest {
// Policy ID to evaluate
string policy_id = 1;

// Evaluation context (JSON-encoded)
google.protobuf.Struct context = 2;

// Request ID for correlation
string request_id = 3;

// Requesting entity SPIFFE ID
string requester = 4;
}

// Policy evaluation result
message EvaluationResult {
// Original request ID
string request_id = 1;

// Policy ID evaluated
string policy_id = 2;

// Allow or deny
bool allow = 3;

// Human-readable rationale
string rationale = 4;

// Applied constraints
repeated string constraints = 5;

// Full explain trace (for debugging)
google.protobuf.Struct explain_trace = 6;

// Evaluation timestamp
google.protobuf.Timestamp evaluated_at = 7;

// Safety token (if allowed)
string safety_token = 8;
}

Digital Twin (twin.proto)

Purpose: Digital twin state and mutations

syntax = "proto3";
package citadel.v1;

import "google/protobuf/timestamp.proto";
import "google/protobuf/struct.proto";

// Digital twin entity
message Entity {
// Entity ID (e.g., "hvac.zone1")
string entity_id = 1;

// Entity type (HVAC zone, door, camera, etc.)
string entity_type = 2;

// Parent entity (for hierarchy)
string parent_id = 3;

// Site/building ID
string site_id = 4;

// Ontology model (brick, haystack, custom)
string ontology_model = 5;

// Entity attributes (current state)
map<string, string> attributes = 6;

// Relationships to other entities
repeated Relationship relationships = 7;

// Last updated timestamp
google.protobuf.Timestamp updated_at = 8;
}

// Entity relationship
message Relationship {
// Relationship type (feeds, controls, part_of, etc.)
string type = 1;

// Target entity ID
string target_id = 2;

// Relationship metadata
map<string, string> metadata = 3;
}

// Twin mutation (state change)
message TwinMutation {
// Entity ID to mutate
string entity_id = 1;

// Mutation operation
MutationOp op = 2;

// Ontology model
string ontology_model = 3;

// Mutation payload (JSON-LD or JSON)
string payload = 4;

// Mutation source
string source = 5;

// Mutation timestamp
google.protobuf.Timestamp timestamp = 6;
}

enum MutationOp {
MUTATION_OP_UNSPECIFIED = 0;
UPSERT = 1; // Create or update
PATCH = 2; // Partial update
DELETE = 3; // Remove entity
}

Schema Evolution

Backward Compatibility Rules

  1. Never change field numbers
message Point {
string entity_id = 1; // NEVER change this to 2
string metric = 2;
// ...
}
  1. Add optional fields only
message Point {
string entity_id = 1;
string metric = 2;
double value = 3;
string unit = 4;
google.protobuf.Timestamp timestamp = 5;
string quality = 6;
map<string, string> attributes = 7;

// New field (safe to add)
string source_device_id = 8; // ✅ OK
}
  1. Reserve removed fields
message Point {
reserved 9; // Previously used field
reserved "old_field_name";

string entity_id = 1;
// ...
}
  1. Use new message versions for breaking changes
// Old version
package citadel.v1;
message Command { ... }

// New version with breaking changes
package citadel.v2;
message Command { ... }

Evolution Example

// v1.0 - Initial version
message Point {
string entity_id = 1;
string metric = 2;
double value = 3;
}

// v1.1 - Add optional fields
message Point {
string entity_id = 1;
string metric = 2;
double value = 3;
string unit = 4; // ✅ Added
Timestamp timestamp = 5; // ✅ Added
}

// v1.2 - Add quality field
message Point {
string entity_id = 1;
string metric = 2;
double value = 3;
string unit = 4;
Timestamp timestamp = 5;
string quality = 6; // ✅ Added
}

// v2.0 - Breaking change (requires new package)
package citadel.v2;
message Point {
string id = 1; // ❌ Breaking: renamed entity_id
Metric metric = 2; // ❌ Breaking: changed type
// ...
}

Code Generation

Multi-Language Support

# buf.gen.yaml
version: v1
plugins:
# Python
- plugin: python
out: src/proto_gen/python
opt: pyi_out=src/proto_gen/python

# Python gRPC
- plugin: grpc-python
out: src/proto_gen/python

# C# / .NET
- plugin: csharp
out: src/proto_gen/csharp
opt:
- base_namespace=CitadelMesh

# C# gRPC
- plugin: grpc-csharp
out: src/proto_gen/csharp

# TypeScript
- plugin: es
out: src/proto_gen/typescript
opt:
- target=ts

# TypeScript gRPC-Web
- plugin: grpc-web
out: src/proto_gen/typescript

Generated Code Usage

Python:

from citadel.v1 import telemetry_pb2

point = telemetry_pb2.Point()
point.entity_id = "hvac.zone1.temp"
point.value = 72.5

# Serialize
data = point.SerializeToString()

# Deserialize
received = telemetry_pb2.Point()
received.ParseFromString(data)

C#:

using CitadelMesh.V1;

var point = new Point
{
EntityId = "hvac.zone1.temp",
Value = 72.5
};

// Serialize
byte[] data = point.ToByteArray();

// Deserialize
var received = Point.Parser.ParseFrom(data);

TypeScript:

import { Point } from './proto/citadel/v1/telemetry_pb';

const point = new Point();
point.setEntityId('hvac.zone1.temp');
point.setValue(72.5);

// Serialize
const data: Uint8Array = point.serializeBinary();

// Deserialize
const received = Point.deserializeBinary(data);

Validation

CI Pipeline Checks

# .github/workflows/proto-validation.yml
name: Protobuf Validation
on: [pull_request]

jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Buf lint
run: buf lint

- name: Buf breaking change detection
run: buf breaking --against '.git#branch=main'

- name: Generate code
run: buf generate

- name: Run tests
run: pytest tests/proto/

See Also