feat: complete Epic 1 production foundation
This commit is contained in:
+372
-97
@@ -4,23 +4,26 @@ use std::sync::Arc;
|
||||
|
||||
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
|
||||
use crank_core::{
|
||||
AuditSink, AuthProfile, CapabilityProfile, CommunityCapabilityProfile, EditionCapabilities,
|
||||
AuditActor, AuditEvent, AuditEventId, AuditSink, AuditTarget, AuditTargetKind, AuthProfile,
|
||||
CapabilityProfile, CommunityCapabilityProfile, CorrelationContext, EditionCapabilities,
|
||||
ExecutionMode, IdentityError, IdentityProvider, InvocationLog, InvocationLogId,
|
||||
InvocationSource, NoopAuditSink, OperationSecurityLevel, OwnerOnlyPolicyEngine, PolicyEngine,
|
||||
ProductEdition, Protocol, ToolQualityMappingRule, ToolQualityMappingSet, ToolQualitySchemaKind,
|
||||
ToolQualitySchemaNode, UsagePeriod, WorkspaceId,
|
||||
ProductEdition, Protocol, SecretStatus, ToolQualityMappingRule, ToolQualityMappingSet,
|
||||
ToolQualitySchemaKind, ToolQualitySchemaNode, UsagePeriod, WorkspaceId,
|
||||
};
|
||||
use crank_mapping::{MappingRule, MappingSet};
|
||||
use crank_registry::{
|
||||
AgentSummary, CreateInvocationLogRequest, InvocationHistoryWriteOutcome, OperationAgentRef,
|
||||
OperationSummary, OperationUsageSummary, PostgresRegistry, RegistryOperation, UsageBucket,
|
||||
OperationSummary, OperationUsageSummary, PostgresRegistry, RegistryError, RegistryOperation,
|
||||
UsageBucket,
|
||||
};
|
||||
use crank_runtime::{
|
||||
OutboundHttpPolicy, PreparedRequest, ResolvedAuth, RuntimeError, RuntimeExecutor, SecretCrypto,
|
||||
OutboundHttpPolicy, ResolvedAuth, RuntimeError, RuntimeExecutor, SecretCrypto,
|
||||
};
|
||||
use crank_schema::{Schema, SchemaKind};
|
||||
use crank_trace::{DbOperation, ErrorCategory, Stage, StageOutcome, observe_db_query};
|
||||
use serde_json::{Value, json};
|
||||
use rand::RngExt;
|
||||
use serde_json::json;
|
||||
use sha2::{Digest, Sha256};
|
||||
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
|
||||
use tracing::Instrument;
|
||||
@@ -33,6 +36,7 @@ mod demo;
|
||||
mod import_export;
|
||||
mod imports;
|
||||
mod observability;
|
||||
mod onboarding;
|
||||
mod operation_validation;
|
||||
mod operations;
|
||||
mod samples;
|
||||
@@ -40,7 +44,11 @@ mod secrets;
|
||||
mod upstreams;
|
||||
mod workspaces;
|
||||
|
||||
use crate::{auth::AuthSettings, error::ApiError, storage::LocalArtifactStorage};
|
||||
use crate::{
|
||||
auth::{AuthSettings, AuthenticatedSession},
|
||||
error::ApiError,
|
||||
storage::LocalArtifactStorage,
|
||||
};
|
||||
use operation_validation::{
|
||||
validate_approval_policy, validate_execution_timeout, validate_idempotency_policy,
|
||||
validate_protocol_target, validate_response_cache_policy,
|
||||
@@ -58,6 +66,7 @@ pub struct AdminService {
|
||||
audit_sink: Arc<dyn AuditSink>,
|
||||
capability_profile: Arc<dyn CapabilityProfile>,
|
||||
outbound_http_policy: OutboundHttpPolicy,
|
||||
public_base_url: String,
|
||||
}
|
||||
|
||||
pub struct AdminServiceBuilder {
|
||||
@@ -71,10 +80,46 @@ pub struct AdminServiceBuilder {
|
||||
audit_sink: Option<Arc<dyn AuditSink>>,
|
||||
capability_profile: Option<Arc<dyn CapabilityProfile>>,
|
||||
outbound_http_policy: OutboundHttpPolicy,
|
||||
public_base_url: String,
|
||||
}
|
||||
|
||||
pub use crate::dto::*;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AdminAuditContext {
|
||||
actor: AuditActor,
|
||||
request_id: String,
|
||||
trace_id: String,
|
||||
}
|
||||
|
||||
impl AdminAuditContext {
|
||||
pub fn from_session_and_correlation(
|
||||
session: &AuthenticatedSession,
|
||||
correlation: &CorrelationContext,
|
||||
) -> Self {
|
||||
Self {
|
||||
actor: AuditActor {
|
||||
user_id: session.user.id.clone(),
|
||||
email: session.user.email.clone(),
|
||||
session_id: Some(session.session_id.clone()),
|
||||
},
|
||||
request_id: correlation.request_id().as_str().to_owned(),
|
||||
trace_id: correlation.trace_id().as_str().to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(super) struct CredentialAuditRecord<'a> {
|
||||
pub action: &'static str,
|
||||
pub target_kind: AuditTargetKind,
|
||||
pub workspace_id: &'a WorkspaceId,
|
||||
pub target_id: &'a str,
|
||||
pub credential_type: &'static str,
|
||||
pub outcome: &'static str,
|
||||
pub reason: &'a str,
|
||||
}
|
||||
|
||||
impl AdminService {
|
||||
pub async fn readiness(&self) -> Result<(), ApiError> {
|
||||
self.registry.ping().await?;
|
||||
@@ -132,6 +177,19 @@ impl AdminService {
|
||||
}
|
||||
}
|
||||
|
||||
fn runtime_credential_error_code(error: &RuntimeError) -> &'static str {
|
||||
match error {
|
||||
RuntimeError::AuthorizationStoreUnavailable => "authorization_store_unavailable",
|
||||
RuntimeError::MissingAuthProfile { .. } => "auth_profile_not_found",
|
||||
RuntimeError::InvalidAuthProfileConfig { .. } => "auth_profile_invalid",
|
||||
RuntimeError::MissingSecret { .. } => "secret_not_found",
|
||||
RuntimeError::MissingSecretVersion { .. } => "secret_version_not_found",
|
||||
RuntimeError::InvalidAuthSecretValue { .. } => "secret_invalid",
|
||||
RuntimeError::SecretCrypto { .. } => "secret_crypto_failed",
|
||||
_ => "credential_resolution_failed",
|
||||
}
|
||||
}
|
||||
|
||||
impl AdminServiceBuilder {
|
||||
pub fn new(
|
||||
registry: PostgresRegistry,
|
||||
@@ -151,6 +209,7 @@ impl AdminServiceBuilder {
|
||||
audit_sink: None,
|
||||
capability_profile: None,
|
||||
outbound_http_policy: OutboundHttpPolicy::default(),
|
||||
public_base_url: "http://localhost:3000".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,6 +223,11 @@ impl AdminServiceBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_public_base_url(mut self, public_base_url: String) -> Self {
|
||||
self.public_base_url = public_base_url.trim_end_matches('/').to_owned();
|
||||
self
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn with_policy_engine(mut self, policy_engine: Arc<dyn PolicyEngine>) -> Self {
|
||||
self.policy_engine = Some(policy_engine);
|
||||
@@ -201,6 +265,7 @@ impl AdminServiceBuilder {
|
||||
.capability_profile
|
||||
.unwrap_or_else(|| Arc::new(CommunityCapabilityProfile)),
|
||||
outbound_http_policy: self.outbound_http_policy,
|
||||
public_base_url: self.public_base_url,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,10 +319,56 @@ impl AdminService {
|
||||
self.capability_profile().capabilities()
|
||||
}
|
||||
|
||||
async fn record_credential_audit(
|
||||
&self,
|
||||
audit_context: Option<&AdminAuditContext>,
|
||||
record: CredentialAuditRecord<'_>,
|
||||
) {
|
||||
let Some(audit_context) = audit_context else {
|
||||
return;
|
||||
};
|
||||
|
||||
let event = AuditEvent {
|
||||
id: AuditEventId::new(new_prefixed_id("audit")),
|
||||
occurred_at: OffsetDateTime::now_utc(),
|
||||
actor: audit_context.actor.clone(),
|
||||
action: record.action.to_owned(),
|
||||
target: AuditTarget {
|
||||
workspace_id: record.workspace_id.clone(),
|
||||
kind: record.target_kind,
|
||||
id: record.target_id.to_owned(),
|
||||
},
|
||||
payload: json!({
|
||||
"credential_type": record.credential_type,
|
||||
"outcome": record.outcome,
|
||||
"reason": record.reason,
|
||||
"request_id": audit_context.request_id,
|
||||
"trace_id": audit_context.trace_id
|
||||
}),
|
||||
source_ip: None,
|
||||
user_agent: None,
|
||||
};
|
||||
|
||||
if self.audit_sink.record(event).await.is_err() {
|
||||
tracing::warn!(
|
||||
name: "admin.credential_audit.lost",
|
||||
action = record.action,
|
||||
credential_type = record.credential_type,
|
||||
outcome = record.outcome,
|
||||
reason = record.reason,
|
||||
request_id = %audit_context.request_id,
|
||||
trace_id = %audit_context.trace_id,
|
||||
error_code = "audit_sink_failed",
|
||||
"credential audit event was not recorded"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_operation_auth(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
execution_config: &crank_core::ExecutionConfig,
|
||||
audit_context: Option<&AdminAuditContext>,
|
||||
) -> Result<Option<ResolvedAuth>, RuntimeError> {
|
||||
let Some(auth_profile_id) = execution_config.auth_profile_ref.as_ref() else {
|
||||
return Ok(None);
|
||||
@@ -271,15 +382,14 @@ impl AdminService {
|
||||
.get_auth_profile(workspace_id, auth_profile_id),
|
||||
)
|
||||
.await
|
||||
.map_err(|error| RuntimeError::SecretCrypto {
|
||||
operation: "load auth profile",
|
||||
details: error.to_string(),
|
||||
})?
|
||||
.ok_or_else(|| RuntimeError::MissingAuthProfile {
|
||||
auth_profile_id: auth_profile_id.as_str().to_owned(),
|
||||
})?;
|
||||
.map_err(|_| RuntimeError::AuthorizationStoreUnavailable)?;
|
||||
let Some(auth_profile) = auth_profile else {
|
||||
return Err(RuntimeError::MissingAuthProfile {
|
||||
auth_profile_id: auth_profile_id.as_str().to_owned(),
|
||||
});
|
||||
};
|
||||
|
||||
self.resolve_auth_profile(workspace_id, &auth_profile)
|
||||
self.resolve_auth_profile(workspace_id, &auth_profile, audit_context)
|
||||
.await
|
||||
.map(Some)
|
||||
}
|
||||
@@ -292,6 +402,17 @@ impl AdminService {
|
||||
ErrorCategory::Configuration.record(&span);
|
||||
}
|
||||
}
|
||||
if let Err(error) = &result {
|
||||
self.record_credential_resolution_failure(
|
||||
audit_context,
|
||||
workspace_id,
|
||||
crank_core::AuditTargetKind::AuthProfile,
|
||||
auth_profile_id.as_str(),
|
||||
"auth_profile",
|
||||
runtime_credential_error_code(error),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
@@ -299,6 +420,7 @@ impl AdminService {
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
auth_profile: &AuthProfile,
|
||||
audit_context: Option<&AdminAuditContext>,
|
||||
) -> Result<ResolvedAuth, RuntimeError> {
|
||||
let mut secrets = BTreeMap::new();
|
||||
let used_at = OffsetDateTime::now_utc();
|
||||
@@ -309,45 +431,156 @@ impl AdminService {
|
||||
self.registry.get_secret(workspace_id, secret_id),
|
||||
)
|
||||
.await
|
||||
.map_err(|error| RuntimeError::SecretCrypto {
|
||||
operation: "load secret",
|
||||
details: error.to_string(),
|
||||
})?
|
||||
.ok_or_else(|| RuntimeError::MissingSecret {
|
||||
secret_id: secret_id.as_str().to_owned(),
|
||||
})?;
|
||||
.map_err(|_| RuntimeError::AuthorizationStoreUnavailable)?;
|
||||
let Some(secret) = secret else {
|
||||
let error = RuntimeError::MissingSecret {
|
||||
secret_id: secret_id.as_str().to_owned(),
|
||||
};
|
||||
self.record_credential_resolution_failure(
|
||||
audit_context,
|
||||
workspace_id,
|
||||
crank_core::AuditTargetKind::Secret,
|
||||
secret_id.as_str(),
|
||||
"secret",
|
||||
runtime_credential_error_code(&error),
|
||||
)
|
||||
.await;
|
||||
return Err(error);
|
||||
};
|
||||
if secret.secret.status != SecretStatus::Active {
|
||||
let error = RuntimeError::InvalidAuthSecretValue {
|
||||
secret_id: secret_id.as_str().to_owned(),
|
||||
reason: "secret is not active".to_owned(),
|
||||
};
|
||||
self.record_credential_resolution_failure(
|
||||
audit_context,
|
||||
workspace_id,
|
||||
crank_core::AuditTargetKind::Secret,
|
||||
secret_id.as_str(),
|
||||
"secret",
|
||||
runtime_credential_error_code(&error),
|
||||
)
|
||||
.await;
|
||||
return Err(error);
|
||||
}
|
||||
let version = observe_db_query(
|
||||
DbOperation::SecretRead,
|
||||
self.registry
|
||||
.get_current_secret_version(workspace_id, secret_id),
|
||||
)
|
||||
.await
|
||||
.map_err(|error| RuntimeError::SecretCrypto {
|
||||
operation: "load current secret version",
|
||||
details: error.to_string(),
|
||||
})?
|
||||
.ok_or_else(|| RuntimeError::MissingSecretVersion {
|
||||
secret_id: secret_id.as_str().to_owned(),
|
||||
version: secret.secret.current_version,
|
||||
})?;
|
||||
let plaintext = self.secret_crypto.decrypt(
|
||||
.map_err(|_| RuntimeError::AuthorizationStoreUnavailable)?;
|
||||
let Some(version) = version else {
|
||||
let error = RuntimeError::MissingSecretVersion {
|
||||
secret_id: secret_id.as_str().to_owned(),
|
||||
version: secret.secret.current_version,
|
||||
};
|
||||
self.record_credential_resolution_failure(
|
||||
audit_context,
|
||||
workspace_id,
|
||||
crank_core::AuditTargetKind::Secret,
|
||||
secret_id.as_str(),
|
||||
"secret",
|
||||
runtime_credential_error_code(&error),
|
||||
)
|
||||
.await;
|
||||
return Err(error);
|
||||
};
|
||||
let plaintext = self.secret_crypto.decrypt_for_epoch(
|
||||
&version.secret_version.key_version,
|
||||
version.master_key_epoch,
|
||||
&version.secret_version.ciphertext,
|
||||
)?;
|
||||
);
|
||||
let plaintext = match plaintext {
|
||||
Ok(plaintext) => plaintext,
|
||||
Err(error) => {
|
||||
self.record_credential_resolution_failure(
|
||||
audit_context,
|
||||
workspace_id,
|
||||
crank_core::AuditTargetKind::Secret,
|
||||
secret_id.as_str(),
|
||||
"secret",
|
||||
runtime_credential_error_code(&error),
|
||||
)
|
||||
.await;
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
observe_db_query(
|
||||
DbOperation::SecretTouch,
|
||||
self.registry
|
||||
.touch_secret(workspace_id, secret_id, &used_at),
|
||||
)
|
||||
.await
|
||||
.map_err(|error| RuntimeError::SecretCrypto {
|
||||
operation: "touch secret",
|
||||
details: error.to_string(),
|
||||
})?;
|
||||
.unwrap_or_else(|_| {
|
||||
tracing::warn!(
|
||||
name: "admin.secret.touch_failed",
|
||||
secret_id = %secret_id.as_str(),
|
||||
request_id = audit_context
|
||||
.map(|context| context.request_id.as_str())
|
||||
.unwrap_or("unavailable"),
|
||||
trace_id = audit_context
|
||||
.map(|context| context.trace_id.as_str())
|
||||
.unwrap_or("unavailable"),
|
||||
error_code = "secret_touch_failed",
|
||||
"secret last-used metadata was not updated"
|
||||
);
|
||||
});
|
||||
secrets.insert(secret_id.clone(), plaintext);
|
||||
}
|
||||
|
||||
ResolvedAuth::from_profile(auth_profile, &secrets)
|
||||
let result = ResolvedAuth::from_profile(auth_profile, &secrets);
|
||||
if let Err(error) = &result {
|
||||
self.record_credential_resolution_failure(
|
||||
audit_context,
|
||||
workspace_id,
|
||||
crank_core::AuditTargetKind::AuthProfile,
|
||||
auth_profile.id.as_str(),
|
||||
"auth_profile",
|
||||
runtime_credential_error_code(error),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
async fn record_credential_resolution_failure(
|
||||
&self,
|
||||
audit_context: Option<&AdminAuditContext>,
|
||||
workspace_id: &WorkspaceId,
|
||||
target_kind: crank_core::AuditTargetKind,
|
||||
target_id: &str,
|
||||
credential_type: &'static str,
|
||||
reason: &'static str,
|
||||
) {
|
||||
self.record_credential_audit(
|
||||
audit_context,
|
||||
CredentialAuditRecord {
|
||||
action: "credential.resolve_denied",
|
||||
target_kind,
|
||||
workspace_id,
|
||||
target_id,
|
||||
credential_type,
|
||||
outcome: "failure",
|
||||
reason,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
tracing::warn!(
|
||||
name: "admin.credential.resolve_denied",
|
||||
credential_type,
|
||||
target_kind = ?target_kind,
|
||||
target_id,
|
||||
outcome = "failure",
|
||||
reason,
|
||||
request_id = audit_context
|
||||
.map(|context| context.request_id.as_str())
|
||||
.unwrap_or("unavailable"),
|
||||
trace_id = audit_context
|
||||
.map(|context| context.trace_id.as_str())
|
||||
.unwrap_or("unavailable"),
|
||||
"credential resolution was denied"
|
||||
);
|
||||
}
|
||||
|
||||
fn validate_operation_payload(&self, payload: &OperationPayload) -> Result<(), ApiError> {
|
||||
@@ -376,6 +609,70 @@ impl AdminService {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn validate_registry_operation_in_workspace(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
operation: &RegistryOperation,
|
||||
) -> Result<(), ApiError> {
|
||||
self.validate_registry_operation(operation)?;
|
||||
if let Some(auth_profile_id) = operation.execution_config.auth_profile_ref.as_ref() {
|
||||
let Some(auth_profile) = self
|
||||
.registry
|
||||
.get_auth_profile(workspace_id, auth_profile_id)
|
||||
.await?
|
||||
else {
|
||||
return Err(ApiError::unprocessable_with_context(
|
||||
"operation auth profile reference is unavailable",
|
||||
json!({ "error_code": "operation_auth_profile_invalid" }),
|
||||
));
|
||||
};
|
||||
self.validate_auth_profile_secret_refs_for_execution(workspace_id, &auth_profile)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn validate_auth_profile_secret_refs_for_execution(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
auth_profile: &AuthProfile,
|
||||
) -> Result<(), ApiError> {
|
||||
if auth_profile.kind != auth_profile.config.kind() {
|
||||
return Err(ApiError::unprocessable_with_context(
|
||||
"operation auth profile kind and config do not match",
|
||||
json!({ "error_code": "operation_auth_profile_invalid" }),
|
||||
));
|
||||
}
|
||||
|
||||
for secret_id in auth_profile.config.secret_ids() {
|
||||
let Some(secret) = self.registry.get_secret(workspace_id, secret_id).await? else {
|
||||
return Err(ApiError::unprocessable_with_context(
|
||||
"operation auth profile secret reference is unavailable",
|
||||
json!({ "error_code": "operation_auth_profile_invalid" }),
|
||||
));
|
||||
};
|
||||
if secret.secret.status != SecretStatus::Active {
|
||||
return Err(RegistryError::SecretInactive {
|
||||
secret_id: secret_id.as_str().to_owned(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
if self
|
||||
.registry
|
||||
.get_current_secret_version(workspace_id, secret_id)
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
return Err(ApiError::unprocessable_with_context(
|
||||
"operation auth profile secret has no current version",
|
||||
json!({ "error_code": "operation_auth_profile_invalid" }),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_outbound_target(&self, target: &crank_core::Target) -> Result<(), ApiError> {
|
||||
match target {
|
||||
crank_core::Target::Rest(rest) => self
|
||||
@@ -462,7 +759,9 @@ impl AdminService {
|
||||
id: InvocationLogId::new(new_prefixed_id("log")),
|
||||
workspace_id: request.workspace_id.clone(),
|
||||
agent_id: request.agent_id.cloned(),
|
||||
platform_api_key_id: None,
|
||||
operation_id: request.operation.id.clone(),
|
||||
operation_version: Some(request.operation.version),
|
||||
source: request.source,
|
||||
level: request.level,
|
||||
status: request.status,
|
||||
@@ -473,6 +772,10 @@ impl AdminService {
|
||||
status_code: request.status_code,
|
||||
duration_ms: request.duration_ms,
|
||||
error_kind: request.error_kind,
|
||||
execution_stage: request.execution_stage,
|
||||
execution_error_code: request.execution_error_code,
|
||||
retryability: request.retryability,
|
||||
outcome_certainty: request.outcome_certainty,
|
||||
request_preview: request.request_preview,
|
||||
response_preview: request.response_preview,
|
||||
created_at: OffsetDateTime::now_utc(),
|
||||
@@ -553,29 +856,6 @@ fn invocation_status_label(status: crank_core::InvocationStatus) -> &'static str
|
||||
}
|
||||
}
|
||||
|
||||
fn build_request_preview(
|
||||
mapping: &MappingSet,
|
||||
input: &Value,
|
||||
) -> Result<Value, crank_mapping::MappingError> {
|
||||
let prepared = if mapping.is_empty() {
|
||||
PreparedRequest::default()
|
||||
} else {
|
||||
let mapped = mapping.apply(&json!({ "mcp": input }))?;
|
||||
PreparedRequest::from_mapping_output(&mapped).map_err(|error| {
|
||||
crank_mapping::MappingError::InvalidJsonPath {
|
||||
path: error.to_string(),
|
||||
}
|
||||
})?
|
||||
};
|
||||
|
||||
Ok(json!({
|
||||
"path": prepared.path_params,
|
||||
"query": prepared.query_params,
|
||||
"headers": prepared.headers,
|
||||
"body": prepared.body.unwrap_or(Value::Null)
|
||||
}))
|
||||
}
|
||||
|
||||
fn validate_profile_display_name(value: &str) -> Result<String, ApiError> {
|
||||
let display_name = value.trim();
|
||||
if display_name.is_empty() {
|
||||
@@ -616,7 +896,9 @@ fn new_prefixed_id(prefix: &str) -> String {
|
||||
}
|
||||
|
||||
fn generate_access_secret(marker: &str) -> String {
|
||||
let random = URL_SAFE_NO_PAD.encode(Uuid::now_v7().as_bytes());
|
||||
let mut secret_bytes = [0_u8; 32];
|
||||
rand::rng().fill(&mut secret_bytes);
|
||||
let random = URL_SAFE_NO_PAD.encode(secret_bytes);
|
||||
format!("{marker}{random}")
|
||||
}
|
||||
|
||||
@@ -640,7 +922,7 @@ fn format_timestamp(timestamp: OffsetDateTime) -> String {
|
||||
fn map_identity_error(error: IdentityError) -> ApiError {
|
||||
match error {
|
||||
IdentityError::BadCredentials => ApiError::unauthorized("invalid email or password"),
|
||||
IdentityError::AccountDisabled => ApiError::forbidden("account is disabled"),
|
||||
IdentityError::AccountDisabled => ApiError::unauthorized("invalid email or password"),
|
||||
IdentityError::NotSupportedForProvider => ApiError::internal(
|
||||
"password login is not supported by the configured identity provider",
|
||||
),
|
||||
@@ -648,32 +930,6 @@ fn map_identity_error(error: IdentityError) -> ApiError {
|
||||
}
|
||||
}
|
||||
|
||||
fn runtime_error_code(error: &RuntimeError) -> &'static str {
|
||||
match error {
|
||||
RuntimeError::Schema(_) => "schema_error",
|
||||
RuntimeError::Mapping(_) => "mapping_error",
|
||||
RuntimeError::InvalidPreparedRequest { .. } => "invalid_request",
|
||||
RuntimeError::ConfirmationRequired { .. } => "confirmation_required",
|
||||
RuntimeError::InvalidConfirmationToken { .. } => "invalid_confirmation_token",
|
||||
RuntimeError::ConfirmationStoreUnavailable { .. } => "confirmation_unavailable",
|
||||
RuntimeError::IdempotencyStoreUnavailable { .. } => "idempotency_unavailable",
|
||||
RuntimeError::IdempotencyInProgress { .. } => "idempotency_in_progress",
|
||||
RuntimeError::IdempotencyConflict { .. } => "idempotency_conflict",
|
||||
RuntimeError::IdempotencyOutcomeUnknown { .. } => "idempotency_outcome_unknown",
|
||||
RuntimeError::RestAdapter(_) => "rest_error",
|
||||
RuntimeError::ProtocolAdapter(_) => "adapter_error",
|
||||
RuntimeError::UnsupportedProtocol { .. } => "unsupported_protocol",
|
||||
RuntimeError::ConcurrencyLimitExceeded { .. } => "runtime_overloaded",
|
||||
RuntimeError::UnsupportedExecutionMode { .. } => "streaming_mode_error",
|
||||
RuntimeError::MissingAuthProfile { .. } => "auth_profile_not_found",
|
||||
RuntimeError::MissingSecret { .. } | RuntimeError::MissingSecretVersion { .. } => {
|
||||
"secret_not_found"
|
||||
}
|
||||
RuntimeError::InvalidAuthSecretValue { .. } => "secret_value_error",
|
||||
RuntimeError::SecretCrypto { .. } => "secret_crypto_error",
|
||||
}
|
||||
}
|
||||
|
||||
fn protocol_capability_view(
|
||||
protocol: Protocol,
|
||||
_edition: ProductEdition,
|
||||
@@ -698,7 +954,9 @@ fn protocol_capability_view(
|
||||
}
|
||||
}
|
||||
|
||||
fn usage_window(period: UsagePeriod) -> Result<(UsagePeriod, String, UsageBucket), ApiError> {
|
||||
fn usage_window(
|
||||
period: UsagePeriod,
|
||||
) -> Result<(UsagePeriod, String, String, UsageBucket), ApiError> {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let (start, bucket) = match period {
|
||||
UsagePeriod::Last30Minutes => (
|
||||
@@ -746,13 +1004,14 @@ fn usage_window(period: UsagePeriod) -> Result<(UsagePeriod, String, UsageBucket
|
||||
),
|
||||
};
|
||||
|
||||
Ok((
|
||||
period,
|
||||
start
|
||||
.format(&Rfc3339)
|
||||
.map_err(|error| ApiError::internal(error.to_string()))?,
|
||||
bucket,
|
||||
))
|
||||
let created_after = start
|
||||
.format(&Rfc3339)
|
||||
.map_err(|error| ApiError::internal(error.to_string()))?;
|
||||
let created_before = now
|
||||
.format(&Rfc3339)
|
||||
.map_err(|error| ApiError::internal(error.to_string()))?;
|
||||
|
||||
Ok((period, created_after, created_before, bucket))
|
||||
}
|
||||
|
||||
fn today_start_utc() -> Result<String, ApiError> {
|
||||
@@ -810,6 +1069,7 @@ fn map_agent_summary_view(summary: AgentSummary) -> AgentSummaryView {
|
||||
status: summary.status,
|
||||
current_draft_version: summary.current_draft_version,
|
||||
latest_published_version: summary.latest_published_version,
|
||||
catalog_revision: summary.catalog_revision,
|
||||
created_at: format_timestamp(summary.created_at),
|
||||
updated_at: format_timestamp(summary.updated_at),
|
||||
published_at: summary.published_at.map(format_timestamp),
|
||||
@@ -822,10 +1082,24 @@ fn map_agent_summary_view(summary: AgentSummary) -> AgentSummaryView {
|
||||
}
|
||||
}
|
||||
|
||||
fn agent_mcp_endpoint(workspace_slug: &str, agent_slug: &str) -> String {
|
||||
pub(crate) fn agent_mcp_endpoint(workspace_slug: &str, agent_slug: &str) -> String {
|
||||
format!("/mcp/v1/{workspace_slug}/{agent_slug}")
|
||||
}
|
||||
|
||||
impl AdminService {
|
||||
pub(crate) fn public_agent_mcp_endpoint(
|
||||
&self,
|
||||
workspace_slug: &str,
|
||||
agent_slug: &str,
|
||||
) -> String {
|
||||
format!(
|
||||
"{}{}",
|
||||
self.public_base_url,
|
||||
agent_mcp_endpoint(workspace_slug, agent_slug)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn tool_quality_schema_node(schema: &Schema) -> ToolQualitySchemaNode {
|
||||
ToolQualitySchemaNode {
|
||||
kind: tool_quality_schema_kind(&schema.kind),
|
||||
@@ -1013,6 +1287,7 @@ fn enrich_operation_summary(
|
||||
status: summary.status,
|
||||
current_draft_version: summary.current_draft_version,
|
||||
latest_published_version: summary.latest_published_version,
|
||||
can_delete: summary.can_delete,
|
||||
created_at: format_timestamp(summary.created_at),
|
||||
updated_at: format_timestamp(summary.updated_at),
|
||||
published_at: summary.published_at.map(format_timestamp),
|
||||
|
||||
Reference in New Issue
Block a user