Refine Rust architecture boundaries
Deploy / deploy (push) Successful in 1m33s
CI / Rust Checks (push) Failing after 5m47s
CI / UI Checks (push) Has been skipped
CI / Frontend E2E (push) Has been skipped
CI / Deployment Manifests (push) Has been skipped

This commit is contained in:
github-ops
2026-06-21 08:58:32 +00:00
parent a31862aeeb
commit 7df9b48513
18 changed files with 1376 additions and 1052 deletions
+16 -304
View File
@@ -10,12 +10,9 @@ use axum::{
extract::{Path, State},
http::{
HeaderMap, HeaderValue, StatusCode,
header::{self, ACCEPT, AUTHORIZATION, HeaderName, RETRY_AFTER},
},
response::{
IntoResponse, Response,
sse::{Event, KeepAlive, Sse},
header::{AUTHORIZATION, RETRY_AFTER},
},
response::{IntoResponse, Response, sse::Event},
routing::get,
};
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
@@ -25,8 +22,8 @@ use crank_core::{
};
use crank_registry::{CreateInvocationLogRequest, PostgresRegistry, PublishedAgentTool};
use crank_runtime::{
RateLimitRejection, RequestRateLimiter, ResolvedAuth, RuntimeError, RuntimeExecutor,
RuntimeOperation, RuntimeRequestContext, SecretCrypto,
RateLimitRejection, RequestRateLimiter, ResolvedAuth, RuntimeError, RuntimeExecutionRequest,
RuntimeExecutor, RuntimeOperation, RuntimeRequestContext, SecretCrypto,
};
use futures_util::stream;
use serde::{Deserialize, Serialize};
@@ -39,9 +36,8 @@ use crate::{
auth::{SharedMachineCredentialVerifier, VerifiedMachineCredential},
catalog::PublishedToolCatalog,
jsonrpc::{
CURRENT_PROTOCOL_VERSION, DEFAULT_PROTOCOL_VERSION, is_notification, is_request,
is_response, jsonrpc_error, jsonrpc_result, method_name, negotiated_protocol_version,
params, request_id,
DEFAULT_PROTOCOL_VERSION, is_notification, is_request, is_response, jsonrpc_error,
jsonrpc_result, method_name, negotiated_protocol_version, params, request_id,
},
manifest::tool_definitions,
session::{SessionState, SharedSessionStore},
@@ -49,20 +45,16 @@ use crate::{
ToolErrorContract, generic_tool_error_contract, runtime_error_code,
tool_error_contract_from_runtime, tool_error_text, tool_error_value,
},
transport::{
AllowedOrigins, HEADER_MCP_SESSION_ID, ResponseMode, json_response,
negotiate_post_response_mode, protocol_version_from_headers, resolve_request_id,
session_id_from_headers, sse_response, transport_response, validate_get_accept_header,
validate_origin, validate_session_protocol_version, with_request_id_header,
},
};
const HEADER_MCP_SESSION_ID: &str = "MCP-Session-Id";
const HEADER_MCP_PROTOCOL_VERSION: &str = "MCP-Protocol-Version";
const HEADER_X_REQUEST_ID: HeaderName = HeaderName::from_static("x-request-id");
const MAX_REQUEST_ID_LEN: usize = 128;
const TRANSPORT_SESSION_TTL_MS: u64 = 86_400_000;
#[derive(Clone, Copy)]
enum ResponseMode {
Json,
Sse,
}
#[derive(Clone)]
pub struct AppState {
registry: PostgresRegistry,
@@ -75,11 +67,6 @@ pub struct AppState {
allowed_origins: AllowedOrigins,
}
#[derive(Clone)]
pub struct AllowedOrigins {
public_origin: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct InitializeParams {
#[serde(rename = "protocolVersion")]
@@ -623,11 +610,10 @@ async fn handle_base_tool_call(
Ok(resolved_auth) => {
state
.runtime
.execute_with_auth_and_context(
&operation,
&arguments,
resolved_auth.as_ref(),
Some(&runtime_request_context),
.execute_request(
RuntimeExecutionRequest::new(&operation, &arguments)
.with_optional_auth(resolved_auth.as_ref())
.with_context(&runtime_request_context),
)
.await
}
@@ -1022,141 +1008,6 @@ fn serialize_machine_access_mode(mode: crank_core::MachineAccessMode) -> &'stati
}
}
fn validate_origin(
allowed_origins: &AllowedOrigins,
headers: &HeaderMap,
) -> Result<(), StatusCode> {
let Some(origin) = headers.get(header::ORIGIN) else {
return Ok(());
};
let Ok(origin) = origin.to_str() else {
return Err(StatusCode::FORBIDDEN);
};
if allowed_origins.is_allowed(origin) {
return Ok(());
}
Err(StatusCode::FORBIDDEN)
}
fn negotiate_post_response_mode(headers: &HeaderMap) -> Result<ResponseMode, StatusCode> {
let Some(accept) = headers.get(ACCEPT) else {
return Err(StatusCode::BAD_REQUEST);
};
let Ok(accept) = accept.to_str() else {
return Err(StatusCode::BAD_REQUEST);
};
let mut saw_json = false;
let mut saw_sse = false;
let mut preferred = None;
for part in accept.split(',') {
let media_type = part
.split(';')
.next()
.unwrap_or_default()
.trim()
.to_ascii_lowercase();
match media_type.as_str() {
"application/json" => {
saw_json = true;
if preferred.is_none() {
preferred = Some(ResponseMode::Json);
}
}
"text/event-stream" => {
saw_sse = true;
if preferred.is_none() {
preferred = Some(ResponseMode::Sse);
}
}
_ => {}
}
}
if saw_json || saw_sse {
return preferred.ok_or(StatusCode::NOT_ACCEPTABLE);
}
Err(StatusCode::NOT_ACCEPTABLE)
}
fn validate_get_accept_header(headers: &HeaderMap) -> Result<(), StatusCode> {
let Some(accept) = headers.get(ACCEPT) else {
return Err(StatusCode::BAD_REQUEST);
};
let Ok(accept) = accept.to_str() else {
return Err(StatusCode::BAD_REQUEST);
};
if accept
.split(',')
.map(|part| {
part.split(';')
.next()
.unwrap_or_default()
.trim()
.to_ascii_lowercase()
})
.any(|media_type| media_type == "text/event-stream")
{
return Ok(());
}
Err(StatusCode::NOT_ACCEPTABLE)
}
fn protocol_version_from_headers(headers: &HeaderMap) -> Result<String, StatusCode> {
let Some(version) = headers.get(HEADER_MCP_PROTOCOL_VERSION) else {
return Ok(DEFAULT_PROTOCOL_VERSION.to_owned());
};
let Ok(version) = version.to_str() else {
return Err(StatusCode::BAD_REQUEST);
};
if negotiated_protocol_version(version).is_some() {
return Ok(version.to_owned());
}
Err(StatusCode::BAD_REQUEST)
}
fn validate_session_protocol_version(
headers: &HeaderMap,
negotiated_session_version: &str,
) -> Result<(), StatusCode> {
let Some(version) = headers.get(HEADER_MCP_PROTOCOL_VERSION) else {
return Ok(());
};
let Ok(version) = version.to_str() else {
return Err(StatusCode::BAD_REQUEST);
};
if negotiated_protocol_version(version).is_none() {
return Err(StatusCode::BAD_REQUEST);
}
if version != negotiated_session_version {
return Err(StatusCode::BAD_REQUEST);
}
Ok(())
}
fn session_id_from_headers(headers: &HeaderMap) -> Result<Option<String>, StatusCode> {
let Some(session_id) = headers.get(HEADER_MCP_SESSION_ID) else {
return Ok(None);
};
let Ok(session_id) = session_id.to_str() else {
return Err(StatusCode::BAD_REQUEST);
};
Ok(Some(session_id.to_owned()))
}
fn internal_jsonrpc_error(message: &Value, error: impl std::fmt::Display) -> Response {
transport_response(
StatusCode::INTERNAL_SERVER_ERROR,
@@ -1308,109 +1159,6 @@ fn hash_access_secret(secret: &str) -> String {
URL_SAFE_NO_PAD.encode(digest)
}
fn json_response(
status: StatusCode,
payload: Value,
session_id: Option<&str>,
protocol_version: Option<&str>,
) -> Response {
let mut response = (status, Json(payload)).into_response();
if let Some(session_id) = session_id {
response.headers_mut().insert(
HEADER_MCP_SESSION_ID,
HeaderValue::from_str(session_id)
.unwrap_or_else(|_| HeaderValue::from_static("invalid")),
);
}
if let Some(protocol_version) = protocol_version {
response.headers_mut().insert(
HEADER_MCP_PROTOCOL_VERSION,
HeaderValue::from_str(protocol_version)
.unwrap_or_else(|_| HeaderValue::from_static(CURRENT_PROTOCOL_VERSION)),
);
}
response
}
fn transport_response(
status: StatusCode,
payload: Value,
response_mode: ResponseMode,
session_id: Option<&str>,
protocol_version: Option<&str>,
) -> Response {
if status == StatusCode::OK && matches!(response_mode, ResponseMode::Sse) {
let payload = payload.to_string();
let stream = stream::once(async move { Ok(Event::default().data(payload)) });
return sse_response(status, stream, session_id, protocol_version);
}
json_response(status, payload, session_id, protocol_version)
}
fn with_request_id_header(mut response: Response, request_id: &str) -> Response {
if let Ok(value) = HeaderValue::from_str(request_id) {
response.headers_mut().insert(HEADER_X_REQUEST_ID, value);
}
response
}
fn sse_response<S>(
status: StatusCode,
stream: S,
session_id: Option<&str>,
protocol_version: Option<&str>,
) -> Response
where
S: futures_util::stream::Stream<Item = Result<Event, Infallible>> + Send + 'static,
{
let mut response = (
status,
Sse::new(stream).keep_alive(KeepAlive::new().interval(Duration::from_secs(15))),
)
.into_response();
if let Some(session_id) = session_id {
response.headers_mut().insert(
HEADER_MCP_SESSION_ID,
HeaderValue::from_str(session_id)
.unwrap_or_else(|_| HeaderValue::from_static("invalid")),
);
}
if let Some(protocol_version) = protocol_version {
response.headers_mut().insert(
HEADER_MCP_PROTOCOL_VERSION,
HeaderValue::from_str(protocol_version)
.unwrap_or_else(|_| HeaderValue::from_static(CURRENT_PROTOCOL_VERSION)),
);
}
response
}
fn resolve_request_id(headers: &HeaderMap) -> String {
headers
.get(&HEADER_X_REQUEST_ID)
.and_then(|value| value.to_str().ok())
.map(str::trim)
.filter(|value| is_valid_request_id(value))
.map(ToOwned::to_owned)
.unwrap_or_else(|| uuid::Uuid::now_v7().to_string())
}
fn is_valid_request_id(value: &str) -> bool {
!value.is_empty()
&& value.len() <= MAX_REQUEST_ID_LEN
&& value
.bytes()
.all(|byte| matches!(byte, 0x21..=0x7e) && byte != b',' && byte != b';')
}
fn resolve_generated_tool(
tools: &[PublishedAgentTool],
tool_name: &str,
@@ -1432,42 +1180,6 @@ fn runtime_operation(tool: &PublishedAgentTool) -> RuntimeOperation {
operation
}
impl AllowedOrigins {
fn new(public_base_url: Option<String>) -> Self {
Self {
public_origin: public_base_url.and_then(|value| extract_origin(&value)),
}
}
fn is_allowed(&self, origin: &str) -> bool {
if origin.starts_with("http://localhost")
|| origin.starts_with("http://127.0.0.1")
|| origin.starts_with("https://localhost")
|| origin.starts_with("https://127.0.0.1")
{
return true;
}
match &self.public_origin {
Some(public_origin) => public_origin == origin,
None => false,
}
}
}
fn extract_origin(url: &str) -> Option<String> {
let mut parts = url.split('/');
let scheme = parts.next()?;
let empty = parts.next()?;
let authority = parts.next()?;
if empty.is_empty() {
return Some(format!("{scheme}//{authority}"));
}
None
}
#[cfg(test)]
mod tests {
use axum::body::to_bytes;
+1
View File
@@ -5,5 +5,6 @@ pub mod jsonrpc;
pub mod manifest;
pub mod session;
pub mod tool_error;
mod transport;
pub use app::build_app;
+311
View File
@@ -0,0 +1,311 @@
use std::{convert::Infallible, time::Duration};
use axum::{
Json,
http::{
HeaderMap, HeaderValue, StatusCode,
header::{self, ACCEPT, HeaderName},
},
response::{
IntoResponse, Response,
sse::{Event, KeepAlive, Sse},
},
};
use futures_util::stream;
use serde_json::Value;
use crate::jsonrpc::{
CURRENT_PROTOCOL_VERSION, DEFAULT_PROTOCOL_VERSION, negotiated_protocol_version,
};
pub(super) const HEADER_MCP_SESSION_ID: &str = "MCP-Session-Id";
pub(super) const HEADER_MCP_PROTOCOL_VERSION: &str = "MCP-Protocol-Version";
pub(super) const HEADER_X_REQUEST_ID: HeaderName = HeaderName::from_static("x-request-id");
const MAX_REQUEST_ID_LEN: usize = 128;
#[derive(Clone, Copy)]
pub(super) enum ResponseMode {
Json,
Sse,
}
#[derive(Clone)]
pub(super) struct AllowedOrigins {
public_origin: Option<String>,
}
impl AllowedOrigins {
pub(super) fn new(public_base_url: Option<String>) -> Self {
Self {
public_origin: public_base_url.and_then(|value| extract_origin(&value)),
}
}
pub(super) fn is_allowed(&self, origin: &str) -> bool {
if origin.starts_with("http://localhost")
|| origin.starts_with("http://127.0.0.1")
|| origin.starts_with("https://localhost")
|| origin.starts_with("https://127.0.0.1")
{
return true;
}
match &self.public_origin {
Some(public_origin) => public_origin == origin,
None => false,
}
}
}
pub(super) fn validate_origin(
allowed_origins: &AllowedOrigins,
headers: &HeaderMap,
) -> Result<(), StatusCode> {
let Some(origin) = headers.get(header::ORIGIN) else {
return Ok(());
};
let Ok(origin) = origin.to_str() else {
return Err(StatusCode::FORBIDDEN);
};
if allowed_origins.is_allowed(origin) {
return Ok(());
}
Err(StatusCode::FORBIDDEN)
}
pub(super) fn negotiate_post_response_mode(
headers: &HeaderMap,
) -> Result<ResponseMode, StatusCode> {
let Some(accept) = headers.get(ACCEPT) else {
return Err(StatusCode::BAD_REQUEST);
};
let Ok(accept) = accept.to_str() else {
return Err(StatusCode::BAD_REQUEST);
};
let mut saw_json = false;
let mut saw_sse = false;
let mut preferred = None;
for part in accept.split(',') {
let media_type = part
.split(';')
.next()
.unwrap_or_default()
.trim()
.to_ascii_lowercase();
match media_type.as_str() {
"application/json" => {
saw_json = true;
if preferred.is_none() {
preferred = Some(ResponseMode::Json);
}
}
"text/event-stream" => {
saw_sse = true;
if preferred.is_none() {
preferred = Some(ResponseMode::Sse);
}
}
_ => {}
}
}
if saw_json || saw_sse {
return preferred.ok_or(StatusCode::NOT_ACCEPTABLE);
}
Err(StatusCode::NOT_ACCEPTABLE)
}
pub(super) fn validate_get_accept_header(headers: &HeaderMap) -> Result<(), StatusCode> {
let Some(accept) = headers.get(ACCEPT) else {
return Err(StatusCode::BAD_REQUEST);
};
let Ok(accept) = accept.to_str() else {
return Err(StatusCode::BAD_REQUEST);
};
if accept
.split(',')
.map(|part| {
part.split(';')
.next()
.unwrap_or_default()
.trim()
.to_ascii_lowercase()
})
.any(|media_type| media_type == "text/event-stream")
{
return Ok(());
}
Err(StatusCode::NOT_ACCEPTABLE)
}
pub(super) fn protocol_version_from_headers(headers: &HeaderMap) -> Result<String, StatusCode> {
let Some(version) = headers.get(HEADER_MCP_PROTOCOL_VERSION) else {
return Ok(DEFAULT_PROTOCOL_VERSION.to_owned());
};
let Ok(version) = version.to_str() else {
return Err(StatusCode::BAD_REQUEST);
};
if negotiated_protocol_version(version).is_some() {
return Ok(version.to_owned());
}
Err(StatusCode::BAD_REQUEST)
}
pub(super) fn validate_session_protocol_version(
headers: &HeaderMap,
negotiated_session_version: &str,
) -> Result<(), StatusCode> {
let Some(version) = headers.get(HEADER_MCP_PROTOCOL_VERSION) else {
return Ok(());
};
let Ok(version) = version.to_str() else {
return Err(StatusCode::BAD_REQUEST);
};
if negotiated_protocol_version(version).is_none() {
return Err(StatusCode::BAD_REQUEST);
}
if version != negotiated_session_version {
return Err(StatusCode::BAD_REQUEST);
}
Ok(())
}
pub(super) fn session_id_from_headers(headers: &HeaderMap) -> Result<Option<String>, StatusCode> {
let Some(session_id) = headers.get(HEADER_MCP_SESSION_ID) else {
return Ok(None);
};
let Ok(session_id) = session_id.to_str() else {
return Err(StatusCode::BAD_REQUEST);
};
Ok(Some(session_id.to_owned()))
}
pub(super) fn json_response(
status: StatusCode,
payload: Value,
session_id: Option<&str>,
protocol_version: Option<&str>,
) -> Response {
let mut response = (status, Json(payload)).into_response();
if let Some(session_id) = session_id {
response.headers_mut().insert(
HEADER_MCP_SESSION_ID,
HeaderValue::from_str(session_id)
.unwrap_or_else(|_| HeaderValue::from_static("invalid")),
);
}
if let Some(protocol_version) = protocol_version {
response.headers_mut().insert(
HEADER_MCP_PROTOCOL_VERSION,
HeaderValue::from_str(protocol_version)
.unwrap_or_else(|_| HeaderValue::from_static(CURRENT_PROTOCOL_VERSION)),
);
}
response
}
pub(super) fn transport_response(
status: StatusCode,
payload: Value,
response_mode: ResponseMode,
session_id: Option<&str>,
protocol_version: Option<&str>,
) -> Response {
if status == StatusCode::OK && matches!(response_mode, ResponseMode::Sse) {
let payload = payload.to_string();
let stream = stream::once(async move { Ok(Event::default().data(payload)) });
return sse_response(status, stream, session_id, protocol_version);
}
json_response(status, payload, session_id, protocol_version)
}
pub(super) fn with_request_id_header(mut response: Response, request_id: &str) -> Response {
if let Ok(value) = HeaderValue::from_str(request_id) {
response.headers_mut().insert(HEADER_X_REQUEST_ID, value);
}
response
}
pub(super) fn sse_response<S>(
status: StatusCode,
stream: S,
session_id: Option<&str>,
protocol_version: Option<&str>,
) -> Response
where
S: futures_util::stream::Stream<Item = Result<Event, Infallible>> + Send + 'static,
{
let mut response = (
status,
Sse::new(stream).keep_alive(KeepAlive::new().interval(Duration::from_secs(15))),
)
.into_response();
if let Some(session_id) = session_id {
response.headers_mut().insert(
HEADER_MCP_SESSION_ID,
HeaderValue::from_str(session_id)
.unwrap_or_else(|_| HeaderValue::from_static("invalid")),
);
}
if let Some(protocol_version) = protocol_version {
response.headers_mut().insert(
HEADER_MCP_PROTOCOL_VERSION,
HeaderValue::from_str(protocol_version)
.unwrap_or_else(|_| HeaderValue::from_static(CURRENT_PROTOCOL_VERSION)),
);
}
response
}
pub(super) fn resolve_request_id(headers: &HeaderMap) -> String {
headers
.get(&HEADER_X_REQUEST_ID)
.and_then(|value| value.to_str().ok())
.map(str::trim)
.filter(|value| is_valid_request_id(value))
.map(ToOwned::to_owned)
.unwrap_or_else(|| uuid::Uuid::now_v7().to_string())
}
pub(super) fn is_valid_request_id(value: &str) -> bool {
!value.is_empty()
&& value.len() <= MAX_REQUEST_ID_LEN
&& value
.bytes()
.all(|byte| matches!(byte, 0x21..=0x7e) && byte != b',' && byte != b';')
}
pub(super) fn extract_origin(url: &str) -> Option<String> {
let mut parts = url.split('/');
let scheme = parts.next()?;
let empty = parts.next()?;
let authority = parts.next()?;
if empty.is_empty() {
return Some(format!("{scheme}//{authority}"));
}
None
}
+71
View File
@@ -12,6 +12,77 @@ pub mod secret;
pub mod tool_quality;
pub mod workspace;
pub mod domain {
pub use crate::access::{
InvitationStatus, InvitationToken, Membership, MembershipRole, PlatformApiKey,
PlatformApiKeyScope, PlatformApiKeyStatus, User, UserStatus,
};
pub use crate::agent::{Agent, AgentOperationBinding, AgentStatus, AgentVersion};
pub use crate::auth::{
ApiKeyHeaderAuthConfig, ApiKeyQueryAuthConfig, AuthConfig, AuthProfile, BasicAuthConfig,
BearerAuthConfig,
};
pub use crate::cache::{
CacheBackend, CacheScope, CachedHeader, CachedResponse, ParseCacheBackendError,
RateLimitBucketState,
};
pub use crate::edition::{
EditionCapabilities, EditionLimits, MachineAccessMode, OperationSecurityLevel,
ProductEdition,
};
pub use crate::ids::{
AgentId, AuditEventId, AuthProfileId, DescriptorId, InvitationId, InvocationLogId,
OperationId, PlatformApiKeyId, SampleId, SecretId, ToolId, UserId, UserSessionId,
WorkspaceId,
};
pub use crate::observability::{
InvocationLevel, InvocationLog, InvocationSource, InvocationStatus, UsagePeriod,
UsageRollup,
};
pub use crate::operation::{
ConfigExport, ConfirmationPolicy, ExecutionConfig, GeneratedDraft, GeneratedDraftStatus,
IdempotencyMode, IdempotencyPolicy, Operation, OperationSafetyClass, OperationSafetyPolicy,
OperationStatus, ResponseCachePolicy, RestTarget, RetryPolicy, Samples, Target,
ToolDescription, ToolExample, WizardState,
};
pub use crate::protocol::{AuthKind, ExportMode, HttpMethod, Protocol};
pub use crate::secret::{Secret, SecretKind, SecretStatus, SecretVersion};
pub use crate::tool_quality::{
ToolQualityCatalogTool, ToolQualityFinding, ToolQualityMappingRule, ToolQualityMappingSet,
ToolQualityReport, ToolQualitySchemaKind, ToolQualitySchemaNode, ToolQualitySeverity,
};
pub use crate::workspace::{Workspace, WorkspaceStatus};
}
pub mod ports {
pub use crate::cache::{
CacheStoreError, CoordinationStateStore, CoordinationStateValue, RateLimitStateStore,
ReplayGuardStatus, ReplayGuardStore, ResponseCacheStore,
};
pub use crate::ext::access::{
OwnerOnlyPolicyEngine, PolicyAction, PolicyDecision, PolicyEngine, PolicyScope,
SessionActor,
};
pub use crate::ext::audit::{
AuditActor, AuditError, AuditEvent, AuditEventPage, AuditQuery, AuditSink, AuditTarget,
AuditTargetKind, NoopAuditSink, SharedAuditSink,
};
pub use crate::ext::auth::{
AuthenticatedIdentity, IdentityError, IdentityProvider, IdentityProviderKind, LoginOutcome,
LoginPayload, MachineCredentialVerifier, MachineCredentialVerifierError,
SharedIdentityProvider, SharedMachineCredentialVerifier, VerifiedMachineCredential,
};
pub use crate::ext::capability::{CapabilityProfile, CommunityCapabilityProfile};
pub use crate::ext::metering::{
MeteringEvent, MeteringSink, NoopMeteringSink, SharedMeteringSink,
};
pub use crate::ext::protocol::{
AdapterRegistry, AdapterResponse, ExecutionMode, MeteringContext, PreparedRequest,
ProtocolAdapter, ProtocolAdapterError, ResponseCacheScope, RuntimeRequestContext,
SharedProtocolAdapter,
};
}
pub use access::{
InvitationStatus, InvitationToken, Membership, MembershipRole, PlatformApiKey,
PlatformApiKeyScope, PlatformApiKeyStatus, User, UserStatus,
+31
View File
@@ -6,6 +6,37 @@ mod postgres;
pub use error::RegistryError;
pub use ext::{ExtensionMigration, RegistryExtension, apply_extension_migrations};
pub mod records {
pub use crate::model::{
AgentSummary, AgentVersionRecord, AuthUserRecord, DescriptorKind, DescriptorMetadata,
InvitationRecord, InvocationLogRecord, MembershipRecord, OperationAgentRef,
OperationSampleMetadata, OperationSummary, OperationUsageSummary, OperationVersionRecord,
Page, PlatformApiKeyRecord, PublishedAgentTool, RegistryOperation, SampleKind,
SecretRecord, SecretVersionRecord, SessionRecord, UsageAgentBreakdown, UsageBucket,
UsageOperationBreakdown, UsageRollupRecord, UsageSummary, UsageTimelinePoint,
WorkspaceMembershipRecord, WorkspaceRecord, WorkspaceUpstream, WorkspaceUpstreamId,
YamlImportJob, YamlImportJobCompletion, YamlImportJobId, YamlImportJobStatus,
};
}
pub mod requests {
pub use crate::model::{
CreateAgentDraftVersionRequest, CreateAgentRequest, CreateInvitationRequest,
CreateInvocationLogRequest, CreatePlatformApiKeyRequest, CreateSecretRequest,
CreateVersionRequest, CreateWorkspaceRequest, CreateYamlImportJobRequest,
ListInvocationLogsQuery, PublishAgentRequest, PublishRequest, RotateSecretRequest,
SaveAgentBindingsRequest, SaveAuthProfileRequest, SaveDescriptorMetadataRequest,
SaveSampleMetadataRequest, SaveWorkspaceUpstreamRequest, UpdateWorkspaceRequest,
UsageQuery,
};
}
pub mod infrastructure {
pub use crate::ext::{ExtensionMigration, RegistryExtension, apply_extension_migrations};
pub use crate::postgres::{PostgresPoolConfig, PostgresPoolConfigError, PostgresRegistry};
}
pub use model::{
AgentSummary, AgentVersionRecord, AuthUserRecord, CreateAgentDraftVersionRequest,
CreateAgentRequest, CreateInvitationRequest, CreateInvocationLogRequest,
@@ -0,0 +1,70 @@
use std::time::Duration;
use sqlx::{
PgPool,
postgres::{PgConnectOptions, PgPoolOptions},
};
use crate::{error::RegistryError, migrations};
use super::{PostgresPoolConfig, PostgresRegistry};
impl PostgresRegistry {
pub async fn connect(database_url: &str) -> Result<Self, RegistryError> {
Self::connect_in_schema(database_url, None).await
}
pub async fn connect_with_options(
connect_options: PgConnectOptions,
) -> Result<Self, RegistryError> {
Self::connect_with_options_and_pool_config(connect_options, PostgresPoolConfig::default())
.await
}
pub async fn connect_with_options_and_pool_config(
connect_options: PgConnectOptions,
pool_config: PostgresPoolConfig,
) -> Result<Self, RegistryError> {
let pool = PgPoolOptions::new()
.max_connections(pool_config.max_connections)
.min_connections(pool_config.min_connections)
.acquire_timeout(Duration::from_millis(pool_config.acquire_timeout_ms))
.idle_timeout(Duration::from_millis(pool_config.idle_timeout_ms))
.max_lifetime(Duration::from_millis(pool_config.max_lifetime_ms))
.connect_with(connect_options)
.await?;
migrations::apply_postgres(&pool).await?;
Ok(Self { pool })
}
pub fn pool(&self) -> &PgPool {
&self.pool
}
pub async fn migrate(&self) -> Result<(), RegistryError> {
migrations::apply_postgres(&self.pool).await?;
Ok(())
}
async fn connect_in_schema(
database_url: &str,
schema: Option<&str>,
) -> Result<Self, RegistryError> {
let pool = PgPoolOptions::new()
.min_connections(0)
.max_connections(1)
.idle_timeout(std::time::Duration::from_secs(1))
.connect(database_url)
.await?;
if let Some(schema) = schema {
sqlx::query(&format!("set search_path to {schema}"))
.execute(&pool)
.await?;
}
let registry = Self { pool };
registry.migrate().await?;
Ok(registry)
}
}
+5 -269
View File
@@ -1,8 +1,10 @@
mod agent;
mod api_key;
mod auth;
mod connection;
mod observability;
mod operation;
mod pool_config;
mod secret;
mod upstream;
mod workspace;
@@ -16,18 +18,13 @@ use crank_core::{
};
use serde::{Serialize, de::DeserializeOwned};
use serde_json::Value;
use sqlx::{
PgPool, Postgres, Row, Transaction,
postgres::{PgConnectOptions, PgPoolOptions, PgRow},
types::Json,
};
use std::{collections::BTreeMap, env, time::Duration};
use thiserror::Error;
use sqlx::{PgPool, Postgres, Row, Transaction, postgres::PgRow, types::Json};
use time::OffsetDateTime;
pub use pool_config::{PostgresPoolConfig, PostgresPoolConfigError};
use crate::{
error::RegistryError,
migrations,
model::{
AgentSummary, AgentVersionRecord, AuthUserRecord, CreateAgentDraftVersionRequest,
CreateAgentRequest, CreateInvitationRequest, CreateInvocationLogRequest,
@@ -50,156 +47,7 @@ pub struct PostgresRegistry {
pool: PgPool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PostgresPoolConfig {
pub max_connections: u32,
pub min_connections: u32,
pub acquire_timeout_ms: u64,
pub idle_timeout_ms: u64,
pub max_lifetime_ms: u64,
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum PostgresPoolConfigError {
#[error("invalid postgres pool setting {name}={value}")]
InvalidValue { name: &'static str, value: String },
#[error("POSTGRES_MAX_CONNECTIONS must be greater than zero")]
ZeroMaxConnections,
#[error("POSTGRES_MIN_CONNECTIONS must not exceed POSTGRES_MAX_CONNECTIONS")]
MinConnectionsExceedMax,
}
impl Default for PostgresPoolConfig {
fn default() -> Self {
Self {
max_connections: 20,
min_connections: 2,
acquire_timeout_ms: 5_000,
idle_timeout_ms: 600_000,
max_lifetime_ms: 1_800_000,
}
}
}
impl PostgresPoolConfig {
pub fn from_env() -> Result<Self, PostgresPoolConfigError> {
Self::from_vars(env::vars())
}
fn from_vars<I, K, V>(vars: I) -> Result<Self, PostgresPoolConfigError>
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<str>,
V: AsRef<str>,
{
let vars = vars
.into_iter()
.map(|(name, value)| (name.as_ref().to_owned(), value.as_ref().to_owned()))
.collect::<BTreeMap<_, _>>();
let defaults = Self::default();
let config = Self {
max_connections: parse_u32_setting(
&vars,
"POSTGRES_MAX_CONNECTIONS",
defaults.max_connections,
)?,
min_connections: parse_u32_setting(
&vars,
"POSTGRES_MIN_CONNECTIONS",
defaults.min_connections,
)?,
acquire_timeout_ms: parse_u64_setting(
&vars,
"POSTGRES_ACQUIRE_TIMEOUT_MS",
defaults.acquire_timeout_ms,
)?,
idle_timeout_ms: parse_u64_setting(
&vars,
"POSTGRES_IDLE_TIMEOUT_MS",
defaults.idle_timeout_ms,
)?,
max_lifetime_ms: parse_u64_setting(
&vars,
"POSTGRES_MAX_LIFETIME_MS",
defaults.max_lifetime_ms,
)?,
};
config.validate()?;
Ok(config)
}
fn validate(self) -> Result<Self, PostgresPoolConfigError> {
if self.max_connections == 0 {
return Err(PostgresPoolConfigError::ZeroMaxConnections);
}
if self.min_connections > self.max_connections {
return Err(PostgresPoolConfigError::MinConnectionsExceedMax);
}
Ok(self)
}
}
impl PostgresRegistry {
pub async fn connect(database_url: &str) -> Result<Self, RegistryError> {
Self::connect_in_schema(database_url, None).await
}
pub async fn connect_with_options(
connect_options: PgConnectOptions,
) -> Result<Self, RegistryError> {
Self::connect_with_options_and_pool_config(connect_options, PostgresPoolConfig::default())
.await
}
pub async fn connect_with_options_and_pool_config(
connect_options: PgConnectOptions,
pool_config: PostgresPoolConfig,
) -> Result<Self, RegistryError> {
let pool = PgPoolOptions::new()
.max_connections(pool_config.max_connections)
.min_connections(pool_config.min_connections)
.acquire_timeout(Duration::from_millis(pool_config.acquire_timeout_ms))
.idle_timeout(Duration::from_millis(pool_config.idle_timeout_ms))
.max_lifetime(Duration::from_millis(pool_config.max_lifetime_ms))
.connect_with(connect_options)
.await?;
migrations::apply_postgres(&pool).await?;
Ok(Self { pool })
}
pub fn pool(&self) -> &PgPool {
&self.pool
}
pub async fn migrate(&self) -> Result<(), RegistryError> {
migrations::apply_postgres(&self.pool).await?;
Ok(())
}
async fn connect_in_schema(
database_url: &str,
schema: Option<&str>,
) -> Result<Self, RegistryError> {
let pool = PgPoolOptions::new()
.min_connections(0)
.max_connections(1)
.idle_timeout(std::time::Duration::from_secs(1))
.connect(database_url)
.await?;
if let Some(schema) = schema {
sqlx::query(&format!("set search_path to {schema}"))
.execute(&pool)
.await?;
}
let registry = Self { pool };
registry.migrate().await?;
Ok(registry)
}
async fn list_agent_bindings(
&self,
agent_id: &AgentId,
@@ -226,42 +74,6 @@ impl PostgresRegistry {
}
}
fn parse_u32_setting(
vars: &BTreeMap<String, String>,
name: &'static str,
default: u32,
) -> Result<u32, PostgresPoolConfigError> {
vars.get(name)
.map(|value| {
value
.parse::<u32>()
.map_err(|_| PostgresPoolConfigError::InvalidValue {
name,
value: value.clone(),
})
})
.transpose()
.map(|value| value.unwrap_or(default))
}
fn parse_u64_setting(
vars: &BTreeMap<String, String>,
name: &'static str,
default: u64,
) -> Result<u64, PostgresPoolConfigError> {
vars.get(name)
.map(|value| {
value
.parse::<u64>()
.map_err(|_| PostgresPoolConfigError::InvalidValue {
name,
value: value.clone(),
})
})
.transpose()
.map(|value| value.unwrap_or(default))
}
async fn insert_version_row(
tx: &mut Transaction<'_, Postgres>,
snapshot: &RegistryOperation,
@@ -930,79 +742,3 @@ fn usage_bucket_sql(bucket: crate::model::UsageBucket) -> &'static str {
crate::model::UsageBucket::Month => "month",
}
}
#[cfg(test)]
mod pool_config_tests {
use super::{PostgresPoolConfig, PostgresPoolConfigError};
#[test]
fn pool_config_uses_explicit_defaults() {
let config = PostgresPoolConfig::from_vars(std::iter::empty::<(&str, &str)>()).unwrap();
assert_eq!(
config,
PostgresPoolConfig {
max_connections: 20,
min_connections: 2,
acquire_timeout_ms: 5_000,
idle_timeout_ms: 600_000,
max_lifetime_ms: 1_800_000,
}
);
}
#[test]
fn pool_config_parses_overrides() {
let config = PostgresPoolConfig::from_vars([
("POSTGRES_MAX_CONNECTIONS", "32"),
("POSTGRES_MIN_CONNECTIONS", "4"),
("POSTGRES_ACQUIRE_TIMEOUT_MS", "7000"),
("POSTGRES_IDLE_TIMEOUT_MS", "900000"),
("POSTGRES_MAX_LIFETIME_MS", "3600000"),
])
.unwrap();
assert_eq!(
config,
PostgresPoolConfig {
max_connections: 32,
min_connections: 4,
acquire_timeout_ms: 7_000,
idle_timeout_ms: 900_000,
max_lifetime_ms: 3_600_000,
}
);
}
#[test]
fn pool_config_rejects_invalid_numeric_value() {
let error =
PostgresPoolConfig::from_vars([("POSTGRES_MAX_CONNECTIONS", "abc")]).unwrap_err();
assert_eq!(
error,
PostgresPoolConfigError::InvalidValue {
name: "POSTGRES_MAX_CONNECTIONS",
value: "abc".to_owned(),
}
);
}
#[test]
fn pool_config_rejects_zero_max_connections() {
let error = PostgresPoolConfig::from_vars([("POSTGRES_MAX_CONNECTIONS", "0")]).unwrap_err();
assert_eq!(error, PostgresPoolConfigError::ZeroMaxConnections);
}
#[test]
fn pool_config_rejects_min_connections_above_max() {
let error = PostgresPoolConfig::from_vars([
("POSTGRES_MAX_CONNECTIONS", "2"),
("POSTGRES_MIN_CONNECTIONS", "3"),
])
.unwrap_err();
assert_eq!(error, PostgresPoolConfigError::MinConnectionsExceedMax);
}
}
@@ -0,0 +1,206 @@
use std::{collections::BTreeMap, env};
use thiserror::Error;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PostgresPoolConfig {
pub max_connections: u32,
pub min_connections: u32,
pub acquire_timeout_ms: u64,
pub idle_timeout_ms: u64,
pub max_lifetime_ms: u64,
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum PostgresPoolConfigError {
#[error("invalid postgres pool setting {name}={value}")]
InvalidValue { name: &'static str, value: String },
#[error("POSTGRES_MAX_CONNECTIONS must be greater than zero")]
ZeroMaxConnections,
#[error("POSTGRES_MIN_CONNECTIONS must not exceed POSTGRES_MAX_CONNECTIONS")]
MinConnectionsExceedMax,
}
impl Default for PostgresPoolConfig {
fn default() -> Self {
Self {
max_connections: 20,
min_connections: 2,
acquire_timeout_ms: 5_000,
idle_timeout_ms: 600_000,
max_lifetime_ms: 1_800_000,
}
}
}
impl PostgresPoolConfig {
pub fn from_env() -> Result<Self, PostgresPoolConfigError> {
Self::from_vars(env::vars())
}
fn from_vars<I, K, V>(vars: I) -> Result<Self, PostgresPoolConfigError>
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<str>,
V: AsRef<str>,
{
let vars = vars
.into_iter()
.map(|(name, value)| (name.as_ref().to_owned(), value.as_ref().to_owned()))
.collect::<BTreeMap<_, _>>();
let defaults = Self::default();
let config = Self {
max_connections: parse_u32_setting(
&vars,
"POSTGRES_MAX_CONNECTIONS",
defaults.max_connections,
)?,
min_connections: parse_u32_setting(
&vars,
"POSTGRES_MIN_CONNECTIONS",
defaults.min_connections,
)?,
acquire_timeout_ms: parse_u64_setting(
&vars,
"POSTGRES_ACQUIRE_TIMEOUT_MS",
defaults.acquire_timeout_ms,
)?,
idle_timeout_ms: parse_u64_setting(
&vars,
"POSTGRES_IDLE_TIMEOUT_MS",
defaults.idle_timeout_ms,
)?,
max_lifetime_ms: parse_u64_setting(
&vars,
"POSTGRES_MAX_LIFETIME_MS",
defaults.max_lifetime_ms,
)?,
};
config.validate()?;
Ok(config)
}
fn validate(self) -> Result<Self, PostgresPoolConfigError> {
if self.max_connections == 0 {
return Err(PostgresPoolConfigError::ZeroMaxConnections);
}
if self.min_connections > self.max_connections {
return Err(PostgresPoolConfigError::MinConnectionsExceedMax);
}
Ok(self)
}
}
fn parse_u32_setting(
vars: &BTreeMap<String, String>,
name: &'static str,
default: u32,
) -> Result<u32, PostgresPoolConfigError> {
vars.get(name)
.map(|value| {
value
.parse::<u32>()
.map_err(|_| PostgresPoolConfigError::InvalidValue {
name,
value: value.clone(),
})
})
.transpose()
.map(|value| value.unwrap_or(default))
}
fn parse_u64_setting(
vars: &BTreeMap<String, String>,
name: &'static str,
default: u64,
) -> Result<u64, PostgresPoolConfigError> {
vars.get(name)
.map(|value| {
value
.parse::<u64>()
.map_err(|_| PostgresPoolConfigError::InvalidValue {
name,
value: value.clone(),
})
})
.transpose()
.map(|value| value.unwrap_or(default))
}
#[cfg(test)]
mod pool_config_tests {
use super::{PostgresPoolConfig, PostgresPoolConfigError};
#[test]
fn pool_config_uses_explicit_defaults() {
let config = PostgresPoolConfig::from_vars(std::iter::empty::<(&str, &str)>()).unwrap();
assert_eq!(
config,
PostgresPoolConfig {
max_connections: 20,
min_connections: 2,
acquire_timeout_ms: 5_000,
idle_timeout_ms: 600_000,
max_lifetime_ms: 1_800_000,
}
);
}
#[test]
fn pool_config_parses_overrides() {
let config = PostgresPoolConfig::from_vars([
("POSTGRES_MAX_CONNECTIONS", "32"),
("POSTGRES_MIN_CONNECTIONS", "4"),
("POSTGRES_ACQUIRE_TIMEOUT_MS", "7000"),
("POSTGRES_IDLE_TIMEOUT_MS", "900000"),
("POSTGRES_MAX_LIFETIME_MS", "3600000"),
])
.unwrap();
assert_eq!(
config,
PostgresPoolConfig {
max_connections: 32,
min_connections: 4,
acquire_timeout_ms: 7_000,
idle_timeout_ms: 900_000,
max_lifetime_ms: 3_600_000,
}
);
}
#[test]
fn pool_config_rejects_invalid_numeric_value() {
let error =
PostgresPoolConfig::from_vars([("POSTGRES_MAX_CONNECTIONS", "abc")]).unwrap_err();
assert_eq!(
error,
PostgresPoolConfigError::InvalidValue {
name: "POSTGRES_MAX_CONNECTIONS",
value: "abc".to_owned(),
}
);
}
#[test]
fn pool_config_rejects_zero_max_connections() {
let error = PostgresPoolConfig::from_vars([("POSTGRES_MAX_CONNECTIONS", "0")]).unwrap_err();
assert_eq!(error, PostgresPoolConfigError::ZeroMaxConnections);
}
#[test]
fn pool_config_rejects_min_connections_above_max() {
let error = PostgresPoolConfig::from_vars([
("POSTGRES_MAX_CONNECTIONS", "2"),
("POSTGRES_MIN_CONNECTIONS", "3"),
])
.unwrap_err();
assert_eq!(error, PostgresPoolConfigError::MinConnectionsExceedMax);
}
}
+80 -12
View File
@@ -31,6 +31,48 @@ pub struct RuntimeExecutor {
metering_sink: SharedMeteringSink,
}
#[derive(Clone, Copy)]
pub struct RuntimeExecutionRequest<'a> {
pub operation: &'a RuntimeOperation,
pub input: &'a Value,
pub resolved_auth: Option<&'a ResolvedAuth>,
pub request_context: Option<&'a RuntimeRequestContext>,
}
impl<'a> RuntimeExecutionRequest<'a> {
pub fn new(operation: &'a RuntimeOperation, input: &'a Value) -> Self {
Self {
operation,
input,
resolved_auth: None,
request_context: None,
}
}
pub fn with_auth(mut self, resolved_auth: &'a ResolvedAuth) -> Self {
self.resolved_auth = Some(resolved_auth);
self
}
pub fn with_optional_auth(mut self, resolved_auth: Option<&'a ResolvedAuth>) -> Self {
self.resolved_auth = resolved_auth;
self
}
pub fn with_context(mut self, request_context: &'a RuntimeRequestContext) -> Self {
self.request_context = Some(request_context);
self
}
pub fn with_optional_context(
mut self,
request_context: Option<&'a RuntimeRequestContext>,
) -> Self {
self.request_context = request_context;
self
}
}
impl Default for RuntimeExecutor {
fn default() -> Self {
Self::new()
@@ -86,7 +128,7 @@ impl RuntimeExecutor {
operation: &RuntimeOperation,
input: &Value,
) -> Result<Value, RuntimeError> {
self.execute_with_auth_and_context(operation, input, None, None)
self.execute_request(RuntimeExecutionRequest::new(operation, input))
.await
}
@@ -96,8 +138,10 @@ impl RuntimeExecutor {
input: &Value,
resolved_auth: Option<&ResolvedAuth>,
) -> Result<Value, RuntimeError> {
self.execute_with_auth_and_context(operation, input, resolved_auth, None)
.await
self.execute_request(
RuntimeExecutionRequest::new(operation, input).with_optional_auth(resolved_auth),
)
.await
}
pub async fn execute_with_context(
@@ -106,8 +150,10 @@ impl RuntimeExecutor {
input: &Value,
request_context: Option<&RuntimeRequestContext>,
) -> Result<Value, RuntimeError> {
self.execute_with_auth_and_context(operation, input, None, request_context)
.await
self.execute_request(
RuntimeExecutionRequest::new(operation, input).with_optional_context(request_context),
)
.await
}
pub async fn execute_with_auth_and_context(
@@ -117,16 +163,38 @@ impl RuntimeExecutor {
resolved_auth: Option<&ResolvedAuth>,
request_context: Option<&RuntimeRequestContext>,
) -> Result<Value, RuntimeError> {
log_runtime_event("unary.execute", operation, request_context);
let _permit = self.acquire_unary_permit(operation)?;
self.execute_request(
RuntimeExecutionRequest::new(operation, input)
.with_optional_auth(resolved_auth)
.with_optional_context(request_context),
)
.await
}
pub async fn execute_request(
&self,
request: RuntimeExecutionRequest<'_>,
) -> Result<Value, RuntimeError> {
log_runtime_event("unary.execute", request.operation, request.request_context);
let _permit = self.acquire_unary_permit(request.operation)?;
let started_at = Instant::now();
let prepared_request = self.prepare_request(operation, input)?;
let prepared_request = apply_resolved_auth(prepared_request, resolved_auth);
let prepared_request = self.prepare_request(request.operation, request.input)?;
let prepared_request = apply_resolved_auth(prepared_request, request.resolved_auth);
let result = self
.execute_prepared(operation, input, prepared_request, request_context)
.await;
self.record_metering(operation, request_context, &result, started_at)
.execute_prepared(
request.operation,
request.input,
prepared_request,
request.request_context,
)
.await;
self.record_metering(
request.operation,
request.request_context,
&result,
started_at,
)
.await;
result
}
+1 -1
View File
@@ -24,7 +24,7 @@ pub use cache_factory::{
BuiltinCacheBackendFactory, CacheBackendFactory, SharedCacheBackendFactory,
};
pub use error::RuntimeError;
pub use executor::RuntimeExecutor;
pub use executor::{RuntimeExecutionRequest, RuntimeExecutor};
pub use executor_builder::{RuntimeExecutorBuilder, community_default};
pub use limits::{RuntimeLimits, RuntimeLimitsConfigError};
pub use model::{AdapterResponse, PreparedRequest, RuntimeOperation};