feat: complete Epic 1 production foundation

This commit is contained in:
2026-08-25 01:24:11 +03:00
parent 767428436d
commit 182bde8ac0
298 changed files with 35719 additions and 5299 deletions
+257 -114
View File
@@ -31,6 +31,11 @@ pub enum ApiError {
context: Option<Value>,
},
#[error("{message}")]
Unprocessable {
message: String,
context: Option<Value>,
},
#[error("{message}")]
NotFound {
message: String,
context: Option<Value>,
@@ -41,6 +46,16 @@ pub enum ApiError {
context: Option<Value>,
},
#[error("{message}")]
PayloadTooLarge {
message: String,
context: Option<Value>,
},
#[error("{message}")]
PreconditionRequired {
message: String,
context: Option<Value>,
},
#[error("{message}")]
RateLimited {
message: String,
context: Option<Value>,
@@ -95,6 +110,13 @@ impl ApiError {
}
}
pub(crate) fn unprocessable_with_context(message: impl Into<String>, context: Value) -> Self {
Self::Unprocessable {
message: message.into(),
context: Some(context),
}
}
pub(crate) fn not_found_with_context(message: impl Into<String>, context: Value) -> Self {
Self::NotFound {
message: message.into(),
@@ -109,25 +131,58 @@ impl ApiError {
}
}
pub(crate) fn payload_too_large_with_context(
message: impl Into<String>,
context: Value,
) -> Self {
Self::PayloadTooLarge {
message: message.into(),
context: Some(context),
}
}
pub(crate) fn precondition_required_with_context(
message: impl Into<String>,
context: Value,
) -> Self {
Self::PreconditionRequired {
message: message.into(),
context: Some(context),
}
}
fn status_code(&self) -> StatusCode {
match self {
Self::Unauthorized { .. } => StatusCode::UNAUTHORIZED,
Self::Forbidden { .. } => StatusCode::FORBIDDEN,
Self::Validation { .. } => StatusCode::BAD_REQUEST,
Self::Unprocessable { .. } => StatusCode::UNPROCESSABLE_ENTITY,
Self::NotFound { .. } => StatusCode::NOT_FOUND,
Self::Conflict { .. } => StatusCode::CONFLICT,
Self::PayloadTooLarge { .. } => StatusCode::PAYLOAD_TOO_LARGE,
Self::PreconditionRequired { .. } => StatusCode::PRECONDITION_REQUIRED,
Self::RateLimited { .. } => StatusCode::TOO_MANY_REQUESTS,
Self::Internal { .. } => StatusCode::INTERNAL_SERVER_ERROR,
}
}
fn code(&self) -> &'static str {
pub(crate) fn code(&self) -> &str {
if let Some(code) = self
.context_ref()
.and_then(|context| context.get("error_code"))
.and_then(Value::as_str)
{
return code;
}
match self {
Self::Unauthorized { .. } => "unauthorized",
Self::Forbidden { .. } => "forbidden",
Self::Validation { .. } => "validation_error",
Self::Unprocessable { .. } => "unprocessable_entity",
Self::NotFound { .. } => "not_found",
Self::Conflict { .. } => "conflict",
Self::PayloadTooLarge { .. } => "payload_too_large",
Self::PreconditionRequired { .. } => "precondition_required",
Self::RateLimited { .. } => "rate_limited",
Self::Internal { .. } => "internal_error",
}
@@ -147,8 +202,11 @@ impl IntoResponse for ApiError {
Self::Unauthorized { .. }
| Self::Forbidden { .. }
| Self::Validation { .. }
| Self::Unprocessable { .. }
| Self::NotFound { .. }
| Self::Conflict { .. }
| Self::PayloadTooLarge { .. }
| Self::PreconditionRequired { .. }
| Self::RateLimited { .. } => {
warn!(
name: "admin.response.rejected",
@@ -182,17 +240,24 @@ impl IntoResponse for ApiError {
}
impl ApiError {
fn context(&self) -> Option<Value> {
fn context_ref(&self) -> Option<&Value> {
match self {
Self::Unauthorized { context, .. }
| Self::Forbidden { context, .. }
| Self::Validation { context, .. }
| Self::Unprocessable { context, .. }
| Self::NotFound { context, .. }
| Self::Conflict { context, .. }
| Self::PayloadTooLarge { context, .. }
| Self::PreconditionRequired { context, .. }
| Self::RateLimited { context, .. }
| Self::Internal { context, .. } => context.clone(),
| Self::Internal { context, .. } => context.as_ref(),
}
}
fn context(&self) -> Option<Value> {
self.context_ref().cloned()
}
}
impl From<RegistryError> for ApiError {
@@ -228,10 +293,41 @@ impl From<RegistryError> for ApiError {
format!("platform api key {key_id} was not found"),
json!({ "key_id": key_id }),
),
RegistryError::PlatformApiKeyInactive { key_id } => Self::conflict_with_context(
"platform api key is not active",
json!({
"key_id": key_id,
"error_code": "platform_api_key_not_active",
"recovery": "create_replacement_key"
}),
),
RegistryError::SecretNotFound { secret_id } => Self::not_found_with_context(
format!("secret {secret_id} was not found"),
json!({ "secret_id": secret_id }),
),
RegistryError::SecretInactive { secret_id } => Self::conflict_with_context(
"secret is not active",
json!({
"secret_id": secret_id,
"error_code": "secret_not_active",
"recovery": "rotate_or_replace_secret"
}),
),
RegistryError::SecretConcurrentUpdate { secret_id } => Self::conflict_with_context(
"secret was updated concurrently",
json!({
"secret_id": secret_id,
"error_code": "secret_concurrent_update",
"recovery": "reload"
}),
),
RegistryError::MasterKeyRotationInProgress => Self::conflict_with_context(
"master key rotation is in progress",
json!({
"error_code": "master_key_rotation_in_progress",
"recovery": "retry_after_rotation"
}),
),
RegistryError::InvocationLogNotFound { log_id } => Self::not_found_with_context(
format!("invocation log {log_id} was not found"),
json!({ "log_id": log_id }),
@@ -268,6 +364,57 @@ impl From<RegistryError> for ApiError {
json!({ "operation_id": operation_id }),
)
}
RegistryError::OperationArchived { operation_id } => Self::conflict_with_context(
format!("operation {operation_id} is archived"),
json!({ "operation_id": operation_id, "error_code": "operation_archived" }),
),
RegistryError::OperationStaleVersion {
operation_id,
expected,
actual,
} => Self::conflict_with_context(
format!("operation {operation_id} has a stale base version"),
json!({
"operation_id": operation_id,
"current_version": expected,
"provided_version": actual,
"error_code": "operation_stale_version",
"recovery": "reload"
}),
),
RegistryError::InvalidOperationTransition {
operation_id,
from,
action,
} => Self::conflict_with_context(
format!("operation {operation_id} cannot perform {action} from {from}"),
json!({
"operation_id": operation_id,
"state": from,
"action": action,
"error_code": "operation_invalid_transition"
}),
),
RegistryError::OperationDeleteForbidden { operation_id } => {
Self::conflict_with_context(
format!(
"operation {operation_id} cannot be deleted because durable history exists"
),
json!({
"operation_id": operation_id,
"error_code": "operation_delete_forbidden"
}),
)
}
RegistryError::OperationAuthProfileUnavailable { operation_id } => {
Self::unprocessable_with_context(
"operation auth profile reference is unavailable",
json!({
"operation_id": operation_id,
"error_code": "operation_auth_profile_invalid"
}),
)
}
RegistryError::AuthProfileNotFound { auth_profile_id } => Self::not_found_with_context(
format!("auth profile {auth_profile_id} was not found"),
json!({ "auth_profile_id": auth_profile_id }),
@@ -276,6 +423,17 @@ impl From<RegistryError> for ApiError {
format!("operation {operation_id} already exists"),
json!({ "operation_id": operation_id }),
),
RegistryError::PlatformApiKeyNameAlreadyExists { workspace_id, name } => {
Self::conflict_with_context(
"platform api key name already exists",
json!({
"workspace_id": workspace_id,
"name": name,
"error_code": "platform_api_key_name_conflict",
"recovery": "choose_different_name"
}),
)
}
RegistryError::WorkspaceSlugAlreadyExists { slug } => Self::conflict_with_context(
format!("workspace with slug {slug} already exists"),
json!({ "slug": slug }),
@@ -286,6 +444,8 @@ impl From<RegistryError> for ApiError {
json!({
"workspace_id": workspace_id,
"name": name,
"error_code": "secret_name_conflict",
"recovery": "choose_different_name"
}),
)
}
@@ -297,12 +457,29 @@ impl From<RegistryError> for ApiError {
json!({
"secret_id": secret_id,
"auth_profile_id": auth_profile_id,
"error_code": "secret_referenced_by_auth_profile",
"recovery": "remove_or_update_auth_profile_reference"
}),
),
RegistryError::UserEmailAlreadyExists { email } => Self::conflict_with_context(
format!("user with email {email} already exists"),
json!({ "email": email }),
),
RegistryError::AdminBootstrapUnavailable | RegistryError::AdminBootstrapRejected => {
Self::unauthorized("bootstrap request is invalid or expired")
}
RegistryError::AdminRecoveryRejected => Self::unauthorized("recovery request rejected"),
RegistryError::AdminLoginRateLimited { retry_after_ms } => {
Self::rate_limited_with_context(
"login temporarily unavailable",
json!({
"retry_after_ms": retry_after_ms.clamp(1, 300_000),
"error_code": "login_throttled",
"recovery": "retry_after_delay"
}),
)
}
RegistryError::AdminCsrfRejected => Self::forbidden("csrf validation failed"),
RegistryError::InvalidInitialVersion {
operation_id,
version,
@@ -337,6 +514,39 @@ impl From<RegistryError> for ApiError {
"actual": actual,
}),
),
RegistryError::ImmutableAgentVersion { agent_id, version } => {
Self::conflict_with_context(
"Agent Version is immutable; reload before editing",
json!({
"agent_id": agent_id,
"version": version,
"error_code": "agent_stale_revision",
"recovery": "reload"
}),
)
}
RegistryError::AgentStaleRevision { agent_id } => Self::conflict_with_context(
"Agent state changed; reload before retrying",
json!({
"agent_id": agent_id,
"error_code": "agent_stale_revision",
"recovery": "reload"
}),
),
RegistryError::InvalidAgentTransition {
agent_id,
from,
action,
} => Self::conflict_with_context(
format!("agent {agent_id} cannot transition from {from} using {action}"),
json!({
"agent_id": agent_id,
"from": from,
"action": action,
"error_code": "agent_invalid_transition",
"recovery": "reload"
}),
),
RegistryError::ImmutableOperationFieldChanged {
operation_id,
field,
@@ -347,6 +557,14 @@ impl From<RegistryError> for ApiError {
"field": field,
}),
),
RegistryError::OnboardingStaleRevision => Self::conflict_with_context(
"onboarding state changed; reload before retrying",
json!({"error_code":"onboarding_stale_revision","recovery":"reload"}),
),
RegistryError::OnboardingIncomplete => Self::unprocessable_with_context(
"onboarding domain steps are not complete",
json!({"error_code":"onboarding_incomplete"}),
),
RegistryError::InvalidEnumRepresentation { field } => Self::validation_with_context(
format!("unsupported enum representation for field {field}"),
json!({ "field": field }),
@@ -370,10 +588,18 @@ impl From<RegistryError> for ApiError {
format!("import job {job_id} was already applied with different parameters"),
json!({ "job_id": job_id }),
),
RegistryError::Storage(_) => Self::internal("registry operation failed"),
RegistryError::Migration(_)
| RegistryError::Storage(_)
| RegistryError::Serialization(_)
| RegistryError::InvalidCorrelationIdentity { .. } => Self::internal(value.to_string()),
| RegistryError::MasterKeyIdentityMismatch { .. }
| RegistryError::InvalidMasterKeyIdentity
| RegistryError::MasterKeyRotationNotFound { .. }
| RegistryError::MasterKeyRotationConflict
| RegistryError::MasterKeyRotationVerificationFailed
| RegistryError::InvalidCorrelationIdentity { .. }
| RegistryError::InvalidExecutionRecord { .. } => {
Self::internal("registry operation failed")
}
}
}
}
@@ -405,117 +631,34 @@ impl From<StorageError> for ApiError {
}
pub fn runtime_test_failure(error: &RuntimeError) -> Value {
let failure =
crank_runtime::normalize_runtime_error(error, &crank_core::CorrelationContext::generate());
execution_test_failure(&failure)
}
pub fn execution_test_failure(failure: &crank_core::ExecutionFailure) -> Value {
execution_test_failure_localized(failure, crank_core::ExecutionLocale::En)
}
pub fn execution_test_failure_localized(
failure: &crank_core::ExecutionFailure,
locale: crank_core::ExecutionLocale,
) -> Value {
let mut payload = json!({
"code": runtime_test_failure_code(error),
"message": safe_runtime_test_failure_message(error)
"code": failure.error_code().as_str(),
"message": failure.error_code().message(locale),
"stage": failure.stage().as_str(),
"retryability": failure.retryability().as_str(),
"outcome_certainty": failure.outcome_certainty().as_str(),
});
if let Some(context) = runtime_error_context(error) {
payload["context"] = context;
if let Some(status) = failure.upstream_status() {
payload["context"] = json!({ "upstream_status": status });
}
if let Some(challenge) = failure.confirmation() {
payload["context"] = json!({
"confirmation_token": challenge.token(),
"expires_in_ms": challenge.expires_in_ms(),
});
}
payload
}
fn safe_runtime_test_failure_message(error: &RuntimeError) -> &'static str {
match error {
RuntimeError::Schema(_) => "input schema validation failed",
RuntimeError::Mapping(_) => "input mapping failed",
RuntimeError::RestAdapter(_) | RuntimeError::ProtocolAdapter(_) => {
"upstream execution failed"
}
RuntimeError::UnsupportedProtocol { .. } => "operation protocol is unsupported",
RuntimeError::ConcurrencyLimitExceeded { .. } => "runtime concurrency limit exceeded",
RuntimeError::InvalidPreparedRequest { .. } => "prepared request is invalid",
RuntimeError::ConfirmationRequired { .. } => "operation confirmation is required",
RuntimeError::InvalidConfirmationToken { .. } => "confirmation token is invalid",
RuntimeError::ConfirmationStoreUnavailable { .. } => "confirmation store is unavailable",
RuntimeError::IdempotencyStoreUnavailable { .. } => "idempotency store is unavailable",
RuntimeError::IdempotencyInProgress { .. } => "idempotent execution is in progress",
RuntimeError::IdempotencyConflict { .. } => "idempotency key conflicts with the request",
RuntimeError::IdempotencyOutcomeUnknown { .. } => "previous execution outcome is unknown",
RuntimeError::UnsupportedExecutionMode { .. } => "execution mode is unsupported",
RuntimeError::MissingAuthProfile { .. } => "authorization profile is missing",
RuntimeError::MissingSecret { .. } | RuntimeError::MissingSecretVersion { .. } => {
"authorization secret is missing"
}
RuntimeError::InvalidAuthSecretValue { .. } => "authorization secret is invalid",
RuntimeError::SecretCrypto { .. } => "authorization secret processing failed",
}
}
fn runtime_test_failure_code(error: &RuntimeError) -> &'static str {
match error {
RuntimeError::Schema(_) => "runtime_schema_error",
RuntimeError::Mapping(_) => "runtime_mapping_error",
RuntimeError::RestAdapter(_) => "runtime_rest_error",
RuntimeError::ProtocolAdapter(_) => "runtime_adapter_error",
RuntimeError::UnsupportedProtocol { .. } => "runtime_protocol_error",
RuntimeError::ConcurrencyLimitExceeded { .. } => "runtime_overloaded",
RuntimeError::InvalidPreparedRequest { .. } => "runtime_request_error",
RuntimeError::ConfirmationRequired { .. } => "runtime_confirmation_required",
RuntimeError::InvalidConfirmationToken { .. } => "runtime_confirmation_error",
RuntimeError::ConfirmationStoreUnavailable { .. } => "runtime_confirmation_unavailable",
RuntimeError::IdempotencyStoreUnavailable { .. } => "runtime_idempotency_unavailable",
RuntimeError::IdempotencyInProgress { .. } => "runtime_idempotency_in_progress",
RuntimeError::IdempotencyConflict { .. } => "runtime_idempotency_conflict",
RuntimeError::IdempotencyOutcomeUnknown { .. } => "runtime_idempotency_outcome_unknown",
RuntimeError::UnsupportedExecutionMode { .. } => "runtime_streaming_mode_error",
RuntimeError::MissingAuthProfile { .. } => "runtime_auth_profile_error",
RuntimeError::MissingSecret { .. } | RuntimeError::MissingSecretVersion { .. } => {
"runtime_secret_error"
}
RuntimeError::InvalidAuthSecretValue { .. } => "runtime_secret_value_error",
RuntimeError::SecretCrypto { .. } => "runtime_secret_crypto_error",
}
}
pub fn runtime_error_context(error: &RuntimeError) -> Option<Value> {
match error {
RuntimeError::InvalidPreparedRequest { field, .. } => Some(json!({
"field": field,
})),
RuntimeError::ConfirmationRequired {
confirmation_token,
expires_in_ms,
safety_class,
..
} => Some(json!({
"confirmation_token": confirmation_token,
"expires_in_ms": expires_in_ms,
"safety_class": safety_class,
})),
RuntimeError::InvalidConfirmationToken { operation_id }
| RuntimeError::ConfirmationStoreUnavailable { operation_id }
| RuntimeError::IdempotencyStoreUnavailable { operation_id }
| RuntimeError::IdempotencyInProgress { operation_id }
| RuntimeError::IdempotencyConflict { operation_id }
| RuntimeError::IdempotencyOutcomeUnknown { operation_id } => Some(json!({
"operation_id": operation_id,
})),
RuntimeError::InvalidAuthSecretValue { secret_id, .. } => Some(json!({
"secret_id": secret_id,
})),
RuntimeError::SecretCrypto { .. } => None,
RuntimeError::MissingAuthProfile { auth_profile_id } => Some(json!({
"auth_profile_id": auth_profile_id,
})),
RuntimeError::MissingSecret { secret_id } => Some(json!({
"secret_id": secret_id,
})),
RuntimeError::MissingSecretVersion { secret_id, version } => Some(json!({
"secret_id": secret_id,
"version": version,
})),
RuntimeError::UnsupportedExecutionMode { operation_id, mode } => Some(json!({
"operation_id": operation_id,
"mode": mode,
})),
RuntimeError::UnsupportedProtocol { protocol } => Some(json!({
"protocol": protocol,
})),
RuntimeError::ConcurrencyLimitExceeded { kind, limit } => Some(json!({
"kind": kind,
"limit": limit,
})),
_ => None,
}
}