solti-api
Dual-transport API layer exposing task operations over gRPC and HTTP.
Both transports delegate to an ApiHandler trait, decoupling wire format from business logic. Both speak the same proto contract defined in proto/solti/v1/.
Architecture
Control Plane / Client
│
├──► gRPC (feature = "grpc")
│ └──► SoltiApiService<H>
│ │
├──► HTTP (feature = "http")
│ └──► HttpApi<H> (axum Router)
│ │
▼ ▼
ApiHandler trait (transport-agnostic)
│
▼
SupervisorApiAdapter
│
▼
solti_core::SupervisorApi
Versioning
solti-api exports API_VERSION: u32 - the current protocol version.
Binary passes it to solti_discover::DiscoverConfig::builder(... , API_VERSION), which reports it to the control-plane via SyncRequest. One binary = one API version.
use API_VERSION;
use ;
use AgentId;
let config = builder
.build?;
Bump rules:
- New field in existing message - no bump (proto3 backwards compatible)
- New RPC - no bump (control-plane does not call unsupported RPCs)
- Removed/renamed field, changed semantics - bump
- New proto package (
solti.v2) - bump
Internal crate compatibility is handled by cargo semver.
Per-version API surface is documented in separate files: api_v1.md.
Key types
| Type | Role |
|---|---|
ApiHandler |
Transport-agnostic trait with 6 operations |
SupervisorApiAdapter |
Default adapter bridging to SupervisorApi |
ApiError |
Unified error mapped to gRPC Status / HTTP JSON |
SoltiApiService<H> |
gRPC server impl (feature grpc) |
HttpApi<H> |
axum router builder (feature http) |
API_VERSION |
Protocol version constant reported via discover |
Error model
| Variant | gRPC Status | HTTP Status | error label (HTTP body) |
|---|---|---|---|
InvalidRequest |
INVALID_ARGUMENT |
400 Bad Request |
"InvalidRequest" |
TaskNotFound |
NOT_FOUND |
404 Not Found |
"TaskNotFound" |
Internal |
INTERNAL |
500 Internal Server Error |
"Internal" |
Core |
derived from inner CoreError |
derived from inner | flattened to InvalidRequest / Internal |
Core is split by the wrapped [solti_core::CoreError]: InvalidSpec → INVALID_ARGUMENT / 400 / "InvalidRequest"; everything else → INTERNAL / 500 / "Internal".
HTTP error body:
HTTP requests return 413 Payload Too Large with a JSON envelope ({"error": "PayloadTooLarge", "message": "…"}) when the body exceeds MAX_REQUEST_BYTES (4 MiB).
gRPC calls return RESOURCE_EXHAUSTED for oversize messages.
Script bodies are separately capped in the model at [solti_model::MAX_SCRIPT_BODY_BYTES] (2 MiB after base64 decode): oversize bodies are rejected as InvalidRequest.
Feature flags
| Flag | Enables | Dependencies |
|---|---|---|
grpc |
SoltiApiService, SoltiApiServer, proto codegen |
tonic, tonic-prost, prost |
http |
HttpApi, axum router, proto-JSON serde |
axum, serde_json, prost, pbjson |
Neither feature is enabled by default.
Build
build.rs walks proto/ recursively, collecting every *.proto file (emitting rerun-if-changed for each). Two codegen passes:
tonic_prost_build::configure(): message types always, tonic server/client only undergrpc.pbjson_buildunderhttp: attaches canonical proto-JSONSerialize/Deserializeto the same message types, with.emit_fields()enabled so REST clients see0/false/""/[]/{}for default scalar/repeated/map values (optionalmessagefields still omit onNone).
The proto package selector lives at the top of build.rs as const PROTO_PACKAGE = ".solti.v1";.
If the package declaration in a .proto changes, update this constant — otherwise pbjson generates nothing and HTTP compile fails.
Adding new .proto files anywhere under proto/ requires no changes to build.rs.
Notes
ApiHandlerusesasync_traitfor object safety (Send + Sync + 'static).- Both transports feed input through the same
convert_create_specvalidator. - Re-exports:
solti_api::tonic,solti_api::axumfor version pinning. - Proto contract in
proto/solti/v1/(api.proto,types.proto).