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:
| RPC | Direction | Role |
|---|---|---|
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.