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
+300 -28
View File
@@ -6,12 +6,14 @@ use crank_registry::{CreatePlatformApiKeyRequest, PlatformApiKeyRecord};
use serde_json::json;
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
use tracing::instrument;
use url::Url;
use crate::{
error::ApiError,
service::{
AdminService, CreatedPlatformApiKeyResponse, PlatformApiKeyPayload, generate_access_secret,
hash_access_secret, new_prefixed_id,
AdminAuditContext, AdminService, CreatedPlatformApiKeyResponse, CredentialAuditRecord,
EphemeralMcpClientConfig, EphemeralMcpConnection, PlatformApiKeyPayload,
generate_access_secret, hash_access_secret, new_prefixed_id,
},
};
@@ -38,32 +40,84 @@ impl AdminService {
.await?)
}
#[instrument(skip(self, payload), fields(workspace_id = %workspace_id.as_str(), agent_id = %agent_id.as_str(), key_name = %payload.name))]
#[instrument(skip(self, payload, audit_context), fields(workspace_id = %workspace_id.as_str(), agent_id = %agent_id.as_str(), key_name = %payload.name))]
pub async fn create_agent_platform_api_key(
&self,
workspace_id: &WorkspaceId,
agent_id: &AgentId,
payload: PlatformApiKeyPayload,
mut payload: PlatformApiKeyPayload,
audit_context: Option<&AdminAuditContext>,
) -> Result<CreatedPlatformApiKeyResponse, ApiError> {
self.ensure_workspace_exists(workspace_id).await?;
self.registry
let workspace = match self.get_workspace(workspace_id).await {
Ok(workspace) => workspace,
Err(error) => {
self.record_credential_audit(
audit_context,
CredentialAuditRecord {
action: "credential.platform_api_key.create_failed",
target_kind: crank_core::AuditTargetKind::PlatformApiKey,
workspace_id,
target_id: "pending",
credential_type: "platform_api_key",
outcome: "failure",
reason: error.code(),
},
)
.await;
return Err(error);
}
};
let agent_result = match self
.registry
.get_agent_summary(workspace_id, agent_id)
.await?
.ok_or_else(|| {
.await
{
Ok(agent) => agent.ok_or_else(|| {
ApiError::not_found_with_context(
format!("agent {} was not found", agent_id.as_str()),
json!({ "agent_id": agent_id.as_str() }),
)
})?;
}),
Err(error) => Err(ApiError::from(error)),
};
let agent = match agent_result {
Ok(agent) => agent,
Err(error) => {
self.record_credential_audit(
audit_context,
CredentialAuditRecord {
action: "credential.platform_api_key.create_failed",
target_kind: crank_core::AuditTargetKind::PlatformApiKey,
workspace_id,
target_id: "pending",
credential_type: "platform_api_key",
outcome: "failure",
reason: error.code(),
},
)
.await;
return Err(error);
}
};
validate_platform_api_key_payload(&payload)?;
let expires_at = match payload.expires_at.as_deref() {
Some(value) => Some(
OffsetDateTime::parse(value, &Rfc3339)
.map_err(|_| ApiError::validation("expires_at must be RFC3339 timestamp"))?,
),
None => None,
let expires_at = match validate_platform_api_key_payload(&mut payload) {
Ok(expires_at) => expires_at,
Err(error) => {
self.record_credential_audit(
audit_context,
CredentialAuditRecord {
action: "credential.platform_api_key.create_failed",
target_kind: crank_core::AuditTargetKind::PlatformApiKey,
workspace_id,
target_id: "pending",
credential_type: payload.key_kind.audit_credential_type(),
outcome: "failure",
reason: error.code(),
},
)
.await;
return Err(error);
}
};
let secret = generate_access_secret(payload.key_kind.secret_marker());
let api_key = PlatformApiKeyRecord {
@@ -83,55 +137,213 @@ impl AdminService {
},
};
self.registry
if let Err(error) = self
.registry
.create_platform_api_key(CreatePlatformApiKeyRequest {
api_key: &api_key.api_key,
secret_hash: &hash_access_secret(&secret),
})
.await?;
.await
{
let error = ApiError::from(error);
self.record_credential_audit(
audit_context,
CredentialAuditRecord {
action: "credential.platform_api_key.create_failed",
target_kind: crank_core::AuditTargetKind::PlatformApiKey,
workspace_id,
target_id: api_key.api_key.id.as_str(),
credential_type: api_key.api_key.key_kind.audit_credential_type(),
outcome: "failure",
reason: error.code(),
},
)
.await;
return Err(error);
}
self.record_credential_audit(
audit_context,
CredentialAuditRecord {
action: "credential.platform_api_key.created",
target_kind: crank_core::AuditTargetKind::PlatformApiKey,
workspace_id,
target_id: api_key.api_key.id.as_str(),
credential_type: api_key.api_key.key_kind.audit_credential_type(),
outcome: "success",
reason: "credential_created",
},
)
.await;
Ok(CreatedPlatformApiKeyResponse { api_key, secret })
let connection = (api_key.api_key.key_kind == PlatformApiKeyKind::McpClient).then(|| {
let endpoint = self.public_agent_mcp_endpoint(&workspace.workspace.slug, &agent.slug);
EphemeralMcpConnection {
endpoint: endpoint.clone(),
clients: ephemeral_client_configs(&endpoint, &secret),
secret_display: "once",
}
});
Ok(CreatedPlatformApiKeyResponse {
api_key,
secret,
connection,
})
}
#[instrument(skip(self), fields(workspace_id = %workspace_id.as_str(), agent_id = %agent_id.as_str(), key_id = %key_id.as_str()))]
#[instrument(skip(self, audit_context), fields(workspace_id = %workspace_id.as_str(), agent_id = %agent_id.as_str(), key_id = %key_id.as_str()))]
pub async fn revoke_agent_platform_api_key(
&self,
workspace_id: &WorkspaceId,
agent_id: &AgentId,
key_id: &PlatformApiKeyId,
audit_context: Option<&AdminAuditContext>,
) -> Result<(), ApiError> {
self.registry
if let Err(error) = self
.registry
.revoke_platform_api_key_for_agent(
workspace_id,
agent_id,
key_id,
&OffsetDateTime::now_utc(),
)
.await?;
.await
{
let error = ApiError::from(error);
self.record_credential_audit(
audit_context,
CredentialAuditRecord {
action: "credential.platform_api_key.revoke_failed",
target_kind: crank_core::AuditTargetKind::PlatformApiKey,
workspace_id,
target_id: key_id.as_str(),
credential_type: "platform_api_key",
outcome: "failure",
reason: error.code(),
},
)
.await;
return Err(error);
}
self.record_credential_audit(
audit_context,
CredentialAuditRecord {
action: "credential.platform_api_key.revoked",
target_kind: crank_core::AuditTargetKind::PlatformApiKey,
workspace_id,
target_id: key_id.as_str(),
credential_type: "platform_api_key",
outcome: "success",
reason: "credential_revoked",
},
)
.await;
Ok(())
}
#[instrument(skip(self), fields(workspace_id = %workspace_id.as_str(), agent_id = %agent_id.as_str(), key_id = %key_id.as_str()))]
#[instrument(skip(self, audit_context), fields(workspace_id = %workspace_id.as_str(), agent_id = %agent_id.as_str(), key_id = %key_id.as_str()))]
pub async fn delete_agent_platform_api_key(
&self,
workspace_id: &WorkspaceId,
agent_id: &AgentId,
key_id: &PlatformApiKeyId,
audit_context: Option<&AdminAuditContext>,
) -> Result<(), ApiError> {
self.registry
if let Err(error) = self
.registry
.delete_platform_api_key_for_agent(workspace_id, agent_id, key_id)
.await?;
.await
{
let error = ApiError::from(error);
self.record_credential_audit(
audit_context,
CredentialAuditRecord {
action: "credential.platform_api_key.delete_failed",
target_kind: crank_core::AuditTargetKind::PlatformApiKey,
workspace_id,
target_id: key_id.as_str(),
credential_type: "platform_api_key",
outcome: "failure",
reason: error.code(),
},
)
.await;
return Err(error);
}
self.record_credential_audit(
audit_context,
CredentialAuditRecord {
action: "credential.platform_api_key.deleted",
target_kind: crank_core::AuditTargetKind::PlatformApiKey,
workspace_id,
target_id: key_id.as_str(),
credential_type: "platform_api_key",
outcome: "success",
reason: "credential_deleted",
},
)
.await;
Ok(())
}
}
fn validate_platform_api_key_payload(payload: &PlatformApiKeyPayload) -> Result<(), ApiError> {
fn ephemeral_client_configs(endpoint: &str, secret: &str) -> Vec<EphemeralMcpClientConfig> {
["claude_desktop", "cursor", "vscode"]
.into_iter()
.map(|client| EphemeralMcpClientConfig {
client: client.to_owned(),
config: json!({
"transport": "streamable_http",
"url": endpoint,
"headers": {"Authorization": format!("Bearer {secret}")}
}),
})
.collect()
}
trait PlatformApiKeyKindAuditExt {
fn audit_credential_type(self) -> &'static str;
}
impl PlatformApiKeyKindAuditExt for PlatformApiKeyKind {
fn audit_credential_type(self) -> &'static str {
match self {
PlatformApiKeyKind::McpClient => "mcp_client_key",
PlatformApiKeyKind::Approval => "approval_key",
}
}
}
fn validate_platform_api_key_payload(
payload: &mut PlatformApiKeyPayload,
) -> Result<Option<OffsetDateTime>, ApiError> {
const MAX_KEY_NAME_CHARS: usize = 128;
const MAX_SCOPES: usize = 8;
payload.name = payload.name.trim().to_owned();
if payload.name.trim().is_empty() {
return Err(ApiError::validation("key name is required"));
}
if payload.name.chars().count() > MAX_KEY_NAME_CHARS {
return Err(ApiError::validation(
"key name must be at most 128 characters",
));
}
if payload.scopes.is_empty() {
return Err(ApiError::validation("at least one key scope is required"));
}
if payload.scopes.len() > MAX_SCOPES {
return Err(ApiError::validation("too many key scopes"));
}
if payload
.scopes
.iter()
.enumerate()
.any(|(index, scope)| payload.scopes[..index].contains(scope))
{
return Err(ApiError::validation(
"key scopes must not contain duplicates",
));
}
let valid = payload.scopes.iter().all(|scope| match payload.key_kind {
PlatformApiKeyKind::McpClient => matches!(
@@ -156,6 +368,66 @@ fn validate_platform_api_key_payload(payload: &PlatformApiKeyPayload) -> Result<
"approval key can contain at most 20 allowed origins",
));
}
if payload.key_kind == PlatformApiKeyKind::Approval {
let mut normalized_origins = Vec::with_capacity(payload.allowed_origins.len());
for origin in &payload.allowed_origins {
normalized_origins.push(validate_approval_origin(origin)?);
}
if normalized_origins
.iter()
.enumerate()
.any(|(index, origin)| normalized_origins[..index].contains(origin))
{
return Err(ApiError::validation(
"allowed origins must not contain duplicates",
));
}
payload.allowed_origins = normalized_origins;
} else if !payload.allowed_origins.is_empty() {
return Err(ApiError::validation(
"allowed origins are only supported for approval keys",
));
}
Ok(())
let expires_at = payload
.expires_at
.as_deref()
.map(|value| {
OffsetDateTime::parse(value, &Rfc3339)
.map_err(|_| ApiError::validation("expires_at must be RFC3339 timestamp"))
})
.transpose()?;
if expires_at.is_some_and(|value| value <= OffsetDateTime::now_utc()) {
return Err(ApiError::validation("expires_at must be in the future"));
}
Ok(expires_at)
}
fn validate_approval_origin(origin: &str) -> Result<String, ApiError> {
const MAX_ORIGIN_LEN: usize = 2048;
if origin.is_empty() || origin.len() > MAX_ORIGIN_LEN {
return Err(ApiError::validation("allowed origin is invalid"));
}
if origin
.bytes()
.any(|byte| byte.is_ascii_control() || byte.is_ascii_whitespace())
{
return Err(ApiError::validation("allowed origin is invalid"));
}
let parsed =
Url::parse(origin).map_err(|_| ApiError::validation("allowed origin is invalid"))?;
if !matches!(parsed.scheme(), "http" | "https")
|| parsed.host_str().is_none()
|| !parsed.username().is_empty()
|| parsed.password().is_some()
|| parsed.path() != "/"
|| parsed.query().is_some()
|| parsed.fragment().is_some()
{
return Err(ApiError::validation("allowed origin is invalid"));
}
Ok(parsed.origin().ascii_serialization())
}