Control-plane API

One streaming gRPC protocol drives discovery, routing weights, and flag/sovereignty rules for proxyless SDK clients. The .proto files at the repository root (proto/dona/v1/control.proto, proto/dona/v1/identity.proto) are the shared contract that both the Go and Rust implementations consume.

The service

service ControlPlane {
  // Workloads register themselves over a long-lived stream; the endpoint is
  // removed from the routing table when the stream breaks (lease-by-stream).
  rpc RegisterEndpoint(stream EndpointHeartbeat) returns (RegisterAck);

  // SDK clients subscribe to config for one service; the server pushes a new
  // snapshot whenever endpoints, weights, or flag rules change. Per-service
  // scoping keeps each client's config blast radius minimal (delta-xDS spirit).
  rpc WatchConfig(WatchConfigRequest) returns (stream ConfigSnapshot);

  // SDK clients batch-report per-call outcomes and flag exposures; this feeds
  // the canary analysis loop and the audit log.
  rpc ReportStats(StatsBatch) returns (ReportAck);

  // --- Admin / CLI surface ---
  rpc SetFlag(SetFlagRequest) returns (Empty);
  rpc StartRollout(StartRolloutRequest) returns (Empty);
  rpc RegisterArtifact(RegisterArtifactRequest) returns (Artifact);
  rpc CreateRelease(CreateReleaseRequest) returns (Release);
  rpc StartDeployment(StartDeploymentRequest) returns (Deployment);
  rpc SetIngress(SetIngressRequest) returns (Empty);
  rpc SetSharedService(SetSharedServiceRequest) returns (Empty);
  rpc GetStatus(GetStatusRequest) returns (StatusReply);

  // Auditor-ready sovereignty attestation: the hash-chained audit log for a
  // service over a time range, plus a signature over the chain head
  // (DESIGN-RUST.md §10).
  rpc GetAttestation(AttestationRequest) returns (AttestationReply);
}

Three streaming RPCs carry the whole data-plane relationship:

