solti-api 0.0.2

Solti SDK HTTP/JSON and gRPC API layer.
Documentation

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 solti_api::API_VERSION;
use solti_discover::{DiscoverConfig, DiscoveryTransport};
use solti_model::AgentId;

let config = DiscoverConfig::builder(
    AgentId::new("agentd-001"),
    "agentd",
    "http://0.0.0.0:8085",
    "http://podium:8082",
    DiscoveryTransport::Http,
    10_000,
    API_VERSION,
)
.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 (CRUD + log stream)
OutputEventStream Pin<Box<dyn Stream<Item = OutputEvent> + Send>> returned by stream_task_logs
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]: InvalidSpecINVALID_ARGUMENT / 400 / "InvalidRequest"; everything else → INTERNAL / 500 / "Internal".

HTTP error body:

{ "error": "<label>", "message": "<detail>" }

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
tls to_tonic_server_tls(&ServerTlsConfig) adapter (under grpc); pulls solti-tls solti-tls; activates tonic/tls-ring

No feature is enabled by default. tls is additive on top of grpc (the adapter targets tonic; HTTP TLS is terminated by the binary via axum-server, see below).

Enabling TLS

For gRPC:

use solti_api::{build_grpc_server, to_tonic_server_tls};
use solti_tls::ServerTlsConfig;

let server_tls = ServerTlsConfig::builder()
    .cert_pem_file("/etc/solti/tls/server.crt")
    .key_pem_file("/etc/solti/tls/server.key")
    .require_client_ca_pem_file("/etc/solti/tls/clients-ca.crt")  // optional
    .build()?;

let tls_cfg = to_tonic_server_tls(&server_tls)?;
tonic::transport::Server::builder()
    .tls_config(tls_cfg)?
    .add_service(build_grpc_server(adapter))
    .serve("0.0.0.0:50443".parse()?)
    .await?;

For HTTP, terminate TLS in your binary via axum-server using the rustls::ServerConfig produced by solti_tls::ServerTlsConfig::into_rustls_config(). See the solti-tls README for the full pattern.

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 under grpc.
  • pbjson_build under http: attaches canonical proto-JSON Serialize/Deserialize to the same message types, with .emit_fields() enabled so REST clients see 0 / false / "" / [] / {} for default scalar/repeated/map values (optional message fields still omit on None).

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

  • ApiHandler uses async_trait for object safety (Send + Sync + 'static).
  • Both transports feed input through the same convert_create_spec validator.
  • Re-exports: solti_api::tonic, solti_api::axum for version pinning.
  • Proto contract in proto/solti/v1/ (api.proto, types.proto).