RPCDirectionRole
RegisterEndpoint workload → brain (client stream) The endpoint lease. Heartbeats keep it alive (and carry the workload's current load, which the brain turns into per-endpoint lb_weights); stream death is deregistration.
WatchConfig brain → SDK (server stream) Per-service snapshots. Per-client scoping keeps each client's config blast radius minimal (the delta-xDS spirit).
ReportStats SDK → brain (unary, batched) Outcomes feed the canary health windows; exposures feed experiment analysis; audit events build the sovereignty trail.

SetFlag, StartRollout, deployment, ingress/shared-service, GetStatus, and GetAttestation RPCs are the admin surface that dona drives. The full reference in docs/api-control-plane.md is authoritative.

Config: what an SDK client receives

// ConfigSnapshot is the full per-service config an SDK client needs to make
// every per-request decision in-process.
message ConfigSnapshot {
  string service = 1;
  uint64 version = 2;
  repeated Endpoint endpoints = 3;
  repeated VariantWeight weights = 4;  // soft selection baseline (canary/blue-green)
  repeated FlagRule flags = 5;         // soft selection overrides (feature flags)
}

A snapshot is everything the in-process pipeline needs: the endpoint set (with variant, region, and the brain-computed lb_weight for the constraint filter and load-aware picking), the variant weights the canary controller is currently driving, and the flag rules.

// FlagRule routes individual requests to a variant of the *target service*
// based on caller-supplied attributes — evaluated in-process, deterministic.
message FlagRule {
  string flag = 1;     // flag key, e.g. "use-v2"
  string service = 2;  // service whose routing this flag controls

  // Targeting: explicit attribute matches evaluated in order, first hit wins.
  repeated TargetingRule targeting = 3;

  // Percentage rollout: sticky bucketing by hash(unit, flag) — the same user
  // always lands in the same bucket (DESIGN.md §5.4).
  uint32 rollout_percent = 4;  // 0..100 of remaining traffic
  string rollout_variant = 5;  // variant the bucketed cohort gets
}

Bucketing semantics for rollout_percent are the cross-language FNV-64a contract.

Stats: what an SDK client reports

message StatsBatch {
  repeated CallOutcome calls = 1;
  repeated Exposure exposures = 2;
  repeated AuditEvent audits = 3;
}

This stream is control-loop data, not telemetry: call outcomes drive the canary state machine and audit events extend the hash-chained sovereignty log, so they stay on this gRPC API. Operator-facing traces and metrics leave separately, over OpenTelemetry/OTLP, with Dodona semantic conventions (dona.service, dona.variant, dona.decision, …).

Admin: rollouts and status

message StartRolloutRequest {
  string service = 1;
  string canary_variant = 2;        // variant being introduced
  string stable_variant = 3;        // variant currently serving
  repeated uint32 steps = 4;        // percent steps, e.g. [5,25,50,100]
  uint32 step_seconds = 5;          // dwell time per step
  uint32 max_error_percent = 6;     // rollback threshold
  uint32 min_requests = 7;          // min sample size before judging a step

  // Optional external analysis queries evaluated alongside the built-in
  // error-rate check. Any breach rolls back; missing data holds the step
  // (never promote blind).
  repeated AnalysisQuery analysis = 8;
}

Beyond the built-in error-rate gate, a rollout can carry external analysis queries. The hold-vs-rollback semantics are deliberate: a breach on any query rolls back immediately, while missing data holds the step — the controller never promotes blind.

// One external health signal for canary analysis (e.g. a Prometheus query
// whose scalar result must stay at or below max_value).
message AnalysisQuery {
  string name = 1;            // for status/log lines
  string prometheus_url = 2;  // base URL of a Prometheus-compatible API
  string query = 3;           // instant query; must evaluate to a scalar
  double max_value = 4;       // breach when result > max_value
}
message StatusReply {
  ConfigSnapshot config = 1;
  RolloutStatus rollout = 2;
  repeated VariantStats stats = 3;  // rolling-window health per variant
}

Attestation: provable sovereignty

message AttestationRequest {
  string service = 1;
  int64 from_unix = 2;  // 0 = beginning of the log
  int64 to_unix = 3;    // 0 = now
}

message AttestationReply {
  // The report: audit entries + hash chain metadata, JSON-encoded so
  // auditors can read it without Dodona tooling.
  bytes report_json = 1;
  // Signature over report_json by the brain's attestation key.
  bytes signature = 2;
  bytes signer_public_key_pem = 3;
}

The reply is self-contained: dona attest (or any auditor, offline, with no Dodona tooling) verifies the ed25519 signature over the exact report bytes and recomputes the hash chain across the included entries — mutation, reordering, insertion, and in-range deletion are all detected with nothing but the report, the signature, and the public key.

Identity: SPIFFE SVID issuance

service Identity {
  rpc IssueSvid(IssueSvidRequest) returns (IssueSvidResponse);
}

message IssueSvidRequest {
  string bootstrap_token = 1;
  // Logical service name; becomes spiffe://<trust-domain>/svc/<service>.
  string service = 2;
  // PEM-encoded PKCS#10 CSR. When set, the CA signs this request and the
  // private key never leaves the workload (key_pem is empty in the
  // response). When empty, the brain generates the keypair (legacy mode).
  bytes csr_pem = 3;
}

message IssueSvidResponse {
  string spiffe_id = 1;
  bytes cert_pem = 2;       // the SVID certificate
  bytes key_pem = 3;        // its private key
  bytes bundle_pem = 4;     // CA trust bundle
  int64 expires_unix = 5;
}

Issuance is token-gated on a separate bootstrap listener — the one plaintext exchange in a secured deployment; everything after it runs over mTLS. With csr_pem set, the workload keeps its private key and the CA signs only the public key, imposing server-decided SANs/subject regardless of what the CSR requested. SVIDs default to a 1-hour TTL; the Rust SDK's RotatingIdentity renews via the CSR path at 50% TTL.

Stability: this surface is shared by the Go and Rust trees and grows additively. Go remains the conformance peer, not a feature-equal SDK: deployment, federation, and observe wrapper APIs are documented gaps there. Snapshot versioning is already in the API so delta encoding can slot in without a protocol change.